rig-play 1.0.0 → 1.1.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/README.md +162 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +199 -0
- package/dist/index.js.map +1 -1
- package/dist/player.d.ts +47 -0
- package/dist/player.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -245,6 +245,24 @@ frame takes the one-`clip-path`-on-the-root path), and a mesh's `uvs` and
|
|
|
245
245
|
`triangles` are the same arrays from frame to frame for the same texture
|
|
246
246
|
mapping (a new array re-rasters the mesh).
|
|
247
247
|
|
|
248
|
+
### Reading the pose
|
|
249
|
+
|
|
250
|
+
Two reads off the pose the player holds — the last `frame`/`advance`/`seek`,
|
|
251
|
+
or the setup pose before `play` — for framing a stage around a rig:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const head = player.bone('head'); // { x, y, a, b, c, d, rotationX, rotationY, scaleX, scaleY, active }
|
|
255
|
+
const box = player.bounds(); // { x, y, width, height } | null — world units, Y up
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`bone` is the core's world transform for that bone (`rig-c/core`'s `RawBone`,
|
|
259
|
+
unchanged), and an unknown name is refused by name. `bounds` is the box of
|
|
260
|
+
every region's four corners and every mesh's vertices in the frame record the
|
|
261
|
+
renderer takes; a slot under a clip counts only inside the clip polygon's own
|
|
262
|
+
box, an inverse clip removes nothing, and bounding boxes, paths and points do
|
|
263
|
+
not draw and do not count. Both cost nothing until called and read the same
|
|
264
|
+
numbers in `dom` and `canvas` mode.
|
|
265
|
+
|
|
248
266
|
### One load, several players
|
|
249
267
|
|
|
250
268
|
One `loadModel` backs any number of players: the regions are cut once, and
|
|
@@ -493,6 +511,150 @@ on-device (Chromium, dpr 2, stage under `scale(0.25)`, ~2.2 M CSS px on screen):
|
|
|
493
511
|
| 92 meshes | `devicePixelRatio` (2) | 141.1 Mpx | 36–39 |
|
|
494
512
|
| 92 meshes | `dpr × zoom` (0.5) | 9.1 Mpx | 60–61 |
|
|
495
513
|
|
|
514
|
+
## Coming from spine-html 0.8
|
|
515
|
+
|
|
516
|
+
This is not a drop-in upgrade. 0.8 drew a Spine export (`.json` or `.skel`, plus its
|
|
517
|
+
`.atlas`) that `@esotericsoftware/spine-core` parsed and posed: the caller built the
|
|
518
|
+
`Skeleton` and the `AnimationState`, advanced them each frame, and handed the skeleton to
|
|
519
|
+
`renderer.render(skeleton)`. 1.0 loads one file, the `skeleton.model.json` that
|
|
520
|
+
`rigc build` writes, poses it through `rig-c/core` (bundled into the package, with no peer
|
|
521
|
+
dependency), and `RigPlayer` owns the track, so the caller's frame is `player.frame(dt)`.
|
|
522
|
+
A Spine export is refused by name before anything is fetched: the data has to be a rigc
|
|
523
|
+
build, and this package declares no loader for anything else.
|
|
524
|
+
|
|
525
|
+
What did not move: a positioned root element is still the rig's origin (Y up) with its
|
|
526
|
+
layout and scaling left to the caller, the DOM tier keeps its knobs under a new class
|
|
527
|
+
name, and unloading is still elements first, bitmaps second.
|
|
528
|
+
|
|
529
|
+
### 0.8 → 1.0
|
|
530
|
+
|
|
531
|
+
| 0.8.0 (`spine-html`) | 1.0.0 (`rig-play`) | Note |
|
|
532
|
+
| --- | --- | --- |
|
|
533
|
+
| `npm i spine-html @esotericsoftware/spine-core` | `npm i rig-play`, and `npm i -D rig-c` for the build tool | 0.8 declared spine-core as a peer dependency (`>=4.0.0 <4.4.0`). 1.0.0 declares no dependencies and no peer dependencies. |
|
|
534
|
+
| `loadSkeletonAssets(options)` | `loadModel(options)` | One URL instead of two. The `.atlas` and `skeleton.json` a build writes beside the document are not fetched. |
|
|
535
|
+
| `LoadSkeletonAssetsOptions.atlasUrl`, `skeletonUrl` | `LoadModelOptions.modelUrl` | |
|
|
536
|
+
| `resolvePage(pageName, atlasUrl)` | `resolvePage(pageName, modelUrl)` | The default now resolves a page name against the model URL's directory. |
|
|
537
|
+
| `crossOrigin`, `fetch` | `crossOrigin`, `fetch` | Unchanged. |
|
|
538
|
+
| `scale` (`LoadSkeletonAssetsOptions`, `LoadSkeletonJsonOptions`) | none | See below. |
|
|
539
|
+
| `SkeletonAssets` | `ModelAssets` | |
|
|
540
|
+
| `SkeletonAssets.data` | `ModelAssets.document`, `animations`, `skins`, `stage`, `spec` | `document` is opaque (`ModelDocument`); the player poses it. What a caller used to read off the skeleton data is stated as `animations` (name and duration), `skins` (names) and `stage` (the stage the build declared, or null). |
|
|
541
|
+
| `SkeletonAssets.atlas` | `ModelAssets.pageImages` | The page images, keyed by page name. There is no atlas object; where each region sits is in the document's `pages` section. |
|
|
542
|
+
| `SkeletonAssets.regionImages` | `ModelAssets.regionImages` | Same `Map<string, RegionImage>`. |
|
|
543
|
+
| `SkeletonAssets.dispose()`, `AtlasAssets.dispose()` | `ModelAssets.dispose()` | Same contract: frees the blob URLs the load minted, idempotent, called after the players are disposed. |
|
|
544
|
+
| `loadAtlasAssets` + `loadSkeletonJson` (one atlas, several skeletons) | none | One `loadModel` backs any number of `RigPlayer`s of the same rig. Two rigs are two loads, and each cuts its own regions. See below. |
|
|
545
|
+
| `loadSkeletonBinary` from `spine-html/binary` | none | 1.0.0 has one entry point. A `.skel` URL is refused by name. |
|
|
546
|
+
| `unpackRegions(atlas, pageImages)` | `unpackRegions(pages, pageImages)` | The first argument is the document's `pages` section (`readonly PageLayout[]`) instead of a `TextureAtlas`. |
|
|
547
|
+
| `revokeRegions(images)` | `revokeRegions(images)` | Unchanged. |
|
|
548
|
+
| `RegionImage` | `RegionImage` | Unchanged (`url`, `width`, `height`, in atlas units). |
|
|
549
|
+
| `DomTexture` | none | Not exported; there is no atlas page to attach a texture to. |
|
|
550
|
+
| `new Skeleton(assets.data)` and `new AnimationState(new AnimationStateData(assets.data))` | `new RigPlayer(root, assets, options?)` | The player holds the pose and the track. |
|
|
551
|
+
| `state.setAnimation(0, 'walk', true)` | `player.play('walk', { loop: true })` | `loop` defaults to true. `play` opens the animation at its first pose with the physics reset there. `player.current` names what is playing. |
|
|
552
|
+
| `state.update(delta)`, `state.apply(skeleton)`, `skeleton.update(delta)`, `skeleton.updateWorldTransform(Physics.update)`, `renderer.render(skeleton)` | `player.frame(dt)` | Or `player.advance(dt)` then `player.draw()`, to time the two apart. |
|
|
553
|
+
| time scale (the caller's own, on the spine-core side) | `player.timeScale` | Multiplies every step. 0 holds the pose without re-posing it; a negative value is not stepped. |
|
|
554
|
+
| holding a pose (the caller stopped advancing) | `player.pause()`, `player.resume()` | `frame` keeps drawing the held pose. |
|
|
555
|
+
| moving to a time (the caller's own, on the spine-core side) | `player.seek(time)`, `RigPlayerOptions.seekStep`, `player.time` | A seek reopens the animation and steps forward in `seekStep` increments (1/60 s by default). Events and completes crossed on the way are not fired. |
|
|
556
|
+
| events (the caller's own listener on the spine-core side) | `player.on('event', listener)`, `player.on('complete', listener)` | `'event'` hands a `RigEvent` (`name`, `time`, `int`, `float`, `string`); `'complete'` hands `{ animation }`. `on` returns a function that removes the listener. |
|
|
557
|
+
| skin (set by the caller on its `Skeleton`) | `RigPlayerOptions.skin` | A constructor option. The names are in `ModelAssets.skins`. Without it the rig is posed with no skin set. |
|
|
558
|
+
| `SpineHtmlRenderer` | `DomRenderer`, reached as `player.renderer` | In `mode: 'dom'`, the default. `mode: 'canvas'` is new and makes `player.renderer` a `CanvasRenderer`. |
|
|
559
|
+
| `new SpineHtmlRenderer(root, regionImages)` | `new DomRenderer(root, regionImages, pages?)` | Only for drawing your own frames; `RigPlayer` constructs its renderer. `pages` is the page-image map the mesh tier samples (`ModelAssets.pageImages`). A hand-built `regionImages` map is still accepted. |
|
|
560
|
+
| `renderer.render(skeleton)` | `renderer.render(frame)` | Takes a `PosedFrame`, not a skeleton. `player.posedFrame()` returns the one the player would draw. |
|
|
561
|
+
| `renderer.pixelRatio` | `player.renderer.pixelRatio` | On both renderers. Carried over when `player.mode` changes. |
|
|
562
|
+
| `renderer.syncPixelRatio()` | `DomRenderer.syncPixelRatio()` | DOM mode only. `player.renderer` is typed as either renderer, so narrow it (`instanceof DomRenderer`) before calling. |
|
|
563
|
+
| `renderer.meshBackingPixels` | `DomRenderer.meshBackingPixels` | DOM mode only. |
|
|
564
|
+
| `renderer.meshBackend`, `meshBackendActive`, type `MeshBackend` | `DomRenderer.meshBackend`, `meshBackendActive`, type `MeshBackend` | Same values (`'canvas2d'` default, `'webgl'`). Also settable up front as `RigPlayerOptions.meshBackend`. |
|
|
565
|
+
| `renderer.triangleExpand` | `DomRenderer.triangleExpand` | Same default (0.5). |
|
|
566
|
+
| `renderer.clipping` | `player.renderer.clipping` | On both renderers. Carried over when `player.mode` changes. |
|
|
567
|
+
| `clipCount`, `clipSkipCount`, `clipWriteCount` | same names on `DomRenderer` | `clipSkipCount` counts less than it did: a second clip met while one is in force, and a clip on an inactive bone, no longer reach the frame, so they are not counted. |
|
|
568
|
+
| `meshCount`, `meshReuseCount`, `canvasReallocCount`, `triangleCount` | same names on `DomRenderer` | |
|
|
569
|
+
| tint: skeleton × slot × attachment colour | `player.color` × slot × attachment | `player.color` is `[r, g, b, a]` and takes the place of the skeleton's colour. The product arrives as `tint` on each `PosedSlot`. |
|
|
570
|
+
| dark (two-colour) tint: unsupported | `DomRenderer`: unsupported. `CanvasRenderer`: drawn | Carried as `dark` on a `PosedSlot`. The canvas mode's canvas2d fallback draws alpha only. |
|
|
571
|
+
| `renderer.dispose()` | `player.dispose()` | Then `assets.dispose()`, as before. `DomRenderer.dispose()` and `CanvasRenderer.dispose()` exist for a renderer you constructed yourself. |
|
|
572
|
+
|
|
573
|
+
### No counterpart in 1.0.0
|
|
574
|
+
|
|
575
|
+
Things a 0.8 caller could do because it held the spine-core objects, or through a 0.8
|
|
576
|
+
export, for which 1.0.0 declares nothing:
|
|
577
|
+
|
|
578
|
+
- **A Spine export as input.** `loadSkeletonJson`, `loadSkeletonBinary`, `loadAtlasAssets`,
|
|
579
|
+
the `spine-html/binary` entry and `DomTexture` have no counterpart, and neither does
|
|
580
|
+
choosing which spine-core generation reads the data. `loadModel` reads a compiled model
|
|
581
|
+
document and nothing else.
|
|
582
|
+
- **Scaling the skeleton as it is read.** `LoadModelOptions` declares no `scale`. Scaling
|
|
583
|
+
the root element is still the caller's.
|
|
584
|
+
- **One unpacked atlas shared by different skeletons.** A document is one rig, and each
|
|
585
|
+
`loadModel` cuts its own regions and owns its own blob URLs. Sharing is per rig: one
|
|
586
|
+
load, several players.
|
|
587
|
+
- **More than one track, mixing between animations, or queueing one after another.**
|
|
588
|
+
`RigPlayer` declares one track; `play` reopens it at the animation's first pose.
|
|
589
|
+
- **Reading or writing a bone.** No bone is exposed: nothing sets a bone's local values
|
|
590
|
+
between the step and the draw, and nothing returns a bone's world transform. What is
|
|
591
|
+
readable is the posed slots: `player.posedFrame()` carries each drawn slot's world
|
|
592
|
+
`corners` or `vertices`.
|
|
593
|
+
- **A bounds query for the current pose.** None is declared. `ModelAssets.stage` is the
|
|
594
|
+
stage the document states (or null), which is the rig's declared framing and not the
|
|
595
|
+
box of the pose being drawn.
|
|
596
|
+
- **Changing the skin after construction.** `skin` is a `RigPlayerOptions` field and
|
|
597
|
+
`RigPlayer` declares no way to change it later; a second player over the same assets
|
|
598
|
+
is the declared route.
|
|
599
|
+
- **Changing one slot from code** (its attachment, its colour). `RigPlayer` declares only
|
|
600
|
+
the whole-rig `color`. The frame record is a plain object, so a caller that draws its
|
|
601
|
+
own frames through `DomRenderer.render` or `CanvasRenderer.render` decides what is in
|
|
602
|
+
it.
|
|
603
|
+
- **Choosing how physics is stepped each frame.** 0.8's loop passed `Physics.update`
|
|
604
|
+
itself; 1.0.0 declares no physics option. `play` and `seek` reset physics at the
|
|
605
|
+
animation's first pose and every step advances it.
|
|
606
|
+
- **Track callbacks other than the two declared.** `on` accepts `'event'` and
|
|
607
|
+
`'complete'`.
|
|
608
|
+
|
|
609
|
+
Knobs on `DomRenderer` that `CanvasRenderer` does not declare:
|
|
610
|
+
|
|
611
|
+
- `syncPixelRatio()`. `CanvasRenderer.pixelRatio` exists and is set by hand
|
|
612
|
+
(`devicePixelRatio * rootScale`).
|
|
613
|
+
- `meshBackend` and `meshBackendActive`. The canvas mode has its own pair, `backend` and
|
|
614
|
+
`backendActive` (type `CanvasBackend`, `'webgl'` by default).
|
|
615
|
+
- `triangleExpand`, `meshBackingPixels`, `clipWriteCount`, `meshCount`, `meshReuseCount`,
|
|
616
|
+
`triangleCount`. The canvas mode's counters are `drawCalls`, `blendFlushes`,
|
|
617
|
+
`trianglesDrawn`, `framesReused` and `blits`.
|
|
618
|
+
|
|
619
|
+
Both declare `pixelRatio`, `clipping`, `clipCount`, `clipSkipCount`, `canvasReallocCount`,
|
|
620
|
+
`render` and `dispose`. Only `pixelRatio` and `clipping` carry over when `player.mode`
|
|
621
|
+
changes.
|
|
622
|
+
|
|
623
|
+
### Before and after
|
|
624
|
+
|
|
625
|
+
0.8.0:
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
import { Skeleton, AnimationState, AnimationStateData, Physics } from '@esotericsoftware/spine-core';
|
|
629
|
+
import { loadSkeletonAssets, SpineHtmlRenderer } from 'spine-html';
|
|
630
|
+
const assets = await loadSkeletonAssets({ atlasUrl: '/hero/hero.atlas', skeletonUrl: '/hero/hero.json' });
|
|
631
|
+
const skeleton = new Skeleton(assets.data);
|
|
632
|
+
const state = new AnimationState(new AnimationStateData(assets.data));
|
|
633
|
+
state.setAnimation(0, 'walk', true);
|
|
634
|
+
const renderer = new SpineHtmlRenderer(rootElement, assets.regionImages);
|
|
635
|
+
function frame(delta: number) {
|
|
636
|
+
state.update(delta);
|
|
637
|
+
state.apply(skeleton);
|
|
638
|
+
skeleton.update(delta);
|
|
639
|
+
skeleton.updateWorldTransform(Physics.update);
|
|
640
|
+
renderer.render(skeleton);
|
|
641
|
+
}
|
|
642
|
+
renderer.dispose(); assets.dispose(); // on unload
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
1.0.0:
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
import { loadModel, RigPlayer } from 'rig-play';
|
|
649
|
+
const assets = await loadModel({ modelUrl: '/rigs/walk/skeleton.model.json' });
|
|
650
|
+
const player = new RigPlayer(rootElement, assets);
|
|
651
|
+
player.play('walk', { loop: true });
|
|
652
|
+
function frame(delta: number) {
|
|
653
|
+
player.frame(delta);
|
|
654
|
+
}
|
|
655
|
+
player.dispose(); assets.dispose(); // on unload
|
|
656
|
+
```
|
|
657
|
+
|
|
496
658
|
## Demo (this repository)
|
|
497
659
|
|
|
498
660
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { type CanvasBackend, CanvasRenderer, type CanvasViewport } from './Canva
|
|
|
2
2
|
export { DomRenderer, type MeshBackend } from './DomRenderer.js';
|
|
3
3
|
export { type PageLayout, type PageRegion, type RegionImage, revokeRegions, unpackRegions } from './DomTexture.js';
|
|
4
4
|
export { type LoadModelOptions, loadModel, type ModelAssets, type ModelDocument } from './loadModel.js';
|
|
5
|
-
export { type RenderMode, type RigEvent, RigPlayer, type RigPlayerOptions } from './player.js';
|
|
5
|
+
export { type BoneWorld, type PoseBounds, type RenderMode, type RigEvent, RigPlayer, type RigPlayerOptions } from './player.js';
|
|
6
6
|
export type { BlendMode, PosedClip, PosedFrame, PosedMesh, PosedRegion, PosedSlot, } from './posedFrame.js';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnH,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACxG,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnH,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACxG,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAChI,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4515,6 +4515,8 @@ function rawNumber(v) {
|
|
|
4515
4515
|
function constraintRecords(doc) {
|
|
4516
4516
|
return doc.constraints.flatMap((c) => c.record === void 0 ? [] : [c.record]);
|
|
4517
4517
|
}
|
|
4518
|
+
const NOT_ADMITTED = [];
|
|
4519
|
+
const POSED_BLOCKS = ["setup.bones", "setup.slots", "setup.drawOrder", "setup.attachments", "setup.clips", "setup.clipped"];
|
|
4518
4520
|
const activeOfView = /* @__PURE__ */ new WeakMap();
|
|
4519
4521
|
function activeBones(doc) {
|
|
4520
4522
|
const kept = activeOfView.get(doc);
|
|
@@ -4582,6 +4584,97 @@ function resolveShown(doc, slotName, placeholder) {
|
|
|
4582
4584
|
const agree = rows.every((r) => r.name === rows[0].name && r.path === rows[0].path);
|
|
4583
4585
|
return agree ? filling[0] : { conflict: rows.map((r) => ({ skin: r.skin, shown: r.name, path: r.path })) };
|
|
4584
4586
|
}
|
|
4587
|
+
function poseSetup(doc, plant = {}, physics) {
|
|
4588
|
+
const evaluate = plant.evaluate ?? worldTransforms;
|
|
4589
|
+
const resolve = plant.shown ?? shownAttachment;
|
|
4590
|
+
const colour = plant.colour ?? readColour;
|
|
4591
|
+
const blend = plant.blend ?? readBlend;
|
|
4592
|
+
const round = plant.round ?? gridRound;
|
|
4593
|
+
const bonesWhy = constraintsAbsentWhy(doc, solverRules(plant.solver));
|
|
4594
|
+
let bones = null;
|
|
4595
|
+
let setupWorld = null;
|
|
4596
|
+
const applied = [];
|
|
4597
|
+
if (bonesWhy === null) {
|
|
4598
|
+
const active = activeBones(doc);
|
|
4599
|
+
const records = constraintRecords(doc);
|
|
4600
|
+
setupWorld = applyConstraints(doc.bones, evaluate(doc.bones, active), active, plant.constraints ? plant.constraints(records) : records, null, applied, physics, void 0, solverRules(plant.solver));
|
|
4601
|
+
const world = setupWorld;
|
|
4602
|
+
bones = doc.bones.map((b) => {
|
|
4603
|
+
const t = world.get(b.name);
|
|
4604
|
+
if (t === void 0) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
|
|
4605
|
+
return [b.name, round(t.worldX), round(t.worldY), round(t.a), round(t.b), round(t.c), round(t.d), active.has(b.name) ? 1 : 0, b.parent ?? null];
|
|
4606
|
+
});
|
|
4607
|
+
}
|
|
4608
|
+
const conflicts = [];
|
|
4609
|
+
const slotRows = [];
|
|
4610
|
+
const liveBones = activeBones(doc);
|
|
4611
|
+
const slotGate = plant.slotTimelines ?? slotTimelinesApply;
|
|
4612
|
+
for (const slot of doc.slots) {
|
|
4613
|
+
const pose = {
|
|
4614
|
+
placeholder: slot.setup,
|
|
4615
|
+
light: slot.color === void 0 ? [1, 1, 1, 1] : [...colour(slot.color)],
|
|
4616
|
+
dark: slot.dark === void 0 ? null : readColour(slot.dark).slice(0, 3)
|
|
4617
|
+
};
|
|
4618
|
+
if (slotGate(doc, slot, liveBones)) applySliderSlots(slot.name, pose, applied);
|
|
4619
|
+
const posedRecord = { ...slot, setup: pose.placeholder };
|
|
4620
|
+
const shown = pose.placeholder === null ? null : resolve(doc, posedRecord);
|
|
4621
|
+
if (shown !== null && "conflict" in shown) {
|
|
4622
|
+
conflicts.push(`slot "${slot.name}" placeholder "${pose.placeholder}" is filled by skins ${shown.conflict.map((c) => `"${c.skin}" (shows ${JSON.stringify(c.shown)}, path ${JSON.stringify(c.path)})`).join(", ")}`);
|
|
4623
|
+
continue;
|
|
4624
|
+
}
|
|
4625
|
+
const row = shown === null ? null : shownRow(shown);
|
|
4626
|
+
const [r, g, b, a] = pose.light;
|
|
4627
|
+
const dark = pose.dark;
|
|
4628
|
+
slotRows.push([
|
|
4629
|
+
slot.name,
|
|
4630
|
+
row === null ? null : row.name,
|
|
4631
|
+
round(r),
|
|
4632
|
+
round(g),
|
|
4633
|
+
round(b),
|
|
4634
|
+
round(a),
|
|
4635
|
+
dark === null ? null : [round(dark[0]), round(dark[1]), round(dark[2])],
|
|
4636
|
+
row === null ? null : row.path,
|
|
4637
|
+
blend(slot)
|
|
4638
|
+
]);
|
|
4639
|
+
}
|
|
4640
|
+
const slotsWhy = (bonesWhy === null ? null : slidersWhy(doc)) ?? (conflicts.length === 0 ? null : `${conflicts.join("; ")} — under --skin all the LAST of them in the Spine file's skin order wins, and that order is the emitter's (default first, the rest in the editor's order), not the model's; the per-skin dumps (--skin <name>) judge it`);
|
|
4641
|
+
const slots = slotsWhy === null ? slotRows : null;
|
|
4642
|
+
const upstream = [bonesWhy === null ? null : `setup.bones is absent (${bonesWhy}), and every vertex goes through a bone's world matrix`, slotsWhy === null ? null : "setup.slots is absent, so what a slot shows is not posed"].filter((x) => x !== null);
|
|
4643
|
+
const orderWhy = bonesWhy === null ? null : slidersKeyingWhy(doc, "drawOrder");
|
|
4644
|
+
let drawOrder = null;
|
|
4645
|
+
if (orderWhy === null) {
|
|
4646
|
+
let order = doc.slots.map((_s, i) => i);
|
|
4647
|
+
for (const app of applied) order = (plant.drawOrder ?? drawOrderAt)(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
|
|
4648
|
+
drawOrder = order.map((i) => doc.slots[i].name);
|
|
4649
|
+
}
|
|
4650
|
+
let attachments = null;
|
|
4651
|
+
let clips = null;
|
|
4652
|
+
let clipsWhy = upstream.length === 0 ? null : upstream.join("; ");
|
|
4653
|
+
let attachmentsWhy = clipsWhy;
|
|
4654
|
+
let clipped = null;
|
|
4655
|
+
let clippedWhy = clipsWhy ?? orderWhy;
|
|
4656
|
+
let shownInOrder = null;
|
|
4657
|
+
if (clipsWhy === null && setupWorld !== null && drawOrder !== null) {
|
|
4658
|
+
const world = setupWorld;
|
|
4659
|
+
const placeholders = new Map(doc.slots.map((sl) => [sl.name, sl.setup]));
|
|
4660
|
+
const states = attachmentStates(doc, resolve, placeholders, null, applied, plant);
|
|
4661
|
+
const rank = new Map(drawOrder.map((n, i) => [n, i]));
|
|
4662
|
+
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
4663
|
+
shownInOrder = states.why.length === 0 ? shown : null;
|
|
4664
|
+
const posed = poseGeometry(shown, world, sourceOfDoc(doc), round, { region: plant.region, vertices: plant.vertices }, drawWalkOf(doc, drawOrder, plant));
|
|
4665
|
+
const stateWhy = states.why.length === 0 ? null : `${states.why.join("; ")}`;
|
|
4666
|
+
attachments = stateWhy === null ? posed.attachments : null;
|
|
4667
|
+
attachmentsWhy = stateWhy ?? posed.attachmentsWhy;
|
|
4668
|
+
clips = stateWhy === null ? posed.clips : null;
|
|
4669
|
+
clipsWhy = stateWhy;
|
|
4670
|
+
clipped = stateWhy === null ? posed.clipped : null;
|
|
4671
|
+
clippedWhy = stateWhy ?? posed.clippedWhy;
|
|
4672
|
+
}
|
|
4673
|
+
const why = { "setup.bones": bonesWhy, "setup.slots": slotsWhy, "setup.drawOrder": orderWhy, "setup.attachments": attachmentsWhy, "setup.clips": clipsWhy, "setup.clipped": clippedWhy };
|
|
4674
|
+
const absent = NOT_ADMITTED.map(([block, reason]) => [block, reason]);
|
|
4675
|
+
for (const block of POSED_BLOCKS) if (why[block] !== null) absent.push([block, why[block]]);
|
|
4676
|
+
return { setup: { bones, slots, drawOrder, attachments, clips, clipped }, absent, world: setupWorld, shown: shownInOrder };
|
|
4677
|
+
}
|
|
4585
4678
|
function drawWalkOf(doc, order, plant = {}) {
|
|
4586
4679
|
const active = activeBones(doc);
|
|
4587
4680
|
return {
|
|
@@ -4601,6 +4694,22 @@ function sourceOfDoc(doc) {
|
|
|
4601
4694
|
return g?.kind === "mesh" ? g.vertices : sourceProblem(doc.skins, skin, slot, source) ?? `the linked mesh's source "${source}" carries no vertices`;
|
|
4602
4695
|
};
|
|
4603
4696
|
}
|
|
4697
|
+
function slidersKeyingWhy(doc, group) {
|
|
4698
|
+
const keyed = doc.constraints.flatMap((c) => {
|
|
4699
|
+
if (c.kind !== "slider") return [];
|
|
4700
|
+
const anim = doc.animations.find((a) => a.name === c.animation);
|
|
4701
|
+
return anim !== void 0 && anim.timelines[group].length > 0 ? [`slider "${c.name}" applies animation "${c.animation}", which keys the draw order`] : [];
|
|
4702
|
+
});
|
|
4703
|
+
return keyed.length === 0 ? null : `${keyed.join("; ")} — a slider's time is read off the bones, which are absent`;
|
|
4704
|
+
}
|
|
4705
|
+
function slidersWhy(doc) {
|
|
4706
|
+
const keyed = doc.constraints.flatMap((c) => {
|
|
4707
|
+
if (c.kind !== "slider") return [];
|
|
4708
|
+
const slots = doc.animations.find((a) => a.name === c.animation)?.slots ?? [];
|
|
4709
|
+
return slots.length === 0 ? [] : [`slider "${c.name}" applies animation "${c.animation}", which keys slot(s) ${slots.map((x) => `"${x}"`).join(", ")}`];
|
|
4710
|
+
});
|
|
4711
|
+
return keyed.length === 0 ? null : `${keyed.join("; ")} — the oracle's setup applies sliders, and a slider poses the slots its animation keys at a time read off the bones, which are absent`;
|
|
4712
|
+
}
|
|
4604
4713
|
function drawOrderOf(shown, order) {
|
|
4605
4714
|
const rank = new Map(order.map((n, r) => [n, r]));
|
|
4606
4715
|
return [...shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
@@ -4677,6 +4786,22 @@ function rawDrawn(doc, shown, world, order, plant, mode = RAW_MODE) {
|
|
|
4677
4786
|
const clipped = geometry.drawnClipped.map((c) => [c[0], c[1], c[2], c[3].map((v) => numberOf(v, `clipped vertex of slot "${c[0]}"`, mode)), c[4].map((v) => numberOf(v, `clipped uv of slot "${c[0]}"`, mode)), c[5]]);
|
|
4678
4787
|
return { drawn, clips, clipped };
|
|
4679
4788
|
}
|
|
4789
|
+
function poseRawSetup(doc, plant = {}) {
|
|
4790
|
+
return setupPoseIn(doc, plant, RAW_MODE);
|
|
4791
|
+
}
|
|
4792
|
+
function setupPoseIn(doc, plant, mode) {
|
|
4793
|
+
const { world, shown, slots, drawOrder } = setupPosed(doc, plant, mode);
|
|
4794
|
+
const drawn = rawDrawn(doc, shown, world, drawOrder, plant, mode);
|
|
4795
|
+
return { trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...drawn, events: [], shown: drawOrderOf(shown, drawOrder) };
|
|
4796
|
+
}
|
|
4797
|
+
function setupPosed(doc, plant, mode) {
|
|
4798
|
+
const posed = poseSetup(doc, { ...plant, ...roundPlant(mode) }, freshStepContext(plant.physicsStep));
|
|
4799
|
+
const absent = posed.absent.filter(([block]) => block !== "setup.clipped");
|
|
4800
|
+
if (absent.length > 0) throw new CoreInputError(`the raw setup pose leaves ${absent.map(([b, why]) => `${b} out (${why})`).join("; ")}`);
|
|
4801
|
+
const { setup, world, shown } = posed;
|
|
4802
|
+
if (world === null || shown === null || setup.slots === null || setup.drawOrder === null) throw new CoreInputError("the raw setup pose was not posed");
|
|
4803
|
+
return { world, shown, slots: setup.slots, drawOrder: setup.drawOrder };
|
|
4804
|
+
}
|
|
4680
4805
|
function loopedTime(trackTime, duration) {
|
|
4681
4806
|
return duration === 0 ? 0 : trackTime % duration;
|
|
4682
4807
|
}
|
|
@@ -7095,6 +7220,7 @@ class RigPlayer {
|
|
|
7095
7220
|
loop = true;
|
|
7096
7221
|
paused = false;
|
|
7097
7222
|
pose = null;
|
|
7223
|
+
setup = null;
|
|
7098
7224
|
listeners = {
|
|
7099
7225
|
event: /* @__PURE__ */ new Set(),
|
|
7100
7226
|
complete: /* @__PURE__ */ new Set()
|
|
@@ -7216,6 +7342,79 @@ class RigPlayer {
|
|
|
7216
7342
|
if (this.pose === null) return;
|
|
7217
7343
|
this.renderer.render(this.posedFrame());
|
|
7218
7344
|
}
|
|
7345
|
+
/**
|
|
7346
|
+
* Where `name` is in the pose the player holds — the pose of the last
|
|
7347
|
+
* `advance`/`frame`/`seek`, or the setup pose before `play`. Read off the
|
|
7348
|
+
* core's pose, so it is the same in `dom` and `canvas` mode and costs nothing
|
|
7349
|
+
* until called. An unknown bone is refused by name.
|
|
7350
|
+
*/
|
|
7351
|
+
bone(name) {
|
|
7352
|
+
const pose = this.posed();
|
|
7353
|
+
for (const bone2 of pose.bones) {
|
|
7354
|
+
if (bone2.name !== name) continue;
|
|
7355
|
+
return {
|
|
7356
|
+
name,
|
|
7357
|
+
active: bone2.active,
|
|
7358
|
+
x: bone2.worldX,
|
|
7359
|
+
y: bone2.worldY,
|
|
7360
|
+
a: bone2.a,
|
|
7361
|
+
b: bone2.b,
|
|
7362
|
+
c: bone2.c,
|
|
7363
|
+
d: bone2.d,
|
|
7364
|
+
rotationX: bone2.rotationX,
|
|
7365
|
+
rotationY: bone2.rotationY,
|
|
7366
|
+
scaleX: bone2.scaleX,
|
|
7367
|
+
scaleY: bone2.scaleY
|
|
7368
|
+
};
|
|
7369
|
+
}
|
|
7370
|
+
throw new Error(`no bone named ${JSON.stringify(name)} in this document (bones: ${pose.bones.map((b) => b.name).join(", ")})`);
|
|
7371
|
+
}
|
|
7372
|
+
/**
|
|
7373
|
+
* The box of what the pose the player holds draws, in world units, Y up:
|
|
7374
|
+
* every region's four corners and every mesh's vertices, in the same frame
|
|
7375
|
+
* record the renderer takes (`posedFrame`). A slot under a clip counts only
|
|
7376
|
+
* inside the clip polygon's own box (an inverse clip removes nothing from
|
|
7377
|
+
* the count); bounding boxes, paths and points are not drawn and do not
|
|
7378
|
+
* count. `null` when nothing draws. The same numbers in `dom` and `canvas`
|
|
7379
|
+
* mode; nothing is computed until called.
|
|
7380
|
+
*/
|
|
7381
|
+
bounds() {
|
|
7382
|
+
const frame2 = this.pose === null ? frameOf(this.source, this.posed(), this.color) : this.posedFrame();
|
|
7383
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
7384
|
+
for (const slot of frame2.order) {
|
|
7385
|
+
const points = slot.kind === "region" ? slot.corners : slot.vertices;
|
|
7386
|
+
let lo = -Infinity, lb = -Infinity, hi = Infinity, hb = Infinity;
|
|
7387
|
+
if (slot.clip && !slot.clip.inverse) {
|
|
7388
|
+
const c = slot.clip.polygon;
|
|
7389
|
+
lo = Infinity;
|
|
7390
|
+
lb = Infinity;
|
|
7391
|
+
hi = -Infinity;
|
|
7392
|
+
hb = -Infinity;
|
|
7393
|
+
for (let i = 0; i < c.length; i += 2) {
|
|
7394
|
+
if (c[i] < lo) lo = c[i];
|
|
7395
|
+
if (c[i] > hi) hi = c[i];
|
|
7396
|
+
if (c[i + 1] < lb) lb = c[i + 1];
|
|
7397
|
+
if (c[i + 1] > hb) hb = c[i + 1];
|
|
7398
|
+
}
|
|
7399
|
+
}
|
|
7400
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
7401
|
+
const x = Math.min(Math.max(points[i], lo), hi);
|
|
7402
|
+
const y = Math.min(Math.max(points[i + 1], lb), hb);
|
|
7403
|
+
if (x < minX) minX = x;
|
|
7404
|
+
if (x > maxX) maxX = x;
|
|
7405
|
+
if (y < minY) minY = y;
|
|
7406
|
+
if (y > maxY) maxY = y;
|
|
7407
|
+
}
|
|
7408
|
+
}
|
|
7409
|
+
if (minX === Infinity) return null;
|
|
7410
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
7411
|
+
}
|
|
7412
|
+
/** The pose the reads above see: the track's, or the setup pose before `play` (posed once). */
|
|
7413
|
+
posed() {
|
|
7414
|
+
if (this.pose !== null) return this.pose;
|
|
7415
|
+
if (this.setup === null) this.setup = poseRawSetup(this.view);
|
|
7416
|
+
return this.setup;
|
|
7417
|
+
}
|
|
7219
7418
|
/** The pose the player holds, as the frame record the renderer draws. */
|
|
7220
7419
|
posedFrame() {
|
|
7221
7420
|
if (this.pose === null) return { order: [] };
|