spine-rigc 1.5.2 → 1.5.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/docs/AUTHORING.md +13 -10
- package/docs/MOTION.md +25 -17
- package/package.json +1 -1
- package/src/compile.ts +9 -3
- package/src/keys.ts +76 -2
- package/src/motion.ts +25 -5
- package/src/rig.ts +103 -35
- package/src/types.ts +32 -4
- package/src/validate.ts +112 -56
package/docs/AUTHORING.md
CHANGED
|
@@ -1423,7 +1423,7 @@ it would simply be a second root.
|
|
|
1423
1423
|
| `skin` | `BoneData.skinRequired` | `false` |
|
|
1424
1424
|
| `color` | `rrggbbaa`, editor affordance | — |
|
|
1425
1425
|
| `icon` | the editor's icon for this bone, e.g. `arrowsB`; editor affordance, no rendering effect. Copied through verbatim — no assertion checks the name, because the icon vocabulary is the editor's and an unknown one is not an error | — |
|
|
1426
|
-
| `from` | **rigc extension** — take `x`/`y`
|
|
1426
|
+
| `from` | **rigc extension** — take `x`/`y` from a cut manifest: `from.anchor`, `from.slotWindow` or `from.meshCenter`, exactly one. `from.rotation` is `"axis"` (the manifest's `axis.deg`, negated into y-up) or `"anchor"` (the third element of the named anchor, a screen-space facing angle); any other value is refused by name — it used to build as no rotation at all | — |
|
|
1427
1427
|
|
|
1428
1428
|
⚠️ Spine 4.0/4.1 called `inherit` **`transform`**. That old key still *loads* in 4.3
|
|
1429
1429
|
and the inheritance silently falls back to Normal — assertion `A02` refuses it.
|
|
@@ -1803,7 +1803,7 @@ region, a trigger volume — that moves with the skeleton and draws nothing.
|
|
|
1803
1803
|
| `vertexCount` | **required**, 3 or more. No default — the paragraph above is why |
|
|
1804
1804
|
| `vertices` | the unweighted `x, y` run, in the slot bone's local space; or the index-encoded weighted run, behind `boneIndexing` |
|
|
1805
1805
|
| `weights` | the by-name form: one entry per vertex, each a list of `{ "bone": …, "x": …, "y": …, "weight": … }`, each pair in that bone's local space. Never beside `vertices` |
|
|
1806
|
-
| `boneIndexing` | `"name"` (the default) or `"raw"`, which opts a weighted `vertices` run into Spine's index encoding. `"raw"` beside `weights` is refused: `weights` always binds by name |
|
|
1806
|
+
| `boneIndexing` | `"name"` (the default) or `"raw"`, which opts a weighted `vertices` run into Spine's index encoding. `"raw"` beside `weights` is refused: `weights` always binds by name. Any other value — `"named"` included — is refused by name; it used to build as `"name"` |
|
|
1807
1807
|
| `color` | `rrggbbaa`. **No default in the file** — omitted, the parser never calls `setFromString` and the attachment keeps the runtime's own colour. It is an editor affordance, the colour the polygon is drawn in there; rigc emits it verbatim when stated and leaves the key out when not |
|
|
1808
1808
|
|
|
1809
1809
|
**Clipping polygon** ([Spine: clipping](http://esotericsoftware.com/spine-clipping)) —
|
|
@@ -1861,7 +1861,8 @@ is the wrap-around curve from `x = 180` back to `x = 0`, which nothing reads. Th
|
|
|
1861
1861
|
`worldX = 45.000000`.
|
|
1862
1862
|
|
|
1863
1863
|
The generators are `ring`, `ribbon`, `contour`, `grid` and `segments` (see
|
|
1864
|
-
[`src/mesh.ts`](../src/mesh.ts))
|
|
1864
|
+
[`src/mesh.ts`](../src/mesh.ts)), and a generator's `kind` is required: any other
|
|
1865
|
+
value, or none, is refused naming the five; the first two encode a deformation model rather
|
|
1865
1866
|
than a table of numbers, which is why they are code invoked by data. `contour`
|
|
1866
1867
|
and `grid` are geometry: they pin every vertex to the slot bone and exist to give
|
|
1867
1868
|
a `deform` timeline (§4.12) somewhere to push. `segments` is both at once — a
|
|
@@ -5450,11 +5451,12 @@ out of it.)
|
|
|
5450
5451
|
|
|
5451
5452
|
🔢 **A fourth surface is a pose that is not finite**, which `render` and `check`
|
|
5452
5453
|
refuse before drawing anything, naming the bone or vertex and its value.
|
|
5453
|
-
`A10_NO_NAN_AFTER_STEPPING` reads the same scan, so
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5454
|
+
`A10_NO_NAN_AFTER_STEPPING` reads the same scan, so `validate` gets there first and
|
|
5455
|
+
in the same words: a bone `rotation` of `1e309`, in the setup pose or in a key,
|
|
5456
|
+
leaves the bone's world position finite and its matrix `NaN`, and A10 names the bone
|
|
5457
|
+
and the term. That holds on a skeleton with no animation too — A10 reads its setup
|
|
5458
|
+
pose, which is the whole of what a static rig shows, and says on the stats line that
|
|
5459
|
+
it had nothing to step (`nanStepping=skipped`). What it does not see is a skin other
|
|
5458
5460
|
than the default, which A10 does not pose. The sentence, and where to look for the
|
|
5459
5461
|
number, is under *nothing to draw* in §3.4.
|
|
5460
5462
|
|
|
@@ -5581,7 +5583,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
5581
5583
|
| `a rig spec needs a non-empty "bones" array` | §3.2 — a rig with no bones has nothing to hang a slot on; every one has at least a root |
|
|
5582
5584
|
| `a rig spec needs a "slots" array (it may be empty; its ORDER is the draw order)` | §3.3 — write `[]` for a rig that draws nothing. ⚠️ These three arrive only when the key is really absent: **misspelt**, it is the unknown-key refusal above, naming what you wrote |
|
|
5583
5585
|
| `bone "X" names parent "Y", which is not declared before it` | move `Y` earlier in `bones` |
|
|
5584
|
-
| `<file>: <where> is a string "5"; a number is required` (also `is a boolean true`, `is an array [1]`, `is null`, `is a number 7; a string is required`, `…; an array of numbers is required`, `…; a boolean (true or false) is required`, `…; a string or null is required`) | §3 / §4 — write the value as the JSON type its field holds: a number as a JSON number (`5`, not `"5"`), a flag as `true`/`false`, a name as a string. Every field of the three input files has one type, and a value of another is refused by the field before any reader does arithmetic on it — which it used to: `"x": "5"` built as `5`, `true` as `1` and `[1]` as `1`, and a `null` built too, all green. An array is typed element by element, so the refusal names the index (`weights[3][1].x`, `vertices[0]`). `null` is refused as `null` except where the field states it means something (`skeleton.width`/`height`, `skeleton.audio`, a slot's and a setup entry's `attachment`, a deform key's `vertices`, the manifest's `image`, `states`, `contact_depth` and `cap_containment_ceiling`). `<where>` is the rig spec's wording (`bone "hip" x`, `skin "default" slot "s" attachment "a" rotation`, `constraint "aim" mix`), the dotted path in the cut manifest (`crop.h`, `parts[2].offset[0]`), and the motion spec's own path (`` `animations."walk".ik[0].keys[0].mix` ``). In the rig spec and the manifest it runs right after the key scan and before every other check, so a field check that used to name a type (a mesh name that is not a string, a `lengths` entry spelt as a string) now reads as this row; in the motion spec it runs after that file's own field checks, which keep their sentences. A closed set of names
|
|
5586
|
+
| `<file>: <where> is a string "5"; a number is required` (also `is a boolean true`, `is an array [1]`, `is null`, `is a number 7; a string is required`, `…; an array of numbers is required`, `…; a boolean (true or false) is required`, `…; a string or null is required`) | §3 / §4 — write the value as the JSON type its field holds: a number as a JSON number (`5`, not `"5"`), a flag as `true`/`false`, a name as a string. Every field of the three input files has one type, and a value of another is refused by the field before any reader does arithmetic on it — which it used to: `"x": "5"` built as `5`, `true` as `1` and `[1]` as `1`, and a `null` built too, all green. An array is typed element by element, so the refusal names the index (`weights[3][1].x`, `vertices[0]`). `null` is refused as `null` except where the field states it means something (`skeleton.width`/`height`, `skeleton.audio`, a slot's and a setup entry's `attachment`, a deform key's `vertices`, the manifest's `image`, `states`, `contact_depth` and `cap_containment_ceiling`). `<where>` is the rig spec's wording (`bone "hip" x`, `skin "default" slot "s" attachment "a" rotation`, `constraint "aim" mix`), the dotted path in the cut manifest (`crop.h`, `parts[2].offset[0]`), and the motion spec's own path (`` `animations."walk".ik[0].keys[0].mix` ``). In the rig spec and the manifest it runs right after the key scan and before every other check, so a field check that used to name a type (a mesh name that is not a string, a `lengths` entry spelt as a string) now reads as this row; in the motion spec it runs after that file's own field checks, which keep their sentences. A closed set of names is not a type, and is the next row's |
|
|
5587
|
+
| `<file>: <where> is "foo"; one of "name", "raw" — anything else was read as …` (also `bone "b" from.rotation is 5; one of "axis", "anchor"`, the manifest's `parts[1].mesh.kind is "foo"; one of "ring", "ribbon"`, and `skin "s" slot "x" attachment "a" generator.kind is "foo"; one of "ring", "ribbon", "contour", "grid", "segments"`, or `… generator.kind is absent; …`) | §3.2 / §3.4 — write one of the names listed. A field that holds one of a closed set of names is refused, with the set, when it holds anything else — a number too, since a number is not a name. Most such fields have a reader that refuses by name with its own sentence (`blend`, `inherit`, a constraint's `type`, `scaleY`); these four had none, and each built green or crashed: `boneIndexing` read as `"name"`, `from.rotation` as no rotation, the manifest's `mesh.kind` as a ribbon by one reader and a ring by two others, and a generator `kind` threw a `TypeError`. The sentence after the dash says what the value used to be read as, which is the build you had before this refusal. The first three run right after the type row's walk, before every other check; the generator's `kind` is refused earlier, by the key scan, because it chooses which keys the rest of the generator may hold |
|
|
5585
5588
|
| `<file>: <where> is Infinity; a number in this file is finite at float32 precision, at most ±3.4028234663852886e+38, because the skeleton is written as float32 — …` (also `is -Infinity`, `is 1e+308`, and `is NaN` from a library caller) | §3 — state the number you meant, inside float32's range. One walk over the whole file, run right after the key scan and before any other rig-spec check, so a range rule never prints `NaN`. `<where>` is every number in the rig spec — a bone (`bone "hip" x`), a slot, a skin's attachment down to its arrays (`skin "default" slot "s" attachment "a" weights[3][1].x`), a constraint (`constraint "aim" mixRotate`), an event, `skeleton`, `invariants.meshTriangles` — and the dotted path in the cut manifest (`crop.h`) and in the motion spec (`` `animations.idle.tracks[0].keys[1].v[0]` ``, after the motion spec's own field checks, which keep their sentences). ⚠️ It sees numbers only: `"x": "NaN"` is a string, and the row above refuses it as one |
|
|
5586
5589
|
| `two bones are called "X"` | bone names are the join key; rename one |
|
|
5587
5590
|
| `two ik constraints are called "X" — a constraint resolves by name AND type (\`SkeletonData.findConstraint\`), so names are unique PER KIND: an ik and a transform constraint may share one, two of a kind may not` | §3.5 — rename one of the two. The kind in the sentence is the pair's own, so `two transform constraints are called "X"` is the same refusal on another kind; a name shared **across** kinds is not this error |
|
|
@@ -5808,7 +5811,7 @@ Fix A00 and run it again.
|
|
|
5808
5811
|
| `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file. Three sentences are about blank lines and name three different repairs: `line 1: the file begins with a blank line` (or `with N blank lines` for a run, one finding) is a blank before the first page name — delete it; `line N: the file ends with a blank line` (or `with N blank lines`, one finding) is a blank after the last page block's final line — delete it, since rigc ends the file on one newline; `line N: consecutive blank lines` is two blank lines side by side between the first page and the last — delete one. None of the three is `the last page block declares no region`, which means a page name and its fields with no region after them. rigc writes none of the three: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (§0.2), so any of them means a file rigc did not write, handed to `validate`. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/firejune/rigc/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` |
|
|
5809
5812
|
| `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. There is no `spine-html` clause here — a placeholder is free to differ from the region its `path` names **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once |
|
|
5810
5813
|
| `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the loaded duration ≠ the declared one, or the two sides disagree about which animations exist (R7). Asymmetric by design: a frame of slack for an animation that ends early, and none worth the name for a key *past* the declared end, which is the same rule §4.5 states at compile time — held here against a skeleton the compiler never saw. **SKIP** when neither side has an animation at all — a static rig has no duration |
|
|
5811
|
-
| `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked **
|
|
5814
|
+
| `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked. 🧍 **A skeleton with no animation is still read**: its setup pose is the whole of what it shows, so A10 reads it — the world transform and the vertices as above, and the bone's mode and the slot colours a stepped frame is read for, which on a static rig nothing else would reach — and names it `the setup pose` (`the setup pose: slot "x" colour is non-finite`). The stepping half had nothing, and a PASS row carries no detail, so the stats line says so: `nanStepping=skipped`, beside `animations=0`. `A09` does not follow — a declared duration is a fact about an animation and nothing else, so a static rig still gives it nothing. **SKIP** only when the skeleton carries **no bone and no animation**: there is then no pose to read and nothing to step |
|
|
5812
5815
|
| `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
|
|
5813
5816
|
| `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
|
|
5814
5817
|
| `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh. A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
|
package/docs/MOTION.md
CHANGED
|
@@ -982,9 +982,11 @@ are entirely the motion spec's business.
|
|
|
982
982
|
|
|
983
983
|
### 4. In-between it
|
|
984
984
|
|
|
985
|
-
Duration 0.55 s, proposed (§3.3). Three easings (§3.4). The arm is the driver
|
|
986
|
-
is the
|
|
987
|
-
the
|
|
985
|
+
Duration 0.55 s, proposed (§3.3). Three easings (§3.4). The arm is the driver. The flag
|
|
986
|
+
is the last thing on it and it is cloth, which is the row §3.7's table gives to *a
|
|
987
|
+
pennant* — not the *next link out* row, because nothing hangs off the flag — so its
|
|
988
|
+
extreme lands **+20–35 %** after the arm's, it overshoots, and it settles last with one
|
|
989
|
+
crossing. *"Snap"* buys an anticipation (§3.6) and an
|
|
988
990
|
overshoot (§3.8). The post is planted: ⛔ **no timeline**.
|
|
989
991
|
|
|
990
992
|
`semaphore.motion.json` — complete, nothing trimmed:
|
|
@@ -1020,8 +1022,8 @@ overshoot (§3.8). The post is planted: ⛔ **no timeline**.
|
|
|
1020
1022
|
"keys": [
|
|
1021
1023
|
{ "t": 0, "v": [-22.2], "ease": "gather" },
|
|
1022
1024
|
{ "t": 0.09, "v": [-30.1], "ease": "charge" },
|
|
1023
|
-
{ "t": 0.
|
|
1024
|
-
{ "t": 0.
|
|
1025
|
+
{ "t": 0.47, "v": [-3.8], "ease": "settle" },
|
|
1026
|
+
{ "t": 0.51, "v": [-15.4], "ease": "settle" },
|
|
1025
1027
|
{ "t": 0.55, "v": [-11.7] }
|
|
1026
1028
|
]
|
|
1027
1029
|
}
|
|
@@ -1040,8 +1042,8 @@ which is which is worth being able to point at:
|
|
|
1040
1042
|
| arm `t: 0.55` = 18.3, flag `t: 0.55` = −11.7 | **given** — pose B. The flag's is `6.6 − 18.3`: both world rotations converted first, then differenced, because a child's track carries a **local** rotation under a rotated parent |
|
|
1041
1043
|
| arm `t: 0.07` = −66.4 | §3.6 — 4.5° against an 80° excursion (5.6 %), at 13 % of the duration |
|
|
1042
1044
|
| arm `t: 0.32` = 24.7 | §3.8 — 6.4° past the end value (8 %), at 58 % of the duration |
|
|
1043
|
-
| flag `t: 0.09`, `t: 0.
|
|
1044
|
-
| flag `t: 0.
|
|
1045
|
+
| flag `t: 0.09`, `t: 0.47` | §3.7 — the flag's extreme lands at 85 % of the duration against the arm's 58 %, an offset of **+27 %**, the middle of its row's band, and it drags the other way first. Not the top of the band: at 35 % (`t: 0.51`) the crossing and the settle below would share 0.04 s, and the 24 fps render would still show the flag at its extreme one frame before the last |
|
|
1046
|
+
| flag `t: 0.51` = −15.4 | §3.7's *one crossing* for a loose part: it comes back past its own end value before settling, keyed halfway between its extreme and the end |
|
|
1045
1047
|
| the three easings | §3.4 — one that gathers, one that arrives slowly, one symmetric. The **last key of each track carries no easing**, because there is nothing after it to ease towards (AUTHORING §4.5) |
|
|
1046
1048
|
| the post's absent track | §3.7 — a planted part gets no timeline |
|
|
1047
1049
|
|
|
@@ -1071,7 +1073,9 @@ Open `render/raise@24fps/contact.png` **before anything else** — fourteen fram
|
|
|
1071
1073
|
grid, and spacing is a comparison across frames rather than a property of any one of
|
|
1072
1074
|
them. What to check on it, and it is not a score: frame 0 is pose A — the flag hanging off
|
|
1073
1075
|
the arm's collar, not over its middle — the last frame is pose B, the anticipation dips
|
|
1074
|
-
*after* frame 0, and the flag's extreme is visibly later than the arm's
|
|
1076
|
+
*after* frame 0, and the flag's extreme is visibly later than the arm's: the arm peaks at
|
|
1077
|
+
frame 8 while the flag is still closing on the arm's line, it keeps closing through
|
|
1078
|
+
frame 11 as the arm eases back, and at frame 12 it drops past its resting angle.
|
|
1075
1079
|
|
|
1076
1080
|
🚫 **Do not run `rigc check` against `poseA.png` and `poseB.png`.** Two pictures are not
|
|
1077
1081
|
a frame set: `check` reads the second as the frame 1/12 s in, at its default rate. And
|
|
@@ -1115,21 +1119,21 @@ rigc vote --candidate spine --candidate spine-b
|
|
|
1115
1119
|
|
|
1116
1120
|
```
|
|
1117
1121
|
rigc vote
|
|
1118
|
-
.. ballot
|
|
1122
|
+
.. ballot 862f444c58d10e3e
|
|
1119
1123
|
.. animation raise
|
|
1120
|
-
.. A sha256:
|
|
1124
|
+
.. A sha256:c49e458df44e… 3 page(s), 0.4 KiB <- …/semaphore/spine/skeleton.json
|
|
1121
1125
|
.. B sha256:d1bda24d7c36… 3 page(s), 0.4 KiB <- …/semaphore/spine-b/skeleton.json
|
|
1122
1126
|
.. the page shows A/B and nothing else — the paths above are in its manifest, never on the screen
|
|
1123
1127
|
.. embedded every candidate's skeleton, atlas and page(s) as data URIs; the player itself loads from unpkg (@4.3.*), so the first open needs a network
|
|
1124
1128
|
rigc: wrote …/semaphore/ballot.html (22.3 KiB — open it in a browser)
|
|
1125
|
-
rigc: then record the saved vote with rigc vote --record vote-
|
|
1129
|
+
rigc: then record the saved vote with rigc vote --record vote-862f444c58d10e3e.json --ballot …/semaphore/ballot.html
|
|
1126
1130
|
```
|
|
1127
1131
|
|
|
1128
1132
|
A person opens that page, watches two loops, picks one, and saves the small JSON it hands
|
|
1129
1133
|
them. Then:
|
|
1130
1134
|
|
|
1131
1135
|
```bash
|
|
1132
|
-
rigc vote --record vote-
|
|
1136
|
+
rigc vote --record vote-862f444c58d10e3e.json --ballot ballot.html
|
|
1133
1137
|
```
|
|
1134
1138
|
|
|
1135
1139
|
```
|
|
@@ -1140,8 +1144,8 @@ rigc vote --record vote-bce70ad82fe90941.json --ballot ballot.html
|
|
|
1140
1144
|
PASS V04_CHOICE_IS_ON_THE_BALLOT
|
|
1141
1145
|
PASS V05_REASON_CODE_FITS_THE_CHOICE
|
|
1142
1146
|
PASS V06_NOT_ALREADY_RECORDED
|
|
1143
|
-
.. winner A = sha256:
|
|
1144
|
-
.. coverage 2 candidate(s): A=sha256:
|
|
1147
|
+
.. winner A = sha256:c49e458df44efd277aa6e50ff8fa1962c1211c7617079d63fbc4cf8008eaf0c5, reason code preferred
|
|
1148
|
+
.. coverage 2 candidate(s): A=sha256:c49e458df44e… B=sha256:d1bda24d7c36…
|
|
1145
1149
|
rigc: appended line 1 to …/semaphore/votes.jsonl
|
|
1146
1150
|
```
|
|
1147
1151
|
|
|
@@ -1169,9 +1173,13 @@ that the ends are stateable by construction (`pose`), that the file is checkable
|
|
|
1169
1173
|
(`vote`).
|
|
1170
1174
|
|
|
1171
1175
|
🚫 **No scoring of end-pose reach, and nothing here to add one to.** At L1 and L2 the
|
|
1172
|
-
end poses are given conditions
|
|
1173
|
-
|
|
1174
|
-
|
|
1176
|
+
end poses are given conditions, and what a picture gives is a **pose**: the rotations are
|
|
1177
|
+
`pose`'s readings, which the spec states by construction, so a number for how near the
|
|
1178
|
+
animation got to *them* is a number about the pose estimator. The **offsets** are the
|
|
1179
|
+
other half — derived from the pictures rather than given — and a rig can miss its
|
|
1180
|
+
picture by a wrong one with every key exact. Catching that is a look, not a score (§6.5's
|
|
1181
|
+
frame-0 check). This is why §6 does not run `check` on the two pictures and why no
|
|
1182
|
+
threshold, tolerance or pass bar appears anywhere in this document. The
|
|
1175
1183
|
residuals in a `pose` report are trust signals about *placements*, and AUTHORING §11.1
|
|
1176
1184
|
says the same from the instrument's side.
|
|
1177
1185
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.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": {
|
package/src/compile.ts
CHANGED
|
@@ -38,9 +38,9 @@ import { parseJsonWithPosition } from './json-position.ts';
|
|
|
38
38
|
// The "did you mean" list on a missing atlas region, from the one implementation
|
|
39
39
|
// of it — the same search serves `refuseUnknownKeys`, and a second copy here with
|
|
40
40
|
// a threshold edited is how such a pair drifts apart.
|
|
41
|
-
import { dottedPath, nearMisses, refuseNumbersTheFileCannotCarry, refuseValuesOfTheWrongType } from './keys.ts';
|
|
41
|
+
import { dottedPath, nearMisses, refuseNumbersTheFileCannotCarry, refuseValuesOfTheWrongType, refuseValuesOutsideTheirSet } from './keys.ts';
|
|
42
42
|
import type { ShapeVisit } from './keys.ts';
|
|
43
|
-
import { MANIFEST_TYPES } from './types.ts';
|
|
43
|
+
import { MANIFEST_ENUMS, MANIFEST_TYPES } from './types.ts';
|
|
44
44
|
import { inEditorKeyOrder, withoutParserDefaults } from './keyorder.ts';
|
|
45
45
|
import { EVERY_GLOBAL_PHYSICS, parseMotionSpec } from './motion.ts';
|
|
46
46
|
import {
|
|
@@ -2062,7 +2062,13 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2062
2062
|
// same rule over the other file whose numbers reach the skeleton.
|
|
2063
2063
|
// Its values of the wrong type first (issue #890): `crop.h: "806"` built
|
|
2064
2064
|
// green, the string coerced by whatever arithmetic read it.
|
|
2065
|
-
if (manifest !== null && manifestPath !== null)
|
|
2065
|
+
if (manifest !== null && manifestPath !== null) {
|
|
2066
|
+
const visits = manifestShapeVisits(manifest);
|
|
2067
|
+
refuseValuesOfTheWrongType(visits, MANIFEST_TYPES, manifestPath);
|
|
2068
|
+
// Then a name outside a stated set (issue #900): `mesh.kind: "foo"` was
|
|
2069
|
+
// read as a ribbon by one reader and a ring by two others.
|
|
2070
|
+
refuseValuesOutsideTheirSet(visits, MANIFEST_TYPES, MANIFEST_ENUMS, manifestPath);
|
|
2071
|
+
}
|
|
2066
2072
|
if (manifest !== null && manifestPath !== null) refuseNumbersTheFileCannotCarry(manifest, manifestPath, dottedPath);
|
|
2067
2073
|
|
|
2068
2074
|
// The rig spec names its own path in every message `parseRigSpec` throws (its
|
package/src/keys.ts
CHANGED
|
@@ -219,8 +219,12 @@ export function dottedPath(path: ReadonlyArray<string | number>): string {
|
|
|
219
219
|
* kind of container is the refusal of the reader that walks it (a rig spec's
|
|
220
220
|
* `"bones"` that is not an array is *a rig spec needs a non-empty "bones"
|
|
221
221
|
* array*);
|
|
222
|
-
* - `enum` — a closed set of names
|
|
223
|
-
*
|
|
222
|
+
* - `enum` — a closed set of names. Every `enum` row has an entry in the
|
|
223
|
+
* shape's enum table (`RIG_ENUMS`, `MOTION_ENUMS`, `MANIFEST_ENUMS`, typed by
|
|
224
|
+
* `SpecEnumTable`), which either states the set — and
|
|
225
|
+
* `refuseValuesOutsideTheirSet` refuses a value outside it — or names the
|
|
226
|
+
* reader that refuses it where it reads it, with the names that exist listed
|
|
227
|
+
* (a slot's `blend`, a constraint's `type`);
|
|
224
228
|
* - `mixed` — a union of kinds whose owner decides by the value (`MotionKey.v`
|
|
225
229
|
* is numbers on `rotate`, a name on `attachment`, a member map on a group).
|
|
226
230
|
*/
|
|
@@ -410,3 +414,73 @@ export function refuseValuesOfTheWrongType(
|
|
|
410
414
|
}
|
|
411
415
|
}
|
|
412
416
|
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Who refuses a value outside an `enum` row's closed set (issue #900).
|
|
420
|
+
*
|
|
421
|
+
* - `set` — the names the row accepts, refused from one place by
|
|
422
|
+
* `refuseValuesOutsideTheirSet`; `readAs` is what the readers did with any
|
|
423
|
+
* other value before the set was stated, which the refusal says, because
|
|
424
|
+
* "one of" alone does not tell an author that the build they had was wrong;
|
|
425
|
+
* - `owner` — the function that reads the value and refuses one outside its
|
|
426
|
+
* set by name there, listing the names that exist. It keeps its own sentence,
|
|
427
|
+
* which often says more than a set can (a first letter whose case is free, a
|
|
428
|
+
* row chosen by the value itself).
|
|
429
|
+
*/
|
|
430
|
+
export type SpecEnumRule = { readonly set: readonly string[]; readonly readAs: string } | { readonly owner: string };
|
|
431
|
+
|
|
432
|
+
/** The keys of a type row whose type is `enum`. */
|
|
433
|
+
type EnumKeysOf<R> = { [K in keyof R]: R[K] extends 'enum' ? K : never }[keyof R];
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* A type table's enum table: one entry per `enum` row, and nothing else.
|
|
437
|
+
*
|
|
438
|
+
* 🔒 `satisfies SpecEnumTable<typeof X_TYPES>` is what makes a fourth unowned
|
|
439
|
+
* enum a type error rather than a row nobody holds: a shape with an `enum` row
|
|
440
|
+
* must appear, with exactly its `enum` keys, and a key of another type cannot.
|
|
441
|
+
* A selftest control holds the same claim at runtime and reads each `owner`'s
|
|
442
|
+
* body for the key it is said to own.
|
|
443
|
+
*/
|
|
444
|
+
export type SpecEnumTable<T extends Readonly<Record<string, SpecTypeRow>>> = {
|
|
445
|
+
readonly [S in keyof T as [EnumKeysOf<T[S]>] extends [never] ? never : S]: { readonly [K in EnumKeysOf<T[S]>]: SpecEnumRule };
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Refuse the first value of an `enum` row whose enum table states a set and
|
|
450
|
+
* that is not in it (issue #900).
|
|
451
|
+
*
|
|
452
|
+
* 🚨 The defect this closes was a green build again, one step past #890's:
|
|
453
|
+
* the type walk does not check `enum` (a name's type says nothing about which
|
|
454
|
+
* names exist), and three rows had nobody holding the name — `boneIndexing`,
|
|
455
|
+
* a bone's `from.rotation` and the cut manifest's `mesh.kind`. `"foo"` and `5`
|
|
456
|
+
* were each read as something the spec never said, in silence.
|
|
457
|
+
*
|
|
458
|
+
* It runs after the type walk and over the same visits, so a key the scan
|
|
459
|
+
* refused is never judged here, and before every reader. A row whose table
|
|
460
|
+
* entry names an `owner` is passed over: that reader refuses it with its own
|
|
461
|
+
* sentence. The order is the type walk's — visits in the scan's order, keys in
|
|
462
|
+
* the document's — so the refusal names the same value on every run.
|
|
463
|
+
*/
|
|
464
|
+
export function refuseValuesOutsideTheirSet(
|
|
465
|
+
visits: readonly ShapeVisit[],
|
|
466
|
+
types: Readonly<Record<string, SpecTypeRow>>,
|
|
467
|
+
enums: Readonly<Record<string, Readonly<Record<string, SpecEnumRule>>>>,
|
|
468
|
+
where: string,
|
|
469
|
+
): void {
|
|
470
|
+
for (const visit of visits) {
|
|
471
|
+
const row = types[visit.shape];
|
|
472
|
+
if (row === undefined) throw new Error(`no type row for shape ${visit.shape} (a key scan visited a shape the type table does not have)`);
|
|
473
|
+
for (const key of Object.keys(visit.node)) {
|
|
474
|
+
if (row[key] !== 'enum') continue;
|
|
475
|
+
const rule = enums[visit.shape]?.[key];
|
|
476
|
+
if (rule === undefined) throw new Error(`${visit.shape}.${key} is an enum row with no entry in its enum table`);
|
|
477
|
+
if (!('set' in rule)) continue;
|
|
478
|
+
const value = visit.node[key];
|
|
479
|
+
if (typeof value === 'string' && rule.set.includes(value)) continue;
|
|
480
|
+
throw new CompileError(
|
|
481
|
+
`${where}: ${visit.name([key])} is ${JSON.stringify(value) ?? String(value)}; one of ` +
|
|
482
|
+
`${rule.set.map((name) => JSON.stringify(name)).join(', ')} — ${rule.readAs}`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
package/src/motion.ts
CHANGED
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
* [`keys.ts`](keys.ts) shared with the rig parser.
|
|
55
55
|
*/
|
|
56
56
|
import { CompileError } from './errors.ts';
|
|
57
|
-
import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys, refuseValuesOfTheWrongType } from './keys.ts';
|
|
58
|
-
import type { ShapeVisit, SpecValueType } from './keys.ts';
|
|
57
|
+
import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys, refuseValuesOfTheWrongType, refuseValuesOutsideTheirSet } from './keys.ts';
|
|
58
|
+
import type { ShapeVisit, SpecEnumTable, SpecValueType } from './keys.ts';
|
|
59
59
|
import { SEQUENCE_MODES } from './timelines.ts';
|
|
60
60
|
import type { MotionSpec } from './types.ts';
|
|
61
61
|
|
|
@@ -165,9 +165,8 @@ export const MOTION_KEYS = {
|
|
|
165
165
|
* fields nothing here reads — an ik key's `mix`, a deform key's `offset`, an
|
|
166
166
|
* event key's `float`, a derive's `degrees`.
|
|
167
167
|
*
|
|
168
|
-
* Where an unchecked type is refused: `
|
|
169
|
-
*
|
|
170
|
-
* transform's `kind`, `along` and `axis` — `evaluateDeformTransform`;
|
|
168
|
+
* Where an unchecked type is refused: an `enum` row — the entry `MOTION_ENUMS`
|
|
169
|
+
* has for it, below, which is a table a control reads rather than a list here;
|
|
171
170
|
* `MotionKey.v`, a key's `curve`, a derive's `depth` and `mix.pairs` (`mixed`)
|
|
172
171
|
* — the track compiler by property, the curve reader, the derive evaluator and
|
|
173
172
|
* `parseMix`. A sequence key's `mode` is a closed set too, but its interface
|
|
@@ -227,6 +226,24 @@ export const MOTION_TYPES = {
|
|
|
227
226
|
DeformBend: { kind: 'enum', amount: 'number', from: 'number', to: 'number', power: 'number', along: 'enum', axis: 'enum' },
|
|
228
227
|
} as const satisfies MotionTypeTable;
|
|
229
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Who refuses each `enum` row of `MOTION_TYPES` outside its set (issue #900);
|
|
231
|
+
* `SpecEnumTable` in [`keys.ts`](keys.ts) says what an entry means. Every one
|
|
232
|
+
* is an owner: measured when this table was written, a planted `5` and `"foo"`
|
|
233
|
+
* on each row was refused by name by the reader named here, listing the names
|
|
234
|
+
* that exist, so none of them states a set. A deform transform's and a derive's
|
|
235
|
+
* `kind` choose the row they are checked against, as a generator's does.
|
|
236
|
+
*/
|
|
237
|
+
export const MOTION_ENUMS = {
|
|
238
|
+
MotionSpec: { spec: { owner: 'parseMotionSpecInto' } },
|
|
239
|
+
MotionTrack: { property: { owner: 'compileTrack' } },
|
|
240
|
+
TrackDeriveTurn: { kind: { owner: 'evaluateTrackDerive' } },
|
|
241
|
+
DeformTurn: { kind: { owner: 'evaluateDeformTransform' } },
|
|
242
|
+
DeformAffine: { kind: { owner: 'evaluateDeformTransform' } },
|
|
243
|
+
DeformWave: { kind: { owner: 'evaluateDeformTransform' }, along: { owner: 'evaluateDeformTransform' }, axis: { owner: 'evaluateDeformTransform' } },
|
|
244
|
+
DeformBend: { kind: { owner: 'evaluateDeformTransform' }, along: { owner: 'evaluateDeformTransform' }, axis: { owner: 'evaluateDeformTransform' } },
|
|
245
|
+
} as const satisfies SpecEnumTable<typeof MOTION_TYPES>;
|
|
246
|
+
|
|
230
247
|
/** A deform key's `transform` kinds, and the shape each one's keys come from. */
|
|
231
248
|
const DEFORM_TRANSFORM_SHAPE: Record<string, keyof typeof MOTION_KEYS> = {
|
|
232
249
|
yaw: 'DeformTurn',
|
|
@@ -778,6 +795,9 @@ function parseMotionSpecInto(raw: unknown, where: string, visits: readonly Shape
|
|
|
778
795
|
// as an event with no audio, a yaw transform's `"depth": "true"` as a radius
|
|
779
796
|
// that is undefined.
|
|
780
797
|
refuseValuesOfTheWrongType(visits, MOTION_TYPES, where);
|
|
798
|
+
// Every `enum` row here names an owner, so this refuses nothing today; it is
|
|
799
|
+
// called so that a row given a set is refused from the day it is given one.
|
|
800
|
+
refuseValuesOutsideTheirSet(visits, MOTION_TYPES, MOTION_ENUMS, where);
|
|
781
801
|
|
|
782
802
|
// Last, so every field check above keeps its own sentence for a number that
|
|
783
803
|
// is not finite; what reaches this line is a finite double the float32 file
|
package/src/rig.ts
CHANGED
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
* with no manifest at all declares them here.
|
|
61
61
|
*/
|
|
62
62
|
import { CompileError, NotImplementedError } from './errors.ts';
|
|
63
|
-
import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys, refuseValuesOfTheWrongType } from './keys.ts';
|
|
64
|
-
import type { ShapeVisit, SpecValueType } from './keys.ts';
|
|
63
|
+
import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys, refuseValuesOfTheWrongType, refuseValuesOutsideTheirSet } from './keys.ts';
|
|
64
|
+
import type { ShapeVisit, SpecEnumTable, SpecValueType } from './keys.ts';
|
|
65
65
|
|
|
66
66
|
export { CompileError, NotImplementedError };
|
|
67
67
|
|
|
@@ -1866,29 +1866,20 @@ export const RIG_KEYS = {
|
|
|
1866
1866
|
* field declared `number` and typed `string` here is a red run rather than a
|
|
1867
1867
|
* refusal of correct work.
|
|
1868
1868
|
*
|
|
1869
|
-
* Where an unchecked
|
|
1870
|
-
*
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
1875
|
-
*
|
|
1876
|
-
*
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
1879
|
-
*
|
|
1880
|
-
*
|
|
1881
|
-
*
|
|
1882
|
-
*
|
|
1883
|
-
* 🚨 Measured when this table was written, three `enum` keys have **no** owner,
|
|
1884
|
-
* and the table does not pretend otherwise: `boneIndexing` built green at `5`
|
|
1885
|
-
* and at `"foo"` (on a weighted mesh too), `from.rotation` built green at `5`
|
|
1886
|
-
* and at `"foo"` on an anchored bone, and a generator `kind` of `5` or `"foo"`
|
|
1887
|
-
* threw a `TypeError` (*undefined is not an object (evaluating
|
|
1888
|
-
* 'generator.size')*) rather than the mesh builder's refusal. Each is a closed
|
|
1889
|
-
* set of names with nobody holding the name, which is a value refusal and not a
|
|
1890
|
-
* type one, so it is not fixed here. `scaleY` on a physics constraint was not
|
|
1891
|
-
* measured.
|
|
1869
|
+
* Where an unchecked `object` or `mixed` row is refused: a nested shape by the
|
|
1870
|
+
* reader that walks it; `RigSegmentsGenerator.bones` (`mixed`: a name, a chain
|
|
1871
|
+
* of names or a span) by the segments generator, by index. Where an `enum` row
|
|
1872
|
+
* is refused is not a list in prose any more: `RIG_ENUMS`, below, has one entry
|
|
1873
|
+
* per `enum` row — a set refused by `refuseValuesOutsideTheirSet`, or the
|
|
1874
|
+
* reader that refuses it — and `satisfies` makes a row without one a type error.
|
|
1875
|
+
*
|
|
1876
|
+
* ⚠️ Until issue #900 this comment carried that list, and ended by naming three
|
|
1877
|
+
* `enum` keys with **no** owner: `boneIndexing`, `from.rotation` and a
|
|
1878
|
+
* generator `kind`. The list was right about the three and wrong by omission
|
|
1879
|
+
* about a fourth — the cut manifest's `mesh.kind`, which `MANIFEST_TYPES` said
|
|
1880
|
+
* "the manifest mesh reader" held and which that reader read as a ribbon in one
|
|
1881
|
+
* place and a ring in another. A list of owners in a comment is a claim nothing
|
|
1882
|
+
* checks; the table is one a control reads.
|
|
1892
1883
|
*/
|
|
1893
1884
|
type RigTypeTable = {
|
|
1894
1885
|
readonly [S in keyof typeof RIG_KEYS]: { readonly [K in (typeof RIG_KEYS)[S][number]]: SpecValueType };
|
|
@@ -1998,6 +1989,70 @@ export const RIG_TYPES = {
|
|
|
1998
1989
|
RigSoftRegion: { bone: 'string', mask: 'string' },
|
|
1999
1990
|
} as const satisfies RigTypeTable;
|
|
2000
1991
|
|
|
1992
|
+
/** The two sources a bone's `from.rotation` names (`RigBoneFrom.rotation`). */
|
|
1993
|
+
export const RIG_FROM_ROTATIONS = ['axis', 'anchor'] as const satisfies ReadonlyArray<NonNullable<RigBoneFrom['rotation']>>;
|
|
1994
|
+
|
|
1995
|
+
/** The two ways a weighted `vertices` run names its bones (`RigMeshAttachment.boneIndexing`). */
|
|
1996
|
+
export const RIG_BONE_INDEXING = ['name', 'raw'] as const satisfies ReadonlyArray<NonNullable<RigMeshAttachment['boneIndexing']>>;
|
|
1997
|
+
|
|
1998
|
+
/** What `boneIndexing` outside its set was read as, measured before issue #900. */
|
|
1999
|
+
const BONE_INDEXING_READ_AS =
|
|
2000
|
+
'anything else was read as the default "name" in silence: a weighted "vertices" run was refused as unflagged, ' +
|
|
2001
|
+
'and every other attachment built byte for byte as if the key were absent';
|
|
2002
|
+
|
|
2003
|
+
/**
|
|
2004
|
+
* Who refuses each `enum` row of `RIG_TYPES` outside its set (issue #900) —
|
|
2005
|
+
* `SpecEnumTable` in [`keys.ts`](keys.ts) says what the two kinds of entry
|
|
2006
|
+
* mean, and `satisfies` makes an `enum` row without one a type error.
|
|
2007
|
+
*
|
|
2008
|
+
* ⭐ A `set` is stated for exactly the rows nobody held: measured when this
|
|
2009
|
+
* table was written, `boneIndexing` built green at `5`, `"foo"` and `"named"`
|
|
2010
|
+
* (the spelling the issue itself used) as the default, and `from.rotation`
|
|
2011
|
+
* built green at `5` and `"foo"` with the bone's setup rotation dropped. Every
|
|
2012
|
+
* other row already had a reader that refused a planted `5` and `"foo"` by
|
|
2013
|
+
* name, and keeps it; the generator's `kind` is the one of those that is a
|
|
2014
|
+
* dispatch, and its refusal is new — it threw a `TypeError` (*undefined is not
|
|
2015
|
+
* an object (evaluating 'generator.size')*), because the scan skipped a `kind`
|
|
2016
|
+
* it had no row for and the mesh builder fell through to the ring branch.
|
|
2017
|
+
*/
|
|
2018
|
+
export const RIG_ENUMS = {
|
|
2019
|
+
RigSpec: { spec: { owner: 'parseRigSpec' } },
|
|
2020
|
+
RigBone: { inherit: { owner: 'parseRigSpec' } },
|
|
2021
|
+
RigBoneFrom: {
|
|
2022
|
+
rotation: {
|
|
2023
|
+
set: RIG_FROM_ROTATIONS,
|
|
2024
|
+
readAs: 'anything else was read as no rotation source, and the bone was emitted without the setup rotation it asked for',
|
|
2025
|
+
},
|
|
2026
|
+
},
|
|
2027
|
+
RigSlot: { blend: { owner: 'parseRigSpec' } },
|
|
2028
|
+
RigConsumerDrivenMix: { type: { owner: 'parseRigSpec' } },
|
|
2029
|
+
RigIkConstraint: { type: { owner: 'buildRigConstraint' }, scaleY: { owner: 'buildRigConstraint' } },
|
|
2030
|
+
RigTransformConstraint: { type: { owner: 'buildRigConstraint' } },
|
|
2031
|
+
RigPathConstraint: {
|
|
2032
|
+
type: { owner: 'buildRigConstraint' },
|
|
2033
|
+
positionMode: { owner: 'buildRigConstraint' },
|
|
2034
|
+
spacingMode: { owner: 'buildRigConstraint' },
|
|
2035
|
+
rotateMode: { owner: 'buildRigConstraint' },
|
|
2036
|
+
},
|
|
2037
|
+
RigPhysicsConstraint: { type: { owner: 'buildRigConstraint' }, scaleY: { owner: 'buildRigConstraint' } },
|
|
2038
|
+
RigSliderConstraint: { type: { owner: 'buildRigConstraint' }, property: { owner: 'buildRigConstraint' } },
|
|
2039
|
+
RigRegionAttachment: { type: { owner: 'buildRigAttachment' } },
|
|
2040
|
+
RigMeshAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2041
|
+
RigLinkedMeshAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2042
|
+
RigBoundingBoxAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2043
|
+
RigClippingAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2044
|
+
RigPathAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2045
|
+
// The generator's `kind` chooses the row its other keys are checked against,
|
|
2046
|
+
// so a `kind` outside the five has no row to be visited in: the scan that
|
|
2047
|
+
// dispatches on it is where it is refused.
|
|
2048
|
+
RigRingGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2049
|
+
RigRibbonGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2050
|
+
RigContourGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2051
|
+
RigGridGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2052
|
+
RigSegmentsGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2053
|
+
RigDepthMap: { near: { owner: 'sampleMeshDepth' } },
|
|
2054
|
+
} as const satisfies SpecEnumTable<typeof RIG_TYPES>;
|
|
2055
|
+
|
|
2001
2056
|
/**
|
|
2002
2057
|
* The five constraint `type` names, and the shape each one's keys come from.
|
|
2003
2058
|
*
|
|
@@ -2024,7 +2079,7 @@ const ATTACHMENT_SHAPE: Record<string, keyof typeof RIG_KEYS> = {
|
|
|
2024
2079
|
};
|
|
2025
2080
|
|
|
2026
2081
|
/** The generator `kind` names, and the shape each one's keys come from. */
|
|
2027
|
-
const GENERATOR_SHAPE: Record<
|
|
2082
|
+
const GENERATOR_SHAPE: Record<(typeof RIG_GENERATOR_KINDS)[number], keyof typeof RIG_KEYS> = {
|
|
2028
2083
|
ring: 'RigRingGenerator',
|
|
2029
2084
|
ribbon: 'RigRibbonGenerator',
|
|
2030
2085
|
contour: 'RigContourGenerator',
|
|
@@ -2239,10 +2294,21 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{
|
|
|
2239
2294
|
}
|
|
2240
2295
|
if (!isObj(att.generator)) continue;
|
|
2241
2296
|
const gen = att.generator;
|
|
2242
|
-
const
|
|
2243
|
-
|
|
2244
|
-
//
|
|
2245
|
-
|
|
2297
|
+
const kind = RIG_GENERATOR_KINDS.find((k) => k === gen.kind);
|
|
2298
|
+
const genShape = kind === undefined ? undefined : GENERATOR_SHAPE[kind];
|
|
2299
|
+
// 🚨 An unknown `kind` used to be skipped here as "the mesh builder's
|
|
2300
|
+
// refusal", and the mesh builder had none: `buildGeneratedMesh` fell
|
|
2301
|
+
// through to the ring branch and threw a TypeError reading
|
|
2302
|
+
// `generator.size` (issue #900). `kind` chooses the row every other key
|
|
2303
|
+
// of the generator is checked against, so this dispatch is the one
|
|
2304
|
+
// place that can refuse it — before any of those keys is judged.
|
|
2305
|
+
if (genShape === undefined) {
|
|
2306
|
+
const stated = gen.kind === undefined ? 'absent' : (JSON.stringify(gen.kind) ?? String(gen.kind));
|
|
2307
|
+
throw new CompileError(
|
|
2308
|
+
`${where}: ${who} generator.kind is ${stated}; one of ${RIG_GENERATOR_KINDS.map((k) => JSON.stringify(k)).join(', ')} — ` +
|
|
2309
|
+
'the kind names the builder, and each builder reads its own keys, so nothing about the mesh can be checked or built without one',
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2246
2312
|
at(gen, genShape, `${who} generator (${String(gen.kind)})`, [...here, 'generator']);
|
|
2247
2313
|
at(gen.bias, 'RigMeshBias', `${who} generator.bias`, [...here, 'generator', 'bias']);
|
|
2248
2314
|
at(gen.depth, 'RigDepthMap', `${who} generator.depth`, [...here, 'generator', 'depth']);
|
|
@@ -2338,11 +2404,13 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
|
2338
2404
|
// `constraint.skin === true` reads `"skin": "true"` as a constraint that asks
|
|
2339
2405
|
// for no skin. The price, measured, is three controls whose type half pinned an
|
|
2340
2406
|
// older sentence (RF86, PS177, PS189); their other halves are unchanged.
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2407
|
+
const shapeVisits: ShapeVisit[] = visits.map((v) => ({ node: v.node, shape: v.shape, name: (tail) => place([...v.path, ...tail]) }));
|
|
2408
|
+
refuseValuesOfTheWrongType(shapeVisits, RIG_TYPES, where);
|
|
2409
|
+
// A name outside an `enum` row's stated set (issue #900), after the type walk
|
|
2410
|
+
// and over the same visits: `boneIndexing: "foo"` built as the default and
|
|
2411
|
+
// `from.rotation: "foo"` as no rotation, both green. A row whose entry names
|
|
2412
|
+
// an owner is that reader's to refuse, in its own words.
|
|
2413
|
+
refuseValuesOutsideTheirSet(shapeVisits, RIG_TYPES, RIG_ENUMS, where);
|
|
2346
2414
|
// After the key check, so a misspelled key is named as a misspelling before
|
|
2347
2415
|
// its value is judged, and before every reader below, so no range rule or
|
|
2348
2416
|
// derived number ever meets a value the file cannot carry (issue #881).
|
package/src/types.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import type { AtlasRegion } from './atlas.ts';
|
|
18
18
|
import type { DeformTransform, DeformTransformReport } from './deformgen.ts';
|
|
19
19
|
import type { TurnCeiling } from './depth.ts';
|
|
20
|
-
import type { SpecTypeRow } from './keys.ts';
|
|
20
|
+
import type { SpecEnumTable, SpecTypeRow } from './keys.ts';
|
|
21
21
|
import type { MeshKind } from './mesh.ts';
|
|
22
22
|
import type { TrackDerive, TrackDeriveReport } from './trackgen.ts';
|
|
23
23
|
|
|
@@ -185,9 +185,9 @@ export interface FaceManifest {
|
|
|
185
185
|
* wrong-typed plants on the three fixture manifests built green —
|
|
186
186
|
* `crop.h: "256"` among them — and 4 threw a TypeError from `node:path`.
|
|
187
187
|
*
|
|
188
|
-
* Unchecked: `mesh.kind` and `mesh.hull` (`enum`) —
|
|
189
|
-
* `crop` and the other nested shapes, `parts` and
|
|
190
|
-
* readers that walk them.
|
|
188
|
+
* Unchecked by this walk: `mesh.kind` and `mesh.hull` (`enum`) — see
|
|
189
|
+
* `MANIFEST_ENUMS`, below; `crop` and the other nested shapes, `parts` and
|
|
190
|
+
* `mesh` (`object`) — the readers that walk them.
|
|
191
191
|
*/
|
|
192
192
|
export const MANIFEST_TYPES = {
|
|
193
193
|
FaceManifest: {
|
|
@@ -211,6 +211,34 @@ export const MANIFEST_TYPES = {
|
|
|
211
211
|
'FaceManifestMesh.bias': { axis_deg: 'number', ramp: 'number[]', note: 'string' },
|
|
212
212
|
} as const satisfies Record<string, SpecTypeRow>;
|
|
213
213
|
|
|
214
|
+
/** The two mesh generators a manifest part may name (`FaceManifestMesh.kind`); absent means `ring`. */
|
|
215
|
+
export const MANIFEST_MESH_KINDS = ['ring', 'ribbon'] as const satisfies ReadonlyArray<NonNullable<FaceManifestMesh['kind']>>;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Who refuses each `enum` row of `MANIFEST_TYPES` outside its set (issue #900);
|
|
219
|
+
* `SpecEnumTable` in [`keys.ts`](keys.ts) says what an entry means.
|
|
220
|
+
*
|
|
221
|
+
* 🚨 `mesh.kind` was listed as held by "the manifest mesh reader", and it was
|
|
222
|
+
* not held at all: that reader has three places that ask the kind, and they
|
|
223
|
+
* disagreed about a value outside the two. The mesh checks read `"foo"` as a
|
|
224
|
+
* ribbon, while the control-bone reader and the geometry builder read it as a
|
|
225
|
+
* ring — so a ring part was refused as *a ribbon mesh needs mesh.rows and a
|
|
226
|
+
* mesh.chain*, and a ribbon part as *a mesh with no control bone deforms
|
|
227
|
+
* nothing*, both naming a fault the part did not have. `hull` is held: the ring
|
|
228
|
+
* check refuses anything but `"polygon"` by name, and keeps that sentence.
|
|
229
|
+
*/
|
|
230
|
+
export const MANIFEST_ENUMS = {
|
|
231
|
+
FaceManifestMesh: {
|
|
232
|
+
kind: {
|
|
233
|
+
set: MANIFEST_MESH_KINDS,
|
|
234
|
+
readAs:
|
|
235
|
+
'anything else was read as a ribbon by the mesh checks and as a ring by the control-bone reader and the ' +
|
|
236
|
+
'geometry builder, so the refusal it got named a fault the part did not have',
|
|
237
|
+
},
|
|
238
|
+
hull: { owner: 'compileInto' },
|
|
239
|
+
},
|
|
240
|
+
} as const satisfies SpecEnumTable<typeof MANIFEST_TYPES>;
|
|
241
|
+
|
|
214
242
|
// ---------------------------------------------------------------------------
|
|
215
243
|
// Motion spec (spec: "rigc-motion/1")
|
|
216
244
|
// ---------------------------------------------------------------------------
|
package/src/validate.ts
CHANGED
|
@@ -289,16 +289,17 @@ export const SKIP_NO_ATLAS = 'the round trip did not produce an atlas to measure
|
|
|
289
289
|
* `A15_IDLE_NO_MESH_BONE_KEYS` is the pattern and already reads this way: no
|
|
290
290
|
* `idle` animation, or an `idle` with no bone timeline, is the named subject
|
|
291
291
|
* absent and SKIPs; no mesh-driving bone among the bones `idle` does key is
|
|
292
|
-
* the construct absent and passes. `A10_NO_NAN_AFTER_STEPPING`
|
|
293
|
-
* shape
|
|
294
|
-
*
|
|
292
|
+
* the construct absent and passes. `A10_NO_NAN_AFTER_STEPPING` was the same
|
|
293
|
+
* shape until issue #902, and is the next bullet's now: #882 gave it a
|
|
294
|
+
* setup-pose clause whose subject is the bones, beside the stepping clause
|
|
295
|
+
* whose subject is the animations.
|
|
295
296
|
* * An assertion with more than one clause SKIPs only when EVERY clause had
|
|
296
297
|
* nothing to measure, which is the shape `A09`, `A33` and `A38` already
|
|
297
|
-
* carry (`polygons.length === 0 && endsChecked === 0`)
|
|
298
|
-
* why the clause is stated: a declared slot budget is
|
|
299
|
-
* count of mesh slots, and zero is a count, so that half
|
|
300
|
-
* with no mesh — while a rig that declares only a TRIANGLE
|
|
301
|
-
* nothing left to measure and skips.
|
|
298
|
+
* carry (`polygons.length === 0 && endsChecked === 0`), and `A10` since #902.
|
|
299
|
+
* `A13_MESH_BUDGET` is why the clause is stated: a declared slot budget is
|
|
300
|
+
* measured against a count of mesh slots, and zero is a count, so that half
|
|
301
|
+
* passes on a rig with no mesh — while a rig that declares only a TRIANGLE
|
|
302
|
+
* budget has nothing left to measure and skips.
|
|
302
303
|
*
|
|
303
304
|
* ⚠️ What the criterion is not allowed to become is a table of assertions with
|
|
304
305
|
* their verdicts written beside them. Every row above is decided by reading the
|
|
@@ -318,6 +319,13 @@ export const SKIP_NO_ATLAS = 'the round trip did not produce an atlas to measure
|
|
|
318
319
|
export const SKIP_NO_REGION_ATTACHMENT = 'the skeleton carries no region attachment';
|
|
319
320
|
export const SKIP_NO_MESH_ATTACHMENT = 'the skeleton carries no mesh attachment';
|
|
320
321
|
export const SKIP_NO_ANIMATION = 'the skeleton carries no animation';
|
|
322
|
+
/**
|
|
323
|
+
* A10's (issue #902): its setup clause reads the bones and its stepping clause
|
|
324
|
+
* the animations, so it skips only when the skeleton carries neither — and the
|
|
325
|
+
* sentence is `SKIP_NO_ANIMATION`'s with the second subject added, so the two
|
|
326
|
+
* cannot drift into different words for the same absence.
|
|
327
|
+
*/
|
|
328
|
+
export const SKIP_NO_POSE = `${SKIP_NO_ANIMATION} and no bone, so there is no setup pose to read and nothing to step`;
|
|
321
329
|
export const SKIP_NO_TIMELINE = 'no animation here carries a timeline';
|
|
322
330
|
export const SKIP_NO_PHYSICS_CONSTRAINT = 'the skeleton declares no physics constraint';
|
|
323
331
|
export const SKIP_NO_ATLAS_PAGE = 'the atlas declares no page';
|
|
@@ -4336,13 +4344,31 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
4336
4344
|
|
|
4337
4345
|
// --- A10: step every animation and look for NaN ------------------------
|
|
4338
4346
|
check('A10_NO_NAN_AFTER_STEPPING', () => {
|
|
4339
|
-
//
|
|
4340
|
-
//
|
|
4341
|
-
//
|
|
4342
|
-
//
|
|
4343
|
-
//
|
|
4344
|
-
//
|
|
4345
|
-
|
|
4347
|
+
// Two clauses, and since issue #902 each is read on its own subject. The
|
|
4348
|
+
// SETUP POSE is posed from the bones, so it has something to read on any
|
|
4349
|
+
// skeleton that carries one; the STEPPED FRAMES are posed once per
|
|
4350
|
+
// animation, so a skeleton with none has nothing to step. This used to
|
|
4351
|
+
// skip the whole rule on the second fact alone, which was A15's pattern
|
|
4352
|
+
// (#580) for a rule whose only clause was the stepping — and #882 gave it
|
|
4353
|
+
// the setup clause without moving the skip. Measured on the static probe
|
|
4354
|
+
// with a leaf bone at `rotation: 1e309` in its emitted file: `validate`
|
|
4355
|
+
// printed this rule as SKIP and the run green under both profiles, and
|
|
4356
|
+
// `render` refused the same file by the bone. A static rig's setup pose
|
|
4357
|
+
// is the whole of what it shows, so that was the one pose nothing read.
|
|
4358
|
+
//
|
|
4359
|
+
// ⇒ The multi-clause rule of #580 now governs, the shape A09, A13, A33
|
|
4360
|
+
// and A38 carry: the rule SKIPs only when neither clause has anything to
|
|
4361
|
+
// measure, the clause that ran decides PASS or FAIL, and the clause that
|
|
4362
|
+
// had nothing is named on the stats line (`nanStepping=skipped`, beside
|
|
4363
|
+
// `animations=0`) the way A47 names a constraint it did not measure. A
|
|
4364
|
+
// PASS row carries no detail, which is why the stats line is where.
|
|
4365
|
+
//
|
|
4366
|
+
// ⚠️ A09 does not follow, and that is its own name read at its word: a
|
|
4367
|
+
// declared duration is a fact about an animation and nothing else, so a
|
|
4368
|
+
// static rig still gives it nothing at all.
|
|
4369
|
+
if (data.animations.length === 0 && data.bones.length === 0) {
|
|
4370
|
+
return skip('A10_NO_NAN_AFTER_STEPPING', SKIP_NO_POSE);
|
|
4371
|
+
}
|
|
4346
4372
|
/** Is this a mode `updateWorldTransform`'s switch has a case for? Read off the runtime's own enum. */
|
|
4347
4373
|
const isMode = (inherit: unknown): boolean => typeof inherit === 'number' && Inherit[inherit] !== undefined;
|
|
4348
4374
|
|
|
@@ -4421,6 +4447,73 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
4421
4447
|
// below is posed AFTER `state.apply`, so a bone the animation keys from
|
|
4422
4448
|
// t=0 never shows its setup value to the loop, and a runtime that shows
|
|
4423
4449
|
// the rig at rest does show it.
|
|
4450
|
+
//
|
|
4451
|
+
// 🔸 Posed once, before any animation is set, rather than once per
|
|
4452
|
+
// animation as it was until #902: `setAnimation` does not touch the
|
|
4453
|
+
// skeleton, so every animation's copy of this pose was the same pose, and
|
|
4454
|
+
// the first of them was the only one ever read.
|
|
4455
|
+
if (data.animations.length === 0) stats.nanStepping = 'skipped';
|
|
4456
|
+
const atRest = new Skeleton(data);
|
|
4457
|
+
atRest.setupPose();
|
|
4458
|
+
atRest.update(0);
|
|
4459
|
+
atRest.updateWorldTransform(Physics.reset);
|
|
4460
|
+
const atSetup = nonFiniteOfPosed('the setup pose', atRest);
|
|
4461
|
+
if (atSetup !== null) {
|
|
4462
|
+
fail('A10_NO_NAN_AFTER_STEPPING', atSetup);
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
/**
|
|
4467
|
+
* What a posed frame shows that the world-transform scan does not read: a
|
|
4468
|
+
* bone posing no inheritance mode, and a slot colour that is not finite.
|
|
4469
|
+
* `where` leads the sentence — the animation's name at a stepped frame,
|
|
4470
|
+
* as it always has, or `the setup pose` on a skeleton with no animation.
|
|
4471
|
+
*/
|
|
4472
|
+
const poseDefect = (where: string, skeleton: Skeleton): string | null => {
|
|
4473
|
+
for (const bone of skeleton.bones) {
|
|
4474
|
+
const pose = bone.appliedPose;
|
|
4475
|
+
// The setup half of the inherit-key clause above: a bone's own
|
|
4476
|
+
// `inherit` goes through the same lookup, and a miss there loads
|
|
4477
|
+
// `undefined` into the setup pose, which every frame copies. Only
|
|
4478
|
+
// reachable on a file rigc did not write — `parseRigSpec` refuses the
|
|
4479
|
+
// spelling.
|
|
4480
|
+
if (!isMode(pose.inherit)) {
|
|
4481
|
+
const rawBone = Array.isArray(raw?.bones)
|
|
4482
|
+
? (raw.bones as unknown[]).find((b) => isObj(b) && b.name === bone.data.name)
|
|
4483
|
+
: undefined;
|
|
4484
|
+
return (
|
|
4485
|
+
`${where}: bone "${bone.data.name}" poses inheritance mode ${String(pose.inherit)} — its setup ` +
|
|
4486
|
+
`spells inherit ${JSON.stringify(isObj(rawBone) ? rawBone.inherit : undefined)}, which the ` +
|
|
4487
|
+
`runtime's mode lookup does not resolve (${BONE_INHERIT_KNOWN}), so its world rotation, scale and shear ` +
|
|
4488
|
+
'are never computed — they stay 0 and everything the bone carries collapses to a point'
|
|
4489
|
+
);
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
for (const slot of skeleton.slots) {
|
|
4493
|
+
const c = slot.appliedPose.color;
|
|
4494
|
+
if (![c.r, c.g, c.b, c.a].every(Number.isFinite)) return `${where}: slot "${slot.data.name}" colour is non-finite`;
|
|
4495
|
+
// The other colour a slot poses, and it was outside this loop until
|
|
4496
|
+
// issue #690 for the reason every gap here has: nothing emitted one.
|
|
4497
|
+
// `null` is the ordinary case — a slot with no `dark` allocates no
|
|
4498
|
+
// dark colour at all — and is not a reading to make, so it is
|
|
4499
|
+
// skipped rather than treated as zero.
|
|
4500
|
+
const d = slot.appliedPose.darkColor;
|
|
4501
|
+
if (d !== null && ![d.r, d.g, d.b].every(Number.isFinite)) {
|
|
4502
|
+
return `${where}: slot "${slot.data.name}" dark colour is non-finite`;
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
return null;
|
|
4506
|
+
};
|
|
4507
|
+
|
|
4508
|
+
// The stepping half, and on a static rig there is nothing to step: the
|
|
4509
|
+
// setup pose is then the only frame the skeleton has, and the two readings
|
|
4510
|
+
// a stepped frame gets are made on it instead, so a colour or an
|
|
4511
|
+
// inheritance mode the rig shows at rest is not left to `render`.
|
|
4512
|
+
if (data.animations.length === 0) {
|
|
4513
|
+
const atRestDefect = poseDefect('the setup pose', atRest);
|
|
4514
|
+
if (atRestDefect !== null) fail('A10_NO_NAN_AFTER_STEPPING', atRestDefect);
|
|
4515
|
+
return;
|
|
4516
|
+
}
|
|
4424
4517
|
for (const anim of data.animations) {
|
|
4425
4518
|
const skeleton = new Skeleton(data);
|
|
4426
4519
|
const state = new AnimationState(new AnimationStateData(data));
|
|
@@ -4428,11 +4521,6 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
4428
4521
|
skeleton.setupPose();
|
|
4429
4522
|
skeleton.update(0);
|
|
4430
4523
|
skeleton.updateWorldTransform(Physics.reset);
|
|
4431
|
-
const atSetup = nonFiniteOfPosed('the setup pose', skeleton);
|
|
4432
|
-
if (atSetup !== null) {
|
|
4433
|
-
fail('A10_NO_NAN_AFTER_STEPPING', atSetup);
|
|
4434
|
-
return;
|
|
4435
|
-
}
|
|
4436
4524
|
const step = Math.max(anim.duration, 1) / STEP_FRAMES;
|
|
4437
4525
|
for (let i = 0; i < STEP_FRAMES; i++) {
|
|
4438
4526
|
state.update(step);
|
|
@@ -4447,42 +4535,10 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
4447
4535
|
fail('A10_NO_NAN_AFTER_STEPPING', found);
|
|
4448
4536
|
return;
|
|
4449
4537
|
}
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
// the setup pose, which every frame copies. Only reachable on a
|
|
4455
|
-
// file rigc did not write — `parseRigSpec` refuses the spelling.
|
|
4456
|
-
if (!isMode(pose.inherit)) {
|
|
4457
|
-
const rawBone = Array.isArray(raw?.bones)
|
|
4458
|
-
? (raw.bones as unknown[]).find((b) => isObj(b) && b.name === bone.data.name)
|
|
4459
|
-
: undefined;
|
|
4460
|
-
fail(
|
|
4461
|
-
'A10_NO_NAN_AFTER_STEPPING',
|
|
4462
|
-
`${anim.name}: bone "${bone.data.name}" poses inheritance mode ${String(pose.inherit)} — its setup ` +
|
|
4463
|
-
`spells inherit ${JSON.stringify(isObj(rawBone) ? rawBone.inherit : undefined)}, which the ` +
|
|
4464
|
-
`runtime's mode lookup does not resolve (${BONE_INHERIT_KNOWN}), so its world rotation, scale and shear ` +
|
|
4465
|
-
'are never computed — they stay 0 and everything the bone carries collapses to a point',
|
|
4466
|
-
);
|
|
4467
|
-
return;
|
|
4468
|
-
}
|
|
4469
|
-
}
|
|
4470
|
-
for (const slot of skeleton.slots) {
|
|
4471
|
-
const c = slot.appliedPose.color;
|
|
4472
|
-
if (![c.r, c.g, c.b, c.a].every(Number.isFinite)) {
|
|
4473
|
-
fail('A10_NO_NAN_AFTER_STEPPING', `${anim.name}: slot "${slot.data.name}" colour is non-finite`);
|
|
4474
|
-
return;
|
|
4475
|
-
}
|
|
4476
|
-
// The other colour a slot poses, and it was outside this loop until
|
|
4477
|
-
// issue #690 for the reason every gap here has: nothing emitted one.
|
|
4478
|
-
// `null` is the ordinary case — a slot with no `dark` allocates no
|
|
4479
|
-
// dark colour at all — and is not a reading to make, so it is
|
|
4480
|
-
// skipped rather than treated as zero.
|
|
4481
|
-
const d = slot.appliedPose.darkColor;
|
|
4482
|
-
if (d !== null && ![d.r, d.g, d.b].every(Number.isFinite)) {
|
|
4483
|
-
fail('A10_NO_NAN_AFTER_STEPPING', `${anim.name}: slot "${slot.data.name}" dark colour is non-finite`);
|
|
4484
|
-
return;
|
|
4485
|
-
}
|
|
4538
|
+
const defect = poseDefect(anim.name, skeleton);
|
|
4539
|
+
if (defect !== null) {
|
|
4540
|
+
fail('A10_NO_NAN_AFTER_STEPPING', defect);
|
|
4541
|
+
return;
|
|
4486
4542
|
}
|
|
4487
4543
|
}
|
|
4488
4544
|
}
|