spine-rigc 1.9.1 → 1.9.2
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/cli.ts +39 -17
- package/docs/AUTHORING.md +1 -1
- package/package.json +1 -1
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/index.ts +21 -2
- package/src/assertions/model/slider_composition.ts +191 -0
- package/src/assertions/model/stepped_poses.ts +89 -0
- package/src/bonedist.ts +18 -6
- package/src/core/additive.ts +468 -0
- package/src/core/constraints.ts +12 -2
- package/src/core/constraints_physics.ts +31 -8
- package/src/core/raw.ts +77 -16
- package/src/core/walk.ts +202 -0
- package/src/nonfinite.ts +54 -0
- package/src/render.ts +151 -86
- package/src/render_core.ts +5 -0
- package/src/validate.ts +432 -457
package/cli.ts
CHANGED
|
@@ -338,13 +338,32 @@ function parseArgs(
|
|
|
338
338
|
* unhandled `SyntaxError` with a stack trace instead of a usage error.
|
|
339
339
|
*/
|
|
340
340
|
function readJsonFile(path: string): unknown {
|
|
341
|
+
return parseJsonNamed(readFileSync(path, 'utf8'), path);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** `text`, read from `path`, parsed — and refused naming the file and where it broke when it is not JSON. */
|
|
345
|
+
function parseJsonNamed(text: string, path: string): unknown {
|
|
341
346
|
try {
|
|
342
|
-
return parseJsonWithPosition(
|
|
347
|
+
return parseJsonWithPosition(text);
|
|
343
348
|
} catch (err) {
|
|
344
349
|
throw new UsageError(`cannot read ${path}: ${(err as Error).message}`);
|
|
345
350
|
}
|
|
346
351
|
}
|
|
347
352
|
|
|
353
|
+
/**
|
|
354
|
+
* A skeleton a command was pointed at, as text — refused like every other JSON
|
|
355
|
+
* file on the command line (`parseJsonNamed`) when it is not JSON (issue
|
|
356
|
+
* #1042). `render`, `check`, `bench` and `bonedist` hand the text to a loader
|
|
357
|
+
* that parses it again, and on a file that is not JSON that parse surfaced as
|
|
358
|
+
* the runtime's `SyntaxError` and a stack, where `diff`, `preview`, `vote` and
|
|
359
|
+
* `ingest` already said `cannot read <path>`.
|
|
360
|
+
*/
|
|
361
|
+
function readSkeletonText(path: string): string {
|
|
362
|
+
const text = readFileSync(path, 'utf8');
|
|
363
|
+
parseJsonNamed(text, path);
|
|
364
|
+
return text;
|
|
365
|
+
}
|
|
366
|
+
|
|
348
367
|
/**
|
|
349
368
|
* Read a cuts.json and resolve its three paths against the file's own
|
|
350
369
|
* directory. Anchoring on the table rather than on the process cwd is what lets
|
|
@@ -1480,7 +1499,7 @@ function runCheck(
|
|
|
1480
1499
|
// paths are what `candidatePosers` looks beside for `skeleton.model.json`.
|
|
1481
1500
|
const poser = readPoserFlag(flags);
|
|
1482
1501
|
return checkAgainstFrames({
|
|
1483
|
-
skeletonText:
|
|
1502
|
+
skeletonText: readSkeletonText(skeletonPath),
|
|
1484
1503
|
atlasText,
|
|
1485
1504
|
atlasDir: dirname(atlasPath),
|
|
1486
1505
|
framesDir,
|
|
@@ -1805,7 +1824,7 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
1805
1824
|
// subset roster and its pages without loading spine-core at all, and with a
|
|
1806
1825
|
// rigc-compiled/2 document without its atlas either (issue #1020).
|
|
1807
1826
|
const { choice, facts, pages } = loadCandidate(
|
|
1808
|
-
{ skeletonText:
|
|
1827
|
+
{ skeletonText: readSkeletonText(skeletonPath), atlasText, atlasDir, label: skeletonPath },
|
|
1809
1828
|
{ skeleton: skeletonPath, atlas: atlasPath },
|
|
1810
1829
|
posersAsked(flags),
|
|
1811
1830
|
);
|
|
@@ -1941,12 +1960,7 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
1941
1960
|
|
|
1942
1961
|
/** The animation names an emitted skeleton carries, in the order it lists them. */
|
|
1943
1962
|
function skeletonAnimationNames(skeletonText: string, path: string): string[] {
|
|
1944
|
-
|
|
1945
|
-
try {
|
|
1946
|
-
parsed = parseJsonWithPosition(skeletonText);
|
|
1947
|
-
} catch (err) {
|
|
1948
|
-
throw new UsageError(`cannot read ${path}: ${(err as Error).message}`);
|
|
1949
|
-
}
|
|
1963
|
+
const parsed = parseJsonNamed(skeletonText, path);
|
|
1950
1964
|
if (typeof parsed !== 'object' || parsed === null) throw new UsageError(`${path} is not a skeleton object`);
|
|
1951
1965
|
const animations = (parsed as { animations?: unknown }).animations;
|
|
1952
1966
|
if (animations === undefined) return [];
|
|
@@ -2541,8 +2555,11 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
2541
2555
|
throw new UsageError(`no example corpus at ${exportDir} — ${remedy} (examples/ is gitignored, not shipped)`);
|
|
2542
2556
|
}
|
|
2543
2557
|
|
|
2544
|
-
|
|
2545
|
-
|
|
2558
|
+
// Both files there and the skeleton JSON before the first line is printed, refused by name as `render` refuses
|
|
2559
|
+
// them (issue #1042): a missing file and a skeleton that is not JSON surfaced as an ENOENT or a SyntaxError and a
|
|
2560
|
+
// stack, the second only after the validate block had printed.
|
|
2561
|
+
const { skeletonPath, atlasPath } = resolveViewable({ candidate: flags.candidate, ...(flags.atlas === undefined ? {} : { atlas: flags.atlas }) });
|
|
2562
|
+
const skeletonText = readSkeletonText(skeletonPath);
|
|
2546
2563
|
const atlasText = readFileSync(atlasPath, 'utf8');
|
|
2547
2564
|
|
|
2548
2565
|
console.log(`rigc bench rung ${rung.id} — ${rung.example}`);
|
|
@@ -2586,10 +2603,8 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
2586
2603
|
const boneDist = boneDistance({
|
|
2587
2604
|
candidateSkeleton: skeletonPath,
|
|
2588
2605
|
candidateAtlas: atlasPath,
|
|
2589
|
-
candidateAtlasDir: dirname(atlasPath),
|
|
2590
2606
|
referenceSkeleton: referencePath,
|
|
2591
2607
|
referenceAtlas: join(exportDir, skeleton.atlas),
|
|
2592
|
-
referenceAtlasDir: exportDir,
|
|
2593
2608
|
bones: flags.bones,
|
|
2594
2609
|
// Deliberately NOT `flags.fps`. Inside `bench` that flag already means
|
|
2595
2610
|
// "the rate this frame set was recorded at, for a set with no sidecar",
|
|
@@ -2781,15 +2796,20 @@ function cmdBoneDist(flags: Record<string, string>): void {
|
|
|
2781
2796
|
'names, so the mapping is an input and never a guess; pass `identity` to state that the two use the same names',
|
|
2782
2797
|
);
|
|
2783
2798
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2799
|
+
// Each side's files there and its skeleton JSON, refused by name as `render` refuses them (issue #1042); a pair
|
|
2800
|
+
// that does not load is `boneDistance`'s refusal (`CandidatePairError`, exit 2 below).
|
|
2801
|
+
const sideOf = (target: string, atlas: string | undefined): { skeletonPath: string; atlasPath: string } => {
|
|
2802
|
+
const side = resolveViewable({ candidate: target, ...(atlas === undefined ? {} : { atlas }) });
|
|
2803
|
+
readSkeletonText(side.skeletonPath);
|
|
2804
|
+
return side;
|
|
2805
|
+
};
|
|
2806
|
+
const candidate = sideOf(flags.candidate, flags.atlas);
|
|
2807
|
+
const reference = sideOf(flags.reference, flags['reference-atlas']);
|
|
2786
2808
|
const report = boneDistance({
|
|
2787
2809
|
candidateSkeleton: candidate.skeletonPath,
|
|
2788
2810
|
candidateAtlas: candidate.atlasPath,
|
|
2789
|
-
candidateAtlasDir: dirname(candidate.atlasPath),
|
|
2790
2811
|
referenceSkeleton: reference.skeletonPath,
|
|
2791
2812
|
referenceAtlas: reference.atlasPath,
|
|
2792
|
-
referenceAtlasDir: dirname(reference.atlasPath),
|
|
2793
2813
|
bones: flags.bones,
|
|
2794
2814
|
...(flags.fps === undefined ? {} : { fps: Number(flags.fps) }),
|
|
2795
2815
|
});
|
|
@@ -4388,6 +4408,8 @@ try {
|
|
|
4388
4408
|
// skeleton.json or an atlas from another build beside this one's model document. The same class as the
|
|
4389
4409
|
// missing atlas above: nothing was posed or written, and it is the directory the command was pointed at that
|
|
4390
4410
|
// has to change. The message names both files, the reason the runtime drew them and the runtime's own words.
|
|
4411
|
+
// Since issue #1042 also `bonedist` and `bench --bones` on either side, and a page a candidate is drawn from that
|
|
4412
|
+
// is not there, whichever poser draws it.
|
|
4391
4413
|
if (err instanceof CandidatePairError) {
|
|
4392
4414
|
console.error(`rigc ${command}: ${err.message}`);
|
|
4393
4415
|
process.exit(2);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -207,7 +207,7 @@ What the flags mean:
|
|
|
207
207
|
| `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
|
|
208
208
|
| `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
|
|
209
209
|
| `--profile` | `spine` = the 34 validity rules (**the default**) · `spine-html` = all 49, opt-in |
|
|
210
|
-
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/3` or `/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. A directory whose files are not one build — a `skeleton.json` or a `skeleton.atlas` from another build beside this one's `skeleton.model.json` — is drawn through spine-core where the runtime can load the pair, the `poser` line naming the two digests or the atlas's first difference; where it cannot, it is refused `<skeleton> does not load against <the atlas>: spine-core draws this pair (<the poser line's why>) and could not resolve it — "<the runtime's own words>". <dir> is not one build: …`, exit 2, nothing written (`RC18`) — another build's skeleton beside this build's atlas typically ends `"Region not found in atlas: <region> (attachment: <name>)"`. Rebuild, or put the build's own three files back beside each other. A Spine export beside an atlas it was not exported with is refused in the same words, ending `The skeleton and the atlas are not one pair`. An atlas copied from another build's directory usually stops earlier, at its pages — their paths are relative to the directory it was written in — and is refused naming the first one: `nothing at <the page>: <the atlas> names it as a page, and spine-core draws this pair through that atlas (<why>). … <dir> is not one build: …`, exit 2, nothing written (`RC18`); an export whose atlas names a page that is not there is refused the same way, ending `and the page has to be there`. With the atlas gone, `check` on a `rigc-compiled/3` build reads its pages' `scale:` lines off the document, so its texture note and `check.json`'s `textureFrom.candidateScales` are what they are with the atlas; on a `/2` build its texture note says whether the atlas declares a `scale:` line is not read (that document does not state one), and `candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
210
|
+
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/3` or `/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. A directory whose files are not one build — a `skeleton.json` or a `skeleton.atlas` from another build beside this one's `skeleton.model.json` — is drawn through spine-core where the runtime can load the pair, the `poser` line naming the two digests or the atlas's first difference; where it cannot, it is refused `<skeleton> does not load against <the atlas>: spine-core draws this pair (<the poser line's why>) and could not resolve it — "<the runtime's own words>". <dir> is not one build: …`, exit 2, nothing written (`RC18`) — another build's skeleton beside this build's atlas typically ends `"Region not found in atlas: <region> (attachment: <name>)"`. Rebuild, or put the build's own three files back beside each other. A Spine export beside an atlas it was not exported with is refused in the same words, ending `The skeleton and the atlas are not one pair`. An atlas copied from another build's directory usually stops earlier, at its pages — their paths are relative to the directory it was written in — and is refused naming the first one: `nothing at <the page>: <the atlas> names it as a page, and spine-core draws this pair through that atlas (<why>). … <dir> is not one build: …`, exit 2, nothing written (`RC18`); an export whose atlas names a page that is not there is refused the same way, ending `and the page has to be there`. A build moved or copied whole away from the directory it was built in stops at the same place, on either poser, and is refused naming the page and the file that names it: `nothing at <the page>: <the skeleton.model.json> names it as a page, and the core draws this build from it. A page path is relative to the build's directory, so a build moved or copied away from the directory it was built in names pages that are not here — build it again where it is, or build it with --copy-images, which writes its pages beside it` (under `--poser spine`, `<the atlas> names it as a page, and spine-core draws this pair through that atlas (--poser spine)` with the same ending), exit 2, nothing written (`RC21`); only where the core refused the files does a page refusal say the directory is not one build. `bench --bones` and `rigc bonedist` — on its `--candidate` and on its `--reference` — refuse a pair the runtime cannot load in the words above with `spine-core poses this pair (bonedist's candidate: …)` or `(bonedist's reference: …)`, exit 2, nothing written (`RC20`); they read no page image, so a build whose pages are elsewhere is measured as it is in place. A `skeleton.json` that is not JSON is refused `rigc: cannot read <the file>: <the parser's words> (line N, column N)`, exit 2, by `render`, `check`, `bench` and `bonedist` as by every other command that reads one (`RC22`). With the atlas gone, `check` on a `rigc-compiled/3` build reads its pages' `scale:` lines off the document, so its texture note and `check.json`'s `textureFrom.candidateScales` are what they are with the atlas; on a `/2` build its texture note says whether the atlas declares a `scale:` line is not read (that document does not state one), and `candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
211
211
|
| `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview` (each candidate's own first, with several), and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote` and a several-candidate `preview`, so is a name that only *some* candidates have, naming the one that lacks it |
|
|
212
212
|
| `--geometry` | `render` only: also write a `geometry.json` into each frame directory — per frame, every bone's world transform and every slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry and topology, on the frames' own grid and viewport. Every other file is byte for byte what the render writes without it. Refused with `--slot`/`--hide`, because the geometry is the whole pose whatever is drawn — **§8.2** |
|
|
213
213
|
| `--poser` | `render`, `check` and `explain`: `core` or `spine` — on `render` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core. Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `the "default" skin names` (no `--skin`, and the default skin names a skin-required bone or constraint, each named) · `and no "default" one` (no `--skin`, and skins with none named `default` — spine-core draws nothing there either, so the render stops at `there is nothing to draw`, exit 2, and only `--poser core` prints this refusal) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. A fallback whose pair the runtime cannot load — another build's `skeleton.json` or atlas beside this build's document, or an export beside an atlas it was not exported with — is not drawn: it is refused `… does not load against <the atlas>: spine-core draws this pair (<why>) and could not resolve it — "<the runtime's own words>"` — or, where the atlas names a page that is not there, `nothing at <the page>: …` — exit 2, nothing written, on `render`, `render --geometry`, `check` and `bench --frames` alike; under `--poser core` the same pair is that flag's refusal, in the words above (`RC18`; the `--candidate` row says what to change). The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; so do the order its animations and skins are listed in, its stage and its pages' `scale:` lines, on a `rigc-compiled/3` document (`editorOrder`, `stage`, `pages`) — a `/2` or `/1` document states none of those, so they come off its own `skeleton.json` and atlas, and the `poser` line then says `a rigc-compiled/2 document, which does not state the order its skins and animations are listed in or its stage, read from` and names both files; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`). On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, round-trips it through spine-core and named assertions, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A10, the body (issue #1025, cut 4c-5a of step 4c of #380): no pose the
|
|
3
|
+
* skeleton is stepped through holds a number that is not finite, or a bone
|
|
4
|
+
* with no inheritance mode.
|
|
5
|
+
*
|
|
6
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads, which is
|
|
7
|
+
* `BoneTimelineFacts` (`../facts/bone_timelines.ts`) for the `inherit` keys
|
|
8
|
+
* where it read the skeleton JSON, and `SteppedPoseFacts`
|
|
9
|
+
* (`../facts/stepped_poses.ts`) where it posed a spine-core skeleton itself:
|
|
10
|
+
* the setup pose, each animation's looping walk, and a bone's mode posed at a
|
|
11
|
+
* key — which `validate()` takes from spine-core and the model side from
|
|
12
|
+
* rigc's core. The scan of a pose is `firstNonFinite` (`../../nonfinite.ts`),
|
|
13
|
+
* the sentence `render` refuses on, called rather than restated. The argument
|
|
14
|
+
* for each clause is above the `check` call, which stays in `validate()`.
|
|
15
|
+
*/
|
|
16
|
+
import type { Verdicts } from '../harness.ts';
|
|
17
|
+
import type { BoneTimelineFacts } from '../facts/bone_timelines.ts';
|
|
18
|
+
import type { SteppedFrame, SteppedPoseFacts } from '../facts/stepped_poses.ts';
|
|
19
|
+
import { SKIP_NO_POSE } from '../reasons.ts';
|
|
20
|
+
import { isObj } from '../values.ts';
|
|
21
|
+
import { firstNonFinite } from '../../nonfinite.ts';
|
|
22
|
+
import { BONE_INHERIT_KNOWN } from '../../rig.ts';
|
|
23
|
+
|
|
24
|
+
/** The steps A10 takes through every animation: the walk is `max(duration, 1) / STEP_FRAMES` long per step. */
|
|
25
|
+
export const STEP_FRAMES = 120;
|
|
26
|
+
|
|
27
|
+
export function a10NoNanAfterStepping({ fail, skip, stats }: Verdicts, boneTimelines: BoneTimelineFacts, facts: SteppedPoseFacts): void {
|
|
28
|
+
if (facts.steppedAnimations.length === 0 && facts.boneCount === 0) {
|
|
29
|
+
skip('A10_NO_NAN_AFTER_STEPPING', SKIP_NO_POSE);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// -- the bone's inheritance mode, posed at every `inherit` key
|
|
34
|
+
let unresolved = 0;
|
|
35
|
+
for (const { animation: animName, bone: boneName, timelines } of boneTimelines.boneTimelines) {
|
|
36
|
+
if (!isObj(timelines) || !Array.isArray(timelines.inherit)) continue;
|
|
37
|
+
if (!facts.hasAnimation(animName) || !facts.hasBone(boneName)) continue;
|
|
38
|
+
for (const rawKey of timelines.inherit as unknown[]) {
|
|
39
|
+
if (!isObj(rawKey)) continue;
|
|
40
|
+
const time = typeof rawKey.time === 'number' ? rawKey.time : 0;
|
|
41
|
+
const posed = facts.posedInherit(animName, boneName, time);
|
|
42
|
+
if (posed === null) continue;
|
|
43
|
+
unresolved++;
|
|
44
|
+
fail(
|
|
45
|
+
'A10_NO_NAN_AFTER_STEPPING',
|
|
46
|
+
`animation "${animName}" bone "${boneName}" inherit (t=${time}): the bone poses inheritance mode ` +
|
|
47
|
+
`${String(posed)} — the key spells ${JSON.stringify(rawKey.inherit)}, which the runtime's mode lookup ` +
|
|
48
|
+
`does not resolve (${BONE_INHERIT_KNOWN}), so no mode applies until the next key and the bone keeps ` +
|
|
49
|
+
'the world rotation, scale and shear it had',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (unresolved > 0) return;
|
|
54
|
+
|
|
55
|
+
// -- the whole world transform, at the setup pose and every stepped frame
|
|
56
|
+
if (facts.steppedAnimations.length === 0) stats.nanStepping = 'skipped';
|
|
57
|
+
const atRest = facts.setup();
|
|
58
|
+
const atSetup = firstNonFinite('the setup pose', atRest.drawn, atRest.bones);
|
|
59
|
+
if (atSetup !== null) {
|
|
60
|
+
fail('A10_NO_NAN_AFTER_STEPPING', atSetup);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What a posed frame shows that the world-transform scan does not read: a
|
|
66
|
+
* bone posing no inheritance mode, and a slot colour that is not finite.
|
|
67
|
+
* `where` leads the sentence — the animation's name at a stepped frame,
|
|
68
|
+
* as it always has, or `the setup pose` on a skeleton with no animation.
|
|
69
|
+
*/
|
|
70
|
+
const poseDefect = (where: string, frame: SteppedFrame): string | null => {
|
|
71
|
+
for (const bone of frame.bones) {
|
|
72
|
+
if (bone.inherit !== null) {
|
|
73
|
+
return (
|
|
74
|
+
`${where}: bone "${bone.name}" poses inheritance mode ${bone.inherit} — its setup ` +
|
|
75
|
+
`spells inherit ${facts.statedInherit(bone.name)}, which the ` +
|
|
76
|
+
`runtime's mode lookup does not resolve (${BONE_INHERIT_KNOWN}), so its world rotation, scale and shear ` +
|
|
77
|
+
'are never computed — they stay 0 and everything the bone carries collapses to a point'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const slot of frame.slots) {
|
|
82
|
+
if (!slot.colour.every(Number.isFinite)) return `${where}: slot "${slot.name}" colour is non-finite`;
|
|
83
|
+
if (slot.dark !== null && !slot.dark.every(Number.isFinite)) {
|
|
84
|
+
return `${where}: slot "${slot.name}" dark colour is non-finite`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (facts.steppedAnimations.length === 0) {
|
|
91
|
+
const atRestDefect = poseDefect('the setup pose', atRest);
|
|
92
|
+
if (atRestDefect !== null) fail('A10_NO_NAN_AFTER_STEPPING', atRestDefect);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const anim of facts.steppedAnimations) {
|
|
96
|
+
const step = Math.max(anim.duration, 1) / STEP_FRAMES;
|
|
97
|
+
const frames = facts.walk(anim.name, step, STEP_FRAMES);
|
|
98
|
+
for (let i = 0; i < STEP_FRAMES; i++) {
|
|
99
|
+
const frame = frames[i];
|
|
100
|
+
const found = firstNonFinite(
|
|
101
|
+
`animation ${JSON.stringify(anim.name)} frame ${i + 1} of ${STEP_FRAMES} (t=${((i + 1) * step).toFixed(4)}s)`,
|
|
102
|
+
frame.drawn,
|
|
103
|
+
frame.bones,
|
|
104
|
+
);
|
|
105
|
+
if (found !== null) {
|
|
106
|
+
fail('A10_NO_NAN_AFTER_STEPPING', found);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const defect = poseDefect(anim.name, frame);
|
|
110
|
+
if (defect !== null) {
|
|
111
|
+
fail('A10_NO_NAN_AFTER_STEPPING', defect);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A34, the body (issue #1025, cut 4c-5 of step 4c of #380): a constraint
|
|
3
|
+
* timeline aims at a constraint of that type.
|
|
4
|
+
*
|
|
5
|
+
* The argument — the two shapes, the lookup by name AND type, the empty key
|
|
6
|
+
* array the parser skips in silence, the physics group that names no
|
|
7
|
+
* constraint — stands above the `check` call in `validate()`.
|
|
8
|
+
*
|
|
9
|
+
* Moved out of `src/validate.ts` whole, every clause with it (cut 4c-4
|
|
10
|
+
* deferred it whole because its one rig clause shares this walk with the
|
|
11
|
+
* physics branch): which constraint an entry names, what it holds and whom a
|
|
12
|
+
* physics timeline naming none reaches are the rig's, and a document's records
|
|
13
|
+
* state each — except what no readable document can carry, which only the
|
|
14
|
+
* Spine text holds (a name the skeleton lacks, a name of another type, an
|
|
15
|
+
* empty key array, a bare key array where named timelines go): the model
|
|
16
|
+
* side's walk never meets those, and the reader refuses the document that
|
|
17
|
+
* would state them.
|
|
18
|
+
*/
|
|
19
|
+
import type { Verdicts } from '../harness.ts';
|
|
20
|
+
import type { ConstraintTargetFacts, TargetKeyArray } from '../facts/constraint_targets.ts';
|
|
21
|
+
import { CHANNELS_BY_KIND } from '../../timelines.ts';
|
|
22
|
+
|
|
23
|
+
export function a34ConstraintTimelineTargets({ fail, skip }: Verdicts, facts: ConstraintTargetFacts): void {
|
|
24
|
+
if (facts.groupsByAnimation === null) return skip('A34_CONSTRAINT_TIMELINE_TARGETS', 'the skeleton declares no animations');
|
|
25
|
+
// name -> the KINDS declared under it, because that is the namespace the
|
|
26
|
+
// lookup this assertion is about resolves in: `findConstraint(name, type)`
|
|
27
|
+
// tests the type first, so a skeleton may carry `leg` as an ik constraint
|
|
28
|
+
// AND as a transform one and each group finds its own (issue #692). A map
|
|
29
|
+
// keyed by the name alone let the second of a pair overwrite the first, and
|
|
30
|
+
// this assertion then reported a correct file as a type mismatch.
|
|
31
|
+
const kindsOf = new Map<string, string[]>();
|
|
32
|
+
for (const entry of facts.constraints) {
|
|
33
|
+
if (entry.name !== null) kindsOf.set(entry.name, [...(kindsOf.get(entry.name) ?? []), entry.type]);
|
|
34
|
+
}
|
|
35
|
+
/** The file's physics constraints, which the unnamed physics group is read against. */
|
|
36
|
+
const rawPhysics = facts.constraints.filter((entry) => entry.type === 'physics');
|
|
37
|
+
let sawATimeline = false;
|
|
38
|
+
/** One target of one group: the name resolves, the type matches, keys exist. */
|
|
39
|
+
const checkTarget = (at: string, group: string, name: string, keyArrays: readonly TargetKeyArray[]): void => {
|
|
40
|
+
sawATimeline = true;
|
|
41
|
+
const declared = kindsOf.get(name) ?? [];
|
|
42
|
+
if (declared.length === 0) {
|
|
43
|
+
const known = [...kindsOf.entries()].filter(([, kinds]) => kinds.includes(group)).map(([n]) => n);
|
|
44
|
+
fail(
|
|
45
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
46
|
+
`${at}: the skeleton's constraints array has no "${name}"` +
|
|
47
|
+
(known.length ? ` (${group} constraints: ${known.join(', ')})` : `, and no ${group} constraint at all`),
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!declared.includes(group)) {
|
|
52
|
+
fail(
|
|
53
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
54
|
+
`${at}: "${name}" is declared as a "${declared.join('"/"')}" constraint, so the ${group} lookup misses it and the loader throws`,
|
|
55
|
+
);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
checkKeys(at, keyArrays);
|
|
59
|
+
};
|
|
60
|
+
/** The key arrays of one group entry that resolved: each one is walked, so each has to hold a key. */
|
|
61
|
+
const checkKeys = (at: string, keyArrays: readonly TargetKeyArray[]): void => {
|
|
62
|
+
if (keyArrays.length === 0) {
|
|
63
|
+
fail(
|
|
64
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
65
|
+
`${at}: the constraint is named and carries no timeline at all; the group is walked and nothing happens`,
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const { timeline, keys } of keyArrays) {
|
|
70
|
+
if ('count' in keys && keys.count > 0) continue;
|
|
71
|
+
fail(
|
|
72
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
73
|
+
`${at}${timeline ? ` timeline "${timeline}"` : ''}: the key array is ` +
|
|
74
|
+
`${'count' in keys ? 'empty' : keys.spelled}; the parser reads key 0, finds nothing and ` +
|
|
75
|
+
'skips the whole timeline without a word',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
for (const { name: animName, groups } of facts.groupsByAnimation) {
|
|
80
|
+
for (const { group, entries } of groups) {
|
|
81
|
+
for (const { name, bare, keyArrays } of entries) {
|
|
82
|
+
// group.<constraint> = keys[]
|
|
83
|
+
if (group === 'ik' || group === 'transform') {
|
|
84
|
+
checkTarget(`animation "${animName}" ${group} timeline "${name}"`, group, name, keyArrays);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// group.<constraint>.<timeline> = keys[]
|
|
88
|
+
const at = `animation "${animName}" ${group} constraint "${name}"`;
|
|
89
|
+
if (bare !== null) {
|
|
90
|
+
sawATimeline = true;
|
|
91
|
+
fail(
|
|
92
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
93
|
+
`${at}: this group maps a constraint to NAMED timelines ` +
|
|
94
|
+
`(${Object.keys(CHANNELS_BY_KIND[group]).join('/')}), and this one holds ${bare} — ` +
|
|
95
|
+
'a bare key array here is the ik/transform shape and is walked as an object',
|
|
96
|
+
);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// The empty name is not a miss: it is the physics group's global form,
|
|
100
|
+
// which `SkeletonJson` loads as `constraintIndex -1` rather than looking
|
|
101
|
+
// anything up, and which writes every physics constraint that declares
|
|
102
|
+
// the timeline's property global (issue #726). Refusing it as "no
|
|
103
|
+
// constraint called ''" was a sentence that is false about the file.
|
|
104
|
+
// What CAN be wrong with it is that it reaches nobody: the parser
|
|
105
|
+
// accepts it, the runtime walks every constraint, and none of them
|
|
106
|
+
// takes the key.
|
|
107
|
+
if (group === 'physics' && name === '') {
|
|
108
|
+
sawATimeline = true;
|
|
109
|
+
for (const { timeline: timelineName } of keyArrays) {
|
|
110
|
+
const reach = facts.reach(timelineName);
|
|
111
|
+
// A name the parser skips is skipped here too: no timeline exists
|
|
112
|
+
// to reach anybody, and whether the NAME is right is not a target
|
|
113
|
+
// question.
|
|
114
|
+
if (reach === null || reach.reached.length > 0) continue;
|
|
115
|
+
fail(
|
|
116
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
117
|
+
`${at} timeline "${timelineName}": a physics group that names no constraint writes every physics ` +
|
|
118
|
+
`constraint ${reach.resets ? 'the skeleton has' : `declaring "${timelineName}Global": true`}, and ` +
|
|
119
|
+
(rawPhysics.length === 0 ? 'the skeleton has no physics constraint' : `none of ${rawPhysics.map((one) => `"${one.spelled}"`).join(', ')} does`) +
|
|
120
|
+
' — the parser loads it, the runtime walks every constraint, and no constraint takes the key',
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
checkKeys(at, keyArrays);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
checkTarget(at, group, name, keyArrays);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!sawATimeline) {
|
|
131
|
+
return skip('A34_CONSTRAINT_TIMELINE_TARGETS', 'no animation carries a constraint timeline');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A40, the body (issue #1025, cut 4c-5 of step 4c of #380): two sliders on
|
|
3
|
+
* one property, and the later one erases the other.
|
|
4
|
+
*
|
|
5
|
+
* The argument — why this is `validity`, the three shapes excluded
|
|
6
|
+
* structurally (authority below 1, a skin switch, different properties), the
|
|
7
|
+
* second clause and the events clause removed — stands above the `check` call
|
|
8
|
+
* in `validate()`. What a timeline does with `add` is the one fact here that
|
|
9
|
+
* is behaviour rather than a record: it is posed, and the body asks for it
|
|
10
|
+
* (`behaviour`) only for the later timeline of a shared property, once per
|
|
11
|
+
* timeline.
|
|
12
|
+
*
|
|
13
|
+
* Moved out of `src/validate.ts` whole: the sliders, their setup mix, their
|
|
14
|
+
* `additive` flag, the skins that list them, the animations they apply and the
|
|
15
|
+
* properties those animations' timelines register are the rig's, and a
|
|
16
|
+
* document's records state each; the class a timeline's additive application
|
|
17
|
+
* falls in is the core's computation over the document
|
|
18
|
+
* (`src/core/additive.ts`), held to the runtime's probe by the core suite's
|
|
19
|
+
* `CO27`. The words the sentences print are the facts' own: the runtime class
|
|
20
|
+
* a timeline loads as, the property's name, the target as the sentence names
|
|
21
|
+
* it.
|
|
22
|
+
*/
|
|
23
|
+
import type { Verdicts } from '../harness.ts';
|
|
24
|
+
import type { ConstraintFacts } from '../facts/constraints.ts';
|
|
25
|
+
import type { AddBehaviour, SliderCompositionFacts, SliderFact, SliderTimelineFact } from '../facts/slider_composition.ts';
|
|
26
|
+
import { switchedOn } from '../constraint_words.ts';
|
|
27
|
+
|
|
28
|
+
export function a40SlidersComposeOnASharedTarget({ fail, skip, stats }: Verdicts, facts: SliderCompositionFacts, constraints: ConstraintFacts): void {
|
|
29
|
+
const sliders = facts.sliders;
|
|
30
|
+
if (sliders.length < 2) {
|
|
31
|
+
return skip(
|
|
32
|
+
'A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET',
|
|
33
|
+
`the skeleton declares ${sliders.length} slider constraint${sliders.length === 1 ? '' : 's'}, and one slider has nothing to compose with`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
// Clause 1 of the "could this be correct?" list above the check.
|
|
37
|
+
// "Keyed at all" rather than "keyed live", and on purpose: this clause asks
|
|
38
|
+
// whether the mix can MOVE from its setup value, so any mix timeline
|
|
39
|
+
// disqualifies it — the question `keyedBy` answered here, through the one
|
|
40
|
+
// reading `switchedOn` gives (`../constraint_words.ts`, over the same facts
|
|
41
|
+
// the moved constraint bodies read — issue #1025), and unchanged by issue
|
|
42
|
+
// #752.
|
|
43
|
+
const mixKeyed = switchedOn(constraints, (timeline) => timeline.kind === 'slider' && timeline.word === 'mix', 1, () => true);
|
|
44
|
+
const authoritative = sliders.filter((s) => s.mix >= 1 && !mixKeyed.has(s.index));
|
|
45
|
+
if (authoritative.length < 2) {
|
|
46
|
+
return skip(
|
|
47
|
+
'A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET',
|
|
48
|
+
`${authoritative.length} of the ${sliders.length} slider constraints apply at full authority; below mix 1 an ` +
|
|
49
|
+
'apply is a lerp from the current pose rather than an overwrite, so what the others do to a shared property is a weighting',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
/** Which skins switch a slider on, or null when it is active under every skin. */
|
|
53
|
+
const skinsOf = new Map<SliderFact, Set<string> | null>();
|
|
54
|
+
for (const slider of authoritative) skinsOf.set(slider, slider.skinRequired ? new Set(slider.skins) : null);
|
|
55
|
+
/** Clause 2: can these two ever run in the same frame? */
|
|
56
|
+
const canOverlap = (a: SliderFact, b: SliderFact): boolean => {
|
|
57
|
+
const skinsA = skinsOf.get(a) ?? null;
|
|
58
|
+
const skinsB = skinsOf.get(b) ?? null;
|
|
59
|
+
if (!skinsA || !skinsB) return true;
|
|
60
|
+
for (const name of skinsA) {
|
|
61
|
+
if (skinsB.has(name)) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
type User = { slider: SliderFact; timeline: SliderTimelineFact; animation: string; at: number; property: string; names: string };
|
|
66
|
+
/** property id -> the sliders whose animation keys it, in constraints-array order. */
|
|
67
|
+
const byProperty = new Map<string, User[]>();
|
|
68
|
+
for (const slider of authoritative) {
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
const animation = slider.animation;
|
|
71
|
+
if (animation === null) continue;
|
|
72
|
+
animation.timelines.forEach((timeline, at) => {
|
|
73
|
+
for (const { id, property, names } of timeline.properties) {
|
|
74
|
+
if (seen.has(id)) continue;
|
|
75
|
+
seen.add(id);
|
|
76
|
+
byProperty.set(id, [...(byProperty.get(id) ?? []), { slider, timeline, animation: animation.name, at, property, names }]);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Posed once per timeline, because the answer is the class's and the rigs that reach here share timelines. */
|
|
81
|
+
const behaviour = new Map<string, AddBehaviour>();
|
|
82
|
+
const addBehaviourOf = (user: User): AddBehaviour => {
|
|
83
|
+
const key = `${user.animation}\u0000${user.at}`;
|
|
84
|
+
const known = behaviour.get(key);
|
|
85
|
+
if (known !== undefined) return known;
|
|
86
|
+
const measured = facts.behaviour(user.animation, user.at);
|
|
87
|
+
behaviour.set(key, measured);
|
|
88
|
+
return measured;
|
|
89
|
+
};
|
|
90
|
+
let shared = 0;
|
|
91
|
+
for (const [, users] of byProperty) {
|
|
92
|
+
if (users.length < 2) continue;
|
|
93
|
+
shared++;
|
|
94
|
+
const at = (slider: SliderFact): string => `"${slider.name}" (constraints[${slider.index}], additive: ${String(slider.additive)})`;
|
|
95
|
+
const chain = users.map((u) => at(u.slider)).join(', ');
|
|
96
|
+
for (let j = 1; j < users.length; j++) {
|
|
97
|
+
const later = users[j];
|
|
98
|
+
const composes = addBehaviourOf(later);
|
|
99
|
+
if (composes === 'accumulates' && later.slider.additive) continue;
|
|
100
|
+
// Nothing a second slider could take away: applied with the arguments
|
|
101
|
+
// a slider passes — `firedEvents` null among them — this timeline
|
|
102
|
+
// moves no pose at all.
|
|
103
|
+
if (composes === 'inert') continue;
|
|
104
|
+
const erased = users.slice(0, j).filter((e) => canOverlap(e.slider, later.slider));
|
|
105
|
+
if (!erased.length) continue;
|
|
106
|
+
const erasedNames = `${erased.map((e) => `"${e.slider.name}"`).join(', ')} contribute${erased.length === 1 ? 's' : ''}`;
|
|
107
|
+
const why =
|
|
108
|
+
composes === 'accumulates'
|
|
109
|
+
? `slider "${later.slider.name}" applies animation "${later.slider.animation?.name}" with additive false, and at ` +
|
|
110
|
+
'mix 1 a non-additive apply writes the value outright (`getRelativeValue` returns `setup + value`, ' +
|
|
111
|
+
'`getAbsoluteValue` returns `value`) rather than adding to the pose it found. Set `"additive": true` on ' +
|
|
112
|
+
`slider "${later.slider.name}" in the rig spec — rigc will not choose that flag for you — or key this ` +
|
|
113
|
+
`property from one slider only. [measured] \`${later.timeline.runtimeClass}.apply\` posed twice with ` +
|
|
114
|
+
'`add` set accumulates, so that flag is the repair here'
|
|
115
|
+
: `the ${later.property} timeline they share writes its value outright whatever the ` +
|
|
116
|
+
`flags say — [measured] \`${later.timeline.runtimeClass}.apply\` posed twice with \`add\` set left the ` +
|
|
117
|
+
'same value there rather than adding to it — so `"additive": true` would NOT compose these. Key this ' +
|
|
118
|
+
'property from one slider only, or move both edits into the one animation a single slider applies';
|
|
119
|
+
fail(
|
|
120
|
+
'A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET',
|
|
121
|
+
`${later.names} is keyed by the animations of ${users.length} sliders — ${chain} — and every ` +
|
|
122
|
+
`one of them applies at mix 1. Today ${at(later.slider)} wins that property and ${erasedNames} ` +
|
|
123
|
+
`nothing to it: ${why}.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
stats.sliderSharedTargets = shared;
|
|
128
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The constraint timelines of every animation, as A34 walks them (issue
|
|
3
|
+
* #1025, cut 4c-5 of step 4c of #380): the constraints the skeleton declares,
|
|
4
|
+
* and each animation's `ik`, `transform`, `path`, `physics` and `slider`
|
|
5
|
+
* groups as the Spine file states them — the constraint each entry names and
|
|
6
|
+
* the key arrays under it.
|
|
7
|
+
*
|
|
8
|
+
* ⭐ **The order is the file's.** The animations in the order the file keys
|
|
9
|
+
* them, in each the five groups in that fixed order, each group's entries in
|
|
10
|
+
* the order the file keys them, each entry's timelines likewise: A34 prints
|
|
11
|
+
* one line per finding, so the walk is a fact both suppliers state.
|
|
12
|
+
*
|
|
13
|
+
* 🔸 **Which constraints a physics timeline naming none reaches is a
|
|
14
|
+
* question, not a value** — the runtime answers it by building the timeline
|
|
15
|
+
* its parser builds for the name and asking each constraint's data whether it
|
|
16
|
+
* takes the key (`unnamedPhysicsReach`), the model side by the core's own
|
|
17
|
+
* reading (`posedPhysics`, through `unnamedReach`). A body asks for it
|
|
18
|
+
* (`reach`), and the selftest puts every question to both (`VF14`).
|
|
19
|
+
*
|
|
20
|
+
* Links nothing from the runtime.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** One key array under an entry: the timeline's name (`''` for the `ik`/`transform` shape) and what it holds — how many keys, or the spelling of a value that is not a list. */
|
|
24
|
+
export interface TargetKeyArray {
|
|
25
|
+
readonly timeline: string;
|
|
26
|
+
readonly keys: { readonly count: number } | { readonly spelled: string };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One entry of a group: the constraint name the file keys it by, and either the spelling of a value that is not an object where named timelines go (`bare`) or its key arrays. */
|
|
30
|
+
export interface TargetEntry {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly bare: string | null;
|
|
33
|
+
readonly keyArrays: readonly TargetKeyArray[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One animation: its name and the groups it states, in the walk's order. */
|
|
37
|
+
export interface TargetAnimation {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly groups: ReadonlyArray<{ readonly group: 'ik' | 'transform' | 'path' | 'physics' | 'slider'; readonly entries: readonly TargetEntry[] }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What a physics timeline naming no constraint reaches: whether it is a `reset`, and the constraints that take its key. */
|
|
43
|
+
export interface UnnamedReach {
|
|
44
|
+
readonly resets: boolean;
|
|
45
|
+
readonly reached: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** What A34 reads. */
|
|
49
|
+
export interface ConstraintTargetFacts {
|
|
50
|
+
/** Every entry of the skeleton's `constraints` array that is an object: its name (null when it states none as a string), its type as spelled, and its name as the sentence spells it. */
|
|
51
|
+
readonly constraints: ReadonlyArray<{ readonly name: string | null; readonly type: string; readonly spelled: string }>;
|
|
52
|
+
/** Every animation, or `null` when the skeleton declares none (its `animations` is not an object). */
|
|
53
|
+
readonly groupsByAnimation: readonly TargetAnimation[] | null;
|
|
54
|
+
/** What a physics timeline named `timeline` that names no constraint reaches — or `null` for a name the parser builds no timeline for. */
|
|
55
|
+
reach(timeline: string): UnnamedReach | null;
|
|
56
|
+
}
|