@playcanvas/web-components 0.15.0 → 0.17.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/dist/app.d.cts +14 -13
- package/dist/app.d.ts +14 -13
- package/dist/async-element.d.cts +13 -13
- package/dist/async-element.d.ts +13 -13
- package/dist/components/anim-clip.d.cts +127 -0
- package/dist/components/anim-clip.d.ts +127 -0
- package/dist/components/anim-component.d.cts +235 -0
- package/dist/components/anim-component.d.ts +235 -0
- package/dist/components/{listener-component.d.cts → audio-listener-component.d.cts} +7 -5
- package/dist/components/{listener-component.d.ts → audio-listener-component.d.ts} +7 -5
- package/dist/components/button-component.d.cts +5 -1
- package/dist/components/button-component.d.ts +5 -1
- package/dist/components/camera-component.d.cts +2 -0
- package/dist/components/camera-component.d.ts +2 -0
- package/dist/components/collision-component.d.cts +5 -1
- package/dist/components/collision-component.d.ts +5 -1
- package/dist/components/component.d.cts +3 -2
- package/dist/components/component.d.ts +3 -2
- package/dist/components/element-component.d.cts +7 -0
- package/dist/components/element-component.d.ts +7 -0
- package/dist/components/gsplat-component.d.cts +2 -0
- package/dist/components/gsplat-component.d.ts +2 -0
- package/dist/components/joint-component.d.cts +2 -0
- package/dist/components/joint-component.d.ts +2 -0
- package/dist/components/{layoutchild-component.d.cts → layout-child-component.d.cts} +3 -1
- package/dist/components/{layoutchild-component.d.ts → layout-child-component.d.ts} +3 -1
- package/dist/components/{layoutgroup-component.d.cts → layout-group-component.d.cts} +3 -1
- package/dist/components/{layoutgroup-component.d.ts → layout-group-component.d.ts} +3 -1
- package/dist/components/light-component.d.cts +2 -0
- package/dist/components/light-component.d.ts +2 -0
- package/dist/components/{particlesystem-component.d.cts → particle-system-component.d.cts} +3 -1
- package/dist/components/{particlesystem-component.d.ts → particle-system-component.d.ts} +3 -1
- package/dist/components/render-component.d.cts +2 -0
- package/dist/components/render-component.d.ts +2 -0
- package/dist/components/{rigidbody-component.d.cts → rigid-body-component.d.cts} +3 -1
- package/dist/components/{rigidbody-component.d.ts → rigid-body-component.d.ts} +3 -1
- package/dist/components/screen-component.d.cts +2 -0
- package/dist/components/screen-component.d.ts +2 -0
- package/dist/components/script-component.d.cts +23 -12
- package/dist/components/script-component.d.ts +23 -12
- package/dist/components/{script.d.cts → script-instance.d.cts} +7 -7
- package/dist/components/{script.d.ts → script-instance.d.ts} +7 -7
- package/dist/components/{scrollview-component.d.cts → scroll-view-component.d.cts} +3 -1
- package/dist/components/{scrollview-component.d.ts → scroll-view-component.d.ts} +3 -1
- package/dist/components/scrollbar-component.d.cts +2 -0
- package/dist/components/scrollbar-component.d.ts +2 -0
- package/dist/components/sound-component.d.cts +3 -1
- package/dist/components/sound-component.d.ts +3 -1
- package/dist/components/sound-slot.d.cts +2 -2
- package/dist/components/sound-slot.d.ts +2 -2
- package/dist/custom-elements.json +3154 -2005
- package/dist/entity-base.d.cts +6 -5
- package/dist/entity-base.d.ts +6 -5
- package/dist/entity-owner.d.cts +118 -0
- package/dist/entity-owner.d.ts +118 -0
- package/dist/entity.d.cts +8 -101
- package/dist/entity.d.ts +8 -101
- package/dist/index.d.cts +26 -21
- package/dist/index.d.ts +26 -21
- package/dist/model.d.cts +67 -24
- package/dist/model.d.ts +67 -24
- package/dist/pwc.cjs +1766 -619
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +1766 -619
- package/dist/pwc.js.map +1 -1
- package/dist/pwc.min.js +1 -1
- package/dist/pwc.min.js.map +1 -1
- package/dist/pwc.min.mjs +1 -1
- package/dist/pwc.min.mjs.map +1 -1
- package/dist/pwc.mjs +1762 -618
- package/dist/pwc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +203 -89
- package/dist/{module.d.cts → wasm.d.cts} +6 -6
- package/dist/{module.d.ts → wasm.d.ts} +6 -6
- package/dist/web-types.json +556 -273
- package/package.json +7 -7
- package/src/app.ts +45 -35
- package/src/async-element.ts +14 -14
- package/src/components/anim-clip.ts +395 -0
- package/src/components/anim-component.ts +719 -0
- package/src/components/{listener-component.ts → audio-listener-component.ts} +8 -6
- package/src/components/button-component.ts +5 -1
- package/src/components/camera-component.ts +2 -0
- package/src/components/collision-component.ts +5 -1
- package/src/components/component.ts +4 -3
- package/src/components/element-component.ts +7 -0
- package/src/components/gsplat-component.ts +2 -0
- package/src/components/joint-component.ts +2 -0
- package/src/components/{layoutchild-component.ts → layout-child-component.ts} +4 -2
- package/src/components/{layoutgroup-component.ts → layout-group-component.ts} +4 -2
- package/src/components/light-component.ts +2 -0
- package/src/components/{particlesystem-component.ts → particle-system-component.ts} +4 -2
- package/src/components/render-component.ts +2 -0
- package/src/components/{rigidbody-component.ts → rigid-body-component.ts} +4 -2
- package/src/components/screen-component.ts +2 -0
- package/src/components/script-component.ts +63 -42
- package/src/components/{script.ts → script-instance.ts} +14 -14
- package/src/components/{scrollview-component.ts → scroll-view-component.ts} +4 -2
- package/src/components/scrollbar-component.ts +2 -0
- package/src/components/sound-component.ts +4 -2
- package/src/components/sound-slot.ts +4 -4
- package/src/entity-base.ts +6 -5
- package/src/entity-owner.ts +298 -0
- package/src/entity.ts +15 -270
- package/src/index.ts +32 -24
- package/src/model.ts +159 -79
- package/src/node.ts +9 -5
- package/src/{module.ts → wasm.ts} +8 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcanvas/web-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"author": "PlayCanvas <support@playcanvas.com>",
|
|
5
5
|
"homepage": "https://playcanvas.com",
|
|
6
6
|
"description": "Web Components for the PlayCanvas Engine",
|
|
@@ -91,25 +91,25 @@
|
|
|
91
91
|
"@rollup/plugin-terser": "1.0.0",
|
|
92
92
|
"@rollup/plugin-typescript": "12.3.0",
|
|
93
93
|
"@tweenjs/tween.js": "25.0.0",
|
|
94
|
-
"@vitest/coverage-v8": "4.1.
|
|
95
|
-
"concurrently": "10.0.
|
|
94
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
95
|
+
"concurrently": "10.0.5",
|
|
96
96
|
"custom-element-jet-brains-integration": "1.7.0",
|
|
97
97
|
"custom-element-vs-code-integration": "1.5.0",
|
|
98
98
|
"earcut": "3.2.3",
|
|
99
99
|
"eslint": "9.39.5",
|
|
100
100
|
"globals": "17.11.0",
|
|
101
101
|
"jsdom": "30.0.1",
|
|
102
|
-
"mediabunny": "1.
|
|
102
|
+
"mediabunny": "1.55.2",
|
|
103
103
|
"opentype.js": "2.0.0",
|
|
104
104
|
"playcanvas": "2.21.4",
|
|
105
105
|
"prettier": "3.9.6",
|
|
106
|
-
"publint": "0.3.
|
|
107
|
-
"rollup": "4.62.
|
|
106
|
+
"publint": "0.3.24",
|
|
107
|
+
"rollup": "4.62.5",
|
|
108
108
|
"serve": "14.2.6",
|
|
109
109
|
"tslib": "2.8.1",
|
|
110
110
|
"typedoc": "0.28.20",
|
|
111
111
|
"typedoc-plugin-mdn-links": "5.1.1",
|
|
112
112
|
"typescript": "6.0.3",
|
|
113
|
-
"vitest": "4.1.
|
|
113
|
+
"vitest": "4.1.11"
|
|
114
114
|
}
|
|
115
115
|
}
|
package/src/app.ts
CHANGED
|
@@ -66,16 +66,23 @@ import {
|
|
|
66
66
|
|
|
67
67
|
import type { AssetElement } from './asset';
|
|
68
68
|
import { AsyncElement } from './async-element';
|
|
69
|
-
import type { EntityElement } from './entity';
|
|
70
69
|
import type { EntityBaseElement } from './entity-base';
|
|
70
|
+
import type { EntityOwnerElement } from './entity-owner';
|
|
71
71
|
import { LoadingBar } from './loading-bar';
|
|
72
72
|
import type { MaterialElement } from './material';
|
|
73
|
-
import type { ModuleElement } from './module';
|
|
74
73
|
import { parseBool, parseEnum, parseNumber } from './parse';
|
|
74
|
+
import type { WasmElement } from './wasm';
|
|
75
75
|
|
|
76
76
|
/** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
|
|
77
77
|
const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'] as const;
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The event types whose listeners make an element a hover target. Hover resolution walks past
|
|
81
|
+
* elements listening for none of them, so a silent element never swallows an ancestor's
|
|
82
|
+
* enter/leave pair.
|
|
83
|
+
*/
|
|
84
|
+
const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'] as const;
|
|
85
|
+
|
|
79
86
|
/**
|
|
80
87
|
* Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
|
|
81
88
|
* box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
|
|
@@ -163,9 +170,9 @@ class AppElement extends AsyncElement {
|
|
|
163
170
|
|
|
164
171
|
/**
|
|
165
172
|
* The elements backing this application's entities, keyed by the entity itself. Registered
|
|
166
|
-
* by
|
|
167
|
-
*
|
|
168
|
-
* identity - never by name.
|
|
173
|
+
* by entity-owning elements at creation (pc-entity, and pc-model for its host) and by
|
|
174
|
+
* NodeElement at binding, and removed when an entity is destroyed or unbound, this joins
|
|
175
|
+
* engine scene nodes back to their owning elements by identity - never by name.
|
|
169
176
|
*/
|
|
170
177
|
private _entityElements = new Map<GraphNode, EntityBaseElement>();
|
|
171
178
|
|
|
@@ -260,7 +267,7 @@ class AppElement extends AsyncElement {
|
|
|
260
267
|
// there, leaving the element permanently unready: no canvas, no entities, no application.
|
|
261
268
|
//
|
|
262
269
|
// Upgrading is the fix here rather than skipping whatever has not upgraded, because a
|
|
263
|
-
// <pc-
|
|
270
|
+
// <pc-wasm> is the one child that nothing else ever builds on its own behalf - skipping
|
|
264
271
|
// it would drop the wasm module the app asked for, silently and only for cloned apps.
|
|
265
272
|
// Upgrading runs each descendant's connectedCallback synchronously, a few lines earlier
|
|
266
273
|
// than the parser's path runs them but into the same state they see there: no application
|
|
@@ -270,11 +277,11 @@ class AppElement extends AsyncElement {
|
|
|
270
277
|
// is left completely untouched.
|
|
271
278
|
customElements.upgrade(this);
|
|
272
279
|
|
|
273
|
-
// Get all pc-
|
|
274
|
-
const
|
|
280
|
+
// Get all pc-wasm elements that are direct children of the pc-app element
|
|
281
|
+
const wasmElements = this.querySelectorAll<WasmElement>(':scope > pc-wasm');
|
|
275
282
|
|
|
276
283
|
// Wait for all modules to load
|
|
277
|
-
await Promise.all(Array.from(
|
|
284
|
+
await Promise.all(Array.from(wasmElements).map((element) => element._getLoadPromise()));
|
|
278
285
|
|
|
279
286
|
// The element may have been removed while the modules loaded. Nothing beyond the loading
|
|
280
287
|
// bar exists yet, and disconnectedCallback has already destroyed that.
|
|
@@ -462,15 +469,16 @@ class AppElement extends AsyncElement {
|
|
|
462
469
|
materialElement._createMaterial();
|
|
463
470
|
});
|
|
464
471
|
|
|
465
|
-
// Create all entities
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
472
|
+
// Create all entities. pc-model joins the sweep because it owns a host entity of its
|
|
473
|
+
// own; its instantiated content arrives later, beneath that host.
|
|
474
|
+
const ownerElements = this.querySelectorAll<EntityOwnerElement>('pc-entity, pc-model');
|
|
475
|
+
Array.from(ownerElements).forEach((ownerElement) => {
|
|
476
|
+
ownerElement._createEntity(app);
|
|
469
477
|
});
|
|
470
478
|
|
|
471
479
|
// Build hierarchy
|
|
472
|
-
|
|
473
|
-
|
|
480
|
+
ownerElements.forEach((ownerElement) => {
|
|
481
|
+
ownerElement._buildHierarchy(app);
|
|
474
482
|
});
|
|
475
483
|
|
|
476
484
|
// Building the hierarchy dispatched each entity's ready event synchronously, and a
|
|
@@ -602,9 +610,9 @@ class AppElement extends AsyncElement {
|
|
|
602
610
|
// created from onpointer* attributes when their elements were first upgraded, or
|
|
603
611
|
// listeners carried over from before a re-boot)
|
|
604
612
|
pointerEventTypes.forEach((type) => {
|
|
605
|
-
const anyListeners = Array.from(
|
|
606
|
-
(entity
|
|
607
|
-
);
|
|
613
|
+
const anyListeners = Array.from(
|
|
614
|
+
this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node')
|
|
615
|
+
).some((entity) => entity._hasListeners(type));
|
|
608
616
|
if (anyListeners) {
|
|
609
617
|
this._onPointerListenerAdded(type);
|
|
610
618
|
}
|
|
@@ -659,10 +667,10 @@ class AppElement extends AsyncElement {
|
|
|
659
667
|
}
|
|
660
668
|
|
|
661
669
|
/**
|
|
662
|
-
* Returns the `<pc-entity>` or `<pc-node>` element whose backing entity is
|
|
663
|
-
* `null` if the entity is not fronted by an element of this application - for
|
|
664
|
-
* unbound node inside a model's instantiated hierarchy, or an entity created
|
|
665
|
-
* engine API.
|
|
670
|
+
* Returns the `<pc-entity>`, `<pc-model>` or `<pc-node>` element whose backing entity is
|
|
671
|
+
* `entity`, or `null` if the entity is not fronted by an element of this application - for
|
|
672
|
+
* example, an unbound node inside a model's instantiated hierarchy, or an entity created
|
|
673
|
+
* through the engine API.
|
|
666
674
|
*
|
|
667
675
|
* @param entity - The entity to look up.
|
|
668
676
|
* @returns The element fronting the entity, or `null`.
|
|
@@ -672,18 +680,19 @@ class AppElement extends AsyncElement {
|
|
|
672
680
|
}
|
|
673
681
|
|
|
674
682
|
/**
|
|
675
|
-
* Resolves the element that owns a picked node: the nearest node up the parent
|
|
676
|
-
* starting with the node itself -
|
|
677
|
-
*
|
|
678
|
-
*
|
|
683
|
+
* Resolves the element that owns hover for a picked node: the nearest node up the parent
|
|
684
|
+
* chain - starting with the node itself - whose element listens for any of the hover event
|
|
685
|
+
* types. Skipping silent elements matches {@link _elementWithListener}, so a registered
|
|
686
|
+
* element with no hover listeners (a `<pc-model>` host, a plain child entity) is transparent
|
|
687
|
+
* to hover rather than swallowing a listening ancestor's enter/leave pair.
|
|
679
688
|
*
|
|
680
689
|
* @param node - The picked node, or `null`.
|
|
681
|
-
* @returns The owning element, or `null`.
|
|
690
|
+
* @returns The hover-owning element, or `null`.
|
|
682
691
|
*/
|
|
683
|
-
private
|
|
692
|
+
private _hoverTarget(node: GraphNode | null): EntityBaseElement | null {
|
|
684
693
|
while (node !== null) {
|
|
685
694
|
const element = this._entityElements.get(node);
|
|
686
|
-
if (element) {
|
|
695
|
+
if (element && hoverEventTypes.some((type) => element._hasListeners(type))) {
|
|
687
696
|
return element;
|
|
688
697
|
}
|
|
689
698
|
node = node.parent;
|
|
@@ -831,9 +840,10 @@ class AppElement extends AsyncElement {
|
|
|
831
840
|
const node = await this._pickNode(event);
|
|
832
841
|
if (token !== this._pickToken || !this._picker) return;
|
|
833
842
|
|
|
834
|
-
// The hovered element is the nearest one up the node's parent chain
|
|
835
|
-
//
|
|
836
|
-
|
|
843
|
+
// The hovered element is the nearest one up the node's parent chain with a hover
|
|
844
|
+
// listener - the nearest-listener rule down/up use. Dispatch is still gated per event
|
|
845
|
+
// type below: having any hover listener selects the target, each event needs its own.
|
|
846
|
+
const newHoverEntity = this._hoverTarget(node);
|
|
837
847
|
|
|
838
848
|
// Handle enter/leave events
|
|
839
849
|
if (this._hoveredEntity !== newHoverEntity) {
|
|
@@ -898,9 +908,9 @@ class AppElement extends AsyncElement {
|
|
|
898
908
|
}
|
|
899
909
|
|
|
900
910
|
private _onPointerListenerRemoved(type: string) {
|
|
901
|
-
const hasListeners = Array.from(
|
|
902
|
-
|
|
903
|
-
);
|
|
911
|
+
const hasListeners = Array.from(
|
|
912
|
+
this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node')
|
|
913
|
+
).some((entity) => entity._hasListeners(type));
|
|
904
914
|
|
|
905
915
|
if (!hasListeners && this._canvas) {
|
|
906
916
|
this._hasPointerListeners[type] = false;
|
package/src/async-element.ts
CHANGED
|
@@ -33,13 +33,13 @@ class AsyncElement extends HTMLElement {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* The nearest ancestor element that fronts an entity — `<pc-entity
|
|
37
|
-
* `null` if this element has no such ancestor. The search starts at the
|
|
38
|
-
* never resolves to itself.
|
|
36
|
+
* The nearest ancestor element that fronts an entity — `<pc-entity>`, `<pc-model>` or
|
|
37
|
+
* `<pc-node>` — or `null` if this element has no such ancestor. The search starts at the
|
|
38
|
+
* parent, so an element never resolves to itself.
|
|
39
39
|
* @returns The closest entity-fronting element, or `null`.
|
|
40
40
|
*/
|
|
41
41
|
get closestEntity(): EntityBaseElement | null {
|
|
42
|
-
return (this.parentElement?.closest('pc-entity, pc-node') as EntityBaseElement | null) ?? null;
|
|
42
|
+
return (this.parentElement?.closest('pc-entity, pc-model, pc-node') as EntityBaseElement | null) ?? null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
@@ -95,11 +95,11 @@ type AsyncElementTagName = {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Waits for the first element matching the given tag name to be fully initialized. Note that the
|
|
98
|
-
* promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
|
|
99
|
-
* that is not a direct child of `<pc-
|
|
100
|
-
* device — listen for its `error` event instead). A component element outside
|
|
101
|
-
* the exception: it still becomes ready, but its `component` is `null`. Either way, a
|
|
102
|
-
* element logs a warning naming the parent it requires.
|
|
98
|
+
* promise never settles if the element cannot finish initializing (for example, a `<pc-script-instance>`
|
|
99
|
+
* that is not a direct child of `<pc-script>`, or a `<pc-app>` that could not create a graphics
|
|
100
|
+
* device — listen for its `error` event instead). A component element outside an entity-fronting
|
|
101
|
+
* element is the exception: it still becomes ready, but its `component` is `null`. Either way, a
|
|
102
|
+
* misplaced element logs a warning naming the parent it requires.
|
|
103
103
|
* @param target - The tag name of the element to wait for (e.g. `'pc-app'`).
|
|
104
104
|
* @returns A promise that resolves with the element once it's ready.
|
|
105
105
|
* @example
|
|
@@ -120,11 +120,11 @@ function whenReady<K extends AsyncElementTagName>(target: K): Promise<HTMLElemen
|
|
|
120
120
|
function whenReady<T extends AsyncElement>(target: T): Promise<T>;
|
|
121
121
|
/**
|
|
122
122
|
* Waits for the first element matching the given CSS selector to be fully initialized. Note that
|
|
123
|
-
* the promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
|
|
124
|
-
* that is not a direct child of `<pc-
|
|
125
|
-
* device — listen for its `error` event instead). A component element outside
|
|
126
|
-
* the exception: it still becomes ready, but its `component` is `null`. Either way, a
|
|
127
|
-
* element logs a warning naming the parent it requires.
|
|
123
|
+
* the promise never settles if the element cannot finish initializing (for example, a `<pc-script-instance>`
|
|
124
|
+
* that is not a direct child of `<pc-script>`, or a `<pc-app>` that could not create a graphics
|
|
125
|
+
* device — listen for its `error` event instead). A component element outside an entity-fronting
|
|
126
|
+
* element is the exception: it still becomes ready, but its `component` is `null`. Either way, a
|
|
127
|
+
* misplaced element logs a warning naming the parent it requires.
|
|
128
128
|
* @param target - A CSS selector matching the element to wait for (e.g. `'#my-app'`).
|
|
129
129
|
* @returns A promise that resolves with the element once it's ready.
|
|
130
130
|
* @example
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import type { Asset, EventHandle } from 'playcanvas';
|
|
2
|
+
import { AnimTrack } from 'playcanvas';
|
|
3
|
+
|
|
4
|
+
import { AssetElement, useAsset } from '../asset';
|
|
5
|
+
import { AsyncElement } from '../async-element';
|
|
6
|
+
import { ModelElement } from '../model';
|
|
7
|
+
import { parseBool, parseNumber } from '../parse';
|
|
8
|
+
|
|
9
|
+
import type { ContainerWithAnimations } from './anim-component';
|
|
10
|
+
import { AnimComponentElement } from './anim-component';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The AnimClipElement interface provides properties and methods for manipulating
|
|
14
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim-clip/ | `<pc-anim-clip>`}
|
|
15
|
+
* elements. The AnimClipElement interface also inherits the properties and methods of the
|
|
16
|
+
* {@link HTMLElement} interface.
|
|
17
|
+
*
|
|
18
|
+
* A clip declares one named animation on its parent `<pc-anim>`. `name` is both the clip's name
|
|
19
|
+
* and the track looked up in the clip's source: an explicit `asset` (a `container`, an
|
|
20
|
+
* `animation` `.glb`, or an `animclip` JSON), or, without one, the container of the `<pc-model>`
|
|
21
|
+
* enclosing the parent `<pc-anim>`. A source holding a single track supplies it whatever it is
|
|
22
|
+
* named; in a multi-track source the track named `name` is chosen, falling back to the first
|
|
23
|
+
* with a warning. The element becomes ready once its resolved track is assigned.
|
|
24
|
+
*
|
|
25
|
+
* @category Components
|
|
26
|
+
*/
|
|
27
|
+
class AnimClipElement extends AsyncElement {
|
|
28
|
+
/**
|
|
29
|
+
* The `<pc-anim>` this clip was adopted by, captured when the parent adopts the clip and on
|
|
30
|
+
* connection.
|
|
31
|
+
*
|
|
32
|
+
* `disconnectedCallback` cannot rediscover it: by the time the element is disconnected its
|
|
33
|
+
* `parentElement` is already `null`, so a lookup would both fail to find the component and
|
|
34
|
+
* emit a misleading "must be a direct child" warning for what is an ordinary removal.
|
|
35
|
+
*/
|
|
36
|
+
private _animElement: AnimComponentElement | null = null;
|
|
37
|
+
|
|
38
|
+
private _asset = '';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Incremented on every connect and disconnect, and captured by connectedCallback on entry —
|
|
42
|
+
* a resume from an await abandons itself if the value has moved on, so a stale callback can
|
|
43
|
+
* neither act on a torn-down tree nor register its clip alongside a re-inserted element's
|
|
44
|
+
* own callback.
|
|
45
|
+
*/
|
|
46
|
+
private _connectionGeneration = 0;
|
|
47
|
+
|
|
48
|
+
private _errorHandle: EventHandle | null = null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Incremented on every track resolution and on disconnect, and captured by a resolution when
|
|
52
|
+
* it starts. A resolution that resumes from an await or an asset callback abandons itself if
|
|
53
|
+
* the value has moved on, so a superseded resolution cannot hand a stale track to the parent.
|
|
54
|
+
*/
|
|
55
|
+
private _loadGeneration = 0;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The pending asset subscriptions of the current resolution, if it is waiting for its asset.
|
|
59
|
+
* Held so that whatever supersedes the resolution can detach the handlers from the asset,
|
|
60
|
+
* rather than leave them registered until the asset settles (or forever, if it never does).
|
|
61
|
+
*/
|
|
62
|
+
private _loadHandle: EventHandle | null = null;
|
|
63
|
+
|
|
64
|
+
private _loop = true;
|
|
65
|
+
|
|
66
|
+
private _name = '';
|
|
67
|
+
|
|
68
|
+
private _speed = 1;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The source complaint already made — the asset id it was made for, or `''` for the
|
|
72
|
+
* no-asset-no-model case — so re-resolutions (host cycles, model reloads) do not repeat it.
|
|
73
|
+
*/
|
|
74
|
+
private _warnedSource: string | null = null;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the owning `<pc-anim>` already rejected this clip's name — its sweeps re-run on
|
|
78
|
+
* host cycles and must not repeat the complaint.
|
|
79
|
+
*/
|
|
80
|
+
private _warnedInvalid = false;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The clip's resolved track. `null` until resolution completes, during which the owning
|
|
84
|
+
* `<pc-anim>` assigns the engine's placeholder track in its stead.
|
|
85
|
+
*
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
_track: AnimTrack | null = null;
|
|
89
|
+
|
|
90
|
+
async connectedCallback() {
|
|
91
|
+
const generation = ++this._connectionGeneration;
|
|
92
|
+
|
|
93
|
+
const animElement = this.animElement;
|
|
94
|
+
await animElement?.ready();
|
|
95
|
+
|
|
96
|
+
// The element may have been removed (perhaps re-inserted, which runs a callback of its
|
|
97
|
+
// own), or its parent torn down, while we were waiting. A <pc-app> disconnects before
|
|
98
|
+
// its children, so by the time we resume the component can already be gone - see the
|
|
99
|
+
// matching guard in disconnectedCallback below.
|
|
100
|
+
const component = animElement ? animElement.component : null;
|
|
101
|
+
if (generation !== this._connectionGeneration || !animElement || !component) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this._animElement = animElement;
|
|
106
|
+
animElement._registerClip(this);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
disconnectedCallback() {
|
|
110
|
+
// Invalidate any connectedCallback or track resolution still suspended on an await
|
|
111
|
+
this._connectionGeneration++;
|
|
112
|
+
this._loadGeneration++;
|
|
113
|
+
this._detachLoadHandlers();
|
|
114
|
+
|
|
115
|
+
// Uses the cached parent rather than a fresh lookup, since parentElement is already null
|
|
116
|
+
// by now. The component itself is null if the whole <pc-app> is being torn down —
|
|
117
|
+
// parents disconnect first and have already removed the component.
|
|
118
|
+
this._animElement?._unregisterClip(this);
|
|
119
|
+
this._animElement = null;
|
|
120
|
+
this._track = null;
|
|
121
|
+
this._resetReady();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected get animElement(): AnimComponentElement | null {
|
|
125
|
+
const animElement = this.parentElement as AnimComponentElement;
|
|
126
|
+
|
|
127
|
+
if (!(animElement instanceof AnimComponentElement)) {
|
|
128
|
+
const label = this._name ? ` '${this._name}'` : '';
|
|
129
|
+
console.warn(`pc-anim-clip${label} must be a direct child of a pc-anim element`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return animElement;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private _detachLoadHandlers() {
|
|
137
|
+
this._loadHandle?.off();
|
|
138
|
+
this._loadHandle = null;
|
|
139
|
+
this._errorHandle?.off();
|
|
140
|
+
this._errorHandle = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Reports a name-validation failure from the owning `<pc-anim>`, once per name value.
|
|
145
|
+
*
|
|
146
|
+
* @param message - The complaint.
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
149
|
+
_markInvalid(message: string) {
|
|
150
|
+
if (this._warnedInvalid) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this._warnedInvalid = true;
|
|
154
|
+
console.warn(message);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolves the clip's track from its source and hands it to the owning `<pc-anim>`. Called
|
|
159
|
+
* by the parent whenever the clip is (re)adopted, and again when the source changes; a newer
|
|
160
|
+
* resolution supersedes one still in flight. The element becomes ready once the resolved
|
|
161
|
+
* track is assigned.
|
|
162
|
+
*
|
|
163
|
+
* @param animElement - The owning `<pc-anim>`.
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
async _resolveTrack(animElement: AnimComponentElement) {
|
|
167
|
+
this._animElement = animElement;
|
|
168
|
+
|
|
169
|
+
const generation = ++this._loadGeneration;
|
|
170
|
+
this._detachLoadHandlers();
|
|
171
|
+
|
|
172
|
+
if (this._asset) {
|
|
173
|
+
const asset = useAsset(this._asset);
|
|
174
|
+
if (!asset) {
|
|
175
|
+
this._warnSource(`pc-anim-clip '${this._name}' could not find asset '${this._asset}' - clip not assigned`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (asset.loaded) {
|
|
179
|
+
this._extractTrack(asset, `asset '${this._asset}'`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Whichever of load/error fires first detaches the other. The generation is
|
|
183
|
+
// re-checked even though a superseded handler is detached: the detach relies on how
|
|
184
|
+
// the engine's event emitter treats removal, while the check holds on its own.
|
|
185
|
+
this._loadHandle = asset.once('load', () => {
|
|
186
|
+
this._detachLoadHandlers();
|
|
187
|
+
if (generation !== this._loadGeneration) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this._extractTrack(asset, `asset '${this._asset}'`);
|
|
191
|
+
});
|
|
192
|
+
this._errorHandle = asset.once('error', () => {
|
|
193
|
+
this._detachLoadHandlers();
|
|
194
|
+
if (generation !== this._loadGeneration) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
this._warnSource(`pc-anim-clip '${this._name}' - asset '${this._asset}' failed to load - clip not assigned`);
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const model = animElement.parentElement;
|
|
203
|
+
if (!(model instanceof ModelElement)) {
|
|
204
|
+
this._warnSource(`pc-anim-clip '${this._name}' has no asset and no enclosing pc-model - clip not assigned`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await model.ready();
|
|
209
|
+
if (generation !== this._loadGeneration) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const asset = AssetElement.get(model.asset);
|
|
214
|
+
if (!asset?.resource) {
|
|
215
|
+
// The model's load failed; it already reported the error
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this._extractTrack(asset, `model '${model.asset}'`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Complains about the clip's source, once per source value — resolutions re-run on host
|
|
223
|
+
* cycles and model reloads, and must not repeat the complaint.
|
|
224
|
+
*/
|
|
225
|
+
private _warnSource(message: string) {
|
|
226
|
+
if (this._warnedSource === this._asset) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this._warnedSource = this._asset;
|
|
230
|
+
console.warn(message);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Picks the clip's track out of a loaded source asset: the track named `name`, or a lone
|
|
235
|
+
* track whatever it is named, or the first of several with a warning.
|
|
236
|
+
*
|
|
237
|
+
* @param asset - The loaded source asset.
|
|
238
|
+
* @param source - How warnings name the source.
|
|
239
|
+
*/
|
|
240
|
+
private _extractTrack(asset: Asset, source: string) {
|
|
241
|
+
const label = `pc-anim-clip '${this._name}'`;
|
|
242
|
+
|
|
243
|
+
// Widened: the engine registers an 'animclip' handler but omits the type from the
|
|
244
|
+
// Asset.type union
|
|
245
|
+
const type: string = asset.type;
|
|
246
|
+
|
|
247
|
+
let candidates: unknown[];
|
|
248
|
+
switch (type) {
|
|
249
|
+
case 'container':
|
|
250
|
+
candidates = (asset.resource as ContainerWithAnimations).animations.map(
|
|
251
|
+
(animationAsset) => animationAsset.resource
|
|
252
|
+
);
|
|
253
|
+
break;
|
|
254
|
+
case 'animation':
|
|
255
|
+
candidates = asset.resources;
|
|
256
|
+
break;
|
|
257
|
+
case 'animclip':
|
|
258
|
+
candidates = [asset.resource];
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
this._warnSource(`${label} - ${source} has type '${asset.type}', expected 'container', 'animation' or 'animclip' - clip not assigned`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// A JSON 'animation' asset parses to the engine's legacy Animation class, which the anim
|
|
266
|
+
// system rejects - only real AnimTracks qualify
|
|
267
|
+
const tracks = candidates.filter((candidate): candidate is AnimTrack => candidate instanceof AnimTrack);
|
|
268
|
+
if (tracks.length === 0) {
|
|
269
|
+
this._warnSource(`${label} - ${source} contains no usable animation track - clip not assigned`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let track = tracks.find((candidate) => candidate.name === this._name);
|
|
274
|
+
if (!track) {
|
|
275
|
+
track = tracks[0];
|
|
276
|
+
if (tracks.length > 1) {
|
|
277
|
+
console.warn(
|
|
278
|
+
`${label} - no track named '${this._name}' in ${source} - using '${track.name}' (available: ${tracks.map((candidate) => candidate.name).join(', ')})`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this._track = track;
|
|
284
|
+
if (this._animElement?._onClipResolved(this)) {
|
|
285
|
+
this._onReady();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Sets the id of the `pc-asset` supplying the clip's track: a `container`, an `animation`
|
|
291
|
+
* `.glb`, or an `animclip` JSON. When empty, the track comes from the container of the
|
|
292
|
+
* `<pc-model>` enclosing the parent `<pc-anim>`.
|
|
293
|
+
* @param value - The asset id.
|
|
294
|
+
*/
|
|
295
|
+
set asset(value: string) {
|
|
296
|
+
this._asset = value;
|
|
297
|
+
this._warnedSource = null;
|
|
298
|
+
if (this._animElement) {
|
|
299
|
+
this._resetReady();
|
|
300
|
+
this._track = null;
|
|
301
|
+
this._resolveTrack(this._animElement);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Gets the id of the `pc-asset` supplying the clip's track.
|
|
307
|
+
* @returns The asset id.
|
|
308
|
+
*/
|
|
309
|
+
get asset() {
|
|
310
|
+
return this._asset;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Sets whether the clip loops. A non-looping clip holds its last pose when it ends — the
|
|
315
|
+
* engine reports no completion. Defaults to `true`.
|
|
316
|
+
* @param value - Whether the clip loops.
|
|
317
|
+
*/
|
|
318
|
+
set loop(value: boolean) {
|
|
319
|
+
this._loop = value;
|
|
320
|
+
this._animElement?._onClipParamsChanged(this);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Gets whether the clip loops.
|
|
325
|
+
* @returns Whether the clip loops.
|
|
326
|
+
*/
|
|
327
|
+
get loop() {
|
|
328
|
+
return this._loop;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Sets the name of the clip: the name it is played by, and the track looked up in the
|
|
333
|
+
* clip's source. Names must be unique within a `<pc-anim>` and must not contain `.`.
|
|
334
|
+
* @param value - The clip name.
|
|
335
|
+
*/
|
|
336
|
+
set name(value: string) {
|
|
337
|
+
this._name = value;
|
|
338
|
+
this._warnedInvalid = false;
|
|
339
|
+
if (this._animElement) {
|
|
340
|
+
this._resetReady();
|
|
341
|
+
this._animElement._refreshClips();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Gets the name of the clip.
|
|
347
|
+
* @returns The clip name.
|
|
348
|
+
*/
|
|
349
|
+
get name() {
|
|
350
|
+
return this._name;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Sets the playback speed of the clip, where negative values play it backwards. Applies
|
|
355
|
+
* immediately, preserving the playhead. Defaults to 1.
|
|
356
|
+
* @param value - The playback speed.
|
|
357
|
+
*/
|
|
358
|
+
set speed(value: number) {
|
|
359
|
+
this._speed = value;
|
|
360
|
+
this._animElement?._onClipParamsChanged(this);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Gets the playback speed of the clip.
|
|
365
|
+
* @returns The playback speed.
|
|
366
|
+
*/
|
|
367
|
+
get speed() {
|
|
368
|
+
return this._speed;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
static get observedAttributes() {
|
|
372
|
+
return ['asset', 'loop', 'name', 'speed'];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
376
|
+
switch (name) {
|
|
377
|
+
case 'asset':
|
|
378
|
+
this.asset = newValue ?? '';
|
|
379
|
+
break;
|
|
380
|
+
case 'loop':
|
|
381
|
+
this.loop = parseBool(newValue, true);
|
|
382
|
+
break;
|
|
383
|
+
case 'name':
|
|
384
|
+
this.name = newValue ?? '';
|
|
385
|
+
break;
|
|
386
|
+
case 'speed':
|
|
387
|
+
this.speed = parseNumber(newValue, 1, name);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
customElements.define('pc-anim-clip', AnimClipElement);
|
|
394
|
+
|
|
395
|
+
export { AnimClipElement };
|