@rig-c/core 2.71.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 +85 -11
- package/package.json +1 -1
- package/src/compile.ts +78 -24
- package/src/core/events.ts +17 -5
- package/src/deformgen.ts +2 -2
- package/src/meshcompare.ts +146 -22
- package/src/rig.ts +7 -4
- package/src/runtime/index.ts +8 -4
- package/src/runtime/model.ts +37 -20
- package/src/runtime/pose.ts +41 -1
package/docs/AUTHORING.md
CHANGED
|
@@ -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
|
|
2939
|
-
|
|
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 |
|
|
@@ -7330,7 +7360,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7330
7360
|
| <a id="CE_COMPILE_DEPTH_IMAGE_MISSING"></a>`CE_COMPILE_DEPTH_IMAGE_MISSING` | “<value>: 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. |
|
|
7331
7361
|
| <a id="CE_COMPILE_DEPTH_NEAR_INVALID"></a>`CE_COMPILE_DEPTH_NEAR_INVALID` | “<value>: the depth block says "near": <value>; 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. |
|
|
7332
7362
|
| <a id="CE_COMPILE_DEPTH_TONE_SCALE_INVALID"></a>`CE_COMPILE_DEPTH_TONE_SCALE_INVALID` | “<value>: "zScale" is <value>; it is a finite number of world units”<br>“<value>: "zScale" is <value>; 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>“<value>: depth tone "<value>" is <value>; it is a finite number”<br>“<value>: depth tone "contrast" is <value>; 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>“<value>: depth tone "gamma" is <value>; 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. |
|
|
7333
|
-
| <a id="CE_COMPILE_DEPTH_COVERAGE_INCOMPLETE"></a>`CE_COMPILE_DEPTH_COVERAGE_INCOMPLETE` | “<value>: the depth map "<value>" does not cover <value> of the mesh's <value> vertices — the first is vertex <value> at (<value>, <value>)[ / in the page's texels, pixel (<value>, <value>) of the drawing-sized sheet]. <value> 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` | “<value>: the depth map "<value>" does not cover <value> of the mesh's <value> vertices — the first is vertex <value> at (<value>, <value>)[ / in the page's texels, pixel (<value>, <value>) of the drawing-sized sheet]. <value> 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. |
|
|
7334
7364
|
| <a id="CE_COMPILE_MESH_BONE_SETUP_TRANSFORM_MISSING"></a>`CE_COMPILE_MESH_BONE_SETUP_TRANSFORM_MISSING` | “<value>: bone "<value>" has no setup transform” | Correct the bone/setup hierarchy; report a missing compiled transform for a declared bone. |
|
|
7335
7365
|
| <a id="CE_COMPILE_MESH_BONE_NOT_DECLARED"></a>`CE_COMPILE_MESH_BONE_NOT_DECLARED` | “<value>: bone "<value>" is not in the rig's bone list” | Use a bone name declared in the rig. |
|
|
7336
7366
|
| <a id="CE_COMPILE_GREY_SHEET_IMAGE_MISSING"></a>`CE_COMPILE_GREY_SHEET_IMAGE_MISSING` | “<value>: the "<value>" block has no image to read” | State the image in the named depth/soft block. |
|
|
@@ -7467,7 +7497,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7467
7497
|
| <a id="CE_COMPILE_SEQUENCE_KEY_INDEX_OUTSIDE_SERIES"></a>`CE_COMPILE_SEQUENCE_KEY_INDEX_OUTSIDE_SERIES` | “<value> (t=<value>): index <value> is past the end of a <value>-frame series (frames 0 to <value>). `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. |
|
|
7468
7498
|
| <a id="CE_COMPILE_DEFORM_TRANSFORM_AND_VERTICES"></a>`CE_COMPILE_DEFORM_TRANSFORM_AND_VERTICES` | “<value> (t=<value>): 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 "<value>" deform <value>/<value>/<value>”<br>Here the values named in the message include: “default”. | Use a transform model or authored vertex offsets. |
|
|
7469
7499
|
| <a id="CE_COMPILE_DEFORM_TRANSFORM_START_INDEX"></a>`CE_COMPILE_DEFORM_TRANSFORM_START_INDEX` | “<value> (t=<value>): the key states a "transform" and a start index. A transform is a model of the whole attachment and is evaluated over all <value> 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 "<value>" deform <value>/<value>/<value>”<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. |
|
|
7470
|
-
| <a id="CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED"></a>`CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED` | “<value> (t=<value>): a "transform" states where every vertex of this attachment goes, and this one has a vertex the arithmetic cannot place — <value>. Fix the weights, or write the bind-space pairs yourself and start the run with "offset".”<br>“vertex <value>'s <value> weights sum to <value> 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 "<value>" deform <value>/<value>/<value>”<br>Here the values named in the message include: “default
|
|
7500
|
+
| <a id="CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED"></a>`CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED` | “<value> (t=<value>): a "transform" states where every vertex of this attachment goes, and this one has a vertex the arithmetic cannot place — <value>. Fix the weights, or write the bind-space pairs yourself and start the run with "offset".”<br>“vertex <value>'s <value> weights sum to <value> 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 "<value>" deform <value>/<value>/<value>”<br>Here the values named in the message include: “default”.<br>“<value>: depth needs resolved setup positions — <value>” | Fix weights so setup vertices can be positioned, or author bind-space pairs using offset. |
|
|
7471
7501
|
| <a id="CE_COMPILE_DEFORM_TRANSFORM_OFFSET_COUNT"></a>`CE_COMPILE_DEFORM_TRANSFORM_OFFSET_COUNT` | “<value> (t=<value>): the transform produced <value> numbers for <value> vertices”<br>Also: “animation "<value>" deform <value>/<value>/<value>”<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. |
|
|
7472
7502
|
| <a id="CE_COMPILE_DEFORM_TRANSFORM_RUN_LENGTH"></a>`CE_COMPILE_DEFORM_TRANSFORM_RUN_LENGTH` | “<value> (t=<value>): the transform expanded to <value> numbers and this attachment's deform array is <value> long”<br>Also: “animation "<value>" deform <value>/<value>/<value>”<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. |
|
|
7473
7503
|
| <a id="CE_COMPILE_DEFORM_START_SOURCES_CONFLICT"></a>`CE_COMPILE_DEFORM_START_SOURCES_CONFLICT` | “<value> (t=<value>): 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 "<value>" deform <value>/<value>/<value>”<br>Here the values named in the message include: “default”. | Use offset or fromVertex, rather than both. |
|
|
@@ -7645,7 +7675,9 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7645
7675
|
| <a id="CE_DEFORM_TRANSFORM_OBJECT"></a>`CE_DEFORM_TRANSFORM_OBJECT` | “<where>: "transform" is <JSON.stringify(transform)>; it is an object with a "kind"” | Write transform as an object with a kind. |
|
|
7646
7676
|
| <a id="CE_DEFORM_TRANSFORM_KIND"></a>`CE_DEFORM_TRANSFORM_KIND` | “<where>: "transform" has kind <JSON.stringify(kind)>; this spec evaluates <DEFORM_TRANSFORM_KINDS.join(', ')>. 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. |
|
|
7647
7677
|
| <a id="CE_DEFORM_DEPTH_RADIUS_CONFLICT"></a>`CE_DEFORM_DEPTH_RADIUS_CONFLICT` | “<where>: transform <kind> states both "depth": true and a radius <JSON.stringify(t.radius)>. 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. |
|
|
7648
|
-
| <a id="CE_DEFORM_DEPTH_MAP_REQUIRED"></a>`CE_DEFORM_DEPTH_MAP_REQUIRED` | “<where>: transform <kind> says "depth": true and this attachment has no depth map. The map is named on
|
|
7678
|
+
| <a id="CE_DEFORM_DEPTH_MAP_REQUIRED"></a>`CE_DEFORM_DEPTH_MAP_REQUIRED` | “<where>: transform <kind> 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` | “<where>: 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` | “<where>: an authored mesh with "depth" needs an "image" to locate the part's pixel grid” | Name the part image the sheet describes. |
|
|
7649
7681
|
| <a id="CE_DEFORM_DEPTH_SAMPLE_COUNT"></a>`CE_DEFORM_DEPTH_SAMPLE_COUNT` | “<where>: transform <kind> has <depth.length> sampled depths for <count> vertices” | Report this compiler defect with the inputs and printed sampled-depth and vertex counts; do not edit generated samples. |
|
|
7650
7682
|
| <a id="CE_DEFORM_RADIUS_POSITIVE"></a>`CE_DEFORM_RADIUS_POSITIVE` | “<where>: transform <kind> has radius <radius>; it is the radius of the cylinder the part is painted on, so a positive number” | State a positive cylinder radius. |
|
|
7651
7683
|
| <a id="CE_DEFORM_VERTEX_OUTSIDE_CYLINDER"></a>`CE_DEFORM_VERTEX_OUTSIDE_CYLINDER` | “<where>: transform <kind> has radius <radius>, and vertex <v> sits at <kind === 'yaw' ? 'x' : 'y'>=<setup[2 * v + along]>, which is <round(Math.abs(u) - radius)> past it (about=<about>). 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. |
|
|
@@ -7749,7 +7781,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7749
7781
|
|
|
7750
7782
|
| Code | Fixed message text and alternatives | What to change |
|
|
7751
7783
|
| --- | --- | --- |
|
|
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. |
|
|
7784
|
+
| <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> 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. |
|
|
7753
7785
|
| <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. |
|
|
7754
7786
|
| <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. |
|
|
7755
7787
|
| <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. |
|
|
@@ -12491,7 +12523,7 @@ nonselected output is the stored source geometry through the unchanged setup
|
|
|
12491
12523
|
matrices; it does not borrow a different selection's deform. The control
|
|
12492
12524
|
checks both results and the source of the difference, without an epsilon.
|
|
12493
12525
|
|
|
12494
|
-
## 15. Posing in place for a player — the runtime poser (stage
|
|
12526
|
+
## 15. Posing in place for a player — the runtime poser (stage 1)
|
|
12495
12527
|
|
|
12496
12528
|
rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
|
|
12497
12529
|
(`src/core/`) is the reference: each pose is a pure function of the document,
|
|
@@ -12526,17 +12558,43 @@ exact resolved table identity `(skin, model.slots[attachment.slot].name, placeho
|
|
|
12526
12558
|
The display `name` may be shared by different placeholders; it is never a table
|
|
12527
12559
|
key to guess the source from. These load-time strings do not change the pose path.
|
|
12528
12560
|
|
|
12529
|
-
| | what stage
|
|
12561
|
+
| | what stage 1 poses |
|
|
12530
12562
|
| --- | --- |
|
|
12531
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) |
|
|
12532
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 |
|
|
12533
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 |
|
|
12534
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 |
|
|
12535
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) |
|
|
12536
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
|
+
|
|
12537
12595
|
**Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
|
|
12538
12596
|
every construct met, each `{ feature, detail, animation }`, and the message
|
|
12539
|
-
reads `not posed by the runtime poser (stage
|
|
12597
|
+
reads `not posed by the runtime poser (stage 1), N construct(s) missing:
|
|
12540
12598
|
<feature>: <detail>; …`. A model-level feature refuses the whole model; an
|
|
12541
12599
|
animation-level one refuses that animation alone (`model.refused`;
|
|
12542
12600
|
`model.animation(name)` and `setAnimation(name)` throw its refusal) and every
|
|
@@ -12545,7 +12603,6 @@ refused: nothing is posed approximately.
|
|
|
12545
12603
|
|
|
12546
12604
|
| feature | refuses | what it is |
|
|
12547
12605
|
| --- | --- | --- |
|
|
12548
|
-
| `skin` | the model | a skin other than `default` |
|
|
12549
12606
|
| `path-constraint`, `physics-constraint`, `slider-constraint` | the model | a constraint of that kind |
|
|
12550
12607
|
| `local-transform`, `additive-transform` | the model | a transform constraint with `localSource` or `localTarget`, or `additive` |
|
|
12551
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) |
|
|
@@ -12554,7 +12611,6 @@ refused: nothing is posed approximately.
|
|
|
12554
12611
|
| `skin-conflict` | as above | a placeholder the skins fill differently |
|
|
12555
12612
|
| `oracle-absent` | the model | a pose the oracle itself leaves out (`constraintsAbsentWhy`) |
|
|
12556
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`) |
|
|
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
12614
|
|
|
12559
12615
|
A call it cannot answer throws `RuntimeInputError`, naming the call:
|
|
12560
12616
|
`apply: no animation is set (setAnimation first)`, `animation "x" is not one of
|
|
@@ -12577,3 +12633,21 @@ side here. The selftest's `runtime-poser` suite holds the gate to its plants (a
|
|
|
12577
12633
|
refused feature smuggled through — each caught by name), a seeded population of
|
|
12578
12634
|
every construct above, the looping step against `openTrack`, and a steady-state
|
|
12579
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.
|
|
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": {
|
package/src/compile.ts
CHANGED
|
@@ -3011,7 +3011,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3011
3011
|
const meshes: CompileResult['meshes'] = [];
|
|
3012
3012
|
// `skin/slot/attachment` -> per-vertex z, for the attachments that named a
|
|
3013
3013
|
// depth map. Read at deform-key time; never emitted. See `AttachmentContext`.
|
|
3014
|
-
const attachmentDepths = new Map<string, number[]>();
|
|
3014
|
+
const attachmentDepths = new Map<string, number[] | { z: number[]; geometry: DeformGeometry }>();
|
|
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>();
|
|
@@ -3729,6 +3729,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3729
3729
|
if (deformTimelines.get(skinName)?.get(track.slot)?.has(track.attachment)) {
|
|
3730
3730
|
throw new CompileError(`${at}: two deform timelines on one attachment; merge them into one`, 'CE_COMPILE_DEFORM_TIMELINE_DUPLICATE');
|
|
3731
3731
|
}
|
|
3732
|
+
const sampledDepth = attachmentDepths.get(`${skinName}/${track.slot}/${track.attachment}`);
|
|
3732
3733
|
const keys = compileDeformTrack(
|
|
3733
3734
|
track,
|
|
3734
3735
|
motion,
|
|
@@ -3736,10 +3737,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3736
3737
|
anim.duration,
|
|
3737
3738
|
{
|
|
3738
3739
|
...deformGeometryOf(attachment, at, transforms),
|
|
3739
|
-
depth:
|
|
3740
|
+
depth: (Array.isArray(sampledDepth) ? sampledDepth : sampledDepth?.z) ?? null,
|
|
3740
3741
|
soft: attachmentSoftMasks.get(`${skinName}/${track.slot}/${track.attachment}`) ?? null,
|
|
3741
3742
|
},
|
|
3742
3743
|
deformTransforms,
|
|
3744
|
+
sampledDepth !== undefined && !Array.isArray(sampledDepth) ? sampledDepth.geometry : null,
|
|
3743
3745
|
);
|
|
3744
3746
|
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3745
3747
|
attachmentTimelinesOf(skinName, track.slot).set(track.attachment, { deform: keys });
|
|
@@ -4367,7 +4369,7 @@ interface AttachmentContext {
|
|
|
4367
4369
|
* round trip and then vanish — and the whole point of this table is to be
|
|
4368
4370
|
* read by `evaluateDeformTransform` at key time and never emitted at all.
|
|
4369
4371
|
*/
|
|
4370
|
-
depths: Map<string, number[]>;
|
|
4372
|
+
depths: Map<string, number[] | { z: number[]; geometry: DeformGeometry }>;
|
|
4371
4373
|
/**
|
|
4372
4374
|
* A `soft` mask's file and its sampled level per vertex, keyed like `depths`
|
|
4373
4375
|
* — what a deform envelope's `mask` reads at key time (issue #1413). Never
|
|
@@ -5755,7 +5757,12 @@ function buildRigMesh(
|
|
|
5755
5757
|
if (authored && att.generator) {
|
|
5756
5758
|
throw new CompileError(`${where}: a mesh is either authored geometry or a generator, never both`, 'CE_COMPILE_MESH_AUTHORED_AND_GENERATED');
|
|
5757
5759
|
}
|
|
5758
|
-
if (att.generator)
|
|
5760
|
+
if (att.generator) {
|
|
5761
|
+
if (att.depth !== undefined) {
|
|
5762
|
+
throw new CompileError(`${where}: attachment "depth" describes authored geometry; a generated mesh states its depth on the contour or grid generator instead`, 'CE_COMPILE_DEPTH_AUTHORED_REQUIRED');
|
|
5763
|
+
}
|
|
5764
|
+
return buildGeneratedMesh(att, att.generator, placeholder, where, ctx);
|
|
5765
|
+
}
|
|
5759
5766
|
if (att.vertices && att.weights) {
|
|
5760
5767
|
throw new CompileError(
|
|
5761
5768
|
`${where}: a mesh gives geometry as "vertices" or as "weights", never both — "weights" is the by-name form of the same data`, 'CE_COMPILE_MESH_GEOMETRY_SOURCES_CONFLICT',
|
|
@@ -5833,6 +5840,35 @@ function buildRigMesh(
|
|
|
5833
5840
|
height: f32(height),
|
|
5834
5841
|
};
|
|
5835
5842
|
if (att.sequence !== undefined) out.sequence = buildSequence(att.sequence);
|
|
5843
|
+
let depth: ReturnType<typeof sampleMeshDepth> | undefined;
|
|
5844
|
+
if (att.depth !== undefined) {
|
|
5845
|
+
if (img === undefined) {
|
|
5846
|
+
throw new CompileError(`${where}: an authored mesh with "depth" needs an "image" to locate the part's pixel grid`, 'CE_COMPILE_DEPTH_IMAGE_REQUIRED');
|
|
5847
|
+
}
|
|
5848
|
+
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
5849
|
+
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`, 'CE_COMPILE_SLOT_SETUP_TRANSFORM_MISSING');
|
|
5850
|
+
const geometry = deformGeometryOf(out, where, ctx.transforms, anchor);
|
|
5851
|
+
if (geometry.setup === null) {
|
|
5852
|
+
throw new CompileError(`${where}: depth needs resolved setup positions — ${geometry.setupWhy}`, 'CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED');
|
|
5853
|
+
}
|
|
5854
|
+
const world = geometry.setup;
|
|
5855
|
+
const plate = partPlate(img);
|
|
5856
|
+
const toArt = img.width / plate.width;
|
|
5857
|
+
// The generator's window is centred on the slot bone's setup WORLD origin.
|
|
5858
|
+
// UVs describe texture placement, not a vertex's position in that window.
|
|
5859
|
+
const points: Array<[number, number]> = [];
|
|
5860
|
+
for (let v = 0; v < world.length; v += 2) {
|
|
5861
|
+
points.push([(world[v] - anchor.worldX + img.width / 2) / toArt,
|
|
5862
|
+
(img.height / 2 - (world[v + 1] - anchor.worldY)) / toArt]);
|
|
5863
|
+
}
|
|
5864
|
+
depth = sampleMeshDepth(att.depth, 'authored', points, att.triangles,
|
|
5865
|
+
(px, py) => [anchor.worldX + px * toArt - img.width / 2, anchor.worldY + img.height / 2 - py * toArt],
|
|
5866
|
+
img.pageGrid === undefined ? plateAlpha(plate) : { unlocated: img.pageGrid.said },
|
|
5867
|
+
sheetGridOf(img, plate), where, ctx,
|
|
5868
|
+
Array.from({ length: world.length / 2 }, (_, v) => [world[2 * v], world[2 * v + 1]]));
|
|
5869
|
+
geometry.depth = depth.z;
|
|
5870
|
+
ctx.depths.set(`${ctx.skinName}/${ctx.slotName}/${placeholder}`, { z: depth.z, geometry });
|
|
5871
|
+
}
|
|
5836
5872
|
// Register it as `authored`: geometry rigc did not build and whose topology it
|
|
5837
5873
|
// therefore gets to assume nothing about. The generator-topology assertions
|
|
5838
5874
|
// read this and skip rather than measuring a ring that was never a ring.
|
|
@@ -5843,6 +5879,7 @@ function buildRigMesh(
|
|
|
5843
5879
|
ctx.meshes.push({
|
|
5844
5880
|
slot: ctx.slotName,
|
|
5845
5881
|
kind: 'authored',
|
|
5882
|
+
...(depth === undefined ? {} : { depth: depth.summary }),
|
|
5846
5883
|
attachments: [ctx.slotName],
|
|
5847
5884
|
vertices: uvCount / 2,
|
|
5848
5885
|
triangles: att.triangles.length / 3,
|
|
@@ -6204,8 +6241,8 @@ function buildGeneratedMesh(
|
|
|
6204
6241
|
* not invent tolerances.
|
|
6205
6242
|
*/
|
|
6206
6243
|
function sampleMeshDepth(
|
|
6207
|
-
spec:
|
|
6208
|
-
kind: 'contour' | 'grid',
|
|
6244
|
+
spec: RigDepthMap,
|
|
6245
|
+
kind: 'contour' | 'grid' | 'authored',
|
|
6209
6246
|
points: ReadonlyArray<readonly [number, number]>,
|
|
6210
6247
|
/** The mesh's own triangle list, for the turn ceiling. */
|
|
6211
6248
|
triangles: ReadonlyArray<number>,
|
|
@@ -6244,6 +6281,8 @@ function sampleMeshDepth(
|
|
|
6244
6281
|
grid: SheetGrid,
|
|
6245
6282
|
where: string,
|
|
6246
6283
|
ctx: AttachmentContext,
|
|
6284
|
+
/** Authored setup-world vertices, already in the depth turn's evaluation space. */
|
|
6285
|
+
turnPoints?: ReadonlyArray<readonly [number, number]>,
|
|
6247
6286
|
): {
|
|
6248
6287
|
z: number[];
|
|
6249
6288
|
/** The tone-curved 0..1 nearness per vertex, which `bind` thresholds. */
|
|
@@ -6327,7 +6366,9 @@ function sampleMeshDepth(
|
|
|
6327
6366
|
kind === 'contour'
|
|
6328
6367
|
? 'A contour mesh puts every vertex ON the silhouette and pushes it out by the margin, so a sheet cut to ' +
|
|
6329
6368
|
'the art stops short of every one of them. Dilate the sheet past the mesh margin, or lower the margin.'
|
|
6330
|
-
:
|
|
6369
|
+
: kind === 'authored'
|
|
6370
|
+
? 'The authored setup vertices reach outside the sheet coverage. Extend the sheet to cover their bilinear taps, or correct the setup geometry.'
|
|
6371
|
+
: 'A grid spans the whole part window, corners included, while a sheet is usually cut to the art — so the ' +
|
|
6331
6372
|
'lattice reaches past it wherever the art does not fill the window. Dilate the sheet to the window, or ' +
|
|
6332
6373
|
'state "us"/"vs" that keep the lattice inside the art.';
|
|
6333
6374
|
throw new CompileError(
|
|
@@ -6368,7 +6409,7 @@ function sampleMeshDepth(
|
|
|
6368
6409
|
range: [f32(lo), f32(hi)],
|
|
6369
6410
|
...(partAlpha instanceof Uint8Array ? { undrawn } : { undrawn: null, unlocated: partAlpha.unlocated }),
|
|
6370
6411
|
ceiling: turnCeiling(
|
|
6371
|
-
points.map(([px, py]) => toBind(px, py)),
|
|
6412
|
+
turnPoints ?? points.map(([px, py]) => toBind(px, py)),
|
|
6372
6413
|
z,
|
|
6373
6414
|
triangles,
|
|
6374
6415
|
ctx.turnNarrowing === undefined ? undefined : { narrowing: ctx.turnNarrowing },
|
|
@@ -8737,7 +8778,7 @@ export interface DeformGeometry {
|
|
|
8737
8778
|
/** Why `setup` is null, phrased for the refusal. Null when it is not. */
|
|
8738
8779
|
setupWhy: string | null;
|
|
8739
8780
|
/**
|
|
8740
|
-
* Per-vertex `z`, when this attachment
|
|
8781
|
+
* Per-vertex `z`, when this attachment or its generator named a depth map.
|
|
8741
8782
|
*
|
|
8742
8783
|
* Not measured from the attachment like everything else here — there
|
|
8743
8784
|
* is nowhere in the format to measure it FROM, which is the whole reason the
|
|
@@ -8771,6 +8812,8 @@ export function deformGeometryOf(
|
|
|
8771
8812
|
where: string,
|
|
8772
8813
|
/** Every bone's setup world transform, by name. */
|
|
8773
8814
|
transforms: Map<string, BoneTransform>,
|
|
8815
|
+
/** Force the world-offset path for an authored depth turn, including rigid meshes. */
|
|
8816
|
+
depthAnchor?: BoneTransform,
|
|
8774
8817
|
): DeformGeometry {
|
|
8775
8818
|
const type = att.kind;
|
|
8776
8819
|
let worldVerticesLength: number;
|
|
@@ -8816,6 +8859,15 @@ export function deformGeometryOf(
|
|
|
8816
8859
|
const vertices = att.vertices;
|
|
8817
8860
|
const weighted = vertices.weighted;
|
|
8818
8861
|
if (!vertices.weighted) {
|
|
8862
|
+
if (depthAnchor !== undefined) {
|
|
8863
|
+
return {
|
|
8864
|
+
weighted, deformLength: worldVerticesLength, vertexCount: worldVerticesLength / 2,
|
|
8865
|
+
boneCounts: Array(worldVerticesLength / 2).fill(1),
|
|
8866
|
+
setup: setupWorldVertices(vertices, depthAnchor, transforms, where).flat(),
|
|
8867
|
+
influenceBones: Array(worldVerticesLength / 2).fill(depthAnchor),
|
|
8868
|
+
setupWhy: null, depth: null, soft: null,
|
|
8869
|
+
};
|
|
8870
|
+
}
|
|
8819
8871
|
return {
|
|
8820
8872
|
weighted,
|
|
8821
8873
|
deformLength: worldVerticesLength,
|
|
@@ -8879,7 +8931,7 @@ export function deformGeometryOf(
|
|
|
8879
8931
|
// the runtime's last bits (`b = −2.3e-8` on an unrotated bone) into its
|
|
8880
8932
|
// offset, about 1e-6 on gallery/look's hair locks. Evaluating on the authored
|
|
8881
8933
|
// positions instead would need them carried here from the binder.
|
|
8882
|
-
if (boneCounts.every((n) => n === 1) && bones.size === 1) {
|
|
8934
|
+
if (depthAnchor === undefined && boneCounts.every((n) => n === 1) && bones.size === 1) {
|
|
8883
8935
|
return { ...common, setup: bindSpace, influenceBones: null, setupWhy: null };
|
|
8884
8936
|
}
|
|
8885
8937
|
// -- several influences, evaluated in world (issue #389) -------------------
|
|
@@ -9055,6 +9107,7 @@ function compileDeformTrack(
|
|
|
9055
9107
|
duration: number,
|
|
9056
9108
|
geometry: DeformGeometry,
|
|
9057
9109
|
generated: CompileResult['deformTransforms'],
|
|
9110
|
+
depthWorld: DeformGeometry | null = null,
|
|
9058
9111
|
): ModelKey[] {
|
|
9059
9112
|
const skin = track.skin ?? 'default';
|
|
9060
9113
|
const where = `animation "${animName}" deform ${skin}/${track.slot}/${track.attachment}`;
|
|
@@ -9080,6 +9133,7 @@ function compileDeformTrack(
|
|
|
9080
9133
|
// definite, which the arithmetic does for any weighting that closes at 1 and
|
|
9081
9134
|
// for no weighting that does not (issue #389).
|
|
9082
9135
|
if (key.transform !== undefined) {
|
|
9136
|
+
const evaluated = ('depth' in key.transform && key.transform.depth === true ? depthWorld : null) ?? geometry;
|
|
9083
9137
|
if (key.vertices !== undefined && key.vertices !== null) {
|
|
9084
9138
|
throw new CompileError(
|
|
9085
9139
|
`${where} (t=${key.t}): the key carries both a "transform" and a "vertices" run, and they are two answers to ` +
|
|
@@ -9090,33 +9144,33 @@ function compileDeformTrack(
|
|
|
9090
9144
|
if (key.offset !== undefined || key.fromVertex !== undefined) {
|
|
9091
9145
|
throw new CompileError(
|
|
9092
9146
|
`${where} (t=${key.t}): the key states a "transform" and a start index. A transform is a model of the whole ` +
|
|
9093
|
-
`attachment and is evaluated over all ${
|
|
9147
|
+
`attachment and is evaluated over all ${evaluated.vertexCount} of its vertices, so it always starts at deform ` +
|
|
9094
9148
|
'index 0. A model applied to part of a run leaves a step at the run\'s edge — write the partial run by hand ' +
|
|
9095
9149
|
'if that is what you mean.', 'CE_COMPILE_DEFORM_TRANSFORM_START_INDEX',
|
|
9096
9150
|
);
|
|
9097
9151
|
}
|
|
9098
|
-
if (
|
|
9152
|
+
if (evaluated.setup === null) {
|
|
9099
9153
|
throw new CompileError(
|
|
9100
9154
|
`${where} (t=${key.t}): a "transform" states where every vertex of this attachment goes, and this one has a ` +
|
|
9101
|
-
`vertex the arithmetic cannot place — ${
|
|
9155
|
+
`vertex the arithmetic cannot place — ${evaluated.setupWhy}. Fix the weights, or write the bind-space pairs ` +
|
|
9102
9156
|
'yourself and start the run with "offset".', 'CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED',
|
|
9103
9157
|
);
|
|
9104
9158
|
}
|
|
9105
9159
|
const report = evaluateDeformTransform(
|
|
9106
9160
|
key.transform,
|
|
9107
|
-
|
|
9161
|
+
evaluated.setup,
|
|
9108
9162
|
onModelGrid,
|
|
9109
9163
|
`${where} (t=${key.t})`,
|
|
9110
|
-
|
|
9111
|
-
|
|
9164
|
+
evaluated.depth,
|
|
9165
|
+
evaluated.soft,
|
|
9112
9166
|
);
|
|
9113
|
-
if (report.offsets.length !==
|
|
9167
|
+
if (report.offsets.length !== evaluated.vertexCount * 2) {
|
|
9114
9168
|
// Unreachable while `setup` is one pair per vertex, which is the whole
|
|
9115
9169
|
// reason both are derived from the same walk. Stated rather than
|
|
9116
9170
|
// assumed: a silent mismatch here is the overrun A35 exists for.
|
|
9117
9171
|
throw new CompileError(
|
|
9118
9172
|
`${where} (t=${key.t}): the transform produced ${report.offsets.length} numbers for ` +
|
|
9119
|
-
`${
|
|
9173
|
+
`${evaluated.vertexCount} vertices`, 'CE_COMPILE_DEFORM_TRANSFORM_OFFSET_COUNT',
|
|
9120
9174
|
);
|
|
9121
9175
|
}
|
|
9122
9176
|
// -- one displacement per vertex -> one pair per influence (issue #389) -
|
|
@@ -9128,11 +9182,11 @@ function compileDeformTrack(
|
|
|
9128
9182
|
// displacement it expands is the ROUNDED one the report prints, so a
|
|
9129
9183
|
// reader can reproduce every emitted pair from the audit rather than from
|
|
9130
9184
|
// a second evaluation.
|
|
9131
|
-
const run = expandDeformToInfluences(report.offsets,
|
|
9132
|
-
if (run.length !==
|
|
9185
|
+
const run = expandDeformToInfluences(report.offsets, evaluated);
|
|
9186
|
+
if (run.length !== evaluated.deformLength) {
|
|
9133
9187
|
throw new CompileError(
|
|
9134
9188
|
`${where} (t=${key.t}): the transform expanded to ${run.length} numbers and this attachment's deform ` +
|
|
9135
|
-
`array is ${
|
|
9189
|
+
`array is ${evaluated.deformLength} long`, 'CE_COMPILE_DEFORM_TRANSFORM_RUN_LENGTH',
|
|
9136
9190
|
);
|
|
9137
9191
|
}
|
|
9138
9192
|
generated.push({
|
|
@@ -9148,14 +9202,14 @@ function compileDeformTrack(
|
|
|
9148
9202
|
// other way to know they were not evaluated alike. Said only on the path
|
|
9149
9203
|
// where it is not the attachment's own space, so every existing report
|
|
9150
9204
|
// prints exactly the lines it printed before.
|
|
9151
|
-
...(
|
|
9205
|
+
...(evaluated.influenceBones === null
|
|
9152
9206
|
? {}
|
|
9153
9207
|
: {
|
|
9154
9208
|
expanded: run,
|
|
9155
9209
|
derived: [
|
|
9156
9210
|
...report.derived,
|
|
9157
|
-
`evaluated at setup WORLD positions, because ${
|
|
9158
|
-
`${
|
|
9211
|
+
`evaluated at setup WORLD positions, because ${evaluated.vertexCount} vertices carry ` +
|
|
9212
|
+
`${evaluated.deformLength / 2} influences; each displacement D is written into every influence as ` +
|
|
9159
9213
|
'Mᵢ⁻¹·D, so the runtime composes Σwᵢ·Mᵢ·Mᵢ⁻¹·D = D',
|
|
9160
9214
|
],
|
|
9161
9215
|
}),
|
package/src/core/events.ts
CHANGED
|
@@ -33,6 +33,10 @@ import { gridRound } from './index.ts';
|
|
|
33
33
|
|
|
34
34
|
/** An event definition, as the model states it (`ModelEvent`): the payload a key falls back to. */
|
|
35
35
|
export interface CoreEventDef {
|
|
36
|
+
/** Audio metadata retained additively; the five-column oracle row is unchanged. */
|
|
37
|
+
audio?: string;
|
|
38
|
+
volume?: number;
|
|
39
|
+
balance?: number;
|
|
36
40
|
name: string;
|
|
37
41
|
int: number;
|
|
38
42
|
float: number;
|
|
@@ -41,6 +45,9 @@ export interface CoreEventDef {
|
|
|
41
45
|
|
|
42
46
|
/** One event key, as read: its time (float32) and the payload it fires with, each field resolved. */
|
|
43
47
|
export interface CoreEventKey {
|
|
48
|
+
/** Audio delivery values, as observed from the public EventTimeline API. */
|
|
49
|
+
volume?: number;
|
|
50
|
+
balance?: number;
|
|
44
51
|
time: number;
|
|
45
52
|
name: string;
|
|
46
53
|
int: number;
|
|
@@ -55,8 +62,8 @@ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'obj
|
|
|
55
62
|
const EVENT_DEF_FIELDS = ['name', 'int', 'float', 'string', 'audio', 'volume', 'balance'] as const;
|
|
56
63
|
const EVENT_KEY_FIELDS = ['time', 'name', 'int', 'float', 'string', 'volume', 'balance'] as const;
|
|
57
64
|
|
|
58
|
-
function payload(raw: Record<string, unknown>, at: string, problems: string[]): { int?: number; float?: number; string?: string } {
|
|
59
|
-
const out: { int?: number; float?: number; string?: string } = {};
|
|
65
|
+
function payload(raw: Record<string, unknown>, at: string, problems: string[]): { int?: number; float?: number; string?: string; volume?: number; balance?: number } {
|
|
66
|
+
const out: { int?: number; float?: number; string?: string; volume?: number; balance?: number } = {};
|
|
60
67
|
if (raw.int !== undefined) {
|
|
61
68
|
if (typeof raw.int !== 'number' || !Number.isInteger(raw.int)) problems.push(`${at}: int is ${JSON.stringify(raw.int)}, not a whole number`);
|
|
62
69
|
else out.int = raw.int;
|
|
@@ -69,7 +76,10 @@ function payload(raw: Record<string, unknown>, at: string, problems: string[]):
|
|
|
69
76
|
if (typeof raw.string !== 'string') problems.push(`${at}: string is ${JSON.stringify(raw.string)}, not a string`);
|
|
70
77
|
else out.string = raw.string;
|
|
71
78
|
}
|
|
72
|
-
for (const key of ['volume', 'balance'] as const) if (raw[key] !== undefined
|
|
79
|
+
for (const key of ['volume', 'balance'] as const) if (raw[key] !== undefined) {
|
|
80
|
+
if (typeof raw[key] !== 'number' || !Number.isFinite(raw[key])) problems.push(`${at}: ${key} is ${JSON.stringify(raw[key])}, not a finite number`);
|
|
81
|
+
else out[key] = raw[key];
|
|
82
|
+
}
|
|
73
83
|
return out;
|
|
74
84
|
}
|
|
75
85
|
|
|
@@ -90,7 +100,7 @@ export function readEventDefs(value: unknown, problems: string[]): Map<string, C
|
|
|
90
100
|
if (raw.audio !== undefined && typeof raw.audio !== 'string') problems.push(`${at} "${raw.name}": audio is ${JSON.stringify(raw.audio)}, not a string`);
|
|
91
101
|
if (out.has(raw.name)) problems.push(`${at}: event "${raw.name}" is declared twice`);
|
|
92
102
|
const p = payload(raw, `${at} "${raw.name}"`, problems);
|
|
93
|
-
out.set(raw.name, { name: raw.name, int: p.int ?? 0, float: p.float ?? 0, string: p.string ?? '' });
|
|
103
|
+
out.set(raw.name, { audio: typeof raw.audio === 'string' ? raw.audio : '', volume: p.volume ?? 0, balance: p.balance ?? 0, name: raw.name, int: p.int ?? 0, float: p.float ?? 0, string: p.string ?? '' });
|
|
94
104
|
});
|
|
95
105
|
return out;
|
|
96
106
|
}
|
|
@@ -121,7 +131,9 @@ export function readEventKeys(value: unknown, label: string, defs: ReadonlyMap<s
|
|
|
121
131
|
return;
|
|
122
132
|
}
|
|
123
133
|
const p = payload(raw, at, problems);
|
|
124
|
-
|
|
134
|
+
// Observed oracle behavior: a voiced key's omitted balance falls back to
|
|
135
|
+
// the definition's volume, not its balance. A silent event keeps both at 0.
|
|
136
|
+
out.push({ volume: def.audio ? p.volume ?? def.volume ?? 0 : 0, balance: def.audio ? p.balance ?? def.volume ?? 0 : 0, time: Math.fround(typeof t === 'number' && Number.isFinite(t) ? t : 0), name: def.name, int: p.int ?? def.int, float: p.float ?? def.float, string: p.string ?? def.string });
|
|
125
137
|
});
|
|
126
138
|
return out;
|
|
127
139
|
}
|
package/src/deformgen.ts
CHANGED
|
@@ -374,7 +374,7 @@ export function evaluateDeformTransform(
|
|
|
374
374
|
round: Rounder,
|
|
375
375
|
where: string,
|
|
376
376
|
/**
|
|
377
|
-
* Per-vertex `z` for this attachment, when its generator named a depth map.
|
|
377
|
+
* Per-vertex `z` for this attachment, when it or its generator named a depth map.
|
|
378
378
|
* Null when it named none — which is what makes `"depth": true` refusable by
|
|
379
379
|
* name rather than silently falling back to a cylinder.
|
|
380
380
|
*/
|
|
@@ -448,7 +448,7 @@ export function evaluateDeformTransform(
|
|
|
448
448
|
if (fromDepth && depth === null) {
|
|
449
449
|
throw new CompileError(
|
|
450
450
|
`${where}: transform ${kind} says "depth": true and this attachment has no depth map. The map is named on ` +
|
|
451
|
-
'
|
|
451
|
+
'an authored attachment or its contour/grid generator, as `"depth": { "image": …, "near": …, "zScale": … }`, because it describes ' +
|
|
452
452
|
'the art rather than this key. Name it there, or give this key a radius.', 'CE_DEFORM_DEPTH_MAP_REQUIRED',
|
|
453
453
|
);
|
|
454
454
|
}
|
package/src/meshcompare.ts
CHANGED
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
type SourceMesh,
|
|
108
108
|
type WorstSample,
|
|
109
109
|
} from './meshquality.ts';
|
|
110
|
+
import { createHash } from 'node:crypto';
|
|
110
111
|
import { areaBand, stretchSingularValues, triangleAreas } from './areaband.ts';
|
|
111
112
|
import { uvCarriers, uvCarriersWith, type Carrier, type CarrierPlant } from './meshcarriers.ts';
|
|
112
113
|
import { CoreInputError, readModel, underNoSkin, underSkin, type CompiledDocument } from './core/index.ts';
|
|
@@ -187,6 +188,12 @@ export interface MotionComparisonInput {
|
|
|
187
188
|
overBound?: OverBoundRequest;
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
/** The candidate list is supplied on each use; every other input is bound to the reference. */
|
|
192
|
+
export type MotionReferenceInput = Omit<MotionComparisonInput, 'candidates'>;
|
|
193
|
+
|
|
194
|
+
/** An opaque reference prepared by {@link prepareMotionReference}. Its data is held outside the returned object. */
|
|
195
|
+
export interface PreparedMotionReference { readonly kind: 'prepared-motion-reference/1' }
|
|
196
|
+
|
|
190
197
|
/**
|
|
191
198
|
* Issue #1291's faults in carrying `motionAmplitude` into the setup measurement, for the `mesh-compare` suite's
|
|
192
199
|
* negative controls: the amplitude not handed over; `null` handed over as the field left out; the field not validated
|
|
@@ -896,6 +903,110 @@ function foldingSlots(raw: unknown, who: string): Set<string> {
|
|
|
896
903
|
return new Set(list as string[]);
|
|
897
904
|
}
|
|
898
905
|
|
|
906
|
+
type GeometryRead = { geometry: EvidenceSection | null; counts: MeshCounts | null };
|
|
907
|
+
type MotionRead = { section: EvidenceSection & { schedule: ScheduleUsed }; perFrame: NonNullable<CandidateReport['perFrame']>; overBound: OverBoundReport | null } | null;
|
|
908
|
+
interface PreparedMotionData {
|
|
909
|
+
frames: Planned[];
|
|
910
|
+
walks: Walk[];
|
|
911
|
+
folding: Set<string>;
|
|
912
|
+
scheduleUsed: ScheduleUsed;
|
|
913
|
+
samplesOf: Array<{ samples: Sample[]; art: number }>;
|
|
914
|
+
refCarriers: Array<Array<Carrier | null>>;
|
|
915
|
+
posedRef: Posed | { refused: string };
|
|
916
|
+
}
|
|
917
|
+
interface PreparedState {
|
|
918
|
+
reference: Build;
|
|
919
|
+
refs: AttachmentRef[];
|
|
920
|
+
order: number[];
|
|
921
|
+
refGeometry: GeometryRead;
|
|
922
|
+
motionData: PreparedMotionData | null;
|
|
923
|
+
referenceMotion: MotionRead;
|
|
924
|
+
digests: Record<string, string>;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const preparedReferences = new WeakMap<PreparedMotionReference, PreparedState>();
|
|
928
|
+
|
|
929
|
+
/** One digest per bound field, so a mismatch names the field instead of accepting a stale pose or mask. */
|
|
930
|
+
function boundDigests(input: MotionReferenceInput): Record<string, string> {
|
|
931
|
+
// JSON.stringify maps -0 to 0, drops undefined object fields, and maps non-finite numbers to null. A bound
|
|
932
|
+
// option's signed zero can survive validation and is echoed in a report, so these are not safe digest aliases.
|
|
933
|
+
const exact = (value: unknown, visiting = new Set<object>()): unknown => {
|
|
934
|
+
if (value === null) return ['null'];
|
|
935
|
+
if (typeof value === 'number') return ['number', Object.is(value, -0) ? '-0' : String(value)];
|
|
936
|
+
if (typeof value === 'string' || typeof value === 'boolean' || typeof value === 'undefined') return [typeof value, value ?? null];
|
|
937
|
+
if (typeof value !== 'object') return [typeof value, String(value)];
|
|
938
|
+
if (visiting.has(value)) throw new Error('cyclic bound input');
|
|
939
|
+
visiting.add(value);
|
|
940
|
+
const encoded = Array.isArray(value)
|
|
941
|
+
? ['array', Array.from({ length: value.length }, (_, i) => Object.prototype.hasOwnProperty.call(value, i) ? exact(value[i], visiting) : ['hole'])]
|
|
942
|
+
: ['object', Object.keys(value).map((key) => [key, exact((value as Record<string, unknown>)[key], visiting)])];
|
|
943
|
+
visiting.delete(value);
|
|
944
|
+
return encoded;
|
|
945
|
+
};
|
|
946
|
+
const digest = (value: unknown): string => createHash('sha256').update(JSON.stringify(exact(value))).digest('hex');
|
|
947
|
+
return {
|
|
948
|
+
reference: digest(input.reference),
|
|
949
|
+
schedule: digest(input.schedule),
|
|
950
|
+
attachments: digest(input.attachments.map((a) => ({
|
|
951
|
+
attachment: a.attachment,
|
|
952
|
+
art: { threshold: a.art.threshold, frame: a.art.frame, mask: {
|
|
953
|
+
width: a.art.mask.width,
|
|
954
|
+
height: a.art.mask.height,
|
|
955
|
+
alpha: createHash('sha256').update(a.art.mask.alpha).digest('hex'),
|
|
956
|
+
} },
|
|
957
|
+
finalThreshold: a.finalThreshold,
|
|
958
|
+
minArtSamples: a.minArtSamples,
|
|
959
|
+
regions: a.regions,
|
|
960
|
+
}))),
|
|
961
|
+
referenceArtFit: digest(input.referenceArtFit),
|
|
962
|
+
candidateArtFit: digest(input.candidateArtFit),
|
|
963
|
+
bounds: digest(input.bounds),
|
|
964
|
+
motionRequired: digest(input.motionRequired),
|
|
965
|
+
perFrame: digest(input.perFrame),
|
|
966
|
+
motionAmplitude: digest({ present: Object.prototype.hasOwnProperty.call(input, 'motionAmplitude'), value: input.motionAmplitude }),
|
|
967
|
+
overBound: digest({ present: Object.prototype.hasOwnProperty.call(input, 'overBound'), value: input.overBound }),
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Prepare the source once for sequential candidates on the same schedule and the same art. */
|
|
972
|
+
export function prepareMotionReference(input: MotionReferenceInput): PreparedMotionReference {
|
|
973
|
+
if (!isObject(input)) missing('the prepared reference input', input, 'a MotionReferenceInput object');
|
|
974
|
+
const ref: unknown = input.reference;
|
|
975
|
+
let id = 'prepared-candidate';
|
|
976
|
+
while (isObject(ref) && id === ref.id) id += '-';
|
|
977
|
+
const model = isObject(ref) && typeof ref.model === 'string' ? ref.model : '';
|
|
978
|
+
validateInput({ ...input, candidates: [{ id, model }] }, null);
|
|
979
|
+
// No caller-owned array enters the cache: a later mutation is detected against this snapshot at use.
|
|
980
|
+
let snapshot: MotionReferenceInput;
|
|
981
|
+
try { snapshot = structuredClone(input); }
|
|
982
|
+
catch (e) { refuse('COMPARE_INPUT_MISSING', `the prepared reference input cannot be copied (${(e as Error).message}); required cloneable model, schedule, art and options`); }
|
|
983
|
+
let state: PreparedState | null = null;
|
|
984
|
+
const asComparison: MotionComparisonInput = { ...snapshot, candidates: [{ id, model: snapshot.reference.model }] };
|
|
985
|
+
compareMeshesInMotionWith(asComparison, null, undefined, (ready) => { state = ready; });
|
|
986
|
+
if (state === null) throw new Error('prepareMotionReference: reference preparation produced no state');
|
|
987
|
+
const ready: PreparedState = state;
|
|
988
|
+
try { ready.digests = boundDigests(snapshot); }
|
|
989
|
+
catch (e) { refuse('COMPARE_INPUT_MISSING', `the prepared reference input cannot be digested (${(e as Error).message}); required a complete mask and comparison options`); }
|
|
990
|
+
const handle: PreparedMotionReference = Object.freeze({ kind: 'prepared-motion-reference/1' });
|
|
991
|
+
preparedReferences.set(handle, ready);
|
|
992
|
+
return handle;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Compare a candidate against the prepared source; every bound input must still have the prepared bytes. */
|
|
996
|
+
export function comparePreparedMotion(handle: PreparedMotionReference, input: MotionComparisonInput): MeshQualityReport {
|
|
997
|
+
const ready = preparedReferences.get(handle);
|
|
998
|
+
if (ready === undefined) refuse('COMPARE_PREPARED_HANDLE', 'the prepared reference handle was not made by prepareMotionReference; required its opaque handle');
|
|
999
|
+
let now: Record<string, string>;
|
|
1000
|
+
try { now = boundDigests(input); }
|
|
1001
|
+
catch { refuse('COMPARE_PREPARED_MISMATCH', 'the bound input cannot be digested; required the reference, schedule, attachments and options used to prepare the handle'); }
|
|
1002
|
+
for (const [field, digest] of Object.entries(ready.digests)) {
|
|
1003
|
+
if (now[field] !== digest) refuse('COMPARE_PREPARED_MISMATCH', `${field} changed after prepareMotionReference; required the same reference, schedule, art and options for this handle`);
|
|
1004
|
+
}
|
|
1005
|
+
const report = compareMeshesInMotionWith(input, null, ready);
|
|
1006
|
+
if (report === null) throw new Error('comparePreparedMotion: comparison produced no report');
|
|
1007
|
+
return report;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
899
1010
|
/**
|
|
900
1011
|
* Compare one or more candidate builds with a reference build in motion — §3
|
|
901
1012
|
* and §4's motion rows — and measure each build's art fit at the setup pose
|
|
@@ -907,7 +1018,9 @@ function foldingSlots(raw: unknown, who: string): Set<string> {
|
|
|
907
1018
|
* undercut at the setup pose fails `referenceArtFit`, and `measureMeshQuality`'s own codes for the art).
|
|
908
1019
|
*/
|
|
909
1020
|
export function compareMeshesInMotion(input: MotionComparisonInput): MeshQualityReport {
|
|
910
|
-
|
|
1021
|
+
const report = compareMeshesInMotionWith(input, null);
|
|
1022
|
+
if (report === null) throw new Error('compareMeshesInMotion: comparison produced no report');
|
|
1023
|
+
return report;
|
|
911
1024
|
}
|
|
912
1025
|
|
|
913
1026
|
/**
|
|
@@ -916,18 +1029,20 @@ export function compareMeshesInMotion(input: MotionComparisonInput): MeshQuality
|
|
|
916
1029
|
* surface*).
|
|
917
1030
|
*/
|
|
918
1031
|
export function compareMeshesInMotionPlanted(input: MotionComparisonInput, plant: ComparePlant): MeshQualityReport {
|
|
919
|
-
|
|
1032
|
+
const report = compareMeshesInMotionWith(input, plant);
|
|
1033
|
+
if (report === null) throw new Error('compareMeshesInMotionPlanted: comparison produced no report');
|
|
1034
|
+
return report;
|
|
920
1035
|
}
|
|
921
1036
|
|
|
922
|
-
function compareMeshesInMotionWith(input: MotionComparisonInput, plant: ComparePlant | null): MeshQualityReport {
|
|
1037
|
+
function compareMeshesInMotionWith(input: MotionComparisonInput, plant: ComparePlant | null, prepared?: PreparedState, capture?: (state: PreparedState) => void): MeshQualityReport | null {
|
|
923
1038
|
validateInput(input, plant);
|
|
924
1039
|
const amplitude = input.motionAmplitude;
|
|
925
1040
|
// What the setup measurement is handed: the field as the caller set it — left out as left out, `null` as `null`.
|
|
926
1041
|
const handed: { motionAmplitude?: MotionAmplitude | null } =
|
|
927
1042
|
plant === 'amplitude-not-carried' || amplitude === undefined || (plant === 'null-read-as-left-out' && amplitude === null) ? {} : { motionAmplitude: amplitude };
|
|
928
|
-
const refs = input.attachments.map((a) => a.attachment);
|
|
929
|
-
const reference = readBuild(input.reference, 'reference', refs);
|
|
930
|
-
const candidates = input.candidates.map((c, i) => readBuild(c, `candidates[${i}]`, refs));
|
|
1043
|
+
const refs = prepared?.refs ?? input.attachments.map((a) => a.attachment);
|
|
1044
|
+
const reference = prepared?.reference ?? readBuild(input.reference, 'reference', refs);
|
|
1045
|
+
const candidates = capture === undefined ? input.candidates.map((c, i) => readBuild(c, `candidates[${i}]`, refs)) : [];
|
|
931
1046
|
for (const c of candidates) {
|
|
932
1047
|
const d = firstDifference(reference.raw, c.raw, [], refs);
|
|
933
1048
|
if (d !== null) {
|
|
@@ -939,13 +1054,13 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
939
1054
|
}
|
|
940
1055
|
// The slots in skeleton order fix the order attachments are reported in (§2).
|
|
941
1056
|
const slotRank = new Map(reference.doc.slots.map((s, i) => [s.name, i]));
|
|
942
|
-
const order = input.attachments.map((_a, i) => i).sort((p, q) => (slotRank.get(refs[p].slot) ?? 0) - (slotRank.get(refs[q].slot) ?? 0) || (refs[p].attachment < refs[q].attachment ? -1 : refs[p].attachment > refs[q].attachment ? 1 : 0));
|
|
1057
|
+
const order = prepared?.order ?? input.attachments.map((_a, i) => i).sort((p, q) => (slotRank.get(refs[p].slot) ?? 0) - (slotRank.get(refs[q].slot) ?? 0) || (refs[p].attachment < refs[q].attachment ? -1 : refs[p].attachment > refs[q].attachment ? 1 : 0));
|
|
943
1058
|
for (const ref of refs) {
|
|
944
1059
|
if (!slotRank.has(ref.slot)) missing(`attachment ${nameOf(ref)}'s slot`, ref.slot, `one of the reference's slots [${[...slotRank.keys()].join(', ')}]`);
|
|
945
1060
|
}
|
|
946
1061
|
|
|
947
1062
|
// --- the setup art fit, each build under its own bounds (§3 (a)) ----------------------------
|
|
948
|
-
const geometryOf = (b: Build, fit: ArtFitBounds):
|
|
1063
|
+
const geometryOf = (b: Build, fit: ArtFitBounds): GeometryRead => {
|
|
949
1064
|
const sections: EvidenceSection[] = [];
|
|
950
1065
|
const counts: MeshCounts[] = [];
|
|
951
1066
|
for (const i of order) {
|
|
@@ -987,7 +1102,7 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
987
1102
|
};
|
|
988
1103
|
return { geometry: joinSections(sections), counts: total };
|
|
989
1104
|
};
|
|
990
|
-
const refGeometry = geometryOf(reference, input.referenceArtFit);
|
|
1105
|
+
const refGeometry = prepared?.refGeometry ?? geometryOf(reference, input.referenceArtFit);
|
|
991
1106
|
// §3 *Independent evidence* (a), P8: a reference that fails its own art fit — coverage, overshoot or undercut against
|
|
992
1107
|
// its own mask under `referenceArtFit` — is refused AS A REFERENCE: a deviation from a reference that does not carry
|
|
993
1108
|
// its own art is not evidence, so no candidate is measured against it. (a) is those three rows and no other: winding
|
|
@@ -1006,24 +1121,26 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1006
1121
|
|
|
1007
1122
|
// --- motion ---------------------------------------------------------------------------------
|
|
1008
1123
|
const schedule = input.schedule;
|
|
1009
|
-
type Motion = { section: EvidenceSection & { schedule: ScheduleUsed }; perFrame: NonNullable<CandidateReport['perFrame']>; overBound: OverBoundReport | null } | null;
|
|
1010
1124
|
// Issue #1315: the request as the caller set it; `?? undefined` only matters under the unvalidated plant.
|
|
1011
1125
|
const overBound: OverBoundRequest | undefined = input.overBound ?? undefined;
|
|
1012
|
-
let motionOf: (b: Build, isReference: boolean) =>
|
|
1126
|
+
let motionOf: (b: Build, isReference: boolean) => MotionRead = () => null;
|
|
1127
|
+
let motionData: PreparedMotionData | null = null;
|
|
1013
1128
|
if (schedule !== null) {
|
|
1014
|
-
const
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1017
|
-
const
|
|
1129
|
+
const existing = prepared?.motionData;
|
|
1130
|
+
const planned = existing === undefined ? planSchedule(schedule, reference.doc, reference.doc.constraints.filter((c) => c.kind === 'physics').length) : null;
|
|
1131
|
+
const frames = existing?.frames ?? planned!.frames;
|
|
1132
|
+
const walks = existing?.walks ?? planned!.walks;
|
|
1133
|
+
const folding = existing?.folding ?? foldingSlots(reference.raw, `reference "${reference.id}"`);
|
|
1134
|
+
const scheduleUsed: ScheduleUsed = existing?.scheduleUsed ?? {
|
|
1018
1135
|
...schedule,
|
|
1019
1136
|
walked: frames.map((f) => f.ref),
|
|
1020
1137
|
roles: (['baseline', 'selection', 'held-out'] as const).filter((r) => frames.some((f) => f.ref.role === r)),
|
|
1021
1138
|
heldOutClaim: frames.some((f) => f.ref.role === 'held-out'),
|
|
1022
1139
|
reset: 'physics reset at time 0',
|
|
1023
|
-
walks: walks.map((w, i) => ({ animation: w.animation, phase: w.phase, steps: walkSteps[i] })),
|
|
1140
|
+
walks: walks.map((w, i) => ({ animation: w.animation, phase: w.phase, steps: planned!.walkSteps[i] })),
|
|
1024
1141
|
};
|
|
1025
1142
|
// The samples, per attachment: art pixel centres at the final threshold, then the reference hull's UVs (§3).
|
|
1026
|
-
const samplesOf = order.map((i) => {
|
|
1143
|
+
const samplesOf = existing?.samplesOf ?? order.map((i) => {
|
|
1027
1144
|
const a = input.attachments[i];
|
|
1028
1145
|
const { mask, frame } = a.art;
|
|
1029
1146
|
const bits = artOf(mask, a.finalThreshold);
|
|
@@ -1046,9 +1163,10 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1046
1163
|
const carrierPlant: CarrierPlant | null = plant === 'slack-support-unread' || plant === 'narrowest-slack' || plant === 'any-shared-vertex' ? plant : null;
|
|
1047
1164
|
const carriersOf = (uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> =>
|
|
1048
1165
|
uvCarriersWith(uvs, triangles, samples, who, carrierPlant);
|
|
1049
|
-
const refCarriers = order.map((i, k) => carriersOf(reference.meshes[i].uvs, reference.meshes[i].triangles, samplesOf[k].samples, `reference "${reference.id}", attachment ${nameOf(refs[i])}`));
|
|
1166
|
+
const refCarriers = existing?.refCarriers ?? order.map((i, k) => carriersOf(reference.meshes[i].uvs, reference.meshes[i].triangles, samplesOf[k].samples, `reference "${reference.id}", attachment ${nameOf(refs[i])}`));
|
|
1050
1167
|
const clock = plant === 'frame-physics-clock' ? 'frame' : 'render';
|
|
1051
|
-
const posedRef = poseBuild(reference.doc, order.map((i) => refs[i]), frames, walks, clock);
|
|
1168
|
+
const posedRef = existing?.posedRef ?? poseBuild(reference.doc, order.map((i) => refs[i]), frames, walks, clock);
|
|
1169
|
+
motionData = { frames, walks, folding, scheduleUsed, samplesOf, refCarriers, posedRef };
|
|
1052
1170
|
|
|
1053
1171
|
motionOf = (b: Build, isReference: boolean) => {
|
|
1054
1172
|
const carriers = isReference ? refCarriers : order.map((i, k) => carriersOf(b.meshes[i].uvs, b.meshes[i].triangles, samplesOf[k].samples, `candidate "${b.id}", attachment ${nameOf(refs[i])}`));
|
|
@@ -1311,9 +1429,13 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1311
1429
|
return { section: { ...sectionOf(grouped), schedule: scheduleUsed }, perFrame, overBound: located };
|
|
1312
1430
|
};
|
|
1313
1431
|
}
|
|
1314
|
-
const referenceMotion = motionOf(reference, true);
|
|
1432
|
+
const referenceMotion = prepared?.referenceMotion ?? motionOf(reference, true);
|
|
1433
|
+
if (capture !== undefined) {
|
|
1434
|
+
capture({ reference, refs, order, refGeometry, motionData, referenceMotion, digests: {} });
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1315
1437
|
const candidateMotions = candidates.map((c) => motionOf(c, false));
|
|
1316
|
-
const reportOf = (b: Build, geometry:
|
|
1438
|
+
const reportOf = (b: Build, geometry: GeometryRead, measured: MotionRead, isCandidate: boolean): CandidateReport => {
|
|
1317
1439
|
const motion = measured === null ? null : measured.section;
|
|
1318
1440
|
// P6: the geometry verdict pass (every required row measured and passing), and — when motion is required — the motion verdict pass.
|
|
1319
1441
|
const accepted = geometry.geometry !== null && geometry.geometry.verdict === 'pass' && (!input.motionRequired || (motion !== null && motion.verdict === 'pass'));
|
|
@@ -1354,7 +1476,7 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1354
1476
|
: {}
|
|
1355
1477
|
: { overBound: { maxSamples: overBound.maxSamples, triangles: overBound.triangles } }),
|
|
1356
1478
|
};
|
|
1357
|
-
|
|
1479
|
+
const report: MeshQualityReport = {
|
|
1358
1480
|
spec: MESH_QUALITY_REPORT_SPEC,
|
|
1359
1481
|
operation: 'compare',
|
|
1360
1482
|
effective,
|
|
@@ -1365,4 +1487,6 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1365
1487
|
candidates: candidates.map((c, i) => reportOf(c, candGeometry[i], candidateMotions[i], true)),
|
|
1366
1488
|
termination: null,
|
|
1367
1489
|
};
|
|
1490
|
+
// The prepared geometry, pose and reference rows live across calls. A caller may edit a returned report.
|
|
1491
|
+
return prepared === undefined ? report : structuredClone(report);
|
|
1368
1492
|
}
|
package/src/rig.ts
CHANGED
|
@@ -471,8 +471,8 @@ export interface RigDepthMap {
|
|
|
471
471
|
*/
|
|
472
472
|
near: 'white' | 'black';
|
|
473
473
|
/**
|
|
474
|
-
* How many
|
|
475
|
-
*
|
|
474
|
+
* How many units the map's full 0..1 range spans: setup world units on an
|
|
475
|
+
* authored mesh, the generator's bind units on a generated mesh.
|
|
476
476
|
*
|
|
477
477
|
* Authored, never measured: 8 bits of level say nothing about scale, so a
|
|
478
478
|
* compiler that picked one would be inventing the depth of the art.
|
|
@@ -971,6 +971,8 @@ export interface RigMeshAttachment {
|
|
|
971
971
|
color?: string;
|
|
972
972
|
/** Build the geometry instead of authoring it — see `RigMeshGenerator`. */
|
|
973
973
|
generator?: RigMeshGenerator;
|
|
974
|
+
/** A setup-world depth turn over authored geometry; sampled in the part's pixel grid. */
|
|
975
|
+
depth?: RigDepthMap;
|
|
974
976
|
/** A numbered image series over this one triangulation — see `RigSequence`. */
|
|
975
977
|
sequence?: RigSequence;
|
|
976
978
|
}
|
|
@@ -2077,7 +2079,7 @@ export const RIG_KEYS = {
|
|
|
2077
2079
|
RigRegionAttachment: ['type', 'name', 'path', 'image', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'width', 'height', 'color', 'sequence'],
|
|
2078
2080
|
RigMeshAttachment: [
|
|
2079
2081
|
'type', 'name', 'path', 'image', 'uvs', 'triangles', 'vertices', 'weights', 'boneIndexing', 'hull', 'edges',
|
|
2080
|
-
'width', 'height', 'color', 'generator', 'sequence',
|
|
2082
|
+
'width', 'height', 'color', 'generator', 'depth', 'sequence',
|
|
2081
2083
|
],
|
|
2082
2084
|
RigLinkedMeshAttachment: [
|
|
2083
2085
|
'type', 'name', 'path', 'image', 'source', 'slot', 'skin', 'timelines', 'width', 'height', 'color', 'sequence',
|
|
@@ -2201,7 +2203,7 @@ export const RIG_TYPES = {
|
|
|
2201
2203
|
RigMeshAttachment: {
|
|
2202
2204
|
type: 'enum', name: 'string', path: 'string', image: 'string', uvs: 'number[]', triangles: 'number[]',
|
|
2203
2205
|
vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', hull: 'number', edges: 'number[]',
|
|
2204
|
-
width: 'number', height: 'number', color: 'string', generator: 'object', sequence: 'object',
|
|
2206
|
+
width: 'number', height: 'number', color: 'string', generator: 'object', depth: 'object', sequence: 'object',
|
|
2205
2207
|
},
|
|
2206
2208
|
RigLinkedMeshAttachment: {
|
|
2207
2209
|
type: 'enum', name: 'string', path: 'string', image: 'string', source: 'string', slot: 'string', skin: 'string',
|
|
@@ -2557,6 +2559,7 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{
|
|
|
2557
2559
|
at(binding, 'RigMeshBinding', `${who} weights[${i}][${j}]`, [...here, 'weights', i, j]);
|
|
2558
2560
|
}
|
|
2559
2561
|
}
|
|
2562
|
+
at(att.depth, 'RigDepthMap', `${who} depth`, [...here, 'depth']);
|
|
2560
2563
|
if (!isObj(att.generator)) continue;
|
|
2561
2564
|
const gen = att.generator;
|
|
2562
2565
|
const kind = RIG_GENERATOR_KINDS.find((k) => k === gen.kind);
|
package/src/runtime/index.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* corpus through both at the oracle's samples and compares full doubles at
|
|
22
22
|
* tolerance 0 (*The runtime poser* there). The oracle is not changed for it.
|
|
23
23
|
*
|
|
24
|
-
* ## Stage
|
|
24
|
+
* ## Stage 1: what it poses, and what it refuses
|
|
25
25
|
*
|
|
26
26
|
* Bones and their five inherit modes; the rotate, translate, scale and shear
|
|
27
27
|
* timelines; a slot's attachment and colour (`rgba`, `rgb`, `alpha`)
|
|
@@ -32,10 +32,14 @@
|
|
|
32
32
|
* looping track. Everything else is refused at load by name with the list of
|
|
33
33
|
* what is missing (`./model.ts`'s header and `RUNTIME_FEATURES`); a consumer
|
|
34
34
|
* falls back to the oracle for a refused model or animation, so nothing breaks
|
|
35
|
-
* while coverage grows.
|
|
35
|
+
* while coverage grows. Named skin views and their required bones are posed;
|
|
36
|
+
* to switch, compile the new underSkin/underNoSkin view at load and restore the
|
|
37
|
+
* animation/time and event cursor with newPose.restore(name, old.trackTime,
|
|
38
|
+
* old.time). Restoration delivers no already-passed cue. `pose.events` borrows immutable seven-field
|
|
39
|
+
* deliveries over the last interval, in oracle order, until the next apply/step.
|
|
36
40
|
*
|
|
37
41
|
* ```ts
|
|
38
|
-
* const model = compileRuntime(underNoSkin(readModel(text))
|
|
42
|
+
* const model = compileRuntime(underNoSkin(readModel(text)));
|
|
39
43
|
* const pose = new RuntimePose(model);
|
|
40
44
|
* pose.setAnimation('walk');
|
|
41
45
|
* pose.step(1 / 60); // or pose.apply(t)
|
|
@@ -76,5 +80,5 @@ export {
|
|
|
76
80
|
L_SHEAR_X,
|
|
77
81
|
L_SHEAR_Y,
|
|
78
82
|
} from './model.ts';
|
|
79
|
-
export type { RuntimeAnimation, RuntimeAttachment, RuntimeFeature, RuntimeMissing, RuntimeModel, RuntimeOptions, RuntimePlant, RuntimeSlot } from './model.ts';
|
|
83
|
+
export type { RuntimeAnimation, RuntimeEvent, RuntimeAttachment, RuntimeFeature, RuntimeMissing, RuntimeModel, RuntimeOptions, RuntimePlant, RuntimeSlot } from './model.ts';
|
|
80
84
|
export { RuntimePose } from './pose.ts';
|
package/src/runtime/model.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The runtime poser's load-time half (issue #1591, stage
|
|
2
|
+
* The runtime poser's load-time half (issue #1591, stage 1): a skin view of
|
|
3
3
|
* the compiled model (`readModel`, then `underSkin` / `underNoSkin` — the
|
|
4
4
|
* oracle's reader and its views, read and never written) compiled once into
|
|
5
5
|
* index-resolved bones and slots, a constraint plan, compiled timelines and
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* (`RuntimeMissing`), and:
|
|
13
13
|
*
|
|
14
14
|
* - **refuses the model** (`RuntimeRefusal`, every missing feature listed)
|
|
15
|
-
* when one is a property of the whole model — a
|
|
16
|
-
* path, physics or slider constraint, a transform constraint reading or
|
|
15
|
+
* when one is a property of the whole model — a path, physics or slider constraint, a transform constraint reading or
|
|
17
16
|
* writing local values or adding (`localSource`, `localTarget`,
|
|
18
17
|
* `additive`), an ik or transform constraint over a bone the view leaves
|
|
19
18
|
* inactive, a world-space transform onto a `noScale` or
|
|
@@ -23,16 +22,15 @@
|
|
|
23
22
|
* - **refuses an animation** (`RuntimeModel.refused`; `animation(name)`
|
|
24
23
|
* throws the same `RuntimeRefusal`) when the construct is the animation's
|
|
25
24
|
* — an `inherit` timeline, a deform or sequence timeline, a draw-order
|
|
26
|
-
* timeline,
|
|
25
|
+
* timeline, a dark-colour (`rgba2`, `rgb2`) timeline, or an
|
|
27
26
|
* attachment key showing a clipping attachment or a region with no atlas
|
|
28
27
|
* rectangle. Every other animation of the model is posed.
|
|
29
28
|
*
|
|
30
|
-
* `RUNTIME_FEATURES` is the closed list of what can be named.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* a player that misses its cues and reports nothing.
|
|
29
|
+
* `RUNTIME_FEATURES` is the closed list of what can be named. Stage 1 reads
|
|
30
|
+
* named/no-skin views and delivers event keys, with immutable payloads compiled
|
|
31
|
+
* at load. `ignore: ['events']` remains an explicit diagnostic opt-out; it is
|
|
32
|
+
* never the default. A merged view with conflicting attachments still refuses
|
|
33
|
+
* by name, as the oracle itself cannot resolve that view without file order.
|
|
36
34
|
*
|
|
37
35
|
* ## What the oracle decides, the runtime reads
|
|
38
36
|
*
|
|
@@ -48,18 +46,16 @@
|
|
|
48
46
|
* no randomness, no I/O.
|
|
49
47
|
*/
|
|
50
48
|
import type { ModelVertices } from '../model.ts';
|
|
51
|
-
import { activeBones, constraintRecords, CORE_REGION_KINDS, foldInheritMode, readBlend, readColour, shownAttachment, shownRow, sourceOfDoc, type CompiledDocument, type CoreAnimation, type CoreBlendMode } from '../core/index.ts';
|
|
49
|
+
import { activeBones, constraintRecords, CORE_REGION_KINDS, foldInheritMode, readBlend, readColour, shownAttachment, shownRow, sourceOfDoc, underSkin, type CompiledDocument, type CoreAnimation, type CoreBlendMode } from '../core/index.ts';
|
|
52
50
|
import { constraintInactiveWhy, constraintsAbsentWhy, TRANSFORM_PROPERTIES, type CoreIkRecord, type CoreTransformRecord } from '../core/constraints.ts';
|
|
53
|
-
import { CORE_DEFAULT_SKIN } from '../core/skins.ts';
|
|
54
51
|
import { RUNTIME_PI } from '../core/world.ts';
|
|
55
52
|
import { compileCurve, type RuntimeCurve } from './curves.ts';
|
|
56
53
|
|
|
57
54
|
/** The stage of issue #1591 this poser implements. */
|
|
58
|
-
export const RUNTIME_STAGE =
|
|
55
|
+
export const RUNTIME_STAGE = 1;
|
|
59
56
|
|
|
60
57
|
/** Every construct the runtime poser can refuse, by the name its refusals carry. */
|
|
61
58
|
export const RUNTIME_FEATURES = [
|
|
62
|
-
'skin',
|
|
63
59
|
'path-constraint',
|
|
64
60
|
'physics-constraint',
|
|
65
61
|
'slider-constraint',
|
|
@@ -75,7 +71,6 @@ export const RUNTIME_FEATURES = [
|
|
|
75
71
|
'deform-timeline',
|
|
76
72
|
'sequence-timeline',
|
|
77
73
|
'draw-order-timeline',
|
|
78
|
-
'events',
|
|
79
74
|
'dark-colour-timeline',
|
|
80
75
|
] as const;
|
|
81
76
|
export type RuntimeFeature = (typeof RUNTIME_FEATURES)[number];
|
|
@@ -124,6 +119,12 @@ export interface RuntimePlant {
|
|
|
124
119
|
readonly admit?: readonly RuntimeFeature[];
|
|
125
120
|
/** The update order's scratch sized by the bone count instead of the longest target list: the short-buffer plant. */
|
|
126
121
|
readonly scratchByBones?: boolean;
|
|
122
|
+
/** Wrong-skin source selected by a control, never by a consumer. */
|
|
123
|
+
readonly attachmentSkin?: string;
|
|
124
|
+
/** Drops a key at zero on a wrap, the event boundary plant. */
|
|
125
|
+
readonly eventWrapOffByOne?: boolean;
|
|
126
|
+
/** The old restore behavior, which repeats keys up to the restored time. */
|
|
127
|
+
readonly restoreEventsFromStart?: boolean;
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
// A bone's local values, seven per bone.
|
|
@@ -277,6 +278,17 @@ export interface RuntimeConstraintTimeline {
|
|
|
277
278
|
readonly flags: Uint8Array | null;
|
|
278
279
|
}
|
|
279
280
|
|
|
281
|
+
/** One immutable event delivery, reused by every pose of this model. */
|
|
282
|
+
export interface RuntimeEvent {
|
|
283
|
+
readonly name: string;
|
|
284
|
+
readonly time: number;
|
|
285
|
+
readonly int: number;
|
|
286
|
+
readonly float: number;
|
|
287
|
+
readonly string: string;
|
|
288
|
+
readonly volume: number;
|
|
289
|
+
readonly balance: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
280
292
|
/** One covered animation, compiled. */
|
|
281
293
|
export interface RuntimeAnimation {
|
|
282
294
|
readonly name: string;
|
|
@@ -287,12 +299,16 @@ export interface RuntimeAnimation {
|
|
|
287
299
|
readonly constraints: ReadonlyArray<RuntimeConstraintTimeline | null>;
|
|
288
300
|
/** How many key cursors a pose keeps for it. */
|
|
289
301
|
readonly cursors: number;
|
|
290
|
-
/**
|
|
302
|
+
/** Number of authored event keys (including an explicitly muted diagnostic). */
|
|
291
303
|
readonly events: number;
|
|
304
|
+
readonly eventKeys: readonly RuntimeEvent[];
|
|
292
305
|
}
|
|
293
306
|
|
|
294
307
|
/** The compiled model a pose is made of (`compileRuntime`). Read-only once built. */
|
|
295
308
|
export interface RuntimeModel {
|
|
309
|
+
/** Control-only boundary defect; fixed at load. */
|
|
310
|
+
readonly eventWrapOffByOne: boolean;
|
|
311
|
+
readonly restoreEventsFromStart: boolean;
|
|
296
312
|
readonly doc: CompiledDocument;
|
|
297
313
|
readonly boneNames: readonly string[];
|
|
298
314
|
readonly parents: Int32Array;
|
|
@@ -365,7 +381,6 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
365
381
|
const refusals = new Refusals(new Set(plant.admit ?? []));
|
|
366
382
|
const ignoreEvents = (options.ignore ?? []).includes('events');
|
|
367
383
|
|
|
368
|
-
if (doc.skins.some((k) => k.name !== CORE_DEFAULT_SKIN)) refusals.note('skin', `the document declares skins [${doc.skins.map((k) => k.name).join(', ')}]; stage ${RUNTIME_STAGE} poses the default skin alone`);
|
|
369
384
|
const absentWhy = constraintsAbsentWhy(doc);
|
|
370
385
|
if (absentWhy !== null) refusals.note('oracle-absent', `the oracle leaves the bones out: ${absentWhy}`);
|
|
371
386
|
|
|
@@ -440,6 +455,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
440
455
|
const attachments: RuntimeAttachment[] = [];
|
|
441
456
|
let vertexCount = 0;
|
|
442
457
|
const sourceOf = sourceOfDoc(doc);
|
|
458
|
+
const attachmentView = plant.attachmentSkin === undefined ? doc : underSkin(doc, plant.attachmentSkin);
|
|
443
459
|
/** Per slot, each placeholder's attachment index (−1 shows nothing) and what it misses, resolved once and its refusal noted for every animation that shows it. */
|
|
444
460
|
const resolvedBySlot = doc.slots.map(() => new Map<string | null, { index: number; missing: readonly [RuntimeFeature, string] | null }>());
|
|
445
461
|
const resolve = (si: number, placeholder: string | null, animation: string | null): number => {
|
|
@@ -454,7 +470,7 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
454
470
|
};
|
|
455
471
|
const resolveRecord = (si: number, placeholder: string | null): { index: number; missing: readonly [RuntimeFeature, string] | null } => {
|
|
456
472
|
const slot = doc.slots[si];
|
|
457
|
-
const shown = placeholder === null ? null : shownAttachment(
|
|
473
|
+
const shown = placeholder === null ? null : shownAttachment(attachmentView, slot, placeholder);
|
|
458
474
|
if (shown === null) return { index: -1, missing: null };
|
|
459
475
|
if ('conflict' in shown) return { index: -1, missing: ['skin-conflict', `slot "${slot.name}" placeholder "${placeholder}" is filled by skins that disagree`] };
|
|
460
476
|
const row = shownRow(shown);
|
|
@@ -538,6 +554,8 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
538
554
|
if (refusals.model.length > 0) throw new RuntimeRefusal(refusals.model);
|
|
539
555
|
const names = doc.animations.map((a) => a.name);
|
|
540
556
|
return {
|
|
557
|
+
eventWrapOffByOne: plant.eventWrapOffByOne === true,
|
|
558
|
+
restoreEventsFromStart: plant.restoreEventsFromStart === true,
|
|
541
559
|
doc,
|
|
542
560
|
boneNames: doc.bones.map((b) => b.name),
|
|
543
561
|
parents,
|
|
@@ -667,7 +685,6 @@ function compileAnimation(
|
|
|
667
685
|
if (deform.length > 0) refusals.note('deform-timeline', `${deform.length} deform timeline(s): ${deform.slice(0, 3).map((a) => `${a.skin}/${a.slot}/${a.attachment}`).join(', ')}${deform.length > 3 ? ', …' : ''}`, name);
|
|
668
686
|
if (sequence.length > 0) refusals.note('sequence-timeline', `${sequence.length} sequence timeline(s): ${sequence.slice(0, 3).map((a) => `${a.skin}/${a.slot}/${a.attachment}`).join(', ')}${sequence.length > 3 ? ', …' : ''}`, name);
|
|
669
687
|
if (tl.drawOrder.length > 0) refusals.note('draw-order-timeline', `${tl.drawOrder.length} draw-order key(s)`, name);
|
|
670
|
-
if (tl.events.length > 0 && !ignoreEvents) refusals.note('events', `${tl.events.length} event key(s); a poser that fires none is handed out only on request (ignore: ['events'])`, name);
|
|
671
688
|
|
|
672
689
|
const lastBy = <T extends { name: string }>(targets: readonly T[]): Map<string, T> => new Map(targets.map((t) => [t.name, t]));
|
|
673
690
|
const bones: RuntimeBoneTimeline[] = [];
|
|
@@ -715,5 +732,5 @@ function compileAnimation(
|
|
|
715
732
|
constraintTimelines[c.index] = { constraint: c.index, curve: compileCurve(keyed.keys, TRANSFORM_PROPERTIES.length), cursor: cursors++, flags: null };
|
|
716
733
|
}
|
|
717
734
|
}
|
|
718
|
-
return { name, duration: tl.duration, bones, slots: slotTimelines, constraints: constraintTimelines, cursors, events: tl.events.length };
|
|
735
|
+
return { name, duration: tl.duration, bones, slots: slotTimelines, constraints: constraintTimelines, cursors, events: tl.events.length, eventKeys: Object.freeze(ignoreEvents ? [] : tl.events.map(key => Object.freeze({ name: key.name, time: key.time, int: key.int, float: key.float, string: key.string, volume: key.volume ?? 0, balance: key.balance ?? 0 }))) };
|
|
719
736
|
}
|
package/src/runtime/pose.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The runtime poser's per-instance half (issue #1591, stage
|
|
2
|
+
* The runtime poser's per-instance half (issue #1591, stage 1): one
|
|
3
3
|
* `RuntimePose` per skeleton on screen, every buffer allocated when it is
|
|
4
4
|
* made and overwritten in place on every pose, so that posing a frame
|
|
5
5
|
* allocates nothing.
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
W_Y,
|
|
72
72
|
WORLDS,
|
|
73
73
|
type RuntimeAnimation,
|
|
74
|
+
type RuntimeEvent,
|
|
74
75
|
type RuntimeIk,
|
|
75
76
|
type RuntimeModel,
|
|
76
77
|
type RuntimeTransform,
|
|
@@ -114,6 +115,11 @@ export class RuntimePose {
|
|
|
114
115
|
trackTime = 0;
|
|
115
116
|
time = 0;
|
|
116
117
|
|
|
118
|
+
private readonly fired: RuntimeEvent[] = [];
|
|
119
|
+
/** Borrowed immutable event records, in delivery order, until the next apply/step. */
|
|
120
|
+
get events(): readonly RuntimeEvent[] { return this.fired; }
|
|
121
|
+
private eventTime = -1;
|
|
122
|
+
|
|
117
123
|
private current: RuntimeAnimation | null = null;
|
|
118
124
|
private readonly cursors: Int32Array;
|
|
119
125
|
// The constraints' values as posed: an ik's mix, softness and flags, a transform's six mixes.
|
|
@@ -160,12 +166,30 @@ export class RuntimePose {
|
|
|
160
166
|
this.cursors.fill(-1);
|
|
161
167
|
this.trackTime = 0;
|
|
162
168
|
this.time = 0;
|
|
169
|
+
this.eventTime = -1;
|
|
170
|
+
this.fired.length = 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Restore one animation's two clocks without delivering keys already passed.
|
|
175
|
+
* A skin/view replacement uses the old pose's trackTime and wrapped time.
|
|
176
|
+
* Unlike setAnimation + apply, restoration never replays the initial interval.
|
|
177
|
+
*/
|
|
178
|
+
restore(name: string, trackTime: number, animationTime: number): void {
|
|
179
|
+
if (!Number.isFinite(trackTime)) throw new RuntimeInputError(`restore: track time ${trackTime} is not finite`);
|
|
180
|
+
if (!Number.isFinite(animationTime)) throw new RuntimeInputError(`restore: animation time ${animationTime} is not finite`);
|
|
181
|
+
this.setAnimation(name);
|
|
182
|
+
this.trackTime = trackTime;
|
|
183
|
+
if (!this.model.restoreEventsFromStart) this.eventTime = animationTime;
|
|
184
|
+
this.apply(animationTime);
|
|
163
185
|
}
|
|
164
186
|
|
|
165
187
|
/** The setup pose: every constraint applied with its own values, every slot as it is set up. */
|
|
166
188
|
setupPose(): void {
|
|
167
189
|
this.current = null;
|
|
168
190
|
this.time = 0;
|
|
191
|
+
this.eventTime = -1;
|
|
192
|
+
this.fired.length = 0;
|
|
169
193
|
this.locals.set(this.model.setup);
|
|
170
194
|
this.hierarchy();
|
|
171
195
|
this.constraintValues(null, 0);
|
|
@@ -178,6 +202,7 @@ export class RuntimePose {
|
|
|
178
202
|
const anim = this.current;
|
|
179
203
|
if (anim === null) throw new RuntimeInputError('apply: no animation is set (setAnimation first)');
|
|
180
204
|
if (!Number.isFinite(t)) throw new RuntimeInputError(`apply: the time ${t} is not finite`);
|
|
205
|
+
this.collectEvents(anim, t);
|
|
181
206
|
this.time = t;
|
|
182
207
|
this.boneTimelines(anim, t);
|
|
183
208
|
this.hierarchy();
|
|
@@ -194,6 +219,21 @@ export class RuntimePose {
|
|
|
194
219
|
this.apply(anim.duration === 0 ? 0 : this.trackTime % anim.duration);
|
|
195
220
|
}
|
|
196
221
|
|
|
222
|
+
/** The oracle interval (last,t], tail then head on a wrap; never copies a payload. */
|
|
223
|
+
private collectEvents(anim: RuntimeAnimation, t: number): void {
|
|
224
|
+
const last = this.eventTime;
|
|
225
|
+
const wrapped = t < last;
|
|
226
|
+
const keys = anim.eventKeys;
|
|
227
|
+
let count = 0;
|
|
228
|
+
if (wrapped) for (let i = 0; i < keys.length; i++) if (keys[i].time > last) this.fired[count++] = keys[i];
|
|
229
|
+
for (let i = 0; i < keys.length; i++) {
|
|
230
|
+
const key = keys[i];
|
|
231
|
+
if (key.time <= t && (wrapped ? !this.model.eventWrapOffByOne || key.time > 0 : key.time > last)) this.fired[count++] = key;
|
|
232
|
+
}
|
|
233
|
+
this.fired.length = count;
|
|
234
|
+
this.eventTime = t;
|
|
235
|
+
}
|
|
236
|
+
|
|
197
237
|
// ---------------------------------------------------------------------------
|
|
198
238
|
// the bones
|
|
199
239
|
// ---------------------------------------------------------------------------
|