@playcanvas/web-components 0.11.0 → 0.11.1
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 +1 -1
- package/dist/app.d.ts +18 -38
- package/dist/asset.d.ts +8 -7
- package/dist/async-element.d.ts +21 -5
- package/dist/components/button-component.d.ts +3 -7
- package/dist/components/camera-component.d.ts +3 -7
- package/dist/components/collision-component.d.ts +3 -7
- package/dist/components/component.d.ts +22 -4
- package/dist/components/element-component.d.ts +4 -8
- package/dist/components/gsplat-component.d.ts +2 -7
- package/dist/components/layoutchild-component.d.ts +2 -7
- package/dist/components/layoutgroup-component.d.ts +3 -7
- package/dist/components/light-component.d.ts +3 -7
- package/dist/components/listener-component.d.ts +1 -6
- package/dist/components/particlesystem-component.d.ts +2 -7
- package/dist/components/render-component.d.ts +2 -7
- package/dist/components/rigidbody-component.d.ts +3 -7
- package/dist/components/screen-component.d.ts +3 -7
- package/dist/components/script-component.d.ts +8 -20
- package/dist/components/script.d.ts +2 -22
- package/dist/components/scrollbar-component.d.ts +2 -7
- package/dist/components/scrollview-component.d.ts +3 -7
- package/dist/components/sound-component.d.ts +2 -7
- package/dist/components/sound-slot.d.ts +8 -6
- package/dist/custom-elements.json +5049 -10696
- package/dist/entity.d.ts +5 -11
- package/dist/index.d.ts +37 -0
- package/dist/material.d.ts +12 -12
- package/dist/model.d.ts +21 -5
- package/dist/module.d.ts +0 -6
- package/dist/parse.d.ts +2 -1
- package/dist/pwc.cjs +496 -136
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +496 -136
- 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 +496 -136
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.ts +4 -7
- package/dist/sky.d.ts +13 -5
- package/dist/vscode.html-custom-data.json +26 -26
- package/dist/web-types.json +545 -536
- package/package.json +8 -7
- package/src/app.ts +142 -70
- package/src/asset.ts +34 -28
- package/src/async-element.ts +34 -8
- package/src/components/button-component.ts +5 -9
- package/src/components/camera-component.ts +24 -10
- package/src/components/collision-component.ts +26 -15
- package/src/components/component.ts +58 -8
- package/src/components/element-component.ts +26 -30
- package/src/components/gsplat-component.ts +4 -9
- package/src/components/layoutchild-component.ts +4 -9
- package/src/components/layoutgroup-component.ts +14 -9
- package/src/components/light-component.ts +42 -12
- package/src/components/listener-component.ts +1 -7
- package/src/components/particlesystem-component.ts +7 -15
- package/src/components/render-component.ts +5 -10
- package/src/components/rigidbody-component.ts +23 -16
- package/src/components/screen-component.ts +5 -9
- package/src/components/script-component.ts +108 -46
- package/src/components/script.ts +38 -33
- package/src/components/scrollbar-component.ts +6 -16
- package/src/components/scrollview-component.ts +16 -11
- package/src/components/sound-component.ts +10 -15
- package/src/components/sound-slot.ts +30 -20
- package/src/entity.ts +42 -19
- package/src/index.ts +45 -1
- package/src/loading-bar.ts +8 -8
- package/src/material.ts +63 -37
- package/src/model.ts +69 -14
- package/src/module.ts +8 -7
- package/src/parse.ts +62 -17
- package/src/scene.ts +12 -9
- package/src/sky.ts +50 -9
package/dist/pwc.cjs
CHANGED
|
@@ -5,12 +5,14 @@ var playcanvas = require('playcanvas');
|
|
|
5
5
|
/**
|
|
6
6
|
* Base class for all PlayCanvas Web Components that initialize asynchronously.
|
|
7
7
|
*
|
|
8
|
-
* @fires {CustomEvent} ready - Fired
|
|
9
|
-
*
|
|
8
|
+
* @fires {CustomEvent} ready - Fired when the element is fully initialized — once per readiness
|
|
9
|
+
* cycle, so an element that is torn down and re-initialized (for example by removing and
|
|
10
|
+
* re-inserting it) fires it again. Bubbles and is composed.
|
|
10
11
|
*/
|
|
11
12
|
class AsyncElement extends HTMLElement {
|
|
12
13
|
_readyPromise;
|
|
13
14
|
_readyResolve;
|
|
15
|
+
_readyResolved = false;
|
|
14
16
|
/** @ignore */
|
|
15
17
|
constructor() {
|
|
16
18
|
super();
|
|
@@ -37,15 +39,39 @@ class AsyncElement extends HTMLElement {
|
|
|
37
39
|
/**
|
|
38
40
|
* Called when the element is fully initialized and ready. Subclasses should call this when
|
|
39
41
|
* they're ready. Resolves the ready promise and dispatches a bubbling, composed `ready`
|
|
40
|
-
* event.
|
|
42
|
+
* event. Signals at most once per readiness cycle: a repeat call before {@link _resetReady}
|
|
43
|
+
* has re-armed the promise does nothing.
|
|
41
44
|
*/
|
|
42
45
|
_onReady() {
|
|
46
|
+
if (this._readyResolved)
|
|
47
|
+
return;
|
|
48
|
+
this._readyResolved = true;
|
|
43
49
|
this._readyResolve();
|
|
44
50
|
this.dispatchEvent(new CustomEvent('ready', { bubbles: true, composed: true }));
|
|
45
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns the ready promise to its pending state. Subclasses should call this when the
|
|
54
|
+
* resource their readiness announced is torn down (typically from `disconnectedCallback`),
|
|
55
|
+
* so that a later re-initialization can signal readiness again. Does nothing while the
|
|
56
|
+
* promise is still pending — an in-flight waiter carries over to the next readiness cycle
|
|
57
|
+
* rather than being stranded on a promise nothing will ever resolve.
|
|
58
|
+
*/
|
|
59
|
+
_resetReady() {
|
|
60
|
+
if (!this._readyResolved)
|
|
61
|
+
return;
|
|
62
|
+
this._readyResolved = false;
|
|
63
|
+
this._readyPromise = new Promise((resolve) => {
|
|
64
|
+
this._readyResolve = resolve;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
46
67
|
/**
|
|
47
68
|
* Returns a promise that resolves with this element when it's ready. This is the low-level
|
|
48
69
|
* primitive underlying {@link whenReady}, which is the recommended way to wait for elements.
|
|
70
|
+
*
|
|
71
|
+
* Readiness tracks the element's current lifecycle: once a ready element is torn down (for
|
|
72
|
+
* example by removing it from the document), this returns a fresh promise that resolves when
|
|
73
|
+
* the element is next ready. A promise obtained earlier stays resolved — call this again
|
|
74
|
+
* after re-inserting an element rather than reusing a promise from before its removal.
|
|
49
75
|
* @returns A promise that resolves with this element when it's ready.
|
|
50
76
|
*/
|
|
51
77
|
ready() {
|
|
@@ -120,7 +146,14 @@ class ModuleElement extends HTMLElement {
|
|
|
120
146
|
});
|
|
121
147
|
}
|
|
122
148
|
}
|
|
123
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Returns the promise that settles when the module has loaded. Awaited by the containing
|
|
151
|
+
* `<pc-app>` element before it creates its graphics device.
|
|
152
|
+
*
|
|
153
|
+
* @returns The load promise.
|
|
154
|
+
* @internal
|
|
155
|
+
*/
|
|
156
|
+
_getLoadPromise() {
|
|
124
157
|
return this.loadPromise;
|
|
125
158
|
}
|
|
126
159
|
}
|
|
@@ -179,10 +212,7 @@ class LoadingBar {
|
|
|
179
212
|
// aria-valuenow is set, which is what marks a progressbar indeterminate. jsdom has no Web
|
|
180
213
|
// Animations API, so the guard degrades to a static bar there rather than crashing boot.
|
|
181
214
|
if (typeof this._fill.animate === 'function') {
|
|
182
|
-
this._sweep = this._fill.animate([
|
|
183
|
-
{ transform: 'scaleX(0.25) translateX(-100%)' },
|
|
184
|
-
{ transform: 'scaleX(0.25) translateX(500%)' }
|
|
185
|
-
], {
|
|
215
|
+
this._sweep = this._fill.animate([{ transform: 'scaleX(0.25) translateX(-100%)' }, { transform: 'scaleX(0.25) translateX(500%)' }], {
|
|
186
216
|
duration: 1000,
|
|
187
217
|
iterations: Infinity,
|
|
188
218
|
easing: 'ease-in-out'
|
|
@@ -415,7 +445,7 @@ const CSS_COLORS = {
|
|
|
415
445
|
*/
|
|
416
446
|
const parseComponents = (value, count) => {
|
|
417
447
|
const components = value.trim().split(/\s+/).map(Number);
|
|
418
|
-
if (components.length !== count || components.some(component => !Number.isFinite(component))) {
|
|
448
|
+
if (components.length !== count || components.some((component) => !Number.isFinite(component))) {
|
|
419
449
|
return null;
|
|
420
450
|
}
|
|
421
451
|
return components;
|
|
@@ -471,7 +501,10 @@ const parseColor = (value, defaultValue, attribute) => {
|
|
|
471
501
|
if (/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)) {
|
|
472
502
|
let hex = value.slice(1);
|
|
473
503
|
if (hex.length === 3 || hex.length === 4) {
|
|
474
|
-
hex = hex
|
|
504
|
+
hex = hex
|
|
505
|
+
.split('')
|
|
506
|
+
.map((char) => char + char)
|
|
507
|
+
.join('');
|
|
475
508
|
}
|
|
476
509
|
return new playcanvas.Color().fromString(`#${hex}`);
|
|
477
510
|
}
|
|
@@ -568,7 +601,10 @@ const parseTags = (value, defaultValue = []) => {
|
|
|
568
601
|
// caller's default, or a later mutation would write back through it.
|
|
569
602
|
return [...defaultValue];
|
|
570
603
|
}
|
|
571
|
-
return value
|
|
604
|
+
return value
|
|
605
|
+
.split(',')
|
|
606
|
+
.map((tag) => tag.trim())
|
|
607
|
+
.filter((tag) => tag !== '');
|
|
572
608
|
};
|
|
573
609
|
/**
|
|
574
610
|
* Parse a Vec2 attribute value. The expected format is 2 space-separated numbers (e.g. '1 2').
|
|
@@ -660,6 +696,8 @@ const getEntity = (ref) => {
|
|
|
660
696
|
return element?.entity ?? null;
|
|
661
697
|
};
|
|
662
698
|
|
|
699
|
+
/** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
|
|
700
|
+
const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
|
|
663
701
|
/**
|
|
664
702
|
* The AppElement interface provides properties and methods for manipulating
|
|
665
703
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
@@ -690,7 +728,20 @@ class AppElement extends AsyncElement {
|
|
|
690
728
|
*/
|
|
691
729
|
_optionsLocked = false;
|
|
692
730
|
_bar = null;
|
|
731
|
+
/**
|
|
732
|
+
* Whether the application has created its initial entity hierarchy. Read by EntityElement to
|
|
733
|
+
* decide whether a newly connected element must create its entity itself or leave it to the
|
|
734
|
+
* boot sweep.
|
|
735
|
+
* @internal
|
|
736
|
+
*/
|
|
693
737
|
_hierarchyReady = false;
|
|
738
|
+
/**
|
|
739
|
+
* Incremented on every connect and disconnect. Boot captures the value on entry and abandons
|
|
740
|
+
* itself wherever it resumes from an await if the value has moved on — so a boot whose
|
|
741
|
+
* element was removed cannot complete against a torn-down element, and a boot whose element
|
|
742
|
+
* was removed and re-inserted (which starts a boot of its own) cannot race the newer one.
|
|
743
|
+
*/
|
|
744
|
+
_bootGeneration = 0;
|
|
694
745
|
/**
|
|
695
746
|
* The elements backing this application's entities, keyed by the entity itself. Registered
|
|
696
747
|
* by EntityElement at creation and removed when an entity is destroyed, this joins engine
|
|
@@ -743,8 +794,16 @@ class AppElement extends AsyncElement {
|
|
|
743
794
|
super();
|
|
744
795
|
// Bind methods to maintain 'this' context
|
|
745
796
|
this._onWindowResize = this._onWindowResize.bind(this);
|
|
797
|
+
// Track pointer listeners being added to and removed from descendant entities.
|
|
798
|
+
// Registered once here rather than on every boot - the handlers no-op while there is no
|
|
799
|
+
// canvas, and a re-booted element must not stack a second set.
|
|
800
|
+
pointerEventTypes.forEach((type) => {
|
|
801
|
+
this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
|
|
802
|
+
this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
|
|
803
|
+
});
|
|
746
804
|
}
|
|
747
805
|
async connectedCallback() {
|
|
806
|
+
const generation = ++this._bootGeneration;
|
|
748
807
|
// Created before the first await, so the bar is visible while modules and the graphics
|
|
749
808
|
// device are created, and exists before any disconnect could need to clean it up
|
|
750
809
|
if (this._loadingBar && !this._bar) {
|
|
@@ -753,7 +812,12 @@ class AppElement extends AsyncElement {
|
|
|
753
812
|
// Get all pc-module elements that are direct children of the pc-app element
|
|
754
813
|
const moduleElements = this.querySelectorAll(':scope > pc-module');
|
|
755
814
|
// Wait for all modules to load
|
|
756
|
-
await Promise.all(Array.from(moduleElements).map(module => module.
|
|
815
|
+
await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
|
|
816
|
+
// The element may have been removed while the modules loaded. Nothing beyond the loading
|
|
817
|
+
// bar exists yet, and disconnectedCallback has already destroyed that.
|
|
818
|
+
if (generation !== this._bootGeneration) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
757
821
|
// Create and append the canvas to the element
|
|
758
822
|
this._canvas = document.createElement('canvas');
|
|
759
823
|
this.appendChild(this._canvas);
|
|
@@ -773,6 +837,13 @@ class AppElement extends AsyncElement {
|
|
|
773
837
|
deviceTypes: deviceTypes,
|
|
774
838
|
stencil: this._stencilBuffer
|
|
775
839
|
});
|
|
840
|
+
// The element may have been removed while the device was created. disconnectedCallback
|
|
841
|
+
// has already cleaned up the canvas; the device was created inside the await, so it is
|
|
842
|
+
// this boot's to release.
|
|
843
|
+
if (generation !== this._bootGeneration) {
|
|
844
|
+
device.destroy();
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
776
847
|
// Assigned rather than resolved to a number here: the engine caps against the live
|
|
777
848
|
// window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
|
|
778
849
|
// display when a window moves between monitors of differing density.
|
|
@@ -849,27 +920,41 @@ class AppElement extends AsyncElement {
|
|
|
849
920
|
this._pickerCreate();
|
|
850
921
|
// Get all pc-asset elements that are direct children of the pc-app element
|
|
851
922
|
const assetElements = this.querySelectorAll(':scope > pc-asset');
|
|
852
|
-
Array.from(assetElements)
|
|
853
|
-
assetElement.
|
|
923
|
+
for (const assetElement of Array.from(assetElements)) {
|
|
924
|
+
assetElement._createAsset();
|
|
854
925
|
const asset = assetElement.asset;
|
|
855
926
|
if (asset) {
|
|
856
927
|
app.assets.add(asset);
|
|
928
|
+
// Adding a fileless asset (one built purely from data, such as a sprite)
|
|
929
|
+
// completes it synchronously, dispatching the element's load event - whose
|
|
930
|
+
// listeners may have removed this element. Stop before the next addition
|
|
931
|
+
// reaches the destroyed registry, and before orphan entities are created.
|
|
932
|
+
if (generation !== this._bootGeneration) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
857
935
|
}
|
|
858
|
-
}
|
|
936
|
+
}
|
|
859
937
|
// Get all pc-material elements that are direct children of the pc-app element
|
|
860
938
|
const materialElements = this.querySelectorAll(':scope > pc-material');
|
|
861
939
|
Array.from(materialElements).forEach((materialElement) => {
|
|
862
|
-
materialElement.
|
|
940
|
+
materialElement._createMaterial();
|
|
863
941
|
});
|
|
864
942
|
// Create all entities
|
|
865
943
|
const entityElements = this.querySelectorAll('pc-entity');
|
|
866
944
|
Array.from(entityElements).forEach((entityElement) => {
|
|
867
|
-
entityElement.
|
|
945
|
+
entityElement._createEntity(app);
|
|
868
946
|
});
|
|
869
947
|
// Build hierarchy
|
|
870
948
|
entityElements.forEach((entityElement) => {
|
|
871
|
-
entityElement.
|
|
949
|
+
entityElement._buildHierarchy(app);
|
|
872
950
|
});
|
|
951
|
+
// Building the hierarchy dispatched each entity's ready event synchronously, and a
|
|
952
|
+
// listener may have removed the element. The sweep itself degrades safely - destroying
|
|
953
|
+
// the application nulls every element's entity, so the remaining builds no-op - but the
|
|
954
|
+
// teardown's reset must not be overwritten here.
|
|
955
|
+
if (generation !== this._bootGeneration) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
873
958
|
this._hierarchyReady = true;
|
|
874
959
|
// Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
|
|
875
960
|
// listener must be attached before preload() is called: an asset that is already loaded
|
|
@@ -886,8 +971,19 @@ class AppElement extends AsyncElement {
|
|
|
886
971
|
this._loadProgress = total === 0 ? 1 : 0;
|
|
887
972
|
this._bar?.progress(0, total);
|
|
888
973
|
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
|
|
974
|
+
// The progress dispatch above ran listeners synchronously, and one may have removed the
|
|
975
|
+
// element. The application is already destroyed - it must not be asked to preload.
|
|
976
|
+
if (generation !== this._bootGeneration) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
889
979
|
// Load assets before starting the application
|
|
890
980
|
app.preload(() => {
|
|
981
|
+
// The element may have been removed while assets loaded. The application is already
|
|
982
|
+
// destroyed, so it must not be started — and readiness must not be signaled for a
|
|
983
|
+
// boot that no longer owns the element.
|
|
984
|
+
if (generation !== this._bootGeneration) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
891
987
|
// Scope the counter to this preload pass, so a later app.preload() call by user code
|
|
892
988
|
// cannot push `loaded` past `total`
|
|
893
989
|
app.off('preload:progress', onPreloadProgress);
|
|
@@ -903,6 +999,9 @@ class AppElement extends AsyncElement {
|
|
|
903
999
|
});
|
|
904
1000
|
}
|
|
905
1001
|
disconnectedCallback() {
|
|
1002
|
+
// Invalidate any boot still in flight, so it abandons itself when it next resumes
|
|
1003
|
+
// instead of completing against a torn-down element.
|
|
1004
|
+
this._bootGeneration++;
|
|
906
1005
|
this._optionsLocked = false;
|
|
907
1006
|
this._pickerDestroy();
|
|
908
1007
|
// Clean up the application. Destroying it destroys every entity, whose destroy hooks
|
|
@@ -915,6 +1014,11 @@ class AppElement extends AsyncElement {
|
|
|
915
1014
|
this._loadProgress = 0;
|
|
916
1015
|
this._bar?.destroy();
|
|
917
1016
|
this._bar = null;
|
|
1017
|
+
// Return the element to its pre-boot state, so re-inserting it boots afresh: descendants
|
|
1018
|
+
// must neither see a hierarchy that no longer exists nor resume against a readiness that
|
|
1019
|
+
// no longer holds.
|
|
1020
|
+
this._hierarchyReady = false;
|
|
1021
|
+
this._resetReady();
|
|
918
1022
|
// Remove event listeners
|
|
919
1023
|
window.removeEventListener('resize', this._onWindowResize);
|
|
920
1024
|
// Remove the canvas
|
|
@@ -942,14 +1046,11 @@ class AppElement extends AsyncElement {
|
|
|
942
1046
|
this._pointerHandlers.pointermove = listener(this._onPointerMove);
|
|
943
1047
|
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
944
1048
|
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
945
|
-
//
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
// created from onpointer* attributes when their elements were first upgraded)
|
|
951
|
-
const anyListeners = Array.from(this.querySelectorAll('pc-entity'))
|
|
952
|
-
.some(entity => entity.hasListeners(type));
|
|
1049
|
+
// Attach canvas handlers for listeners registered before this boot (e.g. handlers
|
|
1050
|
+
// created from onpointer* attributes when their elements were first upgraded, or
|
|
1051
|
+
// listeners carried over from before a re-boot)
|
|
1052
|
+
pointerEventTypes.forEach((type) => {
|
|
1053
|
+
const anyListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
|
|
953
1054
|
if (anyListeners) {
|
|
954
1055
|
this._onPointerListenerAdded(type);
|
|
955
1056
|
}
|
|
@@ -964,6 +1065,7 @@ class AppElement extends AsyncElement {
|
|
|
964
1065
|
});
|
|
965
1066
|
}
|
|
966
1067
|
this._picker = null;
|
|
1068
|
+
this._hoveredEntity = null;
|
|
967
1069
|
this._pointerHandlers = {
|
|
968
1070
|
pointermove: null,
|
|
969
1071
|
pointerdown: null,
|
|
@@ -983,7 +1085,7 @@ class AppElement extends AsyncElement {
|
|
|
983
1085
|
*
|
|
984
1086
|
* @param entity - The entity.
|
|
985
1087
|
* @param element - The element that created it.
|
|
986
|
-
* @
|
|
1088
|
+
* @internal
|
|
987
1089
|
*/
|
|
988
1090
|
_registerEntityElement(entity, element) {
|
|
989
1091
|
this._entityElements.set(entity, element);
|
|
@@ -992,7 +1094,7 @@ class AppElement extends AsyncElement {
|
|
|
992
1094
|
* Removes the registration for a destroyed entity. Called by EntityElement.
|
|
993
1095
|
*
|
|
994
1096
|
* @param entity - The entity.
|
|
995
|
-
* @
|
|
1097
|
+
* @internal
|
|
996
1098
|
*/
|
|
997
1099
|
_unregisterEntityElement(entity) {
|
|
998
1100
|
this._entityElements.delete(entity);
|
|
@@ -1038,7 +1140,7 @@ class AppElement extends AsyncElement {
|
|
|
1038
1140
|
_elementWithListener(node, type) {
|
|
1039
1141
|
while (node !== null) {
|
|
1040
1142
|
const element = this._entityElements.get(node);
|
|
1041
|
-
if (element?.
|
|
1143
|
+
if (element?._hasListeners(type)) {
|
|
1042
1144
|
return element;
|
|
1043
1145
|
}
|
|
1044
1146
|
node = node.parent;
|
|
@@ -1095,17 +1197,17 @@ class AppElement extends AsyncElement {
|
|
|
1095
1197
|
const newHoverEntity = this._elementFromNode(node);
|
|
1096
1198
|
// Handle enter/leave events
|
|
1097
1199
|
if (this._hoveredEntity !== newHoverEntity) {
|
|
1098
|
-
if (this._hoveredEntity && this._hoveredEntity.
|
|
1200
|
+
if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
|
|
1099
1201
|
this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
|
|
1100
1202
|
}
|
|
1101
|
-
if (newHoverEntity && newHoverEntity.
|
|
1203
|
+
if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
|
|
1102
1204
|
newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
|
|
1103
1205
|
}
|
|
1104
1206
|
}
|
|
1105
1207
|
// Update hover state
|
|
1106
1208
|
this._hoveredEntity = newHoverEntity;
|
|
1107
1209
|
// Handle pointermove event
|
|
1108
|
-
if (newHoverEntity && newHoverEntity.
|
|
1210
|
+
if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
|
|
1109
1211
|
newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
|
|
1110
1212
|
}
|
|
1111
1213
|
}
|
|
@@ -1135,22 +1237,21 @@ class AppElement extends AsyncElement {
|
|
|
1135
1237
|
if (!this._hasPointerListeners[type] && this._canvas) {
|
|
1136
1238
|
this._hasPointerListeners[type] = true;
|
|
1137
1239
|
// For enter/leave events, we need the move handler
|
|
1138
|
-
const handler =
|
|
1139
|
-
this._pointerHandlers.pointermove
|
|
1140
|
-
this._pointerHandlers[type];
|
|
1240
|
+
const handler = type === 'pointerenter' || type === 'pointerleave'
|
|
1241
|
+
? this._pointerHandlers.pointermove
|
|
1242
|
+
: this._pointerHandlers[type];
|
|
1141
1243
|
if (handler) {
|
|
1142
1244
|
this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
|
|
1143
1245
|
}
|
|
1144
1246
|
}
|
|
1145
1247
|
}
|
|
1146
1248
|
_onPointerListenerRemoved(type) {
|
|
1147
|
-
const hasListeners = Array.from(this.querySelectorAll('pc-entity'))
|
|
1148
|
-
.some(entity => entity.hasListeners(type));
|
|
1249
|
+
const hasListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
|
|
1149
1250
|
if (!hasListeners && this._canvas) {
|
|
1150
1251
|
this._hasPointerListeners[type] = false;
|
|
1151
|
-
const handler =
|
|
1152
|
-
this._pointerHandlers.pointermove
|
|
1153
|
-
this._pointerHandlers[type];
|
|
1252
|
+
const handler = type === 'pointerenter' || type === 'pointerleave'
|
|
1253
|
+
? this._pointerHandlers.pointermove
|
|
1254
|
+
: this._pointerHandlers[type];
|
|
1154
1255
|
if (handler) {
|
|
1155
1256
|
this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
|
|
1156
1257
|
}
|
|
@@ -1231,14 +1332,6 @@ class AppElement extends AsyncElement {
|
|
|
1231
1332
|
get depthBuffer() {
|
|
1232
1333
|
return this._depthBuffer;
|
|
1233
1334
|
}
|
|
1234
|
-
/**
|
|
1235
|
-
* Gets the hierarchy ready flag.
|
|
1236
|
-
* @returns The hierarchy ready flag.
|
|
1237
|
-
* @ignore
|
|
1238
|
-
*/
|
|
1239
|
-
get hierarchyReady() {
|
|
1240
|
-
return this._hierarchyReady;
|
|
1241
|
-
}
|
|
1242
1335
|
/**
|
|
1243
1336
|
* Sets whether the application shows its built-in loading bar while it boots and preloads its
|
|
1244
1337
|
* assets. Enabled by default; setting `false` removes the bar immediately, while setting
|
|
@@ -1407,7 +1500,14 @@ class EntityElement extends AsyncElement {
|
|
|
1407
1500
|
get entity() {
|
|
1408
1501
|
return this._entity;
|
|
1409
1502
|
}
|
|
1410
|
-
|
|
1503
|
+
/**
|
|
1504
|
+
* Creates the backing entity. Called by the containing `<pc-app>` element during its boot
|
|
1505
|
+
* sweep, and on connection for elements inserted while the application is already running.
|
|
1506
|
+
*
|
|
1507
|
+
* @param app - The application to create the entity in.
|
|
1508
|
+
* @internal
|
|
1509
|
+
*/
|
|
1510
|
+
_createEntity(app) {
|
|
1411
1511
|
// Guard against double creation. When a subtree is inserted at runtime (e.g. cloning a
|
|
1412
1512
|
// `<template>`), an ancestor's connectedCallback eagerly creates descendant entities; the
|
|
1413
1513
|
// descendants' own connectedCallbacks would otherwise create them a second time.
|
|
@@ -1438,8 +1538,10 @@ class EntityElement extends AsyncElement {
|
|
|
1438
1538
|
}
|
|
1439
1539
|
/**
|
|
1440
1540
|
* Handles the destruction of the backing entity. Resets the element so a later re-insertion
|
|
1441
|
-
* starts clean: `_built` must be cleared alongside `_entity`, or
|
|
1442
|
-
* and a re-created entity would never be parented.
|
|
1541
|
+
* starts clean: `_built` must be cleared alongside `_entity`, or _buildHierarchy would bail
|
|
1542
|
+
* and a re-created entity would never be parented. Readiness is re-armed for the same
|
|
1543
|
+
* reason — with the entity gone, a resolved ready promise would resume its awaiters against
|
|
1544
|
+
* a null `entity`.
|
|
1443
1545
|
*
|
|
1444
1546
|
* @param entity - The entity that was destroyed.
|
|
1445
1547
|
*/
|
|
@@ -1448,8 +1550,18 @@ class EntityElement extends AsyncElement {
|
|
|
1448
1550
|
this._appElement = null;
|
|
1449
1551
|
this._entity = null;
|
|
1450
1552
|
this._built = false;
|
|
1553
|
+
this._resetReady();
|
|
1451
1554
|
}
|
|
1452
|
-
|
|
1555
|
+
/**
|
|
1556
|
+
* Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` when
|
|
1557
|
+
* there is one, and under the application root otherwise. Called by the containing `<pc-app>`
|
|
1558
|
+
* element once a sweep has created every entity, so a parent's existence never depends on
|
|
1559
|
+
* document order.
|
|
1560
|
+
*
|
|
1561
|
+
* @param app - The application whose root adopts parentless entities.
|
|
1562
|
+
* @internal
|
|
1563
|
+
*/
|
|
1564
|
+
_buildHierarchy(app) {
|
|
1453
1565
|
if (!this.entity || this._built)
|
|
1454
1566
|
return;
|
|
1455
1567
|
this._built = true;
|
|
@@ -1475,17 +1587,17 @@ class EntityElement extends AsyncElement {
|
|
|
1475
1587
|
return;
|
|
1476
1588
|
}
|
|
1477
1589
|
// If app is already running, create entity immediately
|
|
1478
|
-
if (closestApp.
|
|
1590
|
+
if (closestApp._hierarchyReady) {
|
|
1479
1591
|
const app = closestApp.app;
|
|
1480
|
-
this.
|
|
1481
|
-
this.
|
|
1592
|
+
this._createEntity(app);
|
|
1593
|
+
this._buildHierarchy(app);
|
|
1482
1594
|
// Handle any child entities that might exist
|
|
1483
1595
|
const childEntities = this.querySelectorAll('pc-entity');
|
|
1484
1596
|
childEntities.forEach((child) => {
|
|
1485
|
-
child.
|
|
1597
|
+
child._createEntity(app);
|
|
1486
1598
|
});
|
|
1487
1599
|
childEntities.forEach((child) => {
|
|
1488
|
-
child.
|
|
1600
|
+
child._buildHierarchy(app);
|
|
1489
1601
|
});
|
|
1490
1602
|
}
|
|
1491
1603
|
}
|
|
@@ -1679,14 +1791,23 @@ class EntityElement extends AsyncElement {
|
|
|
1679
1791
|
}
|
|
1680
1792
|
removeEventListener(type, listener, options) {
|
|
1681
1793
|
if (this._listeners[type]) {
|
|
1682
|
-
this._listeners[type] = this._listeners[type].filter(l => l !== listener);
|
|
1794
|
+
this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
|
|
1683
1795
|
}
|
|
1684
1796
|
super.removeEventListener(type, listener, options);
|
|
1685
1797
|
if (type.startsWith('pointer')) {
|
|
1686
1798
|
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
1687
1799
|
}
|
|
1688
1800
|
}
|
|
1689
|
-
|
|
1801
|
+
/**
|
|
1802
|
+
* Whether the element has a listener for an event type, registered either with
|
|
1803
|
+
* {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
|
|
1804
|
+
* containing `<pc-app>` element to gate pointer event synthesis.
|
|
1805
|
+
*
|
|
1806
|
+
* @param type - The event type.
|
|
1807
|
+
* @returns Whether a listener is registered.
|
|
1808
|
+
* @internal
|
|
1809
|
+
*/
|
|
1810
|
+
_hasListeners(type) {
|
|
1690
1811
|
return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
|
|
1691
1812
|
}
|
|
1692
1813
|
}
|
|
@@ -1977,7 +2098,7 @@ class AssetElement extends AsyncElement {
|
|
|
1977
2098
|
const app = appElement.app;
|
|
1978
2099
|
if (!app)
|
|
1979
2100
|
return; // pc-app is re-connecting; its own boot will create this asset
|
|
1980
|
-
this.
|
|
2101
|
+
this._createAsset();
|
|
1981
2102
|
if (this.asset) {
|
|
1982
2103
|
app.assets.add(this.asset); // add() auto-loads when preload is true
|
|
1983
2104
|
if (!this.lazy) {
|
|
@@ -1985,13 +2106,15 @@ class AssetElement extends AsyncElement {
|
|
|
1985
2106
|
}
|
|
1986
2107
|
}
|
|
1987
2108
|
}
|
|
1988
|
-
// Never ready if
|
|
2109
|
+
// Never ready if _createAsset failed (unsupported asset type)
|
|
1989
2110
|
if (this.asset) {
|
|
1990
2111
|
this._onReady();
|
|
1991
2112
|
}
|
|
1992
2113
|
}
|
|
1993
2114
|
disconnectedCallback() {
|
|
1994
|
-
this.
|
|
2115
|
+
this._destroyAsset();
|
|
2116
|
+
// Re-arm readiness so a re-inserted element announces the asset it creates then
|
|
2117
|
+
this._resetReady();
|
|
1995
2118
|
}
|
|
1996
2119
|
_onAssetLoad() {
|
|
1997
2120
|
this.dispatchEvent(new Event('load'));
|
|
@@ -2001,7 +2124,14 @@ class AssetElement extends AsyncElement {
|
|
|
2001
2124
|
message: err instanceof Error ? err.message : String(err)
|
|
2002
2125
|
}));
|
|
2003
2126
|
}
|
|
2004
|
-
|
|
2127
|
+
/**
|
|
2128
|
+
* Creates the asset from the element's attributes. Called by the containing `<pc-app>`
|
|
2129
|
+
* element during its boot sweep, and on connection for elements inserted while the
|
|
2130
|
+
* application is already running.
|
|
2131
|
+
*
|
|
2132
|
+
* @internal
|
|
2133
|
+
*/
|
|
2134
|
+
_createAsset() {
|
|
2005
2135
|
const id = this.getAttribute('id') || '';
|
|
2006
2136
|
const src = this.getAttribute('src') || '';
|
|
2007
2137
|
let type = this.getAttribute('type');
|
|
@@ -2091,7 +2221,7 @@ class AssetElement extends AsyncElement {
|
|
|
2091
2221
|
}
|
|
2092
2222
|
return data;
|
|
2093
2223
|
}
|
|
2094
|
-
|
|
2224
|
+
_destroyAsset() {
|
|
2095
2225
|
if (this.asset) {
|
|
2096
2226
|
// A caller that keeps the Asset alive must not dispatch on a removed element
|
|
2097
2227
|
this.asset.off('load', this._onAssetLoad, this);
|
|
@@ -2119,6 +2249,13 @@ class AssetElement extends AsyncElement {
|
|
|
2119
2249
|
get lazy() {
|
|
2120
2250
|
return this._lazy;
|
|
2121
2251
|
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Returns the {@link Asset} created by the `<pc-asset>` element with the given `id`, or
|
|
2254
|
+
* `undefined` if there is no such element or its asset has not been created yet.
|
|
2255
|
+
*
|
|
2256
|
+
* @param id - The `id` of the `<pc-asset>` element.
|
|
2257
|
+
* @returns The asset, or `undefined`.
|
|
2258
|
+
*/
|
|
2122
2259
|
static get(id) {
|
|
2123
2260
|
const assetElement = document.querySelector(`pc-asset[id="${id}"]`);
|
|
2124
2261
|
return assetElement?.asset;
|
|
@@ -2144,6 +2281,14 @@ class ComponentElement extends AsyncElement {
|
|
|
2144
2281
|
_enabled = true;
|
|
2145
2282
|
_component = null;
|
|
2146
2283
|
_appElement = null;
|
|
2284
|
+
/**
|
|
2285
|
+
* Incremented on every connect and disconnect. connectedCallback captures the value on entry
|
|
2286
|
+
* and abandons itself wherever it resumes from an await if the value has moved on — so a
|
|
2287
|
+
* callback whose element was removed cannot act on a torn-down tree, and one whose element
|
|
2288
|
+
* was removed and re-inserted (which runs a callback of its own) cannot add the component a
|
|
2289
|
+
* second time.
|
|
2290
|
+
*/
|
|
2291
|
+
_connectionGeneration = 0;
|
|
2147
2292
|
/**
|
|
2148
2293
|
* Creates a new ComponentElement instance.
|
|
2149
2294
|
*
|
|
@@ -2154,11 +2299,17 @@ class ComponentElement extends AsyncElement {
|
|
|
2154
2299
|
super();
|
|
2155
2300
|
this._componentName = componentName;
|
|
2156
2301
|
}
|
|
2157
|
-
|
|
2302
|
+
/**
|
|
2303
|
+
* Returns the data the component is created with. Overridden by subclasses to supply the
|
|
2304
|
+
* initial values of their cached properties.
|
|
2305
|
+
*
|
|
2306
|
+
* @returns The initial component data.
|
|
2307
|
+
*/
|
|
2158
2308
|
getInitialComponentData() {
|
|
2159
2309
|
return {};
|
|
2160
2310
|
}
|
|
2161
|
-
async
|
|
2311
|
+
async _addComponent() {
|
|
2312
|
+
const generation = this._connectionGeneration;
|
|
2162
2313
|
const entityElement = this.closestEntity;
|
|
2163
2314
|
if (!entityElement) {
|
|
2164
2315
|
// A component can only exist on an entity, so an element placed outside one is inert.
|
|
@@ -2168,19 +2319,42 @@ class ComponentElement extends AsyncElement {
|
|
|
2168
2319
|
return;
|
|
2169
2320
|
}
|
|
2170
2321
|
await entityElement.ready();
|
|
2322
|
+
// The element may have been removed, or removed and re-inserted, while the entity became
|
|
2323
|
+
// ready — the component belongs to the connection that owns the current generation.
|
|
2324
|
+
if (generation !== this._connectionGeneration) {
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2171
2327
|
// Add the component to the entity
|
|
2172
2328
|
const data = this.getInitialComponentData();
|
|
2173
2329
|
this._component = entityElement.entity.addComponent(this._componentName, data);
|
|
2174
2330
|
}
|
|
2175
|
-
|
|
2331
|
+
/**
|
|
2332
|
+
* Configures the newly added component. Overridden by subclasses whose setup goes beyond
|
|
2333
|
+
* the initial data — child-element handling, asset resolution and the like.
|
|
2334
|
+
*/
|
|
2335
|
+
initComponent() {
|
|
2336
|
+
// optional hook
|
|
2337
|
+
}
|
|
2176
2338
|
async connectedCallback() {
|
|
2339
|
+
const generation = ++this._connectionGeneration;
|
|
2177
2340
|
this._appElement = this.closestApp ?? null;
|
|
2178
2341
|
await this._appElement?.ready();
|
|
2179
|
-
|
|
2342
|
+
// The element may have been removed, or removed and re-inserted, while the application
|
|
2343
|
+
// became ready. A re-insertion runs a connectedCallback of its own, so a stale resume
|
|
2344
|
+
// must not add the component alongside it.
|
|
2345
|
+
if (generation !== this._connectionGeneration) {
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
await this._addComponent();
|
|
2349
|
+
if (generation !== this._connectionGeneration) {
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2180
2352
|
this.initComponent();
|
|
2181
2353
|
this._onReady();
|
|
2182
2354
|
}
|
|
2183
2355
|
disconnectedCallback() {
|
|
2356
|
+
// Invalidate any connectedCallback still suspended on an await
|
|
2357
|
+
this._connectionGeneration++;
|
|
2184
2358
|
// Remove the component when the element is disconnected. Skip this when the owning
|
|
2185
2359
|
// application has already been destroyed — removing a <pc-app> disconnects it before
|
|
2186
2360
|
// its children, taking the component systems with it.
|
|
@@ -2189,6 +2363,7 @@ class ComponentElement extends AsyncElement {
|
|
|
2189
2363
|
}
|
|
2190
2364
|
this._component = null;
|
|
2191
2365
|
this._appElement = null;
|
|
2366
|
+
this._resetReady();
|
|
2192
2367
|
}
|
|
2193
2368
|
/**
|
|
2194
2369
|
* The PlayCanvas component instance. `null` until the element is ready, and also for an
|
|
@@ -3246,7 +3421,17 @@ class CollisionComponentElement extends ComponentElement {
|
|
|
3246
3421
|
return this._type;
|
|
3247
3422
|
}
|
|
3248
3423
|
static get observedAttributes() {
|
|
3249
|
-
return [
|
|
3424
|
+
return [
|
|
3425
|
+
...super.observedAttributes,
|
|
3426
|
+
'angular-offset',
|
|
3427
|
+
'axis',
|
|
3428
|
+
'convex-hull',
|
|
3429
|
+
'half-extents',
|
|
3430
|
+
'height',
|
|
3431
|
+
'linear-offset',
|
|
3432
|
+
'radius',
|
|
3433
|
+
'type'
|
|
3434
|
+
];
|
|
3250
3435
|
}
|
|
3251
3436
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
3252
3437
|
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
@@ -4951,7 +5136,7 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
4951
5136
|
}
|
|
4952
5137
|
// Set all the config properties on the component
|
|
4953
5138
|
for (const key in resource) {
|
|
4954
|
-
if (
|
|
5139
|
+
if (Object.hasOwn(resource, key)) {
|
|
4955
5140
|
this.component[key] = resource[key];
|
|
4956
5141
|
}
|
|
4957
5142
|
}
|
|
@@ -5024,10 +5209,7 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
5024
5209
|
}
|
|
5025
5210
|
}
|
|
5026
5211
|
static get observedAttributes() {
|
|
5027
|
-
return [
|
|
5028
|
-
...super.observedAttributes,
|
|
5029
|
-
'asset'
|
|
5030
|
-
];
|
|
5212
|
+
return [...super.observedAttributes, 'asset'];
|
|
5031
5213
|
}
|
|
5032
5214
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
5033
5215
|
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
@@ -5187,7 +5369,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5187
5369
|
_twoSidedLighting = false;
|
|
5188
5370
|
_useFog = true;
|
|
5189
5371
|
_useLighting = true;
|
|
5190
|
-
// Diverges from the engine default of false - see the class docblock and
|
|
5372
|
+
// Diverges from the engine default of false - see the class docblock and _createMaterial()
|
|
5191
5373
|
_useMetalness = true;
|
|
5192
5374
|
_useMetalnessSpecularColor = false;
|
|
5193
5375
|
_useSkybox = true;
|
|
@@ -5200,6 +5382,10 @@ class MaterialElement extends HTMLElement {
|
|
|
5200
5382
|
_mapHandles = new Map();
|
|
5201
5383
|
_updateScheduled = false;
|
|
5202
5384
|
_glossConflictWarned = false;
|
|
5385
|
+
/**
|
|
5386
|
+
* The material. `null` until the containing application has created it — an element present
|
|
5387
|
+
* at startup has its material once the application is ready.
|
|
5388
|
+
*/
|
|
5203
5389
|
material = null;
|
|
5204
5390
|
async connectedCallback() {
|
|
5205
5391
|
const appElement = this.parentElement?.closest('pc-app') ?? null;
|
|
@@ -5217,10 +5403,17 @@ class MaterialElement extends HTMLElement {
|
|
|
5217
5403
|
if (!this.material) {
|
|
5218
5404
|
if (!appElement.app)
|
|
5219
5405
|
return; // pc-app is re-connecting; its own boot will create this
|
|
5220
|
-
this.
|
|
5406
|
+
this._createMaterial();
|
|
5221
5407
|
}
|
|
5222
5408
|
}
|
|
5223
|
-
|
|
5409
|
+
/**
|
|
5410
|
+
* Creates the material from the element's cached properties. Called by the containing
|
|
5411
|
+
* `<pc-app>` element during its boot sweep, and on connection for elements inserted while
|
|
5412
|
+
* the application is already running.
|
|
5413
|
+
*
|
|
5414
|
+
* @internal
|
|
5415
|
+
*/
|
|
5416
|
+
_createMaterial() {
|
|
5224
5417
|
const material = new playcanvas.StandardMaterial();
|
|
5225
5418
|
this.material = material;
|
|
5226
5419
|
material.alphaTest = this._alphaTest;
|
|
@@ -5347,9 +5540,9 @@ class MaterialElement extends HTMLElement {
|
|
|
5347
5540
|
* warning latches and reports once per episode, clearing when the clash is resolved.
|
|
5348
5541
|
*/
|
|
5349
5542
|
_warnGlossConflict() {
|
|
5350
|
-
const quote = (names) => `'${names.join('
|
|
5351
|
-
const roughness = roughnessAliases.filter(name => this.hasAttribute(name));
|
|
5352
|
-
const gloss = glossConflicts.filter(name => this.hasAttribute(name));
|
|
5543
|
+
const quote = (names) => `'${names.join("', '")}'`;
|
|
5544
|
+
const roughness = roughnessAliases.filter((name) => this.hasAttribute(name));
|
|
5545
|
+
const gloss = glossConflicts.filter((name) => this.hasAttribute(name));
|
|
5353
5546
|
if (roughness.length === 0 || gloss.length === 0) {
|
|
5354
5547
|
this._glossConflictWarned = false;
|
|
5355
5548
|
return;
|
|
@@ -5367,7 +5560,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5367
5560
|
* @param id - The id of the `pc-asset`, or an empty string to clear the slot.
|
|
5368
5561
|
* @param slot - The material property to write.
|
|
5369
5562
|
*/
|
|
5370
|
-
|
|
5563
|
+
_setMap(id, slot) {
|
|
5371
5564
|
// Drop any load still pending for this slot - its texture is no longer the one we want
|
|
5372
5565
|
this._mapHandles.get(slot)?.off();
|
|
5373
5566
|
this._mapHandles.delete(slot);
|
|
@@ -5461,7 +5654,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5461
5654
|
*/
|
|
5462
5655
|
set aoMap(value) {
|
|
5463
5656
|
this._aoMap = value;
|
|
5464
|
-
this.
|
|
5657
|
+
this._setMap(value, 'aoMap');
|
|
5465
5658
|
}
|
|
5466
5659
|
/**
|
|
5467
5660
|
* Gets the id of the `pc-asset` used as the ambient occlusion map.
|
|
@@ -5693,7 +5886,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5693
5886
|
*/
|
|
5694
5887
|
set diffuseMap(value) {
|
|
5695
5888
|
this._diffuseMap = value;
|
|
5696
|
-
this.
|
|
5889
|
+
this._setMap(value, 'diffuseMap');
|
|
5697
5890
|
}
|
|
5698
5891
|
/**
|
|
5699
5892
|
* Gets the id of the `pc-asset` used as the diffuse map.
|
|
@@ -5834,7 +6027,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5834
6027
|
*/
|
|
5835
6028
|
set emissiveMap(value) {
|
|
5836
6029
|
this._emissiveMap = value;
|
|
5837
|
-
this.
|
|
6030
|
+
this._setMap(value, 'emissiveMap');
|
|
5838
6031
|
}
|
|
5839
6032
|
/**
|
|
5840
6033
|
* Gets the id of the `pc-asset` used as the emissive map.
|
|
@@ -6012,7 +6205,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6012
6205
|
*/
|
|
6013
6206
|
set glossMap(value) {
|
|
6014
6207
|
this._glossMap = value;
|
|
6015
|
-
this.
|
|
6208
|
+
this._setMap(value, 'glossMap');
|
|
6016
6209
|
}
|
|
6017
6210
|
/**
|
|
6018
6211
|
* Gets the id of the `pc-asset` used as the gloss map.
|
|
@@ -6117,7 +6310,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6117
6310
|
*/
|
|
6118
6311
|
set heightMap(value) {
|
|
6119
6312
|
this._heightMap = value;
|
|
6120
|
-
this.
|
|
6313
|
+
this._setMap(value, 'heightMap');
|
|
6121
6314
|
}
|
|
6122
6315
|
/**
|
|
6123
6316
|
* Gets the id of the `pc-asset` used as the height map.
|
|
@@ -6258,7 +6451,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6258
6451
|
*/
|
|
6259
6452
|
set metalnessMap(value) {
|
|
6260
6453
|
this._metalnessMap = value;
|
|
6261
|
-
this.
|
|
6454
|
+
this._setMap(value, 'metalnessMap');
|
|
6262
6455
|
}
|
|
6263
6456
|
/**
|
|
6264
6457
|
* Gets the id of the `pc-asset` used as the metalness map.
|
|
@@ -6363,7 +6556,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6363
6556
|
*/
|
|
6364
6557
|
set normalMap(value) {
|
|
6365
6558
|
this._normalMap = value;
|
|
6366
|
-
this.
|
|
6559
|
+
this._setMap(value, 'normalMap');
|
|
6367
6560
|
}
|
|
6368
6561
|
/**
|
|
6369
6562
|
* Gets the id of the `pc-asset` used as the normal map.
|
|
@@ -6451,7 +6644,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6451
6644
|
set occludeDirect(value) {
|
|
6452
6645
|
this._occludeDirect = value;
|
|
6453
6646
|
if (this.material) {
|
|
6454
|
-
// @ts-ignore see
|
|
6647
|
+
// @ts-ignore see _createMaterial() - the engine mistypes occludeDirect as a number
|
|
6455
6648
|
this.material.occludeDirect = value;
|
|
6456
6649
|
this._scheduleUpdate();
|
|
6457
6650
|
}
|
|
@@ -6543,7 +6736,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6543
6736
|
*/
|
|
6544
6737
|
set opacityMap(value) {
|
|
6545
6738
|
this._opacityMap = value;
|
|
6546
|
-
this.
|
|
6739
|
+
this._setMap(value, 'opacityMap');
|
|
6547
6740
|
}
|
|
6548
6741
|
/**
|
|
6549
6742
|
* Gets the id of the `pc-asset` used as the opacity map.
|
|
@@ -6861,6 +7054,13 @@ class MaterialElement extends HTMLElement {
|
|
|
6861
7054
|
get useTonemap() {
|
|
6862
7055
|
return this._useTonemap;
|
|
6863
7056
|
}
|
|
7057
|
+
/**
|
|
7058
|
+
* Returns the {@link StandardMaterial} created by the `<pc-material>` element with the given
|
|
7059
|
+
* `id`, or `undefined` if there is no such element or its material has not been created yet.
|
|
7060
|
+
*
|
|
7061
|
+
* @param id - The `id` of the `<pc-material>` element.
|
|
7062
|
+
* @returns The material, or `undefined`.
|
|
7063
|
+
*/
|
|
6864
7064
|
static get(id) {
|
|
6865
7065
|
const materialElement = document.querySelector(`pc-material[id="${id}"]`);
|
|
6866
7066
|
return materialElement?.material;
|
|
@@ -7499,7 +7699,18 @@ class RigidBodyComponentElement extends ComponentElement {
|
|
|
7499
7699
|
return this._type;
|
|
7500
7700
|
}
|
|
7501
7701
|
static get observedAttributes() {
|
|
7502
|
-
return [
|
|
7702
|
+
return [
|
|
7703
|
+
...super.observedAttributes,
|
|
7704
|
+
'angular-damping',
|
|
7705
|
+
'angular-factor',
|
|
7706
|
+
'friction',
|
|
7707
|
+
'linear-damping',
|
|
7708
|
+
'linear-factor',
|
|
7709
|
+
'mass',
|
|
7710
|
+
'restitution',
|
|
7711
|
+
'rolling-friction',
|
|
7712
|
+
'type'
|
|
7713
|
+
];
|
|
7503
7714
|
}
|
|
7504
7715
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
7505
7716
|
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
@@ -7804,13 +8015,7 @@ class ScrollbarComponentElement extends ComponentElement {
|
|
|
7804
8015
|
return this._handle;
|
|
7805
8016
|
}
|
|
7806
8017
|
static get observedAttributes() {
|
|
7807
|
-
return [
|
|
7808
|
-
...super.observedAttributes,
|
|
7809
|
-
'orientation',
|
|
7810
|
-
'value',
|
|
7811
|
-
'handle-size',
|
|
7812
|
-
'handle'
|
|
7813
|
-
];
|
|
8018
|
+
return [...super.observedAttributes, 'orientation', 'value', 'handle-size', 'handle'];
|
|
7814
8019
|
}
|
|
7815
8020
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
7816
8021
|
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
@@ -8036,7 +8241,8 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
8036
8241
|
set horizontalScrollbarVisibility(value) {
|
|
8037
8242
|
this._horizontalScrollbarVisibility = value;
|
|
8038
8243
|
if (this.component) {
|
|
8039
|
-
this.component.horizontalScrollbarVisibility =
|
|
8244
|
+
this.component.horizontalScrollbarVisibility =
|
|
8245
|
+
visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
|
|
8040
8246
|
}
|
|
8041
8247
|
}
|
|
8042
8248
|
/**
|
|
@@ -8054,7 +8260,8 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
8054
8260
|
set verticalScrollbarVisibility(value) {
|
|
8055
8261
|
this._verticalScrollbarVisibility = value;
|
|
8056
8262
|
if (this.component) {
|
|
8057
|
-
this.component.verticalScrollbarVisibility =
|
|
8263
|
+
this.component.verticalScrollbarVisibility =
|
|
8264
|
+
visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
|
|
8058
8265
|
}
|
|
8059
8266
|
}
|
|
8060
8267
|
/**
|
|
@@ -8241,15 +8448,9 @@ customElements.define('pc-scrollview', ScrollViewComponentElement);
|
|
|
8241
8448
|
class ScriptElement extends AsyncElement {
|
|
8242
8449
|
_attributes = {};
|
|
8243
8450
|
_enabled = true;
|
|
8244
|
-
/**
|
|
8245
|
-
* Whether readiness has been signalled. Creation can happen more than once over an
|
|
8246
|
-
* element's life (a runtime `name` change recreates the instance), but `ready` is a
|
|
8247
|
-
* one-shot signal, so only the first successful creation fires it.
|
|
8248
|
-
*/
|
|
8249
|
-
_readySignalled = false;
|
|
8250
8451
|
/**
|
|
8251
8452
|
* The Script instance created for this element by its parent `<pc-scripts>` element.
|
|
8252
|
-
* @
|
|
8453
|
+
* @internal
|
|
8253
8454
|
*/
|
|
8254
8455
|
_script = null;
|
|
8255
8456
|
/**
|
|
@@ -8331,14 +8532,20 @@ class ScriptElement extends AsyncElement {
|
|
|
8331
8532
|
console.warn(`pc-script '${this.getAttribute('name')}' must be a direct child of pc-scripts - script not created`);
|
|
8332
8533
|
}
|
|
8333
8534
|
}
|
|
8535
|
+
disconnectedCallback() {
|
|
8536
|
+
// Re-arm readiness so a re-inserted element announces the instance created for it then.
|
|
8537
|
+
// `_script` is deliberately NOT cleared here: the parent's mutation observer processes
|
|
8538
|
+
// this removal afterwards and reads it to establish which engine script this element
|
|
8539
|
+
// owned - the parent is what clears it.
|
|
8540
|
+
this._resetReady();
|
|
8541
|
+
}
|
|
8334
8542
|
/**
|
|
8335
8543
|
* Called by the parent `<pc-scripts>` element when the script instance has been created.
|
|
8336
|
-
*
|
|
8544
|
+
* Creation can happen more than once per connection (a runtime `name` change recreates the
|
|
8545
|
+
* instance), but `_onReady` signals readiness at most once per cycle.
|
|
8546
|
+
* @internal
|
|
8337
8547
|
*/
|
|
8338
8548
|
_onScriptCreated() {
|
|
8339
|
-
if (this._readySignalled)
|
|
8340
|
-
return;
|
|
8341
|
-
this._readySignalled = true;
|
|
8342
8549
|
this._onReady();
|
|
8343
8550
|
}
|
|
8344
8551
|
static get observedAttributes() {
|
|
@@ -8383,10 +8590,34 @@ customElements.define('pc-script', ScriptElement);
|
|
|
8383
8590
|
*/
|
|
8384
8591
|
const RESERVED_ATTRIBUTES = new Set([
|
|
8385
8592
|
...ScriptElement.observedAttributes,
|
|
8386
|
-
'accesskey',
|
|
8387
|
-
'
|
|
8388
|
-
'
|
|
8389
|
-
'
|
|
8593
|
+
'accesskey',
|
|
8594
|
+
'autocapitalize',
|
|
8595
|
+
'autofocus',
|
|
8596
|
+
'class',
|
|
8597
|
+
'contenteditable',
|
|
8598
|
+
'dir',
|
|
8599
|
+
'draggable',
|
|
8600
|
+
'exportparts',
|
|
8601
|
+
'hidden',
|
|
8602
|
+
'id',
|
|
8603
|
+
'inert',
|
|
8604
|
+
'is',
|
|
8605
|
+
'itemid',
|
|
8606
|
+
'itemprop',
|
|
8607
|
+
'itemref',
|
|
8608
|
+
'itemscope',
|
|
8609
|
+
'itemtype',
|
|
8610
|
+
'lang',
|
|
8611
|
+
'nonce',
|
|
8612
|
+
'part',
|
|
8613
|
+
'popover',
|
|
8614
|
+
'role',
|
|
8615
|
+
'slot',
|
|
8616
|
+
'spellcheck',
|
|
8617
|
+
'style',
|
|
8618
|
+
'tabindex',
|
|
8619
|
+
'title',
|
|
8620
|
+
'translate'
|
|
8390
8621
|
]);
|
|
8391
8622
|
/**
|
|
8392
8623
|
* Checks whether a `pc-script` attribute name is reserved (and so never maps to a script
|
|
@@ -8398,18 +8629,25 @@ const RESERVED_ATTRIBUTES = new Set([
|
|
|
8398
8629
|
* @returns Whether the attribute name is reserved.
|
|
8399
8630
|
*/
|
|
8400
8631
|
const isReservedAttribute = (name) => {
|
|
8401
|
-
return RESERVED_ATTRIBUTES.has(name) ||
|
|
8632
|
+
return (RESERVED_ATTRIBUTES.has(name) ||
|
|
8402
8633
|
name.startsWith('data-') ||
|
|
8403
8634
|
name.startsWith('aria-') ||
|
|
8404
8635
|
name.startsWith('_') ||
|
|
8405
|
-
(name.startsWith('on') && name in HTMLElement.prototype);
|
|
8636
|
+
(name.startsWith('on') && name in HTMLElement.prototype));
|
|
8406
8637
|
};
|
|
8407
8638
|
/**
|
|
8408
8639
|
* Script API members that per-property attributes must never overwrite: the engine bindings and
|
|
8409
8640
|
* the (optional, so possibly undefined) lifecycle methods.
|
|
8410
8641
|
*/
|
|
8411
8642
|
const SCRIPT_API_MEMBERS = new Set([
|
|
8412
|
-
'app',
|
|
8643
|
+
'app',
|
|
8644
|
+
'entity',
|
|
8645
|
+
'destroy',
|
|
8646
|
+
'initialize',
|
|
8647
|
+
'postInitialize',
|
|
8648
|
+
'postUpdate',
|
|
8649
|
+
'swap',
|
|
8650
|
+
'update'
|
|
8413
8651
|
]);
|
|
8414
8652
|
/**
|
|
8415
8653
|
* Converts a kebab-case attribute name to the camelCase script attribute name.
|
|
@@ -8425,7 +8663,7 @@ const kebabToCamel = (name) => {
|
|
|
8425
8663
|
* @returns The kebab-case name.
|
|
8426
8664
|
*/
|
|
8427
8665
|
const camelToKebab = (name) => {
|
|
8428
|
-
return name.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
|
|
8666
|
+
return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
|
8429
8667
|
};
|
|
8430
8668
|
/**
|
|
8431
8669
|
* Resolves an `asset:` prefix to the Asset created by the `pc-asset` element with that id.
|
|
@@ -8624,7 +8862,10 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
8624
8862
|
// Only recurse into plain objects. Class instances (Vec3, Color, Asset, Entity...)
|
|
8625
8863
|
// are leaf values assigned whole, so accessor-typed script attributes receive them
|
|
8626
8864
|
// through their setters instead of having a getter's returned copy mutated.
|
|
8627
|
-
if (value &&
|
|
8865
|
+
if (value &&
|
|
8866
|
+
typeof value === 'object' &&
|
|
8867
|
+
!Array.isArray(value) &&
|
|
8868
|
+
Object.getPrototypeOf(value) === Object.prototype) {
|
|
8628
8869
|
if (!current || typeof current !== 'object') {
|
|
8629
8870
|
target[key] = {};
|
|
8630
8871
|
}
|
|
@@ -8642,7 +8883,11 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
8642
8883
|
* @returns Whether the value is a math type.
|
|
8643
8884
|
*/
|
|
8644
8885
|
isMathType(value) {
|
|
8645
|
-
return value instanceof playcanvas.Vec2 ||
|
|
8886
|
+
return (value instanceof playcanvas.Vec2 ||
|
|
8887
|
+
value instanceof playcanvas.Vec3 ||
|
|
8888
|
+
value instanceof playcanvas.Vec4 ||
|
|
8889
|
+
value instanceof playcanvas.Color ||
|
|
8890
|
+
value instanceof playcanvas.Quat);
|
|
8646
8891
|
}
|
|
8647
8892
|
/**
|
|
8648
8893
|
* Converts a plain numeric array to the math type of `current`. A 3-element array targeting
|
|
@@ -8655,7 +8900,7 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
8655
8900
|
* @returns The converted value, or `null`.
|
|
8656
8901
|
*/
|
|
8657
8902
|
arrayToMathType(current, value, key) {
|
|
8658
|
-
if (value.every(component => typeof component === 'number' && Number.isFinite(component))) {
|
|
8903
|
+
if (value.every((component) => typeof component === 'number' && Number.isFinite(component))) {
|
|
8659
8904
|
if (current instanceof playcanvas.Vec2 && value.length === 2)
|
|
8660
8905
|
return new playcanvas.Vec2(value);
|
|
8661
8906
|
if (current instanceof playcanvas.Vec3 && value.length === 3)
|
|
@@ -8927,7 +9172,10 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
8927
9172
|
mutation.removedNodes.forEach((node) => {
|
|
8928
9173
|
if (node instanceof ScriptElement) {
|
|
8929
9174
|
const scriptName = node.getAttribute('name');
|
|
8930
|
-
if (scriptName &&
|
|
9175
|
+
if (scriptName &&
|
|
9176
|
+
node._script &&
|
|
9177
|
+
this.component &&
|
|
9178
|
+
this.component.get(scriptName) === node._script) {
|
|
8931
9179
|
this.destroyScript(scriptName);
|
|
8932
9180
|
}
|
|
8933
9181
|
node._script = null;
|
|
@@ -9176,18 +9424,27 @@ class SoundSlotElement extends AsyncElement {
|
|
|
9176
9424
|
* emit a misleading "must be a direct child" warning for what is an ordinary removal.
|
|
9177
9425
|
*/
|
|
9178
9426
|
_soundElement = null;
|
|
9427
|
+
/**
|
|
9428
|
+
* Incremented on every connect and disconnect, and captured by connectedCallback on entry —
|
|
9429
|
+
* a resume from an await abandons itself if the value has moved on, so a stale callback can
|
|
9430
|
+
* neither act on a torn-down tree nor add its slot alongside a re-inserted element's own
|
|
9431
|
+
* callback.
|
|
9432
|
+
*/
|
|
9433
|
+
_connectionGeneration = 0;
|
|
9179
9434
|
/**
|
|
9180
9435
|
* The sound slot.
|
|
9181
9436
|
*/
|
|
9182
9437
|
soundSlot = null;
|
|
9183
9438
|
async connectedCallback() {
|
|
9439
|
+
const generation = ++this._connectionGeneration;
|
|
9184
9440
|
const soundElement = this.soundElement;
|
|
9185
9441
|
await soundElement?.ready();
|
|
9186
|
-
// The element may have been removed
|
|
9187
|
-
//
|
|
9188
|
-
// already be gone - see the
|
|
9442
|
+
// The element may have been removed (perhaps re-inserted, which runs a callback of its
|
|
9443
|
+
// own), or its parent torn down, while we were waiting. A <pc-app> disconnects before
|
|
9444
|
+
// its children, so by the time we resume the component can already be gone - see the
|
|
9445
|
+
// matching guard in disconnectedCallback below.
|
|
9189
9446
|
const component = soundElement?.component;
|
|
9190
|
-
if (
|
|
9447
|
+
if (generation !== this._connectionGeneration || !component) {
|
|
9191
9448
|
return;
|
|
9192
9449
|
}
|
|
9193
9450
|
const options = {
|
|
@@ -9210,12 +9467,15 @@ class SoundSlotElement extends AsyncElement {
|
|
|
9210
9467
|
this._onReady();
|
|
9211
9468
|
}
|
|
9212
9469
|
disconnectedCallback() {
|
|
9470
|
+
// Invalidate any connectedCallback still suspended on an await
|
|
9471
|
+
this._connectionGeneration++;
|
|
9213
9472
|
// Uses the cached parent rather than a fresh lookup, since parentElement is already null
|
|
9214
9473
|
// by now. The component itself is null if the parent <pc-sound> (or the whole <pc-app>) is
|
|
9215
9474
|
// being torn down — parents disconnect first and have already removed the component.
|
|
9216
9475
|
this._soundElement?.component?.removeSlot(this._name);
|
|
9217
9476
|
this._soundElement = null;
|
|
9218
9477
|
this.soundSlot = null;
|
|
9478
|
+
this._resetReady();
|
|
9219
9479
|
}
|
|
9220
9480
|
get soundElement() {
|
|
9221
9481
|
const soundElement = this.parentElement;
|
|
@@ -9615,39 +9875,85 @@ customElements.define('pc-gsplat', GSplatComponentElement);
|
|
|
9615
9875
|
class ModelElement extends AsyncElement {
|
|
9616
9876
|
_asset = '';
|
|
9617
9877
|
_entity = null;
|
|
9878
|
+
/**
|
|
9879
|
+
* Incremented on every new load and on disconnect, and captured by a load when it starts. A
|
|
9880
|
+
* load that resumes from an await or a load callback abandons itself if the value has moved
|
|
9881
|
+
* on, so a superseded load can neither instantiate a second entity nor parent one that has
|
|
9882
|
+
* since been destroyed.
|
|
9883
|
+
*/
|
|
9884
|
+
_loadGeneration = 0;
|
|
9885
|
+
/**
|
|
9886
|
+
* The pending asset-load subscription of the current load, if it is waiting for its asset.
|
|
9887
|
+
* Held so that whatever supersedes the load can detach the handler from the asset, rather
|
|
9888
|
+
* than leave it registered until the asset loads (or forever, if it never does).
|
|
9889
|
+
*/
|
|
9890
|
+
_loadHandle = null;
|
|
9891
|
+
/**
|
|
9892
|
+
* The root entity of the instantiated model. `null` until the container asset has loaded
|
|
9893
|
+
* and been instantiated, and again once the element has been removed from the document.
|
|
9894
|
+
* @returns The model's root entity, or `null`.
|
|
9895
|
+
*/
|
|
9896
|
+
get entity() {
|
|
9897
|
+
return this._entity;
|
|
9898
|
+
}
|
|
9618
9899
|
connectedCallback() {
|
|
9619
9900
|
this._loadModel();
|
|
9620
9901
|
this._onReady();
|
|
9621
9902
|
}
|
|
9622
9903
|
disconnectedCallback() {
|
|
9904
|
+
this._loadGeneration++;
|
|
9905
|
+
this._detachLoadHandler();
|
|
9623
9906
|
this._unloadModel();
|
|
9907
|
+
this._resetReady();
|
|
9908
|
+
}
|
|
9909
|
+
_detachLoadHandler() {
|
|
9910
|
+
this._loadHandle?.off();
|
|
9911
|
+
this._loadHandle = null;
|
|
9624
9912
|
}
|
|
9625
9913
|
_instantiate(container) {
|
|
9626
|
-
|
|
9914
|
+
const generation = this._loadGeneration;
|
|
9915
|
+
const entity = container.instantiateRenderEntity();
|
|
9916
|
+
this._entity = entity;
|
|
9627
9917
|
// @ts-ignore
|
|
9628
9918
|
if (container.animations.length > 0) {
|
|
9629
|
-
|
|
9919
|
+
entity.addComponent('anim');
|
|
9630
9920
|
// @ts-ignore
|
|
9631
|
-
|
|
9921
|
+
entity.anim.assignAnimation('animation', container.animations[0].resource);
|
|
9632
9922
|
}
|
|
9923
|
+
// The parent's readiness re-arms when it is torn down, so these can resume in a later
|
|
9924
|
+
// connection cycle. The entity is captured above and the generation re-checked, so a
|
|
9925
|
+
// stale resume cannot parent an entity a newer cycle has already destroyed.
|
|
9633
9926
|
const parentEntityElement = this.closestEntity;
|
|
9634
9927
|
if (parentEntityElement) {
|
|
9635
9928
|
parentEntityElement.ready().then(() => {
|
|
9636
|
-
|
|
9929
|
+
if (generation !== this._loadGeneration) {
|
|
9930
|
+
return;
|
|
9931
|
+
}
|
|
9932
|
+
parentEntityElement.entity.addChild(entity);
|
|
9637
9933
|
});
|
|
9638
9934
|
}
|
|
9639
9935
|
else {
|
|
9640
9936
|
const appElement = this.closestApp;
|
|
9641
9937
|
if (appElement) {
|
|
9642
9938
|
appElement.ready().then(() => {
|
|
9643
|
-
|
|
9939
|
+
if (generation !== this._loadGeneration) {
|
|
9940
|
+
return;
|
|
9941
|
+
}
|
|
9942
|
+
appElement.app.root.addChild(entity);
|
|
9644
9943
|
});
|
|
9645
9944
|
}
|
|
9646
9945
|
}
|
|
9647
9946
|
}
|
|
9648
9947
|
async _loadModel() {
|
|
9649
9948
|
this._unloadModel();
|
|
9949
|
+
// Supersede any load already in flight - only the newest load may instantiate
|
|
9950
|
+
const generation = ++this._loadGeneration;
|
|
9951
|
+
this._detachLoadHandler();
|
|
9650
9952
|
const appElement = await this.closestApp?.ready();
|
|
9953
|
+
// The element may have been removed, or another load started, while we waited
|
|
9954
|
+
if (generation !== this._loadGeneration) {
|
|
9955
|
+
return;
|
|
9956
|
+
}
|
|
9651
9957
|
const app = appElement?.app;
|
|
9652
9958
|
const asset = AssetElement.get(this._asset);
|
|
9653
9959
|
if (!asset) {
|
|
@@ -9657,7 +9963,14 @@ class ModelElement extends AsyncElement {
|
|
|
9657
9963
|
this._instantiate(asset.resource);
|
|
9658
9964
|
}
|
|
9659
9965
|
else {
|
|
9660
|
-
|
|
9966
|
+
// The generation is re-checked even though a superseded handler is detached: the
|
|
9967
|
+
// detach relies on how the engine's event emitter treats removal, while the check
|
|
9968
|
+
// holds on its own.
|
|
9969
|
+
this._loadHandle = asset.once('load', () => {
|
|
9970
|
+
this._loadHandle = null;
|
|
9971
|
+
if (generation !== this._loadGeneration) {
|
|
9972
|
+
return;
|
|
9973
|
+
}
|
|
9661
9974
|
this._instantiate(asset.resource);
|
|
9662
9975
|
});
|
|
9663
9976
|
app.assets.load(asset);
|
|
@@ -9759,10 +10072,17 @@ class SceneElement extends AsyncElement {
|
|
|
9759
10072
|
return;
|
|
9760
10073
|
}
|
|
9761
10074
|
this._scene = app.scene;
|
|
9762
|
-
this.
|
|
10075
|
+
this._updateSceneSettings();
|
|
9763
10076
|
this._onReady();
|
|
9764
10077
|
}
|
|
9765
|
-
|
|
10078
|
+
disconnectedCallback() {
|
|
10079
|
+
// The scene belongs to the application, and removing this element - or the <pc-app>
|
|
10080
|
+
// above it, which disconnects first - parts the two. Re-arm readiness so a re-inserted
|
|
10081
|
+
// element announces the scene it acquires then, not the one it lost here.
|
|
10082
|
+
this._scene = null;
|
|
10083
|
+
this._resetReady();
|
|
10084
|
+
}
|
|
10085
|
+
_updateSceneSettings() {
|
|
9766
10086
|
if (this._scene) {
|
|
9767
10087
|
this._scene.fog.type = this._fog;
|
|
9768
10088
|
this._scene.fog.color = this._fogColor;
|
|
@@ -9930,13 +10250,32 @@ class SkyElement extends AsyncElement {
|
|
|
9930
10250
|
_type = 'infinite';
|
|
9931
10251
|
_scene = null;
|
|
9932
10252
|
_appElement = null;
|
|
10253
|
+
/**
|
|
10254
|
+
* Incremented on every new load and on disconnect, and captured by a load when it starts. A
|
|
10255
|
+
* load that resumes from an await or a load callback abandons itself if the value has moved
|
|
10256
|
+
* on, so a superseded load cannot generate a skybox for a scene it no longer configures.
|
|
10257
|
+
*/
|
|
10258
|
+
_loadGeneration = 0;
|
|
10259
|
+
/**
|
|
10260
|
+
* The pending asset-load subscription of the current load, if it is waiting for its asset.
|
|
10261
|
+
* Held so that whatever supersedes the load can detach the handler from the asset, rather
|
|
10262
|
+
* than leave it registered until the asset loads (or forever, if it never does).
|
|
10263
|
+
*/
|
|
10264
|
+
_loadHandle = null;
|
|
9933
10265
|
connectedCallback() {
|
|
9934
10266
|
this._loadSkybox();
|
|
9935
10267
|
this._onReady();
|
|
9936
10268
|
}
|
|
9937
10269
|
disconnectedCallback() {
|
|
10270
|
+
this._loadGeneration++;
|
|
10271
|
+
this._detachLoadHandler();
|
|
9938
10272
|
this._unloadSkybox();
|
|
9939
10273
|
this._appElement = null;
|
|
10274
|
+
this._resetReady();
|
|
10275
|
+
}
|
|
10276
|
+
_detachLoadHandler() {
|
|
10277
|
+
this._loadHandle?.off();
|
|
10278
|
+
this._loadHandle = null;
|
|
9940
10279
|
}
|
|
9941
10280
|
_generateSkybox(asset) {
|
|
9942
10281
|
if (!this._scene)
|
|
@@ -9944,10 +10283,17 @@ class SkyElement extends AsyncElement {
|
|
|
9944
10283
|
const source = asset.resource;
|
|
9945
10284
|
const skybox = playcanvas.EnvLighting.generateSkyboxCubemap(source);
|
|
9946
10285
|
skybox.anisotropy = 4;
|
|
10286
|
+
// This element owns what it generated (see _unloadSkybox) - replacing a skybox from an
|
|
10287
|
+
// earlier load must release it, not orphan it on the GPU
|
|
10288
|
+
this._scene.skybox?.destroy();
|
|
9947
10289
|
this._scene.skybox = skybox;
|
|
9948
10290
|
if (this._lighting) {
|
|
9949
10291
|
const lighting = playcanvas.EnvLighting.generateLightingSource(source);
|
|
9950
10292
|
const envAtlas = playcanvas.EnvLighting.generateAtlas(lighting);
|
|
10293
|
+
// The lighting source is an intermediate: the atlas is rendered from it and it is
|
|
10294
|
+
// not needed afterwards
|
|
10295
|
+
lighting.destroy();
|
|
10296
|
+
this._scene.envAtlas?.destroy();
|
|
9951
10297
|
this._scene.envAtlas = envAtlas;
|
|
9952
10298
|
}
|
|
9953
10299
|
const layer = this._scene.layers.getLayerById(playcanvas.LAYERID_SKYBOX);
|
|
@@ -9961,7 +10307,14 @@ class SkyElement extends AsyncElement {
|
|
|
9961
10307
|
this._scene.skyboxMip = this._mipLevel;
|
|
9962
10308
|
}
|
|
9963
10309
|
async _loadSkybox() {
|
|
10310
|
+
// Supersede any load already in flight - only the newest load may generate the skybox
|
|
10311
|
+
const generation = ++this._loadGeneration;
|
|
10312
|
+
this._detachLoadHandler();
|
|
9964
10313
|
const appElement = await this.closestApp?.ready();
|
|
10314
|
+
// The element may have been removed, or another load started, while we waited
|
|
10315
|
+
if (generation !== this._loadGeneration) {
|
|
10316
|
+
return;
|
|
10317
|
+
}
|
|
9965
10318
|
const app = appElement?.app;
|
|
9966
10319
|
if (!appElement || !app) {
|
|
9967
10320
|
return;
|
|
@@ -9976,7 +10329,14 @@ class SkyElement extends AsyncElement {
|
|
|
9976
10329
|
this._generateSkybox(asset);
|
|
9977
10330
|
}
|
|
9978
10331
|
else {
|
|
9979
|
-
|
|
10332
|
+
// The generation is re-checked even though a superseded handler is detached: the
|
|
10333
|
+
// detach relies on how the engine's event emitter treats removal, while the check
|
|
10334
|
+
// holds on its own.
|
|
10335
|
+
this._loadHandle = asset.once('load', () => {
|
|
10336
|
+
this._loadHandle = null;
|
|
10337
|
+
if (generation !== this._loadGeneration) {
|
|
10338
|
+
return;
|
|
10339
|
+
}
|
|
9980
10340
|
this._generateSkybox(asset);
|
|
9981
10341
|
});
|
|
9982
10342
|
app.assets.load(asset);
|