spine-rigc 1.9.1 → 1.9.3
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 +28 -4375
- package/docs/AUTHORING.md +2 -2
- package/package.json +1 -1
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a13.ts +3 -3
- package/src/assertions/bodies/a14.ts +3 -3
- package/src/assertions/bodies/a15.ts +5 -5
- package/src/assertions/bodies/a22.ts +3 -3
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/facts/animated_bones.ts +4 -2
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/mesh_attachments.ts +33 -4
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/model/animated_bones.ts +13 -5
- package/src/assertions/model/bone_timelines.ts +5 -3
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/declared.ts +127 -0
- package/src/assertions/model/index.ts +81 -39
- package/src/assertions/model/mesh_attachments.ts +31 -2
- package/src/assertions/model/parse.ts +9 -1
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/stepped_poses.ts +90 -0
- package/src/bonedist.ts +22 -11
- package/src/check.ts +3 -2
- package/src/checkpics.ts +1 -1
- package/src/cli/core_commands.ts +1532 -0
- package/src/cli/shared.ts +1935 -0
- package/src/cli/spine_commands.ts +1141 -0
- package/src/core/additive.ts +458 -0
- package/src/core/constraints.ts +38 -5
- package/src/core/constraints_physics.ts +42 -9
- package/src/core/constraints_slider.ts +136 -6
- package/src/core/index.ts +27 -3
- package/src/core/raw.ts +77 -16
- package/src/core/skins.ts +28 -3
- package/src/core/walk.ts +175 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformmeasure.ts +20 -48
- package/src/deformreport.ts +2 -2
- package/src/framing.ts +1 -1
- package/src/nonfinite.ts +54 -0
- package/src/pose.ts +1 -1
- package/src/preview.ts +1 -1
- package/src/region_joins.ts +54 -0
- package/src/render.ts +243 -2808
- package/src/render_core.ts +19 -31
- package/src/render_shared.ts +2887 -0
- package/src/slots.ts +1 -1
- package/src/spine_side.ts +123 -0
- package/src/validate.ts +443 -548
- package/src/assertions/facts/skin_meshes.ts +0 -49
- package/src/assertions/model/skin_meshes.ts +0 -90
package/docs/AUTHORING.md
CHANGED
|
@@ -207,10 +207,10 @@ 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
|
-
| `--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) · `
|
|
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, and so is a build rendered with no `--skin` whose default skin names a skin-required bone or constraint, or whose skins name no `default` one — posed as a fresh skeleton is, no skin's bones or constraints applied (the default skin's included) and every slot resolved through the default skin alone, so the second draws nothing and stops at `there is nothing to draw` (exit 2) under either poser (issue #1051). 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) · `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) |
|
|
214
214
|
| `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
|
|
215
215
|
| `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
|
|
216
216
|
| `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
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
|
+
}
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
* triangles stay within the budget the rig declares.
|
|
4
4
|
*
|
|
5
5
|
* Moved out of `src/validate.ts` unchanged but for what it reads: the meshes
|
|
6
|
-
* are a fact (`../facts/
|
|
6
|
+
* are a fact (`../facts/mesh_attachments.ts`) rather than the lists `validate()`'s
|
|
7
7
|
* prelude filled from spine-core's loaded skins, and the rig info is the
|
|
8
8
|
* caller's. Why the two budgets come from the rig and never from here stays
|
|
9
9
|
* above the `check` call in `validate()`.
|
|
10
10
|
*/
|
|
11
11
|
import type { Verdicts } from '../harness.ts';
|
|
12
|
-
import type {
|
|
12
|
+
import type { MeshFacts } from '../facts/mesh_attachments.ts';
|
|
13
13
|
import { SKIP_NO_MESH_ATTACHMENT } from '../reasons.ts';
|
|
14
14
|
import type { RigInfo } from '../../types.ts';
|
|
15
15
|
|
|
16
|
-
export function a13MeshBudget({ fail, skip }: Verdicts, { meshes }:
|
|
16
|
+
export function a13MeshBudget({ fail, skip }: Verdicts, { meshes }: Pick<MeshFacts, 'meshes'>, input: { rig?: RigInfo }): void {
|
|
17
17
|
const meshAttachments = meshes;
|
|
18
18
|
const meshSlots = new Set(meshes.map((mesh) => mesh.slot));
|
|
19
19
|
const slotBudget = input.rig?.meshSlotBudget ?? null;
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
* stage.
|
|
4
4
|
*
|
|
5
5
|
* Moved out of `src/validate.ts` unchanged but for what it reads: the meshes
|
|
6
|
-
* and the stage are facts (`../facts/
|
|
6
|
+
* and the stage are facts (`../facts/mesh_attachments.ts`, `../facts/stage.ts`)
|
|
7
7
|
* rather than spine-core's loaded skins and skeleton header. The `check` call
|
|
8
8
|
* stays in `validate()`.
|
|
9
9
|
*/
|
|
10
10
|
import type { Verdicts } from '../harness.ts';
|
|
11
|
-
import type {
|
|
11
|
+
import type { MeshFacts } from '../facts/mesh_attachments.ts';
|
|
12
12
|
import type { StageFacts } from '../facts/stage.ts';
|
|
13
13
|
|
|
14
|
-
export function a14NoFullFrameMesh({ fail, skip }: Verdicts, { meshes }:
|
|
14
|
+
export function a14NoFullFrameMesh({ fail, skip }: Verdicts, { meshes }: Pick<MeshFacts, 'meshes'>, stage: StageFacts): void {
|
|
15
15
|
const meshAttachments = meshes;
|
|
16
16
|
const stageW = stage.width || 0;
|
|
17
17
|
const stageH = stage.height || 0;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* drives a mesh, unless the rig declares that its idle deforms them.
|
|
4
4
|
*
|
|
5
5
|
* Moved out of `src/validate.ts` unchanged but for what it reads: the meshes
|
|
6
|
-
* and the bones that drive them are a fact (`../facts/
|
|
6
|
+
* and the bones that drive them are a fact (`../facts/mesh_attachments.ts`, the bones derived from the weights by `weightBonesOf`) rather
|
|
7
7
|
* than spine-core's loaded skins and the weight run decoded here, the bones
|
|
8
8
|
* `idle` keys are a fact (`../facts/animated_bones.ts`) rather than the raw
|
|
9
9
|
* JSON, and the rig info is the caller's. Why the rule assumes meshes are
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* `check` call in `validate()`.
|
|
12
12
|
*/
|
|
13
13
|
import type { Verdicts } from '../harness.ts';
|
|
14
|
-
import type
|
|
14
|
+
import { weightBonesOf, type MeshEntry, type MeshFacts } from '../facts/mesh_attachments.ts';
|
|
15
15
|
import type { AnimatedBoneFacts } from '../facts/animated_bones.ts';
|
|
16
16
|
import type { RigInfo } from '../../types.ts';
|
|
17
17
|
|
|
18
|
-
export function a15IdleNoMeshBoneKeys({ fail, skip }: Verdicts,
|
|
18
|
+
export function a15IdleNoMeshBoneKeys({ fail, skip }: Verdicts, facts: MeshFacts, animated: AnimatedBoneFacts, input: { rig?: RigInfo }): void {
|
|
19
19
|
const A15 = 'A15_IDLE_NO_MESH_BONE_KEYS';
|
|
20
20
|
/**
|
|
21
21
|
* Every mesh attachment, loaded, with the bones that drive it — its slot's
|
|
@@ -25,9 +25,9 @@ export function a15IdleNoMeshBoneKeys({ fail, skip }: Verdicts, { meshes }: Skin
|
|
|
25
25
|
* actually dirties the canvas every frame.
|
|
26
26
|
*/
|
|
27
27
|
const meshDrivers: Array<{ mesh: MeshEntry; bones: Set<string> }> = [];
|
|
28
|
-
for (const mesh of meshes) {
|
|
28
|
+
for (const mesh of facts.meshes) {
|
|
29
29
|
const bones = new Set<string>([mesh.slotBone]);
|
|
30
|
-
for (const bone of mesh
|
|
30
|
+
for (const bone of weightBonesOf(facts, mesh)) bones.add(bone);
|
|
31
31
|
meshDrivers.push({ mesh, bones });
|
|
32
32
|
}
|
|
33
33
|
const meshBoneNames = new Set<string>();
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* in the unit square, one pair per vertex.
|
|
4
4
|
*
|
|
5
5
|
* Moved out of `src/validate.ts` unchanged but for what it reads: the meshes
|
|
6
|
-
* are a fact (`../facts/
|
|
6
|
+
* are a fact (`../facts/mesh_attachments.ts`). The `check` call stays in
|
|
7
7
|
* `validate()`.
|
|
8
8
|
*/
|
|
9
9
|
import type { Verdicts } from '../harness.ts';
|
|
10
|
-
import type {
|
|
10
|
+
import type { MeshFacts } from '../facts/mesh_attachments.ts';
|
|
11
11
|
import { SKIP_NO_MESH_ATTACHMENT } from '../reasons.ts';
|
|
12
12
|
|
|
13
|
-
export function a22MeshUvsInUnitRange({ fail, skip }: Verdicts, { meshes }:
|
|
13
|
+
export function a22MeshUvsInUnitRange({ fail, skip }: Verdicts, { meshes }: Pick<MeshFacts, 'meshes'>): void {
|
|
14
14
|
const meshAttachments = meshes;
|
|
15
15
|
if (meshAttachments.length === 0) return skip('A22_MESH_UVS_IN_UNIT_RANGE', SKIP_NO_MESH_ATTACHMENT);
|
|
16
16
|
for (const mesh of meshAttachments) {
|
|
@@ -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
|
+
}
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
* rather than folding the first two into an empty list.
|
|
12
12
|
*
|
|
13
13
|
* The order is the file's: A15 prints one line per keyed bone in it. The file
|
|
14
|
-
* keys an animation's bones in the order the emitter
|
|
15
|
-
* (`emitAnimation`), which is the document's own list
|
|
14
|
+
* keys an animation's bones in an object filled in the order the emitter
|
|
15
|
+
* writes the model's (`emitAnimation`), which is the document's own list — so
|
|
16
|
+
* an array-index bone name (`5`, `10`) is listed first whatever its place in
|
|
17
|
+
* that list (issue #1039), and the model side reads it through `keyedOrder`.
|
|
16
18
|
*
|
|
17
19
|
* Links nothing from the runtime: `validate()` reads it off the skeleton JSON
|
|
18
20
|
* as A15 always did, and the model side off the document
|
|
@@ -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
|
+
}
|