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