spine-parts 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/cli.ts +2 -1
- package/docs/AUTHORING.md +22 -14
- package/package.json +2 -2
- package/src/check.ts +4 -3
- package/src/config.ts +43 -2
- package/src/raster/png.ts +5 -5
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ page** (issue #2):
|
|
|
86
86
|
|
|
87
87
|
| path under `--out` | what |
|
|
88
88
|
| --- | --- |
|
|
89
|
-
| `check/build/skeleton.json`, `skeleton.atlas`, `skeleton.png` | **the artifact** — Spine 4.3 skeleton data and one packed page, written by `rigc build --pack --page-edges free --pack-shape polygon` and gated under `spine-html`, which holds every rule `spine` measures (on the demo, rigc 2.1.3: the 14 rules `validate --profile spine` measures are among the build's 24; 16 are not in `spine`) |
|
|
89
|
+
| `check/build/skeleton.json`, `skeleton.atlas`, `skeleton.png` | **the artifact** — Spine 4.3 skeleton data and one packed page, written by `rigc build --pack --page-edges free --pack-shape polygon` and gated under `spine-html`, which holds every rule `spine` measures (on the demo, rigc 2.1.3 and 2.10.1: the 14 rules `validate --profile spine` measures are among the build's 24; 16 are not in `spine`) |
|
|
90
90
|
| `parts/*.png`, `parts.json`, `recomposite_rig.png`, `recomposite_error_rig.png` | the loose parts, each cropped to its alpha box; the record of where every part came from, how many of its pixels were re-taken from the painting, and the recomposite's uncovered holes with their boxes; the flat stack of parts; its error map — red where no part covers a pixel the painting has, blue where a part covers it in the wrong colour |
|
|
91
91
|
| `rig/` | `rig.json` and `motion.json` in spine-rigc's spec, `mesh_report.json`, the padded `images/` |
|
|
92
92
|
| `check/` | both gate files verbatim, the idle's frames and their `geometry.json` (skinned vertices per frame), `contact.png`, `motion_heat.png`, `check.json` (with `pack_mode`, the `--page-edges` and `--pack-shape` the page was packed under) |
|
package/cli.ts
CHANGED
|
@@ -108,7 +108,8 @@ usage:
|
|
|
108
108
|
(RIG_BLINK_HOLD_SPANS_A_FRAME otherwise); no blink when the config
|
|
109
109
|
states no motion.blink, and a blink group that names no bone, or one
|
|
110
110
|
bone twice, is refused by the loader, CONFIG_BLINK_GROUP_MEMBERS or
|
|
111
|
-
CONFIG_BLINK_GROUP_UNIQUE, before rigc starts
|
|
111
|
+
CONFIG_BLINK_GROUP_UNIQUE, before rigc starts, as are two tracks on one
|
|
112
|
+
bone property, CONFIG_BONE_PROPERTY_KEYED_ONCE. --parts is the
|
|
112
113
|
directory holding parts.json and parts/<name>.png. The result is built
|
|
113
114
|
through spine-rigc (profile spine-html, packed with --page-edges and
|
|
114
115
|
--pack-shape as for check; rigc's build gates the compile and the packed pages on disk) in a
|
package/docs/AUTHORING.md
CHANGED
|
@@ -75,7 +75,7 @@ read by no CPU stage).
|
|
|
75
75
|
| `meshes.<part>` | rig | **proposed**, then corrected. `grid` (lattice cell size, px), `r` (added to every distance before weighting: `w = 1/(d + r)²`), `segments` (a chain name, a bone name with a `tip`, or `[bone, [x0,y0], [x1,y1]]`). The segment list is the one authored decision about a layer: which bones may pull it. The slot's bone is the first segment's |
|
|
76
76
|
| `regions.<part>` | rig | **proposed**: the bone a rigid part rides. Every plan part is exactly one of a mesh or a region (`CONFIG_PART_ATTACHED`) |
|
|
77
77
|
| `motion.duration` | rig; check (the loop is measured at this time) | proposed as 4 s; a whole number of 1/12 s ticks, because `check` renders at 12 fps |
|
|
78
|
-
| `motion.tracks` | rig | **proposed**, then tuned. Single `{bone, prop, amp, period, phase, base?}` or chain `{chain, amps, period, phase, lag}` — one amplitude per link, link `i` at phase `phase + lag·i`. Every `period` must divide `duration` (`CONFIG_PERIOD_DIVIDES_DURATION`) |
|
|
78
|
+
| `motion.tracks` | rig | **proposed**, then tuned. Single `{bone, prop, amp, period, phase, base?}` or chain `{chain, amps, period, phase, lag}` — one amplitude per link, link `i` at phase `phase + lag·i`. Every `period` must divide `duration` (`CONFIG_PERIOD_DIVIDES_DURATION`). A bone property is keyed by one track (`CONFIG_BONE_PROPERTY_KEYED_ONCE`): a chain keys `rotate` on every link, the blink's `eyes` group `scaley` and its `brows` group `translatey` on every member, so a single track on any of those is a second track on it — rigc would refuse it one stage later, `animation "idle" has two tracks on eye.scaley` |
|
|
79
79
|
| `motion.blink` | rig | **proposed**: `{t, eyes, brows, squash, brow_drop}`; the whole blink, `t` to `t + 0.364` s (`t + 0.314` s without brows: the brows' keys are the ones that end last), must fit inside the idle (`RIG_BLINK_INSIDE_IDLE`). The `eyes` group's `scaley` squashes every part on the eye bones about the bone's origin, the eyewhite's centre. Optional: a figure with no `eyewhite-r`/`-l` part has no eye bone, so `propose` writes no `blink` and notes `no blink: no eyewhite part (looked for: eyewhite-r, eyewhite-l), …`; with eyes and no `eyebrow-r`/`-l` part it writes the blink without `brows` and `brow_drop` and notes `blink without brows: …`. `brows` and `brow_drop` are stated together or not at all (`CONFIG_BLINK_BROWS_PAIRED`), and a group that names no bone is refused (`CONFIG_BLINK_GROUP_MEMBERS`) — rigc would refuse it one stage later, `group "eyes" declares no members` — as is a group that names one bone twice (`CONFIG_BLINK_GROUP_UNIQUE`; rigc: `group "eyes" names member "eye" twice`). An `irides-*` or `eyelash-*` part on a side with no eyewhite has no eye bone to ride: `propose` places it on `head` as a region, which the blink does not move, and notes `no eyewhite part for eye_r (looked for: eyewhite-r): iris (head:irides-r) is placed on head as a region, …`. The timing is fixed, not a field (table below) |
|
|
80
80
|
| `motion.blink.still` | rig | **proposed** only for a lash `propose` notes (below), then checked: `{<part>: {row, bone}}` — the rows of that region part above `row` (rig px, y down) are drawn by a second slot `<part>_still` on `bone` and do not blink; rows from `row` down keep the part's slot and blink. The part must be a region on a bone `eyes` names, `bone` one it does not (`CONFIG_STILL_OFF_THE_BLINK`), and `row` a row of the part with no art across its whole width (`RIG_STILL_ROW_CLEAR`) |
|
|
81
81
|
|
|
@@ -282,9 +282,10 @@ max for the APNG); a figure is a measurement of the file, not a bar. On the demo
|
|
|
282
282
|
13,645,503 B lossless, 1,706,719 B indexed and 1,812,288 B GIF, both palette files at
|
|
283
283
|
max 57, mean 1.601 — the two share one quantiser, so their error is the same by
|
|
284
284
|
construction and the size is the difference. That is `--seam silhouette` (the README's
|
|
285
|
-
animation, spine-rigc 2.1.3); with `--pack-shape rect` the same
|
|
286
|
-
1,706,520 and 1,812,238 B, and with `--page-edges pot
|
|
287
|
-
1,706,785 and 1,812,625 B, all at the same error.
|
|
285
|
+
animation, spine-rigc 2.1.3, and 2.10.1 to the byte); with `--pack-shape rect` the same
|
|
286
|
+
build writes 13,645,350, 1,706,520 and 1,812,238 B, and with `--page-edges pot
|
|
287
|
+
--pack-shape rect` 13,645,519, 1,706,785 and 1,812,625 B, all at the same error (2.10.1
|
|
288
|
+
reproduces both). The loop shows the blink closed: the
|
|
288
289
|
eyes' hold is at least one 12 fps frame (below), so whatever `motion.blink.t` is, one
|
|
289
290
|
idle frame lands inside it, and `check.json`'s `BLINK_NO_HOLE.idle_frames_closed`
|
|
290
291
|
names it — `[29]` on both examples.
|
|
@@ -303,9 +304,10 @@ the least-area page the parts need. `--page-edges pot` is the opt-out: a power o
|
|
|
303
304
|
two on both edges, which is the page every build wrote before this flag. The pack
|
|
304
305
|
line says which one ran, because rigc writes `, page edges free` after the padding under
|
|
305
306
|
`free`. Measured on the two examples by `tools/atlas_population.ts` with rigc 1.5.1, and
|
|
306
|
-
again with rigc 2.0.3
|
|
307
|
+
again with rigc 2.0.3, 2.1.3 and 2.10.1 under `--pack-shape rect` (the frames table
|
|
307
308
|
by the same idle render), which reproduced every figure in both tables and the one
|
|
308
|
-
check figure below:
|
|
309
|
+
check figure below (2.10.1: pages, atlases, idle frames and `check.json` byte-identical
|
|
310
|
+
to 2.1.3's):
|
|
309
311
|
|
|
310
312
|
| build | page | covered | page opaque | page / figure |
|
|
311
313
|
| --- | --- | --- | --- | --- |
|
|
@@ -339,9 +341,11 @@ attachment stays its rectangle. rigc gates the result on the packed pages with
|
|
|
339
341
|
`A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` (SKIP on the compile pass, where every part has
|
|
340
342
|
its own page; PASS on the packed pass, both shapes, both examples). The pack line ends
|
|
341
343
|
`, shape rect` or `, shape polygon` under either, and `check.json`'s `pack_mode` records
|
|
342
|
-
both flags. Measured on the two examples with spine-rigc 2.1.3
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
both flags. Measured on the two examples with spine-rigc 2.1.3, and again with 2.10.1,
|
|
345
|
+
whose pages, atlases, idle frames and `check.json` are byte-identical to 2.1.3's under
|
|
346
|
+
both shapes and both edges (the full entry, spine-core 4.3.13 beside it),
|
|
347
|
+
`spine-parts build` with `--page-edges free` and each shape, the table columns by
|
|
348
|
+
`tools/atlas_population.ts`:
|
|
345
349
|
|
|
346
350
|
| build | page | page area | covered | page opaque | page / figure |
|
|
347
351
|
| --- | --- | --- | --- | --- | --- |
|
|
@@ -377,8 +381,11 @@ The hull ceiling (spine-parts #58: hull area over rectangle area, 0.812 on demo
|
|
|
377
381
|
rectangles are worth only what fits into them. On the polygon pages, read off the
|
|
378
382
|
atlas, demo's packed rectangles overlap in 21 pairs (17 a region attachment inside a
|
|
379
383
|
mesh's rectangle, 4 a mesh inside another's) and sample's in 12 (10 and 2); no two
|
|
380
|
-
region attachments overlap.
|
|
381
|
-
|
|
384
|
+
region attachments overlap. Meshes in place of regions would be rigc's stage 2 of
|
|
385
|
+
polygon packing (firejune/rigc#1093), which rigc measured and folded: on the three
|
|
386
|
+
production rigs most favourable to it, it adds 0.15 % of the rectangle area over
|
|
387
|
+
stage 1 at tolerance 0, and nothing at the mesher's tolerance 1 or under `pot`; on
|
|
388
|
+
these two examples −0.3 to −1.3 points. No build converts a region attachment.
|
|
382
389
|
|
|
383
390
|
`--project core` (the default) is the reference's projection rule: a layer takes the
|
|
384
391
|
painting's pixel only inside its top-most `alpha >= 250` area eroded by a 5x5 square,
|
|
@@ -574,6 +581,7 @@ See-through with another seed.
|
|
|
574
581
|
| `CONFIG_PERIOD_DIVIDES_DURATION` | a period that is not a whole fraction of the idle — the loop could not close | that track's `period`, or `motion.duration` |
|
|
575
582
|
| `CONFIG_BLINK_GROUP_MEMBERS` | `motion.blink.eyes` or `motion.blink.brows` is `[]`: a group with no members, which rigc refuses at the gate | a figure with no eyewhite part has nothing to blink: leave `motion.blink` out; one with no eyebrow part: leave `brows` and `brow_drop` out |
|
|
576
583
|
| `CONFIG_BLINK_GROUP_UNIQUE` | `motion.blink.eyes` or `motion.blink.brows` names one bone more than once; the detail gives the bone and every index it sits at. rigc refuses it at the gate (`group "eyes" names member "eye" twice`) | name each bone once: the group keys every member it names |
|
|
584
|
+
| `CONFIG_BONE_PROPERTY_KEYED_ONCE` | two tracks key one bone property: two single tracks, a single track on a chain link's `rotate`, or a single track on a blink member's `scaley` (`eyes`) or `translatey` (`brows`); the detail names the property and every track by its config path. rigc refuses it at the gate (`animation "idle" has two tracks on eye.scaley; merge them into one track`) | merge them into one track, or key another property or bone |
|
|
577
585
|
| `CONFIG_BLINK_BROWS_PAIRED` | `motion.blink.brows` without `brow_drop`, or `brow_drop` without `brows` | state both or neither |
|
|
578
586
|
| `CONFIG_STILL_OFF_THE_BLINK` | a `motion.blink.still` entry names a region on a bone the blink's `eyes` does not name (nothing to hold still), or its `bone` is one the blink's `eyes` names (the still piece would blink) | that entry's part, or its `bone` — the eye bone's parent, `head` as proposed |
|
|
579
587
|
|
|
@@ -642,7 +650,7 @@ See-through with another seed.
|
|
|
642
650
|
| `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
|
|
643
651
|
| `CHECK_INPUT_PRESENT` on `…/parts.json` | `--parts` named a directory without `parts.json` — most often `parts/` itself. The detail says it: "--parts names the directory holding parts.json and parts/, not parts/ itself (after build, that is build's --out, whose rig is <out>/rig); without --parts it is --rig", and names the parent when the parent holds `parts.json` | `--parts` — the directory above `parts/` |
|
|
644
652
|
| `CHECK_INPUT_PRESENT`, `CHECK_INPUT_IS_JSON`, `CHECK_PART_PNG_PRESENT`, `CHECK_PART_PNG_MATCHES_BOX`, `CHECK_PART_SLOT_PRESENT`, `CHECK_RIG_STAGE_PRESENT`, `CHECK_RIG_STAGE_IS_THE_CANVAS`, `CHECK_RIG_ROOT_BONE`, `CHECK_IDLE_PRESENT` | the rig directory is incomplete or disagrees with `parts.json` (`CHECK_PART_SLOT_PRESENT`: a part with no slot of its own name, which the judgement lines render it by) | re-run rig (`build` does both) |
|
|
645
|
-
| `CHECK_RIGC_VERSION` | `rigc --version` is below 1.4.0, or prints no version: its `render` has no `--geometry`, which `TEXTURE_STRETCH` reads. Refused before anything is built | `bun install` (this package depends on spine-rigc ^2.1
|
|
653
|
+
| `CHECK_RIGC_VERSION` | `rigc --version` is below 1.4.0, or prints no version: its `render` has no `--geometry`, which `TEXTURE_STRETCH` reads. Refused before anything is built | `bun install` (this package depends on spine-rigc ^2.10.1), or put a newer `rigc` first on `PATH` |
|
|
646
654
|
| `CHECK_RIGC_ENTRY`, `CHECK_RIGC_ENTRY_READS` | `rigc --version` names no entry (a rigc below 2.0.0), or names one in neither launcher form (`entry: cli.ts — @esotericsoftware/spine-core <v> present`, `entry: cli_core.ts — @esotericsoftware/spine-core absent — …`); `check.json`'s `rigc_entry` records the one that gated the build | `bun install` |
|
|
647
655
|
| `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
648
656
|
| `CHECK_LOOP_LAST_FRAME_AT_DURATION` | the idle's last frame does not sit at `duration` | `motion.duration` — a whole number of 1/12 s |
|
|
@@ -658,7 +666,7 @@ See-through with another seed.
|
|
|
658
666
|
| `LOOP_ENCODE` | the loop encoder refused a frame (translucent pixel in a GIF, a size change) | the frames; for a translucent frame write the lossless or the indexed APNG, which keep alpha |
|
|
659
667
|
| `CHECK_PACK_LINE_READS` | rigc printed a `pack:` line that is not `pack: <page> <W>x<H>, <N> region(s), <P>% covered, padding <D>`, then `, page edges free` or nothing, then `, shape rect`, `, shape polygon` or nothing (spine-rigc 2.1 prints the shape; 1.5–2.0 printed none, read as `rect`); an unknown shape is this refusal; the line is quoted | a rigc this package does not know the output of; report it, with `rigc --version` |
|
|
660
668
|
| `CHECK_PACK_PAGE_EDGES` | the pack line disagrees with the `--page-edges` the build was run with — its `, page edges free` clause is there under `pot` or missing under `free`, or a `pot` page is not a power of two on both edges | a rigc problem; report it, with `rigc --version` |
|
|
661
|
-
| `CHECK_PACK_SHAPE` | the pack line disagrees with the `--pack-shape` the build was run with — it ends `, shape rect` under `polygon` or `, shape polygon` under `rect`, or it has no shape clause (the 1.5–2.0 form, read as `rect`) under `polygon` | a rigc older than 2.1.0 on `PATH`: rigc 2.0.3 takes `--pack-shape` without a word, packs by rectangles and prints the form with no shape clause, so this refusal is what catches it — `bun install` (this package depends on spine-rigc ^2.1
|
|
669
|
+
| `CHECK_PACK_SHAPE` | the pack line disagrees with the `--pack-shape` the build was run with — it ends `, shape rect` under `polygon` or `, shape polygon` under `rect`, or it has no shape clause (the 1.5–2.0 form, read as `rect`) under `polygon` | a rigc older than 2.1.0 on `PATH`: rigc 2.0.3 takes `--pack-shape` without a word, packs by rectangles and prints the form with no shape clause, so this refusal is what catches it — `bun install` (this package depends on spine-rigc ^2.10.1); otherwise a rigc problem, report it with `rigc --version` |
|
|
662
670
|
| `BUILD_ARTIFACT_PRESENT` | the packed build lacks its `.json`, `.atlas` or page | a rigc problem; report it |
|
|
663
671
|
|
|
664
672
|
## 7. The bars `check` enforces, and what only an eye answers today
|
|
@@ -671,7 +679,7 @@ implementation's bars):
|
|
|
671
679
|
pages on disk. There is no `spine` gate of its own: `spine-html` holds every rule
|
|
672
680
|
`spine` measures (on the demo, rigc 1.5.1 and 2.0.3: `validate --profile spine`'s 14
|
|
673
681
|
measured rules are all among the build's 23, the 15 others not in `spine`; rigc
|
|
674
|
-
2.1.3: the same 14 among the build's 24, which adds
|
|
682
|
+
2.1.3 and 2.10.1: the same 14 among the build's 24, which adds
|
|
675
683
|
`A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` on the packed pass, 16 not in `spine`; `CH09`
|
|
676
684
|
holds it on every fetched example);
|
|
677
685
|
- **seam**: the setup-pose render against the flat composite of `parts/`: mean
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-parts",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "AI-authored Spine 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through spine-rigc before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"ag-psd": "^31.0.2",
|
|
60
|
-
"spine-rigc": "^2.1
|
|
60
|
+
"spine-rigc": "^2.10.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@esotericsoftware/spine-core": "4.3.13",
|
package/src/check.ts
CHANGED
|
@@ -196,7 +196,7 @@ export const SPINEBOY_YARDSTICK = '1024x256, 40 region(s), 45.8% opaque (alpha >
|
|
|
196
196
|
* `free` is the default here although rigc's own default is `pot`: the packed
|
|
197
197
|
* page is this package's final artifact, and on the two public examples `free`
|
|
198
198
|
* takes the page from 1024x2048 to 967x1338 (covered 56.3 % -> 91.2 %) and
|
|
199
|
-
* from 512x2048 to 479x1166 (49.7 % -> 93.4 %), rigc 1.5.1, 2.0.3
|
|
199
|
+
* from 512x2048 to 479x1166 (49.7 % -> 93.4 %), rigc 1.5.1, 2.0.3, 2.1.3 and 2.10.1 (`rect`) alike. The cost was
|
|
200
200
|
* measured on the same two builds. Each of the 49 idle frames differs from the
|
|
201
201
|
* `pot` build's, by at most 1 level in any channel (demo 2,373 px, sample 651
|
|
202
202
|
* px over all 49). On sample that moves STILL_REGIONS_DARK's reported
|
|
@@ -212,7 +212,7 @@ export const DEFAULT_PAGE_EDGES: PageEdges = 'free';
|
|
|
212
212
|
/**
|
|
213
213
|
* What two packed rectangles may share: spine-rigc's `build --pack
|
|
214
214
|
* --pack-shape`, handed to it verbatim (in spine-rigc's CLI since 2.1.0; this
|
|
215
|
-
* package's range starts at 2.1
|
|
215
|
+
* package's range starts at 2.10.1). `rect` keeps every region's cell apart;
|
|
216
216
|
* `polygon` packs a region that only meshes draw by its emitted hull, so a
|
|
217
217
|
* neighbour may sit inside its rectangle where the hull is not, with the
|
|
218
218
|
* padding kept between footprints — a region attachment stays its rectangle
|
|
@@ -222,7 +222,8 @@ export const DEFAULT_PAGE_EDGES: PageEdges = 'free';
|
|
|
222
222
|
* reason {@link DEFAULT_PAGE_EDGES} is `free`: the packed page is this
|
|
223
223
|
* package's final artifact, rigc gates the footprints on the pages on disk
|
|
224
224
|
* (`A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP`), and the cost is the class already
|
|
225
|
-
* accepted for `free`. Measured with spine-rigc 2.1.3
|
|
225
|
+
* accepted for `free`. Measured with spine-rigc 2.1.3, and again with 2.10.1,
|
|
226
|
+
* whose pages, atlases and idle frames are byte-identical to it (the full entry,
|
|
226
227
|
* spine-core 4.3.13 beside it), `spine-parts build` on the two public examples
|
|
227
228
|
* under `--page-edges free`: `polygon` takes demo's page from 967x1338 (91.2 %
|
|
228
229
|
* covered) to 922x1348 (95.0 %), 3.9 % less area, and sample's from 479x1166
|
package/src/config.ts
CHANGED
|
@@ -840,6 +840,13 @@ function checkMotion(c: Check, v: Json, bones: Set<string>, chains: Map<string,
|
|
|
840
840
|
const m = c.object('config.motion', v, ['duration', 'tracks'], ['blink']);
|
|
841
841
|
if (m === null) return;
|
|
842
842
|
const p = 'config.motion';
|
|
843
|
+
// Every bone property the idle will key, and the tracks that key it, in the
|
|
844
|
+
// order idleMotion writes them: single and chain tracks, then the eyes
|
|
845
|
+
// group, then the brows group.
|
|
846
|
+
const claims = new Map<string, string[]>();
|
|
847
|
+
const claim = (target: string, by: string): void => {
|
|
848
|
+
claims.set(target, [...(claims.get(target) ?? []), by]);
|
|
849
|
+
};
|
|
843
850
|
const duration = 'duration' in m && c.number(`${p}.duration`, m.duration, 'positive') ? (m.duration as number) : null;
|
|
844
851
|
const periodFits = (at: string, period: Json): void => {
|
|
845
852
|
if (!c.number(at, period, 'positive') || duration === null) return;
|
|
@@ -863,6 +870,8 @@ function checkMotion(c: Check, v: Json, bones: Set<string>, chains: Map<string,
|
|
|
863
870
|
if ('lag' in t) c.number(`${at}.lag`, t.lag);
|
|
864
871
|
const links = typeof t.chain === 'string' ? chains.get(t.chain) : undefined;
|
|
865
872
|
if (links === undefined) c.fail('CONFIG_NAME_RESOLVES', `${at}.chain`, `is ${show(t.chain)}; a chain declared in config.bones is required`);
|
|
873
|
+
// A chain track keys rotate on every link (idleMotion), one link per point.
|
|
874
|
+
else for (let k = 0; k < links; k++) claim(`${String(t.chain)}${k}.rotate`, `${at} (chain "${String(t.chain)}", link ${k})`);
|
|
866
875
|
if ('amps' in t && c.array(`${at}.amps`, t.amps, true)) {
|
|
867
876
|
const amps = t.amps as Json[];
|
|
868
877
|
amps.forEach((a, k) => c.number(`${at}.amps[${k}]`, a));
|
|
@@ -877,7 +886,7 @@ function checkMotion(c: Check, v: Json, bones: Set<string>, chains: Map<string,
|
|
|
877
886
|
if (typeof t.bone !== 'string' || !bones.has(t.bone)) c.fail('CONFIG_NAME_RESOLVES', `${at}.bone`, `is ${show(t.bone)}; a declared bone is required`);
|
|
878
887
|
if ('prop' in t && !(SINGLE_PROPS as readonly string[]).includes(t.prop as string)) {
|
|
879
888
|
c.fail('CONFIG_FIELD_TYPE', `${at}.prop`, `is ${show(t.prop)}; one of ${SINGLE_PROPS.join(', ')} is required`);
|
|
880
|
-
}
|
|
889
|
+
} else if ('prop' in t && typeof t.bone === 'string' && bones.has(t.bone)) claim(`${t.bone}.${String(t.prop)}`, at);
|
|
881
890
|
if ('amp' in t) c.number(`${at}.amp`, t.amp);
|
|
882
891
|
if ('period' in t) periodFits(`${at}.period`, t.period);
|
|
883
892
|
if ('phase' in t) c.number(`${at}.phase`, t.phase);
|
|
@@ -886,7 +895,7 @@ function checkMotion(c: Check, v: Json, bones: Set<string>, chains: Map<string,
|
|
|
886
895
|
}
|
|
887
896
|
if ('blink' in m) {
|
|
888
897
|
const b = c.object(`${p}.blink`, m.blink, ['t', 'eyes', 'squash'], ['brows', 'brow_drop', 'still']);
|
|
889
|
-
if (b === null) return;
|
|
898
|
+
if (b === null) return refuseSharedTargets(c, claims);
|
|
890
899
|
if ('t' in b) c.number(`${p}.blink.t`, b.t, 'non-negative');
|
|
891
900
|
if ('squash' in b) c.number(`${p}.blink.squash`, b.squash, 'positive');
|
|
892
901
|
if ('brow_drop' in b) c.number(`${p}.blink.brow_drop`, b.brow_drop);
|
|
@@ -908,6 +917,15 @@ function checkMotion(c: Check, v: Json, bones: Set<string>, chains: Map<string,
|
|
|
908
917
|
if (typeof name !== 'string' || !bones.has(name)) c.fail('CONFIG_NAME_RESOLVES', `${p}.blink.${key}[${k}]`, `is ${show(name)}; a declared bone is required`);
|
|
909
918
|
if (typeof name === 'string') at.set(name, [...(at.get(name) ?? []), k]);
|
|
910
919
|
});
|
|
920
|
+
// The group track keys one property on every member: scaley for the
|
|
921
|
+
// eyes, translatey for the brows — and the brows track is written only
|
|
922
|
+
// when brow_drop is stated beside them (idleMotion). A member named
|
|
923
|
+
// twice is CONFIG_BLINK_GROUP_UNIQUE's, so each bone claims once, at
|
|
924
|
+
// its first index.
|
|
925
|
+
const prop = key === 'eyes' ? 'scaley' : 'translatey';
|
|
926
|
+
if (key === 'eyes' || 'brow_drop' in b) {
|
|
927
|
+
for (const [name, ks] of at) if (bones.has(name)) claim(`${name}.${prop}`, `${p}.blink.${key}[${ks[0]}] (the blink's ${key} group, which keys ${prop} on every member)`);
|
|
928
|
+
}
|
|
911
929
|
// rigc refuses a member named twice (`group "eyes" names member "eye"
|
|
912
930
|
// twice`) at the gate, one stage late — issue #45, the sibling of the
|
|
913
931
|
// empty group above. One refusal per repeated name, in first-seen order.
|
|
@@ -924,4 +942,27 @@ function checkMotion(c: Check, v: Json, bones: Set<string>, chains: Map<string,
|
|
|
924
942
|
}
|
|
925
943
|
if ('still' in b) checkBlinkStill(c, b.still, b.eyes, bones, regions);
|
|
926
944
|
}
|
|
945
|
+
refuseSharedTargets(c, claims);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Two tracks keying one bone property — two single tracks, a single track
|
|
950
|
+
* beside a chain link's rotate or a blink group's member — are one refusal
|
|
951
|
+
* per property, naming every track that keys it (issue #49). rigc refuses the
|
|
952
|
+
* pair at the rig gate (`animation "idle" has two tracks on eye.scaley`), one
|
|
953
|
+
* stage after the input that was wrong; the loader knows every track it
|
|
954
|
+
* writes. The control bones the rig stage adds (`<bone>_ctl`) cannot make two
|
|
955
|
+
* distinct targets one: the rename is the same suffix on every key of a bone,
|
|
956
|
+
* and a declared bone already holding the control's name is the rig stage's
|
|
957
|
+
* refusal, `RIG_CONTROL_NAME_FREE`.
|
|
958
|
+
*/
|
|
959
|
+
function refuseSharedTargets(c: Check, claims: ReadonlyMap<string, string[]>): void {
|
|
960
|
+
for (const [target, by] of claims) {
|
|
961
|
+
if (by.length < 2) continue;
|
|
962
|
+
c.fail(
|
|
963
|
+
'CONFIG_BONE_PROPERTY_KEYED_ONCE',
|
|
964
|
+
`bone property "${target}"`,
|
|
965
|
+
`is keyed by ${by.length} tracks: ${by.join(' and ')}; one track per bone property is required — the idle would hold ${by.length} timelines on ${target}, and rigc refuses two tracks on one bone property. Merge them into one track, or key another property or bone`,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
927
968
|
}
|
package/src/raster/png.ts
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* refuses by name rather than misreading. So adding `pngjs` would have been a
|
|
13
13
|
* second codec for the same bytes, and two codecs are two answers.
|
|
14
14
|
*
|
|
15
|
-
* 🔌 **The deep path is the interface
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* 🔌 **The deep path is the interface.** spine-rigc's `exports` map carries
|
|
16
|
+
* `./*.ts`, so `spine-rigc/tools/plate.ts` is how a consumer reaches the codec
|
|
17
|
+
* and it is what the installed package resolves. If a rigc release narrows the
|
|
18
|
+
* map to omit these paths, this import is what breaks, by name, and
|
|
19
|
+
* `bun run smoke` is where that is seen first.
|
|
20
20
|
*/
|
|
21
21
|
import { assertPng } from 'spine-rigc/src/png.ts';
|
|
22
22
|
import { decodePng, encodePng } from 'spine-rigc/tools/plate.ts';
|