spine-rigc 2.1.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/docs/AUTHORING.md +69 -22
- package/docs/INGEST.md +44 -25
- package/package.json +1 -1
- package/src/assertions/emitted/index.ts +28 -1
- package/src/assertions/facts/stage.ts +6 -3
- package/src/assertions/model/index.ts +41 -1
- package/src/cli/core_commands.ts +15 -8
- package/src/cli/shared.ts +70 -3
- package/src/cli/spine_commands.ts +13 -7
- package/src/compile.ts +100 -4
- package/src/core/raw.ts +101 -1
- package/src/diff.ts +71 -65
- package/src/emit_spine.ts +26 -12
- package/src/ingest.ts +38 -9
- package/src/model.ts +14 -10
- package/src/rig.ts +9 -2
- package/src/types.ts +4 -2
- package/src/validate.ts +41 -7
- package/tools/editor_roundtrip.ts +1 -1
package/README.md
CHANGED
|
@@ -622,7 +622,10 @@ example corpus, ingested, rebuilt through the pack beside it and `diff`ed agains
|
|
|
622
622
|
source, comes back **12 of 12, no blockers, 1.000 on every measure the report
|
|
623
623
|
carries** — and each rebuild is its export's own text in canonical form, apart from
|
|
624
624
|
the header's `hash` and `spine`: the editor's project hash and the runtime version
|
|
625
|
-
rigc stamps, the two keys the rig spec has no field for by design (12 of 12)
|
|
625
|
+
rigc stamps, the two keys the rig spec has no field for by design (12 of 12) — and
|
|
626
|
+
the header's box, which `build` computes rather than carries (issue #907): the rebuild
|
|
627
|
+
writes spine-core's `getBounds` over the export on the header's 1e-6 grid at float32, equal on all twelve,
|
|
628
|
+
where the editor wrote its own arithmetic, up to 0.0071 units away.
|
|
626
629
|
[INGEST.md §2.3](docs/INGEST.md) states that pass line and why those two are the
|
|
627
630
|
exceptions.
|
|
628
631
|
|
|
@@ -643,7 +646,10 @@ specs are still written — a spec plus a list of what is missing from it beats
|
|
|
643
646
|
too**, for the opposite reason: two sources for one value, where the file is the record
|
|
644
647
|
of what was measured. **All twelve exports in the example corpus carry a stage** and
|
|
645
648
|
none of them needs the flag. It is the value that costs least to get wrong, because
|
|
646
|
-
`diff` reports the box and gates nothing on it.
|
|
649
|
+
`diff` reports the box and gates nothing on it. 🔁 The box an export's header carries
|
|
650
|
+
is its setup-pose bounding box, and it becomes the rebuild's *stage*; the rebuild's own
|
|
651
|
+
header is computed again, because since issue #907 `build` writes the setup-pose
|
|
652
|
+
bounding box there — never the stage — held to spine-core's `getBounds`.
|
|
647
653
|
- **An animation's duration.** The format has no such field. The largest key time is
|
|
648
654
|
the only derivable answer and it is what a runtime plays to; it is wrong for an
|
|
649
655
|
animation that holds its last pose past its last key, so it is recorded as a finding
|
package/docs/AUTHORING.md
CHANGED
|
@@ -947,7 +947,7 @@ repository builds on every run.
|
|
|
947
947
|
| `--art loose` (default) | name an `image` per attachment — `<path or placeholder>.png` — so the rebuild resolves loose PNGs and rigc measures them |
|
|
948
948
|
| `--art none` | state `width`/`height` only, so the rebuild is `build --atlas-in <pack.atlas>` and every part resolves out of the pack |
|
|
949
949
|
| `--images <dir>` | **write** the rig spec's own `images` directory, spelled relative to `--out`, so the rebuild is a plain `build --rig … --motion … --out …`. Without it the field is left out and every `image` resolves against `--out` itself, which holds the specs and no art — so every rebuild has to repeat `build --images <dir>`. Refused together with `--art none`, which writes no `image` for it to be the base of |
|
|
950
|
-
| `--stage x,y,w,h` | a
|
|
950
|
+
| `--stage x,y,w,h` | a stage to **add** to a skeleton whose header carries no box (a header's box — its setup-pose bounding box — is otherwise read as the rebuild's stage) — without it the absence is carried as `"width": null, "height": null`. An editor export *may* be such a file; every export under `examples/` carries a box and `ingest` reads it straight through — so passing the flag at one of them is **refused**, naming both boxes, rather than silently doing nothing |
|
|
951
951
|
| `--name <n>` | the rig spec's `name`, which the motion spec's `archetype` must equal (default: the file's basename) |
|
|
952
952
|
|
|
953
953
|
⚠️ **`ingest --images` and `build --images` point opposite ways.** `build --images`
|
|
@@ -968,23 +968,40 @@ supply it from the project the file came from, or from the editor's own canvas
|
|
|
968
968
|
is recorded as a `NO_STAGE` judgement. A header stating **half** a stage (an origin
|
|
969
969
|
with no extent) is a `NO_STAGE` blocker: the spec holds a stage as four fields or none.
|
|
970
970
|
|
|
971
|
+
🔁 **Where the rebuild's stage comes from** (issue #907). A **rigc build**'s stage is read
|
|
972
|
+
from the `skeleton.model.json` beside it, when that document's `spine.sha256` is the
|
|
973
|
+
skeleton's digest (the run prints `stage … read from the skeleton.model.json beside it`) —
|
|
974
|
+
its header is the setup-pose bounding box, not the stage. An **export**'s stage is its
|
|
975
|
+
header box. A file's header box is its setup-pose bounding box, and for an export `ingest`
|
|
976
|
+
writes it into the spec as the rebuild's **stage** — the only box an export carries, so `A14` and `A19` on the rebuild measure against it, as
|
|
977
|
+
they did on the export. The rebuild's own header is then not carried but computed:
|
|
978
|
+
`build` writes the bounding box of what it drew, which is `getBounds` of the source at
|
|
979
|
+
float32 when the rebuild draws the source's vertices — on the twelve example exports it
|
|
980
|
+
does, to the bit — and the editor's numbers are its own arithmetic, up to 0.0071 units
|
|
981
|
+
away. So the four header numbers of a rebuild are not the export's spelling, and that
|
|
982
|
+
is not a loss: the export's box is in the spec, as the stage. (A licensed 4.3.26
|
|
983
|
+
editor's command-line import and export carried a header box through **verbatim** —
|
|
984
|
+
a forged `0, 0, 1, 1` came back as `width 1, height 1` — so the editor does not
|
|
985
|
+
recompute it on a round trip either; whatever box a file states travels.)
|
|
986
|
+
|
|
971
987
|
⛔ **The flag is refused beside a box the file states.** Two sources for one value, and
|
|
972
988
|
the file is the one that was measured — so `ingest` names both boxes and stops rather
|
|
973
989
|
than writing one of them and saying nothing. Drop the flag, or correct `skeleton` in the
|
|
974
990
|
source if its box is wrong. What it does **not** do is refuse an *omitted origin*:
|
|
975
991
|
inside a declared extent an omitted `x`/`y` is `0` — the reading `build` emits and
|
|
976
992
|
`diff` compares — so the written spec states it and a `LOSS HEADER_ORIGIN` line says
|
|
977
|
-
the source omitted it and that the
|
|
993
|
+
the source omitted it and that the rebuilt header will spell an origin — the bounding
|
|
994
|
+
box's, wherever the art sits.
|
|
978
995
|
|
|
979
996
|
⚠️ **All twelve exports under `examples/` declare `x`, `y`, `width` and `height`**,
|
|
980
|
-
`ingest` reads each box straight through, and not one needs the flag. What an
|
|
997
|
+
`ingest` reads each box straight through into the stage, and not one needs the flag. What an
|
|
981
998
|
editor export *may* do is carry none: a rigc build that declares no stage came back
|
|
982
999
|
from a Spine 4.3.26 round trip with a header of `hash`, `spine`, `images`, `audio`
|
|
983
1000
|
and **no box at all** — the editor preserves the absence rather than inventing a
|
|
984
1001
|
stage. So `--stage` is for a file that really has none; the example corpus holds no
|
|
985
1002
|
such file, and production exports do — 48 of 48 measured. It is the value that
|
|
986
|
-
costs least to get wrong: `diff` reports the box as `
|
|
987
|
-
`
|
|
1003
|
+
costs least to get wrong: `diff` reports the header's box as `bounds_present` and
|
|
1004
|
+
`bounds_box` and both are `(reported)`, so nothing on the ladder consults them.
|
|
988
1005
|
|
|
989
1006
|
⚠️ **The duration is a convention, and it is recorded as one.** Skeleton JSON has no
|
|
990
1007
|
duration field. The largest key time is the only derivable answer and it is what a
|
|
@@ -1530,8 +1547,8 @@ that is a defect in this guide: report it.
|
|
|
1530
1547
|
|
|
1531
1548
|
| Field | Spine meaning | Default |
|
|
1532
1549
|
| --- | --- | --- |
|
|
1533
|
-
| `x`, `y` |
|
|
1534
|
-
| `width`, `height` |
|
|
1550
|
+
| `x`, `y` | the **stage**'s origin — rigc's own field: see below | `0` — and refused outright beside a stated absence, below |
|
|
1551
|
+
| `width`, `height` | the **stage**'s size, **or both `null` for "this skeleton declares no stage"** | falls back to the manifest's crop; **with neither the number nor the `null`, the compile fails** |
|
|
1535
1552
|
| `fps` | nonessential editor hint | `SkeletonData.fps` stays 30 |
|
|
1536
1553
|
| `referenceScale` | 4.2+ physics/scale reference: a physics constraint's `wind` and `gravity` act over it | parser default 100. Carried as stated — never rounded — and the header leaves it out at exactly 100. `skeleton.model.json` always states it, as `referenceScale` after `spec`: the number stated, or 100 when none is — the value the Spine file is read as — and rigc's posing core steps wind and gravity over it (issue #958) |
|
|
1537
1554
|
| `images` | where the editor's import looks for the part PNGs, as a path from the skeleton file | **written for you**: under `--copy-images` the `--out` directory itself, spelled `../<its basename>/` (a literal `./` is dropped by the editor on import; a named directory is kept and every part is found — measured on 4.3.23); otherwise the relative path from `--out` to the one directory the spec names every part PNG in (the rig's images directory, or the manifest's plates). A declared value is carried through verbatim — and overridden by `--copy-images`, which moved the parts. Parts spread over several directories have no single true path, so nothing is written |
|
|
@@ -1547,6 +1564,33 @@ links, not the editor that will open the file, and the warning is harmless.
|
|
|
1547
1564
|
`width`/`height` are what `A14` and `A19` measure against, so a guessed stage is a
|
|
1548
1565
|
gate measuring against a number nobody wrote down.
|
|
1549
1566
|
|
|
1567
|
+
🔁 **The stage is not what `skeleton.json`'s header carries** (issue #907). In the
|
|
1568
|
+
Spine format the header's `x`, `y`, `width`, `height` are the **setup-pose bounding
|
|
1569
|
+
box** — the box around the attachments at rest, which a player reads to scale and lay
|
|
1570
|
+
the figure out. rigc's stage is something else: the working area the art was painted
|
|
1571
|
+
in (a manifest's crop), the frame the coordinate transform flips against, and what
|
|
1572
|
+
`A14`/`A19` measure against. Until issue #907 `build` copied the stage into the header,
|
|
1573
|
+
so a consumer reading the file the way the format says got the crop — 3.18 times the
|
|
1574
|
+
figure's area on a figure that fills a quarter of its crop. Now:
|
|
1575
|
+
|
|
1576
|
+
- **the header** carries the setup-pose bounding box, which `build` computes with
|
|
1577
|
+
rigc's own core: every region and mesh the default skin shows at the setup pose, no
|
|
1578
|
+
skin set, every constraint applied, on active bones only — clipping polygons,
|
|
1579
|
+
bounding boxes, paths and points not counted — `x`/`y` the least corner (bottom-left,
|
|
1580
|
+
y up), each of the four on the model's 1e-6 grid at float32 (a box edge at the origin
|
|
1581
|
+
is otherwise a cancellation residue such as `1.1368684e-13` that follows the platform's
|
|
1582
|
+
libm to the last bit). It is held to spine-core's
|
|
1583
|
+
`Skeleton.getBounds()` at tolerance 0 (`tools/core_gate.ts`, its `BOUNDS` rows), so
|
|
1584
|
+
both entries write the same bytes;
|
|
1585
|
+
- **the stage** stays the rig spec's (or the crop), and `skeleton.model.json` states it
|
|
1586
|
+
as `stage` — `A14`, `A19` and `explain` read it there, on `build` and on `validate`
|
|
1587
|
+
over a directory whose model document is the skeleton's own;
|
|
1588
|
+
- **no box** is written where the rig declares no stage (`null`, below), where the
|
|
1589
|
+
setup pose draws nothing, or where a region has no atlas rectangle (that build is
|
|
1590
|
+
refused by `A08` anyway).
|
|
1591
|
+
|
|
1592
|
+
You do not author the box and there is no field for it: it is a measurement of the rig.
|
|
1593
|
+
|
|
1550
1594
|
⭐ **A skeleton may declare no stage, and saying so is not the same as saying
|
|
1551
1595
|
nothing.** Write the pair as `null`:
|
|
1552
1596
|
|
|
@@ -1554,8 +1598,9 @@ nothing.** Write the pair as `null`:
|
|
|
1554
1598
|
"skeleton": { "width": null, "height": null }
|
|
1555
1599
|
```
|
|
1556
1600
|
|
|
1557
|
-
and the emitted header carries **none** of `x`/`y`/`width`/`height` —
|
|
1558
|
-
|
|
1601
|
+
and the emitted header carries **none** of `x`/`y`/`width`/`height` — the input
|
|
1602
|
+
that asked for no box gets none, which is also what an export of a skeleton whose
|
|
1603
|
+
bounds were never set looks like. `null` is this
|
|
1559
1604
|
spec's spelling for a stated absence wherever it has one (`slots[].attachment` is
|
|
1560
1605
|
`null` for "show nothing").
|
|
1561
1606
|
|
|
@@ -1574,32 +1619,34 @@ would be:
|
|
|
1574
1619
|
|
|
1575
1620
|
| Reader | With a stage | Without one |
|
|
1576
1621
|
| --- | --- | --- |
|
|
1577
|
-
| `build` | emits `x`/`y`/`width`/`height` | emits none of them |
|
|
1622
|
+
| `build` | emits `x`/`y`/`width`/`height` — the setup-pose bounding box, not the stage | emits none of them |
|
|
1578
1623
|
| `A14_NO_FULL_FRAME_MESH` | fails a mesh as big as the stage | **SKIP**, by name |
|
|
1579
1624
|
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | exempts the base plate a cut manifest names — the part whose window is the crop — and, on a build that names none, the one image that covers the stage | a manifest build exempts the plate it names, exactly as with one. A rig-spec build names no base plate, so an opaque part is refused with *"this skeleton declares no stage size to measure one against, and the build names no base plate"* and the two ways to decide it: a `skeleton` stage the plate covers, or *"build from a cut manifest, whose base plate is the part whose window is the crop"* |
|
|
1580
|
-
| `diff` | `
|
|
1625
|
+
| `diff` | `bounds_present` and `bounds_box` — the header's box, which is the bounding box | `bounds_present` 1/1 when neither header carries one (agreement), `bounds_box` 0/0 |
|
|
1581
1626
|
| `explain` | `stage W x H` | `stage none declared` |
|
|
1582
1627
|
| `render` | frames the posed extent of every animation | the same frames, plus a line saying the viewport is the posed extent and no stage |
|
|
1583
1628
|
| `preview` | the Spine Web Player frames the posed extent of the animation it plays | the same, plus the same line |
|
|
1584
1629
|
| `check` | fits the candidate's world box from what it draws | the same figures, plus a note saying so |
|
|
1585
1630
|
| `build --pack` | never reads it | the same pages and atlas |
|
|
1586
|
-
| `ingest` | reads
|
|
1631
|
+
| `ingest` | reads the header's box into the rebuild's stage — an export carries no other box, so its bounding box becomes the rebuild's stage, and the rebuild's header is computed again | writes `"width": null, "height": null`; `--stage` adds one |
|
|
1587
1632
|
|
|
1588
1633
|
⚠️ A stage-less skeleton is **unmeasured, not certified**: `A14_NO_FULL_FRAME_MESH`
|
|
1589
1634
|
reports **SKIP** on one, because there is no full frame for a mesh to span. And
|
|
1590
|
-
`rigc diff` reports
|
|
1591
|
-
header block at the top of the report — so a
|
|
1592
|
-
below 1.000 against a source that
|
|
1635
|
+
`rigc diff` reports the header's box — `skeleton.bounds_present` and
|
|
1636
|
+
`skeleton.bounds_box`, in the header block at the top of the report — so a build that
|
|
1637
|
+
carries a box reads below 1.000 against a source that carries none.
|
|
1593
1638
|
|
|
1594
|
-
⭐ **A
|
|
1639
|
+
⭐ **A box at `0,0` is not a box-less header, and an editor export spells it by
|
|
1595
1640
|
saying nothing.** The editor omits a header field that is at its default, so a
|
|
1596
1641
|
skeleton whose box sits at the origin exports as a `width` and a `height` with no
|
|
1597
|
-
`x`/`y`; `
|
|
1598
|
-
— *the extent as stated, an omitted origin as the 0 it means*.
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1642
|
+
`x`/`y`; `bounds_box` reads that omission as the `0` it means, and its line says so
|
|
1643
|
+
— *the extent as stated, an omitted origin as the 0 it means*. The box is compared
|
|
1644
|
+
exactly: rigc's is `getBounds` on the header's 1e-6 grid at float32, the editor's is its own arithmetic, which
|
|
1645
|
+
on the twelve example exports sits up to 0.0071 units from `getBounds` over the same
|
|
1646
|
+
file — so a rebuild of an export reads below 4/4 here by the editor's arithmetic
|
|
1647
|
+
alone, and that is a finding about two writers rather than a moved rig. The extent
|
|
1648
|
+
is the half that is read exactly as stated: omit a `width` and the header carries no
|
|
1649
|
+
box, which `bounds_present` is the measure of. Both are reported and gate nothing, for
|
|
1603
1650
|
the reason every reported measure is: no reading of the rendered frames could have
|
|
1604
1651
|
decided a setup-pose bounding box. The measure inventory that says so lives in
|
|
1605
1652
|
[BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md), which
|
package/docs/INGEST.md
CHANGED
|
@@ -217,7 +217,8 @@ Spine runtime plays it, whatever rigc's own rasteriser or validator thinks.
|
|
|
217
217
|
|
|
218
218
|
`diff` takes two compiled skeletons and reports 54 measures in eight groups, plus two
|
|
219
219
|
blocks that report and gate nothing: the `(reported)` measures beside `attachments`
|
|
220
|
-
and `animations`, and the `skeleton` header block at the top, which measures the
|
|
220
|
+
and `animations`, and the `skeleton` header block at the top, which measures the header's setup-pose
|
|
221
|
+
bounding box.
|
|
221
222
|
A ninth group of six joins them when something has paired the two sides'
|
|
222
223
|
animations — `--as <candidate>=<reference>`, or one animation each side, which pairs by
|
|
223
224
|
position (§1.3.1). Both sides may be foreign; the interesting pairing during ingest is
|
|
@@ -234,9 +235,9 @@ rigc diff
|
|
|
234
235
|
reference …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
|
|
235
236
|
.. bones=3/3 slots=2/2 skins=1/1 attachments=2/2 constraints=0/0 animations=2/2 events=0/0 (candidate/reference)
|
|
236
237
|
|
|
237
|
-
skeleton (reported) (no mean) over 2 measures — the
|
|
238
|
-
1.000
|
|
239
|
-
|
|
238
|
+
skeleton (reported) (no mean) over 2 measures — the header's setup-pose bounding box, which no reading of the frames could decide
|
|
239
|
+
1.000 bounds_present 1/1 both headers carry a setup-pose bounding box, or neither does — …
|
|
240
|
+
0.250 bounds_box 1/4 the header carries the same box (x, y, width, height — the extent as stated, an omitted origin as the 0 it means) — …
|
|
240
241
|
|
|
241
242
|
bones mean 1.000 over 8 measures
|
|
242
243
|
1.000 count 3/3 how many bones
|
|
@@ -277,20 +278,21 @@ both files through `spine-core`, and a skeleton whose attachments carry a `seque
|
|
|
277
278
|
cannot be parsed without the atlas that resolves it — so the measure takes two
|
|
278
279
|
skeletons **and two packs**, which `rigc diff <a.json> <b.json>` does not have.
|
|
279
280
|
|
|
280
|
-
⚠️ **The one exception is the
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
⚠️ **The one exception is the header's own box**, and it is an exception to the
|
|
282
|
+
sentence and not to the rule: `skeleton.bounds_box` compares four world numbers — the
|
|
283
|
+
setup-pose bounding box each writer put in its header (rigc's is spine-core's
|
|
284
|
+
`getBounds` on the header's 1e-6 grid at float32 since issue #907; the editor's is its own arithmetic, up to
|
|
285
|
+
0.0071 units from `getBounds` on the twelve examples) — exactly, and the block they
|
|
286
|
+
sit in gates nothing. The measure was called `stage_box` until #907, when `build`
|
|
287
|
+
stopped writing the stage there.
|
|
285
288
|
|
|
286
289
|
⭐ **Declared is not the same as written down, and for the origin it is the
|
|
287
|
-
difference between a
|
|
288
|
-
field at its default, so a
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
`width` is an absent stage rather than a stage of width zero.
|
|
290
|
+
difference between a true reading and a false finding.** The editor omits a header
|
|
291
|
+
field at its default, so a box sitting at `0,0` exports as a `width` and a `height`
|
|
292
|
+
and no `x`/`y` at all — there is no other spelling for it. The measure reads that
|
|
293
|
+
omission as the `0` it means. The extent is still read exactly as stated: it is what
|
|
294
|
+
decides whether there is a box at all, so a missing `width` is an absent box rather
|
|
295
|
+
than a box of width zero.
|
|
294
296
|
|
|
295
297
|
⛔ **And its ratios are not a score.** [`src/diff.ts`](../src/diff.ts) says so in the
|
|
296
298
|
type itself (*"Unweighted mean of the measures below. NOT a quality score"*), and the
|
|
@@ -509,12 +511,17 @@ at the policy rather than implying the file was read.
|
|
|
509
511
|
is the file that was read, byte for byte. No finding is recorded, because nothing
|
|
510
512
|
was lost and nobody decided anything. `--stage x,y,w,h` is how a caller *adds* a box
|
|
511
513
|
to such a file, and that is a `NO_STAGE` **judgement**. All twelve exports in the
|
|
512
|
-
fetched corpus carry a
|
|
513
|
-
|
|
514
|
-
|
|
514
|
+
fetched corpus carry a box, and `ingest` reads it straight through into the rebuild's
|
|
515
|
+
stage. A **rigc build**'s stage is read instead from the `skeleton.model.json` beside it,
|
|
516
|
+
when that document's `spine.sha256` is the skeleton's digest — its header is the setup-pose
|
|
517
|
+
bounding box — and an export's from its header box. 🔁 What an export's header carries is its setup-pose bounding box; it is the
|
|
518
|
+
only box the file has, so it becomes the rebuild's stage, and the rebuild's own header
|
|
519
|
+
is computed again — `build` writes the setup-pose bounding box there, never the stage
|
|
520
|
+
(issue #907). The stage cannot be *derived*: it is the working area the art was
|
|
521
|
+
painted in, and no pose of the rig states it. 🔸 **Half a stage is a `NO_STAGE` blocker**: an origin with no
|
|
515
522
|
extent, or one extent without the other, declares no stage and is not the absence
|
|
516
523
|
either, and the rig spec holds a stage as four fields or none. It is also the value that costs least to get wrong: `diff`
|
|
517
|
-
reports
|
|
524
|
+
reports the header's box as two measures of its own (`bounds_present`, `bounds_box`) and they are
|
|
518
525
|
`(reported)`, so no score reads them and an absurd box is green nearly everywhere.
|
|
519
526
|
⛔ **The flag is refused beside a box the file states** — two sources for one value,
|
|
520
527
|
both named, and the file is the record of what was measured;
|
|
@@ -659,8 +666,9 @@ states.
|
|
|
659
666
|
⚠️ **If the export's `skeleton` block carries no `x`/`y`/`width`/`height`, write
|
|
660
667
|
`"width": null, "height": null` and do not invent one** — which is also what
|
|
661
668
|
`rigc ingest` writes for such a file. A made-up stage is a number no gate refuses;
|
|
662
|
-
`
|
|
663
|
-
|
|
669
|
+
`A14` and `A19` measure against the stage you state, and `diff`'s header block
|
|
670
|
+
reports `skeleton.bounds_present` and `skeleton.bounds_box` — your build's bounding
|
|
671
|
+
box, which you do not author, against the source's. Copy the four numbers when they are there;
|
|
664
672
|
state the absence when they are not.
|
|
665
673
|
4. **`explain`, then `build`.** `explain` first, because it prints what you wrote in a
|
|
666
674
|
shape you can compare against the export by eye (§1.5) and it never gates. Then
|
|
@@ -704,11 +712,11 @@ State the ambition in the right units, because three different things get called
|
|
|
704
712
|
| --- | --- | --- |
|
|
705
713
|
| **Structural agreement** — same bones, slots, attachments, timelines, key counts, curve kinds | ✅ yes, and `diff` measures it | the 3-timing transcription reads **1.000 on all 54 measures**. Aim here first |
|
|
706
714
|
| **Geometric agreement** — the same drawn pixels, allowing for the atlas | ✅ yes, and `check` measures it | see below |
|
|
707
|
-
| **Byte-identical JSON** | ✅ **for a rebuild, in canonical form, apart from `hash` and
|
|
715
|
+
| **Byte-identical JSON** | ✅ **for a rebuild, in canonical form, apart from `hash`, `spine` and the header's box** — the pass line below | a **rebuild** of an editor export (`ingest`, then `build`) is the export. A **transcription** by hand is not held to it: what differs there is what a person chose to write, not the emitter |
|
|
708
716
|
|
|
709
717
|
⭐ **The pass line of the byte round trip, stated once:** `build(ingest(x))` of an
|
|
710
718
|
editor export `x` is **identical to `x` in canonical form, apart from `hash` and
|
|
711
|
-
`spine
|
|
719
|
+
`spine`**, and from the header's box (below) — canonical form being `JSON.stringify(JSON.parse(text), null, 2)` of each
|
|
712
720
|
file, which keeps every number as parsed, every key in its order and every omitted key
|
|
713
721
|
omitted, and drops only whitespace and the exponent's spelling (an export setting, not
|
|
714
722
|
a property of the rig). It holds on all twelve exports under `examples/`; for a
|
|
@@ -722,11 +730,22 @@ has no field for, by design, and each has its finding:
|
|
|
722
730
|
| `hash` | `undefined` vs `"VFWbaK2UoCM"` | the editor's project hash — a value about a file rigc did not write, so a spec that carried it would be claiming an export it did not come from. `HEADER_BOOKKEEPING` |
|
|
723
731
|
| `spine` | `"4.3.13"` vs `"4.3.75-beta"` | the version of the runtime rigc links, stamped by design and re-checked by `A16`. `HEADER_REDERIVED` |
|
|
724
732
|
|
|
733
|
+
🔁 **The header's box is the third difference, and it is not a key the spec lacks** (issue
|
|
734
|
+
#907). The rig spec carries the export's `x`, `y`, `width`, `height` — as the rebuild's
|
|
735
|
+
stage — and the rebuild does not copy them back: its header carries the setup-pose bounding
|
|
736
|
+
box `build` computes, which is spine-core's `getBounds` over the export on the header's grid
|
|
737
|
+
(the rebuild draws the export's vertices to the bit, so its box is the export's box), where
|
|
738
|
+
the editor wrote its own arithmetic, at most 0.0071 units away on the twelve —
|
|
739
|
+
`spineboy-pro`'s rebuild writes `-188.63641, -7.936074, 418.453, 686.19934` against the
|
|
740
|
+
editor's `-188.6338, -7.939564, 418.4499, 686.2023`. The suite holds each rebuild's box to
|
|
741
|
+
`getBounds` of its source at tolerance 0 (`IG97`) and reads every other byte against a copy
|
|
742
|
+
of the export carrying that box.
|
|
743
|
+
|
|
725
744
|
🔢 **The rest of the header, and every omitted default, comes back as the export
|
|
726
745
|
spells it.** The **header's `audio`** is a stated value like `images`, and the rig
|
|
727
746
|
spec carries it ([AUTHORING §3.1](AUTHORING.md)). The emitter leaves out exactly the
|
|
728
747
|
keys the parser reads the same way without them ([AUTHORING §10.6c](AUTHORING.md)).
|
|
729
|
-
One shape is left and it is deliberate: a
|
|
748
|
+
One shape is left and it is deliberate: a box at the origin, which the editor writes as
|
|
730
749
|
`width`/`height` with no `x`/`y` and rigc spells whole. The 4.3 JSON reader has no
|
|
731
750
|
default for the origin — an absent one loads as `undefined`, a written `0` as `0` — so
|
|
732
751
|
it is not a key the parser reads the same way without it, and the row is not in the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@ import { ASSERTION_KIND, type AssertionProfile } from '../kinds.ts';
|
|
|
26
26
|
import { verdictHarness, type VerdictLists } from '../harness.ts';
|
|
27
27
|
import { MOVED_ASSERTIONS } from '../model/index.ts';
|
|
28
28
|
import type { Verdicts } from '../harness.ts';
|
|
29
|
-
import type
|
|
29
|
+
import { isObj, type Json } from '../values.ts';
|
|
30
30
|
import { a01NoLegacyToplevelConstraintArrays } from '../bodies/a01.ts';
|
|
31
31
|
import { a02NoBoneTransformKey } from '../bodies/a02.ts';
|
|
32
32
|
import { a05CurveArrayLength } from '../bodies/a05.ts';
|
|
@@ -35,6 +35,7 @@ import { a16SkeletonVersion43 } from '../bodies/a16.ts';
|
|
|
35
35
|
import { a18DeterministicEmit } from '../bodies/a18.ts';
|
|
36
36
|
import { a31DrawOrderOffsetsResolve } from '../bodies/a31.ts';
|
|
37
37
|
import { a35DeformKeysFitTheAttachment } from '../bodies/a35.ts';
|
|
38
|
+
import { parseAtlasText } from '../../atlas.ts';
|
|
38
39
|
|
|
39
40
|
/** What the restated rules read: the emitted texts, the document written beside them, and a second compile's three. */
|
|
40
41
|
export interface EmittedTextInput {
|
|
@@ -92,6 +93,31 @@ export const EMITTED_TEXT_RULES: ReadonlyArray<{ code: string; run: (v: Verdicts
|
|
|
92
93
|
},
|
|
93
94
|
];
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The figures `validate()` reads off the pair it loaded, read off the same two
|
|
98
|
+
* texts here (issue #1114), into `stats` first — the order the round trip's
|
|
99
|
+
* report holds them in: `pages` and `regions` from the atlas text by rigc's own
|
|
100
|
+
* reader (`parseAtlasText`, whose pages and regions are the runtime atlas's in
|
|
101
|
+
* file order), then `bones`, `slots`, `animations` and `version` from the
|
|
102
|
+
* skeleton JSON — the arrays and the animation map the runtime loads them
|
|
103
|
+
* from, and `skeleton.spine` as it states it, `(none)` where it states none.
|
|
104
|
+
* The skeleton's four are left out where its JSON does not parse, as the round
|
|
105
|
+
* trip leaves them out where it loaded nothing; nothing here is a value the
|
|
106
|
+
* texts do not state.
|
|
107
|
+
*/
|
|
108
|
+
function pairFigures(stats: Record<string, number | string>, raw: Json | null, atlasText: string): void {
|
|
109
|
+
const atlas = parseAtlasText(atlasText);
|
|
110
|
+
stats.pages = atlas.pages.length;
|
|
111
|
+
stats.regions = atlas.regions.length;
|
|
112
|
+
if (!isObj(raw)) return;
|
|
113
|
+
const listed = (v: unknown): number => (Array.isArray(v) ? v.length : 0);
|
|
114
|
+
stats.bones = listed(raw.bones);
|
|
115
|
+
stats.slots = listed(raw.slots);
|
|
116
|
+
stats.animations = isObj(raw.animations) ? Object.keys(raw.animations).length : 0;
|
|
117
|
+
const declared = isObj(raw.skeleton) ? raw.skeleton.spine : undefined;
|
|
118
|
+
stats.version = declared === undefined || declared === null ? '(none)' : typeof declared === 'string' ? declared : JSON.stringify(declared);
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
/** Every code the model side does not run, read off the registry — what this file must account for. */
|
|
96
122
|
export function roundTripOnlyCodes(): string[] {
|
|
97
123
|
const moved = new Set(MOVED_ASSERTIONS.map((m) => m.code));
|
|
@@ -112,6 +138,7 @@ export function validateEmittedText(input: EmittedTextInput): VerdictLists & { p
|
|
|
112
138
|
} catch (err) {
|
|
113
139
|
h.fail(A00, `skeleton JSON is not parseable: ${(err as Error).message}`);
|
|
114
140
|
}
|
|
141
|
+
pairFigures(h.stats, raw, input.atlasText);
|
|
115
142
|
for (const rule of EMITTED_TEXT_RULES) {
|
|
116
143
|
if (rule.code === A00 && raw === null) continue;
|
|
117
144
|
h.check(rule.code, () => rule.run(h.verdicts, { raw, input }));
|
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* rather than a number standing in for it, and each body reads it as it always
|
|
12
12
|
* did.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* A `rigc-compiled/3` document states it (`stage`, issue #1026), and since
|
|
15
|
+
* issue #907 that is the only place a rigc build states it: the Spine header
|
|
16
|
+
* carries the setup-pose bounding box. So both suppliers read a rigc build's
|
|
17
|
+
* stage off its document (`spineStage` in `../../validate.ts`,
|
|
18
|
+
* `../model/stage.ts`); an export, which has no document, is read off its
|
|
19
|
+
* header.
|
|
17
20
|
* Links nothing from the runtime.
|
|
18
21
|
*/
|
|
19
22
|
|
|
@@ -45,7 +45,7 @@ import { a11NoClippingAttachments } from '../bodies/a11.ts';
|
|
|
45
45
|
import { a17AtlasPageFilesExist } from '../bodies/a17.ts';
|
|
46
46
|
import { a45SeparableColorTimelinesOwnTheirChannelsAndPoseAsWritten } from '../bodies/a45.ts';
|
|
47
47
|
import { A00_MODEL_READ, A00_MODEL_REGIONS_ON_PAGES, MODEL_PARSE_KIND, modelRead, modelRegionsOnPages, SKIP_NO_MODEL_DOCUMENT, SKIP_NO_MODEL_PAGES, type ReadDocument } from './parse.ts';
|
|
48
|
-
import { modelSkinEntries } from './skin_entries.ts';
|
|
48
|
+
import { fileOrderedEntries, modelSkinEntries } from './skin_entries.ts';
|
|
49
49
|
import { modelAtlasPages } from './atlas_pages.ts';
|
|
50
50
|
import { modelSlotColour } from './slot_colour.ts';
|
|
51
51
|
import type { SkinEntryFacts } from '../facts/skin_entries.ts';
|
|
@@ -332,6 +332,44 @@ function parse(h: VerdictHarness, modelText: string): { read: ReadDocument | nul
|
|
|
332
332
|
return { read, held: h.check(A00_MODEL_REGIONS_ON_PAGES, () => modelRegionsOnPages(h.verdicts, read)) === true };
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
/**
|
|
336
|
+
* The row the rig's two figures are written before (issue #1114): `validate()`
|
|
337
|
+
* sets `rig` and `profile` where its archetype assertions begin, after every
|
|
338
|
+
* figure the bodies above them write and before A29's and A30's, so the figures
|
|
339
|
+
* line lists them in that place on both entries. `RC39` in `selftest.ts`
|
|
340
|
+
* compares the two lines key by key.
|
|
341
|
+
*/
|
|
342
|
+
const RIG_FIGURES_BEFORE = 'A24_AXIS_SPACE_STROKE';
|
|
343
|
+
if (!MOVED_ASSERTIONS.some((m) => m.code === RIG_FIGURES_BEFORE)) throw new Error(`internal: the model side writes the rig's figures before ${RIG_FIGURES_BEFORE}, which it no longer runs`);
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The attachment figures `validate()` counts over the skins it loaded (issue
|
|
347
|
+
* #1114), counted over the document's skins in the file's walk order: a
|
|
348
|
+
* `region` record is a region attachment, and a `mesh` or `linkedmesh` record
|
|
349
|
+
* a mesh attachment — the runtime loads a linked mesh as a mesh. Written only
|
|
350
|
+
* where the document read and its regions held, as the round trip writes them
|
|
351
|
+
* only where it loaded the skeleton, and before any body's figure, which is
|
|
352
|
+
* where `validate()` writes them.
|
|
353
|
+
*/
|
|
354
|
+
function attachmentFigures(stats: Record<string, number | string>, read: ReadDocument): void {
|
|
355
|
+
const kinds = fileOrderedEntries(read).map((entry) => entry.record.kind);
|
|
356
|
+
stats.regionAttachments = kinds.filter((kind) => kind === 'region').length;
|
|
357
|
+
stats.meshAttachments = kinds.filter((kind) => kind === 'mesh' || kind === 'linkedmesh').length;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* `rig` — the archetype the document's rig section states (`validate()` prints
|
|
362
|
+
* its caller's; the model side reads the document's, issue #1054 — read
|
|
363
|
+
* directly rather than through `supply.rigInfo`, whose refusal of a caller's
|
|
364
|
+
* `rig` beside the document is the rules' line to print, not this figure's),
|
|
365
|
+
* left out where there is no document to state one — and `profile`, the profile this
|
|
366
|
+
* run was handed: the same argument both entries read off `--profile`.
|
|
367
|
+
*/
|
|
368
|
+
function rigFigures(stats: Record<string, number | string>, read: ReadDocument | null, input: ModelValidateInput): void {
|
|
369
|
+
if (read !== null) stats.rig = modelRigInfo(read).archetype;
|
|
370
|
+
stats.profile = input.profile;
|
|
371
|
+
}
|
|
372
|
+
|
|
335
373
|
/**
|
|
336
374
|
* Run the model side: the parse, then every moved assertion — each over the
|
|
337
375
|
* document, or skipped naming the parse when there is none — and return the
|
|
@@ -343,7 +381,9 @@ export function validateModel(input: ModelValidateInput, plant: Partial<ModelSup
|
|
|
343
381
|
const supply: ModelSupply = { ...MODEL_SUPPLY, ...plant };
|
|
344
382
|
const h = verdictHarness(input.profile, { ...ASSERTION_KIND, ...MODEL_PARSE_KIND }, 'the model side');
|
|
345
383
|
const { read, held } = parse(h, input.modelText);
|
|
384
|
+
if (read !== null && held) attachmentFigures(h.stats, read);
|
|
346
385
|
for (const moved of MOVED_ASSERTIONS) {
|
|
386
|
+
if (moved.code === RIG_FIGURES_BEFORE) rigFigures(h.stats, read, input);
|
|
347
387
|
const doc = read !== null && (held || moved.beforeTheParse === true) ? read : null;
|
|
348
388
|
h.check(moved.code, () => (doc === null ? h.skip(moved.code, moved.unread) : moved.run(h.verdicts, doc, input, supply)));
|
|
349
389
|
}
|
package/src/cli/core_commands.ts
CHANGED
|
@@ -28,13 +28,13 @@ import { deformReportBlock } from '../deformreport.ts';
|
|
|
28
28
|
import { type DiffAnimationPair, diffLines, diffSkeletons } from '../diff.ts';
|
|
29
29
|
import { ingest, INGEST_GUTTERS, type IngestFinding, IngestSpecRefused, type IngestStage } from '../ingest.ts';
|
|
30
30
|
import { PARSER_DEFAULTS, parserReading } from '../keyorder.ts';
|
|
31
|
-
import { modelDocument } from '../model.ts';
|
|
31
|
+
import { MODEL_DOCUMENT_FILE, modelDocument } from '../model.ts';
|
|
32
32
|
import { parseMotionSpec } from '../motion.ts';
|
|
33
33
|
import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
|
|
34
34
|
import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, type FrameSet, type FramesSidecar, framingViewport, GEOMETRY_FILE, geometryFileOf, geometryText, loadCandidate, POSER_NAMES, type PoserName, PROTOCOL_FPS, refuseUnchosen, renderFrame, sampleAll, sampleAnimation, SETUP_POSE_DIR, SHEET_FILE, SHEET_TILE, sidecarViewport, type SkeletonFacts, type SlotSubset, SlotSubsetError, throughPoser } from '../render_shared.ts';
|
|
35
35
|
import { type CompileResult } from '../types.ts';
|
|
36
36
|
import { attachmentRegionJoins } from '../region_joins.ts';
|
|
37
|
-
import { ATLAS_ABSENT, COMMANDS, type CommandRun, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson } from './shared.ts';
|
|
37
|
+
import { ATLAS_ABSENT, COMMANDS, type CommandRun, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
38
38
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
39
39
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
40
40
|
|
|
@@ -889,11 +889,9 @@ export function cmdExplain(flags: Record<string, string>): void {
|
|
|
889
889
|
// A rig may state that it has no stage at all (issue #578), and the two must
|
|
890
890
|
// not print alike: `undefined x undefined` is what a template does with an
|
|
891
891
|
// absence, and it reads like a defect in the tool rather than a claim in the
|
|
892
|
-
// spec.
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
? 'none declared'
|
|
896
|
-
: `${result.skeleton.skeleton.width} x ${result.skeleton.skeleton.height}`;
|
|
892
|
+
// spec. The stage is the model's (issue #907): the header's box is the
|
|
893
|
+
// setup-pose bounding box, which is not the stage and is not printed as it.
|
|
894
|
+
const stage = result.model.stage === null ? 'none declared' : `${result.model.stage.width} x ${result.model.stage.height}`;
|
|
897
895
|
console.log(`\nstage ${stage} (spine ${result.skeleton.skeleton.spine})`);
|
|
898
896
|
|
|
899
897
|
// The crop note describes where the numbers CAME from, and without a manifest
|
|
@@ -1295,9 +1293,16 @@ export function cmdIngest(flags: Record<string, string>, positional: string[]):
|
|
|
1295
1293
|
* with them — `JSON.stringify` — and a cast to `RigSpec` here would be this
|
|
1296
1294
|
* file claiming a parse that did not happen.
|
|
1297
1295
|
*/
|
|
1296
|
+
// A rigc build's stage is the model document's beside it, when that document is this skeleton's (issue #907): its header is the
|
|
1297
|
+
// setup-pose bounding box. An export, or a skeleton with no such document, is read off its header.
|
|
1298
|
+
const documentStage = documentStageBeside(skeletonPath, readFileSync(skeletonPath, 'utf8'));
|
|
1299
|
+
if (documentStage !== undefined) {
|
|
1300
|
+
console.log(` .. stage ${documentStage === null ? 'none declared' : `${documentStage.width} x ${documentStage.height}`} — read from the ${MODEL_DOCUMENT_FILE} beside it (its spine.sha256 is this skeleton's); the header's box is the setup-pose bounding box`);
|
|
1301
|
+
}
|
|
1298
1302
|
let result: { rig: unknown; motion: unknown; findings: IngestFinding[] };
|
|
1299
1303
|
try {
|
|
1300
1304
|
result = ingest(readJsonFile(skeletonPath), {
|
|
1305
|
+
...(documentStage === undefined ? {} : { documentStage }),
|
|
1301
1306
|
name: flags.name ?? basename(skeletonPath, '.json'),
|
|
1302
1307
|
art,
|
|
1303
1308
|
images: specImages,
|
|
@@ -1559,7 +1564,9 @@ const MODEL_AND_TEXT_GATE: BuildGate = {
|
|
|
1559
1564
|
passed: [...model.passed, ...text.passed],
|
|
1560
1565
|
skipped: [...model.skipped, ...text.skipped],
|
|
1561
1566
|
profileSkipped: [...model.profileSkipped, ...text.profileSkipped],
|
|
1562
|
-
|
|
1567
|
+
// The emitted text's figures first: they are the pair's (`pages` … `version`), which `validate()` writes
|
|
1568
|
+
// before any other, and the model side's follow in its order — one line, keyed and ordered as `cli.ts build`'s (issue #1114, `RC39`).
|
|
1569
|
+
stats: { ...text.stats, ...model.stats },
|
|
1563
1570
|
profile,
|
|
1564
1571
|
};
|
|
1565
1572
|
for (const line of reportLines(report)) console.log(line);
|