spine-rigc 1.5.0 โ 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/AUTHORING.md +45 -19
- package/docs/INGEST.md +20 -13
- package/docs/MOTION.md +31 -17
- package/package.json +1 -1
- package/src/compile.ts +6 -1
- package/src/keys.ts +87 -0
- package/src/motion.ts +7 -1
- package/src/pose.ts +31 -6
- package/src/render.ts +32 -0
- package/src/rig.ts +46 -2
- package/src/validate.ts +34 -4
package/docs/AUTHORING.md
CHANGED
|
@@ -1298,6 +1298,14 @@ emitted as written, in the default skin or anywhere else.
|
|
|
1298
1298
|
`spec` must be exactly `"rigc-rig/1"`. `name` must be a non-empty string. `bones`
|
|
1299
1299
|
must be non-empty. `slots` must be present (it may be empty).
|
|
1300
1300
|
|
|
1301
|
+
๐ข **Every number in a rig spec is finite at float32 precision** โ at most
|
|
1302
|
+
ยฑ3.4028234663852886e38 โ whatever field it is in, so the ranges the tables below
|
|
1303
|
+
state are ranges *within* that. The skeleton is written as float32, and a number
|
|
1304
|
+
past that line would be emitted as Infinity, which JSON writes as `null`: `1e309`
|
|
1305
|
+
(which JSON reads as Infinity) and `1e308` (a finite double with no float32) are
|
|
1306
|
+
both refused by the field they are in, before anything is compiled (ยง5.1). The cut
|
|
1307
|
+
manifest and the motion spec are held to the same line.
|
|
1308
|
+
|
|
1301
1309
|
๐ซ **Every example value below is invented.** Names, coordinates, vertex lists and
|
|
1302
1310
|
payloads in this guide are written to illustrate a field, never copied out of a
|
|
1303
1311
|
reference export โ an example lifted from one would be handing an authoring agent an
|
|
@@ -5429,11 +5437,14 @@ corpus inventory, and a ladder run reads this guide without following its refere
|
|
|
5429
5437
|
out of it.)
|
|
5430
5438
|
|
|
5431
5439
|
๐ข **A fourth surface is a pose that is not finite**, which `render` and `check`
|
|
5432
|
-
refuse before drawing anything, naming the bone or vertex and its value.
|
|
5433
|
-
|
|
5434
|
-
in
|
|
5435
|
-
|
|
5436
|
-
|
|
5440
|
+
refuse before drawing anything, naming the bone or vertex and its value.
|
|
5441
|
+
`A10_NO_NAN_AFTER_STEPPING` reads the same scan, so on a skeleton with an animation
|
|
5442
|
+
`validate` gets there first and in the same words: a bone `rotation` of `1e309`, in
|
|
5443
|
+
the setup pose or in a key, leaves the bone's world position finite and its matrix
|
|
5444
|
+
`NaN`, and A10 names the bone and the term. What it does not see is a skeleton with
|
|
5445
|
+
no animation (A10 SKIPs, so `render` is where that one is named) and a skin other
|
|
5446
|
+
than the default, which A10 does not pose. The sentence, and where to look for the
|
|
5447
|
+
number, is under *nothing to draw* in ยง3.4.
|
|
5437
5448
|
|
|
5438
5449
|
### 5.1 Compile errors โ before the gate
|
|
5439
5450
|
|
|
@@ -5558,6 +5569,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
5558
5569
|
| `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 |
|
|
5559
5570
|
| `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 |
|
|
5560
5571
|
| `bone "X" names parent "Y", which is not declared before it` | move `Y` earlier in `bones` |
|
|
5572
|
+
| `<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 is not refused by this row โ write a number as a JSON number |
|
|
5561
5573
|
| `two bones are called "X"` | bone names are the join key; rename one |
|
|
5562
5574
|
| `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 |
|
|
5563
5575
|
| `physics constraint "X" is declared in both the rig spec and the motion spec's physics table` | ยง4.6 โ the rig spec declares a physics constraint's structure and the motion spec's `physics` table declares one outright; pick the file it belongs in. Per kind, like every other constraint name: an `ik` "X" in the rig spec beside a `physics` "X" here is two constraints and is not this error |
|
|
@@ -5783,7 +5795,7 @@ Fix A00 and run it again.
|
|
|
5783
5795
|
| `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` |
|
|
5784
5796
|
| `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 |
|
|
5785
5797
|
| `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 |
|
|
5786
|
-
| `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `
|
|
5798
|
+
| `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 **SKIP** when the skeleton carries no animation: the NaN is produced by stepping, and a static rig is never stepped โ the same subject `A09` skips on |
|
|
5787
5799
|
| `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
|
|
5788
5800
|
| `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
|
|
5789
5801
|
| `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 |
|
|
@@ -8840,8 +8852,8 @@ Found within 2 px of the truth, out of 15 trials per cell (free search, default
|
|
|
8840
8852
|
|
|
8841
8853
|
| texture level (mean) | 24 px | 32 px | 48 px | 64 px | 96 px | 128 px | 192 px |
|
|
8842
8854
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
8843
|
-
| `native` (detail 3.49) | 13/15 (2 amb) | 15/15 | 15/15 | 15/15 |
|
|
8844
|
-
| `blur1` (detail 1.58) | 12/15 (3 amb) | 12/15 (
|
|
8855
|
+
| `native` (detail 3.49) | 13/15 (2 amb) | 15/15 | 15/15 | 15/15 | 14/15 (1 wrong) | 14/15 (1 wrong) | 14/15 (1 wrong) |
|
|
8856
|
+
| `blur1` (detail 1.58) | 12/15 (3 amb) | 12/15 (3 amb) | 13/15 (2 amb) | 12/15 (3 amb) | 12/15 (2 amb, 1 wrong) | 11/15 (1 amb, 3 wrong) | 11/15 (2 amb, 2 wrong) |
|
|
8845
8857
|
| `blur2` (detail 0.59) | 9/15 (5 amb, 1 wrong) | 9/15 (4 amb, 2 wrong) | 9/15 (4 amb, 2 wrong) | 8/15 (3 amb, 4 wrong) | 7/15 (3 amb, 5 wrong) | 6/15 (4 amb, 5 wrong) | 7/15 (3 amb, 5 wrong) |
|
|
8846
8858
|
| `flat` (detail 0.00) | 0/15 (14 amb, 1 wrong) | 0/15 (14 amb, 1 wrong) | 0/15 (13 amb, 2 wrong) | 0/15 (11 amb, 4 wrong) | 0/15 (13 amb, 2 wrong) | 0/15 (12 amb, 3 wrong) | 0/15 (12 amb, 3 wrong) |
|
|
8847
8859
|
|
|
@@ -8850,14 +8862,14 @@ The same trials, grouped by `detail`:
|
|
|
8850
8862
|
| detail | found | ambiguous | wrong | refused |
|
|
8851
8863
|
| --- | --- | --- | --- | --- |
|
|
8852
8864
|
| under 0.25 | 8/119 | 89 | 22 | 0 |
|
|
8853
|
-
| 0.25โ0.5 |
|
|
8854
|
-
| 0.5โ0.75 |
|
|
8865
|
+
| 0.25โ0.5 | 0/3 | 2 | 1 | 0 |
|
|
8866
|
+
| 0.5โ0.75 | 49/81 | 13 | 19 | 0 |
|
|
8855
8867
|
| 0.75โ1 | 17/34 | 14 | 3 | 0 |
|
|
8856
8868
|
| 1โ1.5 | 25/25 | 0 | 0 | 0 |
|
|
8857
|
-
| 1.5โ2 | 34/48 |
|
|
8869
|
+
| 1.5โ2 | 34/48 | 13 | 1 | 0 |
|
|
8858
8870
|
| 2โ3 | 40/44 | 1 | 3 | 0 |
|
|
8859
8871
|
| 3โ4 | 34/34 | 0 | 0 | 0 |
|
|
8860
|
-
| 4 and over |
|
|
8872
|
+
| 4 and over | 31/32 | 1 | 0 | 0 |
|
|
8861
8873
|
|
|
8862
8874
|
**Method.** The truth set is `examples/spineboy`: its setup pose and one frame of
|
|
8863
8875
|
`walk` (a third of the way in). For every trial the figure is rendered twice with one
|
|
@@ -8879,8 +8891,8 @@ part has a low per-pixel gradient and still places โ and size alone has no
|
|
|
8879
8891
|
measurable effect across 24โ192 px at any level.
|
|
8880
8892
|
|
|
8881
8893
|
**The edge**, derived by `deriveFloor` as the largest rung of a fixed ladder under
|
|
8882
|
-
which at most a tenth of the trials were found: **`detail` 0.5**. Under it, **
|
|
8883
|
-
of the trials were found (
|
|
8894
|
+
which at most a tenth of the trials were found: **`detail` 0.5**. Under it, **6.6%**
|
|
8895
|
+
of the trials were found (8/122); at or over it, **77.2%** (230/298). A part is below
|
|
8884
8896
|
the floor when its `detail` is under 0.5, or when its own longest side is under 24 px,
|
|
8885
8897
|
the smallest size measured (there, nothing measured says `pose` can place it).
|
|
8886
8898
|
|
|
@@ -8911,16 +8923,29 @@ blurred part now reported with a second placement at the same spot and another
|
|
|
8911
8923
|
scale, one of them with the truth itself as its best answer), for 43% more
|
|
8912
8924
|
refinement samples and the coarse pass untouched.
|
|
8913
8925
|
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8926
|
+
Since issue #886 the stopped polish tries **a quarter rung** (2^(1/12)) beside the
|
|
8927
|
+
half and keeps the better re-fit. The case was not a grid trial but ยง6's stationary
|
|
8928
|
+
post in [MOTION.md](MOTION.md): under `--scale 0.85,1.2` it came back at scale
|
|
8929
|
+
0.917, residual 0.0649, where the old placement at 0.975 still scores 0.0589. The
|
|
8930
|
+
half rung stepped from 0.917 to 1.029, where the best position scores 0.0980 โ a part
|
|
8931
|
+
that fills its image pays for every pixel pushed past the figure, so above its truth
|
|
8932
|
+
the objective is a cliff, and half a rung crossed the basin onto it. With both, the
|
|
8933
|
+
post places at 0.971 (0.0593, 0.1 px from the old placement), and the grid goes from
|
|
8934
|
+
237 to 238 found and 8 to 6 misses (2 gained, 1 lost) for 16% more user CPU. The
|
|
8935
|
+
quarter alone found 234, losing the native arms #877 was made for. The one lost is a
|
|
8936
|
+
margin reading again: the blurred 32 px front shin (walk) now reports its truth as its
|
|
8937
|
+
best (0.07 px, 0.00968), with its old 0.51 px answer 0.0045 above it.
|
|
8938
|
+
|
|
8939
|
+
โ ๏ธ **What this is and is not.** The grid is not a reliability line: six native cells
|
|
8940
|
+
place 14 or 15 of 15 and the trials at `detail` 3 and over place 65 of 66, but the
|
|
8941
|
+
goggles still place 13 of 42, and a threshold read off two dozen parts of one figure
|
|
8917
8942
|
would be a claim about that figure. What the grid can separate is **whose** failure a
|
|
8918
8943
|
failure is. `bun tools/pose_floor.ts` prints the residual at the truth for every
|
|
8919
8944
|
failed trial and marks it **missed** when the truth scores better than everything
|
|
8920
8945
|
reported and nothing reported is on it โ a failure the search could fix. The other
|
|
8921
8946
|
failures are the objective preferring another placement (a blurred part shrunk into
|
|
8922
8947
|
its own region scores better than its truth) or the truth reported and tied, and no
|
|
8923
|
-
search can fix those. After #
|
|
8948
|
+
search can fix those. After #886, 6 of the 182 failed trials are misses, and 5 of
|
|
8924
8949
|
those sit within 3.1 px of the truth rather than on another hill; the `--from`
|
|
8925
8950
|
listing prints, for each, where the refinement left the truth level by level. All this was
|
|
8926
8951
|
measured on **ideal cuts**, the frame's own pixels, where a residual at the truth is
|
|
@@ -8935,7 +8960,8 @@ print the table, derive the edge and list the misses per cell. `PO22` in
|
|
|
8935
8960
|
no miss at all, and `PO23` places the 48 px fist #865 was made for; `PO28` and
|
|
8936
8961
|
`PO29` place the four trials #877 was made for and read where the shin survived the
|
|
8937
8962
|
cutoff, and `PO26` holds every polish to strictly descending residuals, read through
|
|
8938
|
-
the same trace.
|
|
8963
|
+
the same trace. `PO31` places ยง6's post at the old placement from the page's own
|
|
8964
|
+
input bytes, and `PO32` holds it to taking the quarter-rung escape and reading the same twice.
|
|
8939
8965
|
|
|
8940
8966
|
## 12. Reading the half of that picture `pose` refuses โ `rigc chainfit`
|
|
8941
8967
|
|
package/docs/INGEST.md
CHANGED
|
@@ -370,6 +370,8 @@ rigc check --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing
|
|
|
370
370
|
--frames ref3/light
|
|
371
371
|
```
|
|
372
372
|
|
|
373
|
+
**No run reproduces this:** abridged โ the head's `atlas`, `frames`, `skin`, `scope`, `reference`, `content` and `in units` lines and its two โ ๏ธ notes are cut, so the `โคท fit` line the run prints under `content` stands under `framed to`; the section's `frames` line and every `โคท` note are cut; and so is everything the run prints after `per-frame`
|
|
374
|
+
|
|
373
375
|
```
|
|
374
376
|
rigc check
|
|
375
377
|
candidate โฆ/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
|
|
@@ -378,14 +380,14 @@ rigc check
|
|
|
378
380
|
declared frames.json's own box: TAKEN, coincident โ a fit there asks for 0.00 px, under the 1 px that separates a candidate in the frames' coordinates from one in its own, over 21 frame(s).
|
|
379
381
|
|
|
380
382
|
โโ light โ candidate animation "light", 12 fps โโ
|
|
381
|
-
MAE mean 0.00 worst 0.00
|
|
382
|
-
slot drift worst 0.
|
|
383
|
+
MAE mean 0.00 worst 0.00 (exact: none of the 21 compared frame(s) differs from the reference) (0..255 over the union alpha; over the whole frame, mean 0.00)
|
|
384
|
+
slot drift worst 0.4 px "pendulum" at f0012
|
|
383
385
|
per-frame all 20 adjacent pair(s) change by as much as the reference's own frames do
|
|
384
386
|
```
|
|
385
387
|
|
|
386
388
|
Two things to take from the control, both of which you need before reading any real
|
|
387
389
|
number: the **framing** resolved to the frames' own box at 0.00 px, so the MAE is a
|
|
388
|
-
comparison of pictures rather than of framings; and **slot drift still reads 0.
|
|
390
|
+
comparison of pictures rather than of framings; and **slot drift still reads 0.4 px at
|
|
389
391
|
MAE 0.00**, which is that instrument's own floor rather than a difference.
|
|
390
392
|
|
|
391
393
|
โ ๏ธ **`--texture-from` is not optional on ingest work, and the reason is structural.**
|
|
@@ -1339,7 +1341,9 @@ against one picture. A foreign export hands you a packed page instead, and point
|
|
|
1339
1341
|
`pose` at one is worse than useless โ it treats the whole page as a single part and
|
|
1340
1342
|
answers confidently:
|
|
1341
1343
|
|
|
1342
|
-
```bash
|
|
1344
|
+
```bash setup
|
|
1345
|
+
rigc render --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json \
|
|
1346
|
+
--fps 4 --max 900 --out ref-big
|
|
1343
1347
|
mkdir -p packed-only
|
|
1344
1348
|
cp examples/3-timing-and-spacing/export/3-timing-and-spacing.png packed-only/
|
|
1345
1349
|
rigc pose --images packed-only --frame 'ref-big/light@4fps/f0002.png'
|
|
@@ -1350,12 +1354,12 @@ rigc pose
|
|
|
1350
1354
|
.. frame โฆ/ref-big/light@4fps/f0002.png (900x409)
|
|
1351
1355
|
.. ground rgb(232, 232, 232) over 100% of the border ring
|
|
1352
1356
|
.. parts โฆ/packed-only (1 png)
|
|
1353
|
-
.. search scale 0.5โ2 in 7 step(s) ยท rotation -180ยฐโ180ยฐ step 15ยฐ ยท refuse above residual 0.25
|
|
1354
|
-
PLACE 3-timing-and-spacing.png x= 324.3 y= 188.
|
|
1355
|
-
found on a
|
|
1357
|
+
.. search scale 0.5โ2 in 7 step(s) ยท rotation -180ยฐโ180ยฐ in 24 step(s) of 15ยฐ ยท refuse above residual 0.25
|
|
1358
|
+
PLACE 3-timing-and-spacing.png x= 324.3 y= 188.3 rot= -91.6ยฐ scale=0.630 residual=0.2085 unexplained= 30%
|
|
1359
|
+
found on a 14x7 anchor grid, step 4 at 16x reduction
|
|
1356
1360
|
```
|
|
1357
1361
|
|
|
1358
|
-
โ ๏ธ **`residual=0.
|
|
1362
|
+
โ ๏ธ **`residual=0.2085` is *under* the default 0.25 refusal bar**, so nothing refused
|
|
1359
1363
|
it, and `PLACE` rather than `AMBIG` means nothing flagged it either. With the same frame
|
|
1360
1364
|
and the two real loose parts, the answer is what it should be:
|
|
1361
1365
|
|
|
@@ -1365,12 +1369,15 @@ rigc pose --images examples/3-timing-and-spacing/images \
|
|
|
1365
1369
|
```
|
|
1366
1370
|
|
|
1367
1371
|
```
|
|
1372
|
+
rigc pose
|
|
1373
|
+
.. frame โฆ/ref-big/light@4fps/f0002.png (900x409)
|
|
1374
|
+
.. ground rgb(232, 232, 232) over 100% of the border ring
|
|
1368
1375
|
.. parts โฆ/examples/3-timing-and-spacing/images (2 png)
|
|
1369
|
-
.. search scale 0.3โ0.6 in 4 step(s) ยท rotation -180ยฐโ180ยฐ step 15ยฐ ยท refuse above residual 0.25
|
|
1370
|
-
PLACE pendulum.png x= 319.3 y= 214.
|
|
1371
|
-
found on a
|
|
1372
|
-
PLACE square.png x= 437.
|
|
1373
|
-
found on a
|
|
1376
|
+
.. search scale 0.3โ0.6 in 4 step(s) ยท rotation -180ยฐโ180ยฐ in 24 step(s) of 15ยฐ ยท refuse above residual 0.25
|
|
1377
|
+
PLACE pendulum.png x= 319.3 y= 214.6 rot= -88.9ยฐ scale=0.411 residual=0.0428 unexplained= 7%
|
|
1378
|
+
found on a 28x13 anchor grid, step 2 at 16x reduction
|
|
1379
|
+
PLACE square.png x= 437.1 y= 343.1 rot= 0.0ยฐ scale=0.410 residual=0.0334 unexplained= 4%
|
|
1380
|
+
found on a 113x51 anchor grid, step 2 at 4x reduction
|
|
1374
1381
|
```
|
|
1375
1382
|
|
|
1376
1383
|
โ **Check what is in `--images` before trusting a `pose` report on ingest work.** One
|
package/docs/MOTION.md
CHANGED
|
@@ -767,7 +767,7 @@ late, ยง3.8's overshoot is live; animation name โ **`raise`**.
|
|
|
767
767
|
|
|
768
768
|
### 1. The art, and the two pictures
|
|
769
769
|
|
|
770
|
-
```bash
|
|
770
|
+
```bash setup
|
|
771
771
|
mkdir -p semaphore/parts && cd semaphore
|
|
772
772
|
bun -e '
|
|
773
773
|
const files = {
|
|
@@ -806,15 +806,15 @@ rigc pose
|
|
|
806
806
|
.. frame โฆ/semaphore/poseA.png (160x200)
|
|
807
807
|
.. ground rgb(238, 238, 234) over 100% of the border ring
|
|
808
808
|
.. parts โฆ/semaphore/parts (3 png)
|
|
809
|
-
.. search scale 0.5โ2 in 7 step(s) ยท rotation -180ยฐโ180ยฐ step 15ยฐ ยท refuse above residual 0.25
|
|
810
|
-
PLACE arm.png x=
|
|
811
|
-
found on a 10x13 anchor grid, step 4 at 4x reduction
|
|
812
|
-
PLACE flag.png x= 104.7 y= 156.1 rot= 84.3ยฐ scale=0.955 residual=0.0135 unexplained= 0%
|
|
809
|
+
.. search scale 0.5โ2 in 7 step(s) ยท rotation -180ยฐโ180ยฐ in 24 step(s) of 15ยฐ ยท refuse above residual 0.25
|
|
810
|
+
PLACE arm.png x= 92.0 y= 122.8 rot= 61.9ยฐ scale=0.970 residual=0.0320 unexplained= 4%
|
|
813
811
|
found on a 20x25 anchor grid, step 4 at 2x reduction
|
|
814
|
-
|
|
815
|
-
found on a
|
|
816
|
-
|
|
817
|
-
|
|
812
|
+
PLACE flag.png x= 104.7 y= 156.1 rot= 84.3ยฐ scale=0.955 residual=0.0135 unexplained= 0%
|
|
813
|
+
found on a 40x50 anchor grid, step 2 at 2x reduction
|
|
814
|
+
AMBIG post.png x= 79.9 y= 149.0 rot= -0.1ยฐ scale=0.971 residual=0.0593 unexplained= 16%
|
|
815
|
+
found on a 14x17 anchor grid, step 6 at 2x reduction
|
|
816
|
+
alt 2: x= 79.0 y= 141.7 rot= -0.5ยฐ scale=0.691 residual=0.0687 unexplained= 25%
|
|
817
|
+
ambiguous: part is 14x96 px (span 93.2 frame px, opaque 1) with texture 0.0146, detail 1.406; 43 candidate(s), best 0.0593, next 0.0687, spread 0.0095 โ above the measured floor (AUTHORING ยง11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; either the frame holds more than one place this part fits, or every candidate missed the true one โ a narrower --scale or --rotation window around what you know of the part tells the two apart
|
|
818
818
|
```
|
|
819
819
|
|
|
820
820
|
โ ๏ธ **The post came back at scale 0.690 with two alternates trailing it down to 0.500 โ
|
|
@@ -830,17 +830,31 @@ rigc pose --images parts --frame poseB.png --scale 0.85,1.2 --out poseB.json
|
|
|
830
830
|
```
|
|
831
831
|
|
|
832
832
|
```
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
833
|
+
rigc pose
|
|
834
|
+
.. frame โฆ/semaphore/poseA.png (160x200)
|
|
835
|
+
.. ground rgb(238, 238, 234) over 100% of the border ring
|
|
836
|
+
.. parts โฆ/semaphore/parts (3 png)
|
|
837
|
+
.. search scale 0.85โ1.2 in 2 step(s) ยท rotation -180ยฐโ180ยฐ in 24 step(s) of 15ยฐ ยท refuse above residual 0.25
|
|
838
|
+
PLACE arm.png x= 91.9 y= 122.8 rot= 61.9ยฐ scale=0.970 residual=0.0320 unexplained= 4%
|
|
839
|
+
found on a 20x25 anchor grid, step 4 at 2x reduction
|
|
840
|
+
PLACE flag.png x= 104.7 y= 156.1 rot= 84.1ยฐ scale=0.956 residual=0.0135 unexplained= 0%
|
|
841
|
+
found on a 40x50 anchor grid, step 2 at 2x reduction
|
|
842
|
+
PLACE post.png x= 79.9 y= 148.5 rot= -0.1ยฐ scale=0.971 residual=0.0593 unexplained= 16%
|
|
843
|
+
found on a 14x17 anchor grid, step 6 at 2x reduction
|
|
837
844
|
```
|
|
838
845
|
|
|
839
846
|
```
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
847
|
+
rigc pose
|
|
848
|
+
.. frame โฆ/semaphore/poseB.png (160x200)
|
|
849
|
+
.. ground rgb(238, 238, 234) over 100% of the border ring
|
|
850
|
+
.. parts โฆ/semaphore/parts (3 png)
|
|
851
|
+
.. search scale 0.85โ1.2 in 2 step(s) ยท rotation -180ยฐโ180ยฐ in 24 step(s) of 15ยฐ ยท refuse above residual 0.25
|
|
852
|
+
PLACE arm.png x= 102.5 y= 94.8 rot= -18.3ยฐ scale=0.974 residual=0.0305 unexplained= 4%
|
|
853
|
+
found on a 20x25 anchor grid, step 4 at 2x reduction
|
|
854
|
+
PLACE flag.png x= 137.4 y= 86.0 rot= -6.6ยฐ scale=0.956 residual=0.0134 unexplained= 0%
|
|
855
|
+
found on a 40x50 anchor grid, step 2 at 2x reduction
|
|
843
856
|
PLACE post.png x= 80.0 y= 148.0 rot= 0.0ยฐ scale=0.995 residual=0.0394 unexplained= 9%
|
|
857
|
+
found on a 14x17 anchor grid, step 6 at 2x reduction
|
|
844
858
|
```
|
|
845
859
|
|
|
846
860
|
Three things to read out of that pair, none of which is a score:
|
|
@@ -1168,7 +1182,7 @@ skeleton โ AUTHORING ยง10.3 and PROMPTING clause 4 both price it. Keys are str
|
|
|
1168
1182
|
The bytes of `poseA.png` and `poseB.png`, so ยง6 runs end to end. Both are 160ร200 on a
|
|
1169
1183
|
flat ground; both are invented.
|
|
1170
1184
|
|
|
1171
|
-
```bash
|
|
1185
|
+
```bash setup
|
|
1172
1186
|
bun -e '
|
|
1173
1187
|
const frames = {
|
|
1174
1188
|
"poseA.png": "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",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
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,7 +38,7 @@ 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 { nearMisses } from './keys.ts';
|
|
41
|
+
import { dottedPath, nearMisses, refuseNumbersTheFileCannotCarry } from './keys.ts';
|
|
42
42
|
import { inEditorKeyOrder, withoutParserDefaults } from './keyorder.ts';
|
|
43
43
|
import { EVERY_GLOBAL_PHYSICS, parseMotionSpec } from './motion.ts';
|
|
44
44
|
import {
|
|
@@ -2030,6 +2030,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2030
2030
|
// and which refusals it deliberately leaves in this file.
|
|
2031
2031
|
const motion = parseMotionSpec(readJson<unknown>(motionPath), motionPath);
|
|
2032
2032
|
const manifest = manifestPath === null ? null : readJson<FaceManifest>(manifestPath);
|
|
2033
|
+
// The manifest is the record of what was measured, and its numbers become
|
|
2034
|
+
// bone positions and the stage: a `crop.h` of 1e309 built as `"height": null`
|
|
2035
|
+
// before issue #881. The rig spec's walk is `parseRigSpec`'s; this is the
|
|
2036
|
+
// same rule over the other file whose numbers reach the skeleton.
|
|
2037
|
+
if (manifest !== null && manifestPath !== null) refuseNumbersTheFileCannotCarry(manifest, manifestPath, dottedPath);
|
|
2033
2038
|
|
|
2034
2039
|
// The rig spec names its own path in every message `parseRigSpec` throws (its
|
|
2035
2040
|
// `where` argument, above). The motion spec gets no such treatment below โ
|
package/src/keys.ts
CHANGED
|
@@ -115,3 +115,90 @@ export function refuseUnknownKeys(
|
|
|
115
115
|
(known.length === 0 ? 'This shape has no fields at all.' : `Known here: ${[...known].sort().join(', ')}.`),
|
|
116
116
|
);
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The largest float32, `3.4028234663852886e38`. The skeleton file is written at
|
|
121
|
+
* float32 precision (`f32` in `src/compile.ts` rounds every emitted number), so
|
|
122
|
+
* this is the largest magnitude a stated number can have and still come out of
|
|
123
|
+
* the emitter as a number.
|
|
124
|
+
*/
|
|
125
|
+
export const FLOAT32_MAX = 3.4028234663852886e38;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Refuse the first number in an input file that the skeleton file cannot carry
|
|
129
|
+
* โ one that is not finite, or that is finite only as a double (issue #881).
|
|
130
|
+
*
|
|
131
|
+
* ๐จ The defect this closes was a green build. `JSON.parse` reads `1e309` as
|
|
132
|
+
* `Infinity`, the emitter wrote it, and `JSON.stringify(Infinity)` is `null`:
|
|
133
|
+
* a bone stated at `x: 1e309` built green on the overlay probe as `"x": null`
|
|
134
|
+
* and was read as 0, a value the spec never stated. `1e308` did exactly the same
|
|
135
|
+
* โ it is a finite double, but every emitted number is rounded to a float32 and
|
|
136
|
+
* `Math.fround(1e308)` is `Infinity` โ so the line is float32's range and not
|
|
137
|
+
* the double's: `3.4028234663852886e38` built green and was emitted as itself,
|
|
138
|
+
* the next double up that rounds past it (`3.4028235677973366e38`) was emitted
|
|
139
|
+
* as `null`. The predicate is therefore `Number.isFinite(Math.fround(n))`,
|
|
140
|
+
* which is `NaN`, both infinities and exactly that overflow.
|
|
141
|
+
*
|
|
142
|
+
* โญ It walks the file, not the emitter's route โ `checkRigSpecKeys`'s design
|
|
143
|
+
* and its reason. Before this, a handful of readers in `compile.ts` carried a
|
|
144
|
+
* finite check of their own (vertex arrays, a path's lengths, a segments
|
|
145
|
+
* falloff, a slider's mapping, an event's payload), each right about its own
|
|
146
|
+
* field, and none of them was a rule. Measured when this was written, planting
|
|
147
|
+
* `1e309` at every numeric leaf of nine rig specs โ 610 plants over 57 kinds
|
|
148
|
+
* of field: 19 kinds were refused by a rule of their own (30 of those 70
|
|
149
|
+
* refusals printing the Infinity as `null`, and some only by a later symptom
|
|
150
|
+
* such as *hull Infinity disagrees with the triangles*), 1 was caught only by
|
|
151
|
+
* the gate, and 37 built green with a `null` in them on at least one rig. One
|
|
152
|
+
* pass over every number the document holds cannot miss a reader added next
|
|
153
|
+
* month, and it runs before any of them โ so a range rule further down (a
|
|
154
|
+
* radius that is positive, a weight in 0..1) never has to print `NaN`.
|
|
155
|
+
*
|
|
156
|
+
* ๐ The report order is fixed: `Object.keys` of parsed JSON is the
|
|
157
|
+
* document's key order (integer-like keys first, as the language orders them)
|
|
158
|
+
* and arrays are visited by index, so the refusal names the same number on
|
|
159
|
+
* every run.
|
|
160
|
+
*
|
|
161
|
+
* โ ๏ธ What it cannot see is a number spelled as something else. `"x": "NaN"`
|
|
162
|
+
* is a string; there is no number here to refuse, and the reader downstream
|
|
163
|
+
* that coerces it is a type question this walk does not answer.
|
|
164
|
+
*
|
|
165
|
+
* `place` turns a path into the words the refusal uses for it.
|
|
166
|
+
*/
|
|
167
|
+
export function refuseNumbersTheFileCannotCarry(
|
|
168
|
+
raw: unknown,
|
|
169
|
+
where: string,
|
|
170
|
+
place: (path: ReadonlyArray<string | number>) => string,
|
|
171
|
+
): void {
|
|
172
|
+
const visit = (node: unknown, path: Array<string | number>): void => {
|
|
173
|
+
if (typeof node === 'number') {
|
|
174
|
+
if (Number.isFinite(Math.fround(node))) return;
|
|
175
|
+
const why = Number.isNaN(node)
|
|
176
|
+
? ''
|
|
177
|
+
: Number.isFinite(node)
|
|
178
|
+
? ' This one is finite as a double and has no float32 but Infinity.'
|
|
179
|
+
: ' JSON has no spelling for Infinity: a literal past ยฑ1.7976931348623157e308, such as 1e309, parses to it.';
|
|
180
|
+
throw new CompileError(
|
|
181
|
+
`${where}: ${place(path)} is ${String(node)}; a number in this file is finite at float32 precision, at most ` +
|
|
182
|
+
`ยฑ${String(FLOAT32_MAX)}, because the skeleton is written as float32 โ past that it would be emitted as ` +
|
|
183
|
+
`Infinity, which JSON writes as null, a value the file never stated.${why}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(node)) {
|
|
187
|
+
node.forEach((child, i) => visit(child, [...path, i]));
|
|
188
|
+
} else if (node !== null && typeof node === 'object') {
|
|
189
|
+
const record = node as Record<string, unknown>;
|
|
190
|
+
for (const key of Object.keys(record)) visit(record[key], [...path, key]);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
visit(raw, []);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** A path as the file spells it: `.key`, `."odd key"`, `[3]`. */
|
|
197
|
+
export function dottedPath(path: ReadonlyArray<string | number>): string {
|
|
198
|
+
let out = '';
|
|
199
|
+
for (const step of path) {
|
|
200
|
+
if (typeof step === 'number') out += `[${step}]`;
|
|
201
|
+
else out += /^[A-Za-z_][A-Za-z0-9_]*$/.test(step) ? `${out === '' ? '' : '.'}${step}` : `${out === '' ? '' : '.'}${JSON.stringify(step)}`;
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
package/src/motion.ts
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* [`keys.ts`](keys.ts) shared with the rig parser.
|
|
55
55
|
*/
|
|
56
56
|
import { CompileError } from './errors.ts';
|
|
57
|
-
import { refuseUnknownKeys } from './keys.ts';
|
|
57
|
+
import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys } from './keys.ts';
|
|
58
58
|
import { SEQUENCE_MODES } from './timelines.ts';
|
|
59
59
|
import type { MotionSpec } from './types.ts';
|
|
60
60
|
|
|
@@ -665,5 +665,11 @@ export function parseMotionSpec(raw: unknown, where: string): MotionSpec {
|
|
|
665
665
|
parseAnimation(anim, where, name);
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
+
// Last, so every field check above keeps its own sentence for a number that
|
|
669
|
+
// is not finite; what reaches this line is a finite double the float32 file
|
|
670
|
+
// cannot carry โ a key at 1e308 built green with a `null` in it before issue
|
|
671
|
+
// #881 โ and any number in a field no check above reads.
|
|
672
|
+
refuseNumbersTheFileCannotCarry(raw, where, (path) => `\`${dottedPath(path)}\``);
|
|
673
|
+
|
|
668
674
|
return raw as unknown as MotionSpec;
|
|
669
675
|
}
|
package/src/pose.ts
CHANGED
|
@@ -274,8 +274,9 @@ const MINIMA_PER_SCALE = 5;
|
|
|
274
274
|
const REFINE_CANDIDATES = 15;
|
|
275
275
|
|
|
276
276
|
/**
|
|
277
|
-
* The
|
|
278
|
-
* stops โ half a coarse scale rung, the resolution the coarse ladder itself had
|
|
277
|
+
* The factors a converged full-resolution polish tries its scale up by before it
|
|
278
|
+
* stops โ half a coarse scale rung, the resolution the coarse ladder itself had,
|
|
279
|
+
* and a quarter of one (issue #886); the better of the two re-fits wins.
|
|
279
280
|
*
|
|
280
281
|
* ๐ What the pattern search cannot do, and the trace table of issue #877 is
|
|
281
282
|
* how it was seen: every probe moves ONE degree of freedom, and the objective
|
|
@@ -306,8 +307,32 @@ const REFINE_CANDIDATES = 15;
|
|
|
306
307
|
* margin, and the blurred 24 px front shin (walk) now places AT its truth
|
|
307
308
|
* (0.03 px, 0.00989) where it placed 0.57 px off at 0.0191 โ the old answer is
|
|
308
309
|
* still reported beside it, 0.0094 above, inside the 0.01 absolute margin.
|
|
310
|
+
*
|
|
311
|
+
* ๐ The quarter rung is issue #886, and the fixed-scale profile is what
|
|
312
|
+
* named it. MOTION.md ยง6's post (14x96, pose A, `--scale 0.85,1.2`) came back
|
|
313
|
+
* at scale 0.917 and residual 0.0649 where the page had recorded 0.975 at
|
|
314
|
+
* 0.0589 โ the old placement still scores 0.0589 on this objective, so the
|
|
315
|
+
* search missed it. The trace: the coarse cell nearest the truth (2.4 px off
|
|
316
|
+
* it, at the 2x level) kept a half-turned post, its polish ended 6.7 px from
|
|
317
|
+
* the truth, the full-resolution rotation re-grid set it upright there, and
|
|
318
|
+
* the polish climbed the scaleโposition valley to 0.917.
|
|
319
|
+
* The half-rung escape then tried 1.029: best position there 0.0980, worse
|
|
320
|
+
* than 0.0649, so it declined โ while the best position at every scale
|
|
321
|
+
* between 0.94 and 1.00 scores 0.0619โ0.0589. A part whose material fills its
|
|
322
|
+
* image pays for every pixel pushed past the figure, so its profile above the
|
|
323
|
+
* truth is a cliff, and half a rung stepped over the basin onto it. A quarter
|
|
324
|
+
* rung lands at 0.971 (0.0593, 0.1 px off the page's placement).
|
|
325
|
+
*
|
|
326
|
+
* ๐ Both, not the quarter alone, and measured on the grid rather than argued:
|
|
327
|
+
* the quarter alone found 234 (3 gained, 6 lost โ the native arms #877 was
|
|
328
|
+
* made for need the half); half then quarter, the quarter tried only when the
|
|
329
|
+
* half declines, 237 (1, 1); both, the better kept, 238 (2 gained, 1 lost) and
|
|
330
|
+
* misses 8 โ 6, for 16% more user CPU over the grid. The one it loses is a
|
|
331
|
+
* margin reading of the kind named above: the blurred 32 px front shin (walk)
|
|
332
|
+
* now reports its truth as its BEST (0.07 px, 0.00968) with the old 0.51 px
|
|
333
|
+
* answer 0.0045 above it, inside the 0.01 absolute margin.
|
|
309
334
|
*/
|
|
310
|
-
const
|
|
335
|
+
const POLISH_SCALE_ESCAPES = [2 ** (1 / (4 * SCALE_STEPS_PER_OCTAVE)), 2 ** (1 / (2 * SCALE_STEPS_PER_OCTAVE))];
|
|
311
336
|
|
|
312
337
|
/** Sample budgets per stage. The reported residual uses every pixel regardless. */
|
|
313
338
|
const COARSE_SAMPLES = 96;
|
|
@@ -347,8 +372,8 @@ export const POSE_FLOOR = {
|
|
|
347
372
|
span: 24,
|
|
348
373
|
detail: 0.5,
|
|
349
374
|
/** Found share of the grid's trials under `detail`, and over it โ both measured by `deriveFloor`. */
|
|
350
|
-
rateBelow: 0.
|
|
351
|
-
rateAbove: 0.
|
|
375
|
+
rateBelow: 0.066,
|
|
376
|
+
rateAbove: 0.772,
|
|
352
377
|
};
|
|
353
378
|
|
|
354
379
|
/** Which side of `POSE_FLOOR` a part is on, from its own longest side in part pixels and its detail. */
|
|
@@ -1934,7 +1959,7 @@ function placePart(
|
|
|
1934
1959
|
}
|
|
1935
1960
|
const paths = seeds.map((): { start: number; steps: PoseTraceStep[] } => ({ start: 0, steps: [] }));
|
|
1936
1961
|
candidates = seeds.map((seed, i) =>
|
|
1937
|
-
polish(level, plate, s, seed, step, floor, smooth, scaleBounds, rotationBounds, li === 0 ?
|
|
1962
|
+
polish(level, plate, s, seed, step, floor, smooth, scaleBounds, rotationBounds, li === 0 ? POLISH_SCALE_ESCAPES : [], trace === undefined ? undefined : paths[i]),
|
|
1938
1963
|
);
|
|
1939
1964
|
const polished = candidates.slice();
|
|
1940
1965
|
candidates.sort((a, b) => a.residual - b.residual);
|
package/src/render.ts
CHANGED
|
@@ -1288,6 +1288,38 @@ function firstNonFinite(where: string, entries: readonly PosedVertices[], bones:
|
|
|
1288
1288
|
return null;
|
|
1289
1289
|
}
|
|
1290
1290
|
|
|
1291
|
+
/**
|
|
1292
|
+
* The same sentence for one skeleton as it stands posed now โ its bones, then
|
|
1293
|
+
* the vertices of every region and mesh its slots show, in draw order โ or
|
|
1294
|
+
* `null` when every one of those numbers is finite.
|
|
1295
|
+
*
|
|
1296
|
+
* โญ This is how `A10_NO_NAN_AFTER_STEPPING` reads each pose it steps (issue
|
|
1297
|
+
* #882), so the gate and the renderer hold one definition of "not finite": the
|
|
1298
|
+
* six terms `firstNonFinite` reads off a bone, and the vertices the runtime
|
|
1299
|
+
* computes from them. Before it A10 read the world POSITION alone, and a bone at
|
|
1300
|
+
* `rotation: 1e309` โ finite position, NaN `a`, `b`, `c`, `d` โ was gated green
|
|
1301
|
+
* and then refused here. The vertices are read too because a bone can be finite
|
|
1302
|
+
* and still carry one that is not: a two-bone `scaleX` chain of 1e154 ร 1e154
|
|
1303
|
+
* leaves the child's `a` at 1e308, finite, and every vertex of its attachment
|
|
1304
|
+
* past the largest double. Only computed once every bone is finite, so a
|
|
1305
|
+
* broken bone is named as the cause rather than through its vertices.
|
|
1306
|
+
*/
|
|
1307
|
+
export function nonFiniteOfPosed(where: string, skeleton: Skeleton): string | null {
|
|
1308
|
+
const bones: WorldTransform[] = skeleton.bones.map((bone) => {
|
|
1309
|
+
const { a, b, c, d, worldX, worldY } = bone.appliedPose;
|
|
1310
|
+
return { name: bone.data.name, a, b, c, d, worldX, worldY };
|
|
1311
|
+
});
|
|
1312
|
+
const found = firstNonFinite(where, [], bones);
|
|
1313
|
+
if (found !== null) return found;
|
|
1314
|
+
const entries: PosedVertices[] = [];
|
|
1315
|
+
for (const slot of skeleton.drawOrder.appliedPose) {
|
|
1316
|
+
const attachment = slot.appliedPose.attachment;
|
|
1317
|
+
if (!(attachment instanceof MeshAttachment) && !(attachment instanceof RegionAttachment)) continue;
|
|
1318
|
+
entries.push({ slot: slot.data.name, attachment: attachment.name, vertices: worldVerticesOf(skeleton, slot, attachment) });
|
|
1319
|
+
}
|
|
1320
|
+
return firstNonFinite(where, entries, []);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1291
1323
|
/**
|
|
1292
1324
|
* How a sampled frame is named in that sentence: the animation, the frame's
|
|
1293
1325
|
* index at the rate it was sampled and its time โ or the bare index for the one
|
package/src/rig.ts
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
* with no manifest at all declares them here.
|
|
61
61
|
*/
|
|
62
62
|
import { CompileError, NotImplementedError } from './errors.ts';
|
|
63
|
-
import { refuseUnknownKeys } from './keys.ts';
|
|
63
|
+
import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys } from './keys.ts';
|
|
64
64
|
|
|
65
65
|
export { CompileError, NotImplementedError };
|
|
66
66
|
|
|
@@ -1925,6 +1925,10 @@ function checkRigSequence(att: Record<string, unknown>, who: string, where: stri
|
|
|
1925
1925
|
const whole = (field: string, min: number): void => {
|
|
1926
1926
|
const value = seq[field];
|
|
1927
1927
|
if (value === undefined) return;
|
|
1928
|
+
// A number the file cannot carry is `refuseNumbersTheFileCannotCarry`'s,
|
|
1929
|
+
// one sentence for the whole family. This check runs inside the key scan,
|
|
1930
|
+
// before that walk, and printed a stated 1e309 as `null` (issue #881).
|
|
1931
|
+
if (typeof value === 'number' && !Number.isFinite(Math.fround(value))) return;
|
|
1928
1932
|
if (typeof value !== 'number' || !Number.isInteger(value) || value < min) {
|
|
1929
1933
|
throw new CompileError(
|
|
1930
1934
|
`${where}: ${at}.${field} is ${JSON.stringify(value) ?? String(value)}; it is a whole number` +
|
|
@@ -1946,7 +1950,7 @@ function checkRigSequence(att: Record<string, unknown>, who: string, where: stri
|
|
|
1946
1950
|
whole('digits', 0);
|
|
1947
1951
|
whole('setup', 0);
|
|
1948
1952
|
const count = seq.count as number;
|
|
1949
|
-
if (typeof seq.setup === 'number' && seq.setup >= count) {
|
|
1953
|
+
if (typeof seq.setup === 'number' && Number.isFinite(Math.fround(seq.setup)) && Number.isFinite(Math.fround(count)) && seq.setup >= count) {
|
|
1950
1954
|
throw new CompileError(
|
|
1951
1955
|
`${where}: ${at}.setup is ${seq.setup}, and a ${count}-frame series has frames 0 to ${count - 1}. ` +
|
|
1952
1956
|
'`Sequence.resolveIndex` clamps an index at or past the end to the LAST frame (measured: `setup: 7` on ' +
|
|
@@ -2100,6 +2104,42 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
|
|
|
2100
2104
|
}
|
|
2101
2105
|
}
|
|
2102
2106
|
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* A rig-spec path in the words the other refusals in this file use for it:
|
|
2110
|
+
* `bone "hip" x`, `constraint "aim" mixRotate`, `skin "default" slot "tail"
|
|
2111
|
+
* attachment "tail" weights[0][1].x`, `event "step" float`. Anything else is
|
|
2112
|
+
* the dotted path, which names every number exactly.
|
|
2113
|
+
*/
|
|
2114
|
+
function rigPlace(raw: Record<string, unknown>): (path: ReadonlyArray<string | number>) => string {
|
|
2115
|
+
const named = (list: unknown, i: number, kind: string, plural: string): string => {
|
|
2116
|
+
const node = Array.isArray(list) ? list[i] : undefined;
|
|
2117
|
+
return isObj(node) && typeof node.name === 'string' ? `${kind} ${JSON.stringify(node.name)}` : `${plural}[${i}]`;
|
|
2118
|
+
};
|
|
2119
|
+
const rest = (path: ReadonlyArray<string | number>): string => {
|
|
2120
|
+
const tail = dottedPath(path);
|
|
2121
|
+
return tail === '' ? '' : ` ${tail}`;
|
|
2122
|
+
};
|
|
2123
|
+
return (path) => {
|
|
2124
|
+
const [head, second] = path;
|
|
2125
|
+
if (typeof second === 'number') {
|
|
2126
|
+
if (head === 'bones') return `${named(raw.bones, second, 'bone', 'bones')}${rest(path.slice(2))}`;
|
|
2127
|
+
if (head === 'slots') return `${named(raw.slots, second, 'slot', 'slots')}${rest(path.slice(2))}`;
|
|
2128
|
+
if (head === 'constraints') return `${named(raw.constraints, second, 'constraint', 'constraints')}${rest(path.slice(2))}`;
|
|
2129
|
+
}
|
|
2130
|
+
if (head === 'events' && typeof second === 'string') return `event ${JSON.stringify(second)}${rest(path.slice(2))}`;
|
|
2131
|
+
if (head === 'skins' && typeof second === 'string') {
|
|
2132
|
+
const inner = path[2] === 'attachments' ? path.slice(3) : path.slice(2);
|
|
2133
|
+
const [slot, attachment] = inner;
|
|
2134
|
+
if (typeof slot === 'string' && typeof attachment === 'string') {
|
|
2135
|
+
return `skin ${JSON.stringify(second)} slot ${JSON.stringify(slot)} attachment ${JSON.stringify(attachment)}${rest(inner.slice(2))}`;
|
|
2136
|
+
}
|
|
2137
|
+
return `skin ${JSON.stringify(second)}${rest(path.slice(2))}`;
|
|
2138
|
+
}
|
|
2139
|
+
return dottedPath(path);
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2103
2143
|
/**
|
|
2104
2144
|
* Parse and check the envelope, then hand back a typed spec.
|
|
2105
2145
|
*
|
|
@@ -2137,6 +2177,10 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
|
2137
2177
|
// throws directly below are the required-key checks, and each of them is the
|
|
2138
2178
|
// consequence a typo at the root produces.
|
|
2139
2179
|
checkRigSpecKeys(raw, where);
|
|
2180
|
+
// After the key check, so a misspelled key is named as a misspelling before
|
|
2181
|
+
// its value is judged, and before every reader below, so no range rule or
|
|
2182
|
+
// derived number ever meets a value the file cannot carry (issue #881).
|
|
2183
|
+
refuseNumbersTheFileCannotCarry(raw, where, rigPlace(raw));
|
|
2140
2184
|
|
|
2141
2185
|
if (typeof raw.name !== 'string' || raw.name.length === 0) {
|
|
2142
2186
|
throw new CompileError(`${where}: a rig spec needs a "name" โ a motion spec names it to pick this rig`);
|
package/src/validate.ts
CHANGED
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
type SpineGeneration,
|
|
83
83
|
} from './generation.ts';
|
|
84
84
|
import { colourTypeName, readPngHeader } from './png.ts';
|
|
85
|
+
import { nonFiniteOfPosed } from './render.ts';
|
|
85
86
|
import { BONE_INHERIT_KNOWN } from './rig.ts';
|
|
86
87
|
import {
|
|
87
88
|
CHANNELS_BY_KIND,
|
|
@@ -4400,6 +4401,26 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
4400
4401
|
}
|
|
4401
4402
|
if (unresolved > 0) return;
|
|
4402
4403
|
|
|
4404
|
+
// -- the whole world transform, at the setup pose and every stepped frame
|
|
4405
|
+
//
|
|
4406
|
+
// ๐จ Read through `nonFiniteOfPosed`, the scan `render` refuses on, and
|
|
4407
|
+
// not a second opinion of it (issue #882). This loop read `worldX` and
|
|
4408
|
+
// `worldY` alone until then, and a bone at `rotation: 1e309` โ in its setup
|
|
4409
|
+
// pose or as a `rotate` key โ poses a finite position over a NaN `a`, `b`,
|
|
4410
|
+
// `c` and `d` whenever it has no child offset from it: measured on every
|
|
4411
|
+
// leaf bone of the three generated probes, 20 of 20 green through the whole
|
|
4412
|
+
// gate, and `render` then refused the same file by the bone. So the gate
|
|
4413
|
+
// said the skeleton was fine and the renderer said it was not, which is two
|
|
4414
|
+
// definitions of one word. The scan reads the six terms of every bone, then
|
|
4415
|
+
// the vertices of every region and mesh shown โ because a bone can be
|
|
4416
|
+
// finite over a vertex that is not (a `scaleX` chain whose product stays
|
|
4417
|
+
// under the largest double while every corner of the child's region
|
|
4418
|
+
// passes it) โ and names the bone or the vertex, the term and the frame.
|
|
4419
|
+
//
|
|
4420
|
+
// โ ๏ธ The setup pose is its own surface and is read as such: every frame
|
|
4421
|
+
// below is posed AFTER `state.apply`, so a bone the animation keys from
|
|
4422
|
+
// t=0 never shows its setup value to the loop, and a runtime that shows
|
|
4423
|
+
// the rig at rest does show it.
|
|
4403
4424
|
for (const anim of data.animations) {
|
|
4404
4425
|
const skeleton = new Skeleton(data);
|
|
4405
4426
|
const state = new AnimationState(new AnimationStateData(data));
|
|
@@ -4407,18 +4428,27 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
4407
4428
|
skeleton.setupPose();
|
|
4408
4429
|
skeleton.update(0);
|
|
4409
4430
|
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
|
+
}
|
|
4410
4436
|
const step = Math.max(anim.duration, 1) / STEP_FRAMES;
|
|
4411
4437
|
for (let i = 0; i < STEP_FRAMES; i++) {
|
|
4412
4438
|
state.update(step);
|
|
4413
4439
|
state.apply(skeleton);
|
|
4414
4440
|
skeleton.update(step);
|
|
4415
4441
|
skeleton.updateWorldTransform(Physics.update);
|
|
4442
|
+
const found = nonFiniteOfPosed(
|
|
4443
|
+
`animation ${JSON.stringify(anim.name)} frame ${i + 1} of ${STEP_FRAMES} (t=${((i + 1) * step).toFixed(4)}s)`,
|
|
4444
|
+
skeleton,
|
|
4445
|
+
);
|
|
4446
|
+
if (found !== null) {
|
|
4447
|
+
fail('A10_NO_NAN_AFTER_STEPPING', found);
|
|
4448
|
+
return;
|
|
4449
|
+
}
|
|
4416
4450
|
for (const bone of skeleton.bones) {
|
|
4417
4451
|
const pose = bone.appliedPose;
|
|
4418
|
-
if (!Number.isFinite(pose.worldX) || !Number.isFinite(pose.worldY)) {
|
|
4419
|
-
fail('A10_NO_NAN_AFTER_STEPPING', `${anim.name}: bone "${bone.data.name}" world is (${pose.worldX}, ${pose.worldY})`);
|
|
4420
|
-
return;
|
|
4421
|
-
}
|
|
4422
4452
|
// The setup half of the clause above: a bone's own `inherit` goes
|
|
4423
4453
|
// through the same lookup, and a miss there loads `undefined` into
|
|
4424
4454
|
// the setup pose, which every frame copies. Only reachable on a
|