@rig-c/core 2.77.0 → 2.79.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 +58 -13
- package/package.json +1 -1
- package/src/runtime/clip_draw.ts +52 -0
- package/src/runtime/clipping.ts +223 -0
- package/src/runtime/curves.ts +16 -7
- package/src/runtime/index.ts +6 -5
- package/src/runtime/model.ts +82 -37
- package/src/runtime/pose.ts +109 -33
package/docs/AUTHORING.md
CHANGED
|
@@ -12647,7 +12647,7 @@ verdict on those figures.
|
|
|
12647
12647
|
| <a id="CE_COMPILE_ALPHA_CLIP_CONFLICT"></a>`CE_COMPILE_ALPHA_CLIP_CONFLICT` | Names authored fields or convex/inverse flags conflicting with generated inside-clipping geometry. Use one authored/generated form.<br>Exact fixed diagnostic chunks (named values between chunks vary): ⟦: alphaClip conflicts with [⟧<br>⟦vertexCount⟧<br>⟦vertices⟧<br>⟦weights⟧<br>⟦boneIndexing⟧<br>⟦, ⟧<br>⟦], convex=⟧<br>⟦, inverse=⟧<br>⟦; required ordinary inside clipping with generated geometry only. Remove authored geometry/flags.⟧ |
|
|
12648
12648
|
| <a id="CE_COMPILE_ALPHA_CLIP_DEFORM"></a>`CE_COMPILE_ALPHA_CLIP_DEFORM` | Names animation and source/clip key/frame counts that cannot share the existing projection contract. Give matching explicit supported transform keys in compatible actual frames, with constant world shares where needed, or author the corresponding clip geometry and motion; weighted binding does not copy source deform.<br>Exact fixed diagnostic chunks (named values between chunks vary): ⟦ alphaClip from "⟧<br>⟦"⟧<br>⟦: animation "⟧<br>⟦" has ⟧<br>⟦ source and ⟧<br>⟦ clip keys; source frame=⟧<br>⟦no vertex geometry⟧<br>⟦clip frame=⟧<br>⟦, constant world influence shares=⟧<br>⟦Required matching explicit affine/depth-yaw/pitch transforms in compatible DeformGeometry frames, ⟧<br>⟦with constant shares for world-space transfer. Fix the named frame/keys or author corresponding clip motion.⟧ |
|
|
12649
12649
|
|
|
12650
|
-
## 15. Posing in place for a player — the runtime poser (stage
|
|
12650
|
+
## 15. Posing in place for a player — the runtime poser (stage 9)
|
|
12651
12651
|
|
|
12652
12652
|
rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
|
|
12653
12653
|
(`src/core/`) is the reference: each pose is a pure function of the document,
|
|
@@ -12673,8 +12673,8 @@ pose.setAnimation('walk'); // a refused animat
|
|
|
12673
12673
|
pose.step(1 / 60); // or pose.apply(t) at an animation time
|
|
12674
12674
|
pose.computeVertices();
|
|
12675
12675
|
// pose.world: 6 numbers per bone (a, b, c, d, worldX, worldY — W_A … W_Y), y up
|
|
12676
|
-
// pose.light: 4 per slot; pose.shown: per slot an index into model.attachments, or −1
|
|
12677
|
-
// pose.vertices: model.attachments[i].count numbers at model.attachments[i].offset
|
|
12676
|
+
// pose.light: 4 per slot; pose.dark: 3 per slot; pose.shown: per slot an index into model.attachments, or −1
|
|
12677
|
+
// pose.drawOrder: slot indices in the live frame's draw order; pose.vertices: model.attachments[i].count numbers at model.attachments[i].offset
|
|
12678
12678
|
```
|
|
12679
12679
|
|
|
12680
12680
|
Each `model.attachments[i]` also carries readonly `skin` and `placeholder`: the
|
|
@@ -12682,16 +12682,31 @@ exact resolved table identity `(skin, model.slots[attachment.slot].name, placeho
|
|
|
12682
12682
|
The display `name` may be shared by different placeholders; it is never a table
|
|
12683
12683
|
key to guess the source from. These load-time strings do not change the pose path.
|
|
12684
12684
|
|
|
12685
|
-
| | what stage
|
|
12685
|
+
| | what stage 9 poses |
|
|
12686
12686
|
| --- | --- |
|
|
12687
12687
|
| 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) |
|
|
12688
|
-
| timelines | `rotate`, `translate(x/y)`, `scale(x/y)`, `shear(x/y)`; a slot's `attachment`, `rgba`, `rgb` and `
|
|
12689
|
-
| constraints | physics with its exact clock, reset and parameter timelines, and optional constraint-scoped external translate/rotate; value-mapped sliders with bone/slot/deform/physics composition; path constraints across all position, spacing and rotate modes on open or closed, constant-speed or arc-length, weighted or unweighted, deformed curves; one- and two-bone ik;
|
|
12688
|
+
| timelines | `rotate`, `translate(x/y)`, `scale(x/y)`, `shear(x/y)`; a slot's `attachment`, light colour (`rgba`, `rgb`, `alpha`) and dark colour (`rgba2`, `rgb2`); `drawOrder`; ik, transform and path timelines; linear, stepped and Bézier keys, each read from a per-pose key cursor |
|
|
12689
|
+
| constraints | physics with its exact clock, reset and parameter timelines, and optional constraint-scoped external translate/rotate; value-mapped sliders with bone/slot/draw-order/deform/physics composition; path constraints across all position, spacing and rotate modes on open or closed, constant-speed or arc-length, weighted or unweighted, deformed curves; one- and two-bone ik; absolute/additive transform constraints with local or world sources and targets; the document's order, each followed by the bones below what it moved posed again |
|
|
12690
12690
|
| vertices | regions, meshes, weighted meshes and linked meshes, written into the pose's buffer |
|
|
12691
12691
|
| deform | weighted and unweighted mesh and path timelines, linear/stepped/Bézier, per-pose key cursors and in-place buffers; linked meshes inherit their source's timelines only with `timelines: true` |
|
|
12692
12692
|
| 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 |
|
|
12693
12693
|
| 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) |
|
|
12694
12694
|
|
|
12695
|
+
Clipping draw output is read by `pose.computeClipped()`. It recomputes vertices
|
|
12696
|
+
and overwrites `pose.clipping`'s preallocated `clipSlots`, `drawSlots` and per-slot
|
|
12697
|
+
`outputs`, with `clipCount` and `drawCount` delimiting live rows. `pose.clipping`
|
|
12698
|
+
is null on a model without clipping. Only draws under an active clip appear
|
|
12699
|
+
there; this is not the complete draw list. Each output has vertex/triangle
|
|
12700
|
+
counts, float32 coordinates and UVs, triangle indices, source triangle indices
|
|
12701
|
+
and the cut flag. Slot-end/start order follows `poseClipped`; polygon planning
|
|
12702
|
+
and arithmetic follow `clipShapeOf`, `clipThrough` and `clipTriangles`.
|
|
12703
|
+
Concave/inverse rows follow the core's drawn coverage. The oracle's native
|
|
12704
|
+
triangle list is absent there, so the third dumper refuses that reading as
|
|
12705
|
+
`oracle-absent` rather than relabeling coverage. Self-crossing, unsupported
|
|
12706
|
+
repeated vertices, collapsed polygons and non-finite clipped coordinates or
|
|
12707
|
+
UVs are rejected when the polygon is prepared or clipped, as the core rejects
|
|
12708
|
+
those inputs; no arbitrary-polygon or singular-input coverage is claimed.
|
|
12709
|
+
|
|
12695
12710
|
Deform keys are expanded once at load. Unweighted keys hold float32 positions,
|
|
12696
12711
|
`fround(fround(setup) + fround(offset))`; weighted keys hold float32 offsets,
|
|
12697
12712
|
**one x/y pair per influence**, in vertex then influence order. At a pose,
|
|
@@ -12761,15 +12776,47 @@ values and blends from the current pose. Ordinary tracks still read only the
|
|
|
12761
12776
|
last target for each bone. An earlier unsupported inherit target is refused
|
|
12762
12777
|
when a slider applies it, even if the track's last target would hide it.
|
|
12763
12778
|
Absolute/additive bone and
|
|
12764
|
-
deform blends and slot switches follow the oracle, including direct scale and
|
|
12765
|
-
colour targets at mix one. Slider physics writes last one pass and affect only
|
|
12779
|
+
deform blends, draw-order keys and slot switches follow the oracle, including direct scale and
|
|
12780
|
+
light/dark colour targets at mix one. Slider physics writes last one pass and affect only
|
|
12766
12781
|
constraints later in update order. An unsupported timeline in a slider's
|
|
12767
12782
|
animation refuses the whole model under that timeline's existing feature name,
|
|
12768
12783
|
so an unselected track cannot hide a construct the slider always applies.
|
|
12769
12784
|
|
|
12785
|
+
Transform constraints read each source property from the current local
|
|
12786
|
+
values (`localSource: true`) or world matrix, including earlier constraints'
|
|
12787
|
+
read-back, apply offsets and cross-mappings in declaration order, then clamp and
|
|
12788
|
+
blend by the keyed property mix. `localTarget: true` writes local values and
|
|
12789
|
+
re-poses the changed bones and their descendants in bone order without a world
|
|
12790
|
+
read-back; world targets keep their edited world matrices and read back locals.
|
|
12791
|
+
A zero mix leaves that property untouched. Both local and world targets accept
|
|
12792
|
+
all five inherit modes. For a parented `noScale` or `noScaleOrReflection` world
|
|
12793
|
+
target, read-back derives rotation from the parent's adjugate and the edited
|
|
12794
|
+
world x column, rebuilds that mode's normalised frame, and decomposes the world
|
|
12795
|
+
columns in it. The residual x angle is kept as `shearX`; `noScale` preserves a
|
|
12796
|
+
parent reflection and `noScaleOrReflection` removes it. This is the oracle's
|
|
12797
|
+
`localFromWorld`, `noScaleDirection` and `decompose` order; subsequent local
|
|
12798
|
+
readers and parent edits observe those same locals. Stage-seven controls
|
|
12799
|
+
`RP43`–`RP46` hold both modes to the oracle with omission and allocation plants.
|
|
12800
|
+
With `additive: true`, a local position, rotation or shear adds `v * mix`
|
|
12801
|
+
and a local scale multiplies by `1 + (v - 1) * mix`. A world rotation turns by
|
|
12802
|
+
`v * RAD` wrapped once at the runtime pi before applying mix; world translation
|
|
12803
|
+
adds `v * mix`, and world scale multiplies by `1 + (v - 1) * mix` (a zero-length
|
|
12804
|
+
column is left alone). World additive shear uses `(v + 90) * RAD - 90 * RAD`
|
|
12805
|
+
without wrapping, then adds the mixed angle to the existing y-column angle.
|
|
12806
|
+
That subtraction order is kept exactly, as in the oracle's `applyWorld`;
|
|
12807
|
+
replacing it with `v * RAD` is not bit-equivalent. Source reading, mappings,
|
|
12808
|
+
clamp, keyed mixes and the final union repose retain `solveTransform` order.
|
|
12809
|
+
Stage-nine controls `RP53`–`RP56` compare buffered primitives and default
|
|
12810
|
+
admission to the oracle, require every omission to differ, and measure pose
|
|
12811
|
+
stepping plus clipping with a per-frame object plant. Stage-eight controls
|
|
12812
|
+
`RP47`–`RP52` hold all four source/target forms to the
|
|
12813
|
+
oracle, with an omission plant and the steady-state allocation meter.
|
|
12814
|
+
Stage-six controls `RP38`–`RP42` hold local-source, local-target
|
|
12815
|
+
and both forms to the oracle with omission and allocation plants.
|
|
12816
|
+
|
|
12770
12817
|
**Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
|
|
12771
12818
|
every construct met, each `{ feature, detail, animation }`, and the message
|
|
12772
|
-
reads `not posed by the runtime poser (stage
|
|
12819
|
+
reads `not posed by the runtime poser (stage 9), N construct(s) missing:
|
|
12773
12820
|
<feature>: <detail>; …`. A model-level feature refuses the whole model; an
|
|
12774
12821
|
animation-level one refuses that animation alone (`model.refused`;
|
|
12775
12822
|
`model.animation(name)` and `setAnimation(name)` throw its refusal) and every
|
|
@@ -12778,13 +12825,11 @@ refused: nothing is posed approximately.
|
|
|
12778
12825
|
|
|
12779
12826
|
| feature | refuses | what it is |
|
|
12780
12827
|
| --- | --- | --- |
|
|
12781
|
-
| `local-transform`, `additive-transform` | the model | a transform constraint with `localSource` or `localTarget`, or `additive` |
|
|
12782
12828
|
| `inactive-constrained-bone` | the model | an applied ik, transform or path constraint naming a bone the view leaves inactive (what the oracle writes there depends on the pass before) |
|
|
12783
|
-
| `
|
|
12784
|
-
| `clipping`, `null-atlas-region` | the model when shown at setup, else the animation that keys it | a clipping attachment; a region whose atlas rectangle the model states as `null` |
|
|
12829
|
+
| `null-atlas-region` | the model when shown at setup, else the animation that keys it | a region whose atlas rectangle the model states as `null` |
|
|
12785
12830
|
| `skin-conflict` | as above | a placeholder the skins fill differently |
|
|
12786
12831
|
| `oracle-absent` | the model | a pose the oracle itself leaves out (`constraintsAbsentWhy`, `pathAnimationsWhy`, `steppedPreviousPassWhy`, `sliderDeformsWalkedPathWhy`) |
|
|
12787
|
-
| `inherit-timeline`, `sequence-timeline
|
|
12832
|
+
| `inherit-timeline`, `sequence-timeline` | the animation | a timeline of that kind |
|
|
12788
12833
|
|
|
12789
12834
|
A call it cannot answer throws `RuntimeInputError`, naming the call:
|
|
12790
12835
|
`apply: no animation is set (setAnimation first)`, `animation "x" is not one of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.79.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": {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Stage-nine buffered draw walk; RuntimePose owns the frame buffers. */
|
|
2
|
+
import type { RuntimeModel } from './model.ts';
|
|
3
|
+
import type { RuntimePose } from './pose.ts';
|
|
4
|
+
import { RuntimeClipper, RuntimeClipBuffer } from './clipping.ts';
|
|
5
|
+
/** Load once from the same model/view as the pose; frame() overwrites arrays and counts. */
|
|
6
|
+
export class RuntimeDrawClipper {
|
|
7
|
+
readonly clipSlots: Int32Array;
|
|
8
|
+
readonly drawSlots: Int32Array;
|
|
9
|
+
readonly outputs: readonly RuntimeClipBuffer[];
|
|
10
|
+
nativeRows = true;
|
|
11
|
+
clipCount = 0;
|
|
12
|
+
drawCount = 0;
|
|
13
|
+
private readonly triangles: readonly (Int32Array | null)[];
|
|
14
|
+
private readonly uvs: readonly (Float64Array | null)[];
|
|
15
|
+
private readonly clipper: RuntimeClipper;
|
|
16
|
+
constructor(readonly model: RuntimeModel) {
|
|
17
|
+
const triangles: Array<Int32Array | null> = [], uvs: Array<Float64Array | null> = [], maxTriangles = new Int32Array(model.slots.length);
|
|
18
|
+
let maxPolygon = 3;
|
|
19
|
+
for (const at of model.attachments) {
|
|
20
|
+
const t = at.clipTopology?.triangles ?? null, uv = at.clipTopology?.uvs ?? null;
|
|
21
|
+
if (at.clip !== undefined) maxPolygon = Math.max(maxPolygon, at.count / 2);
|
|
22
|
+
triangles.push(t); uvs.push(uv); if (t !== null) maxTriangles[at.slot] = Math.max(maxTriangles[at.slot], t.length / 3);
|
|
23
|
+
}
|
|
24
|
+
this.triangles = triangles; this.uvs = uvs; this.clipper = new RuntimeClipper(maxPolygon);
|
|
25
|
+
this.clipSlots = new Int32Array(model.slots.length); this.drawSlots = new Int32Array(model.slots.length);
|
|
26
|
+
this.outputs = Array.from(maxTriangles, n => new RuntimeClipBuffer(n, maxPolygon));
|
|
27
|
+
}
|
|
28
|
+
/** pose.computeVertices() must precede this walk. Omission is a diagnostic plant only. */
|
|
29
|
+
frame(pose: RuntimePose, omit = false): void {
|
|
30
|
+
if (pose.model !== this.model) throw new Error('runtime clip draw: pose differs from the prepared model');
|
|
31
|
+
this.clipCount = 0; this.drawCount = 0; this.nativeRows = true;
|
|
32
|
+
let active = false, end: string | null = null;
|
|
33
|
+
for (let i = 0; i < pose.drawOrder.length; i++) {
|
|
34
|
+
const s = pose.drawOrder[i], slot = this.model.slots[s], shown = pose.shown[s], at = shown < 0 ? null : this.model.attachments[shown];
|
|
35
|
+
if (at?.clip !== undefined) {
|
|
36
|
+
this.clipSlots[this.clipCount++] = s;
|
|
37
|
+
if (active && end === slot.name) active = false;
|
|
38
|
+
if (!omit && !active && this.model.active[at.bone] === 1) {
|
|
39
|
+
this.clipper.prepare(pose.vertices, at.offset, at.count, at.clip.inverse, at.clip.convex); this.nativeRows &&= this.clipper.nativeRows; active = true; end = at.clip.end;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (active && at !== null) {
|
|
44
|
+
const triangles = this.triangles[shown], uvs = this.uvs[shown];
|
|
45
|
+
if (triangles !== null && uvs !== null) {
|
|
46
|
+
this.clipper.clip(pose.vertices, at.offset, triangles, uvs, this.outputs[s]); this.drawSlots[this.drawCount++] = s;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (active && end === slot.name) active = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/** Buffered counterpart of clipShapeOf/clipThrough/clipTriangles in src/core/clipping.ts.
|
|
2
|
+
* RuntimePose.computeClipped overwrites these buffers for the live draw order.
|
|
3
|
+
* Scratch and outputs are allocated at load; a frame changes counts and array cells only.
|
|
4
|
+
* Concave/inverse output follows the core's drawn geometry, not an unclaimed Spine triangulation.
|
|
5
|
+
*/
|
|
6
|
+
export class RuntimeClipBuffer {
|
|
7
|
+
readonly vertices: Float32Array;
|
|
8
|
+
readonly uvs: Float32Array;
|
|
9
|
+
readonly triangles: Int32Array;
|
|
10
|
+
readonly sources: Int32Array;
|
|
11
|
+
vertexCount = 0;
|
|
12
|
+
triangleCount = 0;
|
|
13
|
+
clipped = false;
|
|
14
|
+
constructor(triangles: number, polygonVertices: number) {
|
|
15
|
+
const points = triangles * Math.max(3, polygonVertices * (polygonVertices + 3));
|
|
16
|
+
if (!Number.isSafeInteger(points) || points < 0) throw new Error('runtime clip: output capacity is not a safe non-negative integer');
|
|
17
|
+
this.vertices = new Float32Array(points * 2); this.uvs = new Float32Array(points * 2);
|
|
18
|
+
this.triangles = new Int32Array(points * 3); this.sources = new Int32Array(points);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class RuntimeClipper {
|
|
23
|
+
/** Whether the original world polygon has the oracle native convex triangle-list contract. */
|
|
24
|
+
nativeRows = false;
|
|
25
|
+
private readonly points: Float64Array;
|
|
26
|
+
private readonly ring: Int32Array;
|
|
27
|
+
private readonly work: Int32Array;
|
|
28
|
+
private readonly sorted: Int32Array;
|
|
29
|
+
private readonly hull: Int32Array;
|
|
30
|
+
private readonly pieces: Int32Array;
|
|
31
|
+
private readonly sizes: Int32Array;
|
|
32
|
+
private readonly merged: Int32Array;
|
|
33
|
+
private readonly regions: Float64Array;
|
|
34
|
+
private readonly regionSizes: Int32Array;
|
|
35
|
+
private readonly a: Float64Array;
|
|
36
|
+
private readonly b: Float64Array;
|
|
37
|
+
private readonly outside: Float64Array;
|
|
38
|
+
private readonly tri = new Float64Array(6);
|
|
39
|
+
private readonly uv = new Float64Array(6);
|
|
40
|
+
private regionsCount = 0;
|
|
41
|
+
private inverse = false;
|
|
42
|
+
private cut = false;
|
|
43
|
+
private n = 0;
|
|
44
|
+
constructor(readonly maxPolygonVertices: number) {
|
|
45
|
+
const n = Math.max(3, maxPolygonVertices);
|
|
46
|
+
this.points = new Float64Array(n * 2); this.ring = new Int32Array(n); this.work = new Int32Array(n);
|
|
47
|
+
this.sorted = new Int32Array(n); this.hull = new Int32Array(n * 2); this.pieces = new Int32Array(n * n);
|
|
48
|
+
this.sizes = new Int32Array(n); this.merged = new Int32Array(n * 2);
|
|
49
|
+
this.regions = new Float64Array(n * n * 2); this.regionSizes = new Int32Array(n);
|
|
50
|
+
this.a = new Float64Array((n * 2 + 16) * 2); this.b = new Float64Array(this.a.length); this.outside = new Float64Array(this.a.length);
|
|
51
|
+
}
|
|
52
|
+
private cross(a: number, b: number, c: number): number {
|
|
53
|
+
const p = this.points; return (p[b * 2] - p[a * 2]) * (p[c * 2 + 1] - p[a * 2 + 1]) - (p[b * 2 + 1] - p[a * 2 + 1]) * (p[c * 2] - p[a * 2]);
|
|
54
|
+
}
|
|
55
|
+
private area(indices: Int32Array, n: number): number {
|
|
56
|
+
const p = this.points; let area = 0;
|
|
57
|
+
for (let i = 0; i < n; i++) { const a = indices[i] * 2, b = indices[(i + 1) % n] * 2; area += p[a] * p[b + 1] - p[b] * p[a + 1]; }
|
|
58
|
+
return area;
|
|
59
|
+
}
|
|
60
|
+
private convex(indices: Int32Array, n: number, start = 0): boolean {
|
|
61
|
+
let sign = 0;
|
|
62
|
+
for (let i = 0; i < n; i++) {
|
|
63
|
+
const p = this.points, a = indices[start + i] * 2, b = indices[start + (i + 1) % n] * 2, c = indices[start + (i + 2) % n] * 2;
|
|
64
|
+
// convexWhy takes the second edge from b, unlike cross3 used by triangulation.
|
|
65
|
+
const cross = (p[b] - p[a]) * (p[c + 1] - p[b + 1]) - (p[b + 1] - p[a + 1]) * (p[c] - p[b]);
|
|
66
|
+
if (cross === 0) return false;
|
|
67
|
+
const next = Math.sign(cross); if (sign === 0) sign = next; else if (sign !== next) return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
private on(a: number, b: number, r: number): boolean {
|
|
72
|
+
const p = this.points; return Math.min(p[a * 2], p[b * 2]) <= p[r * 2] && p[r * 2] <= Math.max(p[a * 2], p[b * 2]) && Math.min(p[a * 2 + 1], p[b * 2 + 1]) <= p[r * 2 + 1] && p[r * 2 + 1] <= Math.max(p[a * 2 + 1], p[b * 2 + 1]);
|
|
73
|
+
}
|
|
74
|
+
private meet(a: number, b: number, c: number, d: number): boolean {
|
|
75
|
+
const d1 = this.cross(c, d, a), d2 = this.cross(c, d, b), d3 = this.cross(a, b, c), d4 = this.cross(a, b, d);
|
|
76
|
+
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) return true;
|
|
77
|
+
return d1 === 0 && this.on(c, d, a) || d2 === 0 && this.on(c, d, b) || d3 === 0 && this.on(a, b, c) || d4 === 0 && this.on(a, b, d);
|
|
78
|
+
}
|
|
79
|
+
private region(indices: Int32Array, n: number, r: number, start = 0): void {
|
|
80
|
+
const p = this.points; let area = 0;
|
|
81
|
+
for (let i = 0; i < n; i++) { const a = indices[start + i] * 2, b = indices[start + (i + 1) % n] * 2; area += p[a] * p[b + 1] - p[b] * p[a + 1]; }
|
|
82
|
+
const base = r * this.maxPolygonVertices * 2;
|
|
83
|
+
for (let i = 0; i < n; i++) { const j = indices[start + (area > 0 ? n - 1 - i : i)] * 2; this.regions[base + i * 2] = p[j]; this.regions[base + i * 2 + 1] = p[j + 1]; }
|
|
84
|
+
this.regionSizes[r] = n;
|
|
85
|
+
}
|
|
86
|
+
private makeHull(count: number): number {
|
|
87
|
+
const idx = this.sorted, p = this.points, h = this.hull;
|
|
88
|
+
for (let i = 0; i < count; i++) {
|
|
89
|
+
const v = this.ring[i]; let j = i;
|
|
90
|
+
while (j > 0) { const u = idx[j - 1]; if (p[u * 2] < p[v * 2] || p[u * 2] === p[v * 2] && p[u * 2 + 1] <= p[v * 2 + 1]) break; idx[j] = u; j--; }
|
|
91
|
+
idx[j] = v;
|
|
92
|
+
}
|
|
93
|
+
let used = 0;
|
|
94
|
+
for (let i = 0; i < count; i++) { const v = idx[i]; while (used >= 2 && this.cross(h[used - 2], h[used - 1], v) <= 0) used--; h[used++] = v; }
|
|
95
|
+
used--;
|
|
96
|
+
const lower = used;
|
|
97
|
+
for (let i = count - 1; i >= 0; i--) { const v = idx[i]; while (used >= lower + 2 && this.cross(h[used - 2], h[used - 1], v) <= 0) used--; h[used++] = v; }
|
|
98
|
+
return used - 1;
|
|
99
|
+
}
|
|
100
|
+
private plan(inverse: boolean, convex: boolean): boolean {
|
|
101
|
+
this.nativeRows = false;
|
|
102
|
+
const n = this.n, p = this.points, ring = this.ring;
|
|
103
|
+
let repeats = 0, distinct = 0;
|
|
104
|
+
for (let i = 0; i < n; i++) {
|
|
105
|
+
ring[i] = i;
|
|
106
|
+
for (let j = i + 1; j < n; j++) if (p[i * 2] === p[j * 2] && p[i * 2 + 1] === p[j * 2 + 1]) repeats++;
|
|
107
|
+
const j = (i + 1) % n;
|
|
108
|
+
if (p[i * 2] !== p[j * 2] || p[i * 2 + 1] !== p[j * 2 + 1]) this.work[distinct++] = i;
|
|
109
|
+
}
|
|
110
|
+
if (repeats > 0) {
|
|
111
|
+
if (!inverse && distinct === n - repeats && distinct >= 3 && this.convex(this.work, distinct)) { this.regionsCount = 1; this.inverse = false; this.region(ring, n, 0); return false; }
|
|
112
|
+
throw new Error('runtime clip: polygon has repeated vertices unsupported by clipShapeOf');
|
|
113
|
+
}
|
|
114
|
+
for (let i = 0; i < n; i++) {
|
|
115
|
+
const j = (i + 1) % n, k = (i + 2) % n;
|
|
116
|
+
if (this.cross(i, j, k) === 0 && (p[k * 2] - p[j * 2]) * (p[i * 2] - p[j * 2]) + (p[k * 2 + 1] - p[j * 2 + 1]) * (p[i * 2 + 1] - p[j * 2 + 1]) > 0) throw new Error('runtime clip: neighbouring polygon edges fold back');
|
|
117
|
+
for (let q = i + 2; q < n; q++) if (!(i === 0 && q === n - 1) && this.meet(i, j, q, (q + 1) % n)) throw new Error('runtime clip: polygon edges cross or touch');
|
|
118
|
+
}
|
|
119
|
+
const originalConvex = this.convex(ring, n);
|
|
120
|
+
this.nativeRows = !inverse && originalConvex;
|
|
121
|
+
let count = n;
|
|
122
|
+
for (let changed = true; changed && count > 3;) {
|
|
123
|
+
changed = false;
|
|
124
|
+
for (let i = 0; i < count; i++) if (this.cross(ring[(i + count - 1) % count], ring[i], ring[(i + 1) % count]) === 0) { ring.copyWithin(i, i + 1, count); count--; changed = true; break; }
|
|
125
|
+
}
|
|
126
|
+
if (count === 3 && this.cross(ring[0], ring[1], ring[2]) === 0) throw new Error('runtime clip: every polygon vertex lies on one line');
|
|
127
|
+
this.inverse = inverse;
|
|
128
|
+
if (inverse || !originalConvex && convex) { this.regionsCount = 1; this.region(this.hull, this.makeHull(count), 0); return false; }
|
|
129
|
+
if (originalConvex) { for (let i = 0; i < n; i++) ring[i] = i; this.regionsCount = 1; this.region(ring, n, 0); return false; }
|
|
130
|
+
if (this.area(ring, count) < 0) for (let i = 0; i < count / 2; i++) { const j = count - 1 - i, v = ring[i]; ring[i] = ring[j]; ring[j] = v; }
|
|
131
|
+
this.work.set(ring); let left = count, pieces = 0;
|
|
132
|
+
while (left > 3) {
|
|
133
|
+
let found = false;
|
|
134
|
+
for (let k = 0; k < left; k++) {
|
|
135
|
+
const a = this.work[(k + left - 1) % left], b = this.work[k], c = this.work[(k + 1) % left];
|
|
136
|
+
if (this.cross(a, b, c) <= 0) continue;
|
|
137
|
+
let inside = false;
|
|
138
|
+
for (let j = 0; j < left; j++) { const v = this.work[j]; if (v !== a && v !== b && v !== c && this.cross(a, b, v) >= 0 && this.cross(b, c, v) >= 0 && this.cross(c, a, v) >= 0) { inside = true; break; } }
|
|
139
|
+
if (inside) continue;
|
|
140
|
+
const base = pieces * this.maxPolygonVertices; this.pieces[base] = a; this.pieces[base + 1] = b; this.pieces[base + 2] = c; this.sizes[pieces++] = 3;
|
|
141
|
+
this.work.copyWithin(k, k + 1, left); left--; found = true; break;
|
|
142
|
+
}
|
|
143
|
+
if (!found) throw new Error('runtime clip: no ear left in a polygon held simple');
|
|
144
|
+
}
|
|
145
|
+
for (let i = 0; i < 3; i++) this.pieces[pieces * this.maxPolygonVertices + i] = this.work[i]; this.sizes[pieces++] = 3;
|
|
146
|
+
for (let changed = true; changed;) {
|
|
147
|
+
changed = false;
|
|
148
|
+
search: for (let a = 0; a < pieces; a++) for (let b = a + 1; b < pieces; b++) {
|
|
149
|
+
const an = this.sizes[a], bn = this.sizes[b], ab = a * this.maxPolygonVertices, bb = b * this.maxPolygonVertices;
|
|
150
|
+
for (let i = 0; i < an; i++) {
|
|
151
|
+
const u = this.pieces[ab + i], v = this.pieces[ab + (i + 1) % an]; let j = -1;
|
|
152
|
+
for (let k = 0; k < bn; k++) if (this.pieces[bb + k] === v) { j = k; break; }
|
|
153
|
+
if (j < 0 || this.pieces[bb + (j + 1) % bn] !== u) continue;
|
|
154
|
+
let size = 0;
|
|
155
|
+
for (let k = 0; k <= i; k++) this.merged[size++] = this.pieces[ab + k];
|
|
156
|
+
for (let k = (j + 2) % bn; k !== j; k = (k + 1) % bn) this.merged[size++] = this.pieces[bb + k];
|
|
157
|
+
for (let k = i + 1; k < an; k++) this.merged[size++] = this.pieces[ab + k];
|
|
158
|
+
let strict = true;
|
|
159
|
+
for (let k = 0; k < size; k++) if (this.cross(this.merged[(k + size - 1) % size], this.merged[k], this.merged[(k + 1) % size]) <= 0) { strict = false; break; }
|
|
160
|
+
if (!strict) continue;
|
|
161
|
+
for (let k = 0; k < size; k++) this.pieces[ab + k] = this.merged[k]; this.sizes[a] = size;
|
|
162
|
+
this.pieces.copyWithin(bb, bb + this.maxPolygonVertices, pieces * this.maxPolygonVertices); this.sizes.copyWithin(b, b + 1, pieces); pieces--; changed = true; break search;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
this.regionsCount = pieces;
|
|
167
|
+
for (let i = 0; i < pieces; i++) this.region(this.pieces, this.sizes[i], i, i * this.maxPolygonVertices);
|
|
168
|
+
return pieces > 1;
|
|
169
|
+
}
|
|
170
|
+
/** Plan again after float32 conversion exactly when clipThrough first finds pieces. */
|
|
171
|
+
prepare(polygon: Float64Array, offset: number, count: number, inverse: boolean, convex: boolean): void {
|
|
172
|
+
this.n = count / 2;
|
|
173
|
+
if (!Number.isInteger(this.n) || this.n < 3 || this.n > this.maxPolygonVertices) throw new Error('runtime clip: polygon vertex count exceeds its load-time capacity');
|
|
174
|
+
for (let i = 0; i < count; i++) { const value = polygon[offset + i]; if (!Number.isFinite(value)) throw new Error('runtime clip: polygon contains a non-finite coordinate'); this.points[i] = value; }
|
|
175
|
+
const replan = this.plan(inverse, convex), nativeRows = this.nativeRows;
|
|
176
|
+
if (replan) { for (let i = 0; i < count; i++) this.points[i] = Math.fround(polygon[offset + i]); this.plan(inverse, convex); }
|
|
177
|
+
this.nativeRows = nativeRows;
|
|
178
|
+
}
|
|
179
|
+
private half(input: Float64Array, n: number, px: number, py: number, qx: number, qy: number, out: Float64Array): number {
|
|
180
|
+
let size = 0; this.cut = false; const ex = qx - px, ey = qy - py;
|
|
181
|
+
for (let i = 0; i < n; i++) {
|
|
182
|
+
const j = (i + 1) % n, sx = input[i * 2], sy = input[i * 2 + 1], tx = input[j * 2], ty = input[j * 2 + 1];
|
|
183
|
+
const si = ex * (sy - py) - ey * (sx - px) < 0, ei = ex * (ty - py) - ey * (tx - px) < 0;
|
|
184
|
+
if (!si || !ei) this.cut = true;
|
|
185
|
+
if (si !== ei) { const dx = tx - sx, dy = ty - sy, t = (ex * (sy - py) - ey * (sx - px)) / (ey * dx - ex * dy); if (size * 2 + 2 > out.length) throw new Error('runtime clip: scratch polygon exceeds its load-time capacity'); out[size * 2] = sx + t * dx; out[size * 2 + 1] = sy + t * dy; size++; }
|
|
186
|
+
if (ei) { if (size * 2 + 2 > out.length) throw new Error('runtime clip: scratch polygon exceeds its load-time capacity'); out[size * 2] = tx; out[size * 2 + 1] = ty; size++; }
|
|
187
|
+
}
|
|
188
|
+
return size;
|
|
189
|
+
}
|
|
190
|
+
private emit(points: Float64Array, count: number, cut: boolean, source: number, out: RuntimeClipBuffer): void {
|
|
191
|
+
if (count < 3) return;
|
|
192
|
+
if ((out.vertexCount + count) * 2 > out.vertices.length || out.triangleCount + count - 2 > out.sources.length) throw new Error('runtime clip: output exceeds its load-time capacity');
|
|
193
|
+
const tri = this.tri, uv = this.uv, base = out.vertexCount;
|
|
194
|
+
const d0 = tri[3] - tri[5], d1 = tri[4] - tri[2], d2 = tri[0] - tri[4], d4 = tri[5] - tri[1], d = 1 / (d0 * d2 + d1 * (tri[1] - tri[5]));
|
|
195
|
+
for (let i = 0; i < count; i++) {
|
|
196
|
+
const x = cut ? points[i * 2] : tri[i * 2], y = cut ? points[i * 2 + 1] : tri[i * 2 + 1], at = out.vertexCount++ * 2;
|
|
197
|
+
out.vertices[at] = x; out.vertices[at + 1] = y;
|
|
198
|
+
if (!cut) { out.uvs[at] = uv[i * 2]; out.uvs[at + 1] = uv[i * 2 + 1]; }
|
|
199
|
+
else { const a = (d0 * (x - tri[4]) + d1 * (y - tri[5])) * d, b = (d4 * (x - tri[4]) + d2 * (y - tri[5])) * d, c = 1 - a - b; out.uvs[at] = uv[0] * a + uv[2] * b + uv[4] * c; out.uvs[at + 1] = uv[1] * a + uv[3] * b + uv[5] * c; }
|
|
200
|
+
}
|
|
201
|
+
for (let i = base * 2; i < out.vertexCount * 2; i++) if (!Number.isFinite(out.vertices[i]) || !Number.isFinite(out.uvs[i])) throw new Error(`runtime clip: triangle ${source} produced a non-finite clipped coordinate or UV`);
|
|
202
|
+
for (let i = 1; i + 1 < count; i++) { const at = out.triangleCount * 3; out.triangles[at] = base; out.triangles[at + 1] = base + i; out.triangles[at + 2] = base + i + 1; out.sources[out.triangleCount++] = source; }
|
|
203
|
+
}
|
|
204
|
+
clip(vertices: Float64Array, offset: number, triangles: Int32Array, uvs: Float64Array, out: RuntimeClipBuffer): void {
|
|
205
|
+
out.vertexCount = 0; out.triangleCount = 0; out.clipped = this.inverse;
|
|
206
|
+
for (let k = 0; k < triangles.length; k += 3) {
|
|
207
|
+
for (let j = 0; j < 3; j++) { const n = triangles[k + j] * 2; this.tri[j * 2] = vertices[offset + n]; this.tri[j * 2 + 1] = vertices[offset + n + 1]; this.uv[j * 2] = Math.fround(uvs[n]); this.uv[j * 2 + 1] = Math.fround(uvs[n + 1]); }
|
|
208
|
+
if (!this.inverse) for (let r = 0; r < this.regionsCount; r++) {
|
|
209
|
+
let input = this.a, output = this.b, count = 3, cut = false; for (let i = 0; i < 6; i++) input[i] = this.tri[i];
|
|
210
|
+
const n = this.regionSizes[r], base = r * this.maxPolygonVertices * 2;
|
|
211
|
+
for (let e = 0; e < n && count > 0; e++) { const p = base + e * 2, q = base + ((e + 1) % n) * 2; count = this.half(input, count, this.regions[p], this.regions[p + 1], this.regions[q], this.regions[q + 1], output); cut ||= this.cut; const swap = input; input = output; output = swap; }
|
|
212
|
+
out.clipped ||= cut; this.emit(input, count, cut, k / 3, out);
|
|
213
|
+
} else {
|
|
214
|
+
let input = this.a, output = this.b, count = 3, cutSoFar = false; for (let i = 0; i < 6; i++) input[i] = this.tri[i];
|
|
215
|
+
const n = this.regionSizes[0];
|
|
216
|
+
for (let e = 0; e < n && count > 0; e++) { const p = e * 2, q = ((e + 1) % n) * 2, px = this.regions[p], py = this.regions[p + 1], qx = this.regions[q], qy = this.regions[q + 1];
|
|
217
|
+
const outside = this.half(input, count, qx, qy, px, py, this.outside), cut = this.cut || cutSoFar; this.emit(this.outside, outside, cut, k / 3, out);
|
|
218
|
+
count = this.half(input, count, px, py, qx, qy, output); cutSoFar ||= this.cut; const swap = input; input = output; output = swap;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/runtime/curves.ts
CHANGED
|
@@ -46,8 +46,13 @@ export interface RuntimeCurve {
|
|
|
46
46
|
readonly polylines: Float64Array;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/** A timeline's keys compiled; `channels` is how many values each key carries (0 for an attachment timeline). */
|
|
50
|
-
export function compileCurve(keys: readonly
|
|
49
|
+
/** A timeline's keys compiled; `channels` is how many values each key carries (0 for an attachment or draw-order timeline). */
|
|
50
|
+
export function compileCurve(keys: readonly { readonly time: number }[], channels: 0): RuntimeCurve;
|
|
51
|
+
export function compileCurve(keys: readonly CoreKey[], channels: number): RuntimeCurve;
|
|
52
|
+
export function compileCurve(
|
|
53
|
+
keys: readonly { readonly time: number; readonly values?: readonly number[]; readonly stated?: CoreKey['stated']; readonly curve?: CoreKey['curve'] }[],
|
|
54
|
+
channels: number,
|
|
55
|
+
): RuntimeCurve {
|
|
51
56
|
const n = keys.length;
|
|
52
57
|
const times = new Float64Array(n);
|
|
53
58
|
const values = new Float64Array(n * channels);
|
|
@@ -56,20 +61,24 @@ export function compileCurve(keys: readonly CoreKey[], channels: number): Runtim
|
|
|
56
61
|
for (let i = 0; i < n; i++) {
|
|
57
62
|
const a = keys[i];
|
|
58
63
|
times[i] = a.time;
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
const av = a.values;
|
|
65
|
+
if (av !== undefined) for (let c = 0; c < channels; c++) values[i * channels + c] = av[c];
|
|
66
|
+
const curve = a.curve ?? 'stepped';
|
|
61
67
|
kinds[i] = curve === 'linear' ? CURVE_LINEAR : curve === 'stepped' ? CURVE_STEPPED : CURVE_BEZIER;
|
|
62
68
|
if (typeof curve === 'string' || i + 1 >= n) continue;
|
|
63
69
|
const b = keys[i + 1];
|
|
70
|
+
const aStated = a.stated!;
|
|
71
|
+
const bStated = b.stated!;
|
|
72
|
+
const bValues = b.values!;
|
|
64
73
|
for (let c = 0; c < channels; c++) {
|
|
65
74
|
// The oracle's polyline (`channelAt`): the segment's float32 ends around the recurrence over the numbers as the document states them.
|
|
66
|
-
const inner = bezierPolyline(
|
|
75
|
+
const inner = bezierPolyline(aStated.time, aStated.values[c], curve[c * 4], curve[c * 4 + 1], curve[c * 4 + 2], curve[c * 4 + 3], bStated.time, bStated.values[c]);
|
|
67
76
|
const o = (i * channels + c) * POLYLINE;
|
|
68
77
|
polylines[o] = a.time;
|
|
69
|
-
polylines[o + 1] =
|
|
78
|
+
polylines[o + 1] = av![c];
|
|
70
79
|
for (let q = 0; q < inner.length; q++) polylines[o + 2 + q] = inner[q];
|
|
71
80
|
polylines[o + POLYLINE - 2] = b.time;
|
|
72
|
-
polylines[o + POLYLINE - 1] =
|
|
81
|
+
polylines[o + POLYLINE - 1] = bValues[c];
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
84
|
return { keys: n, channels, times, values, kinds, polylines };
|
package/src/runtime/index.ts
CHANGED
|
@@ -21,12 +21,13 @@
|
|
|
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 8: what it poses, and what it refuses
|
|
25
25
|
*
|
|
26
26
|
* Bones and their five inherit modes; the rotate, translate, scale and shear
|
|
27
|
-
* timelines; a slot's attachment
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* timelines; a slot's attachment, light colour (`rgba`, `rgb`, `alpha`) and
|
|
28
|
+
* dark colour (`rgba2`, `rgb2`) timelines; draw-order timelines (including
|
|
29
|
+
* slider draw-order application); weighted/unweighted deform timelines; region, mesh, weighted mesh and linked mesh vertices; one- and
|
|
30
|
+
* two-bone ik, absolute/additive transforms with local/world sources and targets in all five inherit modes, path constraints, physics (with optional movement scope) and value-mapped sliders with their
|
|
30
31
|
* timelines; skin-required bones and constraints under the view's skin. One
|
|
31
32
|
* animation at full weight from the setup pose, at a time or stepped as a
|
|
32
33
|
* looping track. Everything else is refused at load by name with the list of
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
* const pose = new RuntimePose(model);
|
|
44
45
|
* pose.setAnimation('walk');
|
|
45
46
|
* pose.step(1 / 60); // or pose.apply(t)
|
|
46
|
-
* pose.computeVertices(); // pose.world, pose.light, pose.shown, pose.vertices
|
|
47
|
+
* pose.computeVertices(); // pose.world, pose.light, pose.dark, pose.shown, pose.drawOrder, pose.vertices
|
|
47
48
|
* ```
|
|
48
49
|
*
|
|
49
50
|
* This module is the `rig-c/runtime` entry (`exports` in package.json), which
|
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 9): 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,22 +12,23 @@
|
|
|
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 —
|
|
16
|
-
*
|
|
17
|
-
* `additive`), an ik, transform or path constraint over a bone the view leaves
|
|
18
|
-
* inactive, a world-space transform onto a `noScale` or
|
|
19
|
-
* `noScaleOrReflection` bone, a clipping attachment or a region with no
|
|
15
|
+
* when one is a property of the whole model — an ik, transform or path constraint
|
|
16
|
+
* over a bone the view leaves inactive, a region with no
|
|
20
17
|
* atlas rectangle shown at setup, a placeholder the skins fill
|
|
21
18
|
* differently, or a block the oracle itself leaves out;
|
|
22
19
|
* - **refuses an animation** (`RuntimeModel.refused`; `animation(name)`
|
|
23
20
|
* throws the same `RuntimeRefusal`) when the construct is the animation's
|
|
24
|
-
* — an `inherit` timeline, a sequence timeline,
|
|
25
|
-
*
|
|
26
|
-
* attachment key showing a clipping attachment or a region with no atlas
|
|
21
|
+
* — an `inherit` timeline, a sequence timeline, or an
|
|
22
|
+
* attachment key showing a region with no atlas
|
|
27
23
|
* rectangle. Every other animation of the model is posed.
|
|
28
24
|
*
|
|
29
|
-
* `RUNTIME_FEATURES` is the closed list of what can be named. Stage
|
|
30
|
-
*
|
|
25
|
+
* `RUNTIME_FEATURES` is the closed list of what can be named. Stage 9 adds buffered clipping draw output; stage 8 added
|
|
26
|
+
* additive local/world transform constraints. Stage 7 admitted
|
|
27
|
+
* world transform targets in both no-scale inherit modes, using the existing
|
|
28
|
+
* localFromWorld read-back. Stage 6 added local-source and local-target transforms. Draw-order,
|
|
29
|
+
* light/dark colour, mesh and path deforms, named/no-skin views, path constraints,
|
|
30
|
+
* scoped physics and immutable event deliveries remain covered.
|
|
31
|
+
* `ignore: ['events']` remains an explicit diagnostic opt-out; it is
|
|
31
32
|
* never the default. A merged view with conflicting attachments still refuses
|
|
32
33
|
* by name, as the oracle itself cannot resolve that view without file order.
|
|
33
34
|
*
|
|
@@ -55,22 +56,16 @@ import type { CoreSliderRecord } from '../core/constraints_slider.ts';
|
|
|
55
56
|
import { compileCurve, compileDeformCurve, type RuntimeCurve } from './curves.ts';
|
|
56
57
|
|
|
57
58
|
/** The stage of issue #1591 this poser implements. */
|
|
58
|
-
export const RUNTIME_STAGE =
|
|
59
|
+
export const RUNTIME_STAGE = 9;
|
|
59
60
|
|
|
60
61
|
/** Every construct the runtime poser can refuse, by the name its refusals carry. */
|
|
61
62
|
export const RUNTIME_FEATURES = [
|
|
62
|
-
'local-transform',
|
|
63
|
-
'additive-transform',
|
|
64
63
|
'inactive-constrained-bone',
|
|
65
|
-
'noscale-world-target',
|
|
66
|
-
'clipping',
|
|
67
64
|
'null-atlas-region',
|
|
68
65
|
'skin-conflict',
|
|
69
66
|
'oracle-absent',
|
|
70
67
|
'inherit-timeline',
|
|
71
68
|
'sequence-timeline',
|
|
72
|
-
'draw-order-timeline',
|
|
73
|
-
'dark-colour-timeline',
|
|
74
69
|
] as const;
|
|
75
70
|
export type RuntimeFeature = (typeof RUNTIME_FEATURES)[number];
|
|
76
71
|
|
|
@@ -131,6 +126,14 @@ export interface RuntimePlant {
|
|
|
131
126
|
readonly deformAfterWeights?: boolean;
|
|
132
127
|
readonly omitPath?: boolean;
|
|
133
128
|
readonly ignorePhysicsScope?: boolean;
|
|
129
|
+
readonly omitDrawOrder?: boolean;
|
|
130
|
+
readonly omitDarkColour?: boolean;
|
|
131
|
+
/** Omit local-source/target transforms at load, to prove the stage-six gate. */
|
|
132
|
+
readonly omitLocalTransform?: boolean;
|
|
133
|
+
/** Omit world transforms with parented no-scale targets, to prove stage seven. */
|
|
134
|
+
readonly omitNoScaleWorldTransform?: boolean;
|
|
135
|
+
/** Omit additive transforms at load, to prove stage eight. */
|
|
136
|
+
readonly omitAdditiveTransform?: boolean;
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
// A bone's local values, seven per bone.
|
|
@@ -201,6 +204,10 @@ export const RAD = RUNTIME_PI / 180;
|
|
|
201
204
|
* bindings (bone index, float32 coordinates, float32 weight) in their order.
|
|
202
205
|
*/
|
|
203
206
|
export interface RuntimeAttachment {
|
|
207
|
+
/** Source topology resolved at load for models containing clipping. */
|
|
208
|
+
readonly clipTopology?: { readonly triangles: Int32Array; readonly uvs: Float64Array };
|
|
209
|
+
/** Clip polygon metadata, resolved once with the attachment. */
|
|
210
|
+
readonly clip?: { readonly end: string | null; readonly inverse: boolean; readonly convex: boolean };
|
|
204
211
|
/** Source timeline identity, including linked-mesh inheritance, resolved once. */
|
|
205
212
|
readonly timeline: string | null;
|
|
206
213
|
readonly deformOffset: number;
|
|
@@ -249,7 +256,7 @@ export interface RuntimeIk {
|
|
|
249
256
|
readonly flags: number;
|
|
250
257
|
}
|
|
251
258
|
|
|
252
|
-
/**
|
|
259
|
+
/** An absolute or additive transform constraint as the plan runs it: each source property's offset and its targets'. */
|
|
253
260
|
export interface RuntimeTransform {
|
|
254
261
|
readonly kind: 'transform';
|
|
255
262
|
readonly name: string;
|
|
@@ -269,6 +276,9 @@ export interface RuntimeTransform {
|
|
|
269
276
|
readonly offsets: Float64Array;
|
|
270
277
|
readonly mixes: Float64Array;
|
|
271
278
|
readonly clamp: boolean;
|
|
279
|
+
readonly localSource: boolean;
|
|
280
|
+
readonly localTarget: boolean;
|
|
281
|
+
readonly additive: boolean;
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
export interface RuntimePhysics {
|
|
@@ -354,7 +364,7 @@ export interface RuntimeBoneTimeline {
|
|
|
354
364
|
readonly multiply: Uint8Array;
|
|
355
365
|
}
|
|
356
366
|
|
|
357
|
-
/** A slot timeline: an attachment timeline (each key's attachment index, −1 nothing) or a colour timeline (each channel's index into the light colour). */
|
|
367
|
+
/** A slot timeline: an attachment timeline (each key's attachment index, −1 nothing) or a colour timeline (each channel's index into the light or dark colour). */
|
|
358
368
|
export interface RuntimeSlotTimeline {
|
|
359
369
|
readonly slot: number;
|
|
360
370
|
readonly curve: RuntimeCurve;
|
|
@@ -391,6 +401,13 @@ export interface RuntimeDeformTimeline {
|
|
|
391
401
|
readonly count: number;
|
|
392
402
|
}
|
|
393
403
|
|
|
404
|
+
/** An animation's draw-order timeline: each key's slot-index permutation of `slots.length` (`orderOf` in `src/core/draw_order.ts`), expanded once at load. */
|
|
405
|
+
export interface RuntimeDrawOrderTimeline {
|
|
406
|
+
readonly curve: RuntimeCurve;
|
|
407
|
+
readonly cursor: number;
|
|
408
|
+
readonly orders: Int32Array;
|
|
409
|
+
}
|
|
410
|
+
|
|
394
411
|
/** One covered animation, compiled. */
|
|
395
412
|
export interface RuntimeAnimation {
|
|
396
413
|
readonly name: string;
|
|
@@ -403,6 +420,7 @@ export interface RuntimeAnimation {
|
|
|
403
420
|
readonly sliders: ReadonlyArray<RuntimeSliderTimeline | null>;
|
|
404
421
|
readonly paths: ReadonlyArray<RuntimePathTimeline | null>;
|
|
405
422
|
readonly slots: readonly RuntimeSlotTimeline[];
|
|
423
|
+
readonly drawOrder: RuntimeDrawOrderTimeline | null;
|
|
406
424
|
/** Per constraint of the plan, its timeline in this animation or `null`. */
|
|
407
425
|
readonly constraints: ReadonlyArray<RuntimeConstraintTimeline | null>;
|
|
408
426
|
/** How many key cursors a pose keeps for it. */
|
|
@@ -421,6 +439,8 @@ export interface RuntimeModel {
|
|
|
421
439
|
readonly sliderAsTrack: boolean;
|
|
422
440
|
readonly omitPath: boolean;
|
|
423
441
|
readonly ignorePhysicsScope: boolean;
|
|
442
|
+
readonly omitDrawOrder: boolean;
|
|
443
|
+
readonly omitDarkColour: boolean;
|
|
424
444
|
readonly hasPath: boolean;
|
|
425
445
|
readonly maxPathSpaces: number;
|
|
426
446
|
readonly maxPathVertices: number;
|
|
@@ -472,8 +492,8 @@ const BONE_KINDS: Record<string, ReadonlyArray<readonly [number, 0 | 1]>> = {
|
|
|
472
492
|
sheary: [[L_SHEAR_Y, 0]],
|
|
473
493
|
};
|
|
474
494
|
|
|
475
|
-
/** The light colour
|
|
476
|
-
const COLOUR_CHANNELS: Record<string, readonly number[]> = { rgba: [0, 1, 2, 3], rgb: [0, 1, 2], alpha: [3] };
|
|
495
|
+
/** The light (`0 .. 3`) and dark (`4 .. 6`) colour channels each colour timeline sets, in its curve's channel order (`COLOUR_CHANNELS` in `src/core/animation.ts` and `src/core/constraints_slider.ts`). */
|
|
496
|
+
const COLOUR_CHANNELS: Record<string, readonly number[]> = { rgba: [0, 1, 2, 3], rgb: [0, 1, 2], alpha: [3], rgba2: [0, 1, 2, 3, 4, 5, 6], rgb2: [0, 1, 2, 4, 5, 6] };
|
|
477
497
|
|
|
478
498
|
/** Where refusals are gathered while a model compiles. */
|
|
479
499
|
class Refusals {
|
|
@@ -576,20 +596,14 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
576
596
|
else if (r.kind === 'path') {
|
|
577
597
|
constraints.push(compilePath(r, index, applied, bones, indexOf, slotIndexOf, pathPlan.get(index) ?? null));
|
|
578
598
|
} else {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const t = compileTransform(r, index, applied, bones, indexOf);
|
|
582
|
-
if (applied) {
|
|
583
|
-
for (let k = 0; k < bones.length; k++) {
|
|
584
|
-
const m = modes[bones[k]];
|
|
585
|
-
if (parents[bones[k]] >= 0 && (m === MODE_NO_SCALE || m === MODE_NO_SCALE_OR_REFLECTION)) refusals.note('noscale-world-target', `transform constraint "${r.name}" moves bone "${r.bones[k]}" in world space, and its local values are read back in a noScale mode`);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
599
|
+
const noScaleWorld = !r.localTarget && bones.some(b => parents[b] >= 0 && (modes[b] === MODE_NO_SCALE || modes[b] === MODE_NO_SCALE_OR_REFLECTION));
|
|
600
|
+
const t = compileTransform(r, index, applied && !(plant.omitLocalTransform && (r.localSource || r.localTarget)) && !(plant.omitNoScaleWorldTransform && noScaleWorld) && !(plant.omitAdditiveTransform && r.additive), bones, indexOf);
|
|
588
601
|
constraints.push(t);
|
|
589
602
|
}
|
|
590
603
|
}
|
|
591
604
|
|
|
592
605
|
// --- slots and the records they show ---
|
|
606
|
+
const hasClipping = doc.skins.some(s => Object.values(s.attachments).some(table => Object.values(table).some(at => at.geometry?.kind === 'clipping')));
|
|
593
607
|
const attachments: RuntimeAttachment[] = [];
|
|
594
608
|
let vertexCount = 0;
|
|
595
609
|
let deformCount = 0;
|
|
@@ -617,7 +631,9 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
617
631
|
const at = { timeline: timelineIdentity(slot.name, shown), deformOffset: deformCount, slot: si, name: row.name, skin: shown.skin, placeholder: shown.placeholder, path: CORE_REGION_KINDS.has(shown.record.kind) ? row.path : null, bone: indexOf(slot.bone), offset: vertexCount };
|
|
618
632
|
let made: RuntimeAttachment | null = null;
|
|
619
633
|
let missing: readonly [RuntimeFeature, string] | null = null;
|
|
620
|
-
if (g?.kind === 'clipping')
|
|
634
|
+
if (g?.kind === 'clipping') {
|
|
635
|
+
made = { ...meshAttachment(at, g.vertices, indexOf, plant.bindingIndex, nb), clip: { end: g.end, inverse: g.inverse, convex: g.convex } };
|
|
636
|
+
}
|
|
621
637
|
else if (g?.kind === 'region') {
|
|
622
638
|
const r = g.region;
|
|
623
639
|
if (r.atlas === null) missing = ['null-atlas-region', `slot "${slot.name}" shows region "${row.name}", whose atlas rectangle the model states as null`];
|
|
@@ -658,6 +674,14 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
658
674
|
}
|
|
659
675
|
// A bounding box, a refused record admitted by a plant: the row's name and path, and nothing drawn.
|
|
660
676
|
if (made === null) made = vertexAttachment(at, DRAW_NONE, new Float64Array(0), 0);
|
|
677
|
+
if (hasClipping) {
|
|
678
|
+
if (g?.kind === 'region') made = { ...made, clipTopology: { triangles: new Int32Array([0, 1, 2, 2, 3, 0]), uvs: new Float64Array([0, 1, 0, 0, 1, 0, 1, 1]) } };
|
|
679
|
+
else if (g?.kind === 'mesh' || g?.kind === 'linkedmesh') {
|
|
680
|
+
const mesh = g.kind === 'mesh' ? g : doc.skins.find(s => s.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
|
|
681
|
+
if (mesh?.kind !== 'mesh') throw new RuntimeInputError('clipping topology: linked source is not a mesh (readModel refuses it first)');
|
|
682
|
+
made = { ...made, clipTopology: { triangles: Int32Array.from(mesh.triangles), uvs: Float64Array.from(mesh.uvs) } };
|
|
683
|
+
}
|
|
684
|
+
}
|
|
661
685
|
attachments.push(made);
|
|
662
686
|
vertexCount += made.count;
|
|
663
687
|
deformCount += made.deformCount;
|
|
@@ -721,6 +745,8 @@ export function compileRuntime(doc: CompiledDocument, options: RuntimeOptions =
|
|
|
721
745
|
deformAfterWeights: plant.deformAfterWeights === true,
|
|
722
746
|
omitPath: plant.omitPath === true,
|
|
723
747
|
ignorePhysicsScope: plant.ignorePhysicsScope === true,
|
|
748
|
+
omitDrawOrder: plant.omitDrawOrder === true,
|
|
749
|
+
omitDarkColour: plant.omitDarkColour === true,
|
|
724
750
|
hasPath,
|
|
725
751
|
maxPathSpaces,
|
|
726
752
|
maxPathVertices,
|
|
@@ -871,7 +897,7 @@ function compileTransform(r: CoreTransformRecord, index: number, applied: boolea
|
|
|
871
897
|
toProperty: Int32Array.from(toProperty), toOffset: Float64Array.from(toOffset), toScale: Float64Array.from(toScale), toMax: Float64Array.from(toMax),
|
|
872
898
|
offsets: Float64Array.from(TRANSFORM_PROPERTIES.map((p) => r.offsets[p])),
|
|
873
899
|
mixes: Float64Array.from(TRANSFORM_PROPERTIES.map((p) => r.mixes[p])),
|
|
874
|
-
clamp: r.clamp,
|
|
900
|
+
clamp: r.clamp, localSource: r.localSource, localTarget: r.localTarget, additive: r.additive,
|
|
875
901
|
};
|
|
876
902
|
}
|
|
877
903
|
|
|
@@ -899,7 +925,28 @@ function compileAnimation(
|
|
|
899
925
|
const sequence = tl.attachments.filter((a) => a.sequence !== null);
|
|
900
926
|
|
|
901
927
|
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);
|
|
902
|
-
|
|
928
|
+
|
|
929
|
+
let drawOrder: RuntimeDrawOrderTimeline | null = null;
|
|
930
|
+
if (tl.drawOrder.length > 0) {
|
|
931
|
+
const ns = slots.length;
|
|
932
|
+
const orders = new Int32Array(tl.drawOrder.length * ns).fill(-1);
|
|
933
|
+
const moved = new Uint8Array(ns);
|
|
934
|
+
for (let i = 0; i < tl.drawOrder.length; i++) {
|
|
935
|
+
const base = i * ns;
|
|
936
|
+
moved.fill(0);
|
|
937
|
+
for (const m of tl.drawOrder[i].moves) {
|
|
938
|
+
orders[base + m.slot + m.offset] = m.slot;
|
|
939
|
+
moved[m.slot] = 1;
|
|
940
|
+
}
|
|
941
|
+
for (let s = 0, d = 0; s < ns; s++) {
|
|
942
|
+
if (moved[s] !== 0) continue;
|
|
943
|
+
while (orders[base + d] !== -1) d++;
|
|
944
|
+
orders[base + d] = s;
|
|
945
|
+
d++;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
drawOrder = { curve: compileCurve(tl.drawOrder, 0), cursor: cursors++, orders };
|
|
949
|
+
}
|
|
903
950
|
|
|
904
951
|
const lastBy = <T extends { name: string }>(targets: readonly T[]): Map<string, T> => new Map(targets.map((t) => [t.name, t]));
|
|
905
952
|
const bones: RuntimeBoneTimeline[] = [];
|
|
@@ -941,8 +988,6 @@ function compileAnimation(
|
|
|
941
988
|
if (t.kind === 'attachment') {
|
|
942
989
|
const ids = Int32Array.from(t.keys.map((k) => resolve(si, k.name ?? null)));
|
|
943
990
|
slotTimelines.push({ slot: si, curve: compileCurve(t.keys, 0), cursor: cursors++, attachments: ids, channels: null });
|
|
944
|
-
} else if (t.kind === 'rgba2' || t.kind === 'rgb2') {
|
|
945
|
-
refusals.note('dark-colour-timeline', `slot "${target.name}" keys an ${t.kind} timeline`, name);
|
|
946
991
|
} else {
|
|
947
992
|
const channels = COLOUR_CHANNELS[t.kind];
|
|
948
993
|
slotTimelines.push({ slot: si, curve: compileCurve(t.keys, channels.length), cursor: cursors++, attachments: null, channels: Int32Array.from(channels) });
|
|
@@ -991,5 +1036,5 @@ function compileAnimation(
|
|
|
991
1036
|
const arrays = keys.map(k => heldArray(g.vertices, k));
|
|
992
1037
|
return { identity: `${a.skin}/${a.slot}/${a.attachment}`, cursor: cursors++, curve: compileDeformCurve(keys), count: arrays[0].length, held: Float64Array.from(arrays.flat()) };
|
|
993
1038
|
});
|
|
994
|
-
return { name, duration: tl.duration, physics, sliders, paths, deforms, bones, sliderBones, 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 }))) };
|
|
1039
|
+
return { name, duration: tl.duration, physics, sliders, paths, deforms, bones, sliderBones, slots: slotTimelines, drawOrder, 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 }))) };
|
|
995
1040
|
}
|
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 9): 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.
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
* The oracle's sample, as `src/core/animation.ts` poses it (`poseAnimations`):
|
|
10
10
|
* every pose is posed from the setup pose — the bones' setup local values,
|
|
11
11
|
* then the animation's bone timelines at `t` at full weight, then the world
|
|
12
|
-
* transforms parents first, then each slot's
|
|
13
|
-
* timelines, then the ik, transform, path,
|
|
14
|
-
* document's order with their timelines'
|
|
15
|
-
* bones below what it moved posed again,
|
|
16
|
-
* timelines; then, when asked,
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* transforms parents first, then the draw order and each slot's light and dark
|
|
13
|
+
* colour, attachment and own deform timelines, then the ik, transform, path,
|
|
14
|
+
* physics and slider constraints in the document's order with their timelines'
|
|
15
|
+
* values at `t`, each followed by the bones below what it moved posed again,
|
|
16
|
+
* then the sliders' draw-order, slot and deform timelines; then, when asked,
|
|
17
|
+
* every shown attachment's world vertices. Nothing of one unstepped pose
|
|
18
|
+
* survives but key cursors. A stepped pose also keeps its physics clock,
|
|
19
|
+
* offsets, velocities and lags; sliders select values within this pose rather
|
|
20
|
+
* than advancing an independent track.
|
|
20
21
|
*
|
|
21
22
|
* ## The arithmetic is the oracle's
|
|
22
23
|
*
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
* exactly 0 touching nothing; an inactive bone posed as zeros by the
|
|
40
41
|
* hierarchy and never posed again by a constraint, nor the bones below it.
|
|
41
42
|
*/
|
|
43
|
+
import { RuntimeDrawClipper } from './clip_draw.ts';
|
|
42
44
|
import type { CorePhysicsRecord } from '../core/constraints_physics.ts';
|
|
43
45
|
import { freshPhysicsState, resetPhysicsState, stepRuntimePhysics, type PhysicsState } from './physics.ts';
|
|
44
46
|
import { COLLAPSED_X_AXIS_SQ, RUNTIME_DEG, RUNTIME_PI } from '../core/world.ts';
|
|
@@ -101,6 +103,8 @@ const P_X = 1;
|
|
|
101
103
|
const P_Y = 2;
|
|
102
104
|
const P_SCALE_X = 3;
|
|
103
105
|
const P_SCALE_Y = 4;
|
|
106
|
+
// `applyLocal` in src/core/constraints.ts: the six transform properties' local fields.
|
|
107
|
+
const PROPERTY_LOCAL = new Int32Array([L_ROTATION, L_X, L_Y, L_SCALE_X, L_SCALE_Y, L_SHEAR_Y]);
|
|
104
108
|
|
|
105
109
|
/** A change brought once into (−180, 180] — −180 goes to 180 (src/core/constraints.ts). */
|
|
106
110
|
function wrap180(r: number): number {
|
|
@@ -118,7 +122,7 @@ const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
|
118
122
|
export interface RuntimePoseOptions { readonly clock?: (dt: number) => number }
|
|
119
123
|
export type RuntimePhysicsPhase = 'none' | 'reset' | 'update';
|
|
120
124
|
|
|
121
|
-
/** One skeleton's pose, every value in place. Read `world`, `light`, `shown` and `vertices` after a pose. */
|
|
125
|
+
/** One skeleton's pose, every value in place. Read `world`, `light`, `dark`, `shown`, `drawOrder` and `vertices` after a pose. */
|
|
122
126
|
export class RuntimePose {
|
|
123
127
|
readonly model: RuntimeModel;
|
|
124
128
|
/** Every bone's local values, `LOCALS` per bone (`L_*`). */
|
|
@@ -127,10 +131,16 @@ export class RuntimePose {
|
|
|
127
131
|
readonly world: Float64Array;
|
|
128
132
|
/** Every slot's light colour, four per slot. */
|
|
129
133
|
readonly light: Float64Array;
|
|
134
|
+
/** Every slot's dark colour, three per slot (`r2`, `g2`, `b2`), or zeros when `model.slots[s].dark === null`. */
|
|
135
|
+
readonly dark: Float64Array;
|
|
130
136
|
/** What every slot shows: an index into `model.attachments`, or −1 for nothing. */
|
|
131
137
|
readonly shown: Int32Array;
|
|
138
|
+
/** The slot indices in the live frame's draw order (`0 .. model.slots.length - 1`), updated in place. */
|
|
139
|
+
readonly drawOrder: Int32Array;
|
|
132
140
|
/** Every attachment's world vertices at its `offset`, written by `computeVertices` for the attachments shown. */
|
|
133
141
|
readonly vertices: Float64Array;
|
|
142
|
+
/** Buffered clipping output; null when the compiled model has no clipping attachments. */
|
|
143
|
+
readonly clipping: RuntimeDrawClipper | null;
|
|
134
144
|
/** Held/interpolated deform values, one pair per binding when weighted. */
|
|
135
145
|
readonly deform: Float64Array;
|
|
136
146
|
private readonly deformed: Uint8Array;
|
|
@@ -185,6 +195,7 @@ export class RuntimePose {
|
|
|
185
195
|
|
|
186
196
|
constructor(model: RuntimeModel, options: RuntimePoseOptions = {}) {
|
|
187
197
|
this.model = model;
|
|
198
|
+
this.clipping = model.attachments.some(at => at.clip !== undefined) ? new RuntimeDrawClipper(model) : null;
|
|
188
199
|
this.clock = options.clock;
|
|
189
200
|
this.hasPhysics = model.constraints.some(c => c.kind === 'physics');
|
|
190
201
|
this.physicsStates = model.constraints.map(() => freshPhysicsState());
|
|
@@ -199,7 +210,10 @@ export class RuntimePose {
|
|
|
199
210
|
this.locals = new Float64Array(nb * LOCALS);
|
|
200
211
|
this.world = new Float64Array(nb * WORLDS);
|
|
201
212
|
this.light = new Float64Array(ns * 4);
|
|
213
|
+
this.dark = new Float64Array(ns * 3);
|
|
202
214
|
this.shown = new Int32Array(ns);
|
|
215
|
+
this.drawOrder = new Int32Array(ns);
|
|
216
|
+
for (let s = 0; s < ns; s++) this.drawOrder[s] = s;
|
|
203
217
|
this.vertices = new Float64Array(model.vertexCount);
|
|
204
218
|
this.deform = new Float64Array(model.deformCount);
|
|
205
219
|
this.deformed = new Uint8Array(model.attachments.length);
|
|
@@ -1179,6 +1193,13 @@ export class RuntimePose {
|
|
|
1179
1193
|
const k = index, c = cs[k];
|
|
1180
1194
|
if (c.kind !== 'slider' || !c.applied || this.sliderMix[k] === 0) return;
|
|
1181
1195
|
const a = c.animation, t = this.sliderAt[k], alpha = this.sliderMix[k];
|
|
1196
|
+
if (!this.model.omitDrawOrder && a.drawOrder !== null) {
|
|
1197
|
+
const di = keyAt(a.drawOrder.curve, this.cursors, a.drawOrder.cursor, t);
|
|
1198
|
+
if (di >= 0) {
|
|
1199
|
+
const ns = this.drawOrder.length, orders = a.drawOrder.orders, base = di * ns;
|
|
1200
|
+
for (let s = 0; s < ns; s++) this.drawOrder[s] = orders[base + s];
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1182
1203
|
for (let q = 0; q < a.slots.length; q++) {
|
|
1183
1204
|
const tl = a.slots[q], i = keyAt(tl.curve, this.cursors, tl.cursor, t);
|
|
1184
1205
|
if (i < 0) continue;
|
|
@@ -1189,9 +1210,17 @@ export class RuntimePose {
|
|
|
1189
1210
|
}
|
|
1190
1211
|
else {
|
|
1191
1212
|
const channels = tl.channels!;
|
|
1213
|
+
const slot = this.model.slots[tl.slot];
|
|
1214
|
+
const o = tl.slot * 4, d = tl.slot * 3;
|
|
1192
1215
|
for (let f = 0; f < channels.length; f++) {
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1216
|
+
const at = channels[f], v = channelValue(tl.curve, i, f, t);
|
|
1217
|
+
if (at < 4) {
|
|
1218
|
+
const n = o + at;
|
|
1219
|
+
this.light[n] = clamp01(alpha === 1 ? v : this.light[n] + (v - this.light[n]) * alpha);
|
|
1220
|
+
} else if (slot.dark !== null && !this.model.omitDarkColour) {
|
|
1221
|
+
const n = d + (at - 4);
|
|
1222
|
+
this.dark[n] = clamp01(alpha === 1 ? v : this.dark[n] + (v - this.dark[n]) * alpha);
|
|
1223
|
+
}
|
|
1195
1224
|
}
|
|
1196
1225
|
}
|
|
1197
1226
|
}
|
|
@@ -1485,7 +1514,10 @@ export class RuntimePose {
|
|
|
1485
1514
|
return true;
|
|
1486
1515
|
}
|
|
1487
1516
|
|
|
1488
|
-
/**
|
|
1517
|
+
/** Absolute/additive transform, matching `solveTransform` in src/core/constraints.ts:
|
|
1518
|
+
* read the source per target/property, edit locals or the scratch world,
|
|
1519
|
+
* then repose the union in bone order (`repose` in that module). No read-back for a local target.
|
|
1520
|
+
*/
|
|
1489
1521
|
private solveTransform(c: RuntimeTransform, k: number): void {
|
|
1490
1522
|
const W = this.world;
|
|
1491
1523
|
const w = this.w;
|
|
@@ -1515,25 +1547,34 @@ export class RuntimePose {
|
|
|
1515
1547
|
v = v < lo ? lo : v > hi ? hi : v;
|
|
1516
1548
|
}
|
|
1517
1549
|
applied = true;
|
|
1518
|
-
|
|
1550
|
+
if (c.localTarget) {
|
|
1551
|
+
const at = bi * LOCALS + PROPERTY_LOCAL[property];
|
|
1552
|
+
const current = this.locals[at];
|
|
1553
|
+
this.locals[at] = c.additive
|
|
1554
|
+
? property === P_SCALE_X || property === P_SCALE_Y ? current * (1 + (v - 1) * mix) : current + v * mix
|
|
1555
|
+
: current + (v - current) * mix;
|
|
1556
|
+
} else applyWorld(w, property, v, mix, c.additive);
|
|
1519
1557
|
}
|
|
1520
1558
|
}
|
|
1521
1559
|
if (!applied) continue;
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1560
|
+
if (!c.localTarget) {
|
|
1561
|
+
W[o + W_A] = w[W_A];
|
|
1562
|
+
W[o + W_B] = w[W_B];
|
|
1563
|
+
W[o + W_C] = w[W_C];
|
|
1564
|
+
W[o + W_D] = w[W_D];
|
|
1565
|
+
W[o + W_X] = w[W_X];
|
|
1566
|
+
W[o + W_Y] = w[W_Y];
|
|
1567
|
+
}
|
|
1528
1568
|
this.changed[n] = bi;
|
|
1529
1569
|
this.inWorld[n] = bi;
|
|
1530
1570
|
n++;
|
|
1531
1571
|
}
|
|
1532
|
-
if (n > 0) this.repose(n, n);
|
|
1572
|
+
if (n > 0) this.repose(n, c.localTarget ? 0 : n);
|
|
1533
1573
|
}
|
|
1534
1574
|
|
|
1535
|
-
/** The source's
|
|
1575
|
+
/** The source's current local or world value (`sourceValue` in src/core/constraints.ts). */
|
|
1536
1576
|
private sourceValue(c: RuntimeTransform, property: number): number {
|
|
1577
|
+
if (c.localSource) return this.locals[c.source * LOCALS + PROPERTY_LOCAL[property]] + c.offsets[property];
|
|
1537
1578
|
const W = this.world;
|
|
1538
1579
|
const s = c.source * WORLDS;
|
|
1539
1580
|
const o = c.offsets;
|
|
@@ -1617,7 +1658,10 @@ export class RuntimePose {
|
|
|
1617
1658
|
if (below[b] === 1 && this.inWorldMark[b] === 0) this.poseBone(b, false);
|
|
1618
1659
|
}
|
|
1619
1660
|
|
|
1620
|
-
/** A bone's
|
|
1661
|
+
/** A bone's locals read back from its world transform (`localFromWorld` in src/core/constraints.ts).
|
|
1662
|
+
* All five inherit modes: the no-scale forms read rotation first, rebuild
|
|
1663
|
+
* `noScaleDirection`, and retain the x-angle residual as shearX in `decompose`.
|
|
1664
|
+
*/
|
|
1621
1665
|
private localFromWorld(bi: number): void {
|
|
1622
1666
|
const m = this.model;
|
|
1623
1667
|
const W = this.world;
|
|
@@ -1715,16 +1759,32 @@ export class RuntimePose {
|
|
|
1715
1759
|
private slotValues(anim: RuntimeAnimation | null, t: number): void {
|
|
1716
1760
|
const slots = this.model.slots;
|
|
1717
1761
|
const light = this.light;
|
|
1762
|
+
const dark = this.dark;
|
|
1718
1763
|
for (let s = 0; s < slots.length; s++) {
|
|
1719
1764
|
const slot = slots[s];
|
|
1720
1765
|
this.shown[s] = slot.setupAttachment;
|
|
1766
|
+
this.drawOrder[s] = s;
|
|
1721
1767
|
const o = s * 4;
|
|
1722
1768
|
light[o] = slot.light[0];
|
|
1723
1769
|
light[o + 1] = slot.light[1];
|
|
1724
1770
|
light[o + 2] = slot.light[2];
|
|
1725
1771
|
light[o + 3] = slot.light[3];
|
|
1772
|
+
const setupDark = slot.dark;
|
|
1773
|
+
if (setupDark !== null) {
|
|
1774
|
+
const d = s * 3;
|
|
1775
|
+
dark[d] = setupDark[0];
|
|
1776
|
+
dark[d + 1] = setupDark[1];
|
|
1777
|
+
dark[d + 2] = setupDark[2];
|
|
1778
|
+
}
|
|
1726
1779
|
}
|
|
1727
1780
|
if (anim === null) return;
|
|
1781
|
+
if (!this.model.omitDrawOrder && anim.drawOrder !== null) {
|
|
1782
|
+
const di = keyAt(anim.drawOrder.curve, this.cursors, anim.drawOrder.cursor, t);
|
|
1783
|
+
if (di >= 0) {
|
|
1784
|
+
const ns = slots.length, orders = anim.drawOrder.orders, base = di * ns;
|
|
1785
|
+
for (let s = 0; s < ns; s++) this.drawOrder[s] = orders[base + s];
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1728
1788
|
const timelines = anim.slots;
|
|
1729
1789
|
for (let k = 0; k < timelines.length; k++) {
|
|
1730
1790
|
const tl = timelines[k];
|
|
@@ -1735,11 +1795,18 @@ export class RuntimePose {
|
|
|
1735
1795
|
continue;
|
|
1736
1796
|
}
|
|
1737
1797
|
const channels = tl.channels as Int32Array;
|
|
1738
|
-
const
|
|
1798
|
+
const slot = slots[tl.slot];
|
|
1799
|
+
const setup = slot.light;
|
|
1800
|
+
const setupDark = slot.dark;
|
|
1739
1801
|
const o = tl.slot * 4;
|
|
1802
|
+
const d = tl.slot * 3;
|
|
1740
1803
|
for (let c = 0; c < channels.length; c++) {
|
|
1741
1804
|
const at = channels[c];
|
|
1742
|
-
|
|
1805
|
+
if (at < 4) {
|
|
1806
|
+
light[o + at] = i < 0 ? setup[at] : clamp01(channelValue(tl.curve, i, c, t));
|
|
1807
|
+
} else if (setupDark !== null && !this.model.omitDarkColour) {
|
|
1808
|
+
dark[d + (at - 4)] = i < 0 ? setupDark[at - 4] : clamp01(channelValue(tl.curve, i, c, t));
|
|
1809
|
+
}
|
|
1743
1810
|
}
|
|
1744
1811
|
}
|
|
1745
1812
|
}
|
|
@@ -1770,6 +1837,14 @@ export class RuntimePose {
|
|
|
1770
1837
|
}
|
|
1771
1838
|
}
|
|
1772
1839
|
|
|
1840
|
+
/** Recompute vertices and overwrite clipping output, without per-frame allocation.
|
|
1841
|
+
* Concave/inverse rows describe the core's drawn coverage, not Spine's native triangle list.
|
|
1842
|
+
*/
|
|
1843
|
+
computeClipped(): void {
|
|
1844
|
+
this.computeVertices();
|
|
1845
|
+
this.clipping?.frame(this);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1773
1848
|
/** Every shown attachment's world vertices into `vertices` at its offset (`regionCorners`, `worldVertices`). */
|
|
1774
1849
|
computeVertices(): void {
|
|
1775
1850
|
const m = this.model;
|
|
@@ -1843,11 +1918,11 @@ export class RuntimePose {
|
|
|
1843
1918
|
}
|
|
1844
1919
|
}
|
|
1845
1920
|
|
|
1846
|
-
/** A world-space
|
|
1847
|
-
function applyWorld(w: Float64Array, property: number, v: number, mix: number): void {
|
|
1921
|
+
/** A world-space absolute/additive property, in `applyWorld` order from src/core/constraints.ts. */
|
|
1922
|
+
function applyWorld(w: Float64Array, property: number, v: number, mix: number, additive: boolean): void {
|
|
1848
1923
|
switch (property) {
|
|
1849
1924
|
case P_ROTATE: {
|
|
1850
|
-
let r = v * RAD - Math.atan2(w[W_C], w[W_A]);
|
|
1925
|
+
let r = additive ? v * RAD : v * RAD - Math.atan2(w[W_C], w[W_A]);
|
|
1851
1926
|
if (r > RUNTIME_PI) r -= TWO_PI;
|
|
1852
1927
|
else if (r < -RUNTIME_PI) r += TWO_PI;
|
|
1853
1928
|
r *= mix;
|
|
@@ -1862,10 +1937,10 @@ function applyWorld(w: Float64Array, property: number, v: number, mix: number):
|
|
|
1862
1937
|
return;
|
|
1863
1938
|
}
|
|
1864
1939
|
case P_X:
|
|
1865
|
-
w[W_X] = w[W_X] + (v - w[W_X]) * mix;
|
|
1940
|
+
w[W_X] = additive ? w[W_X] + v * mix : w[W_X] + (v - w[W_X]) * mix;
|
|
1866
1941
|
return;
|
|
1867
1942
|
case P_Y:
|
|
1868
|
-
w[W_Y] = w[W_Y] + (v - w[W_Y]) * mix;
|
|
1943
|
+
w[W_Y] = additive ? w[W_Y] + v * mix : w[W_Y] + (v - w[W_Y]) * mix;
|
|
1869
1944
|
return;
|
|
1870
1945
|
case P_SCALE_X:
|
|
1871
1946
|
case P_SCALE_Y: {
|
|
@@ -1873,7 +1948,7 @@ function applyWorld(w: Float64Array, property: number, v: number, mix: number):
|
|
|
1873
1948
|
const q = property === P_SCALE_X ? W_C : W_D;
|
|
1874
1949
|
const s = Math.sqrt(w[p] * w[p] + w[q] * w[q]);
|
|
1875
1950
|
if (s === 0) return;
|
|
1876
|
-
const k = 1 + ((v - s) * mix) / s;
|
|
1951
|
+
const k = additive ? 1 + (v - 1) * mix : 1 + ((v - s) * mix) / s;
|
|
1877
1952
|
w[p] *= k;
|
|
1878
1953
|
w[q] *= k;
|
|
1879
1954
|
return;
|
|
@@ -1881,8 +1956,9 @@ function applyWorld(w: Float64Array, property: number, v: number, mix: number):
|
|
|
1881
1956
|
default: {
|
|
1882
1957
|
const yAngle = Math.atan2(w[W_D], w[W_B]);
|
|
1883
1958
|
const xAngle = Math.atan2(w[W_C], w[W_A]);
|
|
1884
|
-
|
|
1885
|
-
r =
|
|
1959
|
+
// Keep the right-angle subtraction: simplifying it to v * RAD changes bits.
|
|
1960
|
+
let r = (v + 90) * RAD - (additive ? 90 * RAD : yAngle - xAngle);
|
|
1961
|
+
if (!additive) r = r > RUNTIME_PI ? r - TWO_PI : r < -RUNTIME_PI ? r + TWO_PI : r;
|
|
1886
1962
|
const angle = yAngle + r * mix;
|
|
1887
1963
|
const s = Math.sqrt(w[W_B] * w[W_B] + w[W_D] * w[W_D]);
|
|
1888
1964
|
w[W_B] = Math.cos(angle) * s;
|