@rig-c/parts 0.0.0-stage → 1.5.4

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +134 -0
  3. package/README.md +433 -2
  4. package/bin/rig-parts.cjs +43 -0
  5. package/cli.ts +1240 -0
  6. package/docs/AUTHORING.md +2387 -0
  7. package/docs/MIGRATION.md +150 -0
  8. package/docs/STABILITY.md +262 -0
  9. package/package.json +70 -4
  10. package/skills/rig-parts/SKILL.md +167 -0
  11. package/src/apng.ts +381 -0
  12. package/src/assemble.ts +2259 -0
  13. package/src/autoamplitude.ts +194 -0
  14. package/src/autoenvelope.ts +404 -0
  15. package/src/automesh.ts +585 -0
  16. package/src/automotion.ts +447 -0
  17. package/src/autoreplay.ts +516 -0
  18. package/src/build.ts +1240 -0
  19. package/src/check.ts +2942 -0
  20. package/src/comfy/client.ts +245 -0
  21. package/src/comfy/index.ts +19 -0
  22. package/src/comfy/painting.ts +136 -0
  23. package/src/comfy/seethrough.ts +171 -0
  24. package/src/config.ts +2283 -0
  25. package/src/contour.ts +1901 -0
  26. package/src/coords.ts +26 -0
  27. package/src/diagnostics.ts +198 -0
  28. package/src/errors.ts +39 -0
  29. package/src/gif.ts +213 -0
  30. package/src/graphs.ts +302 -0
  31. package/src/headbox.ts +173 -0
  32. package/src/heat.ts +313 -0
  33. package/src/inputs.ts +100 -0
  34. package/src/instruments.ts +1145 -0
  35. package/src/keypoints.ts +296 -0
  36. package/src/layers.ts +598 -0
  37. package/src/localweights.ts +217 -0
  38. package/src/mesh.ts +362 -0
  39. package/src/motion.ts +311 -0
  40. package/src/palette.ts +208 -0
  41. package/src/parts.ts +410 -0
  42. package/src/propose.ts +2540 -0
  43. package/src/pyfmt.ts +135 -0
  44. package/src/raster/blur.ts +80 -0
  45. package/src/raster/components.ts +165 -0
  46. package/src/raster/composite.ts +81 -0
  47. package/src/raster/index.ts +9 -0
  48. package/src/raster/morph.ts +116 -0
  49. package/src/raster/png.ts +82 -0
  50. package/src/raster/poly.ts +166 -0
  51. package/src/raster/resize.ts +301 -0
  52. package/src/raster/types.ts +89 -0
  53. package/src/raster/warp.ts +121 -0
  54. package/src/requirements.ts +1240 -0
  55. package/src/rig.ts +1440 -0
  56. package/src/round.ts +62 -0
  57. package/src/scene.ts +1414 -0
  58. package/src/sheet.ts +128 -0
  59. package/src/skeleton.ts +400 -0
  60. package/src/structure.ts +858 -0
  61. package/src/tags.ts +121 -0
  62. package/src/weights.ts +124 -0
  63. package/workflows/seethrough.json +68 -0
@@ -0,0 +1,2387 @@
1
+ # Authoring a character with rig-parts
2
+
3
+ This is the guide an agent authors from. It assumes you cannot see the painting,
4
+ the layers or the rig: what you have is this page, the tool's printed lines, and
5
+ the files it writes. Every refusal the tool prints names a rule, an object, the
6
+ value found and the value required — §6 maps each rule to the field or input that
7
+ has to change.
8
+
9
+ The worked example throughout is [`examples/sample`](../examples/sample): its
10
+ `config.json` is a complete, loading config, `proposal.json` is what the proposer
11
+ wrote for it, and `expected/` is what the reference implementation produced from
12
+ the same inputs — except `check.json`, which this port's `build` regenerates since
13
+ the reference has no judgement lines (§7). `bun run fetch-examples` puts its painting and See-through layers
14
+ into `examples/sample/inputs/`.
15
+
16
+ ## 1. Prerequisites
17
+
18
+ | what | why | how |
19
+ | --- | --- | --- |
20
+ | [Bun](https://bun.sh) 1.2 or later | rig-parts and rig-c are Bun programs | `npm install -g @rig-c/parts` installs the command; it says so if Bun is missing |
21
+ | [rig-c](https://www.npmjs.com/package/rig-c) | compiles, gates, packs and renders every rig; rig-parts writes no Spine data itself | installed with rig-parts as a dependency (`node_modules/.bin/rigc`); with no Spine runtime beside it, it gates with its own validator, and `rigc --version` says `entry: cli_core.ts` |
22
+ | [See-through](https://github.com/shitagaki-lab/see-through), somewhere | the layer decomposition is the input | any route in README *Getting See-through layers*; it runs twice per character, outside this tool |
23
+ | a GPU, wherever See-through runs | See-through is a diffusion model | nothing in rig-parts itself uses a GPU |
24
+
25
+ The painting is yours to bring: a PNG of one character, front-facing, full body,
26
+ taller than wide, on a plain light background. How it was made is not read by any
27
+ stage (`config.generation` is recorded for the optional adapter only).
28
+
29
+ ## 2. The folder
30
+
31
+ Nothing in rig-parts requires a layout; every command takes paths. The one the
32
+ examples use, and the one this guide assumes:
33
+
34
+ ```
35
+ <key>/
36
+ config.json the character config (§3)
37
+ inputs/painting.png the painting
38
+ inputs/st_input_full.png what See-through's full run was fed: the painting, white-padded to a square
39
+ inputs/st_input_head.png what its head run was fed: the square crop at seethrough.head_box
40
+ inputs/layers/full/ the full run (wrapper form: layers.json + parts/<tag>.png), or full.psd
41
+ inputs/layers/head/ the head run, the same
42
+ proposal.json what `propose` wrote (§4, step 7)
43
+ basis.json what each proposed bone rests on, written beside proposal.json (§4, step 7)
44
+ keypoints.json optional: where the figure's joints are, for `propose --keypoints` (§3, *A posed figure*)
45
+ out/ what `build` writes (§5)
46
+ ```
47
+
48
+ ## 3. `config.json`, field by field
49
+
50
+ `src/config.ts` is the schema and the loader. Every key is known or refused
51
+ (`CONFIG_KEY_KNOWN`); a required key that is missing is `CONFIG_FIELD_PRESENT`.
52
+ There are two open doors, and nothing reads what stands behind either. An
53
+ annotation: `note`, and any key ending in `_note`, may hold a string anywhere an
54
+ object is — write a hand correction's reason there, beside the value it corrected
55
+ (the examples do). A record: any key beginning `x-` with a name after it may hold
56
+ any JSON value in the same places — a project's own provenance, such as the gate
57
+ results of a past build (`"x-status": {…}`) or the seeds a search rejected, with
58
+ reasons (`generation.x-seeds_tried`). The record test comes first, so
59
+ `x-seed_note` is a record. `xstatus`, `x_status`, `X-status` and a bare `x-` are
60
+ not record names and are refused like any unknown key. The doors are not open in
61
+ `meshes`, `regions` or `motion.blink.still`: their keys are part names, and an
62
+ `x-` key there is a part like any other. Every refusal of an unknown key, of a
63
+ retired one and of an annotation that is not a string says where a record goes.
64
+
65
+ **Coordinates** in `bones`, `meshes` and `motion` are **rig pixels, y down, origin
66
+ top-left** — the parts' own space, which is the painting times `assemble.rig_scale`.
67
+ `root` is added by the rig stage at the bottom centre of the rig canvas; never
68
+ declare it.
69
+
70
+ "Where it comes from" is one of: **proposed** (a command prints or writes a value
71
+ you copy and check), **authored** (you decide it), **recorded** (a fact about a run,
72
+ read by no CPU stage).
73
+
74
+ | field | read by | where it comes from |
75
+ | --- | --- | --- |
76
+ | `key` | every stage's report | authored; the examples use the directory name |
77
+ | `generation.*` | the optional `comfy` adapter only; no CPU stage | recorded: checkpoint, LoRAs, sampler, prompt parts, `latent`, `seed`, `control` |
78
+ | `generation.control.skeleton` | `comfy paint` — the OpenPose control image, drawn at `latent` | **authored**: a built-in standing pose, `"stand_sides"` or `"stand_clasp"`, or (issue #198) any other string, read as a path relative to the config file's directory to a pose file `{"width": W, "height": H, "points": {"nose": [x, y], "neck": [x, y], …}}` — `width` x `height` is the canvas and must be `generation.latent` (the points are not scaled; `CONFIG_POSE_CANVAS`), and `points` holds the 18 body-18 keypoints of `src/skeleton.ts` by name (`nose`, `neck`, `r_shoulder`, `r_elbow`, `r_wrist`, `l_shoulder`, `l_elbow`, `l_wrist`, `r_hip`, `r_knee`, `r_ankle`, `l_hip`, `l_knee`, `l_ankle`, `r_eye`, `l_eye`, `r_ear`, `l_ear`; `r`/`l` the subject's sides), each `[x, y]` in canvas px, x right, y down, `0 <= x <= W`, `0 <= y <= H`. The image is drawn from the file exactly as from a built-in (same renderer, same size), written as `control_pose-<first 12 hex of its sha256>.png`. A pose file carries no words, so `generation.pose` is required beside it (`CONFIG_FIELD_PRESENT`). The file is read wherever the config is read from a path, every problem at once: no such file — a misspelt built-in lands here too — or one that cannot be read (`CONFIG_FILE_PRESENT`), not JSON (`CONFIG_IS_JSON`), not an object, a size that is not a whole positive px count, a point that is not two numbers (`CONFIG_FIELD_TYPE`), a key besides the three (`CONFIG_KEY_KNOWN`) or written twice (`CONFIG_KEY_UNIQUE`), `points` holding other than the 18 names (`CONFIG_POSE_KEYPOINTS`, with the count found and required), a point off the canvas (`CONFIG_POSE_IN_CANVAS`, naming the point and the canvas size) |
79
+ | `seethrough.resolution` | assemble — each run's canvas must be `resolution` square (`ASSEMBLE_RUN_CANVAS`) | recorded from the See-through runs; 1024 in both examples |
80
+ | `seethrough.steps`, `.seed`, `.offload` | no CPU stage | recorded from the runs |
81
+ | `seethrough.head_box` | inputs (cuts the head image there; optional at the first call); assemble — where the head run sits on the painting | **proposed** by `propose --head-box` from the full run, square, source pixels, shifted inside the painting when needed; the head run must then be fed exactly that crop |
82
+ | `assemble.rig_scale` | assemble | authored; rig pixels per source pixel. 0.5 in both examples (a 1664x2432 painting makes an 832x1216 rig) |
83
+ | `assemble.plan` | assemble; also the list every `meshes`/`regions` key must come from | **proposed** by `assemble --propose-plan`. `[part name, "full" \| "head", See-through tag]`, in draw order back to front. Names are free; roles come from the tag |
84
+ | `assemble.extend_below_crop` | assemble | **proposed** with the plan: `{part, run, tag}` — a head-run part that reaches the bottom of the head crop is continued from a full-run layer, whole connected components |
85
+ | `assemble.patches` | assemble; `meshes`/`regions` keys may name them too | **authored**, optional: `[{name, box, alpha, draw}]` — an extra part cut from the **painting** itself, for figure no See-through layer holds (a hem both runs dropped). `box` is `[x0, y0, x1, y1]` in **rig** pixels, `x1`/`y1` exclusive — the space of `parts.json` and `recomposite_rig.png`, where the hole is found; `alpha` is `"silhouette"` (the painting's figure silhouette inside the box, the one `--seam silhouette` uses) or `"box"` (the whole box); `draw` is `"back"`, `"front"` or `{"before": "<plan part>"}`. A patch is always a region: its bone is `regions.<name>`, in the one place every region's bone lives, and a `meshes` entry for it is refused. See §5 for how to place one |
86
+ | `assemble.cuts` | assemble; `meshes`/`regions` keys may name the new parts too, and must (each is a part like any other) | **authored**, optional (issue #170): `[{from, into, polygon, draw, overlap, under}]`, `under` optional — a plan part's layer cut by a polygon the author draws: the pixels of plan part `from` whose **centre** lies inside `polygon` (even-odd; a centre exactly on an edge is outside) leave `from` and make the new part `into`, colour and alpha, so every pixel of the layer ends in exactly one of the two. `polygon` is `[[x, y], …]`, at least three points, non-negative integer **rig** pixels — the space of `parts.json` and of the patches' boxes. The file that shows the layer in those coordinates is `assemble`'s own `rig/parts/<from>.png`, placed at its `parts.json` `x`, `y` (the PNG's pixel `(i, j)` is rig pixel `(x + i, y + j)`), or `render/recomposite_rig.png`, the whole rig 1:1; the contact sheets (`sheet`) are scaled and carry no coordinates. `draw` is `"back"`, `"front"` or `{"before": "<plan part>"}`, as for a patch (to keep the setup pose, draw the piece immediately in front of `from` — `{"before": "<the plan part after from>"}`, or `"front"` when `from` is the last plan part; `{"before": "<from>"}` draws it behind `from`, which `under` refuses as `ASSEMBLE_UNDER_DRAW` because the fill would hide it). `from` must be a plan part (`CONFIG_NAME_RESOLVES`; a patch or another cut's piece is refused), `into` a new name (`CONFIG_PART_UNIQUE`), the polygon three or more integer points (`CONFIG_FIELD_TYPE`). The piece's `parts.json` record carries `from`'s `<run>:<tag>` and its own counts, each split from the layer's by where the pixel lies; it is a mesh or a region under its own name, and the coverage rule holds it like a plan part. What the config cannot know the stage refuses: a polygon point past the rig (`ASSEMBLE_CUT_INSIDE`), two cuts of one part sharing a pixel (`ASSEMBLE_CUT_OVERLAP`), a cut that takes none of the layer's opaque pixels or leaves `from` none (`ASSEMBLE_CUT_PIXELS`). `overlap` is required, a non-negative integer of rig pixels with no default (`CONFIG_FIELD_TYPE`): the **band** — the polygon's pixels within `overlap` px of its edge by Chebyshev distance (the polygon's mask less its erosion by a `2 * overlap + 1` square) — stays in `from` as well as going to the piece, so the piece's resampled edge has art under it; the counts stay a strict partition and the band is the base's extra copy, printed on the `cut:` line. At `0` the two abut and `check`'s seam bar judges the edge between them (`CHECK_SEAM_WITHIN_BAR`, §5). A band only covers the edge at rest: a piece that swings well clear of where it was painted shows the hole it leaves, and a thin piece's band shows a ghost of the piece. `under` (issue #193), optional, is the author's image of what lies under the piece: a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE` unless a non-empty string), in rig pixels over the polygon's **bounding box** — the least and greatest polygon x and y, so the image is `max x − min x` by `max y − min y` and its pixel `(i, j)` is rig pixel `(min x + i, min y + j)`. Where the cut removed pixels from `from` — the piece's pixels (alpha above 0) less the band — `from` takes the image's pixel, colour and alpha wherever the image's alpha is at least the alpha the cut removed there (**filled**); elsewhere `from` stays empty there (**left empty**): a transparent pixel is the author's statement that nothing lies under it (the background, or a part drawn behind), and a less opaque one is not half-filled. The image is read nowhere else, so its pixels over the band, over the box outside the polygon or where the layer had no art never change `from`. The `cut:` line then ends `; under <n> px filled, <m> px left empty` (`n + m` is every removed pixel), so an unintended hole is a number in the log, not a guess; the filled pixels count in `from`'s `opaque_px` and `occluded_px` (the piece is drawn over them), never as visible or projected. The stage refuses: a file that is not there (`ASSEMBLE_UNDER_PRESENT`) or not a PNG (`ASSEMBLE_UNDER_DECODES`), an image not exactly the box's size (`ASSEMBLE_UNDER_SIZE`, both sizes given), an image that fills none of the removed pixels — all transparent there, or another file (`ASSEMBLE_UNDER_EMPTY`; to leave the whole hole, drop `under`), and a piece drawn behind `from` (`ASSEMBLE_UNDER_DRAW`: the fill would hide it; draw it `"front"` or `{"before": "<the plan part after from>"}`). Producing the image is the author's, by whatever means; absent, `from` keeps only its band |
87
+ | `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip}` whose links are named `<chain>0 … <chain>n`. Parents come before children. In `rig.json` each link is **turned along its chain** — `rotation` is the direction from its origin to the next link's (the last link's to `tip`) and `length` that distance (issue #73), so a physics constraint added downstream finds a lever; under `--idle-keys ctl` the link's `<link>_ctl` carries the turn and the length and the link sits at local rotation 0 beneath it. A single bone is never turned; one whose parent is a link is turned back upright |
88
+ | `meshes.<part>` | rig | **proposed**, then corrected. Exactly one of `grid` (the lattice: cell size, px — what `propose` writes), `contour` (the outline mode, authored; the next row and "A contour mesh" below) or `auto` (the automatic mode, authored; "An automatic mesh" below) — two, or none, is `CONFIG_MESH_MODE`. In every mode: `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]]`), and, **authored**, optional, `exponent` (issue #161): a number above 0 that replaces the 2 in that rule, `w = 1/(d + r)^exponent` — a larger one hands each vertex more fully to its nearest bone; absent is 2 and changes no byte, a value that is not a number above 0 is `CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`, and a declared one is echoed as `exponent` in the mesh's `mesh_report.json` row; the exponent is the distance rule's, so declared beside `rule: "heat"` it is refused (`CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`). 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. Optional `rule` (issue #161): `"distance"` — the rule above, and what an absent `rule` means (every byte is the same) — or `"heat"`, bone heat over the part's own silhouette, in every mode: the lattice's art (alpha above 8) with holes filled, or for `contour` and `auto` the silhouette their outline is traced from (that mode's art — alpha above 8 for `contour`, alpha 1 and above for `auto` — less any island a declared `stray` left out, holes filled, grown by its `margin`); each bound bone's sources are the silhouette pixels whose centre lies within 0.75 px of its segment (a pixel two bones reach goes to the nearer, a tie to the first listed), held at 1 for that bone and 0 for the others; the field is solved by row-major Gauss–Seidel over-relaxation to a sweep change under 1e-7 within 20000 sweeps; a vertex reads the nearest silhouette pixel and takes the same cap and floor (and `regions`) as the distance rule; `r` is not read. The mesh's `mesh_report.json` row gains `heat` (the constants, each bone's sources and sweeps, the residual). Any other value is `CONFIG_FIELD_TYPE`; a bound bone with no source pixel is `RIG_HEAT_BONE_SOURCE`, an island no bone has a source in `RIG_HEAT_ISLAND_SOURCE`, a solve past its sweep bound `RIG_HEAT_CONVERGED` — none is weighted 0 |
89
+ | `meshes.<part>.contour` | rig | **authored**, optional (issue #84): `{tolerance, margin, spacing, budget?, stray?, regions?}`. `tolerance` — rig-c's Douglas–Peucker tolerance on the traced outline, px, 0 or more; `margin` — how far the silhouette grows before it is traced, px: 0, or 1 or more (a pixel joins when its centre lies within `margin` of an art pixel's centre; a value above 0 and below 1 adds nothing and is refused, `CONFIG_FIELD_TYPE`); `spacing` — the background interior spacing, px, above 0; `budget` — the most vertices the mesh may have (refused above it, nothing thinned); `stray` — the largest island, in art pixels, that may be left out of the mesh (absent: none is); `regions` — local deformation regions, each `{name, shape, bone, spacing, band}` with `shape` `"circle"` (`cx, cy, r`) or `"polygon"` (`points`, 3 or more): `bone` is the region's control bone (any bone `bones` declares), `spacing` the interior spacing inside the region and its band, `band` the width the bone's weight falls across. Every length and position is **rig px**, like every other in the config; the part image is the rig less `(x − 4, y − 4)` (its box less the pad), a translation, so a length is the same number in both. A region's `cx`, `cy`, `r`, `band` and `points` are multiples of 1/256 px (`CONFIG_FIELD_TYPE` otherwise); region names are unique per mesh (`CONFIG_REGION_NAME_UNIQUE`). The alpha threshold is not a field: art is alpha above 8, the lattice's and `check`'s |
90
+ | `meshes.<part>.ribs` | rig | **authored**, optional (issue #188), in the `contour` and `auto` modes: `{<chain>: {stations}, …}`, each key a chain this mesh's `segments` name. Each link of the chain gets a row of vertices across the part at its origin and at `stations` points evenly between its origin and its end (`stations`, a whole number, 0 or more, required; a chain of L links carries L × (stations + 1) ribs), each rib a row of vertices across the part, kept as a row of locked edges; the rule is "Ribs along a chain" under "A contour mesh". Absent places none and changes no byte. Refused: a lattice mesh with ribs (`CONFIG_RIBS_MODE` — its vertices are its grid's corners), a key that is not one of the mesh's chains (`CONFIG_NAME_RESOLVES`), an object naming no chain or a `stations` that is not a whole number from 0 (`CONFIG_FIELD_TYPE`); at the rig stage `CONTOUR_RIB_ART`, `CONTOUR_RIB_LINK`, `CONTOUR_RIB_CROSSING`, `CONTOUR_RIB_EDGE` (the contour refusals). The mesh's `mesh_report.json` row gains `ribs` — the declaration, the ribs placed and the vertices they hold — and its contour report (the source's, in `auto`) lists each rib's vertices |
91
+ | `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`) |
92
+ | `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 |
93
+ | `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`. A chain link is turned along its chain (the `bones` row), so a `translatex`/`translatey` key on a bone whose parent is a turned link, or a `scale`/`shear` key on a turned link, would move or stretch along the link and not along the picture's axis: the rig stage refuses it (`RIG_KEY_FRAME_UNTURNED`); `rotate` is keyed on any bone |
94
+ | `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) |
95
+ | `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`) |
96
+ | `motion.animations_from` | rig; check (named, not measured) | optional, never proposed (issue #183): a path, relative to the config file's directory, to a file in rig-c's motion-spec shape, `{"spec": "rigc-motion/1", "animations": {"<name>": {"duration": …, "tracks": [...]}, …}}` — an expression, a gesture, anything beside the idle. On every `rig` and `build` each of its animations is written into `rig/motion.json` after `idle`, as read: nothing added, re-keyed or judged here, and each is rigc's to accept or refuse at the rig stage's gate, in rigc's words. It may name what `motion.json` already holds — the bones `bones` declares, the easings `shut` and `open`, the groups `eyes` and `brows` when the blink writes them — and nothing else from the file reaches it. The loader refuses, every problem at once: no such file or one that cannot be read (`CONFIG_FILE_PRESENT`), text that is not JSON (`CONFIG_IS_JSON`), a file that is not an object, a `spec` other than `"rigc-motion/1"`, an `animations` that is not a table of objects (`CONFIG_FIELD_TYPE`), any other key — rig-c's `easings`, `groups`, `setup`, `physics`, `mix`, `archetype` and `cut` included, which would be read and not written (`CONFIG_KEY_KNOWN`), an animation named `idle` (`CONFIG_ANIMATION_NAME_FREE`), and a key written twice in one object of the file, which JSON would fold into its last value without a word (`CONFIG_KEY_UNIQUE`). `check` measures the idle alone and prints one line naming the others (`beside the idle: …`). Absent, `motion.json` holds the idle alone, byte for byte as before |
97
+ | `constraints` | rig (handed to rig-c as `rig.json`'s `constraints`); `compare` and the coverage lines (roles) | **authored**, optional; `propose` writes none. A list of constraints in **rig-c's own rig-spec shape** (`RigConstraint`: `ik`, `transform`, `path`, `physics`, `slider` — rig-c's `src/rig.ts` documents every field), in the order rigc applies them. The loader checks only what this package owns: each entry is an object with a `type` rigc names (`CONFIG_CONSTRAINT_TYPE_KNOWN`) and a non-empty `name`, unique within its kind as rigc finds constraints (`CONFIG_CONSTRAINT_NAME_UNIQUE`; an ik and a transform may share one); every bone it names — `bones`, an ik's `target`, a transform's `source`, a physics or slider constraint's `bone` — is a bone `bones` declares, a chain link as `<chain><i>` (`CONFIG_NAME_RESOLVES`); the bone a constraint follows does not sit under a bone it drives (`CONFIG_CONSTRAINT_TARGET_DETACHED`). Every other field (`mix`, `softness`, `properties`, a path's `slot`, a slider's `animation`) and every value is rigc's to accept or refuse at the rig stage's gate, in rigc's words (`RIG_RIGC_GREEN`) — an ik's `bones` as a shape too: from rig-c 2.15.0 rigc refuses an ik over more than two bones, or over a pair whose second bone is not the first's child (issue #103; this loader refused both itself until then). A note and an `x-` record may ride on an entry; the rig stage leaves them out of what it hands rigc. Under the default `--idle-keys ctl` a two-bone ik whose child link the idle keys is split by that link's control, and rigc refuses the pair: run `rig --idle-keys direct` or `build --idle-keys direct` (the rig stage adds that sentence, naming the flag on the command that ran, to rigc's line; `build` forwards it to its rig stage, issue #95). Each bone a constraint follows is declared to rigc as `invariants.detached` from every bone it drives, which rigc's gate rule `A25` checks |
98
+
99
+ **A scene target is an ordinary bone.** A target the scene places — a point a hand
100
+ reaches for, a face something turns toward — is a single bone parented to `root`,
101
+ `{"name": "tgt", "parent": "root", "at": [x, y]}`, that a constraint names as its
102
+ `target` (an ik) or `source` (a transform). There is no other bone kind: it binds no
103
+ art, nothing in the idle keys it, and `compare` and the coverage lines call it a
104
+ `target`. Its `at` is where it rests; the scene moves it later. The rig stage declares
105
+ it detached from the bones its constraint drives (`invariants.detached`), so rigc's
106
+ gate refuses the built rig if it is ever re-parented under them. What `--idle-keys`
107
+ does to a constraint, measured through rig-c 2.10.1 on the selftest's rig
108
+ fixture (a two-link chain whose links the idle keys and a mesh is weighted to, its
109
+ 4 s idle rendered at 12 fps), the first row again through 2.15.0 (issue #103); the
110
+ selftest's `RG56`–`RG58` hold every row on the installed rigc:
111
+
112
+ | constraint on the chain | `--idle-keys ctl` | `--idle-keys direct` |
113
+ | --- | --- | --- |
114
+ | ik over both links, target under `root` | refused by rigc (from 2.15.0; `RIG_RIGC_GREEN`): `<link1>_ctl` stands between the links, so the pair is not a parent and its child — at 2.10.1 it gated green and the tip missed the target by 2.58 rig px in every frame. rigc's sentence ends with a remedy a config cannot follow (it names the control); the rig stage adds one naming `--idle-keys direct` on the command that ran | gates green; tip on the target in every frame (largest gap 1.7e-7) |
115
+ | ik over one link (an aim) | gates green | gates green; the same per-frame tip distances as `ctl` (0.67 to 0.90 rig px; an aim is not a reach) |
116
+ | physics on a link (`rotate`) | gates green | gates green; the link's world rotation within 1.7e-9 degrees of `ctl`'s in every frame |
117
+ | transform on a link (`rotate` from the target) | gates green; the link's world rotation is the target's | the same |
118
+
119
+ The blink's timing is the tree's (`BLINK` in `src/motion.ts`), in seconds after
120
+ `motion.blink.t`:
121
+
122
+ | phase | eyes (`scaley` 1 → `squash` → 1) | brows (`translatey` 0 → `-brow_drop` → 0) | easing |
123
+ | --- | --- | --- | --- |
124
+ | shut | 0.07 | 0.08 | `shut` |
125
+ | hold | **0.084** (the reference: 0.04) | **0.084** (the reference: 0.04) | none |
126
+ | open | 0.16 | 0.20 | `open` |
127
+
128
+ The holds are the one departure from the reference implementation, and the reason is
129
+ the loop (issue #32). `check` renders the idle at 12 fps, a frame every 0.083333 s, and
130
+ the loop is encoded from those frames. A 0.04 s hold is shorter than a frame, so it
131
+ held a frame only for some `t`: at the examples' 2.3 s the eyes were shut from 2.37 s to
132
+ 2.41 s, between frames 28 (2.333 s, `scaley` 0.8356) and 29 (2.417 s, 0.2435), and over
133
+ every key time the 6-decimal grid can write, 129,999 of the 250,000 phases against the
134
+ frame grid showed no closed frame. A closed window at least one frame long holds a frame
135
+ wherever it starts, so the hold is 1/12 s rounded up to the third place; the brows' hold
136
+ grew by the same 0.044 s, so they still reach their drop 0.01 s after the lids shut and
137
+ leave it 0.01 s after the lids open. The rig stage refuses a hold under one frame by
138
+ name (`RIG_BLINK_HOLD_SPANS_A_FRAME`, §6), counted over the same 250,000 phases.
139
+
140
+ The proposer's reach (from `src/propose.ts`): roles come from each part's tag —
141
+ `face` makes `hip`/`chest`/`neck`/`head`, the eyewhites make the eye bones (irides and
142
+ lashes ride them, or `head` on a side with no eyewhite), brows are
143
+ told apart by position, `bottomwear` makes the hip and three skirt chains,
144
+ `handwear` makes sleeve chains (one blob or two), `front hair` makes fringe chains
145
+ and a lock chain for each strand that hangs below the chin, `back hair` makes a bun
146
+ bone or two hanging chains, and `headwear`/`earwear` get a rigid bone and a pendant
147
+ chain — or, when the accessory has hanging strands, one pendulum chain per strand
148
+ (below). What it cannot know — something painted inside another layer that should
149
+ swing, hair of another shape, whether an accessory swings — is yours to add, and
150
+ `notes` in `proposal.json` says where it guessed.
151
+
152
+ **A posed figure: `propose --keypoints`.** The proposer's ratios assume a
153
+ standing figure: the hip below the chest, sleeves hanging from the shoulders. A
154
+ seated, reclining or otherwise posed figure is told where its joints are with one
155
+ explicit file (issue #75, `src/keypoints.ts`). A pose file named by
156
+ `generation.control.skeleton` is not that file and is not offered as its default:
157
+ the control skeleton is what was asked of the model, not where the joints ended up
158
+ (a consumer measured a generated picture not following the control's raised hand),
159
+ so `keypoints.json` is read off the generated picture:
160
+
161
+ ```json
162
+ {
163
+ "spec": "spine-parts-keypoints/1",
164
+ "space": { "units": "painting-px", "origin": "top-left", "y": "down" },
165
+ "width": 1664, "height": 2432,
166
+ "source": "what produced this file (recorded, never interpreted)",
167
+ "people": [
168
+ { "id": "a", "joints": {
169
+ "neck": { "state": "observed", "at": [826, 464] },
170
+ "l_elbow": { "state": "occluded", "at": [1030, 926], "score": 0.4 },
171
+ "r_ear": { "state": "missing" } } }
172
+ ]
173
+ }
174
+ ```
175
+
176
+ The space is stated and any other is refused (`KEYPOINTS_SPACE_STATED`); `width`
177
+ and `height` are the painting's and are held to `--source` (`KEYPOINTS_IMAGE_SIZE`).
178
+ Joint names are the body-18 names of `src/skeleton.ts` (`nose`, `neck`,
179
+ `r_shoulder` … `l_ear`), and `r` / `l` are the **subject's** sides. Each joint is
180
+ `observed` (with `at`), `occluded` (`at` optional — when given it is the producer's
181
+ estimate and said as that) or `missing` (no `at`); a joint the file does not list is
182
+ missing. A `score` is the producer's and is printed beside the joint; nothing here
183
+ computes one. With more than one person, `--person <id>` names one; nothing picks.
184
+ Painting px reach rig px by the overlay's own map, `x * W/width`, `y * H/height`
185
+ (0.5 for both examples).
186
+
187
+ What a joint with a position places, as given — no ratio is applied over it:
188
+
189
+ | bone | from | when the joint has no position |
190
+ | --- | --- | --- |
191
+ | `neck` | `neck` | the rule: half way from the chin to the neck part's bottom (or 0.12 face heights below the chin), on the eye axis |
192
+ | `hip` | the midpoint of `r_hip` and `l_hip` — a point between two joints, said as that | the hip rule (bottomwear top, the waist, or 0.32 of the figure); one hip alone is said to be unused |
193
+ | `chest` | its rule, half way from neck to hip, taken along the line between the placed ends | the rule as before when neither end is a joint |
194
+ | `sleeve_<s>` | `<s>_shoulder` → `<s>_elbow` (two links), tip at `<s>_wrist`, with the two-blob rule's first two amplitudes and its chest stub laid along the upper arm — when every handwear part over 500 px holds one such wrist within 15 px of its art (the larger of the x and y gaps, the LINT's own tolerance), or is bound one by `--wrist-snap` (below) | the sleeve rule places every sleeve chain, and each arm joint's note says why: for a wrist on no part, `… lies on the art of no handwear part within 15 px, so it does not say which sleeve it ends; the nearest is <part>, <d> px from its art by the larger of the x and y gaps (--wrist-snap <d> would bind it)` — `(beyond --wrist-snap <px>)` when the flag was given and fell short, and `the nearest are <a> and <b>, each <d> px …, so no tolerance says which` when two parts tie |
195
+ | `eye_<s>`, `head`, the face box | not moved: they are measured off the parts | the note prints the eye joint's distance from the eye bone, with no bar |
196
+
197
+ **`--wrist-snap <px>`** (issue #199), with `--keypoints` only, declares a tolerance
198
+ for a wrist that lies on no handwear part within the built-in 15 px — hands folded
199
+ together, or a sleeve painted over the hand. `<px>` is an integer above 15 (the
200
+ distance is counted in whole px, so a fraction would bind exactly what its floor
201
+ binds, and 15 or less binds nothing the built-in does not); anything else is refused
202
+ as a usage error naming the value, as is the flag without `--keypoints` or with
203
+ `--from-config`, which proposes nothing. Such a wrist is measured to every handwear
204
+ part's art by the larger of the x and y gaps — the measure of the built-in 15 px —
205
+ and bound to the one nearest part when it is at most `<px>` away; two parts tied
206
+ nearest bind neither. A bound wrist's joint lines say `(the wrist is bound to <part>
207
+ by --wrist-snap)`, and a note says which wrist went to which part and how far:
208
+
209
+ wrist snap: r_wrist at [x, y] lies on no handwear part within 15 px; --wrist-snap <px> bound it to <part>, the nearest, <d> px from its art by the larger of the x and y gaps
210
+
211
+ A wrist already on a part is not moved by the flag, and without the flag nothing
212
+ changes, byte for byte. Choosing the tolerance automatically is not done: the note
213
+ for an unbound wrist prints the distance that would have bound it.
214
+
215
+ **One handwear part holding both wrists** — two sleeves and the hands decomposed as
216
+ one piece, not clasped at the midline — gets both sleeve chains on its one mesh, as
217
+ before, and a note says so rather than leaving two chains to share the part
218
+ silently:
219
+
220
+ one handwear part holds both wrists: r_wrist and l_wrist both end on <part>, so chains sleeve_r and sleeve_l share its mesh and its art bends with both arms; to separate them, cut one sleeve out of <part> with an assemble.cuts entry (a polygon round it) and propose again
221
+
222
+ The `assemble.cuts` entry (§3; §5, *Cutting a piece out of a layer*) splits the part; the
223
+ proposer does not.
224
+
225
+ Every other joint (`nose`, knees, ankles, ears; the eyes with no eyewhite) is named
226
+ with its state in one note: no bone is authored from it. Every joint the proposer
227
+ could use gets its own note — `used as given for bone X`, `used as the producer's
228
+ estimate for bone X`, or `missing: the rule "…" placed bone X instead` — after every
229
+ note the rules wrote. LINT then reads the torso by the relations the joints declare:
230
+ where `neck`, `r_hip` and `l_hip` all have a position, along the line from the neck
231
+ to the hips' midpoint the chest must fall strictly between them, the hip past the
232
+ chest, and the hip nearer the hips than the neck — these replace the two screen-y
233
+ torso lines; and each `sleeve_<s>` chain's links and tip must advance from
234
+ `<s>_shoulder` toward `<s>_wrist`. The first lines say which rule set ran and on what
235
+ basis (`lint rule set: joints …` or `lint rule set: screen — …`). The off-art check is
236
+ unchanged, and `--from-config … --keypoints` lints a corrected config the same way.
237
+ Without `--keypoints` nothing changes, byte for byte. Two rules still read the screen
238
+ under keypoints and are not this file's to change: the skirt chains hang down from
239
+ the hip, and a part no rule claims rides the trunk bone its centre is nearest by y.
240
+
241
+ **A figure with no `face` part** (turned away, or half hidden behind a partner,
242
+ so See-through found hair and a neck and no face) still gets a proposal. The face
243
+ box every rule scales by is derived from the head run's hair (`front hair`,
244
+ `back hair`: their highest top) and its `neck` (its top, and its centre x for the
245
+ axis): with `L` the span from the hair's top to the neck's top, the face is
246
+ 1.067 `L` tall, its top 0.131 `L` below the hair's, its width 0.798 of its height.
247
+ Those ratios are the means of the two public examples (`demo` 1.073 / 0.105 /
248
+ 0.820, `sample` 1.061 / 0.158 / 0.777) — a sample of two. On each example
249
+ re-assembled without its face, the derived box put `head` and `neck` within
250
+ 5.0 px (0.038 face heights) of the with-face proposal's, `chest` and `hip` at
251
+ the same pixel. The first note says so —
252
+ `no face part: face box derived from the head run's hair (…; top y=…) and neck (…), a span of … px — … (ratios measured on the two public examples); …`
253
+ — and names each face-reading rule left with nothing to read (eye bones and blink,
254
+ brows, mouth). Correct `head` and `neck` against the overlay. With no head-run
255
+ hair or neck there is nothing to derive from, and `PROPOSE_FACE_PRESENT` still
256
+ refuses (§6).
257
+
258
+ Two rules read the figure rather than one tag, and each says so in a note:
259
+
260
+ - **The hip.** It sits 0.14 face heights below `bottomwear`'s top edge — unless
261
+ that is above 0.25 of the figure's height (every part's union, top row to bottom
262
+ row), which is a long robe tagged `bottomwear` that starts at the collar. Then
263
+ the note `hip: <part> (<from>) starts at y=…, so its top + 0.14 face heights
264
+ (y=…) is above 0.25 of the figure height (y=…, figure y …): its top edge is not
265
+ the waist` is followed by one of `hip from the waist: silhouette narrowest at
266
+ y=170 (width 51 px, shoulders 80 px at y=96), hip 0.14 face heights below it` —
267
+ the torso layers (`neck`, `neckwear`, `topwear`, `bottomwear`, `legwear`,
268
+ `footwear`) at their narrowest between the shoulder line (their widest row
269
+ within one face height below the neck) and the figure's middle, and at most
270
+ 0.8 of the shoulders — or `hip from 0.32 of figure height (no waist found: …)`,
271
+ naming why. Without `bottomwear` the hip is 0.32 of the figure height, as before
272
+ (`no bottomwear: hip from 0.32 of figure height`). Neither published example
273
+ meets either branch: their hips sit at 0.436 (`demo`) and 0.342 (`sample`) of
274
+ their figures.
275
+ - **Clasped hands.** When every `handwear` part together is one blob (over
276
+ 500 px) at most half the shoulders' width and centred within a quarter of it
277
+ of the eye axis, it is the hands clasped in front, not two sleeves: it rides
278
+ `hip` as a region, no sleeve chain is made, and the note reads `handwear is one
279
+ blob (<parts>) WxH px centred at x=…: at most 0.5 of the shoulder width (… px
280
+ at y=…) and within 0.25 of it of the eye axis x=…, so clasped hands: a region on
281
+ hip, no sleeve chains`. Two sleeves hanging from the shoulders are at least as
282
+ wide as them: the examples' one-blob sleeves are 1.51 and 1.58 shoulder widths,
283
+ and keep their two chains.
284
+
285
+ **Hanging strands** (tassels, cords, drop earrings) on a `headwear` or `earwear`
286
+ layer — the two tags the proposer has an accessory rule for; any other tag rides a
287
+ trunk bone as a region and says so in its own note. A strand is an 8-connected
288
+ sub-shape of the layer's **pendant rows** (the run of rows at the bottom narrower
289
+ than 35 % of its widest row; the whole layer for `earwear` or a layer that is over
290
+ 80 % pendant) that is at least **3×** as tall as wide, at least **0.2** of the
291
+ layer's height and at least **10** rows. On the public examples nothing is one: the
292
+ demo's hairpin pendant sub-shapes measure 0.54 and 0.89 tall-per-wide and its
293
+ earrings 1.04 and 1.08, so both proposals are unchanged. Every layer with strands
294
+ gets exactly one note carrying each strand's column centroid, first–last row and
295
+ width, and what became of it (two of the selftest's fixtures, PR12 and PR15):
296
+
297
+ crown (head:headwear): 2 hanging strands at x=7,92, y 16-55,16-55, width 5,5 -> pendulum chains hairpin_strand0_, hairpin_strand1_
298
+ veil (full:headwear): 1 hanging strand at x=20, y 120-159, width 5 -- no chain proposed (it rides the head as a region)
299
+
300
+ `-- no chain proposed` is the line to act on: that strand hangs stiff unless you add
301
+ a chain. It is written for a second layer of the tag (a region cannot swing) and for
302
+ a strand whose chain would leave the art (`...; no chain proposed at x=151 (a chain
303
+ down it would run off the art)`). The chain rule: `<bone>_strand<k>_` (links
304
+ `<bone>_strand<k>_0`, `_1`), two links at the strand's top row and 0.45 of the way
305
+ down, each at the strand's column centroid in a 24-row band, tip on its last row —
306
+ the reference tassel's shape and sway (`amps [4, 7]`, period 2, phase 0.2, lag
307
+ 0.12). On a layer with a body it hangs from the layer's bone, as the tassel does,
308
+ and the mesh adds one segment of that bone across the body's full width at its mean
309
+ row, so the body stays on its bone (on the selftest's crown: 0.55 px off it over the
310
+ idle with that segment, 1.07 without; the tassel tips swing 5.0). On an all-pendant
311
+ layer each chain hangs from `head`, with a `head` stub above each strand. Strands
312
+ replace the single pendant chain: a chain at the mean x of two strands hangs between
313
+ them.
314
+
315
+ **The lash that carries a crease** (issue #26). See-through sometimes paints the
316
+ double-eyelid crease into the `eyelash` layer, and the blink squashes the whole layer
317
+ about the eye bone — so the crease is squashed with it, on one eye and not the other.
318
+ `propose` notes an `eyelash-r`/`-l` part that reaches more than 35 % of its own height
319
+ above its eyewhite's top, or is more than 1.2 times the height of its pair; on the two
320
+ examples the four lashes reach 17-26 % (4-6 px, the lash line itself) and the pairs are
321
+ 1.00 and 1.04 apart, so neither is noted. When such a lash has a row between its top and
322
+ the eyewhite's top with no art across its whole width, the lowest such row is proposed as
323
+ `motion.blink.still` on `head`, and the rows above it stop blinking; when it has none, the
324
+ note says so and nothing is cut — clear a row between the crease and the lash line in the
325
+ part, or leave the crease to blink. The cut has to be a clear row: two pieces cut through
326
+ art are each resampled against their own transparent edge, and on the demo example a cut
327
+ through its lash line changed 36 px of the setup-pose render (up to 7 levels, at render
328
+ scale 0.942) and up to 21 levels in the idle frames where the head rolls; a clear-row cut
329
+ changed none, at rest or rolled. The eye bone is not moved: scaling about the eyewhite's
330
+ top edge instead would close the eye upward onto the upper lid and still move whatever sits
331
+ above it by (1 − squash) × its height above the pivot, so the cut, not the pivot, is what
332
+ holds the crease.
333
+
334
+ ### A contour mesh (`meshes.<part>.contour`, issue #84)
335
+
336
+ The lattice keeps every grid cell that holds an art pixel, so its boundary and its interior
337
+ density are one number: a finer `grid` tightens the outline and densifies the whole part at
338
+ once. A contour mesh separates them. Its outline is rig-c's own trace of the part's
339
+ silhouette — the art with its holes filled, grown by `margin` (a pixel joins when its centre lies
340
+ within `margin` of an art pixel's centre: at 1 its four neighbours, from √2 its 3x3 block), with
341
+ any diagonal pinch the growth made filled (both clear pixels of it, repeated until none is left)
342
+ — then `simplifyClosedPolygon(tolerance)` (issue #106: this replaced rigc's `offsetPolygon`,
343
+ which folded the outline over any notch narrower than twice the margin; a grown silhouette is a
344
+ set of pixels and its trace cannot cross itself — a narrow notch grows shut instead). Its
345
+ interior vertices are the ones declared — each region's boundary, its band's outer edge and its
346
+ own `spacing` grid inside region and band, then the background `spacing` grid — and the
347
+ triangulation is a constrained Delaunay one that never crosses the outline (`src/contour.ts`).
348
+ Where the outline passes through a region and its band, the outline is cut there at the
349
+ region's `spacing` too (issue #110): points are inserted exactly on its edges, held to the same
350
+ keep radius (half the spacing) from the edge's ends and from each other, so the outline encloses
351
+ the same pixels and only gains vertices. Without it an outline edge crossing the band is held by
352
+ its two ends alone — no interior point may sit within half a spacing of the outline — and one
353
+ long edge from inside the region (`g` = 1) to past its band (`g` = 0) spreads the region's
354
+ motion along its whole length. A region whose band reaches no outline edge changes no hull
355
+ vertex; one that does keeps every hull vertex in the same order from index 0, and a vertex moves
356
+ up only by the points inserted before it.
357
+
358
+ **Weights.** A region's bone takes `g` at a vertex: 1 inside the region (its edge included),
359
+ `1 − d/band` at a distance `d` past it, 0 at `d ≥ band` (with `band` 0, a step) — linear, no
360
+ curve. The rest, `1 − g`, is shared among the segment bones exactly as the lattice shares a
361
+ vertex (`w = 1/(d + r)²`, at most 4 bones, the 0.03 floor inside that share). A vertex no
362
+ region reaches is weighted and rounded exactly as a lattice vertex at the same point would be.
363
+ With a region present: a region bone that is also a segment bone is one entry,
364
+ `g + (1 − g)·share`; when the segment share already holds 4 bones and the region's bone is not
365
+ one of them, the lightest segment bone is dropped (never `g`); the 0.03 floor is not applied
366
+ again to the scaled shares; the weights are rounded to 5 places, an entry rounding to 0 is
367
+ dropped, and the remainder goes to the heaviest entry, so each vertex sums to exactly 1
368
+ (`src/localweights.ts`, `roundShares` in `src/rig.ts`). The 1 and the 0 are decided exactly on
369
+ the 1/256 px grid; only a value in between is a floating-point figure. A region's bone counts
370
+ as a mesh bone, so an idle key on it moves to `<bone>_ctl` under `--idle-keys ctl` like a
371
+ segment bone's.
372
+
373
+ **What it reports.** `mesh_report.json`'s row for a contour mesh carries `mode: "contour"`, the
374
+ parameters it ran at, `src/contour.ts`'s whole report (outline and interior vertex counts,
375
+ triangles, coverage, overshoot and its bound, enclosed transparent area, the smallest angle and
376
+ largest edge ratio with their triangles, the islands and pixels left out, and what the traced
377
+ silhouette holds besides the art, each pixel counted once: `filledHolePixels` (the art's own
378
+ holes), `grownPixels` (added by the margin), `pinchFilledPixels` (added by the pinch fill) and
379
+ `grownHolePixels` (transparent pixels the grown silhouette encloses — a notch whose mouth grew
380
+ shut))
381
+ and, per region, the vertices its bone reaches (`g > 0`) and holds alone (`g = 1`).
382
+ `art_coverage` is over all the part's art, stray islands included (in the automatic mode it is
383
+ not: there the islands `source.stray` leaves out are cleared from every reader's art, see
384
+ `source.stray` below). A lattice mesh's row is
385
+ unchanged.
386
+
387
+ **Refusals** — every one names the part, the value found and the value required, and every
388
+ contour refusal in a run is listed together; nothing falls back to the lattice:
389
+
390
+ | rule | means | change |
391
+ | --- | --- | --- |
392
+ | `CONTOUR_COVERAGE` | an art pixel lies outside the mesh after the margin and simplification | raise `margin` or lower `tolerance` |
393
+ | `CONTOUR_OVERSHOOT` | the mesh reaches further than `margin + tolerance + 1` px past the art | lower `margin` or `tolerance`; a grown pixel is within `margin` of art by definition, so what passes the bound is simplification, or a transparent pocket the grown silhouette closed (counted in `grownHolePixels`) |
394
+ | `CONTOUR_BUDGET` | more vertices than `budget` | coarser `spacing` or region `spacing`, or a larger `budget` |
395
+ | `CONTOUR_ONE_ISLAND` | the art is more than one 4-connected island (every island's pixel count named); with `stray` declared, an island other than the largest above it, or a tie for the largest | `stray`, if the extra islands are stray pixels you accept not drawing; a part that is two pieces stays a lattice part, or is split |
396
+ | `CONTOUR_TRACE` | rigc's tracer refuses a diagonal pinch (two pixels meeting at one corner) in the silhouette before it grows, in its words. A pinch through a hole is not one (the hole is filled first), and no island with its holes filled has one (selftest `CE08`, over every island of every 4x4 mask), so this is a guard; a pinch the growth makes is filled, not refused | the lattice, or the art |
397
+ | `CONTOUR_SELF_INTERSECTION` | the simplified outline crosses itself, or too few vertices are left | lower `tolerance` |
398
+ | `CONTOUR_INDEX`, `CONTOUR_GRID`, `CONTOUR_COINCIDENT_VERTICES`, `CONTOUR_ZERO_AREA_TRIANGLE`, `CONTOUR_ONE_LOOP`, `CONTOUR_TILING` | the built mesh fails a topology check (a bug, not an authoring error) | report it |
399
+ | `CONTOUR_PART_HAS_ART` | no pixel above alpha 8 | re-run assemble |
400
+ | `CONTOUR_PARAMETER` | a parameter `src/contour.ts` cannot run at: a margin above 0 and below 1 px (it would add no pixel), a spacing whose keep radius (half of it) snaps to 0, a polygon whose band folds it, a part over 32768 px | the named field |
401
+ | `RIG_CONTOUR_REGIONS_OVERLAP` | a vertex two regions both reach (`g > 0`); the detail names the vertex and both regions | move the regions apart or narrow a band |
402
+ | `CONTOUR_RIB_ART` | a rib's point (a link origin or a station) is not on the part's art (its pixel at or below the threshold) or not inside the outline, or a point between its ends falls within 1/256 px of the outline — a rib that leaves the art | move the chain onto the part, or leave the chain out of `ribs` |
403
+ | `CONTOUR_RIB_LINK` | a chain link shorter than the outline's `tolerance` (or of no length), or a joint where the chain turns straight back on itself; the detail names the chain, the link and the tolerance field | fewer, longer links, or a lower `tolerance` |
404
+ | `CONTOUR_RIB_CROSSING` | two ribs cross, touch or share an end (both named, with their ends) | fewer `stations`, or a chain that bends less sharply inside a wide part |
405
+ | `CONTOUR_RIB_EDGE` | a rib's consecutive vertices could not be joined by an edge (no edge across them could be flipped) — a guard: the rule keeps every other point off a rib | report it |
406
+
407
+ **Ribs along a chain** (`meshes.<part>.ribs`, issue #188). A rib is a row of vertices across
408
+ the part at a joint of a chain the mesh is bound to, or at a station along one of its links;
409
+ what a rib does to how the part deforms is not claimed. For each chain `ribs` names,
410
+ `src/contour.ts` (`placeRibs`) places, in part-image px:
411
+
412
+ - **where**: on each link k (from `points[k]` to `points[k + 1]`, the last to the tip) a rib at
413
+ its origin and at `points[k] + m/(stations + 1) · (end − origin)`, m = 1..`stations`; the tip
414
+ carries none;
415
+ - **across**: along n = (−d_y, d_x)/|d|, d the link's direction — at a joint between two links
416
+ the sum of their unit directions, the bend's bisector;
417
+ - **clipped to the art**: the point must be on the part's art (its pixel above the mode's
418
+ threshold) and inside the outline; from it the line runs both ways to the first point where it
419
+ meets the outline the mode traced (art, holes filled, grown by `margin`, simplified at
420
+ `tolerance`), and those two points, on the 1/256 px grid, are the rib's ends. Each end joins
421
+ the hull on the edge it lands on (or is that edge's vertex), moved off the edge by at most
422
+ 1/512 px; coverage, overshoot and self-intersection then read the outline with them;
423
+ - **between**: the point itself (so the joint is a vertex), and each half from an end to the
424
+ point cut into ⌈half / spacing⌉ equal parts at the mode's background `spacing`
425
+ (`auto.source.spacing` in the automatic mode);
426
+ - **kept a row**: no other interior point is kept within half a spacing of a rib, every
427
+ consecutive pair of a rib is made an edge of the triangulation (edges across it are flipped
428
+ until it is one) and locked, as an outline edge is — so `nonDelaunayEdges` does not count it.
429
+
430
+ Nothing reads a weight: every vertex is weighted as any other at its point. The rib on a strand
431
+ painted inside a wider layer runs across the whole opaque run there, sleeve and body included —
432
+ cutting the strand out is #170's. In the automatic mode every rib vertex is added to
433
+ `protect.vertices` (after the author's, as written) and each rib is sent to rig-c's
434
+ `reduceMesh` as a named line (`lines`, rig-c 2.33.0; `maxDeviation` 0), so the reduction keeps
435
+ every rib vertex and its post-pass flips no rib edge; the result's measurement carries one
436
+ `MQ_LINE_DEVIATION` row per rib.
437
+
438
+ **The window between coverage and overshoot**, measured in #106 (`tools/contour_survey.ts`, five
439
+ generated shapes, margins 0, 1, 1.5, 2, 2.5, 3): the smallest margin that covered every art
440
+ pixel was 0 at tolerance 0.5, 1 at tolerance 1 and 1.5, 1.5 at 2, and 3 at 3 (a two-horned
441
+ crescent; the other four 1.5 or less). No cell refused overshoot, and along each tolerance, once
442
+ a margin passed every larger one passed — where the offset this replaced moved an acute corner
443
+ up to 4 × margin and closed the window from above. That is the generated shapes: on a real part
444
+ a larger margin can close a gap whose enclosed pocket lies further from the art than the bound,
445
+ which the overshoot check refuses (the demo's hairpin builds at margin 1 and refuses at 1.5, 2
446
+ and 3: 5.39 px). On the public examples at tolerance 1, margin 1, `stray` 4 and spacing = the
447
+ part's grid, 13 of the 14 mesh parts build; the 14th is the demo's earring, two pieces.
448
+
449
+ **When not to use it.** A part that is two pieces (the demo's earring: 411 and 358 px) — keep
450
+ the lattice, which bridges islands, or split the part. And a part nothing deforms locally: at
451
+ tolerance 1 the contour outline is dense, and on the examples' 13 parts the contour mesh used
452
+ more vertices than the lattice at the same spacing (demo 1785 against 1431, sample 974 against
453
+ 852) while enclosing 5.8 and 9.8 times less transparent area (demo 25078 against 144782 px², sample
454
+ 6563 against 64445), with thinner triangles — smallest angles 2.46°–11.31° against the lattice's
455
+ 12.09°–45° (#106). Switched, every part that builds: the sample passes `check` (9 of 9;
456
+ `TEXTURE_STRETCH` 1.227 → 1.384, ceiling 1.927); the demo's gate is green but `check` refuses
457
+ `TEXTURE_STRETCH` on its back hair (2.535: an edge of a contour triangle squeezed to 0.394 of
458
+ its length in the idle), and with the back hair left on the lattice it passes 9 of 9 (1.388 →
459
+ 1.68).
460
+
461
+ **Why the demo's back hair fails, and what passes** (issue #115). The squeezed edge is an
462
+ outline edge 2 px long, near the hair's tip, whose two ends sit either side of a step in the
463
+ weight field: one end keeps `hairback_l1` at 0.031, the other drops it, because a weight under
464
+ 0.03 is dropped (`src/weights.ts`, the reference's `>= 0.03`). `hairback_l1` and `hairback_r2`
465
+ swing half a period apart, so that 0.03 step moves the two ends 1.2 px against each other — on
466
+ a 2 px edge, a ratio of 0.394 to 1.639. Re-deriving every vertex's weights by the same formula
467
+ with the drop and the four-bone cut both left out (an off-tree measurement, not a proposal)
468
+ gives 1.316 on the contour mesh and 1.336 on the lattice: the lattice's 36 px edges hide the
469
+ step, the outline's 2 px edges show it. Interior vertices cannot change it — an outline edge is
470
+ in every triangulation — so the background spacing does not move the figure (36, 24, 18 and
471
+ 12 px all give 2.535). Raising `tolerance` to 1.5 removes every short edge across a step (the
472
+ shortest is then 8.2 px) and the part passes (1.345; with every buildable demo part switched,
473
+ `check` passes 9 of 9 at 1.68), at 313 vertices against the lattice's 268 and 18155.5 px²
474
+ enclosed against 58346. The window is one value wide on this part: 1.25 keeps the same 2 px
475
+ edge (2.535) and 1.75 leaves art uncovered at margin 1. No other switched part of either
476
+ example comes near it: the largest length change on any edge under 5 px is 1.349 px on the
477
+ back hair and 0.784 px at most elsewhere, where a 2 px edge fails once it shortens by
478
+ 0.962 px (2 − 2 / 1.926544). The mode is for a part with a declared soft region inside a stable surround; on the
479
+ generated fixture of `tools/local_compare.ts` it reached a local shape error (0.336 px at most,
480
+ 63 vertices) the lattice reached only at grid 2, with 1025 vertices (re-run on the grown outline
481
+ in #106, the same figures).
482
+
483
+ **On real parts the generated result did not carry over unchanged** (issues #107 and #110,
484
+ `bun tools/real_compare.ts --work <dir> --examples demo,sample --builds --sweep`, a declared test region on two demo
485
+ parts — a place to measure, not a claim about the painting; the region spacing chosen by vertex
486
+ count alone). On `bottomwear` (a region of radius 65 px inside the part) the contour mesh passes
487
+ the same rule: 336 vertices against the lattice's 352, largest local shape error 1.535 px against
488
+ 7.613. On `hair_front` (radius 11 px, beside the part's own outline) it did not until the outline
489
+ was cut inside the region's band: one 35 px outline edge ran from inside the region (`g` = 1) to
490
+ past its band (`g` = 0), and every pose's worst pixel sat in a 7.1° triangle on it, 0.74 px from
491
+ the outline — 4.481 px against the lattice's 2.453, at 119 vertices against 123, at every
492
+ tolerance that builds (1, 1.25, 1.5; 1.75 and up leave art uncovered at margin 1, 0.5 and below
493
+ cannot fit 123 vertices). With the outline cut there it passes: 120 vertices, 0.333 px against
494
+ 2.453, transition 1.335 against 2.543, deformation outside 0.997 against 2.168; the lattice
495
+ needs grid 2 (2590 vertices) to match it. Moving a region still opens the seam with the part
496
+ drawn beside it — over the idle, translated, by 5.19 px (lattice with the region's weights) and
497
+ 5.76 px (contour) on `hair_front` against the face, 6.91 and 5.84 px on `bottomwear` against the
498
+ shoes, beyond what the same idle opens without the region — and `check`'s seam bar does not see
499
+ it (0.326 or 0.327 in every variant: it compares the setup pose). A declared `seam` requirement
500
+ does (§7, *Declared requirements*; the region translated, the idle at 12 fps): `hair_front`
501
+ against `face` reads 3.022 px on the tracked build and on the lattice variant — the idle's own
502
+ sway, at frame 23 — and 6.809 (lattice + region) or 6.888 (contour) at frame 0, at pixel
503
+ 372,135 or 373,130 inside the region; `bottomwear` against `shoes` reads 6.478 (frame 8) and
504
+ 11.410 or 10.489 at frame 27, pixel 375,1192. `TEXTURE_STRETCH` does: the
505
+ translated region fails it in both modes on `bottomwear` (2.314 lattice, 2.448 contour) and in the
506
+ contour mode on `hair_front` (2.183; lattice 1.552), where the face half of `STILL_REGIONS_DARK`
507
+ failed in both (its pixel reading, until issue #123; not re-measured since). The ceiling is 1.927. The outline cut moved the stretch little (`hair_front`
508
+ contour, per pose, before → after: still 1.152 → 1.149, rotate 1.444 → 1.415, translate 2.186 →
509
+ 2.183, scale 1.771 → 1.670) and the translated seam from 5.70 to 5.76 px.
510
+
511
+ ### An automatic mesh (`meshes.<part>.auto`, issue #126)
512
+
513
+ The third mode asks for an economical mesh under declared quality bounds instead of a spacing.
514
+ It is opt-in per mesh: `propose` still writes `grid`, a config that names no `auto` builds byte
515
+ for byte as before, and the top-level default does not change. The reduction itself is chosen
516
+ on geometry alone; the result is then written only when its **motion** is measured against its
517
+ unreduced source on the rig's idle and passes the author's bounds (step 4). The steps
518
+ (`src/automesh.ts`, `src/automotion.ts`):
519
+
520
+ 1. **The source** — this package's contour mesh over the padded part image at **alpha 1 and
521
+ above** (the threshold rig-c's authored-fit gate uses; the lattice and contour modes stay
522
+ at alpha above 8), at `source.tolerance`, `source.margin` and the background `source.spacing`,
523
+ with no region. It is gated by the contour mode's own checks, and their refusals
524
+ (`CONTOUR_*`, the detail ending "the automatic mode's source, at alpha 1 and above") refuse the
525
+ part. One reading differs from the contour mode's: the source's overshoot is measured with the
526
+ background flooded **8-connected** (rig-c 2.23.0's `measureAuthoredMeshFit` at connectivity 8,
527
+ the fill rig-c's `MQ_OVERSHOOT` and `reduceMesh`'s admission read), so a pocket of background
528
+ joined to the outside only at a corner counts as outside and a source spanning it is refused
529
+ here (`CONTOUR_OVERSHOOT`, "the background flooded 8-connected"), with the number rig-c would
530
+ read, rather than by rig-c's admission after the call. The row's `source.contour` says so
531
+ (`fitConnectivity: 8`); the contour mode keeps its 4-connected reading and writes no such key. Its weights are the contour mode's — segments and region falloff — under the author's
532
+ `influences`, with no 0.03 floor unless the author writes one.
533
+ 2. **The call** — rig-c's `reduceMesh` (`rig-c/mesh`, 2.28.x) refines inside the declared
534
+ regions, then removes vertices while every declared bound still holds. Every number it is
535
+ handed is one of the fields below; `preset` is null (no preset exists yet: a preset will be a
536
+ named, versioned set of these numbers, expanded into the report), and no deform key or linked
537
+ mesh is passed (this package writes none). The three Stage B opt-ins (below) are handed over
538
+ only when written.
539
+ 3. **Acceptance** — the result is written only when rig-c's report ends
540
+ `no-further-valid-reduction`, or `budget-exhausted` with `best-meeting-every-bound`, and its
541
+ candidate is `accepted` with every row that has a declared bound at `pass`. Anything else
542
+ refuses the part (below); nothing falls back to the lattice or to the source.
543
+ 4. **Motion** — the rig stage builds the rig twice through rigc's gate: as written (the
544
+ **candidate**, the reduced mesh, packed as `--out` receives it) and with that one attachment
545
+ swapped for the unreduced source — its UVs, triangles and hull as `reduceMesh` was handed them,
546
+ its own weights bound the same way (the **reference**, `build --profile spine-html` without
547
+ `--pack`: the same gate over the compile, and nothing reads a reference's pages). rig-c's
548
+ `compareMeshesInMotion` (`rig-c/meshcompare`) poses both model documents on the idle
549
+ exactly as `check` renders it — `idle` at 12 fps over its duration, physics reset at 0 and
550
+ stepped by 1/12 s, no warm-up — at the render's frames (`grid`, i/12 s) and a frame interval's 0.381966 past each (`irr`, frames the render never
551
+ draws), and measures how far the reduced mesh carries each art pixel from where the source
552
+ carries it (`MQ_LOCAL_DEFORMATION`, world units = rig px), its triangles' stretch and squash
553
+ from the setup pose, and the triangles it turns over (`MQ_INVERSION`). The part is written
554
+ only when rig-c's verdict is `accepted` (geometry and motion both pass). Nothing chooses the
555
+ full reduction by motion, so every frame of this comparison is held out from selection. A part the idle cannot deform
556
+ is refused before anything is compiled: the idle has to key, with changing values, a bone that
557
+ moves some of the bones the part is weighted to and not all of them (a key on their shared
558
+ ancestor, or on the one bone a single-bone part binds, moves every vertex by one affine map and
559
+ would read 0 over nothing). Constraints are not read for this: a part only a constraint moves
560
+ is refused, never passed — unless its author declares the motion, `stimulus` (issue #196,
561
+ the field table below and *A part only physics moves* after the physics recipe): each
562
+ declared bend is two more frames, every link of the chain turned by +`degrees` and by
563
+ −`degrees`, walked in both builds at time 0 beside the idle's, and its links count as keys
564
+ by the rule above.
565
+ 5. **The acceptance loop** (rig-c 2.24.0, rigc#1266 mechanism 2; `src/autoreplay.ts`) — only
566
+ when step 4 refuses the full reduction on motion (`AUTO_MESH_MOTION`). rig-c reports every
567
+ accepted **operation** of the call (`acceptedAt`: N entries `{ step, kind, count,
568
+ sourceVertices }` since rig-c 2.25.0 — the attempt number, `insertion`, `removal` or
569
+ `boundary-run`, the vertices it inserted or removed and their source indices; the first I of
570
+ them, up to the last of kind `insertion`, are the refinement's, the rest removals and boundary
571
+ runs) and replays the call to any operation n by `stopAfterAccepted: n`, byte for byte the mesh
572
+ the call held after that operation. A boundary run is one step however many vertices it
573
+ removes, so every count below — N, I, the chosen step, ⌈log₂(N − I)⌉ — is in operations. The stage first compares the source with
574
+ itself on the idle's `grid` frames (a source the gate would refuse leaves the part refused as
575
+ in step 4, and nothing is searched), then **bisects** over the removal steps strictly between
576
+ I and N: each probe replays step n, holds it to step 3's acceptance (the termination rig-c
577
+ promised, `replayed-to-accepted-step` with that step, and every declared geometry row
578
+ passing), builds the rig with that step through rigc's gate without packing, and compares it
579
+ with the source on the `grid` frames only — the irr frames are not walked at all, so they
580
+ cannot inform the choice. It keeps the largest step it saw pass. Validity along the order is
581
+ not monotone (the contract measured it), so the search finds *a* passing prefix, not
582
+ necessarily the last, and at most ⌈log₂(N − I)⌉ replays are taken — derived from the steps,
583
+ not an author number. The one field that changes the search is the opt-in
584
+ `motion.selection` (*The multi-interval selection*, below); without it the search is this
585
+ bisection and the stage writes the bytes it wrote before the field existed. The chosen step is then
586
+ written into the rig, which goes through rigc's gate again exactly as written, and is
587
+ compared with the source on the **whole** idle with the grid frames declared `selection` and
588
+ the irr frames held out; it is written only when that comparison is `accepted` on every
589
+ frame, the held-out ones included. A step at or below I (the source, refined or not) is never
590
+ written as an automatic result: when no removal step passes, the part is refused with "no
591
+ reduction passes the motion bound". A part the gate accepts in step 4 runs no replay. Since
592
+ rig-c 2.24.0 the report carries `acceptedAt`, last in `changes`; since 2.25.0 one entry per
593
+ operation; since 2.26.0 five more geometry rows (below).
594
+
595
+ **The multi-interval selection** (issue #148; opt-in, `motion.selection: { "policy":
596
+ "multi-interval", "maxProbes": <n> }`). The bisection keeps the step it lands on; where validity
597
+ is not monotone, a later step with fewer vertices may pass in an interval it never visits (the
598
+ synthetic fixture shows one: control `MO52`). With the field, step 5's search becomes:
599
+
600
+ - **The order.** The bisection first, whole — its own result is always among the tested — then,
601
+ while fewer than `maxProbes` distinct steps have been replayed, the midpoint (rounded down) of
602
+ the longest run of removal steps not yet replayed, the lower run when two are equally long.
603
+ It depends only on the answers, so two runs make the same probes. Each step is replayed at
604
+ most once: the budget counts distinct replays, never a repeat.
605
+ - **The budget** is the author's whole number (1 or more; never defaulted, never derived). It
606
+ must hold the bisection: below ⌈log₂(N − I)⌉ the part is refused
607
+ `AUTO_MESH_SELECTION_BUDGET`, naming N, I and the number required, before any replay. There
608
+ is no unbounded walk: at most `maxProbes` replays, each one unpacked gated build and one
609
+ comparison.
610
+ - **The choice.** Among the tested steps that passed on the `grid` frames, the fewest vertices —
611
+ each replay's own count, read off its mesh, never inferred from the step number (a boundary run
612
+ removes several vertices in one operation) — and the lower step on a tie. The source (step I,
613
+ refined or not) and the full result (step N) bound the domain and are never candidates. It is
614
+ the **best among tested candidates**: never the last, the minimal or a complete walk, unless
615
+ the row's `termination` says `every-removal-step-tested`.
616
+ - **The roles.** Every frame a probe reads is a `grid` frame, declared `selection` in the final
617
+ comparison; the irr frames are read once, by that final comparison on the whole idle, and are
618
+ held out. A chosen step that fails held out is refused as under the bisection, and **no second
619
+ candidate is taken** — nothing reads a held-out result to choose. A probe that read a frame the
620
+ final comparison holds out (or does not declare) is refused `AUTO_MESH_SELECTION_ROLES` and the
621
+ held-out claim withdrawn; the shipped search never does this.
622
+ - **Nothing tested passed**: refused `AUTO_MESH_MOTION`, "no reduction passes the motion bound
623
+ among the tested candidates", every probe named; a step never replayed is not claimed to fail.
624
+
625
+ It cannot skip an operation inside a prefix — every candidate is the call's own mesh after its
626
+ n-th accepted operation — and it does not change what the economy of the automatic mode is
627
+ measured against; it only looks at more prefixes of the same call.
628
+
629
+ **The skinning residual as a per-step veto** (issue #126, rig-c 2.31.0; opt-in,
630
+ `motion.residual: { "maxResidual": <px> }`). Without the field nothing is sent and the stage is
631
+ byte for byte what it was. With it, rig-c's `reduceMesh` receives `targets.skinning` — the
632
+ envelope below and the author's bound — and, in rig-c's words, "every removal, boundary run and
633
+ post-pass is held to `MQ_SKINNING_RESIDUAL` against the call's own source and refused by name
634
+ when over it": a step whose result's residual exceeds the bound is a refusal like any other
635
+ (`MQ_SKINNING_RESIDUAL: <value> against <= <bound>`), the working mesh as it was, the search going
636
+ on within the budget. Refinement insertions are not vetoed one by one; the refinement's outcome is
637
+ held to it whole before any removal. `acceptedAt` records accepted operations only, so a replay
638
+ (step 5) under the veto is the same call's mesh after its n-th accepted operation, byte for byte.
639
+
640
+ - **What the value is.** "The largest over the samples carried by both meshes of
641
+ Σk εk·|Δsk| + Σk |Δw̄k|·(εk·|p − ck| + τk): a bound, under the declared envelope only, on how
642
+ far the candidate draws a UV from where the source draws it — positions only; it certifies no
643
+ orientation, stretch or squash, no motion outside the envelope, and replaces no motion
644
+ comparison" (rig-c's `SKINNING_READING`). Drawing px: the part's own pixels (pageScale 1), the
645
+ same unit as `maxLocalDeformation`. It is **not the motion verdict**: the motion comparison of
646
+ step 4 still runs on every written candidate and still decides, so a residual that passes
647
+ never writes a part the comparison refuses. It is conservative, never understating the posed
648
+ error rig-c measured, and loose on scale and multi-bone chains (rig-c measured 0.26–0.72 of
649
+ the posed error there, 1.000 on a pure rotation).
650
+ - **The envelope is derived, never written** (`src/autoenvelope.ts`). The reference is the
651
+ lowest common ancestor of the slot's bone (`segments[0]`'s) and every bone the part's source
652
+ weights bind — the deepest bone on all their ancestries, read off the rig as written (#165).
653
+ When every bound bone hangs below the slot's bone that is the slot's bone itself, and the
654
+ envelope is the one derived before #165, byte for byte. When a bound bone hangs in a sibling
655
+ subtree — a skirt link beside the chest, the hip above it — the reference is the bone they
656
+ share, and the slot's bone is sent as one more bone, with its own range, **only when the
657
+ weights bind it**: rig-c's envelope holds every bone either mesh binds and no other, and
658
+ refuses one neither binds (`SKINNING_BONE_UNKNOWN`). The row's `reference` names the bone
659
+ used and its `rule` says which of the two cases it is. For every bone sent, rig-c's
660
+ `skinningEnvelopeBone({ referenceChain, chain })` composes the chain from the root to the
661
+ reference and from the reference's child down to the bone, each bone's range read off the
662
+ idle this package writes: rotation and scale as the [min, max] of every key value and Bézier
663
+ control value, translation as the length of the largest |x| and |y| offsets, the pivot the
664
+ bone's setup joint in the part's drawing frame, and the setup the rig's (no scale, shear or
665
+ inherit mode is written). An author cannot supply one (`CONFIG_KEY_KNOWN`).
666
+ - **Not measurable** — the veto is then **not applied** and the comparison accepts or refuses
667
+ the part alone, as without the field; no number is invented. Each stop is named with its bone
668
+ and code: a bone a constraint drives (an ik's, transform's or path's `bones`, a physics
669
+ constraint's `bone`) is handed to rig-c with that kind as its source and refused by rig-c,
670
+ `SKINNING_RANGE_UNSUPPORTED` in its words — rig-c certifies only ranges declared by keys and
671
+ exposes no range read off a posed walk; a shear key (`ENVELOPE_SHEAR_KEYED`: rig-c's range has
672
+ no shear field); a slider in the rig (`ENVELOPE_SLIDER_UNREAD`: it drives an animation's bones,
673
+ which are not read). `ENVELOPE_BONE_NOT_BELOW_REFERENCE` — a bound bone with no chain to the
674
+ reference, which the helper cannot compose — keeps its name and its check, and is
675
+ **unreachable by construction** since #165: a common ancestor is above every bound bone, and
676
+ in one skeleton (one root) a common ancestor always exists. It would fire only on a bone
677
+ list with two roots, which is not a skeleton; the selftest plants that case so the check is
678
+ seen to fire.
679
+ - **The bound is the author's** number, 0 or more, never defaulted. The tracked evidence uses 1,
680
+ the policy's motion bound: the residual bounds the same displacement the motion row measures,
681
+ without posing, so the distance already accepted for the posed reading is its first reading.
682
+ - **What it costs.** Every step the rows let through is measured once more, carried rather than
683
+ recomputed (rig-c measured the carried call at 1.4–5.8 times the call without it on recorded
684
+ inputs, under a stand-in envelope); a veto changes the trajectory, so the result is a different
685
+ reduction, not the same steps checked twice.
686
+ - rig-c's report does not count the steps the veto refused; `tools/veto_tally.ts` counts them
687
+ over the very call (the survey prints them).
688
+
689
+ The public examples' evidence is re-run from the tree: `bun run fetch-examples`, then
690
+ `bun tools/auto_motion_survey.ts` switches each part item 2 accepted on geometry to `auto`, alone,
691
+ under `examplePolicy` (`fixtures/automesh.ts`) plus `policyMotion` (`fixtures/automotion.ts`), runs
692
+ the real rig stage with the motion gate and the acceptance loop, and prints two tables — the full
693
+ reduction's comparison and the loop's outcome per part (N, the chosen step, the replays, the
694
+ candidates they tried, the selection and held-out values) — every row from rig-c's report, the
695
+ schedule walked and each refusal's text, the same bytes on every run of one tree (each part's
696
+ wall time goes to standard error, not into the table).
697
+
698
+ | field | means |
699
+ | --- | --- |
700
+ | `source.tolerance`, `source.margin`, `source.spacing` | the contour mode's parameters for the source, px (margin 0, or 1 or more) |
701
+ | `source.stray` | optional: the largest island, in art pixels at alpha 1 and above, that may be left out, as in the contour mode (absent: none is; an island other than the largest above it is still `CONTOUR_ONE_ISLAND`). Unlike the contour mode, the islands left out are cleared from the art **every reader of the mesh takes** (issue #172): the reduction's `art` and its `sourceBounds`, the result's `targets.artFit`, `art_coverage`, and the motion gate's art bounds all read the mask the outline was traced from, so a part whose only extra islands are at or under the figure builds where it refused (`REDUCE_SOURCE_FAILS_ITS_ART_BOUNDS`) before. What was cleared is named: the row's `stray_cleared` — `islands`, `pixels`, and `art_pixels` (the part's art at alpha 1 and above with those islands counted in), present whenever `stray` is declared (zeros when nothing was left out), absent otherwise — and the part's mesh line prints `stray cleared N island(s), P of A art px` when N is above 0. The image is not touched: `rig/images/<part>.png` is the part as assembled, so a cleared pixel the written mesh's triangles reach is still drawn and one outside them is not. That is the one answer every reader of the image gives: `check` reads `parts/<part>.png` as assembled, through the attachment's rest triangles (`CHECK_TIP_OVER_ROOT`, `CHECK_STILL_REGIONS_DARK`, a `seam` requirement's pairs) or composited flat against the render (`CHECK_SEAM_WITHIN_BAR`), and the runtime draws the rig's image through the same triangles; clearing the rig's image alone would make `check` count pixels the runtime no longer draws. On the public examples the declared crumbs carry alpha 24 or less (docs/evidence/production-trial-stray.md) |
702
+ | `sourceBounds.{minCoverage, maxOvershoot, maxUndercut}` | what the source must already meet at alpha 1 and above: the share of art pixel centres covered (0..1), the furthest a covered pixel may sit outside the filled silhouette (px, or `null`), the furthest an uncovered art pixel may sit from the covered set (px, or `null`) |
703
+ | `targets.artFit.{minCoverage, maxOvershoot, maxUndercut}` | the same three for the result |
704
+ | `targets.maxBoundaryDeviation` | the largest Hausdorff distance between the result's outline and the source's, px |
705
+ | `targets.minAngle` | optional: the smallest triangle angle, degrees; absent, it is reported and not gated |
706
+ | `influences.{maxInfluences, minWeight}` | the cap on bindings per vertex (1 or more) and the floor below which a share is dropped (0 up to 1; 0 drops only shares that are 0 on the weight grid) — for the source's weights and every inserted vertex |
707
+ | `budget.maxCandidates` | the most steps rig-c may try (each insertion and each removal attempt counts one); 0 returns the source |
708
+ | `minArtSamples` | the fewest art pixels a raster row is taken over, 1 or more |
709
+ | `motion` | **required**: `maxLocalDeformation` (rig px, 0 or more — how far the reduced mesh may carry any art pixel from where its source carries it, at any frame); optional `maxStretch` / `minStretch` (ratios; absent, the rows are reported and not gated); optional `deformMayFold` (absent is **false**: a triangle that turns over refuses the part; true declares the slot in `invariants.deformMayFold` and lists every fold instead); optional `gradation` (px per px, 0 or more, or `null`; the author's G for `MQ_ALLOCATION_CONTRAST`, never derived — absent or `null`, the contrast reads `not-measurable` naming it; see *The allocation rows* below); optional `selection` (`{ "policy": "multi-interval", "maxProbes": <whole number, 1 or more> }`; absent, the acceptance loop is the bisection — see *The multi-interval selection* above); optional `residual` (`{ "maxResidual": <px, 0 or more> }`; absent, nothing is sent — see *The skinning residual as a per-step veto* above) |
710
+ | `protect` | optional; each field optional: `hull` (true keeps every source outline vertex; absent is **false**, the default agreed for this mode), `vertices` and `edges` (source vertex indices and pairs that must survive), `regionBoundaries` (region names whose outline vertices must survive), `weightJump` (an L1 weight difference above which a source edge is kept; absent is none), `influences` (bones never pruned from a vertex; every region's bone is added) |
711
+ | `regions` | optional, each `{name, shape, bone?, band?, maxEdgeLength, transition, grade, minArtSamples}` with `shape` `"circle"` (`cx, cy, r`) or `"polygon"` (`points`): `bone` and `band` are the control bone and its weight falloff exactly as a contour region's (rig px, multiples of 1/256 px), written together (the region weights its bone and asks for density) or not at all (density only, issue #155; one without the other is refused by name); `maxEdgeLength` is L0, the longest an edge meeting the region may be, px; outside it, across `transition` px, the bound relaxes as `L0 + grade·d`; `transition` 0 is a hard edge; `minArtSamples` is the region's own sample floor |
712
+ | `boundaryRuns` | optional, `{ maxVertices }`, a whole number 2 or more, no default: each removal pass first tries to replace a run of 2 to `maxVertices` consecutive source-hull vertices with one chord, as one step held to every declared row (below) |
713
+ | `retriangulate` | optional, `"delaunay"`: once the reduction ends, the kept vertices are re-triangulated by Delaunay flips, taken whole only when every declared row still passes (below) |
714
+ | `removalOrder` | optional, `"deformation-load"`: the single removals are tried in ascending predicted load instead of ascending source index (below) |
715
+ | `stimulus` | optional (issue #196), `{ "chain", "degrees" }` or a non-empty list of them: a bend the motion gate measures the part against, for a part no idle key deforms (*A part only physics moves*, after the physics recipe). `chain` is a chain this mesh's `segments` name, each named once; `degrees` a number above 0. Each link of the chain is turned by +`degrees` and by −`degrees` about its joint — a `rotate` key on the link, so the chain curls — in two animations of duration 0 (`stimulus <chain> +<degrees>`, `stimulus <chain> -<degrees>`) that only the comparison's builds carry: the reference and a candidate built from the written rig with them added (`build --profile spine-html` without `--pack`, one more gated build per part); `rig/motion.json` never holds them. Each is walked at time 0, right after the physics reset, so the pose is the declared bend and no constraint has moved it; the idle is walked as before, so where idle keys also move the part both are measured, against the one `motion` bound. The acceptance loop's probes walk them too, as `selection` beside the grid frames. The row echoes `stimulus` (chain, degrees, links, the two animation names); absent, nothing is added and every byte is the same. A chain the segments do not name is `CONFIG_NAME_RESOLVES`, `degrees` that is not a number above 0 `CONFIG_FIELD_TYPE`, both at the field; an animation of `motion.animations_from` already called by one of the two names is `AUTO_MESH_MOTION_INPUT`. The amplitude (*The allocation rows*) is still derived from the idle alone |
716
+
717
+ **A bound declared absent.** `maxOvershoot` and `maxUndercut`, in `sourceBounds` and in
718
+ `targets.artFit`, each take a number of px 0 or more or `null`. `null` means *measured and
719
+ reported, not bounded*: rig-c (2.21.0 and later) still measures the row, reports it `undeclared`
720
+ with its value and no bound, and never refuses the part or stops a reduction step on it. Leaving
721
+ the field out is not the same thing and is still refused by name (`CONFIG_FIELD_PRESENT`), as
722
+ rig-c refuses it: an omission cannot be told from a forgotten field. `minCoverage` has no such
723
+ form. Write `null` where the policy deliberately accepts loss the distance cannot express — for
724
+ instance a coverage floor below 1, where every uncovered pixel is at least 1 px from the covered
725
+ set, so an undercut of 0 cannot hold beside it — rather than a large number, which would read as a
726
+ measured limit. The row in `mesh_report.json` echoes the `null` in `settings`, carries the
727
+ residual as rig-c reports it (`state: "undeclared"`, the value, `bound: null`), and
728
+ `worst_residual` never picks it; the `build` line prints it after the worst residual as
729
+ `overshoot <value> (not bounded)` / `undercut <value> (not bounded)`. The motion gate holds the
730
+ reference and the candidate to the same bounds, `null` included.
731
+
732
+ **Stage B opt-ins** (rigc#1271, rig-c 2.25.0–2.28.0). Three fields, each the author's and each
733
+ absent by default: left out, it is not sent and the call — mesh and report — is the one it was
734
+ before the field existed. What each promises and does not is rig-c's (its docs/MESH_REDUCTION.md
735
+ §8, the three *Stage B* subsections):
736
+
737
+ - `boundaryRuns: { maxVertices }` — the boundary half of the reduction. A traced source hull is
738
+ simplified at about the same 1 px the deviation bound is measured at, so a single removal
739
+ almost always leaves a sagitta over it; one chord for a run of vertices can meet it. A run is
740
+ 2 to `maxVertices` surviving source-hull vertices, removed as **one** operation held to every
741
+ declared row exactly as a single removal is, before that pass's single removals; it is one
742
+ `acceptedAt` entry (`kind: "boundary-run"`, `count` ≥ 2), so the acceptance loop replays to it
743
+ like any step. Each run tried costs a candidate against `budget.maxCandidates`.
744
+ - `retriangulate: "delaunay"` — the interior half. After the reduction ends (a replay's
745
+ included) the kept vertices are re-triangulated by Lawson flips toward Delaunay: no vertex
746
+ added, moved or removed, so UVs and weights are the removals'; no outline edge, protected edge
747
+ or edge a region holds is flipped. The pass is taken whole only when every declared row still
748
+ passes, and otherwise the mesh is returned as the removals left it with the row that refused
749
+ it named. It is not a step: `acceptedAt` and the replay are the call's without it, and a replay
750
+ to step k is byte for byte the budget cut at that step followed by the same pass (`MO43`
751
+ holds it on this package's inputs). rig-c does not promise that passing motion is monotone
752
+ along the steps under it; the acceptance loop finds a passing prefix, not necessarily the last,
753
+ as it always did.
754
+ - `removalOrder: "deformation-load"` — each pass tries its single removals in ascending
755
+ L · Δshare (the longest edge the removal's hole adds, times half the L1 weight difference
756
+ across it), ties by source index; boundary runs keep their own order. It reads weights only:
757
+ on an unweighted source it is the default order.
758
+
759
+ A malformed value — `maxVertices` 1 or 2.5, `null`, another spelling — is refused by the loader by
760
+ name (`CONFIG_FIELD_TYPE`, `CONFIG_FIELD_PRESENT`, `CONFIG_KEY_KNOWN`). The row echoes each field
761
+ set in `settings` (after `preset`) and, in `result`, `boundary_runs` (the runs taken and the
762
+ vertices they removed) and `retriangulation` (`taken`, `flips`, `sweeps`, `refusedBy`); the
763
+ `build` line adds `; boundary runs <= k: r run(s), v vertex(es)`, `; retriangulate delaunay taken,
764
+ n flip(s)` (or `refused by <row>`) and `; removal order deformation-load`. A part that sets none
765
+ writes none of these. `bun tools/auto_motion_survey.ts --config
766
+ baseline,boundaryRuns,retriangulate,removalOrder,all` runs the eight public parts under each
767
+ (`docs/evidence/auto-stageb-survey.md`).
768
+
769
+ **The allocation rows** (rig-c 2.26.0, rigc#1280). rig-c reports five more geometry rows on every
770
+ reduction: `MQ_GRADE` (the sharpest change of local edge size across an edge), `MQ_MIN_ANGLE_P10`
771
+ (the tenth percentile of the triangles' smallest angles), `MQ_ALLOCATION_CONTRAST` (Δ, density no
772
+ declared need explains, with economy E), `MQ_DEFORM_LOAD` (the largest L · Δshare · θ / 4 — a
773
+ location reading, not predicted motion) and `MQ_BOUNDARY_NECESSARY` (B\*, the fewest source-hull
774
+ vertices an outline can keep with every static row held). Every one is `undeclared`: no field
775
+ declares a bound on any of them (a `targets.maxGrade` or the like is refused,
776
+ `CONFIG_KEY_KNOWN`), none is ever required or the `worst_residual`, and each lands in the row's
777
+ `residuals` as rig-c reports it. Δ and the load read a **motion amplitude** — per idle track,
778
+ θ = ‖M − I‖ for each pair of bound bones the track turns or scales against each other, an ε in
779
+ px, and an attachment-wide gradation G (rig-c 2.29.0). The tracks are not an author field: the
780
+ rig stage derives them from the idle it writes (`src/autoamplitude.ts` — θ from each track's
781
+ keys, 2 sin(α/2) for a rotation, |s − 1| for a scale, 0 for a translation; ε the part's
782
+ `motion.maxLocalDeformation`). **G is the author's**, `motion.gradation`, because nothing a rig
783
+ declares fixes it (rig-c measured every candidate derivation, rigc#1291). In rig-c's own words,
784
+ G (px per px, 0 or more) "is how fast the mesh may coarsen away from something that needs
785
+ density — an edge of the B\* outline, a triangle whose weights turn under the declared motion.
786
+ At d px from a need of size h the reading allows edges up to h + G · d, exactly the form of a
787
+ region's `grade` … if the author would grade a region at some rate on this mesh, that rate is the
788
+ G to declare." A larger G lets a need relax sooner, so more dense vertices read as removable and Δ
789
+ and E rise; at 0 nothing reads as removable. rig-c's fixtures used 0.75, "a value those fixtures
790
+ chose, not a default rigc holds", and this package holds none either.
791
+
792
+ The amplitude is sent to `reduceMesh` and to the motion gate's comparison whenever its tracks are
793
+ derived, with `gradation` as written or `null` when the config leaves it out. Then:
794
+
795
+ - `MQ_DEFORM_LOAD` is **measured** — the largest L · Δshare · θ / 4, px, a location reading and
796
+ not predicted motion; it never reads G. It is `undeclared` (no bound, never the
797
+ `worst_residual`), and the build line prints it after the unbounded art rows:
798
+ `; deform load <value> px (undeclared)`.
799
+ - `MQ_ALLOCATION_CONTRAST` (Δ, with economy E in its `allocation.contrast`) is `not-measurable`
800
+ naming `motionAmplitude.gradation` unless the author set one; with one it is measured,
801
+ `undeclared`, and printed as `; allocation contrast <value> (undeclared)`.
802
+ - On the comparison, each build's setup section measures the load; Δ there stays
803
+ `not-measurable` (naming the gradation when it is null, and otherwise
804
+ `targets.maxBoundaryDeviation`, which a comparison does not declare).
805
+ - The row's `settings.motionAmplitude` is rig-c's echo of what it read, and `motion_amplitude`
806
+ says `{ "sent": true, "stops": [] }`.
807
+
808
+ No mesh, step, verdict or acceptance reads either row, so writing a gradation changes the report
809
+ and nothing the rig writes. A shear track, a constraint, a motion bound of 0, or a pair moved by
810
+ two bones of one group still stop the tracks by name (`"sent": false`, each term in `stops`), and
811
+ then nothing is sent and both rows read `not-measurable`.
812
+
813
+ A circle is handed to rig-c as the regular polygon with the fewest sides, 3 or more,
814
+ circumscribed about the circle, whose outline lies within 1/256 px of it (the grid the region's
815
+ numbers are on); the rule and its error are echoed in the region's `approximation`. The weights
816
+ still read the circle.
817
+
818
+ A region takes one of two forms, told apart by `bone` (issue #155):
819
+
820
+ - **Weight and density** — `bone` and `band` written. Inside the shape the bone takes weight 1,
821
+ falling off across `band` as in the contour mode, and rig-c refines the mesh there to the
822
+ region's density. Use it where a control bone should move that patch of the part on its own
823
+ (a pinch, a fold) and the mesh needs the vertices to carry it.
824
+ - **Density only** — neither written. rig-c refines the mesh there to the same density, and the
825
+ weights are untouched: no vertex takes a share from the region, the row's bone list and
826
+ `protect.influences` gain nothing, and every source vertex is weighted exactly as with the
827
+ region left out. A vertex rig-c inserts there carries rig-c's interpolation of the source
828
+ triangle that holds it (its contract §6), the rule every inserted vertex follows in either
829
+ form. Use it to place density by itself — where the art's shading, a joint or a field says
830
+ the mesh needs vertices — so that a change in the vertex count or the motion verdict against
831
+ the same part without the region is a change in density alone. The row writes its `bone` and
832
+ `band` as `null`, and its counts of vertices reached and bound as 0.
833
+
834
+ `bone` without `band`, or `band` without `bone`, is neither form and is refused by name. Two
835
+ regions that both weight a bone and both reach one source vertex is `RIG_CONTOUR_REGIONS_OVERLAP`,
836
+ as in the contour mode. A density-only region decides no weight, so it may overlap any region:
837
+ rig-c holds each region's density rows on its own, the smallest bound applying to an edge two of
838
+ them hold.
839
+
840
+ **What it reports.** `mesh_report.json`'s row carries `mode: "auto"`, `settings` (every number
841
+ the call saw, the threshold and the circle approximations included), `source` (the contour
842
+ mesh's report and rig-c's counts of it), `stray_cleared` — only when `source.stray` is
843
+ declared: the islands and pixels cleared from every reader's art, and the art pixels they were
844
+ part of — `result` (boundary and interior vertices,
845
+ triangles, bindings, vertices removed and inserted), every `residuals` row with its state, value
846
+ and bound, `worst_residual` (the declared row nearest its bound, as the share of the bound used)
847
+ and `worst_region`, the `termination` with its reason and `candidatesTried`, what the weights
848
+ lost to the grid (`sharesDroppedOnGrid`, `sharesPruned`, `droppedAtFivePlaces`), per region the
849
+ source vertices its bone reaches and the result vertices bound to it, `motion_amplitude` (whether
850
+ the allocation rows' amplitude was sent, and every term that stopped it; what was sent is rig-c's
851
+ echo in `settings.motionAmplitude`), `skinning_residual` — only under `motion.residual`: `rule`
852
+ (what the residual is and that it is not the motion verdict), `max_residual`, `sent` (whether the
853
+ veto was applied), `reference`, `envelope` (each bone's `linear`, `pivot` and `translation` as
854
+ sent, or null), `stops` (each bone and code that left it not measurable), and `measured` (the
855
+ written mesh's own `MQ_SKINNING_RESIDUAL`: state, value, bound, the worst sample's UV and pixel
856
+ with the value's covariance and lever sums there, the samples carried); the row is also among
857
+ `residuals`, declared with its bound — `replay` — only on a part
858
+ the acceptance loop wrote at a replayed step (step 5): `rule` (the search in words, "a passing
859
+ prefix, not necessarily the last"), `accepted_steps` (N, in operations), `refinement_steps` (I), `chosen_step`,
860
+ `replays` and `max_replays`, `candidates_tried` across the replays, `full` (the full result's
861
+ reading that the gate refused), every probe (step, pass/fail, value and frame on the grid frames),
862
+ and `selection` and `held_out` (the chosen step's value and worst frame per role). Under
863
+ `motion.selection` the `replay` object is the multi-interval search's instead: `rule` ("the best
864
+ among tested candidates"), `policy`, `max_probes`, `accepted_steps`, `refinement_steps`,
865
+ `chosen_step`, `bisection_step` (the bisection's own result inside the search), `replays`,
866
+ `candidates_tried`, `termination` (`every-removal-step-tested` or `budget-exhausted`), `tested`
867
+ (the replayed steps, ascending), `passing_intervals` (runs of tested passing steps with no tested
868
+ failure between), `untested_intervals` (runs of removal steps never replayed), `chosen`
869
+ (`vertices`, `boundary`, `interior`, `triangles`, `bindings` of the written mesh), `full`, every
870
+ probe with its `vertices`, `roles` (`selection: "grid"`, `held_out: "irr"`, and the distinct
871
+ frames the probes read), then `selection` and `held_out`. The row's other
872
+ fields are then that step's, its `termination` `replayed-to-accepted-step` — `deformation` — the motion
873
+ gate's verdict, the bounds echoed (`deformMayFold` included), each motion row (`MQ_INVERSION`,
874
+ `MQ_LOCAL_DEFORMATION` and one per region, `MQ_SQUASH`, `MQ_STRETCH`) with its state, value, bound,
875
+ worst frame and sample counts, and the schedule walked (frames per phase, held out, selection) —
876
+ `quality_report`, the whole `mesh-quality-report/1` document of the reduction, and
877
+ `motion_report`, the whole `compare` document of the motion gate, inside the row so one file holds
878
+ every part's evidence. `art_coverage` is over alpha above 8, comparable with the
879
+ other modes; the alpha-1 coverage is a residual. `build` prints one line per auto mesh: source →
880
+ result counts (hull + interior), bindings, the termination, the worst residual, each art bound
881
+ declared `null` with its value and `(not bounded)`, under `motion.residual`
882
+ `; residual <value> <= <bound> (a pose-free bound, not the motion verdict)` or
883
+ `; residual not measurable: <bone> <code>, … (veto not applied; the motion comparison decides)`, and
884
+ `motion <value> <= <bound> at <frame>` (the frame id is `idle@<phase>@<time>`), and the reference
885
+ build's gate lines after the candidate's. A part written at a replayed step adds
886
+ `; replayed to accepted step <n> of <N> after <k> replay(s); selection <value> <= <bound> at
887
+ <frame>; held out <value> <= <bound> at <frame>`, and the stage prints one `replay "<part>"` line
888
+ with every probe, then the gate of the rig as written and its references. Under
889
+ `motion.selection` that line reads `replay "<part>": multi-interval selection over the removal
890
+ steps …, <k> replay(s) of at most <maxProbes> (maxProbes), ended <termination>: <step> <verdict>
891
+ <value> v=<vertices>, …; the bisection kept step <b>; the fewest vertices among the tested
892
+ passing steps: step <n>, <v> vertices (best among tested candidates)`. Weights are written by `roundShares` on every vertex (5 places, zeros
893
+ dropped, the heaviest entry closes): the 0.03 floor that makes the lattice's last-entry close
894
+ safe is the author's to choose here.
895
+
896
+ **Refusals** — every one names the part and the rule; nothing falls back:
897
+
898
+ | rule | means |
899
+ | --- | --- |
900
+ | `CONFIG_MESH_MODE` | `auto` beside `grid` or `contour`, or no mode at all |
901
+ | `CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE`, `CONFIG_NAME_RESOLVES`, `CONFIG_REGION_NAME_UNIQUE` | a missing or out-of-range number, an unknown bone or region name, a region named twice, a Stage B opt-in that is not exactly a value rig-c accepts — every one named in one run |
902
+ | `CONFIG_REGION_BAND_NEEDS_BONE`, `CONFIG_REGION_BONE_NEEDS_BAND` | a region with a `band` and no `bone`, or a `bone` and no `band`: write both for a region that weights its bone, neither for a density-only region (issue #155) |
903
+ | `CONFIG_KEY_KNOWN` | a field the mode does not have — a bound on one of the five allocation rows, or an authored `motionAmplitude` (it is derived, never written) |
904
+ | `CONTOUR_*` | the source is refused by the contour mode's own checks at alpha 1 and above — most often `CONTOUR_ONE_ISLAND`: faint pixels the alpha-above-8 modes never saw are islands here |
905
+ | `AUTO_MESH_INPUT` | rig-c refused the call's input by throwing (a protected vertex the source does not have), in its words and code |
906
+ | `AUTO_MESH_TERMINATION` | rig-c returned no mesh: `invalid-input` (the source fails its own `sourceBounds`, a region it refuses, protected influences over the cap), `unsupported-topology`, or `budget-exhausted` with `none-met-the-targets`; or, in the acceptance loop, a replay that does not end as rig-c's contract promises (`replayed-to-accepted-step` at the step asked, `candidatesTried` the full run's `acceptedAt[n − 1].step`, its `acceptedAt` the full run's first n operations, each the same `step`, `kind`, `count` and `sourceVertices`) |
907
+ | `AUTO_MESH_ACCEPTED` | rig-c returned a mesh that is not accepted; the detail names every declared row not passing and the constraint that stopped it |
908
+ | `AUTO_MESH_NO_STIMULUS` | neither the idle nor a declared `stimulus` moves some of the part's bound bones against the others, so no frame deforms it — the detail names the bones with no motion against each other and the field to declare; missing stimulus is not a pass; declare `meshes.<part>.auto.stimulus`, put the part in `contour` or `grid` mode, or key a bone it binds |
909
+ | `AUTO_MESH_MOTION` | the reduced mesh against its source on the idle is not accepted and the acceptance loop wrote no step in its place: "no reduction passes the motion bound" (the full result's failing rows and every probe named; under `motion.selection`, "… among the tested candidates", with `maxProbes` and the termination), or the chosen step passes on the grid frames and not on the whole idle (its selection and held-out values and worst frames named); when the source itself is not accepted against itself, the full result's rows as before |
910
+ | `AUTO_MESH_SELECTION_BUDGET` | `motion.selection.maxProbes` is below ⌈log₂(N − I)⌉, the bisection the multi-interval search runs whole first; N, I and the number required named; nothing replayed |
911
+ | `AUTO_MESH_SELECTION_ROLES` | a multi-interval choice whose probes read a frame the final comparison holds out, or one it does not declare `selection`; the frames named, the held-out claim withdrawn |
912
+ | `AUTO_MESH_MOTION_INPUT` | the comparison could not be made: rig-c refused its input (its code carried — `COMPARE_REFERENCE_FAILS` when the source fails its own `sourceBounds`, `COMPARE_INPUTS_DIFFER`, …), the reference build is red at rigc's gate, or a gate build wrote no `skeleton.model.json`; in the acceptance loop, a replayed step's build that rigc's gate refuses |
913
+
914
+ **What rig-c cannot refine.** Refinement inserts only inside a region and its band (the
915
+ contract's P16). rig-c 2.19.0 stopped by name when an edge's far end lay further beyond the
916
+ band than the edge's bound; 2.19.1 and later implement the decision on issue #126 that an edge leaving the
917
+ band at a point is not held to it, so a coarse source refines. With `transition` 0 the authored
918
+ boundary stays held, and an edge from the region to a vertex further out than its bound — or one
919
+ that crosses the region with no point of it inside to split at — is still stopped by name
920
+ (`AUTO_MESH_ACCEPTED`, the detail naming P16).
921
+
922
+ ### Recipe: expressions and other named animations
923
+
924
+ The package proposes no expression and keys nothing but the idle; what it gives is a rig whose
925
+ face is bones (`eye_r`, `eye_l`, `brow_r`, `brow_l`, `mouth` on `sample`) and one door for
926
+ animations you write over them, `motion.animations_from` (the row above, issue #183). Write a
927
+ file in rig-c's motion-spec shape beside the config and name it:
928
+
929
+ ```jsonc
930
+ // config.json
931
+ "motion": { "duration": 4.0, "tracks": [ … ], "blink": { … },
932
+ "animations_from": "expressions.json" }
933
+
934
+ // expressions.json — the bones the config declares, keyed as rig-c keys them
935
+ { "spec": "rigc-motion/1",
936
+ "animations": {
937
+ "surprised": { "duration": 0.5, "tracks": [
938
+ { "bone": "brow_r", "property": "translatey", "keys": [ { "t": 0, "v": [0] }, { "t": 0.25, "v": [2] }, { "t": 0.5, "v": [0] } ] },
939
+ { "bone": "brow_l", "property": "translatey", "keys": [ { "t": 0, "v": [0] }, { "t": 0.25, "v": [2] }, { "t": 0.5, "v": [0] } ] },
940
+ { "bone": "mouth", "property": "scaley", "keys": [ { "t": 0, "v": [1] }, { "t": 0.25, "v": [1.4] }, { "t": 0.5, "v": [1] } ] } ] } } }
941
+ ```
942
+
943
+ `build` writes `surprised` into `rig/motion.json` after `idle`, gates it with the rig, and
944
+ `check` names it on a line of its own (`examples/sample` carries one, `animations.json` with `hop`, the
945
+ recipe below). With exactly this file beside `examples/sample`'s config in its place,
946
+ `build` printed (this tree, rig-c as `package.json` pins it):
947
+
948
+ ```
949
+ [rig] animations beside the idle, from …/expressions.json as written: surprised
950
+ [check] beside the idle: 1 animation(s), surprised — built and gated by rigc with the rig, not measured: every bar here reads the idle
951
+ [check] check: PASS; 9 of 9 bar(s) measured, 0 skipped
952
+ ```
953
+
954
+ A bone the rig lacks is rigc's refusal at the rig stage, in rigc's words (`RIG_RIGC_GREEN`); a
955
+ fault in the file itself is the loader's (the row above names each one).
956
+
957
+ **Limits.** Every slot holds one attachment, the part's own image, so an expression moves,
958
+ turns and scales the face's parts and never swaps one for another: there is no second mouth
959
+ shape to key, and no lip-sync. `check` measures the idle alone: an animation beside it has
960
+ passed rigc's gate and nothing else — no seam, stretch or hole bar has read it.
961
+
962
+ ### Recipe: a soft region driven by physics
963
+
964
+ A jiggle (a bust, the hips) is a patch of one part that lags behind the body. The package
965
+ proposes none and judges none; what it gives is the three pieces to build one, all in the
966
+ config. `examples/sample` carries a worked one (issue #183, PR #187), and its config is the
967
+ fragment below, cut to the bust:
968
+
969
+ ```jsonc
970
+ "bones": [ …,
971
+ { "name": "chest_jiggle", "parent": "chest", "at": [419, 320] }, … ],
972
+ "meshes": { "topwear": {
973
+ "contour": { "tolerance": 1, "margin": 1, "spacing": 18, "stray": 4,
974
+ "regions": [ { "name": "bust", "shape": "circle", "cx": 419, "cy": 320, "r": 28,
975
+ "bone": "chest_jiggle", "spacing": 6, "band": 16 } ] },
976
+ "r": 8, "segments": [ ["chest", [419, 223], [419, 437]], ["hip", [419, 437], [419, 557]] ] } },
977
+ "constraints": [
978
+ { "type": "physics", "name": "bust_phys", "bone": "chest_jiggle",
979
+ "x": 0.5, "y": 1, "inertia": 0.25, "strength": 120, "damping": 0.92, "mass": 1, "mix": 1 } ]
980
+ ```
981
+
982
+ - **A bone the config authors**, under the bone the patch rides on. `propose` writes no such
983
+ bone and no constraint; it is yours, and nothing keys it — the constraint moves it.
984
+ - **A region in a `contour` (or `auto`) mesh** that hands the patch to that bone: weight 1
985
+ inside the shape, falling to 0 across `band` (*A contour mesh*, above). A `grid` mesh carries
986
+ no region, so the part moves from the lattice to the outline mode first; `sample` traced
987
+ `topwear` at its former grid spacing (18) and `bottomwear` at 28. Place the shape on the
988
+ patch alone: on `sample` one circle at the hip centre reached the clasped hands, painted
989
+ inside `bottomwear`, so the hips are two side circles (`hip_r`, `hip_l`) on one bone.
990
+ - **A `physics` constraint** on that bone in `constraints`, in rig-c's shape; every field but
991
+ the bone is rigc's to accept.
992
+
993
+ `build` on `sample` as tracked (PR #187; the values above are the ones tuned for its `hop`, issue #183):
994
+
995
+ ```
996
+ check: PASS; 9 of 9 bar(s) measured, 0 skipped
997
+ loop physics: 2 physics constraint(s) left out of the loop's frames, reported, not judged — "bust_phys" on bone "chest_jiggle" (rig.json), "hip_phys" on bone "hip_jiggle" (rig.json)
998
+ loop: idle 49 frame(s) at 12 fps, f0000 vs f0048 (t = 4s): max |d| 0 (0 required)
999
+ ```
1000
+
1001
+ The `loop physics:` line is the loop bar saying what it did not read (§7, *loop*): rig-c's
1002
+ render resets physics at frame 0 and steps it, so the idle's last frame differs from its first
1003
+ by whatever the spring is still doing; the bar is read off the same rig with every physics
1004
+ constraint left out, and the constraints left out are listed, reported and not judged
1005
+ (`check.json`'s `loop_physics`, PR #184). It is not a measure of the jiggle.
1006
+
1007
+ **Limits.** The idle drives the spring, and a slow idle barely moves it. Measured on `sample`
1008
+ (PR #187, rig-c `render --geometry`, the physics build against the same build with
1009
+ `constraints` removed): under its 4 s breath (`chest` `translatey` amplitude 1.3 px), the bust
1010
+ moves at most 0.155 rig px (idle frame 3, t 0.25 s); the hips move 0.000 at all 49 frames,
1011
+ because the idle keys `chest` and never `hip`, so a child of `hip` receives no motion. A spring
1012
+ of strength 100 trails a parent moving at about 2 px/s by the order of speed over its angular
1013
+ frequency, about 0.2 px (PR #187, from the definition): no setting makes a 4 s breath read as a
1014
+ jiggle (with the values tracked now, `bust_phys` inertia 0.25, strength 120, damping 0.92, the
1015
+ idle moves the bust at most 0.066 rig px, idle frame 2). Faster keys on the parent bone — a hop,
1016
+ a turn, in a named animation (the recipe above) or a consumer's own — are what the spring
1017
+ answers. `sample`'s `animations.json` holds one, `hop` (1 s: `hip` `translate` y down 5, up 24,
1018
+ landing at -6 at 0.45 s, settling at 0.55 s; nothing keys the jiggle bones): rendered at 30 fps
1019
+ against the same build without its constraints, the bust moves at most 6.583 rig px and the hips
1020
+ (`hip_phys` inertia 0.2, strength 160, damping 0.88, mass 1.5) at most 4.432 (frame 13, the fall
1021
+ into the landing), and both overshoot after it, 3.38 and 1.38 rig px (frames 21 and 22;
1022
+ `docs/evidence/sample-hop.png`). No bar here judges the jiggle, and two limits follow from that:
1023
+
1024
+ - **The region's `band` caps the displacement.** The patch moves as one piece and the band is
1025
+ where the mesh stretches to follow it, so physics values that carry the patch further than
1026
+ its band absorbs squeeze or stretch the band's triangles past the texture-stretch ceiling.
1027
+ `check` reads the idle alone and does not see it; check's own reading applied to `hop`'s
1028
+ geometry with PR #187's values (inertia 0.5, strength 100, damping 0.85, mass 1, a bone
1029
+ displacement of 9.85 rig px against bands of 16 and 12) gives
1030
+ `CHECK_TEXTURE_STRETCH: mesh "bottomwear" triangle 13 (vertices 5 183 6), edge 6-5, idle frame 13 — the edge is 0.233 times its rest length (max(ratio, 1/ratio) 4.29); <= 1.926544 is required`
1031
+ and `topwear` 2.597; with the values tracked now it reads 1.697 (`topwear`) and 1.566
1032
+ (`bottomwear`). Tune the constraint against the animation that drives it, not the idle.
1033
+
1034
+ #### A part only physics moves, in the automatic mode
1035
+
1036
+ A tie, a strand or a hem whose chain only a `physics` constraint turns has no idle key that
1037
+ deforms it, so `auto` refuses it, `AUTO_MESH_NO_STIMULUS`, naming the bones with no motion and
1038
+ the field: the motion gate measures against motion, and the package derives none from a
1039
+ constraint. Declare the bend in `meshes.<part>.auto.stimulus` (issue #196, the field table in
1040
+ *An automatic mesh*):
1041
+
1042
+ ```jsonc
1043
+ "scarf": { "auto": { …, "motion": { "maxLocalDeformation": 1 },
1044
+ "stimulus": { "chain": "robe_r", "degrees": 9 } },
1045
+ "r": 6, "segments": [ "robe_r" ] }
1046
+ ```
1047
+
1048
+ Choose `degrees` as the largest bend, per link, that the physics is expected to reach in the
1049
+ animations that drive it: the mesh is held only to what was declared, so a smaller bend passes a
1050
+ mesh that tears at the real one, and nothing here checks the declaration against the
1051
+ constraint. The recipe for a thin strand only physics moves is therefore two steps: put its
1052
+ chain on the strand's own art, and declare the stimulus. The chain's position decides whether
1053
+ the motion bound can be held at all: on the scarf strap cut out of the sweater, with the chain
1054
+ on the strap (the tracked example's), the automatic mesh passes at the default
1055
+ `maxLocalDeformation` 1 with 68 vertices (the contour mesh's 133); with the chain beside the
1056
+ strap, as the proposal first placed it, the same part is refused `AUTO_MESH_MOTION` at 1 and
1057
+ passes only at 3, with its hull alone (31 vertices) — a hull cannot hold a bend the chain does
1058
+ not run through. Raising the bound is the author's choice, not the package's; moving the chain
1059
+ is the fix.
1060
+ - **An added animation can move the idle's figures.** The render's viewport is fitted over
1061
+ every animation, and the idle frames share it, so an animation that travels further than the
1062
+ idle redraws the idle at another size: adding `hop` to `sample` narrowed the idle render from
1063
+ 201 to 198 px wide and moved `check.json`'s `BREATH_VISIBLE` `torso_heat_mean` from 7.8 to
1064
+ 7.705, every status unchanged, although `check` reads the idle alone.
1065
+
1066
+ ## 4. The command order
1067
+
1068
+ Two steps are external: the See-through runs (by any route; the optional `comfy seethrough` adapter is only a client for a ComfyUI box). Everything else is this tool.
1069
+
1070
+ **What the config holds at each step.** The config fills in as the loop runs, and
1071
+ each step reads it through the loader for that point, which requires exactly what
1072
+ the step reads. `src/config.ts` states it once (`CONFIG_REQUIRES`), the loaders take
1073
+ their required keys from there, and the selftest compares this table with it
1074
+ (`AS15`). Every loader refuses unknown and retired keys and checks `generation`
1075
+ whenever it is present; a section a later step writes may already be there, and an
1076
+ early loader does not read it.
1077
+
1078
+ | loader | steps | requires | the step also refuses without |
1079
+ | --- | --- | --- | --- |
1080
+ | `paint` | 0 `comfy paint` | `key`, `generation` | — |
1081
+ | `layers` | 1 and 4 `inputs`, 6 `assemble --propose-plan` | `key`, `seethrough`, `assemble.rig_scale` | `seethrough.head_box`, for `--propose-plan` only (`ASSEMBLE_FIELD_PRESENT`) |
1082
+ | `assemble` | 7 `assemble` | `key`, `seethrough`, `assemble.rig_scale`, `assemble.plan` | `seethrough.head_box` (`ASSEMBLE_FIELD_PRESENT`) |
1083
+ | `full` | 8 `propose --from-config`, `propose --compare`, `compare` (a side that states a `key`), `rig`, 9 `build` | `key`, `assemble.rig_scale`, `assemble.plan`, `bones`, `meshes`, `regions`, `motion` | for `build`, the `assemble` row's too: its assemble stage reads through that loader after this one |
1084
+
1085
+ A missing field is `CONFIG_FIELD_PRESENT` naming it; a missing `generation` names
1086
+ the fields the block holds, and a missing `assemble.plan` names
1087
+ `assemble --propose-plan`, which prints one. `build` runs `rig`, so it asks the
1088
+ full loader before its assemble stage writes anything.
1089
+
1090
+ 0. **The painting (optional).** `rig-parts comfy paint --config config.json --out inputs --host <url>`
1091
+ generates `painting_<seed>.png` on a ComfyUI box; any other route to a painting
1092
+ skips this step and leaves `generation` out. Config: the `paint` row.
1093
+ 1. **The full image.** `rig-parts inputs --source inputs/painting.png --config config.json --out inputs`
1094
+ writes `st_input_full.png`, the painting centred on a white square of its longer
1095
+ side: white left and right of a portrait painting, above and below a landscape
1096
+ one. The pad is See-through's input only; `propose --head-box` and `assemble` map
1097
+ the full run back through it, so the rig stays in the painting's pixels — unlike
1098
+ a painting padded to a square by hand, whose padding becomes part of the rig.
1099
+ Config: the `layers` row, `head_box` not yet. A translucent painting is refused
1100
+ (`INPUTS_PAINTING_OPAQUE`).
1101
+ 2. **See-through, full run** (external).
1102
+ 3. `rig-parts layers inputs/layers/full` — read it. A refusal here is about the
1103
+ files, not the art (§6, *Reading the inputs*). A `WARN` line is about the art: a
1104
+ layer See-through made that is not plausibly part of the figure, which step 6
1105
+ leaves out (§6, *Plausibility*). It refuses nothing.
1106
+ 4. `rig-parts propose --head-box --full inputs/layers/full --canvas <W>x<H>` →
1107
+ `seethrough.head_box`. Run `rig-parts inputs` again: it now also writes
1108
+ `st_input_head.png`, the painting cropped to that box at its exact size.
1109
+ 5. **See-through, head run** (external), on that crop. See-through's own second,
1110
+ head stage does not replace it: that stage's layers are pasted back at the full
1111
+ run's scale on every route that was read (README *Getting See-through layers*),
1112
+ so the full run's eyes stay at its density — 22×13 px for the demo's right eye
1113
+ white at `resolution` 1024, against 60×38 px in the head run.
1114
+ 6. `rig-parts assemble --propose-plan --source … --full … --head … --config config.json`
1115
+ → paste `plan` and `extend_below_crop` into `config.assemble`. Config: the
1116
+ `layers` row, with `head_box`.
1117
+ Read `notes`: a layer left out by a plausibility rule is named there with its
1118
+ figures and the rule (§6, *Plausibility*).
1119
+ 7. `rig-parts assemble … --out work` — read its `uncovered hole` lines and look at
1120
+ `work/render/recomposite_error_rig.png` (§5): red is painting no part holds, which
1121
+ no later stage can see — then
1122
+ `rig-parts propose --parts work/rig --source inputs/painting.png --out work` →
1123
+ `work/proposal.json` and `work/render/landmarks.png` (+ `_head`). Copy
1124
+ `bones`, `meshes`, `regions` and `motion` into the config. Then read what
1125
+ was looked at. After the LINT summary, one `coverage <bone> [<role>]:` line
1126
+ per bone says `checked, clean` (and which checks read it), `checked, LINT`,
1127
+ or `not checked` and why — its role (a `control` binds nothing and may sit
1128
+ off the art by design; the role is `src/structure.ts`'s, as `compare`
1129
+ prints it), a single bone named as a segment or an explicit
1130
+ `[bone, from, to]` segment (the off-art check reads only chain links), a
1131
+ region, a mesh that names no part, a torso bone absent, a chain the joints
1132
+ declare no line for — and a `coverage:` summary counts the three apart.
1133
+ A `not checked` bone is one nothing verified: look at it on
1134
+ `landmarks.png`. `work/basis.json` (spec `spine-parts-basis/1`) says, per
1135
+ proposed bone, what its origin and its stated tip rest on: `joint` (which,
1136
+ with its state and score as the keypoint file declared them), `mask` (which
1137
+ part, which measure), `ratio` (which rule, which constants) or `derived`
1138
+ (which bones), and `fallback` when a rule stood a value in because what it
1139
+ reads was empty; `frame` says where the face box, the eye axis and line,
1140
+ the figure and the torso came from. A `ratio` bone or one with a
1141
+ `fallback` is a guess to check first. Add `--compare config.json` to have
1142
+ each bone also say whether the config's differs (origin, tip, parent, in
1143
+ px) or is `proposal only`, and `config_only` list the bones the proposal
1144
+ lacks. No confidence is computed. Config for
1145
+ `assemble`: the `assemble` row — no rig section yet, because this `propose`
1146
+ drafts them from the parts `assemble` writes.
1147
+ For a posed figure (seated, reclining), add `--keypoints keypoints.json` (and
1148
+ `--person <id>` when the file holds more than one person): the joints it gives
1149
+ place the neck, hip, chest and sleeves (§3, *A posed figure*).
1150
+ 8. Correct, then `rig-parts propose … --from-config config.json` to redraw the
1151
+ config's own bones and LINT them — every chain link against its mesh's art, and
1152
+ the single bones `hip` and `chest` against each other and the figure (a config
1153
+ without them prints a `note:` that those lines did not run); repeat until it
1154
+ prints no LINT line (exit 0). Add the same `--keypoints` to lint by the joints.
1155
+ Config from here on: the `full` row.
1156
+ `rig-parts compare --left work/proposal.json --right config.json` says what the
1157
+ correction changed, bone by bone: each origin, parent, tip, length and direction
1158
+ as its own figure, and the bones renamed, added or removed by name (§5). It reads
1159
+ no parts and no painting, and fails only on a bone a `--map` file's `required`
1160
+ list names that is missing (exit 1). A proposal is read under the loader's own
1161
+ rules for `bones`, `meshes`, `regions` and `motion`.
1162
+ 9. `rig-parts build --config config.json --source inputs/painting.png --full … --head … --out out [--loop]`
1163
+ — assemble, rig and check in one process. `propose` is not part of it, on purpose:
1164
+ a proposal is a draft, and the config you corrected is the input.
1165
+ `--idle-keys direct` is forwarded to the rig stage as `rig` takes it; a config
1166
+ with a two-bone ik over chain links the idle keys needs it (§3, `constraints`).
1167
+ `rig-parts compare --left config.json --right out/rig/rig.json` reads the rig
1168
+ the build wrote against the config it came from: the rig's stage carries it into
1169
+ rig px, so every origin reads 0, each chain link's tip, length and direction its
1170
+ config's, and every `<bone>_ctl` the rig stage inserted is the bone between a
1171
+ keyed mesh bone and its parent (§5).
1172
+ 10. **What a scene requires (optional).** When the motion a scene asks of the rig
1173
+ is known — a hand that stays on a cup, a sleeve that takes half an arm's turn, a
1174
+ joint that must stay inside a range — write it into a
1175
+ `spine-parts-requirements/1` file and hand it to `check` (or `build`):
1176
+ `rig-parts check --rig out/rig --parts out --out out/scene --requirements scene.json`.
1177
+ Every bar in it is yours; `check` supplies none, solves nothing and poses every
1178
+ frame through rig-c (§7, *Declared requirements*). A rig composed of
1179
+ several characters is measured the same way, `--rig` naming the composed spec.
1180
+ 11. **Several characters in one rig (optional).** Run steps 0–9 for each
1181
+ character on its own, isolated input, to a green `build`. Then write a
1182
+ `spine-parts-scene/1` file — the canvas, each character's build and offset,
1183
+ an optional background plate with its provenance, and the draw order between
1184
+ the characters' slots — and run
1185
+ `rig-parts compose --scene scene.json --out scene [--requirements req.json]`
1186
+ (§7, *Composing several characters*). Nothing in steps 0–9 changes for it, and
1187
+ nothing is inferred from the art: the order, the offsets and the plate's
1188
+ provenance are yours.
1189
+
1190
+ The selftest runs this order, with the README's flags, on each fetched example from
1191
+ a config holding only `key`, `seethrough` and `assemble.rig_scale`, pasting each
1192
+ proposal in as the steps above say (`RL01`); every step must exit 0.
1193
+
1194
+ ## 5. After each stage: what to read
1195
+
1196
+ | stage | read | green looks like | a common red |
1197
+ | --- | --- | --- | --- |
1198
+ | `layers` | the table, then the `WARN` lines | every tag the plan will need has opaque pixels; one `face` in the head run; `0 WARN line(s)` | a run whose layer PNG is not its box's size (`LAYERS_PNG_MATCHES_BBOX`); a `WARN PLAN_LAYER_…` line (§6, *Plausibility*) |
1199
+ | `sheet` of both runs | the tile list (and the sheet, if you can see) | eyes, irises, lashes and brows as left/right pairs in the head run | a head box that cut off an ornament: move `head_box`, re-run the head crop |
1200
+ | `assemble` | one line per part, the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`; and look at `render/recomposite_error_rig.png`. Between the part lines and the totals, one `cut: "<part>" opaque <n> = "<part>" <kept> + "<piece>" <taken> …` line per part an `assemble.cuts` entry cut (below; it ends `; under <n> px filled, <m> px left empty` when a cut of that part has `under`), then one `fringe pushed back: "<part>" <px> px, where the painting shows "<holder>" <px> px, …` line per part whose fringe was cleared (below) | on the examples: `sample` 0.83 / 98.0 % / 4,359 / 1,185, 259 holes, the largest 94 px; `demo` 2.34 / 95.8 % / 10,340 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (the demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where. When neither run holds it at all, an `assemble.patches` entry cuts it from the painting (below) |
1201
+ | `propose` | `note:` lines, `LINT` lines, the `coverage` lines, `landmarks.png`, `basis.json` | no LINT line: every chain link lies on its mesh's art, and the hip is below the chest and the figure's top quarter; the `coverage:` summary says how many bones that verdict covers — a `not checked` bone was read by no check (its line says why), and a `ratio` or `fallback` bone in `basis.json` is a guess | a link off the art (a bone on the background) — move it onto the layer; `LINT hip at [x, y] is not below chest at [x, y]: …` or `LINT hip at [x, y] is above 0.25 of the figure height (figure y T..B, so hip y must be at least L): a hip at the shoulders` — move `hip` down to the waist (and `chest` between it and the neck); a `hanging strand … -- no chain proposed` note — that strand hangs stiff until you add a chain down the x and rows it names (§3); a `no blink: …` or `blink without brows: …` note — the idle will not blink (or its brows will not drop), because no part came from an `eyewhite` (or `eyebrow`) layer; the note names the tags looked for; a `no eyewhite part for eye_<s> …` note — the iris and lash parts it names ride `head` and do not blink, because no `eyewhite-<s>` part made that side's eye bone: take the eyewhite from the other run, or keep them on `head`; a `no face part: face box derived from …` note (always the first) — `head`, `neck` and every face-height scale come from a box guessed off the hair and neck (§3): check `head` and `neck` on `landmarks_head.png` and move them; when it adds `the blink shuts the eyes over no face part`, read `BLINK_NO_HOLE` after `check` — an eye with no face under it can open a hole (the `sample` with only its face removed: 587 px). Under `--keypoints`: the `lint rule set:` line first (joints, or screen and why), then the joint notes — `missing: the rule "…" placed bone X instead` is a bone still guessed; `not used — …` names why an arm's joints did not place its sleeve (for a wrist on no part, the nearest part and the `--wrist-snap` that would bind it); a `wrist snap: …` note is a wrist bound past 15 px by the flag — check that chain's tip on `landmarks.png`; a `one handwear part holds both wrists` note is two chains on one mesh — cut the part with `assemble.cuts` to separate them; `LINT chest at … is not between the neck joint …`, `LINT hip at … is not past chest …`, `LINT hip at … is nearer the neck joint …` and `LINT <chain><i> at … does not advance from <s>_shoulder toward <s>_wrist` are the joint-frame lines: move that bone between, past or along the joints they name |
1202
+ | `compare` | one line per pair (origin, parent, tip, length, direction — or SKIP and why), then the per-row summary, the unmapped bones of each side, `required:`, each side's roles and the `controls:` line | proposal against the config you corrected: the rows you changed and no others. Config against the `rig.json` `build` writes for it: measured on both examples, every origin within 6.19e-7 px (the rig's offsets are written to 6 places) and every chain link's tip and length within 4.99e-4 px (`length` is written to 3), printed to three places as `0.000` (a signed figure as `+0.000` or `-0.000`); `parent … is the right's ancestor at depth 2, <bone>_ctl between` for every keyed mesh bone, no `DIFFERENT` and no `NOT MAPPED` | `parent DIFFERENT: left a, right b` — a bone hangs from another parent than the other side's; `frames not related` — a rig with no stage (`skeleton.width`/`height`), two configs at different `rig_scale`: the distance rows SKIP, so declare the frame in a `--map` file if the author knows it; `FAIL STRUCTURE_REQUIRED_PRESENT` — a bone the map requires is missing through its pairs |
1203
+ | `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`); in `rig.json`, each chain link's (or its `_ctl`'s) `length` and `rotation` | `cover 1.00000` on every mesh, both gate summaries `0 failed` (the compile and the packed pages); every link's `length` the distance to the next link and `rotation` its direction (Spine degrees, counter-clockwise, y up, local to the parent), every offset under it — a child bone's `x, y`, a weight's bind `x, y`, a region's `x, y` — in that turned frame, and a region on a link carrying `rotation` that turns it back upright. Nothing moved: `flattenRig` (`src/rig.ts`) turns every offset back and gives the unturned numbers | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
1204
+ | `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:` (and `loop physics:` when the rig declares a physics constraint, §7), `seam:`, the six judgement lines, `RECOMPOSITE_HOLES` and, under `--source`, `SETUP_POSE_VS_SOURCE` (§7), `check.json`; the last line says how many of the nine bars measured and names the ones that said SKIP | `check: PASS; 9 of 9 bar(s) measured, 0 skipped` on the examples, and a judgement line SKIP only where the character lacks what it reads; on a rig spec with no `parts.json` or no `idle` (a merged rig, §7 *Measuring a rig rig-parts did not assemble*), PASS with the skipped bars named | `CHECK_SEAM_WITHIN_BAR` or `CHECK_LOOP_CLOSES` (§6) |
1205
+ | `check --requirements` (and `build --requirements`) | after the lines above, one line per declared requirement, `NAME: PASS`, `FAIL` or `NOT MEASURABLE — …` with its figures, the bar as declared and the worst frame; then `requirements: N declared — M measured (P PASS, F FAIL), K NOT MEASURABLE; not declared: <kinds>`; `check.json`'s `requirements` block; the renders under `requirements/` (§7, *Declared requirements*) | every line PASS and `0 NOT MEASURABLE`; a follow's `released_copy` says whether rigc's consumer-driven door was taken on the throwaway copy | `CHECK_REQUIREMENT_MET` (a figure past its bar; the line names the frame) or `CHECK_REQUIREMENT_MEASURABLE` (the quantity is undefined in the frames: a tip or an axis on a bone of length 0, an aim with no line, a follow no frame of which reaches its least drive); the last line then reads `check: FAIL — B bar(s) not met, R declared requirement(s) not PASS` |
1206
+ | `compose` | the `[compose]` lines: one per character (its offset, its scale, its placed bounds, the shift and how many first-level bones, regions on `root` and weights bound to `root` it moved, its bone, slot, track and constraint counts, its `rig_scale` and idle), the declared order and the slot count it expands to, rigc's gate lines; then the `[check]` lines as for `check`, and `scene.json` | both gate summaries `0 failed`, `check: PASS` with the bars that measured and the ones that said SKIP named — on a composed rig `4 of 9`: the gate, the loop, `CHAIN_LAG` and `TEXTURE_STRETCH` (there is no `parts.json`; each character's own `build` measured all nine). `scene.json`'s `order.slots` is the order the compiled skeleton draws | a `SCENE_*` refusal (§6, *compose*) — the scene file, a build, or the order; `COMPOSE_RIGC_GREEN` — rigc's own line, quoted |
1207
+ | `loop` (inside `build --loop`, or `loop --frames … --out …`) | the dropped-duplicate line, each file's line, then `loop: idle.png N B (lossless); idle-indexed.png N B (max …, mean …); idle.gif N B (max …, mean …)` | `f0048.png equals f0000.png byte for byte, so it is dropped` | `LOOP_ENCODE` (§6) |
1208
+
1209
+ `loop` writes three files from one frame set, and they are not interchangeable.
1210
+ `idle.png` (`loop --out x.png`) is the lossless APNG: every frame decodes to the
1211
+ rendered frame byte for byte, so it is the exactness record. `idle-indexed.png`
1212
+ (`loop --out x.png --palette`) is an indexed APNG — colour type 3, one palette for
1213
+ every frame (255 median-cut colours and one transparent entry, alpha graded per
1214
+ entry, no dithering, filter None) — and is the small file to show. `idle.gif`
1215
+ (`loop --out x.gif`) is the same median cut in a GIF. The indexed APNG and the GIF
1216
+ print their palette error, per channel over R, G and B of every frame (and alpha's
1217
+ max for the APNG); a figure is a measurement of the file, not a bar. On the demo:
1218
+ 13,645,503 B lossless, 1,706,719 B indexed and 1,812,288 B GIF, both palette files at
1219
+ max 57, mean 1.601 — the two share one quantiser, so their error is the same by
1220
+ construction and the size is the difference. That is `--seam silhouette` (the README's
1221
+ animation, rig-c 2.1.3, and 2.10.1 to the byte); with `--pack-shape rect` the same
1222
+ build writes 13,645,350, 1,706,520 and 1,812,238 B, and with `--page-edges pot
1223
+ --pack-shape rect` 13,645,519, 1,706,785 and 1,812,625 B, all at the same error (2.10.1
1224
+ reproduces both). The loop shows the blink closed: the
1225
+ eyes' hold is at least one 12 fps frame (below), so whatever `motion.blink.t` is, one
1226
+ idle frame lands inside it, and `check.json`'s `BLINK_NO_HOLE.idle_frames_closed`
1227
+ names it — `[29]` on both examples.
1228
+
1229
+ `--seam near-white` (the default) is the reference implementation's rule: where the
1230
+ flat stack of parts differs from the painting by more than 60, the top part takes the
1231
+ painting's colour — except where the painting is near-white, which protects the page
1232
+ and also every white garment. `--seam silhouette` protects near-white pixels only
1233
+ outside the figure. On the demo it lowers recomposite error pixels from 10,340 to
1234
+ 8,644 (`assemble`'s `error px > 40`, at v1.5.2); on the sample it changes nothing
1235
+ (4,359 under both). At v0.14.0 the demo's were 11,050 and 9,540, and no check bar
1236
+ changed. The default stays
1237
+ the reference's so the examples stay comparable with it.
1238
+
1239
+ **The fringe push-back** (issue #119; under either seam rule, no flag). Both places
1240
+ that write the painting's colour into a part act only at alpha 250 and above, so a
1241
+ part's anti-aliased fringe in another garment's colour — a scarf's red left on the
1242
+ edge of a sweater layer, over the sleeve — reaches the rig as See-through drew it,
1243
+ and `check`'s seam cannot see it (it compares the setup pose with the same parts).
1244
+ Before the seam override, `assemble` clears a part's pixel when the stack there
1245
+ differs from the painting by more than 60 (the seam override's own test, and its
1246
+ rule's near-white guard), the part is drawn after the pixel's holder — the top-most
1247
+ part at alpha 250 or above — and the holder's own colour is within 60 of the
1248
+ painting's: the painting shows the holder there, so the pixel is the holder's to
1249
+ draw. A pixel nothing holds stays (clearing it would open the page), and so does one
1250
+ over a holder in another colour (clearing it would hand the holder the front part's
1251
+ colour). The clearing never splits a part's art: a crumb it cuts off is cleared too,
1252
+ and a cleared bridge to opaque art is put back, so no part gains an island
1253
+ (`CONTOUR_ONE_ISLAND` reads the islands it read before). `parts.json` records each part's cleared pixels by holder
1254
+ (`fringe_pushed_back`), the counts in it are taken after, and the assemble line above
1255
+ names them. On the examples (default flags, measured against the v0.14.0 build of
1256
+ the same inputs): `demo` 1,193 px over 16 parts, recomposite error pixels 11,050 →
1257
+ 10,340; `sample` 276 px over 15 parts, 4,512 → 4,359; uncovered error pixels and
1258
+ holes unchanged on both, `check` 9 of 9 on both. The `--project` comparison in
1259
+ this section and the `SETUP_POSE_VS_SOURCE` example figures in §7 were measured
1260
+ before it, at v0.14.0; the `--seam` figures above are v1.5.2's.
1261
+
1262
+ **Page edges.** `rig`, `check` and `build` pack through `rigc build --pack
1263
+ --page-edges <v>`, and hand the value to rigc verbatim. The default is `free`,
1264
+ the least-area page the parts need. `--page-edges pot` is the opt-out: a power of
1265
+ two on both edges, which is the page every build wrote before this flag. The pack
1266
+ line says which one ran, because rigc writes `, page edges free` after the padding under
1267
+ `free`. Measured on the two examples by `tools/atlas_population.ts` with rigc 1.5.1, and
1268
+ again with rigc 2.0.3, 2.1.3 and 2.10.1 under `--pack-shape rect` (the frames table
1269
+ by the same idle render), which reproduced every figure in both tables and the one
1270
+ check figure below (2.10.1: pages, atlases, idle frames and `check.json` byte-identical
1271
+ to 2.1.3's):
1272
+
1273
+ | build | page | covered | page opaque | page / figure |
1274
+ | --- | --- | --- | --- | --- |
1275
+ | demo, `pot` | 1024x2048 | 56.3 % | 36.8 % | 2.651 |
1276
+ | demo, `free` | 967x1338 | 91.2 % | 59.7 % | 1.636 |
1277
+ | sample, `pot` | 512x2048 | 49.7 % | 28.7 % | 3.294 |
1278
+ | sample, `free` | 479x1166 | 93.4 % | 53.9 % | 1.755 |
1279
+
1280
+ rigc's help states the cost: *"a smaller page, at the cost of region attachments
1281
+ sampling within 1 LSB of the loose build rather than exactly"*. On these builds
1282
+ every one of the 49 idle frames differs from the `pot` build, by at most 1 level
1283
+ in any channel:
1284
+
1285
+ | example | idle frames that differ | pixels that differ (all 49 frames) | max \|d\| |
1286
+ | --- | --- | --- | --- |
1287
+ | demo | 49 of 49 | 2,373 | 1 |
1288
+ | sample | 49 of 49 | 651 | 1 |
1289
+
1290
+ Only one check figure moved. It had no bar: sample's reported screen-space face
1291
+ mean in `STILL_REGIONS_DARK`, 11.475 under `pot` and 11.472 under `free` (a pixel
1292
+ figure the line no longer reports since issue #123; it reads geometry, which the page
1293
+ does not enter). Every
1294
+ barred figure and every status is the same under both. The atlas rigc writes says
1295
+ `filter: Linear, Linear` and has no `repeat` line, so choose `pot` for a consumer
1296
+ that mipmaps or repeats the page.
1297
+
1298
+ **Pack shape.** `rig`, `check` and `build` also hand rigc `--pack-shape <v>`
1299
+ verbatim (rig-c 2.1.0 and later). `rect` keeps every region's cell apart, the
1300
+ only packing before 2.1. `polygon`, the default here (rigc's own is `rect`), packs a
1301
+ region that only meshes draw by its emitted hull, so a neighbour may sit inside its
1302
+ rectangle where the hull is not, with the padding kept between footprints; a region
1303
+ attachment stays its rectangle. rigc gates the result on the packed pages with
1304
+ `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` (SKIP on the compile pass, where every part has
1305
+ its own page; PASS on the packed pass, both shapes, both examples). The pack line ends
1306
+ `, shape rect` or `, shape polygon` under either, and `check.json`'s `pack_mode` records
1307
+ both flags. Measured on the two examples with rig-c 2.1.3, and again with 2.10.1,
1308
+ whose pages, atlases, idle frames and `check.json` are byte-identical to 2.1.3's under
1309
+ both shapes and both edges (the full entry, spine-core 4.3.13 beside it),
1310
+ `rig-parts build` with `--page-edges free` and each shape, the table columns by
1311
+ `tools/atlas_population.ts`:
1312
+
1313
+ | build | page | page area | covered | page opaque | page / figure |
1314
+ | --- | --- | --- | --- | --- | --- |
1315
+ | demo, `rect` | 967x1338 | 1,293,846 | 91.2 % | 59.7 % | 1.636 |
1316
+ | demo, `polygon` | 922x1348 | 1,242,856 (−3.9 %) | 95.0 % | 62.1 % | 1.571 |
1317
+ | sample, `rect` | 479x1166 | 558,514 | 93.4 % | 53.9 % | 1.755 |
1318
+ | sample, `polygon` | 477x1151 | 549,027 (−1.7 %) | 95.0 % | 54.8 % | 1.725 |
1319
+
1320
+ Covered is Σ region rectangles over page area, as rigc prints it; under `polygon` the
1321
+ rectangles may overlap where a hull leaves room, so it is not a union and can exceed
1322
+ 100 %. Page opaque is the measure of what the page holds. Under `--page-edges pot` the
1323
+ shape changes nothing on these two examples: both shapes write 1024x2048 and 512x2048,
1324
+ and the idle frames are identical.
1325
+
1326
+ The cost, from `rect` to `polygon` (the idle `check` renders, `rigc render --animation
1327
+ idle --fps 12 --max 640`, compared frame by frame, max over R, G, B and A):
1328
+
1329
+ | example | idle frames that differ | pixels that differ (all 49 frames) | max \|d\| |
1330
+ | --- | --- | --- | --- |
1331
+ | demo | 49 of 49 | 2,594 | 1 |
1332
+ | sample | 49 of 49 | 740 | 1 |
1333
+
1334
+ That is the class `free` already costs (the table above, measured by the same
1335
+ comparison), a little larger. No figure in `check.json` moved but `pack_mode`: every
1336
+ bar, every status and every reported figure is the same under both shapes.
1337
+ `--pack-shape rect` gives back the page and atlas 0.7.0 wrote: on both examples
1338
+ they are byte-identical to rigc 2.0.3's `--page-edges free` build of the same rig (the
1339
+ skeleton JSON differs only in its relative `images` path), and selftest `CK53` holds
1340
+ `rect` to rigc's own rectangle page.
1341
+
1342
+ The hull ceiling (rig-parts #58: hull area over rectangle area, 0.812 on demo and
1343
+ 0.814 on sample) is not what the packer realises here: the hollows inside the meshes'
1344
+ rectangles are worth only what fits into them. On the polygon pages, read off the
1345
+ atlas, demo's packed rectangles overlap in 21 pairs (17 a region attachment inside a
1346
+ mesh's rectangle, 4 a mesh inside another's) and sample's in 12 (10 and 2); no two
1347
+ region attachments overlap. Meshes in place of regions would be rigc's stage 2 of
1348
+ polygon packing (firejune/rigc#1093), which rigc measured and folded: on the three
1349
+ production rigs most favourable to it, it adds 0.15 % of the rectangle area over
1350
+ stage 1 at tolerance 0, and nothing at the mesher's tolerance 1 or under `pot`; on
1351
+ these two examples −0.3 to −1.3 points. No build converts a region attachment.
1352
+
1353
+ `--project core` (the default) is the reference's projection rule: a layer takes the
1354
+ painting's pixel only inside its top-most `alpha >= 250` area eroded by a 5x5 square,
1355
+ so a part a few pixels wide (a lash, a brow, an iris) takes none. `--project visible`
1356
+ keeps the erosion only along a rim where a later layer of the run is in front, and
1357
+ takes every other top-most `alpha >= 250` pixel; neither rule takes a fringe pixel
1358
+ (alpha below 250). On the examples it lowered the pixels that are visible but not
1359
+ projected from 36,227 to 22,476 (`sample`) and 76,801 to 49,672 (`demo`), recomposite
1360
+ error pixels from 4,512 to 4,116 and 11,050 to 9,820, and the check seam from 0.207 to
1361
+ 0.206 and 0.326 to 0.325, with no other check figure changed. The default stays the
1362
+ reference's so the examples stay comparable with it. `build` takes both flags.
1363
+
1364
+ **`rig --idle-keys ctl|direct`** (and `build --idle-keys`, which forwards the
1365
+ value to its rig stage; without the flag, or with `ctl`, a build writes and prints
1366
+ what it did before `build` took the flag). rig-c's `A15_IDLE_NO_MESH_BONE_KEYS`
1367
+ (profile `spine-html`) refuses an idle that keys a bone a mesh is weighted to. `ctl`
1368
+ (the default, the reference's answer) gives every such bone a same-origin
1369
+ `<bone>_ctl` parent and moves its keys there, which passes A15 on any rig-c;
1370
+ the stage prints `idle keys ctl: N mesh-driving bone(s) keyed …`. `direct` keys the
1371
+ bones themselves and writes `invariants.idleDrivesMeshes: { "why": … }` into
1372
+ `rig.json` (rig-c 1.3.0 or later), so A15 reports
1373
+ `SKIP A15_IDLE_NO_MESH_BONE_KEYS: declared by the rig (…): idle keys N bone(s) that
1374
+ drive M mesh attachment(s) totalling V vertices …`, which the stage prints; with no
1375
+ mesh-driving bone keyed it declares nothing, because rigc refuses a declaration
1376
+ that switches nothing off. The two write the same slots, skins and keys (`RG18`).
1377
+ Measured on the examples with `bun tools/idle_cost.ts` (rig-parts #13): `direct`
1378
+ has 41 bones for the demo's 72 and 32 for the sample's 56; every shown mesh moves on
1379
+ every idle frame under both (8 of 8 and 6 of 6, so 0 a dirty-skip renderer could
1380
+ skip); the pose costs 14.8 against 15.3 us per frame (demo) and 8.3 against 8.8 us
1381
+ (sample), the difference all in `updateWorldTransform`; the rendered idle frames
1382
+ differ by at most one level in a channel. The default stays `ctl`: the examples'
1383
+ `expected/rig.json` and `motion.json` are the reference's output, and the
1384
+ declaration is in the rig spec only, so `rigc validate <build> --profile spine-html`
1385
+ run on a `direct` build (it has no rig spec to read) refuses A15 once per keyed
1386
+ bone. No stage runs `validate`: the gate is `rigc build`, which reads the rig spec
1387
+ and its declaration, and is green on both (`RG19`).
1388
+
1389
+ **Patching a hole no layer holds.** When an `uncovered hole` stays large after the
1390
+ plan is right — both See-through runs dropped a piece of the figure, red in
1391
+ `recomposite_error_rig.png` — add an `assemble.patches` entry:
1392
+ the box round the hole in rig pixels, `alpha: "silhouette"` so the page round the
1393
+ figure is not taken, and a `regions.<name>` bone. Draw it `"back"` (or
1394
+ `{"before": …}` the part it belongs under) and let the box **reach under its
1395
+ neighbours**: on the selftest's hem fixture a box that only abutted them raised
1396
+ `check`'s seam from 0.323 to 0.86 with 16 px over 40, and one reaching two rows
1397
+ under each measured 0.267 with none [observed] — two regions that merely meet
1398
+ leave a resampled edge between them in the render. The patch is config, so every
1399
+ `build` cuts it again; it is never a file added after the build. Re-run `assemble`
1400
+ and read `uncovered error px` again: a patch covering `n` of the uncovered error
1401
+ pixels lowers it by exactly `n` (the selftest's `AS17` plants that and counts it).
1402
+
1403
+ **Cutting a piece out of a layer.** When one See-through layer holds two things that
1404
+ must move apart — a scarf end painted into the sweater's `topwear`, both arms in one
1405
+ `handwear` — add an `assemble.cuts` entry (§3): open `rig/parts/<part>.png` from an
1406
+ earlier `assemble`, add its `parts.json` `x`, `y` to a pixel's column and row to get
1407
+ rig pixels, and draw the polygon round the piece. The stage prints, between the part
1408
+ lines and the `fringe pushed back` lines, one line per cut part in the `pixels:`
1409
+ line's shape, counted when the cut is made (before the fringe push-back and the seam
1410
+ pass, which the records are counted after):
1411
+
1412
+ cut: "topwear" opaque 134452 = "topwear" 118973 + "scarf" 15479; band 593 px held by both
1413
+
1414
+ Give the piece a `meshes` entry over the bones that should carry it, and take those
1415
+ bones out of the part it left: on the public `scarf`, the scarf end `robe_r` runs down,
1416
+ cut out of `topwear` into its own mesh on `robe_r` (and `robe_r` dropped from `topwear`'s
1417
+ segments) leaves every `topwear` vertex still at wind 6 where the uncut mesh moved
1418
+ one by 101.5 px [observed, `docs/evidence/scarf-cut.png`]. Where the cut runs through
1419
+ opaque art, `overlap` decides the seam: at 0 the pieces abut, each edge is resampled
1420
+ against transparency, and on that cut the seam bar read 170 px over 40 against 50
1421
+ allowed (`CHECK_SEAM_WITHIN_BAR`); at 1 it read 8, at 2 it read 3, at 4 it read 2
1422
+ (the uncut build reads 2) [observed]. Those four readings drew the piece behind
1423
+ `topwear` (`{"before": "topwear"}`); drawn in front of it (`{"before": "brow_r"}`,
1424
+ the plan part after `topwear`), as the setup pose asks, the same cut reads 26 px over
1425
+ 40 at `overlap` 1, 8 and 16 alike, 24 of them along the cut line, which a wider band
1426
+ does not reach (§7, *seam*) [observed, issue #202].
1427
+
1428
+ A band covers the edge at rest; when the piece swings clear, the layer has nothing
1429
+ where it was. `under` (§3) is the author's image of what lies there, in rig pixels
1430
+ over the polygon's bounding box: `from` takes it where the cut removed its pixels and
1431
+ the image is at least as opaque as the layer was, a transparent pixel leaves its pixel
1432
+ empty (nothing lies under it), the piece must be drawn in front of `from`
1433
+ (`ASSEMBLE_UNDER_DRAW`), and the `cut:` line counts both, so an unintended hole is a
1434
+ number in the log:
1435
+
1436
+ cut: "topwear" opaque 134452 = "topwear" 118973 + "scarf" 15479; band 593 px held by both; under 3991 px filled, 11079 px left empty
1437
+
1438
+ That is the scarf cut (overlap 1, the piece drawn `{"before": "brow_r"}`) with an
1439
+ 80x540 image of each row's nearest `topwear` pixel outside the polygon, down to row
1440
+ 681 and transparent below it; `check` passes with the seam bar at 26 px over 40, as
1441
+ without the image [observed, `docs/evidence/scarf-under.png`]. Whether the sweater
1442
+ continues under that scarf end at all is the author's call: continued as a line from
1443
+ its edge just above the polygon (x 306) to its edge at its hem (x 316, row 681), its
1444
+ outline lies right of every removed pixel, and an image of only that silhouette fills
1445
+ none of the 15,070 — `ASSEMBLE_UNDER_EMPTY` [observed]. Producing the image is the
1446
+ author's.
1447
+
1448
+ `parts.json` holds one record per part, in draw order: the plan's, with each patch
1449
+ and each cut's piece where its `draw` puts it (`"back"` patches first in their own
1450
+ order, then `"back"` pieces; each `{"before": X}` patch, then each such piece,
1451
+ immediately behind `X`; `"front"` patches last, then `"front"` pieces). Its counts:
1452
+
1453
+ | field | counts |
1454
+ | --- | --- |
1455
+ | `opaque_px` | the part PNG's pixels with alpha above 8 |
1456
+ | `visible_px` | of those, the ones no later layer of their See-through run is opaque (alpha >= 250) in front of — a pixel copied in below the head crop is judged in its extend layer's run |
1457
+ | `occluded_px` | the rest of `opaque_px`: art the painting does not show, See-through's synthesis by necessity |
1458
+ | `projected_core_px` | the projection rule's candidates: top-most `alpha >= 250`, eroded (`core`) or kept off a front rim (`visible`) |
1459
+ | `source_px_taken` | of those, the ones that took the painting's pixel (the reference's count, before any merge) |
1460
+ | `visible_not_projected_px` | visible pixels whose colour did not come from projection — too thin for the core, a fringe, a rim, refused for drift, or a merge ring |
1461
+ | `refused_drift_px` | candidates refused because See-through's pixel and the painting's differ by more than 90 |
1462
+ | `merged_px` | pixels brought in below the head crop, and the ring that closes their seam |
1463
+ | `seam_override_px` | pixels the seam pass recoloured to the painting |
1464
+
1465
+ A part's `from` is `<full|head>:<tag>` — the See-through run and tag, which every
1466
+ role (`propose`) and every judgement-line region (`check`) is read from — or, for a
1467
+ patch, `painting:<its own name>`; a cut's piece carries the `<run>:<tag>` of the part
1468
+ it was cut from, so two records may share one. **A patch is 100 % source**: every pixel is the
1469
+ painting's, so it is all visible and all taken — `visible_px`, `projected_core_px`
1470
+ and `source_px_taken` equal `opaque_px`, and `occluded_px`,
1471
+ `visible_not_projected_px`, `refused_drift_px` and `merged_px` are 0. The reader
1472
+ refuses a `painting:` record that says anything else, or that names another part
1473
+ (`PARTS_FROM_KNOWN`, `PARTS_COUNTS_ADD_UP`). No tag rule reads a patch: `propose`
1474
+ puts it on the nearest trunk bone as a region with a note, and `check` counts it
1475
+ in no region.
1476
+
1477
+ `visible_px + occluded_px = opaque_px`, and every projected pixel is visible; the stage
1478
+ refuses (`ASSEMBLE_COUNTS_ADD_UP`) rather than write counts that break either, and the
1479
+ reader refuses a record that breaks them (`PARTS_COUNTS_ADD_UP`). The three visibility
1480
+ counts are this port's: a `parts.json` the reference wrote has none of them and still
1481
+ reads, but a record with only some of them is refused. The `pixels:` line after the
1482
+ per-part lines prints their totals.
1483
+
1484
+ After the records, `parts.json` holds a `recomposite` block (this port's too; the
1485
+ reference's files have none and still read): the flat stack of every part, composited
1486
+ on white in plan order, against the painting resampled into the rig.
1487
+
1488
+ | field | holds |
1489
+ | --- | --- |
1490
+ | `mean_abs` | mean over pixels of the mean-channel \|d\|, to 3 places |
1491
+ | `within_limit`, `within_share` | the share of pixels (to 4 places) whose mean-channel \|d\| is at most `within_limit` (8) |
1492
+ | `error_limit`, `error_px` | pixels whose max-channel \|d\| is above `error_limit` (40) |
1493
+ | `covered_alpha`, `uncovered_error_px` | of those, the ones where no part has alpha above `covered_alpha` (128) |
1494
+ | `hole_count` | the 8-connected components of the uncovered error pixels |
1495
+ | `holes_listed`, `holes` | the largest `holes_listed` (5) of them, largest first (ties: top row, then left column), each `{px, x, y, w, h, borders}`: its area, its box in rig pixels (y down), and every part with a covered pixel 8-adjacent to it, as `{part, px}` — how many such pixels it covers — most first, then plan order; `borders` is empty for a hole that touches no part |
1496
+
1497
+ The areas of every hole sum to `uncovered_error_px`; the reader refuses a block whose
1498
+ figures disagree (`PARTS_COUNTS_ADD_UP`), whose holes are not largest first
1499
+ (`PARTS_HOLES_LARGEST_FIRST`), whose box leaves the rig (`PARTS_BOX_INSIDE_RIG`) or
1500
+ whose border names no part of the file (`PARTS_HOLE_PART_KNOWN`).
1501
+
1502
+ `recomposite_error_rig.png`, beside `recomposite_rig.png` (`render/` under `assemble
1503
+ --out`, the top of `build --out`), is the same measurement as a picture at rig size:
1504
+ **red** (255, 0, 0) is an uncovered error pixel — painting that no part holds;
1505
+ **blue** (0, 0, 255) a covered error pixel — a part there, in a colour more than 40
1506
+ off; every other pixel is the painting in light grey (`192 + luma / 4`, so 192 to
1507
+ 255). The painting is dimmed rather than dropped so a hole reads against the figure it
1508
+ falls in at contact-sheet size, and grey so no painting pixel can be either flag
1509
+ colour. Opaque, and the same bytes on every run.
1510
+
1511
+ ## 6. Refusals: the rule, and what has to change
1512
+
1513
+ Every refusal is `FAIL RULE: object — detail`. Inside `build` it is printed under the
1514
+ stage's prefix (`[assemble] FAIL …`), and the build stops there.
1515
+
1516
+ ### Reading the inputs
1517
+
1518
+ | rule | means | change |
1519
+ | --- | --- | --- |
1520
+ | `LAYERS_INPUT_KIND` | the path is neither a directory with `layers.json`, a `layers.json`, nor a `.psd` | the `--full`/`--head` path |
1521
+ | `LAYERS_MANIFEST_PRESENT`, `LAYERS_MANIFEST_IS_JSON`, `LAYERS_FIELD_PRESENT`, `LAYERS_KEY_KNOWN` | the wrapper's `layers.json` is missing, unparsable, or not the wrapper's shape | the run's output directory; re-export it |
1522
+ | `LAYERS_PNG_PRESENT`, `LAYERS_PNG_UNAMBIGUOUS`, `LAYERS_PNG_DECODES`, `LAYERS_PNG_MATCHES_BBOX` | a layer's PNG is missing, found in two places, not a PNG, or not its box's size | the run's files |
1523
+ | `LAYERS_TAG_KNOWN`, `LAYERS_NAME_UNIQUE`, `LAYERS_BBOX_INSIDE_CANVAS` | a layer name that is no See-through v3 tag, a repeated name, a box off the canvas | the run (these are See-through's own outputs) |
1524
+ | `PSD_FILE_PRESENT`, `PSD_PARSES`, `PSD_IS_RGB8`, `PSD_HAS_LAYERS`, `PSD_LAYER_PLAIN`, `PSD_LAYER_HAS_PIXELS` | the `.psd` is missing, unreadable, not 8-bit RGB, has no layer, or has a layer that is hidden, not at full opacity, or without pixels | the `.psd` |
1525
+ | `ASSEMBLE_SOURCE_PRESENT`, `ASSEMBLE_SOURCE_DECODES` | the painting is missing or not a PNG | `--source` |
1526
+ | `SHEET_SOURCE_PRESENT`, `SHEET_PNG_PRESENT` | `sheet`'s painting, or a part PNG a `parts.json` lists, is missing | `--source`, `--layers` |
1527
+
1528
+ ### Plausibility (`WARN` in `layers`, a note in `assemble --propose-plan`)
1529
+
1530
+ See-through writes every tag, and on some seeds it paints a layer the character does
1531
+ not have: **[observed]** on one full-figure seed (issue #21), a `wings` layer of
1532
+ 490,296 px, mostly translucent grey that had absorbed the white background, on a
1533
+ character with no wings. Proposed as an ordinary part it became an 832 x 1096 part and
1534
+ the recomposite error rose to mean |d| 15.01. These are not refusals — the files are
1535
+ fine — so `layers` prints a `WARN` line and exits 0, and `--propose-plan` leaves the
1536
+ layer out of the plan, out of the head-run fallback and out of `extend_below_crop`.
1537
+
1538
+ `layers` prints three figures for every layer, each over its **opaque** pixels (alpha
1539
+ above 8, the table's `opaque_px`), on the layer as read:
1540
+
1541
+ | column | figure |
1542
+ | --- | --- |
1543
+ | `translucent` | the share of opaque pixels with alpha below 128 |
1544
+ | `background` | the share of opaque pixels whose own colour (straight alpha) has a min channel above 235 — the seam rule's "near-white", applied to the layer |
1545
+ | `area` | opaque pixels over the opaque pixels of the union of every **other** layer of the run ("the rest of the figure"); `-` when no other layer has any |
1546
+
1547
+ Not over alpha above 0: See-through hazes whole canvases at alpha 1..8 (the demo's
1548
+ full-run face is 99.1 % translucent that way and 4.2 % over its opaque pixels). Not
1549
+ over the whole union: a layer is part of it, so it could never be more than 1x.
1550
+
1551
+ | rule | crossed when | [observed] on the two examples' 116 layers (four runs) |
1552
+ | --- | --- | --- |
1553
+ | `PLAN_LAYER_TRANSLUCENT` | judged, and `translucent` above 50 % | the most translucent judged layer is 17.8 % (sample, full run, `front hair`) |
1554
+ | `PLAN_LAYER_BACKGROUND` | judged, `background` above 50 %, and `translucent` above 25 % | the most background-coloured judged layer is 21.9 % (demo, head run, `topwear`, a white dress); of judged layers above 10 % background, the most translucent is 5.6 % (demo, full run, `handwear-r`) |
1555
+ | `PLAN_LAYER_OVERSIZED` | `area` above 4x | the largest is 1.681x (demo, head run, `back hair`, which fills the crop) |
1556
+
1557
+ **Judged** means `area` at least 0.05x (or `-`). The two colour rules skip smaller
1558
+ layers on purpose: **[observed]** all 27 example layers of 150 px or more that are above
1559
+ 25 % translucent are small features — lashes, brows, mouths, noses, ears, irises, eye
1560
+ whites, an earring — antialiased strokes that are mostly rim, and the largest of them is
1561
+ 0.011x; 23 of the 116 layers are judged. The background rule needs translucency too,
1562
+ because an opaque white garment has the page's colour and is a real part.
1563
+
1564
+ No example layer crosses a rule, so their proposals are unchanged. For the haze the
1565
+ selftest plants (`PL03`, `PL06`: near-white, four pixels in five at alpha 40, 2.000x the
1566
+ rest of the figure), the `WARN` line and the note say the same thing:
1567
+
1568
+ ```
1569
+ WARN PLAN_LAYER_TRANSLUCENT: layer "wings" — 80.0% translucent, 100.0% background-coloured, 2.000x the rest of the figure; at most 50% translucent (alpha below 128) for a layer at least 0.05x the rest of the figure is required, so --propose-plan leaves it out
1570
+ "wings: full run layer 80.0% translucent, 100.0% background-coloured, 2.000x the rest of the figure -> not proposed by PLAN_LAYER_TRANSLUCENT (…), PLAN_LAYER_BACKGROUND (…)"
1571
+ ```
1572
+
1573
+ A full-run hair layer the head-run hair would have been extended from says `-> not
1574
+ extended below the crop by …` instead. **Change:** nothing, when the character does not
1575
+ have that part. When it does (a real translucent veil, a real white cape that the run
1576
+ drew faintly), the layer is See-through's best attempt at it: add the entry to
1577
+ `assemble.plan` by hand and read `assemble`'s `recomposite vs source` figures — or re-run
1578
+ See-through with another seed.
1579
+
1580
+ ### The config
1581
+
1582
+ | rule | means | change |
1583
+ | --- | --- | --- |
1584
+ | `CONFIG_FILE_PRESENT`, `CONFIG_IS_JSON` | no such file, or not JSON | `--config` |
1585
+ | `CONFIG_KEY_KNOWN`, `CONFIG_KEY_RETIRED`, `CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE` | an unknown key (a retired one says what replaces it), a missing one, a wrong type; a project's own record under a plain key, or an annotation (`note`, `<name>_note`) holding something other than a string | the named field; a record goes under a key beginning `x-` (any JSON, read by nothing), a remark under `note` or `<name>_note` (a string) — §3 |
1586
+ | `CONFIG_HEAD_BOX_SQUARE` | `seethrough.head_box` is not a non-empty square | `head_box` — take `propose --head-box`'s |
1587
+ | `CONFIG_TAG_KNOWN`, `CONFIG_PART_NAME`, `CONFIG_PART_UNIQUE` | a plan entry names no v3 tag, a part name that is not a file name, or a part or layer twice — a patch named like a plan part or another patch, and a cut's `into` named like a plan part, a patch or another cut's, included | `assemble.plan`, `assemble.patches`, `assemble.cuts` |
1588
+ | `CONFIG_NAME_RESOLVES` | a parent, segment, region, track, blink member, blink still part or bone, extend part, patch or cut `draw.before`, or cut `from` names nothing declared above it (a cut's `from` must be a plan part); or a constraint's `bones`, `target`, `source` or `bone` names a bone `bones` does not declare (the detail names the constraint and lists the bones that exist) | the named reference, or the declaration it needs |
1589
+ | `CONFIG_BONE_UNIQUE` | a bone or chain declared twice, or `root` declared | `bones` |
1590
+ | `CONFIG_PART_ATTACHED` | a plan part or a cut's piece with neither or both of a mesh and a region; a patch with a mesh, or with no region | `meshes` / `regions` |
1591
+ | `CONFIG_RIBS_MODE` | `meshes.<part>.ribs` on a lattice (`grid`) mesh: its vertices are its grid's corners, which no row can move (issue #188) | put the part in `contour` or `auto`, or remove `ribs` |
1592
+ | `CONFIG_PATCH_BOX` | an `assemble.patches` box with `x1 <= x0` or `y1 <= y0` | that patch's `box` (`x1`, `y1` are exclusive) |
1593
+ | `CONFIG_AMPS_MATCH_CHAIN` | a chain track's `amps` is not one per link | that track's `amps` |
1594
+ | `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` |
1595
+ | `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 |
1596
+ | `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 |
1597
+ | `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 |
1598
+ | `CONFIG_BLINK_BROWS_PAIRED` | `motion.blink.brows` without `brow_drop`, or `brow_drop` without `brows` | state both or neither |
1599
+ | `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 |
1600
+ | `CONFIG_CONSTRAINT_TYPE_KNOWN` | a `constraints` entry's `type` is not one of rig-c's five: `ik`, `transform`, `path`, `physics`, `slider` (spelled as rigc spells them; `IK` is not `ik`) | that entry's `type` |
1601
+ | `CONFIG_CONSTRAINT_NAME_UNIQUE` | two `constraints` entries of one kind share a `name`; rigc finds a constraint by its kind and its name, so an ik and a transform may share one | rename one |
1602
+ | `CONFIG_CONSTRAINT_TARGET_DETACHED` | the bone an ik follows (`target`) or a transform reads (`source`) is one of the bones that constraint drives, or sits under one (the detail gives the line of parents) — driving them would move what they follow. This package's refusal: rig-c 2.15.0 gates such a rig green when nothing declares the parentage (measured on the selftest's rig fixture, issue #103) | a bone beside the chain; a scene target is a single bone parented to `root` (§3) |
1603
+
1604
+ ### inputs
1605
+
1606
+ | rule | means | change |
1607
+ | --- | --- | --- |
1608
+ | `INPUTS_SOURCE_PRESENT` | no such painting | `--source` |
1609
+ | `INPUTS_PAINTING_OPAQUE` | the painting has a pixel below alpha 255 | the painting |
1610
+ | `INPUTS_HEAD_BOX_INSIDE` | `seethrough.head_box` leaves the painting (the crop is not padded) | `head_box` — take `propose --head-box`'s |
1611
+
1612
+ ### The optional ComfyUI adapter (`comfy paint`, `comfy seethrough`)
1613
+
1614
+ | rule | means | change |
1615
+ | --- | --- | --- |
1616
+ | `COMFY_HOST_GIVEN`, `COMFY_REACHABLE`, `COMFY_REQUEST_OK` | no `--host`/`COMFY_HOST`, nothing answering there, or a non-200 answer | the host, or the box |
1617
+ | `COMFY_NODE_PRESENT`, `COMFY_INPUT_KNOWN`, `COMFY_INPUT_SET`, `COMFY_CHOICE_PRESENT` | the box's `/object_info` lacks a node class the graph uses, an input it sets, an input it must set, or a model file it names | install the node or model on the box, or the config value naming it |
1618
+ | `COMFY_QUEUE_EMPTY`, `COMFY_PROMPT_ACCEPTED`, `COMFY_HISTORY_WITHIN`, `COMFY_RUN_OK` | someone else's job kept the queue busy past `--wait`, the box rejected the graph, the job did not finish within `--timeout`, or it ended in error (quoted) | `--wait`/`--timeout`, or what the quoted error names |
1619
+ | `COMFY_HISTORY_OUTPUTS`, `COMFY_VIEW_PRESENT`, `COMFY_MANIFEST_NAMED`, `COMFY_MANIFEST_IS_THIS_RUN`, `COMFY_LAYER_NAME` | the job's outputs are missing, unreadable, belong to another run, or name a layer that is not a plain file name | the wrapper's version on the box; report it |
1620
+ | `COMFY_IMAGE_PRESENT`, `COMFY_OUT_EMPTY`, `COMFY_OUT_FREE` | no `--image`, an `--out` that already holds files, or a seed whose painting is already on disk | `--image`, `--out`, `--seed0` |
1621
+ | `CONFIG_FIELD_PRESENT` on `config.generation`, `COMFY_PAINTING_SIZE` | `comfy paint` was given a config with no `generation` block (the line names the fields it holds), or the painting that came back is not twice the latent | `config.generation` |
1622
+ | `CONFIG_POSE_CANVAS`, `CONFIG_POSE_KEYPOINTS`, `CONFIG_POSE_IN_CANVAS` | the pose file `generation.control.skeleton` names states another canvas than `generation.latent`, holds other than the 18 body-18 keypoints, or puts one off its canvas | the pose file (§3, the `generation.control.skeleton` row) |
1623
+
1624
+ ### assemble
1625
+
1626
+ | rule | means | change |
1627
+ | --- | --- | --- |
1628
+ | `ASSEMBLE_FIELD_PRESENT` | `seethrough.head_box` is missing | `propose --head-box` proposes it (§3) |
1629
+ | `ASSEMBLE_RIG_SIZE` | `rig_scale` makes an empty rig | `assemble.rig_scale` |
1630
+ | `ASSEMBLE_HEAD_BOX_INSIDE` | the head box is outside the painting | `head_box` — `propose --head-box` holds it inside |
1631
+ | `ASSEMBLE_RUN_CANVAS` | a run's canvas is not `resolution` square | `seethrough.resolution`, or the run |
1632
+ | `ASSEMBLE_PLAN_TAG_IN_RUN`, `ASSEMBLE_EXTEND_TAG_IN_RUN` | an entry takes a tag its run does not hold (the detail lists what it does hold) | that entry's run or tag |
1633
+ | `ASSEMBLE_PART_OPAQUE` | a part ended with no opaque pixel | drop the entry, or take the tag from the other run |
1634
+ | `ASSEMBLE_PATCH_BOX_INSIDE` | a patch's box reaches past the rig (the painting times `rig_scale`; the detail gives the size) | that patch's `box` — it is in rig pixels, not painting pixels |
1635
+ | `ASSEMBLE_PATCH_OPAQUE` | a patch takes no pixel: a `"silhouette"` patch whose box holds none of the figure | move the box onto the figure, or `"alpha": "box"` |
1636
+ | `ASSEMBLE_CUT_INSIDE` | a cut's polygon has a point past the rig (the detail gives the points and the size) | that cut's `polygon` — it is in rig pixels, not painting pixels |
1637
+ | `ASSEMBLE_CUT_OVERLAP` | two cuts of one part share a rig pixel (the detail gives the count and the first, `x,y`) | one of the two polygons: a pixel goes to one part |
1638
+ | `ASSEMBLE_CUT_PIXELS` | a cut takes none of its part's opaque pixels (alpha above 8), or a part's cuts leave it none | draw the polygon over the art; to move the whole layer, rename the plan part instead |
1639
+ | `ASSEMBLE_UNDER_PRESENT`, `ASSEMBLE_UNDER_DECODES` | a cut's `under` names no file (relative to the config's directory), or a file that is not a PNG; every cut's is named in one refusal | that cut's `under` |
1640
+ | `ASSEMBLE_UNDER_SIZE` | a cut's `under` image is not the size of its polygon's bounding box (the detail gives both, and the box's corner) | the image: `max x − min x` by `max y − min y` of the polygon, in rig pixels |
1641
+ | `ASSEMBLE_UNDER_EMPTY` | a cut's `under` image fills none of the pixels the cut removed: its alpha is below the alpha removed at every one (all transparent there, or another file) | the image or its path; to leave the whole hole, drop `under` |
1642
+ | `ASSEMBLE_UNDER_DRAW` | a cut with `under` draws its piece behind its part, which the fill would hide it under | that cut's `draw`: `"front"`, or `{"before": …}` the plan part after its `from` |
1643
+ | `ASSEMBLE_COUNTS_ADD_UP` | a part's visible and occluded counts do not add up to its opaque pixels, or a projected pixel is not visible, or a cut part's pieces do not add up to it — an assembler bug, not an input problem | report it with the part named; nothing was written |
1644
+
1645
+ ### propose
1646
+
1647
+ | rule | means | change |
1648
+ | --- | --- | --- |
1649
+ | `HEADBOX_FACE_PRESENT`, `HEADBOX_RUN_SQUARE`, `HEADBOX_CANVAS_SIZE`, `HEADBOX_FITS_CANVAS` | the full run has no face or is not square; `--canvas` is not a positive size; or the painting cannot hold the proposed box at its size — its shorter side is the bound (shrinking it would cut the head) | `--full`, `--canvas`, or the painting |
1650
+ | `PROPOSE_SOURCE_PRESENT`, `PROPOSE_PNG_PRESENT`, `PROPOSE_PNG_MATCHES_BOX` | the painting or a part PNG is missing, or a PNG is not its box | `--source`, `--parts` (re-run assemble) |
1651
+ | `PROPOSE_FACE_PRESENT` | no part comes from a `face` layer, and the face-less fallback cannot derive a face box either: it needs the head run's `neck` and at least one head-run `front hair`/`back hair` part whose top is above the neck's, and the message names the half that is missing. With both, `propose` does not refuse — it derives the box and says so in its first note (§5) | `assemble.plan`: take the face, or the head run's hair and neck |
1652
+ | `PROPOSE_ACCESSORY_BODY` | an accessory has nothing above its pendant rows to hang its bone on | that part's plan entry, or author its bones by hand |
1653
+ | `KEYPOINTS_FILE_PRESENT`, `KEYPOINTS_IS_JSON`, `KEYPOINTS_KEY_KNOWN`, `KEYPOINTS_FIELD_PRESENT`, `KEYPOINTS_FIELD_TYPE` | the `--keypoints` file is absent, not JSON, has a key this reader does not know, lacks one of `spec`, `space`, `width`, `height`, `source`, `people`, or holds a value of the wrong type (a size that is not a whole positive px count, an empty `source`, no people) | the keypoint file (§3, *A posed figure*) |
1654
+ | `KEYPOINTS_SPEC_KNOWN`, `KEYPOINTS_SPACE_STATED` | the file is not `spine-parts-keypoints/1`, or states a space other than painting-px, origin top-left, y down | convert the file to that space; it is not read as if it were in it |
1655
+ | `KEYPOINTS_JOINT_KNOWN`, `KEYPOINTS_JOINT_STATE`, `KEYPOINTS_POSITION_STATED`, `KEYPOINTS_POSITION_INSIDE` | a joint name outside body-18, a state outside observed / occluded / missing, an observed joint with no `at` or a missing one with one, or a position outside the `width`x`height` image | that joint |
1656
+ | `KEYPOINTS_PERSON_UNIQUE`, `KEYPOINTS_PERSON_CHOSEN` | two people share an id; or the file holds more than one person and no `--person` names one, or `--person` names none of them (the message lists the ids) | `--person <id>` |
1657
+ | `KEYPOINTS_IMAGE_SIZE` | the file's `width`x`height` is not the `--source` painting's | a file measured on this painting |
1658
+ | `PARTS_*` (`PARTS_FILE_PRESENT`, `PARTS_IS_JSON`, `PARTS_KEY_KNOWN`, `PARTS_FIELD_PRESENT`, `PARTS_FIELD_TYPE`, `PARTS_NAME_UNIQUE`, `PARTS_FROM_KNOWN`, `PARTS_BOX_INSIDE_RIG`, `PARTS_COUNTS_ADD_UP`, `PARTS_HOLES_LARGEST_FIRST`, `PARTS_HOLE_PART_KNOWN`) | the `parts.json` read is not assemble's contract (`PARTS_COUNTS_ADD_UP`: `visible_px + occluded_px` is not `opaque_px`, `visible_not_projected_px` is above `visible_px`, or the `recomposite` block's figures disagree — more uncovered than error pixels, a hole larger than its box, the wrong number of holes listed, listed areas that do not sum to `uncovered_error_px`; the last two codes are the block's order and its border names, §5) | re-run assemble; do not edit `parts.json` |
1659
+
1660
+ ### rig
1661
+
1662
+ | rule | means | change |
1663
+ | --- | --- | --- |
1664
+ | `RIG_PART_PRESENT`, `RIG_PART_ATTACHED` | a `meshes`/`regions` key that is not in `parts.json`, a part with neither, or a `painting:` part with a mesh | the config and the plan must name the same parts; a patch is a region |
1665
+ | `RIG_PNG_PRESENT`, `RIG_PNG_MATCHES_BOX`, `RIG_PART_HAS_ART` | a part's PNG is missing, the wrong size, or has no art pixel | re-run assemble |
1666
+ | `RIG_NAME_RESOLVES`, `RIG_SEGMENT_DEFINED`, `RIG_CHAIN_POINTS` | a segment names an undeclared bone, a bone with no `tip` and no next link, or a chain too short to make a link | that bone's `tip`, or write the segment out as `[bone, [x0,y0], [x1,y1]]` |
1667
+ | `RIG_LATTICE_ONE_LOOP` | the lattice over a part does not close into one outline even after the repair passes | that mesh's `grid` |
1668
+ | `RIG_CONTROL_NAME_FREE` | a keyed, mesh-weighted bone needs `<bone>_ctl` and that name is taken | rename the declared bone |
1669
+ | `RIG_KEY_FRAME_UNTURNED` | a `translate` key on a bone whose parent is a chain link turned along its chain, or a `scale`/`shear` key (a track, or the blink's `eyes`/`brows` group) on a turned link: the key would act along the link's axis, not the picture's; the detail names the track, the bone and the turn | key the chain's own parent, or key `rotate`; a link whose turn is 0 (a level chain) keeps every key |
1670
+ | `RIG_BLINK_INSIDE_IDLE` | the blink runs outside the idle | `motion.blink.t` |
1671
+ | `RIG_BLINK_HOLD_SPANS_A_FRAME` | the tree's `BLINK.hold` is shorter than one frame at `IDLE_FPS`, so for some `motion.blink.t` no idle frame — and no frame of the loop — shows the closed eye; the detail counts the phases that miss and names the first | nothing in the config: `BLINK.hold` in `src/motion.ts`, at least `1/IDLE_FPS` s (§3) |
1672
+ | `RIG_STILL_ROW_INSIDE_PART`, `RIG_STILL_ROW_CLEAR`, `RIG_STILL_PIECES_HAVE_ART`, `RIG_STILL_NAME_FREE` | a `motion.blink.still` row that is not strictly inside the part, that crosses art (a cut through art changes the render even at rest), that leaves one piece with no art, or whose `<part>_still` slot name another part already has | that entry's `row` — a row with no art between the crease and the lash line — or rename the other part |
1673
+ | `RIG_RIGC_GREEN` | rig-c refused the rig; its own FAIL or compile-error line is quoted, and nothing was written. A `constraints` field rigc does not read, or a value it refuses, is this line in rigc's words (`constraint "aim" (ik) has a key this compiler does not read: "mixx" (did you mean "mix"?)`), as is `A25_DETACHED_BONE_PARENTAGE` on a declared scene target. So is an ik whose `bones` are more than two, or two of which the second is not the first's child: from rig-c 2.15.0 its rig-spec parser refuses both by name (`ik constraint "reach" names 3 bones …`, `ik constraint "reach": "hem1" is not a child of "body" ("hem0" stands between) …`; firejune/rigc#1205), and this package no longer refuses them itself (issue #103; it did from #92 while rigc's gate passed them). Under `--idle-keys ctl` a two-bone ik over a chain link and its child link is that second refusal too, because the child's `<link>_ctl` stands between them; rigc's remedy names the control, which a config cannot, so when rigc's line says that control alone stands between, the rig stage adds a sentence naming `--idle-keys direct` on the command that ran | the field rigc's line names; for the split pair, `rig --idle-keys direct` or `build --idle-keys direct`, as the line says, which keys the link in place — rig-c's own AUTHORING §5 maps each of its assertions (`node_modules/rig-c/docs/AUTHORING.md`) |
1674
+
1675
+ ### check and build
1676
+
1677
+ | rule | means | change |
1678
+ | --- | --- | --- |
1679
+ | `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
1680
+ | `CHECK_INPUT_PRESENT` on `…/parts.json` | `--parts` named a directory without `parts.json` — most often `parts/` itself — or, without `--parts`, the rig directory has none while the directory above it does (build's layout, `<out>/rig` under `<out>/parts.json`). 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`. With neither (no `--parts`, and no `parts.json` beside `rig.json` or above it), the rig is measured without parts and nothing is refused (§7) | `--parts` — the directory above `parts/` |
1681
+ | `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). `motion.json` is required even with no animation in it, because `rigc build` takes `--motion` (a spec with `"animations": {}` builds green); `CHECK_IDLE_PRESENT` is an `idle` that is there with no positive `duration` — an absent `idle` is measured around (§7) | re-run rig (`build` does both) |
1682
+ | `CHECK_SOURCE_SIZE` | the painting `--source` names is not this rig's: no single `rig_scale` takes its width and height to the stage's (assemble makes the canvas `trunc(w × rig_scale)` x `trunc(h × rig_scale)`), or, with `parts.json` read, its `scale_rig_per_source` does not; or the stage is not whole pixels. Refused before anything is built | the painting the rig was made from — the stage's own size, or the painting `assemble` was given |
1683
+ | `CHECK_SOURCE_PNG` | `--source` names a file that does not read as a PNG | a PNG |
1684
+ | `CHECK_SOURCE_GRID` | the setup-pose still and its black-tinted twin (the coverage `SETUP_POSE_VS_SOURCE` reads) did not come back on one grid over one opaque background | a rigc problem; report it |
1685
+ | `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 rig-c ^2.20.4), or put a newer `rigc` first on `PATH` |
1686
+ | `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` |
1687
+ | `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
1688
+ | `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 |
1689
+ | `CHECK_LOOP_CLOSES` | frame 0 and the frame at `duration` differ (max and first pixel quoted); with a physics constraint, the frames of the rig with every physics constraint left out, and the object says so (§7, *loop*) | a track whose last key is not its first |
1690
+ | `CHECK_SEAM_WITHIN_BAR` | the setup pose does not reproduce the flat stack of parts | read the `seam pair` lines and `check/seam/` pictures (§7; magenta = a counted pixel, cyan = the box round them): the pair, its boxes and the pixels counted. Usually a region or mesh placed off its part; compare with `recomposite_rig.png`. A pair of one layer cut in two with `differing_copies_px` is the cut line: move the polygon's edge, not `overlap` |
1691
+ | `CHECK_BREATH_VISIBLE` | the torso (`topwear`), rendered alone, barely moves over the idle — or the feet (`footwear`), rendered alone, move at all | the chest's breath tracks (`motion.tracks` on `chest`), or the torso mesh's `segments`; for the feet, the bone their region rides (`regions.<part>`, `root` in both examples) |
1692
+ | `CHECK_BLINK_NO_HOLE` | with the blink held shut, the eyewhite box shows the page where the open eye had art | the layer under the eye: the `face` part has no art there. Take the face from the other run, or add a part under the eye; `motion.blink.squash` only hides the hole less |
1693
+ | `CHECK_CHAIN_LAG` | a rotate track leads (or does not lag) the keyed bone above it, or a chain link swings less than the link above | that chain track's `phase`/`lag` (a positive `lag`, a child `phase` above its parent's) or its `amps` (non-decreasing toward the tip) |
1694
+ | `CHECK_TIP_OVER_ROOT` | a `handwear`/`bottomwear` part's art in the lower half of its box travels less than 1.4725 times as far as in the upper half, measured from the idle's posed geometry (rig px quoted) | the chain track's `amps` (grow toward the tip), or the mesh's `segments` (the chain must be among them), or its mode: on `scarf`'s short skirt the proposal's lattice reads 1.207 and the same part in `contour` mode 1.738 with the same segments and idle (issue #118) |
1695
+ | `CHECK_STILL_REGIONS_DARK` | a slot of the face's still set (the `face` slots and every slot drawn over the face that the head bone alone carries) moves relative to the bone the face's slot rides, or a `footwear` slot moves on the screen, by more than the arithmetic's own rounding — measured from the idle's posed geometry (the slot, its largest displacement in rig px, the idle frame and the tolerance quoted) | on the face: a face mesh with a weight on a bone other than the head (`segments`), or one deformed by another bone; on the feet: a mesh weighted to a swinging bone, or a region on the wrong bone. A bang swinging over the face is not this line's failure: its depth is reported (`crossing`), with no bar |
1696
+ | `CHECK_TEXTURE_STRETCH` | a mesh triangle's edge, in some idle frame, is more than 1.926544 times its rest length or less than 1/1.926544 of it (the mesh, triangle, its three vertices, the edge and the frame are named); or a rest edge has length 0 | the chain tracks' `amps` on the bones that mesh is weighted to (the stretch grows with them), or the mesh's `segments` — a vertex blending two bones that swing against each other |
1697
+ | `CHECK_SEAM_FRAME_SIZE`, `FRAMES_SIDECAR`, `CHECK_GEOMETRY_FILE` | rigc's render is not what its `frames.json` says, or its `geometry.json` is not a whole `rigc-geometry/1` export of the frames beside it (a frame count, a mesh whose vertex count differs from its rest entry; for the face half, another viewport than `frames.json`'s, other frame indices, the head bone or a feature's bone missing from a frame, a non-finite transform or time) | a rigc problem; report it |
1698
+ | `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 |
1699
+ | `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 (rig-c 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` |
1700
+ | `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` |
1701
+ | `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 rig-c ^2.20.4); otherwise a rigc problem, report it with `rigc --version` |
1702
+ | `BUILD_ARTIFACT_PRESENT` | the packed build lacks its `.json`, `.atlas` or page | a rigc problem; report it |
1703
+ | `REQUIREMENTS_FILE` | `--requirements` names no file, or one that does not parse as a JSON object. Refused before anything is built (by `build`, before assemble) | the path of a `spine-parts-requirements/1` file |
1704
+ | `REQUIREMENTS_FIELD` | a field of the file is missing, of the wrong type, or not one the format reads: `spec`, `fps` (no default), `requirements`, each requirement's `name` (letters, digits, underscores; once each), `kind`, `animation` and its kind's fields — every bar among them (`within_px`, `fraction`, `tolerance`, `least_drive` above 0, `within_degrees` from 0 to 180, `lo_degrees` ≤ `hi_degrees`, `within_ratio` ≥ 1) — and each target's `bone`, `animation` and exactly one of `at` and `keys` (times strictly increasing). `note`, `*_note` (strings) and `x-…` (any value) are read by nothing, as in the config | write the field; the bar is yours |
1705
+ | `REQUIREMENTS_RESOLVES` | a bone, an `ik` or `transform` constraint (by name and `constraint_type`, as rig-c resolves it), a slot, a mesh attachment or an animation the file names is not in `rig.json` or `motion.json`, or a seam's part is not in `parts.json` — the detail lists the ones that are; or an animation whose name cannot be a directory. Refused before anything is built | the name as the rig spells it |
1706
+ | `REQUIREMENTS_CONSTRAINT_DRIVES` | a `follow` names a bone its constraint does not constrain, or a property it does not drive: an `ik` drives `rotate` only; a `transform` drives `rotate` when a `to` names `rotate`, `translate` when one names `x` or `y` | the constraint's own bone and property |
1707
+ | `REQUIREMENTS_TARGET_PARENT` | a scene target's bone is the root or is not parented to it, or the root is not at rest at the origin, or the animation keys the root — a stage point is a bone's position only under a root that stands still at the origin | a bone under the root for the scene to place |
1708
+ | `REQUIREMENTS_TARGET` | a scene target places one bone twice for one animation, places it for an animation no requirement measures, has a key outside the animation's `[0, duration]`, or sits in a group the animation translates | one placement per bone and animation, inside it, for an animation a requirement reads |
1709
+ | `CHECK_REQUIREMENTS_FRAMES` | a requirement render did not write every frame rigc sampled, or the as-declared, released and full renders of a follow sampled different frames | a rigc problem; report it |
1710
+ | `CHECK_REQUIREMENT_MET` | a declared requirement measured outside its bar; the figure, its frame and the bar are quoted | the rig or its motion (§7, *Declared requirements*) — or the bar, if the scene asks less |
1711
+ | `CHECK_REQUIREMENT_MEASURABLE` | a declared requirement is NOT MEASURABLE: what it asks is undefined in the frames, and the reason is quoted. Not a pass | the declaration (a bone with a length, a target off the bone's origin, a least drive the constraint reaches) or the rig |
1712
+ | `REQUIREMENTS_SEAM` | a `seam` cannot be read against the rig: there is no `parts.json` (a merged rig, §7 *Measuring a rig rig-parts did not assemble*), or the two parts' art does not meet at the setup pose (no 4-adjacent pair). Refused before anything is built | two parts whose art touches, on a rig with its `parts.json` |
1713
+
1714
+ ### compose
1715
+
1716
+ Every refusal below is collected with the others and printed at once, under
1717
+ `[compose]`; nothing is written (`rig/`, `scene.json` and `check/` are cleared first
1718
+ and stay absent).
1719
+
1720
+ | rule | means | change |
1721
+ | --- | --- | --- |
1722
+ | `SCENE_FILE`, `SCENE_SPEC`, `SCENE_FIELD_PRESENT`, `SCENE_FIELD_TYPE`, `SCENE_KEY_KNOWN` | the scene file is missing, not a JSON object, not `spine-parts-scene/1`, or a field is missing, of the wrong type or not one the format reads (`canvas` two positive integers; `characters` at least one, each `id`, `build`, `offset` — an offset is never assumed (0, 0) — and an optional `scale`, a positive finite number: 0, a negative number or a non-number is refused at `characters[i].scale`; `order` a list of strings). `note`, `*_note` (strings) and `x-…` (any value) are read by nothing | the field |
1723
+ | `SCENE_ID_UNIQUE`, `SCENE_ID_FORM` | an `id` repeated, empty, or holding `:` — the separator of a prefixed name — or `/`, `\` or a leading `.`, because an id begins every image file of its character | another id |
1724
+ | `SCENE_PLATE_PROVENANCE`, `SCENE_PLATE_PRESENT`, `SCENE_PLATE_SIZE` | the plate's `provenance` is not `observed`, `generated` or `unknown`; its image is missing or not a PNG; it is not the canvas's size (it is drawn at canvas (0, 0) and never scaled) | the plate |
1725
+ | `SCENE_BUILD_PRESENT` | a build directory, or a file `build` writes in it (`parts.json`, `rig/rig.json`, `rig/motion.json` with an `idle`, an image its skin names, `check/check.json`), is missing | run `build` for that character |
1726
+ | `SCENE_BUILD_GREEN` | the build's own `check/check.json` does not say `PASS` and `gate_spine_html_green`; under an accept list (`--accept`, `characters[].accept`), it failed a bar the list does not name, its gate is red, a declared requirement is not PASS, or it names no failing bar — the bars not met and the ones accepted are listed | finish that character first: compose binds finished characters. To look at the scene meanwhile, name the failing bars (§7, *Composing several characters*) |
1727
+ | `SCENE_ACCEPT_KNOWN` | an `--accept` name, or one in a character's `accept`, is not a bar `check` holds a build to: a problem code (`CHECK_LOOP_CLOSES`) where the bar's name (`loop`) belongs, a misspelling, or `gate`, which is rig-c's and never accepted | one of the names the message lists |
1728
+ | `SCENE_BUILD_FIELD_KNOWN` | a build's `rig.json` or `motion.json` carries a key compose does not know how to prefix or place, or a root with a transform of its own | a rig `build` wrote; a rig written by something else is `check`'s to measure (§7, *Measuring a rig rig-parts did not assemble*), not compose's to merge |
1729
+ | `SCENE_RIG_SCALE_AGREES` | the characters' `parts.json` `scale_rig_per_source` differ (each named) | one `assemble.rig_scale` for every character; `characters[].scale` sizes a placed character, it does not make two `rig_scale`s one |
1730
+ | `SCENE_SCALE_EXACT` | a character at a `scale` other than 1 carries something no per-character value can scale: a physics constraint with a nonzero `wind` or `gravity` (Spine applies both through the skeleton's `referenceScale`, which the composed skeleton holds once for every character), a track whose `property` compose does not know to be a length or not, or a translate key whose `v` is neither a list of numbers nor a map of them | `scale` 1 for that character, or rebuild it at another `assemble.rig_scale` |
1731
+ | `SCENE_DURATION_AGREES` | the characters' idles are of different durations (each named) | one `motion.duration` |
1732
+ | `SCENE_CHARACTER_INSIDE_CANVAS` | a character's part boxes, placed at its offset, leave the canvas (the boxes and the canvas px quoted) | the offset, or the canvas |
1733
+ | `SCENE_ORDER_RESOLVES` | an order entry names no character, or `<id>:<slot>` names a slot that character does not have (its slots listed) | the entry |
1734
+ | `SCENE_ORDER_ONCE`, `SCENE_ORDER_COMPLETE` | a slot is drawn twice (a slot entry repeated, or an id listed twice), or never (the character's id is absent and some of its slots are named nowhere — they are listed) | name each slot once, or the id for the slots not named elsewhere |
1735
+ | `SCENE_NAME_PREFIXED`, `SCENE_NAME_RESOLVES` | a build's constraint or `invariants.detached` names a bone its own character does not declare — one another character does (`…_PREFIXED`, naming the prefixed names it could have meant) or none does (`…_RESOLVES`); or a `--requirements` name (bone, target bone, constraint, slot, attachment, seam part) is a bare name some character holds, which the composed rig holds only prefixed | a bone of the same character; in a requirements file, `<id>:<name>` |
1736
+ | `SCENE_ROOT_SHARED` | a character's idle keys `root` (a track, or a group holding it), or one of its constraints names `root`: the root is every character's, so either would move or reach them all | key or constrain a bone of the character's own |
1737
+ | `SCENE_INVARIANT_AGREES` | two characters declare `invariants.idleDrivesMeshes` with different `why`s | rebuild with this package's `--idle-keys direct`, whose `why` is one sentence |
1738
+ | `SCENE_IMAGE_NAME_FREE` | two images compose to one file name (`<id>.<file>`: an id `x` with an image `y.cloth.png`, and an id `x.y` with `cloth.png`) | another id |
1739
+ | `COMPOSE_RIGC_GREEN` | rig-c refused the composed rig; its line is quoted. A field compose carries as written (a constraint's `mix`, a slot's setup attachment) is rigc's to judge | the build whose field it names |
1740
+ | `REQUIREMENTS_*` | the `--requirements` file, as for `check` | as for `check` |
1741
+
1742
+ ## 7. The bars `check` enforces, and what only an eye answers today
1743
+
1744
+ `check.json`'s `PASS` is true exactly when all of these hold (the reference
1745
+ implementation's bars) — each one that measured; a rig with no `parts.json` or no
1746
+ `idle` skips the ones that read it, by name (*Measuring a rig rig-parts did not
1747
+ assemble*, at the end of this section):
1748
+
1749
+ - `gate_spine-html.txt`: every summary line `(N passed, 0 failed)` — the packed build
1750
+ under the `spine-html` profile, gated once over the compile and once over the packed
1751
+ pages on disk. There is no `spine` gate of its own: `spine-html` holds every rule
1752
+ `spine` measures (on the demo, rigc 1.5.1 and 2.0.3: `validate --profile spine`'s 14
1753
+ measured rules are all among the build's 23, the 15 others not in `spine`; rigc
1754
+ 2.1.3 and 2.10.1: the same 14 among the build's 24, which adds
1755
+ `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` on the packed pass, 16 not in `spine`; `CH09`
1756
+ holds it on every fetched example);
1757
+ - **seam**: the setup-pose render against the flat composite of `parts/`: mean
1758
+ max-channel |d| ≤ 1.0 of 255, and at most 50 **rig** pixels over 40 — the
1759
+ frame pixels over 40 divided by s², `s` the frame scale the `seam:` line
1760
+ prints and `check.json`'s `seam_frame_scale` holds (issue #216). The frame
1761
+ holds s² pixels per rig pixel, so a stack that parts from the render over a
1762
+ fixed area of the rig reads s² times as many frame pixels: on the public
1763
+ `sample` a part moved 2 rig px read 400, 891 and 1,560 frame px over 40 at
1764
+ s = 1.0, 1.5 and 2.0, and 400, 396 and 390 rig px; the sample as built read
1765
+ 3, 3 and 7 frame px [measured, issue #216]. Divided by s² the bar reads the
1766
+ same rig at every scale, and at s = 1 it is the reference's count. The mean
1767
+ is a share of the frame and needs no such division (0.242, 0.188 and 0.226 on
1768
+ the sample as built; 0.353, 0.305 and 0.340 with the part moved).
1769
+ `seam_px_over_40` and `seam_px_over_80` stay frame pixels, and so does each
1770
+ pair's count below. The bar reads one
1771
+ frame, the setup-pose still, never the idle. When it fails (or when `check
1772
+ --seam-pairs` asks, pass or fail), the pixels over 40 are split by the pair of
1773
+ parts that answers for each (issue #202): the two parts drawn last among those
1774
+ whose art (alpha above 0) lies in the 2x2 rig pixels the composite's bilinear
1775
+ sample reads for that frame pixel, lower first, `(no art)` standing in where
1776
+ fewer than two do. Each pair is a console line `seam pair <i> of <n>: "<a>" /
1777
+ "<b>" at the setup pose: <count> px over 40 (<count> over 80), frame box x
1778
+ <x0>..<x1>, y <y0>..<y1>, rig box …, picture seam/<i>_<a>_<b>.png` (in the
1779
+ picture, **magenta = a counted pixel**, cyan = the box round them), worst first,
1780
+ and a row of `check.json`'s `seam_pairs` (`parts`, `px_over_40`, `px_over_80`,
1781
+ `frame_box` in frame pixels, `rig_box` in the rig pixels of `parts.json` and
1782
+ `cuts[].polygon`, `picture`); `CHECK_SEAM_WITHIN_BAR`'s line quotes the worst.
1783
+ The picture, under `check/seam/`, is the setup-pose frame with that pair's
1784
+ counted pixels painted magenta (255, 0, 255) and a cyan box one pixel outside
1785
+ them. A passing run not asked writes neither the key nor the directory. A pair
1786
+ of one layer cut in two (both parts' `from` the same, `assemble.cuts`) carries
1787
+ `cut`: the layer and `differing_copies_px`, how many of its pixels read a pixel
1788
+ both parts hold in different colours. The band a cut declares is **not**
1789
+ excluded from the count, because the two copies are not one drawing: the piece
1790
+ takes the painting where it is seen and the base keeps the layer's colour under
1791
+ it. Along the cut line the render draws the upper part's resampled edge over
1792
+ the lower part's own resampled copy, where the flat stack is resampled once, so
1793
+ the two part where the copies differ and where the art changes colour across the
1794
+ line; only the band's outermost pixel and the one across the line are read, so
1795
+ the band's width does not enter. On the public `scarf` cut (piece drawn in front
1796
+ of `topwear`) the count read 26 px over 40 at `overlap` 1, 8 and 16 alike, the
1797
+ band 593, 4,886 and 9,508 px (the `cut:` line): 24 of the 26 are `"topwear" /
1798
+ "scarf"` along the cut line, 16 of them reading a band pixel where `topwear`
1799
+ holds the layer's colour and `scarf` the painting's, the other 8 where the cut
1800
+ line runs along the scarf's red edge [observed, issue #202]. A wider band moves
1801
+ neither; the polygon's edge does — run it through art of one colour on both
1802
+ sides, in both copies, which `rig/parts/<from>.png` and `rig/parts/<into>.png`
1803
+ show pixel for pixel. The composite meets the frame pixel centre to pixel
1804
+ centre: rig pixel index u lands on frame index s·u + t + 0.5·(s − 1), `s` the
1805
+ scale the `seam:` line prints (issue #209; the reference implementation's map
1806
+ lacks the last term, so the two agree only at s = 1). `check --keep-stills`
1807
+ leaves that frame — rigc's render directory, its `frames.json` (viewport,
1808
+ background) beside the PNG — under `check/setup_still/`;
1809
+ - **loop**: idle frame 0 against the frame at `t = duration`: max |d| exactly 0. The bar
1810
+ measures the keys (issue #183): a rig that declares a physics constraint — a `rig.json`
1811
+ `constraints` entry of `type` `"physics"`, or an entry of `motion.json`'s `physics`
1812
+ table — has the two frames read from the same rig and motion with every one left out
1813
+ (with a skin's `physics` list and any track keying one), built and rendered as the idle
1814
+ is, because rig-c's render resets physics at frame 0 and steps it, so the last frame
1815
+ differs from the first by what the simulation is still doing. The constraints left out
1816
+ are printed on one line of their own, `loop physics: … "<name>" on bone "<bone>"
1817
+ (rig.json|motion.json)`, and written as `check.json`'s `loop_physics` after
1818
+ `loop_max_diff`; they are reported, not judged. A rig with none is measured from the
1819
+ idle render itself, exactly as before, and writes no such line or key.
1820
+
1821
+ On the examples: `sample` 23/23, seam 0.207 with 0 pixels over 40, loop 0;
1822
+ `demo` (default rule) 23/23, seam 0.326 with 2 pixels over 40, loop 0.
1823
+
1824
+ A green gate cannot see a wrong animation, so `check` also writes six **judgement
1825
+ lines** (issue #11; `TEXTURE_STRETCH`, issue #31), each a key of `check.json` and a console line
1826
+ `NAME: PASS|FAIL|SKIP — <figures and bars>`, read the same way as the lines above: a
1827
+ FAIL makes `PASS` false and prints its own `FAIL CHECK_<NAME>` line (§6); a SKIP
1828
+ says why the rig gave the line nothing to read, and is neither a pass nor a failure —
1829
+ report it as not verified. Every region is chosen by the See-through tag in
1830
+ `parts.json`'s `from`, never by a part's name. Heat is a pixel's largest per-channel
1831
+ change from idle frame 0, in levels of 255, on the idle's 640-pixel grid.
1832
+
1833
+ Each bar follows one rule: a floor is half the weaker example's figure and a ceiling
1834
+ twice the worse one's, so the weaker example clears it by a factor of two; a bar the
1835
+ model itself fixes (a part on an unkeyed bone does not move, a lag is above 0, a hole
1836
+ is 0 pixels) is that value, not a margin — and where the model's value is an error
1837
+ the model fixes at "nothing moves" and the instrument reads exactly (the still regions,
1838
+ from geometry since issue #123), the bar is the arithmetic's own rounding, derived, not
1839
+ measured. A bar is set once, from the figures of the day it was set, and is not chased: the torso floor 3.809 is half of sample's 7.618, which reads 7.617 since the chain links are turned (issue #73), a factor of 1.9997. The figures are this port's, measured on
1840
+ the two examples [observed]:
1841
+
1842
+ | line | measures | bar | `demo` | `sample` | SKIP when |
1843
+ | --- | --- | --- | --- | --- | --- |
1844
+ | `BREATH_VISIBLE` | `topwear` parts rendered alone (`rigc render --slot`): heat mean over their box; `footwear` parts alone: heat max over theirs | torso mean ≥ 3.809; feet max ≤ 0 | 15.251; 0 | 7.617; 0 | no `topwear` or no `footwear` part |
1845
+ | `BLINK_NO_HOLE` | the setup pose with every `scaley` track on the eyewhite slots' bones held at its closed value, against the setup pose, at full size: pixels in the eyewhite box that show the page where the open eye had art | 0 px | 0 | 0 | no `eyewhite` part, or no `scaley` track on its bones goes below its first key |
1846
+ | ″ (reported) | the same box: each closed-eye pixel's max-channel distance to the nearest colour the open eye's box holds — max, and pixels over 40 | none | 15; 0 | 11; 0 | as above |
1847
+ | `CHAIN_LAG` | `motion.json`'s rotate tracks read as sines (DFT of the keys: period, amplitude, phase) and arranged by the bone tree — a keyed bone's parent is its nearest keyed ancestor | every lag ≥ 0.001 cycle; amplitude non-decreasing down each unbranched chain | lags 0.040 (neck to head) to 0.120; 12 chains | lags 0.040 to 0.100; 9 chains | no rotate track under another of the same period |
1848
+ | `TIP_OVER_ROOT` | each `handwear`/`bottomwear` part: how far the centroid of its art travels in the lower half of its box against the upper half — the art's AREA centroid inside each fixed half (cut at h/2), every art pixel (alpha above 8) carried through its attachment's posed triangles from the idle's `geometry.json` and clipped to the half exactly, rig px; `instrument: "geometry"` in the line (issue #118) | ratio ≥ 1.4725 | `bottomwear` 3.222, `sleeves` 4.721 | `bottomwear` 4.956, `sleeves` 11.830 | no such part |
1849
+ | `STILL_REGIONS_DARK` | from the idle's posed geometry (`geometry.json`), `instrument: "geometry"` in each half (issue #123). **Face:** the still set — the `face` slots, and every slot drawn after the first of them whose art overlaps a face part's at the setup pose, that the head bone alone carries (a region on it, or a mesh whose every weight is it) and that the idle does not key by slot (`deform`, `sequence`, a slot track: `driven_slots`). The head bone is the bone the `face` parts' slot rides in `rig.json`. Each still slot's art (alpha above 8), carried through its posed attachment and back through the head bone's setup-to-frame map on every idle frame, against its setup position: `max_rig_px` (and where, `at`, when it is a motion), `rms_rig_px`, per slot in `still_slots`. **Feet:** the `footwear` slots the same way in screen space — they ride the root, which the idle does not key, so the screen is their own frame. **Reported, no bar:** `crossing` — per slot drawn after the still set's first slot and not in it, how deep its art reaches into the region (`face` on top at the setup pose, clear of where `eyewhite`, `irides`, `eyelash`, `eyebrow` and `mouth` go in the head's frame; for the feet, `footwear` on top), `depth_rig_px`, and the most of it covered on one frame, `area_rig_px2` | each half's `max_rig_px` ≤ its `tolerance_rig_px`: nothing moves, up to the arithmetic's rounding, √2 × 2 × 35 × M² × ulp(C) (below) | face 4.202e-13 ≤ 2.286e-11; `hair_front` crossing 2.816 rig px deep, 89.395 rig px² (frame 47); feet 0 ≤ 2.814e-12 | face 4.418e-13 ≤ 2.286e-11 (`face`, `ear_l`, `ear_r`); `hair_front` 2.194 deep, 50.605 rig px² (frame 23); feet 0 ≤ 2.814e-12 | no `face` and no `footwear` part (one of the two absent leaves that half unmeasured; so do face parts on two slot bones, or idle frames with no `geometry.json`) |
1850
+ | `TEXTURE_STRETCH` | every mesh triangle's three edges in every idle frame, read from the `geometry.json` the idle render writes (`rigc render --geometry`, rig-c 1.4.0 or later): skinned length over rest length, the rest being the setup pose's bones with no deform. The figure is the rig's worst max(ratio, 1/ratio); each mesh's largest and smallest ratio, with triangle, vertices, edge and frame, is the detail | max(ratio, 1/ratio) ≤ 1.926544 | 1.388: `hair_back` triangle 449, edge 42-43 at 0.720 in frame 26 (largest stretch 1.291, frame 2) | 1.227: `sleeves` triangle 169, edge 126-51 at 0.815 in frame 28 (largest stretch 1.196, frame 5) | the rig draws no mesh, or the render wrote no `geometry.json` |
1851
+
1852
+ **`TIP_OVER_ROOT` is measured from geometry, not pixels (issue #118).** Until #118 the
1853
+ line rendered each part alone at `--max 640` and took, per half of its frame box, the
1854
+ centroid of every pixel that was not the background; its fields were `root_px` and
1855
+ `tip_px`, frame px. A half that travels under a frame pixel puts that centroid at the
1856
+ mercy of which edge pixels the grid flips: over `--max` 620–660 the pixel figure
1857
+ scattered around the exact one by up to 0.3 frame px whatever the travel [observed,
1858
+ 13 half-bands on the three examples and a variant], and every **root** half on every
1859
+ example travels 0.04–0.6 frame px, so the ratio's denominator was noise — sample's
1860
+ `bottomwear` read 4.187–6.849 over the sweep, demo's `sleeves` 2.960–6.503, and sample
1861
+ with its skirt amplitudes ×0.1 read 0.512–13.53, FAIL or PASS by the grid. The exact
1862
+ reading of the same definition is the same at every size (4.007 on that skirt, a PASS).
1863
+ The fields are now `root_rig_px` and `tip_rig_px`, rig px, beside `instrument:
1864
+ "geometry"`, so an old and a new `check.json` are not read as one instrument. The bar
1865
+ is the one set from the pixel instrument (half of demo's `bottomwear` 2.945, by the
1866
+ rule above, not chased); demo's exact 3.222 clears it 2.19 times. The figures the two
1867
+ instruments give the examples, and `scarf` (whose `bottomwear` is tracked in contour
1868
+ mode because its lattice fails the line — its README):
1869
+
1870
+ | part | pixel, `--max 640` (until #118) | geometry |
1871
+ | --- | --- | --- |
1872
+ | `demo` `bottomwear`, `sleeves` | 2.990, 3.287 | 3.222, 4.721 |
1873
+ | `sample` `bottomwear`, `sleeves` | 4.396, 12.475 | 4.956, 11.830 |
1874
+ | `scarf` `handwear_r`, `handwear_l` | 6.242, 5.558 | 6.683, 5.345 |
1875
+ | `scarf` `bottomwear`, lattice / contour | 4.537 / 2.661 | 1.207 / 1.738 |
1876
+
1877
+ The part rendered alone at up to `--max 8000` — the motion spanning pixels — converges
1878
+ on the geometric figure (`scarf`'s lattice `bottomwear` 1.337, 1.316, 1.285, 1.270 at
1879
+ 1200, 2400, 4800, 8000), with one gap not explained: `sample`'s `bottomwear` tip reads
1880
+ 2.342 rig px there against 2.596 from geometry, its root agreeing to 0.4 %.
1881
+
1882
+ **`STILL_REGIONS_DARK` is measured from geometry too, and its scope changed (issue #123).**
1883
+
1884
+ **What it read until #123.** The face half compared two pixel figures on the idle's
1885
+ 640-pixel grid:
1886
+ - the heat mean over the face outline carried into the head's frame;
1887
+ - twice the same mean taken off a calibration render, which moved the whole rig rigidly
1888
+ with the head.
1889
+
1890
+ Both were means of about one level, and both moved with the grid. Over `--max` 620–660
1891
+ on the three examples:
1892
+ - demo read 1.03–1.16 times its calibration;
1893
+ - sample read 0.95–1.07;
1894
+ - scarf read 0.99–1.38, with its head-frame mean between 1.350 and 1.869.
1895
+
1896
+ The feet half was a screen-space heat mean held to 3.244.
1897
+
1898
+ **What the old figures actually read.** Measured exactly from the geometry, they read
1899
+ nothing that moved.
1900
+
1901
+ In all three examples `hair_front` is drawn over the face and weighted to `head` and six
1902
+ bang bones. Its art swings into the face outline, relative to the head, on every idle
1903
+ frame:
1904
+
1905
+ | | `demo` | `sample` | `scarf` |
1906
+ | --- | --- | --- | --- |
1907
+ | deepest reach into the face | 2.816 rig px | 2.194 rig px | 2.479 rig px |
1908
+ | most face area covered on one frame | 89.395 rig px² | 50.605 rig px² | 31.908 rig px² |
1909
+
1910
+ The pixel region dropped a rim of `1 + 1/scale` rig px around every pixel that was not
1911
+ face. That rim measured 2.83–3.09 rig px over the sweep, which is wider than the bangs
1912
+ ever reach. Inside it, nothing but the face was on top, and the face moves exactly 0.
1913
+
1914
+ So:
1915
+ - the old wording, "a bang whose art crosses the face outline", passed the examples only
1916
+ because the region trimmed the band the bangs swing in;
1917
+ - the old figure was resampler noise divided by resampler noise.
1918
+
1919
+ An exact reading of that wording fails all three examples. Every allowed depth between 0
1920
+ and about 2.7 rig px would be a number chosen to keep them green.
1921
+
1922
+ **What it asks now.** The question is whether the face, and the parts over it that the
1923
+ head carries, keep their place relative to the head bone.
1924
+
1925
+ The still set is derived from the rig and the motion, never from a list:
1926
+ - the `face` slots;
1927
+ - every slot drawn after the first of them, overlapping a face part's art at the setup
1928
+ pose, carried by the head bone alone, and not keyed by slot in the idle.
1929
+
1930
+ A blink or a brow moves on a bone of its own, so it is not in the set; its motion is
1931
+ intended. A bang weighted to its chain is not in the set either. How deep it swings into
1932
+ the face is reported per slot as `crossing`, with no bar.
1933
+
1934
+ The feet half is the same reading for the `footwear` slots, in screen space.
1935
+
1936
+ **The bar is 0 in meaning.** The figure may exceed 0 only by the arithmetic's own
1937
+ rounding. That rounding is derived, not measured:
1938
+ - **The arithmetic is double precision.** rigc's core poser reads vertex coordinates and
1939
+ weights through `Math.fround`, computes in doubles, and `geometry.json` carries the
1940
+ doubles. The float32 inputs are the same in the rest vertex and the posed one, so they
1941
+ cancel.
1942
+ - **35 roundings stand on the longest path to one coordinate:**
1943
+ - 4 for the rest vertex and 4 for the posed vertex;
1944
+ - 11 for the head's setup-to-frame map;
1945
+ - 15 for carrying the vertex back through it;
1946
+ - 1 for the difference.
1947
+ - **Each rounding is bounded.** It is at most 2 M² ulp(C), where M is the largest matrix
1948
+ entry (at least 1) and C the largest coordinate.
1949
+ - **The tolerance follows:** √2 × 2 × 35 × M² × ulp(C). On the three examples it is
1950
+ 2.286e-11 rig px for the face and 1.407e-12 to 2.814e-12 for the feet.
1951
+
1952
+ The examples read 4.2e-13 to 4.4e-13 on the face and exactly 0 on the feet. The
1953
+ tolerance is a numerical one only; no visual allowance is in it.
1954
+
1955
+ **Which other lines see the eyes and the bangs.** `BLINK_NO_HOLE` checks one thing: with
1956
+ the blink held shut, no background shows inside the eyewhite box. It does not look at
1957
+ what covers the eyes. Nothing in `check` judges a bang over an eye or over the face; the
1958
+ reported `crossing` depth is the only instrument for it.
1959
+
1960
+ **Before and after, the same builds, only `IDLE_MAX_PX` changed** (scratch copies of the
1961
+ tree):
1962
+
1963
+ | `--max` | before: face mean / calibration (ratio), feet mean, demo · sample · scarf | after: face `max_rig_px`, feet `max_rig_px`, all three |
1964
+ | --- | --- | --- |
1965
+ | 620 | 1.283 / 1.196 (1.07), 1.504 · 0.933 / 0.900 (1.04), 0 · 1.350 / 1.360 (0.99), 0 | 4.202e-13, 0 · 4.418e-13, 0 · 4.224e-13, 0 |
1966
+ | 640 | 1.328 / 1.145 (1.16), 1.619 · 0.898 / 0.897 (1.00), 0 · 1.869 / 1.352 (1.38), 0 | the same |
1967
+ | 660 | 1.297 / 1.118 (1.16), 1.606 · 0.882 / 0.887 (0.99), 0 · 1.578 / 1.310 (1.20), 0 | the same |
1968
+
1969
+ The `STILL_REGIONS_DARK` line is byte-identical at 620, 630, 640, 650 and 660 on every
1970
+ example.
1971
+
1972
+ The selftest's planted cases (by hand):
1973
+
1974
+ | plant | before (every size) | after (every size) |
1975
+ | --- | --- | --- |
1976
+ | a foot whose root moves `fround(0.05)` rig px | PASS, heat 0.779–0.785 | FAIL, 0.05 rig px |
1977
+ | a face mesh weighted to a bone sliding `fround(0.05)` under a rolling head | PASS, 0.851–0.855 against 0.936–0.948 | FAIL, 0.05 rig px |
1978
+
1979
+ After them, one **reported line** with no bar:
1980
+
1981
+ | line | reports | bar | `demo` | `sample` | SKIP when |
1982
+ | --- | --- | --- | --- | --- | --- |
1983
+ | `RECOMPOSITE_HOLES` | `parts.json`'s `recomposite` block (§5), read rather than measured: `error_px`, `uncovered_error_px`, `hole_count`, and the largest hole's `px`, `box` (`x,y wxh`) and `borders` (`<part> <px> px`) | none — status `REPORTED`, never FAIL, and `PASS` does not read it | 1,564 px in 453 holes; largest 70 px at 351,155 6x22, `hair_back` 53 px, `hair_front` 12 px | 1,185 px in 259 holes; largest 94 px at 479,412 7x34, `sleeves` 42 px, `topwear` 40 px, `bottomwear` 6 px | `parts.json` has no `recomposite` block (the reference's files) |
1984
+
1985
+ It exists because no gate can see what it reports. The seam compares the setup pose
1986
+ with the flat stack **of the parts**, so a pixel of the painting that no part holds is
1987
+ missing from both sides and the seam passes over it — a white gap between two legs
1988
+ See-through made of one skirt built green with 7,671 uncovered pixels (issue #25). The line puts that class of defect into
1989
+ `check.json`, where an agent that reads only `check.json` sees it. It is not a bar for
1990
+ the reason the colour-patch half of `BLINK_NO_HOLE` is not one: no threshold is
1991
+ derivable from the examples — both hold hundreds of holes of a few dozen pixels along
1992
+ part edges, and whether a hole matters depends on where it is, which the box and the
1993
+ bordering parts say and a count cannot. Read it as a figure, quote it in a report, and
1994
+ look at `recomposite_error_rig.png` when the largest hole is more than a sliver.
1995
+
1996
+ Under `--source <painting.png>`, a second reported line, also never a FAIL
1997
+ (issue #77):
1998
+
1999
+ | line | reports | bar | `demo` | `sample` | SKIP when |
2000
+ | --- | --- | --- | --- | --- | --- |
2001
+ | `SETUP_POSE_VS_SOURCE` | the setup pose against the painting, by `assemble`'s own `measureRecomposite` over the stage's pixels — `mean_abs` and `within_share` (mean channel, within 8), `error_px` (max channel over 40), `uncovered_error_px` (of those, where the pose has alpha 128 or less), `hole_count` and the largest hole's box — with `painting_px`, `stage_px`, `render_px` and `render_scale` beside them. The painting is resampled onto the stage as assemble resamples it (lanczos3, alpha dropped). rigc renders onto an opaque grey, so the pose over white and its alpha are read off a second still of the same pose with every slot tinted black; both are carried onto the stage with the seam's map turned round (bilinear) | none — status `REPORTED`; assemble applies no bar to the figures it measures, so none is derived here | 3.146 / 93.43 % / 12,399 / 1,732, 518 holes, the largest 61 px at 351,157 6x20 (assemble's recomposite: 2.391 / 95.81 % / 11,050 / 1,564, 453) | 1.095 / 97.30 % / 5,348 / 1,239, 313 holes, the largest 88 px at 479,416 6x29 (assemble's: 0.835 / 97.95 % / 4,512 / 1,185, 259) | not written without `--source` |
2002
+
2003
+ The figures are assemble's by construction, not a second definition beside them;
2004
+ what differs is what they are taken of — the pose rigc draws, not the flat stack —
2005
+ and the gap between the two columns above is that drawing: the render's resampling at
2006
+ its scale (0.942 on the demo, 1.012 on the sample) and the warp back onto the stage,
2007
+ which soften every part edge by a pixel or two. It moves with the render's grid
2008
+ [observed, the demo's still rendered at four sizes: mean 2.663 at scale 1.0003, 3.091
2009
+ at 1.0073, 3.146 at 0.942, 3.186 at 1.884], so compare the line with itself across
2010
+ rigs or edits, and with `RECOMPOSITE_HOLES` for where a hole is, not digit for digit
2011
+ with the recomposite. On the examples the line reads the same with and without
2012
+ `parts.json` (the still's grid does not move).
2013
+
2014
+ What each figure is, and is not:
2015
+
2016
+ - **The seam is the answer to "at rest, no gap, white rim or doubled line between
2017
+ layers".** A gap shows the page, a rim a colour no part has there, a doubled line a
2018
+ part drawn off its place; each changes the setup-pose render against the flat stack,
2019
+ which is what the seam bar measures. No separate line is written for it.
2020
+ - **The blink is measured at the setup pose, not in an idle frame.** Since issue #32
2021
+ an idle frame does fall inside the closed window — on both examples the eyes are
2022
+ fully shut from 2.37 s to 2.454 s and `idle_frames_closed` is `[29]` (2.417 s) — but
2023
+ that frame also carries the idle's sway, so it is not a comparison of what the blink
2024
+ alone changed. rigc's `render` takes no time, so the closed pose is a throwaway
2025
+ animation holding the blink tracks' closed value, built and rendered beside the
2026
+ seam's still on the same grid. `idle_frames_closed` is what says the loop shows the
2027
+ closed eye; before #32 it was `[]` on both examples, with the 0.04 s hold between
2028
+ frames 28 and 29.
2029
+ - **The colour-patch figure is not reliable enough for a bar.** "Nearest colour in the
2030
+ open eye's box" counts a legitimate colour the open eye never showed (skin under the
2031
+ lid) as a patch, and a patch the open eye happened to contain as none. It is
2032
+ reported, and 15 and 11 on the examples are what a clean lid looks like.
2033
+ - **A lag is read modulo half a cycle.** A sine's sign is half a cycle of phase, and
2034
+ the keys cannot tell `amp −0.6, phase 0.2` from `amp 0.6, phase 0.7` — the mirrored
2035
+ chains of both examples are written the first way — so the reading folds a step into
2036
+ (−¼, ¼] of a cycle and reports amplitudes unsigned. A lag of a quarter cycle or more
2037
+ cannot be told from a lead. Tracks whose keys are not a sampled sine are listed as
2038
+ `unread`, and a keyed bone under a keyed ancestor of another period (the demo's
2039
+ tassel under the head) is listed in `other_period` and not compared.
2040
+ - **Tip over root is a centroid, not a displacement.** Art entering or leaving a half
2041
+ moves its centroid too. The halves' mean heat was the brief's first choice and was
2042
+ rejected: heat is texture times motion, and on the demo's sleeves the lower half's
2043
+ mean heat is 1.1 times the upper half's while its centroid travels 3.7 times as far.
2044
+ The halves split the box across its rows, which assumes the part hangs — true of a
2045
+ front-facing standing figure, the only input this tool takes.
2046
+ - **Stretch is read on edges, both ways, and the ceiling doubles |ln r|.** Compression
2047
+ distorts a texture as much as stretch — an edge at half its rest length squeezes
2048
+ the texels on it by the factor an edge at twice its length spreads them — so the
2049
+ figure is max(ratio, 1/ratio), and on both examples the worst triangle is a
2050
+ compression (demo 0.720, sample 0.815). The ceiling rule is applied to the quantity
2051
+ whose zero means "nothing happened", as it is for heat: for a ratio that is |ln r|,
2052
+ so twice demo's |ln 1.388| is ln(1.388²) = ln 1.926544 and sample clears it by 3.2
2053
+ times. Doubling the ratio itself (2.776) was rejected: its zero is not at 1, and it
2054
+ passes an edge drawn at 2.7 times its rest length. The rest is the art's own
2055
+ proportions — on both examples every rest edge equals its `uvs` edge times the
2056
+ attachment's size to within 2.4e-5, the uvs' six-decimal rounding.
2057
+ - **The face is measured in the head's frame (issue #33), from geometry (issue #123).**
2058
+ - **Until #33** the face half read the screen-space heat map. There the head's
2059
+ intended roll lit the outline on both examples, 16.988 and 11.475.
2060
+ - **#33 moved the reading into the head bone's frame.** Its ceiling became twice a
2061
+ calibration render's resampler error: the whole rig moved rigidly as the idle
2062
+ moves the head.
2063
+ - **That reading's region dropped a rim** of `1 + 1/scale` rig px around every
2064
+ pixel that was not face. Measured over the rim instead, both examples sat at 1.31
2065
+ and 1.34 times their calibration, and the excess was the bangs' art sliding over
2066
+ the rim.
2067
+ - **Issue #123 measured the same question exactly** and found the trimmed rim was
2068
+ the whole story: the bangs reach 2.2–2.8 rig px into the face outline. The rim
2069
+ was wider than that, so the pixel figure only ever read resampler noise
2070
+ (*`STILL_REGIONS_DARK` is measured from geometry too*, above).
2071
+ - **The line now asks what it can answer exactly:** whether the still set keeps its
2072
+ place relative to the head bone, up to the arithmetic's rounding.
2073
+ - **The line still does not judge a face that moves with the bone its own slot
2074
+ rides.** That is the head moving, whatever the bone. A face region on the wrong
2075
+ bone is for the feet half and the eye lines to catch.
2076
+ - **What the still set leaves out, and why.** The blink and the brows move features on
2077
+ bones of their own, by design, so those slots are not in the set. A slot the idle keys
2078
+ by slot (`deform`, `sequence`, a slot track) moves by design too, and is listed in
2079
+ `driven_slots` instead. A bang weighted to its chain moves over the face by design;
2080
+ its depth is reported per slot in `crossing`, which no bar reads.
2081
+ - **The feet are measured in screen space.** They ride the root, which the idle does not
2082
+ key, so the screen is their own frame.
2083
+ - Until #123 the feet half read the screen-space heat. On the demo the long skirt,
2084
+ drawn under the shoes, swings beside them and lit their rim to 1.622.
2085
+ - From the geometry, nothing drawn over the shoes enters them on any example, and
2086
+ the shoes move exactly 0.
2087
+ - A foot moved `fround(0.05)` rig px read a heat mean of 0.78 against the old
2088
+ ceiling of 3.244 at every grid, a PASS. It now fails by its 0.05.
2089
+
2090
+ Nothing is left that only an eye answers. "No visible texture stretch" left this list
2091
+ with issue #31 (`TEXTURE_STRETCH`), and "the face outline held still in the head's own
2092
+ frame" with issue #33 (the face half of `STILL_REGIONS_DARK`, above, read from geometry
2093
+ since issue #123). A bang over the face or the eyes is still an eye's to judge: `check`
2094
+ reports how deep it swings (`crossing`) and nothing more.
2095
+
2096
+ ### Measuring a rig rig-parts did not assemble
2097
+
2098
+ `check` takes a rig spec — `rig.json` and `motion.json`, the `rigc-rig/1` and
2099
+ `rigc-motion/1` pair `rigc build` compiles — not a compiled `skeleton.json`, because
2100
+ the gate is `rigc build`. A rig merged from several rig-parts outputs — by
2101
+ `compose` (below), or outside the package (prefixed names, one root, a plate region
2102
+ at slot 0) — is such a spec, with no
2103
+ `parts.json` beside it and possibly no `idle`. Each bar then measures exactly when what
2104
+ it reads is there, and says SKIP with the reason when it is not:
2105
+
2106
+ | bar or line | reads | SKIP when |
2107
+ | --- | --- | --- |
2108
+ | the gate | the rig spec | never — it always runs, on whatever the spec declares |
2109
+ | loop | the `idle` | no `idle` in `motion.json` (`check.json`: `loop_max_diff` null, `skipped.loop` the reason) |
2110
+ | seam | `parts.json` and `parts/` | no `parts.json` (`seam_*` null, `skipped.seam` the reason) |
2111
+ | `BREATH_VISIBLE`, `BLINK_NO_HOLE`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK` | `parts.json`'s tags and the idle | either missing, or as in the table above |
2112
+ | `CHAIN_LAG`, `TEXTURE_STRETCH` | the idle (and `rig.json`) | no `idle`, or as in the table above |
2113
+ | `RECOMPOSITE_HOLES` | `parts.json`'s `recomposite` block | no `parts.json`, or no block |
2114
+ | `SETUP_POSE_VS_SOURCE` | the rig spec and the painting | no `--source` |
2115
+
2116
+ `PASS` reads every bar that measured, and the last console line says how many did:
2117
+ `check: PASS; 1 of 9 bar(s) measured, 8 skipped (loop, seam, …)` is a rig whose gate is
2118
+ green and nothing else was looked at. Without an idle no idle is rendered
2119
+ (`idle_frames/`, `contact.png` and `motion_heat.png` are not written). The demo's own
2120
+ rig directory, copied away from its `parts.json`, reads `4 of 9`: the gate, the loop,
2121
+ `CHAIN_LAG` and `TEXTURE_STRETCH`.
2122
+
2123
+ ### Composing several characters (`compose`, issue #74)
2124
+
2125
+ `rig-parts compose --scene <scene.json> --out <dir> [--requirements <file.json>] [--accept <bar>[,<bar>…]]`
2126
+ binds characters this package already built into one rig. Each character runs the
2127
+ single-character path (§4, steps 0–9) on its own, already isolated input to a green
2128
+ `build` first; compose reads what that build wrote and changes nothing in it.
2129
+
2130
+ ```
2131
+ { "spec": "spine-parts-scene/1",
2132
+ "canvas": { "width": 1664, "height": 1216 },
2133
+ "plate": { "image": "plate.png", "provenance": "generated", "note": "…" },
2134
+ "characters": [
2135
+ { "id": "demo", "build": "out-demo", "offset": [0, 0] },
2136
+ { "id": "sample", "build": "out-sample", "offset": [832, 0] } ],
2137
+ "order": [ "demo", "sample" ] }
2138
+ ```
2139
+
2140
+ | field | what it is |
2141
+ | --- | --- |
2142
+ | `canvas` | the shared stage, canvas px, y down. The composed rig's stage is the canvas, `root` at its bottom centre, as a single character's is its own |
2143
+ | `plate` | optional. `image` (relative to the scene file) is a PNG of exactly the canvas's size; `provenance` is `observed` (the painting's own pixels), `generated` or `unknown` (a residual after the people were removed is `unknown` where they stood, and the author says so); `note` is free text. Both are copied into `scene.json` and **judged by nothing** |
2144
+ | `characters[].id` | the prefix: every bone, slot, attachment (skin entry), constraint (its name, and every bone and path slot it names), `invariants.detached` bone, track, group and easing of that character is renamed `<id>:<name>`. `root` is the one shared root. A name resolves to exactly one character, and nothing resolves across characters without its prefix |
2145
+ | `characters[].build` | a `build --out` directory (relative to the scene file) whose own `check/check.json` is green |
2146
+ | `characters[].offset` | `[x, y]`, a translation from the build's rig px to canvas px. Every character is built at one `assemble.rig_scale`, so its rig px are canvas px at scale 1 |
2147
+ | `characters[].scale` | optional, a positive number; absent is 1, and 1 composes to the bytes a scene without the key writes. The character's size on the canvas, about its stage's top-left corner: a rig px `p` lands at canvas px `scale * p + offset` (issue #197). A character scaled up past its art is drawn soft — the art is not resampled; rebuild it at a higher `assemble.rig_scale` if that matters |
2148
+ | `characters[].accept` | optional, a list of bar names: the bars this character's own check may have failed and still be composed (issue #203). `compose --accept <bar>[,<bar>…]` is the same list for every character; the two are joined, so the command line applies to every character and a character's list adds to it. Each name is one of the bars `check`'s summary names, less the gate — `loop`, `seam`, `BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`; anything else is `SCENE_ACCEPT_KNOWN`. A build is taken when every bar its `check.json` failed is named; its gate red, a declared requirement not PASS, or a bar not named is `SCENE_BUILD_GREEN` as before. **It is for looking, not for shipping:** the `[compose]` line says which failing bars were accepted, `scene.json` records them as the character's `accepted`, the composed rig's own check measures them again and reports them as it finds them, and the run exits 1 even when that check is PASS (a composed rig has no `parts.json`, so the seam and four judgement lines say SKIP there). A list that accepts nothing — every build green — composes to the bytes a scene without one writes |
2149
+ | `order` | the draw order, back to front: a character id (its slots not named elsewhere, in its own order) or one `<id>:<slot>`. Every slot of every character is drawn exactly once. Interleaving is written by naming slots: `["a", "b:arm_l", "b"]` draws `b`'s left arm between `a` and the rest of `b` |
2150
+
2151
+ What is composed:
2152
+
2153
+ - **Images are `<id>.<file>`, not `<id>:<file>`.** rig-c names an atlas region
2154
+ by its PNG's basename, and an atlas region line that holds `:` is read as a
2155
+ `key: value` line: measured through rig-c 2.15.0 on the demo renamed
2156
+ `demo:<name>` with images `demo:<file>`, `A07_ATLAS_TEXT_SHAPE` fails once per
2157
+ region; with the images under a `demo/` folder the rig gates green but the region
2158
+ keeps the bare basename, so two characters' `face` would collide. So the
2159
+ attachment is `demo:face` and its region `demo.face`, and the compiled skeleton
2160
+ carries Spine's `path` for each attachment where the two names differ.
2161
+ - **The offset is applied once,** to what the character places in `root`'s own
2162
+ frame: its first-level bones (the bones whose parent is `root`), and a region on a
2163
+ slot `root` carries or a mesh weight bound to `root` — both public examples'
2164
+ `shoes` ride `root`. Every other offset, weight bind and region in a rig spec is
2165
+ local to a bone the shift already moved. In Spine's axes the shift is the
2166
+ character's stage corner carried to its canvas place — `scene.json` records it per
2167
+ character, with what it moved. The root is shared, so a character whose idle keys
2168
+ it, or whose constraint names it, is refused (`SCENE_ROOT_SHARED`): it would move
2169
+ every character.
2170
+ - **The scale** (issue #197) is applied with the offset: every length the character
2171
+ carries is multiplied by it exactly where Spine's own loader multiplies by its
2172
+ `scale` (`SkeletonJson.js`, spine-core 4.3) — each bone's `x`, `y` and `length`;
2173
+ each region's `x`, `y`, and its drawn size through `scaleX`/`scaleY` (rig-c reads
2174
+ `width`/`height` off the PNG); each weight's bind `x`, `y`; an ik's `softness`; a
2175
+ transform's `x`, `y` and, in its `properties`, the `offset` of an `x`/`y` source and
2176
+ the `offset` and `max` of an `x`/`y` target, its `scale` by the target's unit over
2177
+ the source's; a path's `position` under `positionMode` Fixed and its `spacing` under
2178
+ `spacingMode` Length or Fixed; a physics `limit`; a slider driven by a bone's `x` or
2179
+ `y`, its `from` scaled and its `scale` divided; and the translate keys (`translate`,
2180
+ `translatex`, `translatey`), their curves' values with them. An absent field whose
2181
+ Spine default is not 0 (a physics `limit`, 5000; a transform target's `max`, 1; a
2182
+ slider's `scale`, 1) is written as that default scaled. Degrees, mixes, ratios,
2183
+ physics rates and a mesh's `width`/`height` (its image's, nonessential) are not
2184
+ lengths and stay. Then what sits in `root`'s own frame takes the shift, so the
2185
+ character's stage corner lands at `offset`. `scene.json` writes `scale` beside the
2186
+ offset only when it is not 1, and the `[compose]` line always prints it.
2187
+ - **The plate** is a region on the bone `plate` under `root`, the first slot, its
2188
+ image at canvas (0, 0).
2189
+ - **One `idle`** holds every character's tracks; the characters' idles must be of one
2190
+ duration. Constraints are carried in character order with their
2191
+ `invariants.detached`; `invariants.idleDrivesMeshes` once, when any character
2192
+ declares it.
2193
+ - **Every animation beside a character's idle** — what `motion.animations_from`
2194
+ wrote into its `motion.json` (§3) — rides along as `<id>:<name>` after the idle,
2195
+ its tracks' bones, groups and easings prefixed as the idle's are and every other
2196
+ value as written; the scene schedules none of them, they are the consumer's to
2197
+ play. Each is read with the idle's own key lists, so a key compose does not know
2198
+ how to prefix is `SCENE_BUILD_FIELD_KNOWN`, as it is on the idle, and a track of
2199
+ one that keys `root` is `SCENE_ROOT_SHARED` (issue #183).
2200
+ - **Gated as `rig` gates its own:** `rigc build --profile spine-html --pack`, the
2201
+ compile and the packed pages on disk, in a scratch directory; `--out` receives
2202
+ `rig/` (`rig.json`, `motion.json`, `images/`) and `scene.json` only when it is
2203
+ green. Then `check` runs over `rig/` into `check/`, as on any rig it did not
2204
+ build: without a `parts.json` the seam, `BREATH_VISIBLE`, `BLINK_NO_HOLE`,
2205
+ `TIP_OVER_ROOT`, `STILL_REGIONS_DARK` and `RECOMPOSITE_HOLES` say SKIP and why,
2206
+ and the gate, the loop, `CHAIN_LAG` and `TEXTURE_STRETCH` measure.
2207
+ `--requirements` is forwarded, and its names are `<id>:<name>`. Every bar and
2208
+ requirement measures the composed rig in canvas px, after each character's
2209
+ `scale`: a ratio (`TEXTURE_STRETCH`'s edge against its rest length) or an angle
2210
+ (`CHAIN_LAG`, a `range` in degrees) does not move with it, while a requirement's
2211
+ px figures and its px bar are canvas px — a character composed at 0.5 shows half
2212
+ the px its own `build` measured.
2213
+ - **`scene.json`** (`spine-parts-scene-report/1`): the scene file's path as given,
2214
+ the canvas, the rig's name (the ids joined by `+`), the pack flags, the plate (its
2215
+ image, provenance, note, bone, slot, size, `judged_by: "nothing"`), per character
2216
+ its build, offset, `scale` (only when it is not 1), `rig_size`, `rig_scale`, placed bounds (the union of its part
2217
+ boxes, canvas px), shift and what it moved (`shifted`: the first-level bones, the
2218
+ regions on `root`, the count of weights bound to `root`), its counts and `accepted`
2219
+ (the failing bars of its own check that an accept list took, only when there are any), and the
2220
+ order —
2221
+ as declared and as the slot list it expands to, `judged_by: "nothing"`.
2222
+
2223
+ A scene of one character at offset (0, 0) on a canvas of its own size, with no
2224
+ plate, composes to that character's own build renamed only: on both public
2225
+ examples `rig.json`, `motion.json`, every image, the atlas, the packed page and
2226
+ every idle frame are byte-identical once `<id>:` and `<id>.` are taken off, and
2227
+ `skeleton.json` and `skeleton.model.json` equal as values but for Spine's `path`
2228
+ and the model's `spine.sha256`, which on each side is the sha256 of its own
2229
+ `skeleton.json` and so changes with the names (selftest `SC33`).
2230
+
2231
+ **Not offered.** Reconciling two `rig_scale`s (one is required; `scale` sizes a
2232
+ character, it does not resample its art); a non-uniform scale, a flip or a rotation
2233
+ of a character; any order, overlap or occlusion inferred from an image — the order is the author's,
2234
+ and a per-pixel vote that holds for a still pose would not hold once the bodies
2235
+ move; person detection, segmentation, isolation or occlusion completion (each
2236
+ character's input arrives isolated); a composed `parts.json`, so the bars that
2237
+ read See-through tags are measured per character by its own `build`, not over the
2238
+ scene; a rig `build` did not write.
2239
+
2240
+ ### Declared requirements (`--requirements`)
2241
+
2242
+ The bars above are this package's. What a *scene* asks of the rig's motion — that
2243
+ a hand stays on a cup, that a sleeve takes half of what an arm's constraint asks of
2244
+ it, that a joint stays inside a range — is the scene's knowledge (issue #87), so it
2245
+ travels in a file `check` is given, `--requirements <file>` (`build` forwards it).
2246
+ Every bar in it is the author's; a missing one is refused by name. Nothing here
2247
+ solves, guesses a value or chooses a bar: every pose is rig-c's, read from the
2248
+ world transforms `rigc render --geometry` writes (`rigc-geometry/1`). Coordinates in
2249
+ the file are stage px, y down, as in the config; a stage pixel is one Spine world
2250
+ unit (the stage box maps to the world by a translation and the y flip,
2251
+ `src/coords.ts`); angles are degrees.
2252
+
2253
+ ```json
2254
+ {
2255
+ "spec": "spine-parts-requirements/1",
2256
+ "fps": 12,
2257
+ "targets": [{ "bone": "cup", "animation": "reach", "keys": [{ "t": 0, "at": [44, 32] }, { "t": 1, "at": [50, 32] }] }],
2258
+ "requirements": [
2259
+ { "name": "HAND_ON_CUP", "kind": "contact", "animation": "reach", "bone": "forearm", "point": "tip", "target": { "bone": "cup", "point": "origin" }, "within_px": 0.5 },
2260
+ { "name": "SLEEVE_HALF", "kind": "follow", "animation": "reach", "constraint": "sleeve_tf", "constraint_type": "transform", "bone": "sleeve", "property": "rotate", "fraction": 0.5, "tolerance": 0.05, "least_drive": 2 },
2261
+ { "name": "EYES_ON_CUP", "kind": "aim", "animation": "reach", "bone": "gaze", "target": { "bone": "cup", "point": "origin" }, "within_degrees": 3 },
2262
+ { "name": "ELBOW", "kind": "range", "animation": "reach", "bone": "forearm", "lo_degrees": -10, "hi_degrees": 150 },
2263
+ { "name": "SLEEVE_SKIN", "kind": "stretch", "animation": "reach", "slot": "sleeve", "attachment": "sleeve", "within_ratio": 1.3 },
2264
+ { "name": "CUFF_ON_HAND", "kind": "seam", "animation": "reach", "part": "sleeve", "neighbour": "hand", "within_px": 1 }
2265
+ ]
2266
+ }
2267
+ ```
2268
+
2269
+ **Four outcomes, never merged** (#87):
2270
+
2271
+ | outcome | when | what it does to the run |
2272
+ | --- | --- | --- |
2273
+ | not declared | the file states no requirement of that kind | the summary line names the kind; not a pass, not a skip of something asked. Without `--requirements` nothing is read, written or printed for it |
2274
+ | refused | the file cannot be read against the rig (§6, `REQUIREMENTS_*`) | refused by name before anything is built (exit 1) |
2275
+ | not measurable | it reads and the rig builds, and the quantity is undefined in the frames | `NAME: NOT MEASURABLE — <why>` with the figures that show it, `CHECK_REQUIREMENT_MEASURABLE`; the run does not PASS |
2276
+ | measured | otherwise | `PASS` or `FAIL` against the author's bar, with the figures and the worst frame; a FAIL is `CHECK_REQUIREMENT_MET` |
2277
+
2278
+ **The six kinds**, each over every frame of its animation sampled at the file's `fps`:
2279
+
2280
+ | kind | declares | measures | not measurable when |
2281
+ | --- | --- | --- | --- |
2282
+ | `contact` | `bone`, `point` (`origin` or `tip`), `target` (`{ "bone", "point" }` or `{ "stage": [x, y] }`), `within_px` | the largest distance between the two points, and its frame; a tip is the origin plus `length` along the bone's x axis | a tip on a bone of length 0, on either side |
2283
+ | `aim` | `bone`, `target`, `within_degrees` | the largest angle between the bone's axis and the line from its origin to the target; the distance is not judged | the bone has length 0; a frame whose target sits on the origin has no line and is not counted, and with no line on any frame the requirement is not measurable |
2284
+ | `range` | `bone`, `lo_degrees`, `hi_degrees` | the bone's world rotation less its parent's (the world's, for the root), less the same at the setup pose, as a signed shortest angle: the least and the greatest, and their frames | — |
2285
+ | `stretch` | `slot`, `attachment` (a mesh), `within_ratio` | `TEXTURE_STRETCH`'s own per-mesh measure (imported, not rewritten): the mesh's worst max(ratio, 1/ratio) over the frames | a rest edge of length 0; no frame shows the mesh |
2286
+ | `follow` | `constraint`, `constraint_type` (`ik` or `transform`), `bone`, `property` (`rotate` or `translate`), `fraction`, `tolerance`, `least_drive` | below | no frame's drive reaches `least_drive` (the line prints the largest drive there was) |
2287
+ | `seam` | `part`, `neighbour` (two part names: each is drawn by the slot of its name), `within_px` | below | no seam pair reads on any frame (each pixel off its attachment's rest geometry, or the slot shows nothing) |
2288
+
2289
+ **`follow`**, #87's definition to the letter: "bone A takes a fraction `f` of what
2290
+ constraint C asks of it" is measured from three poses of the same rig over the same
2291
+ frames, each posed by rig-c and differing in one thing only — *as declared*, the
2292
+ rig as written, `A(t)`; *released*, C's mix for that property forced to 0 and every
2293
+ key of that mix in the animation removed, `A0(t)`; *full*, forced to 1, keys removed,
2294
+ `A1(t)`. For `translate` A is the bone's world origin; for `rotate`, `atan2(c, a)` in
2295
+ degrees, every difference a signed shortest angle (the line notes a largest drive
2296
+ over 90°: a drive beyond 180° is not told from its complement). The drive is
2297
+ `d(t) = A1(t) − A0(t)`; a frame counts when `|d(t)| ≥ least_drive`; the measured
2298
+ fraction is `F = Σ⟨A(t) − A0(t), d(t)⟩ / Σ|d(t)|²` over the frames that count; PASS
2299
+ when `|F − fraction| ≤ tolerance`. The line also carries the frames that counted, the
2300
+ largest drive and its frame, and the largest residual `|A − A0 − F·d|` and its frame
2301
+ — figures with no bar. It measures the realised follow, not the number in `mix`: a
2302
+ keyed mix, a later constraint on the bone or physics on it show up in `A(t)` alone.
2303
+ "Keys removed" is written as every key of that mix holding the forced value (its
2304
+ curve channel's two value numbers with it), which poses what removing them under a
2305
+ setup mix of that value poses, while every other field those keys state stays as
2306
+ written. An ik's mix is `mix`; a transform's is `mixRotate` for `rotate`, and `mixX`
2307
+ and `mixY` for `translate`.
2308
+
2309
+ The released copy rests muted throughout whenever no other animation keys that mix,
2310
+ and rig-c's gate refuses that — `A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT`,
2311
+ `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT`. When rigc refuses the released copy
2312
+ for that constraint and for nothing else, the copy — never the rig under test —
2313
+ declares it in `invariants.consumerDrivenMix`, rigc's own door for a mix the consumer
2314
+ drives, and the line's `released_copy` says so; the full copy drops the constraint's
2315
+ own declaration if the rig has one, since at 1 it rests live. Any other red line from
2316
+ a copy is refused, `CHECK_RIGC_GREEN`, quoting rigc.
2317
+
2318
+ **`seam`** (issue #111): the seam bar (§7) compares the setup pose with the flat stack
2319
+ of parts, so it cannot see two parts part in motion. A `seam` requirement can. Its
2320
+ pairs are read off `parts.json`'s art at the setup pose: every pair (a, b) of
2321
+ 4-adjacent stage pixels with a an art pixel of `part` (alpha above 8, the threshold
2322
+ meshes are built from) and b an art pixel of `neighbour` that is not `part`'s. On
2323
+ each frame, each pixel centre's displacement from rest is read off its attachment in
2324
+ `geometry.json` — the rest triangle holding it and the same barycentric mix of that
2325
+ triangle's posed vertices (a region is the runtime's two triangles) — and a pair's
2326
+ opening is the distance between the two displacements: how far the two sides moved
2327
+ apart or across, in stage px. The line carries the largest opening, its frame and
2328
+ its pair (`"part" pixel x,y against "neighbour" pixel x,y`), the pair count, and the
2329
+ pair-frames not read: a pixel no rest triangle holds, or a slot showing nothing, is
2330
+ counted, never read as 0. It is an upper bound on any gap: what shows through
2331
+ depends on what is drawn under the seam. Two parts turning together by θ read
2332
+ 2 sin(θ/2) px across a pair one pixel apart (0.105 px at 6°), not 0. A part on a
2333
+ bone's sway opens its seams by that sway, so the bar is the scene's: what it
2334
+ declares is how far that pair may part.
2335
+
2336
+ **Scene targets.** A `targets` entry names a bone whose parent is the root and gives
2337
+ it a stage point (`"at": [x, y]`) or stage points at stated times (`"keys"`), linear
2338
+ between them, the first held before and the last after, for one named animation.
2339
+ `check` writes it onto a throwaway copy — the bone's setup position is the first
2340
+ point, the rest are translate keys from it, through the one y door, and the
2341
+ animation's own translation tracks on that bone are dropped — and poses the copy
2342
+ through rig-c; a follow's released and full copies carry the same placement. A
2343
+ root that is not at rest at the origin, or that the animation moves, is refused,
2344
+ because the stage point would not be the bone's position. In a rig composed of
2345
+ several characters, a target can simply be another character's bone, named as the
2346
+ composed rig names it.
2347
+
2348
+ **Where it is written**, under `--out`: `requirements/as-declared/<animation>/` (the
2349
+ render each requirement on that animation reads: the rig under test's own build, or a
2350
+ copy with its scene targets placed), and, per follow, `requirements/released/<name>/`
2351
+ and `requirements/full/<name>/` — each a `rigc render` frame set with its
2352
+ `geometry.json`. The copies' specs and builds are scratch, removed before the stage
2353
+ returns. `check.json` gains `requirements` — `fps`, the summary and each line by name
2354
+ — before `PASS`, and `PASS` needs every declared requirement to PASS.
2355
+
2356
+ The selftest holds each kind and outcome to a hand value on a generated rig
2357
+ (`fixtures/reqrig.ts`): a two-bone ik that reaches its scene target holds its contact
2358
+ and one 5 px short fails it at the frame it is short; an aim-only one-bone ik with a
2359
+ far target passes and is not judged on distance; a follow at mix 0.5 measures 0.5 on
2360
+ an ik and on a transform, and the same ik with its mix keyed 0, 1, 1 measures
2361
+ 3.5 / 5 = 0.7 and fails a declared 0.5; a follow whose target asks nothing and a tip
2362
+ on a bone of length 0 are NOT MEASURABLE and the run is not PASS (`CK68`–`CK79`,
2363
+ `RQ01`–`RQ22`). A seam between two parts on one rolling bone holds and reads only
2364
+ 2 sin(θ/2) px; the same back part sliding 3 px under its bone opens it by 3 at the
2365
+ frame the slide peaks; a seam on a rig with no `parts.json` is refused before anything
2366
+ is built (`CK83`–`CK85`, `RQ30`–`RQ35`).
2367
+
2368
+ ## 8. What one character costs
2369
+
2370
+ | step | sample | demo | source |
2371
+ | --- | --- | --- | --- |
2372
+ | painting (GPU) | 20.2 s | 18.3 s | `generation.json` `elapsed_s`, in [spine-parts-examples](https://github.com/firejune/spine-parts-examples) |
2373
+ | See-through full run (GPU) | 194.9 s | 198.5 s | `inputs/layers/full/meta.json` `elapsed_s` |
2374
+ | See-through head run (GPU) | 170.9 s | 171.4 s | `inputs/layers/head/meta.json` `elapsed_s` |
2375
+ | `build` (CPU, user + sys) | 14.6 s | 30.7 s | measured with `/usr/bin/time -p` on an Apple M4 machine under other load, rigc's child processes included; wall clock 12.7 s and 27.5 s |
2376
+
2377
+ About 6.5 minutes of GPU and under a minute of CPU per character, before any hand
2378
+ correction. The GPU figures are one run each on one machine; the painting's figure
2379
+ excludes choosing among seeds (the demo was chosen from eighteen candidates).
2380
+ Since issue #33 `check` runs one more `rigc build` and one more idle render (the face
2381
+ calibration, §7); the idle's `geometry.json` is the one issue #31 already writes. The `build` row
2382
+ was not re-measured: A/B against the tree before that change, alternating on one
2383
+ machine under a load average of 5 to 11, the difference in user + sys was +5.6 and
2384
+ +0.8 s on the demo and +6.5 and −1.6 s on the sample over two rounds — inside what
2385
+ the load moves the same build by (the pre-change demo build alone took 43.6 to
2386
+ 45.8 s there). Since issue #123 that build and render are gone again — the still regions read the
2387
+ idle's geometry — and the `build` row was not re-measured for it either.