@rig-c/core 2.70.0 → 2.71.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 +143 -8
- package/package.json +2 -1
- package/src/assertions/report.ts +1 -1
- package/src/compile.ts +48 -0
- package/src/core/animation.ts +56 -24
- package/src/core/index.ts +62 -1
- package/src/core/raw.ts +38 -12
- package/src/core/tracks.ts +23 -8
- package/src/core/vertices.ts +182 -44
- package/src/deformgen.ts +23 -7
- package/src/depth.ts +17 -4
- package/src/motion.ts +4 -4
- package/src/runtime/curves.ts +106 -0
- package/src/runtime/index.ts +80 -0
- package/src/runtime/model.ts +719 -0
- package/src/runtime/pose.ts +1014 -0
- package/src/trackgen.ts +27 -4
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
|
|
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 |
|
|
@@ -5219,7 +5219,7 @@ is that a **depth** is the decision while a **residual** is not.
|
|
|
5219
5219
|
|
|
5220
5220
|
| `kind` | Reads | `translatex` / `translatey` | `scalex` / `scaley` | Worked case |
|
|
5221
5221
|
| --- | --- | --- | --- | --- |
|
|
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` |
|
|
5222
|
+
| `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
5223
|
| `pitch` | each member's setup `y` | the same expression with `y` for `x` — a nod | the same | `gallery/nod` |
|
|
5224
5224
|
|
|
5225
5225
|
⭐ **The `property` says which half of the turn a key is.** A turn does two
|
|
@@ -5248,6 +5248,15 @@ is an **auditing** decision before it is a rigging one: a residual is 1–6 unit
|
|
|
5248
5248
|
where a total is 30–40, and nobody can eyeball a wrong sign in the second.
|
|
5249
5249
|
`about` (default 0) is where the axis crosses the driving coordinate, in the
|
|
5250
5250
|
members' shared parent's space.
|
|
5251
|
+
`narrowing` (default `true`) says whether the displacement includes the
|
|
5252
|
+
foreshortening of the member's setup position `(x−about)·(cos t − 1)` (or `y`).
|
|
5253
|
+
With `"narrowing": false`, the displacement evaluates `d = −(depth − carried)·sin t`
|
|
5254
|
+
alone — for a rig whose plate proportions are held at their front-view width
|
|
5255
|
+
across the turn rather than narrowed toward the axis (§4.11.1). It belongs on the
|
|
5256
|
+
displacement projection (`translatex` / `translatey`) only; stating `narrowing` on
|
|
5257
|
+
a `scalex` or `scaley` track is refused by name (`CE_TRACK_NARROWING_PROJECTION`),
|
|
5258
|
+
because a foreshortening track *is* the narrowing — to turn without narrowing a
|
|
5259
|
+
plate's scale, omit that track.
|
|
5251
5260
|
|
|
5252
5261
|
Six things the construct is bounded by, and each one is a refusal:
|
|
5253
5262
|
|
|
@@ -6194,7 +6203,7 @@ example needed it:
|
|
|
6194
6203
|
|
|
6195
6204
|
| `kind` | Parameters | What it evaluates | Worked case |
|
|
6196
6205
|
| --- | --- | --- | --- |
|
|
6197
|
-
| `yaw` | `radius` **or** `depth`, `degrees`, `about` | `dx = (x−about)·(cos t − 1) − z·sin t
|
|
6206
|
+
| `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
6207
|
| `pitch` | the same | the same expression with `y` for `x` — a nod rather than a turn | `gallery/nod` |
|
|
6199
6208
|
| `affine` | `scale`, `about` | `dx = (sx−1)·(x−ax)`, `dy = (sy−1)·(y−ay)` — a scale about a fixed point | `gallery/squash` |
|
|
6200
6209
|
| `wave` | `amplitude`, `wavelength`, `phase`, `along`, `axis` | `d = amplitude · sin(2π·along/wavelength + phase)` | `gallery/nod` |
|
|
@@ -6204,8 +6213,28 @@ example needed it:
|
|
|
6204
6213
|
displaces; they cannot be the same coordinate, because reading and displacing one
|
|
6205
6214
|
axis is a stretch and `affine` states that. `power: 1` is an affine shear and
|
|
6206
6215
|
`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]`), `
|
|
6208
|
-
to 2; nothing else has a default.
|
|
6216
|
+
instead of tilting. `about` defaults to 0 (or `[0, 0]`), `narrowing` to `true`,
|
|
6217
|
+
`phase` to 0 and `power` to 2; nothing else has a default.
|
|
6218
|
+
|
|
6219
|
+
**`"narrowing": false` drops the setup-position foreshortening `(u−about)·(cos t − 1)` and keeps the depth shift `−z·sin t` alone.**
|
|
6220
|
+
A rigid 3D cylinder narrows across the screen as `cos t`, pulling every vertex
|
|
6221
|
+
inward by `(x−about)·(cos t − 1)` on top of its depth shift `−z·sin t`. On a
|
|
6222
|
+
cut-out rig whose facial plates are drawn to keep their front-view proportions
|
|
6223
|
+
across a turn — features translating by `−(depth − carried)·sin t` without
|
|
6224
|
+
foreshortening their setup `x` or `y` — that inward pull squashes the outer cheek
|
|
6225
|
+
while the features riding over it do not narrow to match. Setting `"narrowing": false`
|
|
6226
|
+
on `yaw` or `pitch` (both here and on `derive` in §4.5.1) states that choice
|
|
6227
|
+
in the model rather than forcing the author back to a raw `vertices` array.
|
|
6228
|
+
With the `cos t` term absent, a triangle's deformed twice-signed area is
|
|
6229
|
+
`2A(t) = 2A₀ − 2A_axis·sin t` instead of `2A₀·cos t − 2A_axis·sin t`, so it
|
|
6230
|
+
crosses zero at `sin t = A₀ / A_axis` (`asin(|A₀ / A_axis|)` when `|A₀ / A_axis| < 1`,
|
|
6231
|
+
and never on `(0, 90°)` when `|A₀ / A_axis| ≥ 1`) rather than `tan t = A₀ / A_axis`
|
|
6232
|
+
(`atan(|A₀ / A_axis|)`). When a deform key on a depth-mapped mesh declares
|
|
6233
|
+
`"narrowing": false`, `build` and `explain` recompute that axis's turn ceiling
|
|
6234
|
+
(§3.4.5) with `asin`, so the reported ceiling stays the exact angle `A39` fires at;
|
|
6235
|
+
mixing `"narrowing": false` and the default (`true`) on the same attachment and axis
|
|
6236
|
+
is refused (`CE_COMPILE_DEFORM_TURN_NARROWING_MIXED`), because one attachment and axis
|
|
6237
|
+
cannot have both the `asin` and `atan` ceilings at once.
|
|
6209
6238
|
|
|
6210
6239
|
Six things this construct is bounded by, and each one is a refusal rather than a
|
|
6211
6240
|
convention:
|
|
@@ -7456,6 +7485,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7456
7485
|
| <a id="CE_COMPILE_DEFORM_FROM_VERTEX_INVALID"></a>`CE_COMPILE_DEFORM_FROM_VERTEX_INVALID` | “<value> (t=<value>): fromVertex is <value>; it is a vertex index, so a whole number ≥ 0” | State fromVertex as a whole vertex index at least zero. |
|
|
7457
7486
|
| <a id="CE_COMPILE_DEFORM_FROM_VERTEX_OUTSIDE_ATTACHMENT"></a>`CE_COMPILE_DEFORM_FROM_VERTEX_OUTSIDE_ATTACHMENT` | “<value> (t=<value>): fromVertex <value> plus <value> vertex offset(s) runs to vertex <value><value>, and the attachment has <value>” | Keep fromVertex plus the covered vertex count within the attachment. |
|
|
7458
7487
|
| <a id="CE_COMPILE_DEFORM_WEIGHTED_FROM_VERTEX_AMBIGUOUS"></a>`CE_COMPILE_DEFORM_WEIGHTED_FROM_VERTEX_AMBIGUOUS` | “<value> (t=<value>): "fromVertex" counts VERTICES, and this attachment is weighted — its deform array holds one x, y pair per bone INFLUENCE, and vertex <value> has <value> 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 <value> starts at deform index <value>).” | Key the control bone or author each bone influence's bind-space pair using offset. |
|
|
7488
|
+
| <a id="CE_COMPILE_DEFORM_TURN_NARROWING_MIXED"></a>`CE_COMPILE_DEFORM_TURN_NARROWING_MIXED` | “deform <turnTriple> keys "<turnAxis>" with narrowing=false in animation "<turnFalseAnim>" and with narrowing=true in animation "<turnTrueAnim>". 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 "<turnAxis>" key on <turnTriple>.”<br>Also: “<turnDeformSkin>/<turnTrack.slot>/<turnTrack.attachment>”<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
7489
|
| <a id="CE_COMPILE_TRACK_TARGETS_MULTIPLE"></a>`CE_COMPILE_TRACK_TARGETS_MULTIPLE` | “animation "<value>": a track names more than one target (<value>); the list is <value> 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
7490
|
| <a id="CE_COMPILE_CONSTRAINT_TRACK_PROPERTY_UNKNOWN"></a>`CE_COMPILE_CONSTRAINT_TRACK_PROPERTY_UNKNOWN` | “animation "<value>": <value> has no timeline "<value>" (it has: <value>)”<br>Also: “, ”<br>“path constraint”<br>“physics constraint”<br>“{group "<value>" / <value> "<value>"}”<br>Here the values named in the message include: “slider”. | Use a timeline property supported by this constraint family. |
|
|
7461
7491
|
| <a id="CE_COMPILE_ANIMATION_GROUP_UNKNOWN"></a>`CE_COMPILE_ANIMATION_GROUP_UNKNOWN` | “animation "<value>": unknown group "<value>"” | Declare the motion group or correct its name. |
|
|
@@ -7623,8 +7653,8 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7623
7653
|
| <a id="CE_DEFORM_WAVELENGTH_ZERO"></a>`CE_DEFORM_WAVELENGTH_ZERO` | “<where>: transform wave has wavelength 0; one period cannot be zero long” | State a non-zero wavelength. |
|
|
7624
7654
|
| <a id="CE_DEFORM_BEND_POWER_WHOLE"></a>`CE_DEFORM_BEND_POWER_WHOLE` | “<where>: transform bend has power <power>; 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
7655
|
| <a id="CE_DEFORM_BEND_ENDS_EQUAL"></a>`CE_DEFORM_BEND_ENDS_EQUAL` | “<where>: transform bend has from <from> and to <to>; 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` | “<where>: transform <kind> states <stated>, and every one of this attachment's <count> vertices evaluates to an offset of 0 — the largest value the closed form reached at any of them is <band.toExponential(3)>, 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. <sampledTo>. A key that MEANS the identity states it in its own parameters (<identitySpelling>) 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=[<about[0]>, <about[1]>] on the axis the scale changes — the geometry has collapsed onto the point the key holds still”<br>“amount 0”<br>“amount=<amount> from=<from> to=<to> power=<power> along=<an> axis=<axis === 0 ? 'x' : 'y'>”<br>“amplitude 0”<br>“amplitude=<amplitude> wavelength=<wavelength> phase=<phase> along=<an> axis=<axis === 0 ? 'x' : 'y'>”<br>“degrees 0”<br>“scale [1, 1]”<br>“scale=[<scale[0]>, <scale[1]>]<a.about === undefined ? '' : ` about=[${about[0]}, ${about[1]}]`>”<br>“{Every vertex sits at <an>=<distinct[0]>, 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 <an> coordinates in this attachment are <round(gap)> apart, and a sinusoid has to be sampled to exist: a wavelength of at least 4x that (<round(4 * gap)>) to read as a wave at all and 8x (<round(8 * gap)>) to read as a curve, where this key states <round(wavelength / gap)>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 <an>=<from>, 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=<round(reach)> of 1, and u^<power> of that is <(reach ** power).toExponential(3)> — 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 <degrees>°, and a projection of it can only vanish where every vertex shares one <kind === 'yaw' ? 'x' : 'y'> 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 <degrees>°, and a projection of it can only vanish where every vertex shares one <kind === 'yaw' ? 'x' : 'y'> — check that this attachment's setup geometry is the shape the radius says it is}”<br>“{depth=true degrees=<degrees><t.about === undefined ? '' : ` about=${about}`> / radius=<radius> degrees=<degrees><t.about === undefined ? '' : ` about=${about}`>}”<br>Also: “axis=[<value>, <value>] from=<value> to=<value> shape=<value>”<br>“mask=<value>”<br>“<value> envelope(<value>)”<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` | “<where>: transform <kind> states <stated> and moves <plain.filter((d, i) => i % 2 === 0 && (d !== 0 \|\| plain[i + 1] !== 0)).length> of <count> vertices, and envelope <weighed.stated> weighs every one of them 0 — so the key states a deformation and emits the identity. <weighed.reach>”<br>“The mask "<mask>" weighs <carried> of <count> vertices above 0.”<br>“axis=[<axis[0]>, <axis[1]>] from=<from> to=<to> shape=<shape>”<br>“mask=<mask>”<br>“{The attachment has no vertices. / Along [<axis[0]>, <axis[1]>] this attachment's vertices sit at <round(lo)> to <round(hi)>, and the ramp rises from <from> (weight 0) to <to> (weight 1) — move "from" and "to" onto the part.}”<br>Also: “amplitude=<value> wavelength=<value> phase=<value> along=<value> axis=<value>”<br>“{depth=true degrees=<value><value> / radius=<value> degrees=<value><value>}”<br>“{ / about=[<value>, <value>]}”<br>“amount=<value> from=<value> to=<value> power=<value> along=<value> axis=<value>”<br>“<value> envelope(<value>)”<br>“scale=[<value>, <value>]<value>”<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. |
|
|
7656
|
+
| <a id="CE_DEFORM_TRANSFORM_QUANTISED_IDENTITY"></a>`CE_DEFORM_TRANSFORM_QUANTISED_IDENTITY` | “<where>: transform <kind> states <stated>, and every one of this attachment's <count> vertices evaluates to an offset of 0 — the largest value the closed form reached at any of them is <band.toExponential(3)>, 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. <sampledTo>. A key that MEANS the identity states it in its own parameters (<identitySpelling>) 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=[<about[0]>, <about[1]>] on the axis the scale changes — the geometry has collapsed onto the point the key holds still”<br>“amount 0”<br>“amount=<amount> from=<from> to=<to> power=<power> along=<an> axis=<axis === 0 ? 'x' : 'y'>”<br>“amplitude 0”<br>“amplitude=<amplitude> wavelength=<wavelength> phase=<phase> along=<an> axis=<axis === 0 ? 'x' : 'y'>”<br>“degrees 0”<br>“scale [1, 1]”<br>“scale=[<scale[0]>, <scale[1]>]<a.about === undefined ? '' : ` about=[${about[0]}, ${about[1]}]`>”<br>“{Every vertex sits at <an>=<distinct[0]>, 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 <an> coordinates in this attachment are <round(gap)> apart, and a sinusoid has to be sampled to exist: a wavelength of at least 4x that (<round(4 * gap)>) to read as a wave at all and 8x (<round(8 * gap)>) to read as a curve, where this key states <round(wavelength / gap)>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 <an>=<from>, 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=<round(reach)> of 1, and u^<power> of that is <(reach ** power).toExponential(3)> — 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 <degrees>°, and a projection of it can only vanish where every vertex shares one <kind === 'yaw' ? 'x' : 'y'> 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 <degrees>°, and a projection of it can only vanish where every vertex shares one <kind === 'yaw' ? 'x' : 'y'> — check that this attachment's setup geometry is the shape the radius says it is}”<br>“{depth=true degrees=<degrees><t.about === undefined ? '' : ` about=${about}`><narrowingNote> / radius=<radius> degrees=<degrees><t.about === undefined ? '' : ` about=${about}`><narrowingNote>}”<br>“{ / narrowing=false}”<br>Also: “axis=[<value>, <value>] from=<value> to=<value> shape=<value>”<br>“mask=<value>”<br>“<value> envelope(<value>)”<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. |
|
|
7657
|
+
| <a id="CE_DEFORM_ENVELOPE_ZERO_WEIGHT"></a>`CE_DEFORM_ENVELOPE_ZERO_WEIGHT` | “<where>: transform <kind> states <stated> and moves <plain.filter((d, i) => i % 2 === 0 && (d !== 0 \|\| plain[i + 1] !== 0)).length> of <count> vertices, and envelope <weighed.stated> weighs every one of them 0 — so the key states a deformation and emits the identity. <weighed.reach>”<br>“The mask "<mask>" weighs <carried> of <count> vertices above 0.”<br>“axis=[<axis[0]>, <axis[1]>] from=<from> to=<to> shape=<shape>”<br>“mask=<mask>”<br>“{The attachment has no vertices. / Along [<axis[0]>, <axis[1]>] this attachment's vertices sit at <round(lo)> to <round(hi)>, and the ramp rises from <from> (weight 0) to <to> (weight 1) — move "from" and "to" onto the part.}”<br>Also: “amplitude=<value> wavelength=<value> phase=<value> along=<value> axis=<value>”<br>“{depth=true degrees=<value><value><value> / radius=<value> degrees=<value><value><value>}”<br>“{ / narrowing=false}”<br>“{ / about=[<value>, <value>]}”<br>“amount=<value> from=<value> to=<value> power=<value> along=<value> axis=<value>”<br>“<value> envelope(<value>)”<br>“scale=[<value>, <value>]<value>”<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
7658
|
| <a id="CE_DEFORM_ENVELOPE_NO_EFFECT"></a>`CE_DEFORM_ENVELOPE_NO_EFFECT` | “<where>: transform <kind> states envelope <weighed.stated>, and it changes no emitted number: every vertex the model moves takes weight 1 (<weighed.full> of <count> vertices at 1, <weighed.zero> 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. <weighed.reach>”<br>“The mask "<mask>" weighs <carried> of <count> vertices above 0.”<br>“axis=[<axis[0]>, <axis[1]>] from=<from> to=<to> shape=<shape>”<br>“mask=<mask>”<br>“{The attachment has no vertices. / Along [<axis[0]>, <axis[1]>] this attachment's vertices sit at <round(lo)> to <round(hi)>, and the ramp rises from <from> (weight 0) to <to> (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
7659
|
| <a id="CE_DEFORM_ENVELOPE_OBJECT"></a>`CE_DEFORM_ENVELOPE_OBJECT` | “<where>: transform field "envelope" is <JSON.stringify(envelope)>; 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
7660
|
| <a id="CE_DEFORM_ENVELOPE_MASK_RAMP_CONFLICT"></a>`CE_DEFORM_ENVELOPE_MASK_RAMP_CONFLICT` | “<where>: transform field "envelope" states "mask" <JSON.stringify(e.mask)> beside the ramp's <rampStated.map((f) => `"${f}"`).join(', ')>. 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 +7679,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7649
7679
|
| <a id="CE_TRACK_DERIVE_OBJECT"></a>`CE_TRACK_DERIVE_OBJECT` | “<where>: "derive" is <JSON.stringify(derive)>; it is an object with a "kind"” | Write derive as an object with a kind. |
|
|
7650
7680
|
| <a id="CE_TRACK_DERIVE_KIND"></a>`CE_TRACK_DERIVE_KIND` | “<where>: "derive" has kind <JSON.stringify(kind)>; this spec evaluates <TRACK_DERIVE_KINDS.join(', ')>. 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
7681
|
| <a id="CE_TRACK_CARRIED_PROJECTION"></a>`CE_TRACK_CARRIED_PROJECTION` | “<where>: derive <kind> states carried=<JSON.stringify(turn.carried)> on a "<property>" 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 "<projections.shift>" track, whose shared shift is the thing it names.” | Put carried on the displacement projection rather than the foreshortening track. |
|
|
7682
|
+
| <a id="CE_TRACK_NARROWING_PROJECTION"></a>`CE_TRACK_NARROWING_PROJECTION` | “<where>: derive <kind> states narrowing=<JSON.stringify(turn.narrowing)> on a "<property>" track, and a "<property>" track IS the foreshortening — its whole output is cos(α − t)/cos α. "narrowing" belongs on the "<projections.shift>" track, where it drops (<projections.coordinate>−about)·(cos t − 1) from the displacement; to turn without narrowing the plate's scale, omit the "<property>" track.” | Put narrowing on the displacement projection rather than the foreshortening track, or omit the foreshortening track. |
|
|
7652
7683
|
| <a id="CE_TRACK_FORESHORTEN_DEPTH"></a>`CE_TRACK_FORESHORTEN_DEPTH` | “<where>: derive <kind> projects onto "<property>" and member "<member.name>" states depth <depth>. 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 "<projections.shift>" 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
7684
|
| <a id="CE_TRACK_FORESHORTEN_EDGE_ON"></a>`CE_TRACK_FORESHORTEN_EDGE_ON` | “<where>: derive <kind> turns member "<member.name>" (<projections.coordinate>=<u>, depth=<depth>) past its own edge: α = <round(alpha)> rad and the turn is <round(rad)> rad, so cos(α − t) is <round(Math.cos(alpha - rad))> and the scale would be <round(scale)>. 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
7685
|
| <a id="CE_TRACK_PROPERTY_PROJECTION"></a>`CE_TRACK_PROPERTY_PROJECTION` | “<where>: derive <kind> has no projection onto "<property>". It projects onto "<p.shift>" (the displacement, FACE §3) and "<p.foreshorten>" (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 +7749,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7718
7749
|
|
|
7719
7750
|
| Code | Fixed message text and alternatives | What to change |
|
|
7720
7751
|
| --- | --- | --- |
|
|
7721
|
-
| <a id="CE_KEYS_UNKNOWN_FIELD"></a>`CE_KEYS_UNKNOWN_FIELD` | “<where>: <what> has <strays.length === 1 ? 'a key' : `${strays.length} keys`> this compiler does not read: <named.join(', ')>. 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: <[...known].sort().join(', ')>.}”<br>“{"<key>" / "<key>" (did you mean <near.map((n) => `"${n}"`).join(', ')>?)}”<br>Also: “skin "<value>" slot "<value>" attachment "<value>"”<br>“<value> generator.bones[<value>]”<br>“{constraint "<value>" / constraints[<value>]}”<br>“<value>.transform.envelope”<br>“<value> weights[<value>][<value>]”<br>“invariants.deformMayFold[<value>]”<br>“animations."<value>"”<br>“<value>.offsets[<value>]”<br>“{stop "<value>" / stops[<value>]}”<br>“<value> properties."<value>".to."<value>"”<br>“this rig spec”<br>“<value>.derive”<br>“setup."<value>"”<br>“physics."<value>"”<br>“<value>'s "lower"”<br>“<value> "sequence"”<br>“<value> generator.bias”<br>“<value> generator.depth”<br>“<value>.events[<value>]”<br>“<value>.tracks[<value>]”<br>“invariants.axes[<value>]”<br>“<value> generator.falloff”<br>“<value>.drawOrder[<value>]”<br>“<value> generator.influence”<br>“invariants.detached[<value>]”<br>“{slot "<value>" / slots[<value>]}”<br>“invariants.consumerDrivenMix[<value>]”<br>“event "<value>"”<br>“<value>'s "from"”<br>“this motion spec”<br>“{absent / <value>}”<br>“{region / <value>}”<br>“<value>.keys[<value>]”<br>“<value> generator.soft”<br>“{linkedmesh / <value>}”<br>“<value>.deform[<value>]”<br>“<value>.sequence[<value>]”<br>“<value> generator (<value>)”<br>“{bone "<value>" / bones[<value>]}”<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. |
|
|
7752
|
+
| <a id="CE_KEYS_UNKNOWN_FIELD"></a>`CE_KEYS_UNKNOWN_FIELD` | “<where>: <what> has <strays.length === 1 ? 'a key' : `${strays.length} keys`> this compiler does not read: <named.join(', ')>. 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: <[...known].sort().join(', ')>.}”<br>“{"<key>" / "<key>" (did you mean <near.map((n) => `"${n}"`).join(', ')>?)}”<br>Also: “skin "<value>" slot "<value>" attachment "<value>"”<br>“<value> generator.bones[<value>]”<br>“{constraint "<value>" / constraints[<value>]}”<br>“<value>.transform.envelope”<br>“<value> weights[<value>][<value>]”<br>“invariants.deformMayFold[<value>]”<br>“animations."<value>"”<br>“<value>.offsets[<value>]”<br>“{stop "<value>" / stops[<value>]}”<br>“<value> properties."<value>".to."<value>"”<br>“this rig spec”<br>“<value>.derive”<br>“setup."<value>"”<br>“physics."<value>"”<br>“<value>'s "lower"”<br>“<value> "sequence"”<br>“<value> generator.bias”<br>“<value> generator.depth”<br>“<value>.events[<value>]”<br>“<value>.tracks[<value>]”<br>“invariants.axes[<value>]”<br>“<value> generator.falloff”<br>“<value>.drawOrder[<value>]”<br>“<value> generator.influence”<br>“invariants.detached[<value>]”<br>“{slot "<value>" / slots[<value>]}”<br>“invariants.consumerDrivenMix[<value>]”<br>“event "<value>"”<br>“<value>'s "from"”<br>“this motion spec”<br>“{absent / <value>}”<br>“{region / <value>}”<br>“<value>.keys[<value>]”<br>“<value> generator.soft”<br>“{linkedmesh / <value>}”<br>“<value>.deform[<value>]”<br>“<value>.sequence[<value>]”<br>“<value> generator (<value>)”<br>“{bone "<value>" / bones[<value>]}”<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
7753
|
| <a id="CE_KEYS_NUMBER_FLOAT32_FINITE"></a>`CE_KEYS_NUMBER_FLOAT32_FINITE` | “<where>: <place(path)> is <String(node)>; a number in this file is finite at float32 precision, at most ±<String(FLOAT32_MAX)>, 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.<why>”<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
7754
|
| <a id="CE_KEYS_FIELD_VALUE_TYPE"></a>`CE_KEYS_FIELD_VALUE_TYPE` | “<where>: <visit.name([key, ...wrong.tail])> is <typeFound(wrong.value)>; <wrong.required> 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
7755
|
| <a id="CE_KEYS_ENUM_VALUE"></a>`CE_KEYS_ENUM_VALUE` | “<where>: <visit.name([key])> is <JSON.stringify(value) ?? String(value)>; one of <rule.set.map((name) => JSON.stringify(name)).join(', ')> — <rule.readAs>”<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 +9091,23 @@ Draw order does not enter: a cell is covered or bare whichever piece is in front
|
|
|
9060
9091
|
An owner says which piece drew a ghost cell; it does not say which piece is over
|
|
9061
9092
|
or under the other, or whether that exposure should fail a consumer's policy.
|
|
9062
9093
|
|
|
9094
|
+
**Outside the overlap hull is outside the gap reading.** Both `gap` and its
|
|
9095
|
+
`gapOpen` partition are judged only inside the carried setup-overlap hull. Art
|
|
9096
|
+
that reaches beyond a joint disc, such as a drape over an elbow, can break
|
|
9097
|
+
outside that hull without a gap being reported. A gap of 0 is never a statement
|
|
9098
|
+
that the seam is closed outside the hull; inspect `ghost` and the actual frames
|
|
9099
|
+
too. Ghost independently names one-sided exposure of the carried setup overlap,
|
|
9100
|
+
not every visible break in the silhouette.
|
|
9101
|
+
|
|
9102
|
+
In the public [draped-sleeve fixture](https://github.com/rig-c/parts/blob/0628d7bc311a2b548edf7a11e8407546357d4b4d/fixtures/hybrid_limb.ts),
|
|
9103
|
+
measured on core `f273358` at both −30° and +30°, the hard cut reads `gap = 0`,
|
|
9104
|
+
`gapOpen = 0`, `ghost = 8` cells; the hybrid's two interfaces read 0 in all three
|
|
9105
|
+
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)
|
|
9106
|
+
shows the notch and flap excluded outside the hull, while ghost names different
|
|
9107
|
+
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)
|
|
9108
|
+
pins the inputs, commands and frame digest; these are seam cells, not display
|
|
9109
|
+
pixels. See [#1575](https://github.com/rig-c/core/issues/1575) for the reading.
|
|
9110
|
+
|
|
9063
9111
|
**Its reach, stated.** A piece folded right back over its partner carries the band
|
|
9064
9112
|
across the region it vacated, so a hull that wide reads the vacated cells its
|
|
9065
9113
|
partner's view still finds drawn. And no size is too small to print: on
|
|
@@ -12442,3 +12490,90 @@ calls, with the independent linked mesh selected, agree exactly. The API's
|
|
|
12442
12490
|
nonselected output is the stored source geometry through the unchanged setup
|
|
12443
12491
|
matrices; it does not borrow a different selection's deform. The control
|
|
12444
12492
|
checks both results and the source of the difference, without an epsilon.
|
|
12493
|
+
|
|
12494
|
+
## 15. Posing in place for a player — the runtime poser (stage 0)
|
|
12495
|
+
|
|
12496
|
+
rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
|
|
12497
|
+
(`src/core/`) is the reference: each pose is a pure function of the document,
|
|
12498
|
+
the skin and the time, returned as a fresh, name-keyed, detached record, and it
|
|
12499
|
+
is the poser `tools/core_gate.ts` holds to spine-core 4.3.13 at tolerance 0. The
|
|
12500
|
+
**runtime poser** (`src/runtime/`) is for a player drawing many skeletons a
|
|
12501
|
+
frame: it resolves every name to an index once at load and overwrites typed
|
|
12502
|
+
arrays in place, so a steady-state frame allocates nothing. It computes every
|
|
12503
|
+
value with the oracle's own formulas in the oracle's order of operations, and it
|
|
12504
|
+
is held equal to the oracle at tolerance 0 rather than trusted; the oracle is not
|
|
12505
|
+
changed for it. It is imported from `rig-c/runtime`, an **experimental** entry
|
|
12506
|
+
(RELEASING.md, *The import surface*): not promised until it stabilises, so a
|
|
12507
|
+
minor may reshape it and a dependant pins an exact version.
|
|
12508
|
+
|
|
12509
|
+
```ts
|
|
12510
|
+
import { readModel, underNoSkin } from 'rig-c/core';
|
|
12511
|
+
import { compileRuntime, RuntimePose, RuntimeRefusal } from 'rig-c/runtime'; // experimental
|
|
12512
|
+
|
|
12513
|
+
const view = underNoSkin(readModel(modelText));
|
|
12514
|
+
const model = compileRuntime(view, { ignore: ['events'] }); // throws RuntimeRefusal, every missing construct named
|
|
12515
|
+
const pose = new RuntimePose(model); // every buffer allocated here, once
|
|
12516
|
+
pose.setAnimation('walk'); // a refused animation throws its RuntimeRefusal
|
|
12517
|
+
pose.step(1 / 60); // or pose.apply(t) at an animation time
|
|
12518
|
+
pose.computeVertices();
|
|
12519
|
+
// pose.world: 6 numbers per bone (a, b, c, d, worldX, worldY — W_A … W_Y), y up
|
|
12520
|
+
// pose.light: 4 per slot; pose.shown: per slot an index into model.attachments, or −1
|
|
12521
|
+
// pose.vertices: model.attachments[i].count numbers at model.attachments[i].offset
|
|
12522
|
+
```
|
|
12523
|
+
|
|
12524
|
+
Each `model.attachments[i]` also carries readonly `skin` and `placeholder`: the
|
|
12525
|
+
exact resolved table identity `(skin, model.slots[attachment.slot].name, placeholder)`.
|
|
12526
|
+
The display `name` may be shared by different placeholders; it is never a table
|
|
12527
|
+
key to guess the source from. These load-time strings do not change the pose path.
|
|
12528
|
+
|
|
12529
|
+
| | what stage 0 poses |
|
|
12530
|
+
| --- | --- |
|
|
12531
|
+
| 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) |
|
|
12532
|
+
| 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 |
|
|
12533
|
+
| 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 |
|
|
12534
|
+
| vertices | regions, meshes, weighted meshes and linked meshes, written into the pose's buffer |
|
|
12535
|
+
| 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) |
|
|
12536
|
+
|
|
12537
|
+
**Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
|
|
12538
|
+
every construct met, each `{ feature, detail, animation }`, and the message
|
|
12539
|
+
reads `not posed by the runtime poser (stage 0), N construct(s) missing:
|
|
12540
|
+
<feature>: <detail>; …`. A model-level feature refuses the whole model; an
|
|
12541
|
+
animation-level one refuses that animation alone (`model.refused`;
|
|
12542
|
+
`model.animation(name)` and `setAnimation(name)` throw its refusal) and every
|
|
12543
|
+
other animation is posed. Fall back to the oracle (`openTrack`) for what is
|
|
12544
|
+
refused: nothing is posed approximately.
|
|
12545
|
+
|
|
12546
|
+
| feature | refuses | what it is |
|
|
12547
|
+
| --- | --- | --- |
|
|
12548
|
+
| `skin` | the model | a skin other than `default` |
|
|
12549
|
+
| `path-constraint`, `physics-constraint`, `slider-constraint` | the model | a constraint of that kind |
|
|
12550
|
+
| `local-transform`, `additive-transform` | the model | a transform constraint with `localSource` or `localTarget`, or `additive` |
|
|
12551
|
+
| `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) |
|
|
12552
|
+
| `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 |
|
|
12553
|
+
| `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` |
|
|
12554
|
+
| `skin-conflict` | as above | a placeholder the skins fill differently |
|
|
12555
|
+
| `oracle-absent` | the model | a pose the oracle itself leaves out (`constraintsAbsentWhy`) |
|
|
12556
|
+
| `inherit-timeline`, `deform-timeline`, `sequence-timeline`, `draw-order-timeline`, `dark-colour-timeline` | the animation | a timeline of that kind (`dark-colour` is `rgba2` / `rgb2`) |
|
|
12557
|
+
| `events` | the animation, unless `ignore: ['events']` | event keys. Events move no pose, so a consumer that reads none may ask for them to be ignored and is handed a poser that fires none; without the request a dropped cue would be silent |
|
|
12558
|
+
|
|
12559
|
+
A call it cannot answer throws `RuntimeInputError`, naming the call:
|
|
12560
|
+
`apply: no animation is set (setAnimation first)`, `animation "x" is not one of
|
|
12561
|
+
this document's [...]`, `apply: the time NaN is not finite`.
|
|
12562
|
+
|
|
12563
|
+
**The gate.** `tools/core_gate.ts` runs a third dumper (`runtimeDump` in
|
|
12564
|
+
`tools/pose_oracle.ts`) over every row it builds, at the oracle's samples, and
|
|
12565
|
+
compares its document with the oracle's in full doubles at tolerance 0 twice:
|
|
12566
|
+
through `compare` and cell by cell (which reads the bones `compare`'s
|
|
12567
|
+
ill-conditioned rule leaves out). One `RUNTIME` line per row reads `IDENTICAL`
|
|
12568
|
+
(covered whole and equal), `SKIP` (refused at load, or animations or events left
|
|
12569
|
+
out — every one named), `DIFF` (the first value that differs) or `REFUSED`; then
|
|
12570
|
+
one `RUNTIME SKIP feature <name>` line per feature with the rows it refused, a
|
|
12571
|
+
`HOLE` line when no row is covered whole or nothing was compared, and `RUNTIME
|
|
12572
|
+
GREEN|RED`, counting rows covered whole, covered in part and refused at load, and
|
|
12573
|
+
the poses and values compared. A `DIFF` or `REFUSED` turns the run RED; a SKIP
|
|
12574
|
+
never counts as a pass. The page UVs are the atlas's and are compared by neither
|
|
12575
|
+
side here. The selftest's `runtime-poser` suite holds the gate to its plants (a
|
|
12576
|
+
1e-6° bone perturbation, a weighted binding bound to the wrong bone index, a
|
|
12577
|
+
refused feature smuggled through — each caught by name), a seeded population of
|
|
12578
|
+
every construct above, the looping step against `openTrack`, and a steady-state
|
|
12579
|
+
frame measured at 0 bytes with the collector off.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.71.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",
|
package/src/assertions/report.ts
CHANGED
|
@@ -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
|
|
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
|
/**
|
package/src/compile.ts
CHANGED
|
@@ -3015,6 +3015,46 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3015
3015
|
// The same for a `soft` mask's per-vertex level, which a deform envelope's
|
|
3016
3016
|
// `mask` reads (issue #1413). Never emitted either.
|
|
3017
3017
|
const attachmentSoftMasks = new Map<string, DeformSoftMask>();
|
|
3018
|
+
// `skin/slot/attachment` -> per-axis narrowing mode when a deform turn key on
|
|
3019
|
+
// that attachment declares `narrowing: false` (issue #1584), so `sampleMeshDepth`
|
|
3020
|
+
// evaluates `turnCeiling` with the `asin(|A₀ / A_axis|)` closed form that
|
|
3021
|
+
// matches `A39` on the keyed turn.
|
|
3022
|
+
const attachmentTurnNarrowing = new Map<string, { yaw?: boolean; pitch?: boolean }>();
|
|
3023
|
+
const attachmentTurnNarrowingBy = new Map<string, { yaw?: { mode: boolean; anim: string }; pitch?: { mode: boolean; anim: string } }>();
|
|
3024
|
+
withMotionSource(() => {
|
|
3025
|
+
for (const [turnAnimName, turnAnim] of Object.entries(motion.animations ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
|
3026
|
+
for (const turnTrack of turnAnim.deform ?? []) {
|
|
3027
|
+
const turnDeformSkin = turnTrack.skin ?? DEFAULT_SKIN;
|
|
3028
|
+
const turnTriple = `${turnDeformSkin}/${turnTrack.slot}/${turnTrack.attachment}`;
|
|
3029
|
+
for (const turnKey of turnTrack.keys ?? []) {
|
|
3030
|
+
const turnSpec = turnKey.transform as { kind?: unknown; narrowing?: unknown } | undefined;
|
|
3031
|
+
if (turnSpec && (turnSpec.kind === 'yaw' || turnSpec.kind === 'pitch')) {
|
|
3032
|
+
const turnAxis = turnSpec.kind;
|
|
3033
|
+
const turnNarrowMode = turnSpec.narrowing !== false;
|
|
3034
|
+
const turnPrev = attachmentTurnNarrowingBy.get(turnTriple)?.[turnAxis];
|
|
3035
|
+
if (turnPrev !== undefined && turnPrev.mode !== turnNarrowMode) {
|
|
3036
|
+
const turnFalseAnim = turnNarrowMode ? turnPrev.anim : turnAnimName;
|
|
3037
|
+
const turnTrueAnim = turnNarrowMode ? turnAnimName : turnPrev.anim;
|
|
3038
|
+
throw new CompileError(
|
|
3039
|
+
`deform ${turnTriple} keys "${turnAxis}" with narrowing=false in animation "${turnFalseAnim}" and with narrowing=true in animation "${turnTrueAnim}". ` +
|
|
3040
|
+
'An attachment\'s turn ceiling on one axis is either asin(|A₀ / A_axis|) (without narrowing) or atan(|A₀ / A_axis|) (with narrowing) — ' +
|
|
3041
|
+
`mixing both on one attachment and axis would report one ceiling where two apply. Use one "narrowing" setting across every "${turnAxis}" key on ${turnTriple}.`,
|
|
3042
|
+
'CE_COMPILE_DEFORM_TURN_NARROWING_MIXED',
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
3045
|
+
const curBy = attachmentTurnNarrowingBy.get(turnTriple) ?? {};
|
|
3046
|
+
if (curBy[turnAxis] === undefined) curBy[turnAxis] = { mode: turnNarrowMode, anim: turnAnimName };
|
|
3047
|
+
attachmentTurnNarrowingBy.set(turnTriple, curBy);
|
|
3048
|
+
if (!turnNarrowMode) {
|
|
3049
|
+
const cur = attachmentTurnNarrowing.get(turnTriple) ?? {};
|
|
3050
|
+
cur[turnAxis] = false;
|
|
3051
|
+
attachmentTurnNarrowing.set(turnTriple, cur);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
});
|
|
3018
3058
|
|
|
3019
3059
|
for (const rigSlot of rig.slots) {
|
|
3020
3060
|
const part = partBySlot.get(rigSlot.name);
|
|
@@ -3183,6 +3223,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3183
3223
|
imagesDir,
|
|
3184
3224
|
depths: attachmentDepths,
|
|
3185
3225
|
softMasks: attachmentSoftMasks,
|
|
3226
|
+
turnNarrowing: attachmentTurnNarrowing.get(`${skinName}/${rigSlot.name}/${placeholder}`),
|
|
3186
3227
|
slotNames: new Set(rig.slots.map((s) => s.name)),
|
|
3187
3228
|
atlasRegions: atlasIn === null ? null : atlasIn.byName,
|
|
3188
3229
|
links: pendingLinks,
|
|
@@ -4333,6 +4374,12 @@ interface AttachmentContext {
|
|
|
4333
4374
|
* emitted, for `depths`' reason.
|
|
4334
4375
|
*/
|
|
4335
4376
|
softMasks: Map<string, DeformSoftMask>;
|
|
4377
|
+
/**
|
|
4378
|
+
* Per-axis `narrowing` mode when a deform turn key on this attachment states
|
|
4379
|
+
* `"narrowing": false` (issue #1584), so `sampleMeshDepth` computes the turn
|
|
4380
|
+
* ceiling with `asin(|A₀ / A_axis|)` on that axis.
|
|
4381
|
+
*/
|
|
4382
|
+
turnNarrowing?: { yaw?: boolean; pitch?: boolean };
|
|
4336
4383
|
/** Every slot the rig declares — a clipping attachment's `end` resolves here. */
|
|
4337
4384
|
slotNames: Set<string>;
|
|
4338
4385
|
/**
|
|
@@ -6324,6 +6371,7 @@ function sampleMeshDepth(
|
|
|
6324
6371
|
points.map(([px, py]) => toBind(px, py)),
|
|
6325
6372
|
z,
|
|
6326
6373
|
triangles,
|
|
6374
|
+
ctx.turnNarrowing === undefined ? undefined : { narrowing: ctx.turnNarrowing },
|
|
6327
6375
|
),
|
|
6328
6376
|
},
|
|
6329
6377
|
};
|
package/src/core/animation.ts
CHANGED
|
@@ -189,11 +189,12 @@ import {
|
|
|
189
189
|
foldInheritMode,
|
|
190
190
|
gridRound,
|
|
191
191
|
readBlend,
|
|
192
|
-
readColour,
|
|
193
192
|
shownAttachment,
|
|
194
193
|
shownRow,
|
|
195
194
|
slidersKeyingWhy,
|
|
196
195
|
sourceOfDoc,
|
|
196
|
+
viewColours,
|
|
197
|
+
WHITE,
|
|
197
198
|
type CompiledDocument,
|
|
198
199
|
type CoreAnimation,
|
|
199
200
|
type CoreBoneRow,
|
|
@@ -834,12 +835,15 @@ export function posedSlots(
|
|
|
834
835
|
/** Filled with each slot's placeholder once the sample's own timelines have moved it, before the sliders — what the attachments are posed from (`attachmentStates`). */
|
|
835
836
|
placeholders?: Map<string, string | null>,
|
|
836
837
|
): { rows: CoreSlotRow[]; conflicts: string[] } {
|
|
837
|
-
|
|
838
|
+
// The view's stated colours, read once (`viewColours`); a planted reader of the light colour is called as it always was.
|
|
839
|
+
const stated = viewColours(doc);
|
|
840
|
+
const colour = plant.colour ?? stated;
|
|
838
841
|
const byName = new Map(timelines.slots.map((s) => [s.name, s]));
|
|
839
842
|
return slotRowsFrom(doc, plant, sliders, placeholders, (slot, live) => {
|
|
840
843
|
let placeholder = slot.setup;
|
|
841
|
-
const
|
|
842
|
-
const
|
|
844
|
+
const setupLight = slot.color === undefined ? WHITE : colour(slot.color);
|
|
845
|
+
const light = [setupLight[0], setupLight[1], setupLight[2], setupLight[3]];
|
|
846
|
+
const setupDark = slot.dark === undefined ? null : stated(slot.dark);
|
|
843
847
|
const dark = setupDark === null ? null : [setupDark[0], setupDark[1], setupDark[2]];
|
|
844
848
|
for (const tl of live ? (byName.get(slot.name)?.timelines ?? []) : []) {
|
|
845
849
|
if (tl.kind === 'attachment') {
|
|
@@ -848,7 +852,6 @@ export function posedSlots(
|
|
|
848
852
|
continue;
|
|
849
853
|
}
|
|
850
854
|
const v = valuesAt(tl.keys, t, plant);
|
|
851
|
-
const setupLight = slot.color === undefined ? [1, 1, 1, 1] : colour(slot.color);
|
|
852
855
|
const channels = SLOT_COLOUR_TIMELINES[tl.kind].channels;
|
|
853
856
|
channels.forEach((name, i) => {
|
|
854
857
|
const at = { r: 0, g: 1, b: 2, a: 3, r2: 4, g2: 5, b2: 6 }[name];
|
|
@@ -955,15 +958,22 @@ export function posedBoneRows(doc: CompiledDocument, timelines: CoreAnimationTim
|
|
|
955
958
|
/** `posedBoneRows`, with the world transforms the rows were read off — what the sample's attachments are posed through. */
|
|
956
959
|
export function posedBoneWorld(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant = {}, constraints?: CoreConstraintTimelines, sliders?: SliderApplication[], step?: PoseStep): { rows: CoreBoneRow[]; world: Map<string, CoreWorld> } {
|
|
957
960
|
const posed = posedBoneStep(doc, timelines, t, plant, constraints, sliders, step);
|
|
958
|
-
|
|
961
|
+
const stage = plant.stage;
|
|
962
|
+
if (stage !== undefined) stage.begin('readout');
|
|
963
|
+
const rows = stepRows(posed);
|
|
964
|
+
if (stage !== undefined) stage.end('readout');
|
|
965
|
+
return { rows, world: posed.world };
|
|
959
966
|
}
|
|
960
967
|
|
|
961
968
|
/**
|
|
962
969
|
* `posedBoneWorld`'s world transforms without its rows (issue #1179, the
|
|
963
970
|
* second part): the same pose by the same operations in the same order, since
|
|
964
971
|
* `stepRows` only reads the world once it is posed. A walk reads `world` and
|
|
965
|
-
* no row, so the scan's
|
|
966
|
-
*
|
|
972
|
+
* no row, so every walk asks for this — the scan's (`scanWalkIn` in
|
|
973
|
+
* `./raw.ts`, A10's), the raw entry's batch walks and the live track's step.
|
|
974
|
+
* The rows were the only thing the other variant adds, and their rounding
|
|
975
|
+
* (`rawNumber`, `keptNumber`) refuses nothing, so leaving them out moves no
|
|
976
|
+
* number and no refusal of a walk.
|
|
967
977
|
*/
|
|
968
978
|
export function posedBoneWorldAlone(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step: PoseStep | undefined): Map<string, CoreWorld> {
|
|
969
979
|
return posedBoneStep(doc, timelines, t, plant, constraints, sliders, step).world;
|
|
@@ -997,20 +1007,34 @@ function stepRows(posed: PosedStep): CoreBoneRow[] {
|
|
|
997
1007
|
|
|
998
1008
|
/** `posedBoneWorld` up to its rows, refusing a bone the evaluator gave no transform (`PosedStep`). */
|
|
999
1009
|
function posedBoneStep(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step: PoseStep | undefined): PosedStep {
|
|
1010
|
+
const stage = plant.stage;
|
|
1011
|
+
if (stage !== undefined) stage.begin('timeline');
|
|
1000
1012
|
const bones = posedBones(doc, timelines, t, plant);
|
|
1013
|
+
if (stage !== undefined) stage.end('timeline');
|
|
1001
1014
|
// One step of the stepped phase (issue #956, `./constraints_physics.ts`): the physics records posed by their timelines and stepped under the walk's context; the deformed curve a path walks as above.
|
|
1002
|
-
if (constraints !== undefined && step !== undefined)
|
|
1015
|
+
if (constraints !== undefined && step !== undefined) {
|
|
1016
|
+
return steppedLocals(doc, bones, plant, () => pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, sliders, step);
|
|
1017
|
+
}
|
|
1003
1018
|
const active = activeBones(doc);
|
|
1004
1019
|
// The caller's adjustment, after the timelines and before anything reads the locals (`LocalAdjust`).
|
|
1005
|
-
if (step?.adjustment !== undefined)
|
|
1006
|
-
|
|
1020
|
+
if (step?.adjustment !== undefined) {
|
|
1021
|
+
if (stage !== undefined) stage.begin('timeline');
|
|
1022
|
+
adjustLocals(doc, bones, step.adjustment.callback, step.adjustment.context);
|
|
1023
|
+
if (stage !== undefined) stage.end('timeline');
|
|
1024
|
+
}
|
|
1025
|
+
if (stage !== undefined) stage.begin('world');
|
|
1026
|
+
const initialWorld = (plant.evaluate ?? worldTransforms)(bones, active);
|
|
1027
|
+
if (stage !== undefined) stage.end('world');
|
|
1028
|
+
let world = initialWorld;
|
|
1007
1029
|
if (constraints !== undefined) {
|
|
1030
|
+
if (stage !== undefined) stage.begin('constraints');
|
|
1008
1031
|
const setupRecords = constraintRecords(doc);
|
|
1009
1032
|
// A path walks the curve the sample's deform timelines left on its slot (`./deform.ts`); a slider's deform of a walked path leaves the bones out (`pathAnimationsWhy`).
|
|
1010
1033
|
const records = pathDeformed(doc, posedRecords(setupRecords, constraints, t), timelines, t, plant);
|
|
1011
1034
|
// A path constraint may read a slot bone the runtime has not yet brought up to date in this pass, as the previous pass left it (`./constraints_path.ts`, *Which slot bone*); `poseAnimations` holds that pass to the setup pose's reading.
|
|
1012
1035
|
const previous = setupRecords.some((r) => r.kind === 'path') ? applyConstraints(doc.bones, (plant.evaluate ?? worldTransforms)(doc.bones, active), active, plant.constraints ? plant.constraints(setupRecords) : setupRecords, null, undefined, undefined, undefined, solverRules(plant.solver)) : null;
|
|
1013
|
-
world = applyConstraints(bones,
|
|
1036
|
+
world = applyConstraints(bones, initialWorld, active, plant.constraints ? plant.constraints(records) : records, previous, sliders, undefined, undefined, solverRules(plant.solver));
|
|
1037
|
+
if (stage !== undefined) stage.end('constraints');
|
|
1014
1038
|
}
|
|
1015
1039
|
for (const b of bones) if (world.get(b.name) === undefined) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
|
|
1016
1040
|
return { bones, world, active, round: plant.round ?? gridRound };
|
|
@@ -1027,27 +1051,35 @@ function posedBoneStep(doc: CompiledDocument, timelines: CoreAnimationTimelines,
|
|
|
1027
1051
|
* past the timelines for both.
|
|
1028
1052
|
*/
|
|
1029
1053
|
function steppedLocals(doc: CompiledDocument, bones: ModelBone[], plant: TimelinePlant, records: () => CoreConstraintRecord[], physicsKeyed: NonNullable<CoreConstraintTimelines['physicsKeyed']>, t: number, sliders: SliderApplication[] | undefined, step: PoseStep): PosedStep {
|
|
1054
|
+
const stage = plant.stage;
|
|
1030
1055
|
const active = activeBones(doc);
|
|
1031
1056
|
// The caller's adjustment, after the timelines and before anything reads the locals (`LocalAdjust`).
|
|
1032
|
-
if (step.adjustment !== undefined)
|
|
1033
|
-
|
|
1057
|
+
if (step.adjustment !== undefined) {
|
|
1058
|
+
if (stage !== undefined) stage.begin('timeline');
|
|
1059
|
+
adjustLocals(doc, bones, step.adjustment.callback, step.adjustment.context);
|
|
1060
|
+
if (stage !== undefined) stage.end('timeline');
|
|
1061
|
+
}
|
|
1062
|
+
if (stage !== undefined) stage.begin('world');
|
|
1063
|
+
const initialWorld = (plant.evaluate ?? worldTransforms)(bones, active);
|
|
1064
|
+
if (stage !== undefined) {
|
|
1065
|
+
stage.end('world');
|
|
1066
|
+
stage.begin('constraints');
|
|
1067
|
+
}
|
|
1034
1068
|
const stepped = stepPhysicsRecords(records(), physicsKeyed, t, active, step.ctx, step.before);
|
|
1035
|
-
world = applyConstraints(bones,
|
|
1069
|
+
const world = applyConstraints(bones, initialWorld, active, plant.constraints ? plant.constraints(stepped) : stepped, null, sliders, step.ctx, undefined, solverRules(plant.solver));
|
|
1070
|
+
if (stage !== undefined) stage.end('constraints');
|
|
1036
1071
|
for (const b of bones) if (world.get(b.name) === undefined) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
|
|
1037
1072
|
return { bones, world, active, round: plant.round ?? gridRound };
|
|
1038
1073
|
}
|
|
1039
1074
|
|
|
1040
1075
|
/**
|
|
1041
|
-
* `steppedLocals`' world transforms, the oracle's rows
|
|
1042
|
-
* `
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
* constraint timeline — and no physics timeline keyed.
|
|
1076
|
+
* `steppedLocals`' world transforms, without the oracle's rows (as
|
|
1077
|
+
* `posedBoneWorldAlone` poses them): the live tracks' entry to the stepped
|
|
1078
|
+
* phase (`./tracks.ts`). The document's constraint records as they stand —
|
|
1079
|
+
* the live tracks mix no constraint timeline — and no physics timeline keyed.
|
|
1046
1080
|
*/
|
|
1047
|
-
export function steppedLocalsWorld(doc: CompiledDocument, bones: ModelBone[], plant: TimelinePlant, sliders: SliderApplication[], step: PoseStep
|
|
1048
|
-
|
|
1049
|
-
if (rows) stepRows(posed);
|
|
1050
|
-
return posed.world;
|
|
1081
|
+
export function steppedLocalsWorld(doc: CompiledDocument, bones: ModelBone[], plant: TimelinePlant, sliders: SliderApplication[], step: PoseStep): Map<string, CoreWorld> {
|
|
1082
|
+
return steppedLocals(doc, bones, plant, () => constraintRecords(doc), [], 0, sliders, step).world;
|
|
1051
1083
|
}
|
|
1052
1084
|
|
|
1053
1085
|
/** Each path record with the curve its slot shows deformed by the sample's own deform timelines at `t` (`./deform.ts`), or as it was. */
|