@rig-c/core 2.70.0 → 2.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING.md CHANGED
@@ -273,7 +273,7 @@ What the flags mean:
273
273
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
274
274
  | `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
275
275
  | `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
276
- | `--profile` | `spine` = the 36 validity rules (**the default**) · `rig-play` = all 50, opt-in. `spine-html`, the profile's former name, is read as `rig-play` until the next minor release and prints a deprecation line on stderr naming `rig-play` |
276
+ | `--profile` | `spine` = the 36 validity rules (**the default**) · `rig-play` = all 50, opt-in. `spine-html`, the profile's former name, is read as `rig-play` until the next major release and prints `rigc: --profile spine-html is deprecated and reads as --profile rig-play, the same rules; it is accepted until the next major release — pass --profile rig-play` on stderr |
277
277
  | `--report` | `build` and `repack`: also write the gate's report as a JSON document (`build-report/1`) to this file — every gate's `PASS`, `SKIP` and `FAIL` rows, its summary figures and stats line, which supplier judged it, and every `pack:` line's figures. Written when the command writes `--out` and when a gate is red, before the exit; a file already at the path is removed first, so a compile error or a refused repack leaves none. Never inside `--out`, refused by name. The printed lines are byte-identical with it and without it — **§5.3** |
278
278
  | `--candidate` | `check`, `bench`, `render`, `inspect`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. A directory holding no `skeleton.json`, or a path with nothing at it, is refused `nothing at <path>`, exit 2, nothing written, by every command that takes a build — `validate <dir>` and a directory given to `diff` included (`RC27`). `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/3` or `/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. A directory whose files are not one build — a `skeleton.json` or a `skeleton.atlas` from another build beside this one's `skeleton.model.json` — is drawn through spine-core where the runtime can load the pair, the `poser` line naming the two digests or the atlas's first difference; where it cannot, it is refused `<skeleton> does not load against <the atlas>: spine-core draws this pair (<the poser line's why>) and could not resolve it — "<the runtime's own words>". <dir> is not one build: …`, exit 2, nothing written (`RC18`) — another build's skeleton beside this build's atlas typically ends `"Region not found in atlas: <region> (attachment: <name>)"`. Rebuild, or put the build's own three files back beside each other. A Spine export beside an atlas it was not exported with is refused in the same words, ending `The skeleton and the atlas are not one pair`. An atlas copied from another build's directory usually stops earlier, at its pages — their paths are relative to the directory it was written in — and is refused naming the first one: `nothing at <the page>: <the atlas> names it as a page, and spine-core draws this pair through that atlas (<why>). … <dir> is not one build: …`, exit 2, nothing written (`RC18`); an export whose atlas names a page that is not there is refused the same way, ending `and the page has to be there`. A build moved or copied whole away from the directory it was built in stops at the same place, on either poser, and is refused naming the page and the file that names it: `nothing at <the page>: <the skeleton.model.json> names it as a page, and the core draws this build from it. A page path is relative to the build's directory, so a build moved or copied away from the directory it was built in names pages that are not here — build it again where it is, or build it with --copy-images, which writes its pages beside it` (under `--poser spine`, `<the atlas> names it as a page, and spine-core draws this pair through that atlas (--poser spine)` with the same ending), exit 2, nothing written (`RC21`); only where the core refused the files does a page refusal say the directory is not one build. `bench --bones` and `rigc bonedist` — on its `--candidate` and on its `--reference` — refuse a pair the runtime cannot load in the words above with `spine-core poses this pair (bonedist's candidate: …)` or `(bonedist's reference: …)`, exit 2, nothing written (`RC20`); they read no page image, so a build whose pages are elsewhere is measured as it is in place. A `skeleton.json` that is not JSON is refused `rigc: cannot read <the file>: <the parser's words> (line N, column N)`, exit 2, by `render`, `check`, `bench` and `bonedist` as by every other command that reads one (`RC22`). With the atlas gone, `check` on a `rigc-compiled/3` build reads its pages' `scale:` lines off the document, so its texture note and `check.json`'s `textureFrom.candidateScales` are what they are with the atlas; on a `/2` build its texture note says whether the atlas declares a `scale:` line is not read (that document does not state one), and `candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
279
279
  | `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview` (each candidate's own first, with several), and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote` and a several-candidate `preview`, so is a name that only *some* candidates have, naming the one that lacks it |
@@ -2935,8 +2935,9 @@ rather than being checked afterwards.
2935
2935
 
2936
2936
  #### 3.4.5 `depth` — give every vertex its own z, instead of one cylinder radius
2937
2937
 
2938
- **On a `contour` or a `grid`.** A grid is the one it was built for: a turn
2939
- needs interior vertices to move, and a contour has none of its own.
2938
+ **On a `contour` or a `grid` generator, or directly on an authored mesh**
2939
+ (including named or raw weighted geometry). A grid supplies interior vertices;
2940
+ an authored mesh keeps its existing triangulation and weights.
2940
2941
 
2941
2942
  A `yaw` or `pitch` key (§4.12) turns a part by treating it as painted on a
2942
2943
  cylinder: a vertex `u` off the axis gets `z = √(radius² − u²)`. That is the right
@@ -2954,6 +2955,35 @@ off a greyscale sheet in the part's own pixel grid:
2954
2955
  }
2955
2956
  ```
2956
2957
 
2958
+ For authored geometry, put the same block beside `weights` or `vertices`:
2959
+
2960
+ ```json
2961
+ "depth": { "image": "strand_depth.png", "near": "white", "zScale": 12 }
2962
+ ```
2963
+
2964
+ The attachment must name an `image`. Each vertex's **setup world position**
2965
+ (the weighted sum of its setup bind positions) is located in the same window
2966
+ as the generators: the part is centred on the slot bone's setup world origin,
2967
+ x right and y up, while the sheet is in part pixels, x right and y down.
2968
+ UVs do not locate these samples. On a scaled atlas page the same drawing-sized
2969
+ sheet is read through the existing page-to-drawing conversion. A sheet whose
2970
+ size differs from the part is refused by `CE_COMPILE_GREY_SHEET_SIZE_MISMATCH`.
2971
+ A top-level `depth` beside a generator is refused; use that generator's depth
2972
+ field for generated geometry.
2973
+
2974
+ An authored `yaw` or `pitch` key with `depth: true` evaluates in **setup world**,
2975
+ including `about` in world units. Its driving-axis displacement is
2976
+ `(coordinate - about) * (cos(angle) - 1) - z * sin(angle)`; with
2977
+ `narrowing: false` only `-z * sin(angle)` remains. Each rounded world offset is
2978
+ written through every influencing bone's inverse setup linear transform.
2979
+ Subsequent bone animation and physics carry those bind offsets with the mesh:
2980
+ the displacement after posing is the weighted composition of the posed bones
2981
+ with their inverse setup transforms, rather than a fixed global translation.
2982
+ The reported turn ceiling measures the setup-world triangles on both axes;
2983
+ A39 still measures the actual posed artifact. Other transform kinds and meshes
2984
+ without this new input keep their existing evaluation spaces and emitted bytes.
2985
+ Naming a sheet without a depth turn changes no skeleton or atlas byte.
2986
+
2957
2987
  | Field | Meaning |
2958
2988
  | --- | --- |
2959
2989
  | `image` | **required.** The sheet, relative to the rig's `images` directory, and the **same pixel size as this attachment's `image`**. It is not packed into the atlas — it is a measurement rigc reads at compile time, not art anything draws. Under `--atlas-in` that is still the drawing's size, on a page that declares a `scale:` too: the sheet is read at each vertex's texel position over the scale the atlas states, so one sheet serves the loose parts and every pack of them |
@@ -5219,7 +5249,7 @@ is that a **depth** is the decision while a **residual** is not.
5219
5249
 
5220
5250
  | `kind` | Reads | `translatex` / `translatey` | `scalex` / `scaley` | Worked case |
5221
5251
  | --- | --- | --- | --- | --- |
5222
- | `yaw` | each member's setup `x` | `d = (x−about)·(cos t − 1) − (depth − carried)·sin t` — FACE §3 | `cos(α − t)/cos α`, `α = atan2(x−about, depth)` — FACE §5 | `gallery/portrait` |
5252
+ | `yaw` | each member's setup `x` | `d = (x−about)·(cos t − 1) − (depth − carried)·sin t` (or `−(depth − carried)·sin t` with `"narrowing": false`) — FACE §3 | `cos(α − t)/cos α`, `α = atan2(x−about, depth)` — FACE §5 | `gallery/portrait` |
5223
5253
  | `pitch` | each member's setup `y` | the same expression with `y` for `x` — a nod | the same | `gallery/nod` |
5224
5254
 
5225
5255
  ⭐ **The `property` says which half of the turn a key is.** A turn does two
@@ -5248,6 +5278,15 @@ is an **auditing** decision before it is a rigging one: a residual is 1–6 unit
5248
5278
  where a total is 30–40, and nobody can eyeball a wrong sign in the second.
5249
5279
  `about` (default 0) is where the axis crosses the driving coordinate, in the
5250
5280
  members' shared parent's space.
5281
+ `narrowing` (default `true`) says whether the displacement includes the
5282
+ foreshortening of the member's setup position `(x−about)·(cos t − 1)` (or `y`).
5283
+ With `"narrowing": false`, the displacement evaluates `d = −(depth − carried)·sin t`
5284
+ alone — for a rig whose plate proportions are held at their front-view width
5285
+ across the turn rather than narrowed toward the axis (§4.11.1). It belongs on the
5286
+ displacement projection (`translatex` / `translatey`) only; stating `narrowing` on
5287
+ a `scalex` or `scaley` track is refused by name (`CE_TRACK_NARROWING_PROJECTION`),
5288
+ because a foreshortening track *is* the narrowing — to turn without narrowing a
5289
+ plate's scale, omit that track.
5251
5290
 
5252
5291
  Six things the construct is bounded by, and each one is a refusal:
5253
5292
 
@@ -6194,7 +6233,7 @@ example needed it:
6194
6233
 
6195
6234
  | `kind` | Parameters | What it evaluates | Worked case |
6196
6235
  | --- | --- | --- | --- |
6197
- | `yaw` | `radius` **or** `depth`, `degrees`, `about` | `dx = (x−about)·(cos t − 1) − z·sin t`, with `z = √(radius² − (x−about)²)` from a cylinder or `z` read per vertex off a depth map — the 2.5D turn (FACE §1) | `gallery/portrait` |
6236
+ | `yaw` | `radius` **or** `depth`, `degrees`, `about`, `narrowing` | `dx = (x−about)·(cos t − 1) − z·sin t` (or `dx = −z·sin t` with `"narrowing": false`), with `z = √(radius² − (x−about)²)` from a cylinder or `z` read per vertex off a depth map — the 2.5D turn (FACE §1) | `gallery/portrait` |
6198
6237
  | `pitch` | the same | the same expression with `y` for `x` — a nod rather than a turn | `gallery/nod` |
6199
6238
  | `affine` | `scale`, `about` | `dx = (sx−1)·(x−ax)`, `dy = (sy−1)·(y−ay)` — a scale about a fixed point | `gallery/squash` |
6200
6239
  | `wave` | `amplitude`, `wavelength`, `phase`, `along`, `axis` | `d = amplitude · sin(2π·along/wavelength + phase)` | `gallery/nod` |
@@ -6204,8 +6243,28 @@ example needed it:
6204
6243
  displaces; they cannot be the same coordinate, because reading and displacing one
6205
6244
  axis is a stretch and `affine` states that. `power: 1` is an affine shear and
6206
6245
  `power: 2` is a cantilever — flat at `from`, so a part held at one end bends
6207
- instead of tilting. `about` defaults to 0 (or `[0, 0]`), `phase` to 0 and `power`
6208
- to 2; nothing else has a default.
6246
+ instead of tilting. `about` defaults to 0 (or `[0, 0]`), `narrowing` to `true`,
6247
+ `phase` to 0 and `power` to 2; nothing else has a default.
6248
+
6249
+ **`"narrowing": false` drops the setup-position foreshortening `(u−about)·(cos t − 1)` and keeps the depth shift `−z·sin t` alone.**
6250
+ A rigid 3D cylinder narrows across the screen as `cos t`, pulling every vertex
6251
+ inward by `(x−about)·(cos t − 1)` on top of its depth shift `−z·sin t`. On a
6252
+ cut-out rig whose facial plates are drawn to keep their front-view proportions
6253
+ across a turn — features translating by `−(depth − carried)·sin t` without
6254
+ foreshortening their setup `x` or `y` — that inward pull squashes the outer cheek
6255
+ while the features riding over it do not narrow to match. Setting `"narrowing": false`
6256
+ on `yaw` or `pitch` (both here and on `derive` in §4.5.1) states that choice
6257
+ in the model rather than forcing the author back to a raw `vertices` array.
6258
+ With the `cos t` term absent, a triangle's deformed twice-signed area is
6259
+ `2A(t) = 2A₀ − 2A_axis·sin t` instead of `2A₀·cos t − 2A_axis·sin t`, so it
6260
+ crosses zero at `sin t = A₀ / A_axis` (`asin(|A₀ / A_axis|)` when `|A₀ / A_axis| < 1`,
6261
+ and never on `(0, 90°)` when `|A₀ / A_axis| ≥ 1`) rather than `tan t = A₀ / A_axis`
6262
+ (`atan(|A₀ / A_axis|)`). When a deform key on a depth-mapped mesh declares
6263
+ `"narrowing": false`, `build` and `explain` recompute that axis's turn ceiling
6264
+ (§3.4.5) with `asin`, so the reported ceiling stays the exact angle `A39` fires at;
6265
+ mixing `"narrowing": false` and the default (`true`) on the same attachment and axis
6266
+ is refused (`CE_COMPILE_DEFORM_TURN_NARROWING_MIXED`), because one attachment and axis
6267
+ cannot have both the `asin` and `atan` ceilings at once.
6209
6268
 
6210
6269
  Six things this construct is bounded by, and each one is a refusal rather than a
6211
6270
  convention:
@@ -7301,7 +7360,7 @@ retain the minimal inputs and report that code rather than inventing author data
7301
7360
  | <a id="CE_COMPILE_DEPTH_IMAGE_MISSING"></a>`CE_COMPILE_DEPTH_IMAGE_MISSING` | “&lt;value&gt;: the depth block has no "image"; it is the sheet to sample, relative to the rig's images directory” | State the depth image relative to the rig images directory. |
7302
7361
  | <a id="CE_COMPILE_DEPTH_NEAR_INVALID"></a>`CE_COMPILE_DEPTH_NEAR_INVALID` | “&lt;value&gt;: the depth block says "near": &lt;value&gt;; it is "white" or "black". Both conventions are in use, and a sheet read with the wrong one turns the part inside out with every gate green.” | State near as white or black to declare the sheet convention. |
7303
7362
  | <a id="CE_COMPILE_DEPTH_TONE_SCALE_INVALID"></a>`CE_COMPILE_DEPTH_TONE_SCALE_INVALID` | “&lt;value&gt;: "zScale" is &lt;value&gt;; it is a finite number of world units”<br>“&lt;value&gt;: "zScale" is &lt;value&gt;; it is how many units the map's full range spans, so a positive number. To put the near end at the back, say "near": "black" — a negative scale states the same thing twice and the two can then disagree.”<br>“&lt;value&gt;: depth tone "&lt;value&gt;" is &lt;value&gt;; it is a finite number”<br>“&lt;value&gt;: depth tone "contrast" is &lt;value&gt;; a contrast at or below 0 collapses the range onto the midpoint (or turns it inside out), so the map would describe a flat part. It is a positive number, and 1 leaves the range alone.”<br>“&lt;value&gt;: depth tone "gamma" is &lt;value&gt;; a gamma at or below 0 maps every level to the same nearness, so the map would describe a flat part. It is a positive number, and 1 is the straight line.”<br>Here the values named in the message include: “bias”. | Use finite depth tone values, positive gamma/contrast and a positive finite zScale. |
7304
- | <a id="CE_COMPILE_DEPTH_COVERAGE_INCOMPLETE"></a>`CE_COMPILE_DEPTH_COVERAGE_INCOMPLETE` | “&lt;value&gt;: the depth map "&lt;value&gt;" does not cover &lt;value&gt; of the mesh's &lt;value&gt; vertices — the first is vertex &lt;value&gt; at (&lt;value&gt;, &lt;value&gt;)[ / in the page's texels, pixel (&lt;value&gt;, &lt;value&gt;) of the drawing-sized sheet]. &lt;value&gt; Sampling anyway would give those vertices the sheet's background depth and fold them away from the turn.”<br>“[A contour mesh puts every vertex ON the silhouette and pushes it out by the margin, so a sheet cut to the art stops short of every one of them. Dilate the sheet past the mesh margin, or lower the margin. / A grid spans the whole part window, corners included, while a sheet is usually cut to the art — so the lattice reaches past it wherever the art does not fill the window. Dilate the sheet to the window, or state "us"/"vs" that keep the lattice inside the art.]” | Supply a depth sheet covering every mesh vertex in the part's drawing-sized pixel grid. |
7363
+ | <a id="CE_COMPILE_DEPTH_COVERAGE_INCOMPLETE"></a>`CE_COMPILE_DEPTH_COVERAGE_INCOMPLETE` | “&lt;value&gt;: the depth map "&lt;value&gt;" does not cover &lt;value&gt; of the mesh's &lt;value&gt; vertices — the first is vertex &lt;value&gt; at (&lt;value&gt;, &lt;value&gt;)[ / in the page's texels, pixel (&lt;value&gt;, &lt;value&gt;) of the drawing-sized sheet]. &lt;value&gt; Sampling anyway would give those vertices the sheet's background depth and fold them away from the turn.”<br>“[A contour mesh puts every vertex ON the silhouette and pushes it out by the margin, so a sheet cut to the art stops short of every one of them. Dilate the sheet past the mesh margin, or lower the margin. / A grid spans the whole part window, corners included, while a sheet is usually cut to the art — so the lattice reaches past it wherever the art does not fill the window. Dilate the sheet to the window, or state "us"/"vs" that keep the lattice inside the art. / The authored setup vertices reach outside the sheet coverage. Extend the sheet to cover their bilinear taps, or correct the setup geometry.]” | Supply a depth sheet covering every mesh vertex in the part's drawing-sized pixel grid. |
7305
7364
  | <a id="CE_COMPILE_MESH_BONE_SETUP_TRANSFORM_MISSING"></a>`CE_COMPILE_MESH_BONE_SETUP_TRANSFORM_MISSING` | “&lt;value&gt;: bone "&lt;value&gt;" has no setup transform” | Correct the bone/setup hierarchy; report a missing compiled transform for a declared bone. |
7306
7365
  | <a id="CE_COMPILE_MESH_BONE_NOT_DECLARED"></a>`CE_COMPILE_MESH_BONE_NOT_DECLARED` | “&lt;value&gt;: bone "&lt;value&gt;" is not in the rig's bone list” | Use a bone name declared in the rig. |
7307
7366
  | <a id="CE_COMPILE_GREY_SHEET_IMAGE_MISSING"></a>`CE_COMPILE_GREY_SHEET_IMAGE_MISSING` | “&lt;value&gt;: the "&lt;value&gt;" block has no image to read” | State the image in the named depth/soft block. |
@@ -7438,7 +7497,7 @@ retain the minimal inputs and report that code rather than inventing author data
7438
7497
  | <a id="CE_COMPILE_SEQUENCE_KEY_INDEX_OUTSIDE_SERIES"></a>`CE_COMPILE_SEQUENCE_KEY_INDEX_OUTSIDE_SERIES` | “&lt;value&gt; (t=&lt;value&gt;): index &lt;value&gt; is past the end of a &lt;value&gt;-frame series (frames 0 to &lt;value&gt;). `Sequence.resolveIndex` clamps it to the last frame, so the key would start on a frame it does not name. `index` is 0-based.” | Use a zero-based index within the frame series. |
7439
7498
  | <a id="CE_COMPILE_DEFORM_TRANSFORM_AND_VERTICES"></a>`CE_COMPILE_DEFORM_TRANSFORM_AND_VERTICES` | “&lt;value&gt; (t=&lt;value&gt;): the key carries both a "transform" and a "vertices" run, and they are two answers to one question. Authored offsets and a stated model do not combine, for the same reason a mesh cannot carry both a "generator" and authored geometry — drop one.”<br>Also: “animation "&lt;value&gt;" deform &lt;value&gt;/&lt;value&gt;/&lt;value&gt;”<br>Here the values named in the message include: “default”. | Use a transform model or authored vertex offsets. |
7440
7499
  | <a id="CE_COMPILE_DEFORM_TRANSFORM_START_INDEX"></a>`CE_COMPILE_DEFORM_TRANSFORM_START_INDEX` | “&lt;value&gt; (t=&lt;value&gt;): the key states a "transform" and a start index. A transform is a model of the whole attachment and is evaluated over all &lt;value&gt; of its vertices, so it always starts at deform index 0. A model applied to part of a run leaves a step at the run's edge — write the partial run by hand if that is what you mean.”<br>Also: “animation "&lt;value&gt;" deform &lt;value&gt;/&lt;value&gt;/&lt;value&gt;”<br>Here the values named in the message include: “default”. | Apply a transform over the whole attachment without a start index, or author the partial run. |
7441
- | <a id="CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED"></a>`CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED` | “&lt;value&gt; (t=&lt;value&gt;): a "transform" states where every vertex of this attachment goes, and this one has a vertex the arithmetic cannot place — &lt;value&gt;. Fix the weights, or write the bind-space pairs yourself and start the run with "offset".”<br>“vertex &lt;value&gt;'s &lt;value&gt; weights sum to &lt;value&gt; rather than 1. A stated model becomes a deform run by writing each vertex's world displacement D into every influence as Mᵢ⁻¹·D, which moves the vertex by D·Σwᵢ — so a sum that is not 1 has no displacement that lands it where the model says. A20_MESH_WEIGHTS_COHERENT is the assertion that requires that sum, at the same 1e-3”<br>Also: “animation "&lt;value&gt;" deform &lt;value&gt;/&lt;value&gt;/&lt;value&gt;”<br>Here the values named in the message include: “default”. | Fix weights so setup vertices can be positioned, or author bind-space pairs using offset. |
7500
+ | <a id="CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED"></a>`CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED` | “&lt;value&gt; (t=&lt;value&gt;): a "transform" states where every vertex of this attachment goes, and this one has a vertex the arithmetic cannot place — &lt;value&gt;. Fix the weights, or write the bind-space pairs yourself and start the run with "offset".”<br>“vertex &lt;value&gt;'s &lt;value&gt; weights sum to &lt;value&gt; rather than 1. A stated model becomes a deform run by writing each vertex's world displacement D into every influence as Mᵢ⁻¹·D, which moves the vertex by D·Σwᵢ — so a sum that is not 1 has no displacement that lands it where the model says. A20_MESH_WEIGHTS_COHERENT is the assertion that requires that sum, at the same 1e-3”<br>Also: “animation "&lt;value&gt;" deform &lt;value&gt;/&lt;value&gt;/&lt;value&gt;”<br>Here the values named in the message include: “default”.<br>“&lt;value&gt;: depth needs resolved setup positions — &lt;value&gt;” | Fix weights so setup vertices can be positioned, or author bind-space pairs using offset. |
7442
7501
  | <a id="CE_COMPILE_DEFORM_TRANSFORM_OFFSET_COUNT"></a>`CE_COMPILE_DEFORM_TRANSFORM_OFFSET_COUNT` | “&lt;value&gt; (t=&lt;value&gt;): the transform produced &lt;value&gt; numbers for &lt;value&gt; vertices”<br>Also: “animation "&lt;value&gt;" deform &lt;value&gt;/&lt;value&gt;/&lt;value&gt;”<br>Here the values named in the message include: “default”. | Report the transform model and compiler defect: the produced offset count does not match the vertices. |
7443
7502
  | <a id="CE_COMPILE_DEFORM_TRANSFORM_RUN_LENGTH"></a>`CE_COMPILE_DEFORM_TRANSFORM_RUN_LENGTH` | “&lt;value&gt; (t=&lt;value&gt;): the transform expanded to &lt;value&gt; numbers and this attachment's deform array is &lt;value&gt; long”<br>Also: “animation "&lt;value&gt;" deform &lt;value&gt;/&lt;value&gt;/&lt;value&gt;”<br>Here the values named in the message include: “default”. | Report the transform model and compiler defect: the expanded offsets do not match the deform array. |
7444
7503
  | <a id="CE_COMPILE_DEFORM_START_SOURCES_CONFLICT"></a>`CE_COMPILE_DEFORM_START_SOURCES_CONFLICT` | “&lt;value&gt; (t=&lt;value&gt;): a key gives its start as "offset" (an index into the deform array) or as "fromVertex" (a vertex index rigc translates), never both”<br>Also: “animation "&lt;value&gt;" deform &lt;value&gt;/&lt;value&gt;/&lt;value&gt;”<br>Here the values named in the message include: “default”. | Use offset or fromVertex, rather than both. |
@@ -7456,6 +7515,7 @@ retain the minimal inputs and report that code rather than inventing author data
7456
7515
  | <a id="CE_COMPILE_DEFORM_FROM_VERTEX_INVALID"></a>`CE_COMPILE_DEFORM_FROM_VERTEX_INVALID` | “&lt;value&gt; (t=&lt;value&gt;): fromVertex is &lt;value&gt;; it is a vertex index, so a whole number ≥ 0” | State fromVertex as a whole vertex index at least zero. |
7457
7516
  | <a id="CE_COMPILE_DEFORM_FROM_VERTEX_OUTSIDE_ATTACHMENT"></a>`CE_COMPILE_DEFORM_FROM_VERTEX_OUTSIDE_ATTACHMENT` | “&lt;value&gt; (t=&lt;value&gt;): fromVertex &lt;value&gt; plus &lt;value&gt; vertex offset(s) runs to vertex &lt;value&gt;&lt;value&gt;, and the attachment has &lt;value&gt;” | Keep fromVertex plus the covered vertex count within the attachment. |
7458
7517
  | <a id="CE_COMPILE_DEFORM_WEIGHTED_FROM_VERTEX_AMBIGUOUS"></a>`CE_COMPILE_DEFORM_WEIGHTED_FROM_VERTEX_AMBIGUOUS` | “&lt;value&gt; (t=&lt;value&gt;): "fromVertex" counts VERTICES, and this attachment is weighted — its deform array holds one x, y pair per bone INFLUENCE, and vertex &lt;value&gt; has &lt;value&gt; of them. One offset per vertex is not a thing that array can hold: the world offset of a multi-bone vertex is the weighted sum of a per-bone offset in each bone's own bind space, so rigc will not guess one for you. Either key the control bone instead, or write the bind-space pairs yourself and start the run with "offset" (vertex &lt;value&gt; starts at deform index &lt;value&gt;).” | Key the control bone or author each bone influence's bind-space pair using offset. |
7518
+ | <a id="CE_COMPILE_DEFORM_TURN_NARROWING_MIXED"></a>`CE_COMPILE_DEFORM_TURN_NARROWING_MIXED` | “deform &lt;turnTriple&gt; keys "&lt;turnAxis&gt;" with narrowing=false in animation "&lt;turnFalseAnim&gt;" and with narrowing=true in animation "&lt;turnTrueAnim&gt;". An attachment's turn ceiling on one axis is either asin(\|A₀ / A_axis\|) (without narrowing) or atan(\|A₀ / A_axis\|) (with narrowing) — mixing both on one attachment and axis would report one ceiling where two apply. Use one "narrowing" setting across every "&lt;turnAxis&gt;" key on &lt;turnTriple&gt;.”<br>Also: “&lt;turnDeformSkin&gt;/&lt;turnTrack.slot&gt;/&lt;turnTrack.attachment&gt;”<br>Here the values named in the message include: “default”. | Use one `"narrowing"` setting (`true` or `false`) across every deform turn key on the same attachment and axis. |
7459
7519
  | <a id="CE_COMPILE_TRACK_TARGETS_MULTIPLE"></a>`CE_COMPILE_TRACK_TARGETS_MULTIPLE` | “animation "&lt;value&gt;": a track names more than one target (&lt;value&gt;); the list is &lt;value&gt; and a track names exactly one”<br>Also: “, ”<br>Here the values named in the message include: “/”, “bone”, “slot”, “group”. | Name exactly one target field on a track. |
7460
7520
  | <a id="CE_COMPILE_CONSTRAINT_TRACK_PROPERTY_UNKNOWN"></a>`CE_COMPILE_CONSTRAINT_TRACK_PROPERTY_UNKNOWN` | “animation "&lt;value&gt;": &lt;value&gt; has no timeline "&lt;value&gt;" (it has: &lt;value&gt;)”<br>Also: “, ”<br>“path constraint”<br>“physics constraint”<br>“{group "&lt;value&gt;" / &lt;value&gt; "&lt;value&gt;"}”<br>Here the values named in the message include: “slider”. | Use a timeline property supported by this constraint family. |
7461
7521
  | <a id="CE_COMPILE_ANIMATION_GROUP_UNKNOWN"></a>`CE_COMPILE_ANIMATION_GROUP_UNKNOWN` | “animation "&lt;value&gt;": unknown group "&lt;value&gt;"” | Declare the motion group or correct its name. |
@@ -7615,7 +7675,9 @@ retain the minimal inputs and report that code rather than inventing author data
7615
7675
  | <a id="CE_DEFORM_TRANSFORM_OBJECT"></a>`CE_DEFORM_TRANSFORM_OBJECT` | “&lt;where&gt;: "transform" is &lt;JSON.stringify(transform)&gt;; it is an object with a "kind"” | Write transform as an object with a kind. |
7616
7676
  | <a id="CE_DEFORM_TRANSFORM_KIND"></a>`CE_DEFORM_TRANSFORM_KIND` | “&lt;where&gt;: "transform" has kind &lt;JSON.stringify(kind)&gt;; this spec evaluates &lt;DEFORM_TRANSFORM_KINDS.join(', ')&gt;. A model that is none of those is still authorable as a "vertices" run.”<br>Here the values named in the message include: “yaw”, “bend”, “wave”, “pitch”, “affine”. | Choose a supported transform kind, or author the deformation as a vertices run. |
7617
7677
  | <a id="CE_DEFORM_DEPTH_RADIUS_CONFLICT"></a>`CE_DEFORM_DEPTH_RADIUS_CONFLICT` | “&lt;where&gt;: transform &lt;kind&gt; states both "depth": true and a radius &lt;JSON.stringify(t.radius)&gt;. They are two answers to how far forward each vertex sits — a cylinder derives it, a map states it — and a key carrying both leaves a reader unable to say which one the output came from. Drop one.” | Choose depth:true or a radius and remove the other. |
7618
- | <a id="CE_DEFORM_DEPTH_MAP_REQUIRED"></a>`CE_DEFORM_DEPTH_MAP_REQUIRED` | “&lt;where&gt;: transform &lt;kind&gt; says "depth": true and this attachment has no depth map. The map is named on the attachment's generator, as `"depth": { "image": …, "near": …, "zScale": … }`, because it describes the art rather than this key. Name it there, or give this key a radius.” | Declare the attachment generator depth map or give this transform a radius. |
7678
+ | <a id="CE_DEFORM_DEPTH_MAP_REQUIRED"></a>`CE_DEFORM_DEPTH_MAP_REQUIRED` | “&lt;where&gt;: transform &lt;kind&gt; says "depth": true and this attachment has no depth map. The map is named on an authored attachment or its contour/grid generator, as `"depth": { "image": …, "near": …, "zScale": … }`, because it describes the art rather than this key. Name it there, or give this key a radius.” | Declare the authored attachment or contour/grid generator depth map, or give this transform a radius. |
7679
+ | <a id="CE_COMPILE_DEPTH_AUTHORED_REQUIRED"></a>`CE_COMPILE_DEPTH_AUTHORED_REQUIRED` | “&lt;where&gt;: attachment "depth" describes authored geometry; a generated mesh states its depth on the contour or grid generator instead” | Put depth on the contour/grid generator or remove the generator and author the geometry. |
7680
+ | <a id="CE_COMPILE_DEPTH_IMAGE_REQUIRED"></a>`CE_COMPILE_DEPTH_IMAGE_REQUIRED` | “&lt;where&gt;: an authored mesh with "depth" needs an "image" to locate the part's pixel grid” | Name the part image the sheet describes. |
7619
7681
  | <a id="CE_DEFORM_DEPTH_SAMPLE_COUNT"></a>`CE_DEFORM_DEPTH_SAMPLE_COUNT` | “&lt;where&gt;: transform &lt;kind&gt; has &lt;depth.length&gt; sampled depths for &lt;count&gt; vertices” | Report this compiler defect with the inputs and printed sampled-depth and vertex counts; do not edit generated samples. |
7620
7682
  | <a id="CE_DEFORM_RADIUS_POSITIVE"></a>`CE_DEFORM_RADIUS_POSITIVE` | “&lt;where&gt;: transform &lt;kind&gt; has radius &lt;radius&gt;; it is the radius of the cylinder the part is painted on, so a positive number” | State a positive cylinder radius. |
7621
7683
  | <a id="CE_DEFORM_VERTEX_OUTSIDE_CYLINDER"></a>`CE_DEFORM_VERTEX_OUTSIDE_CYLINDER` | “&lt;where&gt;: transform &lt;kind&gt; has radius &lt;radius&gt;, and vertex &lt;v&gt; sits at &lt;kind === 'yaw' ? 'x' : 'y'&gt;=&lt;setup[2 * v + along]&gt;, which is &lt;round(Math.abs(u) - radius)&gt; past it (about=&lt;about&gt;). The cylinder has no surface there, so its depth would be 0 and the projection would be a different model at that vertex. Raise the radius to where the part actually sits, or move the vertex.” | Raise radius to include the measured vertex or move that vertex within the cylinder. |
@@ -7623,8 +7685,8 @@ retain the minimal inputs and report that code rather than inventing author data
7623
7685
  | <a id="CE_DEFORM_WAVELENGTH_ZERO"></a>`CE_DEFORM_WAVELENGTH_ZERO` | “&lt;where&gt;: transform wave has wavelength 0; one period cannot be zero long” | State a non-zero wavelength. |
7624
7686
  | <a id="CE_DEFORM_BEND_POWER_WHOLE"></a>`CE_DEFORM_BEND_POWER_WHOLE` | “&lt;where&gt;: transform bend has power &lt;power&gt;; it is a whole number ≥ 1 (1 is an affine shear, 2 a cantilever that is flat at "from"). A fractional power is not evaluated because it has no value on the side of "from" the part does not reach.” | Use a whole bend power of at least 1. |
7625
7687
  | <a id="CE_DEFORM_BEND_ENDS_EQUAL"></a>`CE_DEFORM_BEND_ENDS_EQUAL` | “&lt;where&gt;: transform bend has from &lt;from&gt; and to &lt;to&gt;; they are the two ends of the bend, so they differ” | Give bend from and to different values. |
7626
- | <a id="CE_DEFORM_TRANSFORM_QUANTISED_IDENTITY"></a>`CE_DEFORM_TRANSFORM_QUANTISED_IDENTITY` | “&lt;where&gt;: transform &lt;kind&gt; states &lt;stated&gt;, and every one of this attachment's &lt;count&gt; vertices evaluates to an offset of 0 — the largest value the closed form reached at any of them is &lt;band.toExponential(3)&gt;, which quantises to 0 on the 1e-6 grid a model is evaluated on. So the key states a deformation and emits the identity, and nothing downstream can tell it apart from a key that meant the setup pose. &lt;sampledTo&gt;. A key that MEANS the identity states it in its own parameters (&lt;identitySpelling&gt;) or carries no run at all.”<br>“A scale about a fixed point moves nothing that SITS on it, so every vertex this attachment has lies at about=[&lt;about[0]&gt;, &lt;about[1]&gt;] on the axis the scale changes — the geometry has collapsed onto the point the key holds still”<br>“amount 0”<br>“amount=&lt;amount&gt; from=&lt;from&gt; to=&lt;to&gt; power=&lt;power&gt; along=&lt;an&gt; axis=&lt;axis === 0 ? 'x' : 'y'&gt;”<br>“amplitude 0”<br>“amplitude=&lt;amplitude&gt; wavelength=&lt;wavelength&gt; phase=&lt;phase&gt; along=&lt;an&gt; axis=&lt;axis === 0 ? 'x' : 'y'&gt;”<br>“degrees 0”<br>“scale [1, 1]”<br>“scale=[&lt;scale[0]&gt;, &lt;scale[1]&gt;]&lt;a.about === undefined ? '' : ` about=[${about[0]}, ${about[1]}]`&gt;”<br>“{Every vertex sits at &lt;an&gt;=&lt;distinct[0]&gt;, so one value of the sinusoid covers the whole attachment and this phase is where that one value crosses zero. A wave needs the coordinate it reads to VARY across the geometry; a part that displaces as a whole is a bone, not a deform / The closest two distinct &lt;an&gt; coordinates in this attachment are &lt;round(gap)&gt; apart, and a sinusoid has to be sampled to exist: a wavelength of at least 4x that (&lt;round(4 * gap)&gt;) to read as a wave at all and 8x (&lt;round(8 * gap)&gt;) to read as a curve, where this key states &lt;round(wavelength / gap)&gt;x. At 2x every sample lands on the same pair of phases, and at the zero crossings that pair is (0, 0). `gallery/nod`'s README carries that rule and the measured table behind it}”<br>“{Every vertex sits at &lt;an&gt;=&lt;from&gt;, which is "from" — the end the bend is anchored at, where the displacement is 0 by construction. The span the key names does not cross the part it is keyed on / The furthest any vertex reaches into the span is u=&lt;round(reach)&gt; of 1, and u^&lt;power&gt; of that is &lt;(reach ** power).toExponential(3)&gt; — so the part occupies only the flat end of the curve. Move "to" to where the geometry actually ends, or lower the power}”<br>“{The turn is &lt;degrees&gt;°, and a projection of it can only vanish where every vertex shares one &lt;kind === 'yaw' ? 'x' : 'y'&gt; AND one depth — check the depth map's sampled range in the mesh report, which is 0 wide when the sheet is flat where this part sits / The turn is &lt;degrees&gt;°, and a projection of it can only vanish where every vertex shares one &lt;kind === 'yaw' ? 'x' : 'y'&gt; — check that this attachment's setup geometry is the shape the radius says it is}”<br>“{depth=true degrees=&lt;degrees&gt;&lt;t.about === undefined ? '' : ` about=${about}`&gt; / radius=&lt;radius&gt; degrees=&lt;degrees&gt;&lt;t.about === undefined ? '' : ` about=${about}`&gt;}”<br>Also: “axis=[&lt;value&gt;, &lt;value&gt;] from=&lt;value&gt; to=&lt;value&gt; shape=&lt;value&gt;”<br>“mask=&lt;value&gt;”<br>“&lt;value&gt; envelope(&lt;value&gt;)”<br>Here the values named in the message include: “linear”, “smooth”. | Use parameters with a measured non-zero result, explicitly state identity parameters, or omit the run. |
7627
- | <a id="CE_DEFORM_ENVELOPE_ZERO_WEIGHT"></a>`CE_DEFORM_ENVELOPE_ZERO_WEIGHT` | “&lt;where&gt;: transform &lt;kind&gt; states &lt;stated&gt; and moves &lt;plain.filter((d, i) =&gt; i % 2 === 0 &amp;&amp; (d !== 0 \|\| plain[i + 1] !== 0)).length&gt; of &lt;count&gt; vertices, and envelope &lt;weighed.stated&gt; weighs every one of them 0 — so the key states a deformation and emits the identity. &lt;weighed.reach&gt;”<br>“The mask "&lt;mask&gt;" weighs &lt;carried&gt; of &lt;count&gt; vertices above 0.”<br>“axis=[&lt;axis[0]&gt;, &lt;axis[1]&gt;] from=&lt;from&gt; to=&lt;to&gt; shape=&lt;shape&gt;”<br>“mask=&lt;mask&gt;”<br>“{The attachment has no vertices. / Along [&lt;axis[0]&gt;, &lt;axis[1]&gt;] this attachment's vertices sit at &lt;round(lo)&gt; to &lt;round(hi)&gt;, and the ramp rises from &lt;from&gt; (weight 0) to &lt;to&gt; (weight 1) — move "from" and "to" onto the part.}”<br>Also: “amplitude=&lt;value&gt; wavelength=&lt;value&gt; phase=&lt;value&gt; along=&lt;value&gt; axis=&lt;value&gt;”<br>“{depth=true degrees=&lt;value&gt;&lt;value&gt; / radius=&lt;value&gt; degrees=&lt;value&gt;&lt;value&gt;}”<br>“{ / about=[&lt;value&gt;, &lt;value&gt;]}”<br>“amount=&lt;value&gt; from=&lt;value&gt; to=&lt;value&gt; power=&lt;value&gt; along=&lt;value&gt; axis=&lt;value&gt;”<br>“&lt;value&gt; envelope(&lt;value&gt;)”<br>“scale=[&lt;value&gt;, &lt;value&gt;]&lt;value&gt;”<br>Here the values named in the message include: “linear”, “smooth”. | Place the envelope ramp or mask where it weights moved vertices above 0, or remove the ineffective key. |
7688
+ | <a id="CE_DEFORM_TRANSFORM_QUANTISED_IDENTITY"></a>`CE_DEFORM_TRANSFORM_QUANTISED_IDENTITY` | “&lt;where&gt;: transform &lt;kind&gt; states &lt;stated&gt;, and every one of this attachment's &lt;count&gt; vertices evaluates to an offset of 0 — the largest value the closed form reached at any of them is &lt;band.toExponential(3)&gt;, which quantises to 0 on the 1e-6 grid a model is evaluated on. So the key states a deformation and emits the identity, and nothing downstream can tell it apart from a key that meant the setup pose. &lt;sampledTo&gt;. A key that MEANS the identity states it in its own parameters (&lt;identitySpelling&gt;) or carries no run at all.”<br>“A scale about a fixed point moves nothing that SITS on it, so every vertex this attachment has lies at about=[&lt;about[0]&gt;, &lt;about[1]&gt;] on the axis the scale changes — the geometry has collapsed onto the point the key holds still”<br>“amount 0”<br>“amount=&lt;amount&gt; from=&lt;from&gt; to=&lt;to&gt; power=&lt;power&gt; along=&lt;an&gt; axis=&lt;axis === 0 ? 'x' : 'y'&gt;”<br>“amplitude 0”<br>“amplitude=&lt;amplitude&gt; wavelength=&lt;wavelength&gt; phase=&lt;phase&gt; along=&lt;an&gt; axis=&lt;axis === 0 ? 'x' : 'y'&gt;”<br>“degrees 0”<br>“scale [1, 1]”<br>“scale=[&lt;scale[0]&gt;, &lt;scale[1]&gt;]&lt;a.about === undefined ? '' : ` about=[${about[0]}, ${about[1]}]`&gt;”<br>“{Every vertex sits at &lt;an&gt;=&lt;distinct[0]&gt;, so one value of the sinusoid covers the whole attachment and this phase is where that one value crosses zero. A wave needs the coordinate it reads to VARY across the geometry; a part that displaces as a whole is a bone, not a deform / The closest two distinct &lt;an&gt; coordinates in this attachment are &lt;round(gap)&gt; apart, and a sinusoid has to be sampled to exist: a wavelength of at least 4x that (&lt;round(4 * gap)&gt;) to read as a wave at all and 8x (&lt;round(8 * gap)&gt;) to read as a curve, where this key states &lt;round(wavelength / gap)&gt;x. At 2x every sample lands on the same pair of phases, and at the zero crossings that pair is (0, 0). `gallery/nod`'s README carries that rule and the measured table behind it}”<br>“{Every vertex sits at &lt;an&gt;=&lt;from&gt;, which is "from" — the end the bend is anchored at, where the displacement is 0 by construction. The span the key names does not cross the part it is keyed on / The furthest any vertex reaches into the span is u=&lt;round(reach)&gt; of 1, and u^&lt;power&gt; of that is &lt;(reach ** power).toExponential(3)&gt; — so the part occupies only the flat end of the curve. Move "to" to where the geometry actually ends, or lower the power}”<br>“{The turn is &lt;degrees&gt;°, and a projection of it can only vanish where every vertex shares one &lt;kind === 'yaw' ? 'x' : 'y'&gt; AND one depth — check the depth map's sampled range in the mesh report, which is 0 wide when the sheet is flat where this part sits / The turn is &lt;degrees&gt;°, and a projection of it can only vanish where every vertex shares one &lt;kind === 'yaw' ? 'x' : 'y'&gt; — check that this attachment's setup geometry is the shape the radius says it is}”<br>“{depth=true degrees=&lt;degrees&gt;&lt;t.about === undefined ? '' : ` about=${about}`&gt;&lt;narrowingNote&gt; / radius=&lt;radius&gt; degrees=&lt;degrees&gt;&lt;t.about === undefined ? '' : ` about=${about}`&gt;&lt;narrowingNote&gt;}”<br>“{ / narrowing=false}”<br>Also: “axis=[&lt;value&gt;, &lt;value&gt;] from=&lt;value&gt; to=&lt;value&gt; shape=&lt;value&gt;”<br>“mask=&lt;value&gt;”<br>“&lt;value&gt; envelope(&lt;value&gt;)”<br>Here the values named in the message include: “linear”, “smooth”. | Use parameters with a measured non-zero result, explicitly state identity parameters, or omit the run. |
7689
+ | <a id="CE_DEFORM_ENVELOPE_ZERO_WEIGHT"></a>`CE_DEFORM_ENVELOPE_ZERO_WEIGHT` | “&lt;where&gt;: transform &lt;kind&gt; states &lt;stated&gt; and moves &lt;plain.filter((d, i) =&gt; i % 2 === 0 &amp;&amp; (d !== 0 \|\| plain[i + 1] !== 0)).length&gt; of &lt;count&gt; vertices, and envelope &lt;weighed.stated&gt; weighs every one of them 0 — so the key states a deformation and emits the identity. &lt;weighed.reach&gt;”<br>“The mask "&lt;mask&gt;" weighs &lt;carried&gt; of &lt;count&gt; vertices above 0.”<br>“axis=[&lt;axis[0]&gt;, &lt;axis[1]&gt;] from=&lt;from&gt; to=&lt;to&gt; shape=&lt;shape&gt;”<br>“mask=&lt;mask&gt;”<br>“{The attachment has no vertices. / Along [&lt;axis[0]&gt;, &lt;axis[1]&gt;] this attachment's vertices sit at &lt;round(lo)&gt; to &lt;round(hi)&gt;, and the ramp rises from &lt;from&gt; (weight 0) to &lt;to&gt; (weight 1) — move "from" and "to" onto the part.}”<br>Also: “amplitude=&lt;value&gt; wavelength=&lt;value&gt; phase=&lt;value&gt; along=&lt;value&gt; axis=&lt;value&gt;”<br>“{depth=true degrees=&lt;value&gt;&lt;value&gt;&lt;value&gt; / radius=&lt;value&gt; degrees=&lt;value&gt;&lt;value&gt;&lt;value&gt;}”<br>“{ / narrowing=false}”<br>“{ / about=[&lt;value&gt;, &lt;value&gt;]}”<br>“amount=&lt;value&gt; from=&lt;value&gt; to=&lt;value&gt; power=&lt;value&gt; along=&lt;value&gt; axis=&lt;value&gt;”<br>“&lt;value&gt; envelope(&lt;value&gt;)”<br>“scale=[&lt;value&gt;, &lt;value&gt;]&lt;value&gt;”<br>Here the values named in the message include: “linear”, “smooth”. | Place the envelope ramp or mask where it weights moved vertices above 0, or remove the ineffective key. |
7628
7690
  | <a id="CE_DEFORM_ENVELOPE_NO_EFFECT"></a>`CE_DEFORM_ENVELOPE_NO_EFFECT` | “&lt;where&gt;: transform &lt;kind&gt; states envelope &lt;weighed.stated&gt;, and it changes no emitted number: every vertex the model moves takes weight 1 (&lt;weighed.full&gt; of &lt;count&gt; vertices at 1, &lt;weighed.zero&gt; at 0), so the run is the un-enveloped one and the envelope is a field a reader would credit for an output it did not shape. &lt;weighed.reach&gt;”<br>“The mask "&lt;mask&gt;" weighs &lt;carried&gt; of &lt;count&gt; vertices above 0.”<br>“axis=[&lt;axis[0]&gt;, &lt;axis[1]&gt;] from=&lt;from&gt; to=&lt;to&gt; shape=&lt;shape&gt;”<br>“mask=&lt;mask&gt;”<br>“{The attachment has no vertices. / Along [&lt;axis[0]&gt;, &lt;axis[1]&gt;] this attachment's vertices sit at &lt;round(lo)&gt; to &lt;round(hi)&gt;, and the ramp rises from &lt;from&gt; (weight 0) to &lt;to&gt; (weight 1) — move "from" and "to" onto the part.}”<br>Here the values named in the message include: “linear”, “smooth”. | Remove the ineffective envelope, or place its transition over vertices the model moves. |
7629
7691
  | <a id="CE_DEFORM_ENVELOPE_OBJECT"></a>`CE_DEFORM_ENVELOPE_OBJECT` | “&lt;where&gt;: transform field "envelope" is &lt;JSON.stringify(envelope)&gt;; it is an object — a ramp `{ "axis", "from", "to", "shape" }` or a mask `{ "mask" }`” | Write envelope as a ramp object or a mask object. |
7630
7692
  | <a id="CE_DEFORM_ENVELOPE_MASK_RAMP_CONFLICT"></a>`CE_DEFORM_ENVELOPE_MASK_RAMP_CONFLICT` | “&lt;where&gt;: transform field "envelope" states "mask" &lt;JSON.stringify(e.mask)&gt; beside the ramp's &lt;rampStated.map((f) =&gt; `"${f}"`).join(', ')&gt;. They are two answers to one question — what weight each vertex takes — and a key carrying both leaves a reader unable to say which one the output came from. Drop one.”<br>Here the values named in the message include: “axis”, “shape”. | Choose envelope.mask or the ramp fields and remove the other form. |
@@ -7649,6 +7711,7 @@ retain the minimal inputs and report that code rather than inventing author data
7649
7711
  | <a id="CE_TRACK_DERIVE_OBJECT"></a>`CE_TRACK_DERIVE_OBJECT` | “&lt;where&gt;: "derive" is &lt;JSON.stringify(derive)&gt;; it is an object with a "kind"” | Write derive as an object with a kind. |
7650
7712
  | <a id="CE_TRACK_DERIVE_KIND"></a>`CE_TRACK_DERIVE_KIND` | “&lt;where&gt;: "derive" has kind &lt;JSON.stringify(kind)&gt;; this spec evaluates &lt;TRACK_DERIVE_KINDS.join(', ')&gt;. A model that is none of those is still authorable as a per-member "v" map, or as one track per member.”<br>Here the values named in the message include: “yaw”, “pitch”. | Use a supported derive kind, a per-member v map, or one authored track per member. |
7651
7713
  | <a id="CE_TRACK_CARRIED_PROJECTION"></a>`CE_TRACK_CARRIED_PROJECTION` | “&lt;where&gt;: derive &lt;kind&gt; states carried=&lt;JSON.stringify(turn.carried)&gt; on a "&lt;property&gt;" track, and the foreshortening reads no depth difference — it is cos(α − t)/cos α, where α is the member's own angle off the axis. "carried" belongs on the "&lt;projections.shift&gt;" track, whose shared shift is the thing it names.” | Put carried on the displacement projection rather than the foreshortening track. |
7714
+ | <a id="CE_TRACK_NARROWING_PROJECTION"></a>`CE_TRACK_NARROWING_PROJECTION` | “&lt;where&gt;: derive &lt;kind&gt; states narrowing=&lt;JSON.stringify(turn.narrowing)&gt; on a "&lt;property&gt;" track, and a "&lt;property&gt;" track IS the foreshortening — its whole output is cos(α − t)/cos α. "narrowing" belongs on the "&lt;projections.shift&gt;" track, where it drops (&lt;projections.coordinate&gt;−about)·(cos t − 1) from the displacement; to turn without narrowing the plate's scale, omit the "&lt;property&gt;" track.” | Put narrowing on the displacement projection rather than the foreshortening track, or omit the foreshortening track. |
7652
7715
  | <a id="CE_TRACK_FORESHORTEN_DEPTH"></a>`CE_TRACK_FORESHORTEN_DEPTH` | “&lt;where&gt;: derive &lt;kind&gt; projects onto "&lt;property&gt;" and member "&lt;member.name&gt;" states depth &lt;depth&gt;. Foreshortening is cos(α − t)/cos α with α = atan2(coordinate, depth), which needs the part to be IN FRONT of the axis; at or behind it there is no front surface to narrow. A part behind the axis still takes the "&lt;projections.shift&gt;" projection, where a negative depth is exactly what swings it the other way.” | State positive depth for the foreshortening projection; negative depth belongs to displacement. |
7653
7716
  | <a id="CE_TRACK_FORESHORTEN_EDGE_ON"></a>`CE_TRACK_FORESHORTEN_EDGE_ON` | “&lt;where&gt;: derive &lt;kind&gt; turns member "&lt;member.name&gt;" (&lt;projections.coordinate&gt;=&lt;u&gt;, depth=&lt;depth&gt;) past its own edge: α = &lt;round(alpha)&gt; rad and the turn is &lt;round(rad)&gt; rad, so cos(α − t) is &lt;round(Math.cos(alpha - rad))&gt; and the scale would be &lt;round(scale)&gt;. A scale at or below 0 mirrors the drawing rather than narrowing it. The part has turned edge-on — the turn is past what this construction carries, and FACE §8 is the page that picks a construction from the angle.” | Keep the turn within this construction's positive scale range or choose the construction described in FACE §8. |
7654
7717
  | <a id="CE_TRACK_PROPERTY_PROJECTION"></a>`CE_TRACK_PROPERTY_PROJECTION` | “&lt;where&gt;: derive &lt;kind&gt; has no projection onto "&lt;property&gt;". It projects onto "&lt;p.shift&gt;" (the displacement, FACE §3) and "&lt;p.foreshorten&gt;" (the narrowing, FACE §5), and onto nothing else — a turn about one axis moves nothing along it, so a paired "translate"/"scale" would need a second channel this model does not state. State the axis, or write the values as a per-member "v" map.” | Use the derive kind's printed displacement or foreshortening property, or a per-member v map. |
@@ -7718,7 +7781,7 @@ retain the minimal inputs and report that code rather than inventing author data
7718
7781
 
7719
7782
  | Code | Fixed message text and alternatives | What to change |
7720
7783
  | --- | --- | --- |
7721
- | <a id="CE_KEYS_UNKNOWN_FIELD"></a>`CE_KEYS_UNKNOWN_FIELD` | “&lt;where&gt;: &lt;what&gt; has &lt;strays.length === 1 ? 'a key' : `${strays.length} keys`&gt; this compiler does not read: &lt;named.join(', ')&gt;. Nothing reads such a key, so it would be dropped from the emitted skeleton in silence — fix the spelling or remove it. {This shape has no fields at all. / Known here: &lt;[...known].sort().join(', ')&gt;.}”<br>“{"&lt;key&gt;" / "&lt;key&gt;" (did you mean &lt;near.map((n) =&gt; `"${n}"`).join(', ')&gt;?)}”<br>Also: “skin "&lt;value&gt;" slot "&lt;value&gt;" attachment "&lt;value&gt;"”<br>“&lt;value&gt; generator.bones[&lt;value&gt;]”<br>“{constraint "&lt;value&gt;" / constraints[&lt;value&gt;]}”<br>“&lt;value&gt;.transform.envelope”<br>“&lt;value&gt; weights[&lt;value&gt;][&lt;value&gt;]”<br>“invariants.deformMayFold[&lt;value&gt;]”<br>“animations."&lt;value&gt;"”<br>“&lt;value&gt;.offsets[&lt;value&gt;]”<br>“{stop "&lt;value&gt;" / stops[&lt;value&gt;]}”<br>“&lt;value&gt; properties."&lt;value&gt;".to."&lt;value&gt;"”<br>“this rig spec”<br>“&lt;value&gt;.derive”<br>“setup."&lt;value&gt;"”<br>“physics."&lt;value&gt;"”<br>“&lt;value&gt;'s "lower"”<br>“&lt;value&gt; "sequence"”<br>“&lt;value&gt; generator.bias”<br>“&lt;value&gt; generator.depth”<br>“&lt;value&gt;.events[&lt;value&gt;]”<br>“&lt;value&gt;.tracks[&lt;value&gt;]”<br>“invariants.axes[&lt;value&gt;]”<br>“&lt;value&gt; generator.falloff”<br>“&lt;value&gt;.drawOrder[&lt;value&gt;]”<br>“&lt;value&gt; generator.influence”<br>“invariants.detached[&lt;value&gt;]”<br>“{slot "&lt;value&gt;" / slots[&lt;value&gt;]}”<br>“invariants.consumerDrivenMix[&lt;value&gt;]”<br>“event "&lt;value&gt;"”<br>“&lt;value&gt;'s "from"”<br>“this motion spec”<br>“{absent / &lt;value&gt;}”<br>“{region / &lt;value&gt;}”<br>“&lt;value&gt;.keys[&lt;value&gt;]”<br>“&lt;value&gt; generator.soft”<br>“{linkedmesh / &lt;value&gt;}”<br>“&lt;value&gt;.deform[&lt;value&gt;]”<br>“&lt;value&gt;.sequence[&lt;value&gt;]”<br>“&lt;value&gt; generator (&lt;value&gt;)”<br>“{bone "&lt;value&gt;" / bones[&lt;value&gt;]}”<br>Here the values named in the message include: “uvs”, “mixX”, “blend”, “scaleX”, “groups”, “radius”, “images”, “axisBone”, “maxBones”, “massBone”, “softness”, “timelines”, “archetype”, “rotateMode”, “windGlobal”, “localTarget”, “spacingMode”, “positionMode”, “inertiaGlobal”, “dampingGlobal”, “gravityGlobal”, “strengthGlobal”, “skeleton.stageBox”, “invariants.idleDrivesMeshes”, “ik”, “lag”, “why”, “cut”, “fps”, “icon”, “dark”, “cols”, “kind”, “hull”, “cell”, “path”, “loop”, “mask”, “ease”, “ramp”, “note”, “mode”, “size”, “rows”, “mixY”, “alpha”, “pairs”, “inner”, “curve”, “limit”, “edges”, “float”, “start”, “audio”, “along”, “clamp”, “width”, “skins”, “links”, “chain”, “phase”, “about”, “power”, “count”, “color”, “gamma”, “index”, “delay”, “shearY”, “source”, “convex”, “target”, “amount”, “volume”, “shearX”, “slider”, “string”, “center”, “parent”, “closed”, “zScale”, “inward”, “margin”, “across”, “height”, “digits”, “scaleY”, “anchor”, “degrees”, “inverse”, “stretch”, “easings”, “against”, “default”, “inherit”, “lengths”, “carried”, “balance”, “stagger”, “outward”, “through”, “vertices”, “axis_deg”, “contrast”, “compress”, “notUnder”, “property”, “duration”, “controls”, “additive”, “rotation”, “triangles”, “mixScaleY”, “mixScaleX”, “mixShearY”, “tolerance”, “mixRotate”, “meshSlots”, “minWeight”, “mixGlobal”, “amplitude”, “massGlobal”, “meshCenter”, “fromVertex”, “slotWindow”, “wavelength”, “"skeleton"”, “attachments”, “vertexCount”, “maxVertices”, “localSource”, “"invariants"”, “bendPositive”, “boneIndexing”, “constantSpeed”, “meshTriangles”, “referenceScale”. | Fix each printed stray key's spelling using the suggested or known names, or remove it. |
7784
+ | <a id="CE_KEYS_UNKNOWN_FIELD"></a>`CE_KEYS_UNKNOWN_FIELD` | “&lt;where&gt;: &lt;what&gt; has &lt;strays.length === 1 ? 'a key' : `${strays.length} keys`&gt; this compiler does not read: &lt;named.join(', ')&gt;. Nothing reads such a key, so it would be dropped from the emitted skeleton in silence — fix the spelling or remove it. {This shape has no fields at all. / Known here: &lt;[...known].sort().join(', ')&gt;.}”<br>“{"&lt;key&gt;" / "&lt;key&gt;" (did you mean &lt;near.map((n) =&gt; `"${n}"`).join(', ')&gt;?)}”<br>Also: “skin "&lt;value&gt;" slot "&lt;value&gt;" attachment "&lt;value&gt;"”<br>“&lt;value&gt; generator.bones[&lt;value&gt;]”<br>“{constraint "&lt;value&gt;" / constraints[&lt;value&gt;]}”<br>“&lt;value&gt;.transform.envelope”<br>“&lt;value&gt; weights[&lt;value&gt;][&lt;value&gt;]”<br>“invariants.deformMayFold[&lt;value&gt;]”<br>“animations."&lt;value&gt;"”<br>“&lt;value&gt;.offsets[&lt;value&gt;]”<br>“{stop "&lt;value&gt;" / stops[&lt;value&gt;]}”<br>“&lt;value&gt; properties."&lt;value&gt;".to."&lt;value&gt;"”<br>“this rig spec”<br>“&lt;value&gt;.derive”<br>“setup."&lt;value&gt;"”<br>“physics."&lt;value&gt;"”<br>“&lt;value&gt;'s "lower"”<br>“&lt;value&gt; "sequence"”<br>“&lt;value&gt; generator.bias”<br>“&lt;value&gt; generator.depth”<br>“&lt;value&gt; depth”<br>“&lt;value&gt;.events[&lt;value&gt;]”<br>“&lt;value&gt;.tracks[&lt;value&gt;]”<br>“invariants.axes[&lt;value&gt;]”<br>“&lt;value&gt; generator.falloff”<br>“&lt;value&gt;.drawOrder[&lt;value&gt;]”<br>“&lt;value&gt; generator.influence”<br>“invariants.detached[&lt;value&gt;]”<br>“{slot "&lt;value&gt;" / slots[&lt;value&gt;]}”<br>“invariants.consumerDrivenMix[&lt;value&gt;]”<br>“event "&lt;value&gt;"”<br>“&lt;value&gt;'s "from"”<br>“this motion spec”<br>“{absent / &lt;value&gt;}”<br>“{region / &lt;value&gt;}”<br>“&lt;value&gt;.keys[&lt;value&gt;]”<br>“&lt;value&gt; generator.soft”<br>“{linkedmesh / &lt;value&gt;}”<br>“&lt;value&gt;.deform[&lt;value&gt;]”<br>“&lt;value&gt;.sequence[&lt;value&gt;]”<br>“&lt;value&gt; generator (&lt;value&gt;)”<br>“{bone "&lt;value&gt;" / bones[&lt;value&gt;]}”<br>Here the values named in the message include: “uvs”, “mixX”, “blend”, “scaleX”, “groups”, “radius”, “images”, “axisBone”, “maxBones”, “massBone”, “softness”, “timelines”, “archetype”, “rotateMode”, “windGlobal”, “localTarget”, “spacingMode”, “positionMode”, “inertiaGlobal”, “dampingGlobal”, “gravityGlobal”, “strengthGlobal”, “skeleton.stageBox”, “invariants.idleDrivesMeshes”, “ik”, “lag”, “why”, “cut”, “fps”, “icon”, “dark”, “cols”, “kind”, “hull”, “cell”, “path”, “loop”, “mask”, “ease”, “ramp”, “note”, “mode”, “size”, “rows”, “mixY”, “alpha”, “pairs”, “inner”, “curve”, “limit”, “edges”, “float”, “start”, “audio”, “along”, “clamp”, “width”, “skins”, “links”, “chain”, “phase”, “about”, “power”, “count”, “color”, “gamma”, “index”, “delay”, “shearY”, “source”, “convex”, “target”, “amount”, “volume”, “shearX”, “slider”, “string”, “center”, “parent”, “closed”, “zScale”, “inward”, “margin”, “across”, “height”, “digits”, “scaleY”, “anchor”, “degrees”, “inverse”, “stretch”, “easings”, “against”, “default”, “inherit”, “lengths”, “carried”, “balance”, “stagger”, “outward”, “through”, “vertices”, “axis_deg”, “contrast”, “compress”, “notUnder”, “property”, “duration”, “controls”, “additive”, “rotation”, “triangles”, “mixScaleY”, “mixScaleX”, “mixShearY”, “tolerance”, “mixRotate”, “meshSlots”, “minWeight”, “mixGlobal”, “amplitude”, “massGlobal”, “meshCenter”, “fromVertex”, “slotWindow”, “wavelength”, “"skeleton"”, “attachments”, “vertexCount”, “maxVertices”, “localSource”, “"invariants"”, “bendPositive”, “boneIndexing”, “constantSpeed”, “meshTriangles”, “referenceScale”, “narrowing”. | Fix each printed stray key's spelling using the suggested or known names, or remove it. |
7722
7785
  | <a id="CE_KEYS_NUMBER_FLOAT32_FINITE"></a>`CE_KEYS_NUMBER_FLOAT32_FINITE` | “&lt;where&gt;: &lt;place(path)&gt; is &lt;String(node)&gt;; a number in this file is finite at float32 precision, at most ±&lt;String(FLOAT32_MAX)&gt;, because the skeleton is written as float32 — past that it would be emitted as Infinity, which JSON writes as null, a value the file never stated.&lt;why&gt;”<br>“{ / { This one is finite as a double and has no float32 but Infinity. / JSON has no spelling for Infinity: a literal past ±1.7976931348623157e308, such as 1e309, parses to it.}}” | State a finite number whose float32 rounding stays finite; inspect the printed path and magnitude. |
7723
7786
  | <a id="CE_KEYS_FIELD_VALUE_TYPE"></a>`CE_KEYS_FIELD_VALUE_TYPE` | “&lt;where&gt;: &lt;visit.name([key, ...wrong.tail])&gt; is &lt;typeFound(wrong.value)&gt;; &lt;wrong.required&gt; is required”<br>“a boolean (true or false)”<br>“a number”<br>“a number or null”<br>“a string”<br>“a string or null”<br>“an array of arrays of numbers”<br>“an array of numbers”<br>“an array of numbers, or null”<br>“an array of strings”<br>“an object whose every value is a string or null”<br>“an object whose every value is an array of numbers”<br>“an object whose every value is an array of strings” | Replace the named field or element with the required JSON type printed in the message. |
7724
7787
  | <a id="CE_KEYS_ENUM_VALUE"></a>`CE_KEYS_ENUM_VALUE` | “&lt;where&gt;: &lt;visit.name([key])&gt; is &lt;JSON.stringify(value) ?? String(value)&gt;; one of &lt;rule.set.map((name) =&gt; JSON.stringify(name)).join(', ')&gt; — &lt;rule.readAs&gt;”<br>“anything else was read as a ribbon by the mesh checks and as a ring by the control-bone reader and the geometry builder, so the refusal it got named a fault the part did not have”<br>“anything else was read as no rotation source, and the bone was emitted without the setup rotation it asked for”<br>“anything else was read as the default "name" in silence: a weighted "vertices" run was refused as unflagged, and every other attachment built byte for byte as if the key were absent” | Choose one of the printed names for the named field; the readAs explanation describes why another spelling changes its meaning. |
@@ -9060,6 +9123,23 @@ Draw order does not enter: a cell is covered or bare whichever piece is in front
9060
9123
  An owner says which piece drew a ghost cell; it does not say which piece is over
9061
9124
  or under the other, or whether that exposure should fail a consumer's policy.
9062
9125
 
9126
+ **Outside the overlap hull is outside the gap reading.** Both `gap` and its
9127
+ `gapOpen` partition are judged only inside the carried setup-overlap hull. Art
9128
+ that reaches beyond a joint disc, such as a drape over an elbow, can break
9129
+ outside that hull without a gap being reported. A gap of 0 is never a statement
9130
+ that the seam is closed outside the hull; inspect `ghost` and the actual frames
9131
+ too. Ghost independently names one-sided exposure of the carried setup overlap,
9132
+ not every visible break in the silhouette.
9133
+
9134
+ In the public [draped-sleeve fixture](https://github.com/rig-c/parts/blob/0628d7bc311a2b548edf7a11e8407546357d4b4d/fixtures/hybrid_limb.ts),
9135
+ measured on core `f273358` at both −30° and +30°, the hard cut reads `gap = 0`,
9136
+ `gapOpen = 0`, `ghost = 8` cells; the hybrid's two interfaces read 0 in all three
9137
+ classes. The [cell overlay](https://github.com/rig-c/core/blob/570a2954747780a2169fa6036eebe53f3ea66f2a/bench/studies/2026-10-25-seam-reach/evidence/cut-endpoint-cell-overlay.png)
9138
+ shows the notch and flap excluded outside the hull, while ghost names different
9139
+ cells on the joint rim. The [measurement receipt](https://github.com/rig-c/core/blob/570a2954747780a2169fa6036eebe53f3ea66f2a/bench/studies/2026-10-25-seam-reach/evidence/receipt.json)
9140
+ pins the inputs, commands and frame digest; these are seam cells, not display
9141
+ pixels. See [#1575](https://github.com/rig-c/core/issues/1575) for the reading.
9142
+
9063
9143
  **Its reach, stated.** A piece folded right back over its partner carries the band
9064
9144
  across the region it vacated, so a hull that wide reads the vacated cells its
9065
9145
  partner's view still finds drawn. And no size is too small to print: on
@@ -12442,3 +12522,132 @@ calls, with the independent linked mesh selected, agree exactly. The API's
12442
12522
  nonselected output is the stored source geometry through the unchanged setup
12443
12523
  matrices; it does not borrow a different selection's deform. The control
12444
12524
  checks both results and the source of the difference, without an epsilon.
12525
+
12526
+ ## 15. Posing in place for a player — the runtime poser (stage 1)
12527
+
12528
+ rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
12529
+ (`src/core/`) is the reference: each pose is a pure function of the document,
12530
+ the skin and the time, returned as a fresh, name-keyed, detached record, and it
12531
+ is the poser `tools/core_gate.ts` holds to spine-core 4.3.13 at tolerance 0. The
12532
+ **runtime poser** (`src/runtime/`) is for a player drawing many skeletons a
12533
+ frame: it resolves every name to an index once at load and overwrites typed
12534
+ arrays in place, so a steady-state frame allocates nothing. It computes every
12535
+ value with the oracle's own formulas in the oracle's order of operations, and it
12536
+ is held equal to the oracle at tolerance 0 rather than trusted; the oracle is not
12537
+ changed for it. It is imported from `rig-c/runtime`, an **experimental** entry
12538
+ (RELEASING.md, *The import surface*): not promised until it stabilises, so a
12539
+ minor may reshape it and a dependant pins an exact version.
12540
+
12541
+ ```ts
12542
+ import { readModel, underNoSkin } from 'rig-c/core';
12543
+ import { compileRuntime, RuntimePose, RuntimeRefusal } from 'rig-c/runtime'; // experimental
12544
+
12545
+ const view = underNoSkin(readModel(modelText));
12546
+ const model = compileRuntime(view, { ignore: ['events'] }); // throws RuntimeRefusal, every missing construct named
12547
+ const pose = new RuntimePose(model); // every buffer allocated here, once
12548
+ pose.setAnimation('walk'); // a refused animation throws its RuntimeRefusal
12549
+ pose.step(1 / 60); // or pose.apply(t) at an animation time
12550
+ pose.computeVertices();
12551
+ // pose.world: 6 numbers per bone (a, b, c, d, worldX, worldY — W_A … W_Y), y up
12552
+ // pose.light: 4 per slot; pose.shown: per slot an index into model.attachments, or −1
12553
+ // pose.vertices: model.attachments[i].count numbers at model.attachments[i].offset
12554
+ ```
12555
+
12556
+ Each `model.attachments[i]` also carries readonly `skin` and `placeholder`: the
12557
+ exact resolved table identity `(skin, model.slots[attachment.slot].name, placeholder)`.
12558
+ The display `name` may be shared by different placeholders; it is never a table
12559
+ key to guess the source from. These load-time strings do not change the pose path.
12560
+
12561
+ | | what stage 1 poses |
12562
+ | --- | --- |
12563
+ | bones | local and world transforms, all five inherit modes, skin-required bones under the view's skin (an inactive bone is zeros, as the oracle poses it) |
12564
+ | timelines | `rotate`, `translate(x/y)`, `scale(x/y)`, `shear(x/y)`; a slot's `attachment`, `rgba`, `rgb` and `alpha`; ik and transform timelines; linear, stepped and Bézier keys, each read from a per-pose key cursor |
12565
+ | constraints | one- and two-bone ik; world-space, absolute transform constraints; the document's order, each followed by the bones below what it moved posed again |
12566
+ | vertices | regions, meshes, weighted meshes and linked meshes, written into the pose's buffer |
12567
+ | skins and events | named/no-skin views with their skin-required bones and constraints; event delivery in key order over `(last, time]`, tail then head across a wrap |
12568
+ | time | one animation at full weight from the setup pose, at a time (`apply`) or stepped as a looping track (`step`, the track time wrapped by the duration as `openTrack` wraps it) |
12569
+
12570
+ Skin switching is a load-time view change: compile `underSkin(document, name)`
12571
+ (or `underNoSkin(document)`), create the replacement `RuntimePose`, then call
12572
+ `replacement.restore(animationName, oldPose.trackTime, oldPose.time)`. Restore
12573
+ carries both clocks **and the event cursor**: its event list is empty, a key at
12574
+ or before that restored animation time is not fired again, and later keys and
12575
+ subsequent loops still deliver normally. Do not restore by setting trackTime and
12576
+ calling apply: setAnimation starts the event cursor at −1, so that sequence
12577
+ replays already-passed cues. `restore` refuses non-finite times by name.
12578
+ The old buffers belong to the old view; do not keep drawing them as the new
12579
+ skin. An ambiguous merged `all` view still refuses `skin-conflict`; that refusal
12580
+ does not reject either named view.
12581
+
12582
+ Read `pose.events` immediately after `apply`/`step`. It is a reused borrowed list
12583
+ of immutable load-time records: `name`, float32 key `time`, `int`, `float`,
12584
+ `string`, `volume`, `balance`. Duplicate-time keys keep their authored order.
12585
+ The interval and wrap rules are the oracle's `openTrack` rules; a new animation
12586
+ starts at previous event time −1, so an apply at 0 includes its zero-time keys.
12587
+ The list is replaced on the next pose; retain copies only when needed. Delivery
12588
+ itself reuses records and list storage. The legacy five-column oracle event row
12589
+ is unchanged. Public `EventTimeline` observations also hold the audio details:
12590
+ silent keys keep volume/balance at 0; on a voiced key an omitted balance falls
12591
+ back to the definition's **volume**, matching the measured runtime rather than
12592
+ inventing a correction. An explicit `ignore: ['events']` diagnostic still mutes
12593
+ the list; normal compilation never does.
12594
+
12595
+ **Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
12596
+ every construct met, each `{ feature, detail, animation }`, and the message
12597
+ reads `not posed by the runtime poser (stage 1), N construct(s) missing:
12598
+ <feature>: <detail>; …`. A model-level feature refuses the whole model; an
12599
+ animation-level one refuses that animation alone (`model.refused`;
12600
+ `model.animation(name)` and `setAnimation(name)` throw its refusal) and every
12601
+ other animation is posed. Fall back to the oracle (`openTrack`) for what is
12602
+ refused: nothing is posed approximately.
12603
+
12604
+ | feature | refuses | what it is |
12605
+ | --- | --- | --- |
12606
+ | `path-constraint`, `physics-constraint`, `slider-constraint` | the model | a constraint of that kind |
12607
+ | `local-transform`, `additive-transform` | the model | a transform constraint with `localSource` or `localTarget`, or `additive` |
12608
+ | `inactive-constrained-bone` | the model | an applied ik or transform constraint naming a bone the view leaves inactive (what the oracle writes there depends on the pass before) |
12609
+ | `noscale-world-target` | the model | a world-space transform onto a `noScale` or `noScaleOrReflection` bone, whose local values would be read back in that mode |
12610
+ | `clipping`, `null-atlas-region` | the model when shown at setup, else the animation that keys it | a clipping attachment; a region whose atlas rectangle the model states as `null` |
12611
+ | `skin-conflict` | as above | a placeholder the skins fill differently |
12612
+ | `oracle-absent` | the model | a pose the oracle itself leaves out (`constraintsAbsentWhy`) |
12613
+ | `inherit-timeline`, `deform-timeline`, `sequence-timeline`, `draw-order-timeline`, `dark-colour-timeline` | the animation | a timeline of that kind (`dark-colour` is `rgba2` / `rgb2`) |
12614
+
12615
+ A call it cannot answer throws `RuntimeInputError`, naming the call:
12616
+ `apply: no animation is set (setAnimation first)`, `animation "x" is not one of
12617
+ this document's [...]`, `apply: the time NaN is not finite`.
12618
+
12619
+ **The gate.** `tools/core_gate.ts` runs a third dumper (`runtimeDump` in
12620
+ `tools/pose_oracle.ts`) over every row it builds, at the oracle's samples, and
12621
+ compares its document with the oracle's in full doubles at tolerance 0 twice:
12622
+ through `compare` and cell by cell (which reads the bones `compare`'s
12623
+ ill-conditioned rule leaves out). One `RUNTIME` line per row reads `IDENTICAL`
12624
+ (covered whole and equal), `SKIP` (refused at load, or animations or events left
12625
+ out — every one named), `DIFF` (the first value that differs) or `REFUSED`; then
12626
+ one `RUNTIME SKIP feature <name>` line per feature with the rows it refused, a
12627
+ `HOLE` line when no row is covered whole or nothing was compared, and `RUNTIME
12628
+ GREEN|RED`, counting rows covered whole, covered in part and refused at load, and
12629
+ the poses and values compared. A `DIFF` or `REFUSED` turns the run RED; a SKIP
12630
+ never counts as a pass. The page UVs are the atlas's and are compared by neither
12631
+ side here. The selftest's `runtime-poser` suite holds the gate to its plants (a
12632
+ 1e-6° bone perturbation, a weighted binding bound to the wrong bone index, a
12633
+ refused feature smuggled through — each caught by name), a seeded population of
12634
+ every construct above, the looping step against `openTrack`, and a steady-state
12635
+ frame measured at 0 bytes with the collector off.
12636
+
12637
+ ## 16. Compare sequential mesh candidates against one motion reference
12638
+
12639
+ Programmatic callers that gate candidates one at a time can import
12640
+ `prepareMotionReference` and `comparePreparedMotion` from `rig-c/meshcompare`.
12641
+ Pass the one-shot `MotionComparisonInput` without `candidates` to preparation,
12642
+ then pass each complete input with its candidate list to comparison. The handle
12643
+ reuses the reference's parsed build, art samples, carriers and poses only while
12644
+ the reference model text, schedule, attachments (including mask alpha bytes),
12645
+ art-fit bounds and motion options match exactly. A changed bound field, even
12646
+ `+0` to `-0`, throws `MeshReductionError` with
12647
+ `COMPARE_PREPARED_MISMATCH` naming the field; a forged handle throws
12648
+ `COMPARE_PREPARED_HANDLE`. Prepare a new handle for a different selection or
12649
+ held-out schedule. Returned reports are detached from the prepared state, and
12650
+ the one-shot `compareMeshesInMotion` retains its report and refusal contract.
12651
+ The full comparison contract and its input types are in the repository's
12652
+ [MESH_REDUCTION.md](https://github.com/rig-c/core/blob/main/docs/MESH_REDUCTION.md#the-comparison-as-implemented-stage-c1),
12653
+ which is not shipped in the package.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/core",
3
- "version": "2.70.0",
3
+ "version": "2.72.0",
4
4
  "description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "./errors": "./src/errors.ts",
20
20
  "./meshcompare": "./src/meshcompare.ts",
21
21
  "./core": "./src/core/entry.ts",
22
+ "./runtime": "./src/runtime/index.ts",
22
23
  "./cli": "./cli.ts",
23
24
  "./package.json": "./package.json",
24
25
  "./*.ts": "./*.ts",
@@ -20,7 +20,7 @@ export const VALIDATE_PROFILES: readonly AssertionProfile[] = ['spine', 'rig-pla
20
20
  * the target and never the alias.
21
21
  */
22
22
  export function profileAliasLine(alias: string, target: AssertionProfile): string {
23
- return `rigc: --profile ${alias} is deprecated and reads as --profile ${target}, the same rules; it is accepted until the next minor release — pass --profile ${target}`;
23
+ return `rigc: --profile ${alias} is deprecated and reads as --profile ${target}, the same rules; it is accepted until the next major release — pass --profile ${target}`;
24
24
  }
25
25
 
26
26
  /**