@playcanvas/web-components 0.10.1 → 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 +102 -51
- 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 +16 -20
- 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 +28 -11
- 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 +9 -11
- package/dist/components/sound-component.d.ts +2 -7
- package/dist/components/sound-slot.d.ts +8 -6
- package/dist/custom-elements.json +5191 -10695
- package/dist/entity.d.ts +16 -9
- 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 +6 -3
- package/dist/pwc.cjs +799 -287
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +799 -287
- 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 +800 -288
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.ts +4 -7
- package/dist/sky.d.ts +25 -16
- package/dist/vscode.html-custom-data.json +65 -45
- package/dist/web-types.json +585 -557
- package/package.json +8 -7
- package/src/app.ts +326 -144
- 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 +55 -36
- 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 +46 -20
- 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 +22 -15
- package/src/components/sound-component.ts +10 -15
- package/src/components/sound-slot.ts +30 -20
- package/src/entity.ts +75 -34
- 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 +67 -20
- package/src/scene.ts +12 -9
- package/src/sky.ts +76 -34
package/dist/pwc.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, createGraphicsDevice, AppOptions, Keyboard, Mouse, ElementInput, AnimComponentSystem, AnimationComponentSystem, AudioListenerComponentSystem, ButtonComponentSystem, CameraComponentSystem, CollisionComponentSystem, ElementComponentSystem, GSplatComponentSystem, JointComponentSystem, LayoutChildComponentSystem, LayoutGroupComponentSystem, LightComponentSystem, ModelComponentSystem, ParticleSystemComponentSystem, RenderComponentSystem, RigidBodyComponentSystem, ScreenComponentSystem, ScriptComponentSystem, ScrollbarComponentSystem, ScrollViewComponentSystem, SoundComponentSystem, SpriteComponentSystem, ZoneComponentSystem, AnimClipHandler, AnimationHandler, AnimStateGraphHandler, AudioHandler, BinaryHandler, CssHandler, ContainerHandler, CubemapHandler, FolderHandler, FontHandler, GSplatHandler, HierarchyHandler, HtmlHandler, JsonHandler, MaterialHandler, ModelHandler, RenderHandler, ScriptHandler, SceneHandler, ShaderHandler, SpriteHandler, TemplateHandler, TextHandler, TextureAtlasHandler, TextureHandler, SoundManager, Lightmapper, BatchManager, XrManager, AppBase, FILLMODE_FILL_WINDOW, RESOLUTION_AUTO, Picker, MeshInstance, Entity, Asset, SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, BUTTON_TRANSITION_MODE_TINT, BUTTON_TRANSITION_MODE_SPRITE_CHANGE,
|
|
1
|
+
import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, createGraphicsDevice, AppOptions, Keyboard, Mouse, ElementInput, AnimComponentSystem, AnimationComponentSystem, AudioListenerComponentSystem, ButtonComponentSystem, CameraComponentSystem, CollisionComponentSystem, ElementComponentSystem, GSplatComponentSystem, JointComponentSystem, LayoutChildComponentSystem, LayoutGroupComponentSystem, LightComponentSystem, ModelComponentSystem, ParticleSystemComponentSystem, RenderComponentSystem, RigidBodyComponentSystem, ScreenComponentSystem, ScriptComponentSystem, ScrollbarComponentSystem, ScrollViewComponentSystem, SoundComponentSystem, SpriteComponentSystem, ZoneComponentSystem, AnimClipHandler, AnimationHandler, AnimStateGraphHandler, AudioHandler, BinaryHandler, CssHandler, ContainerHandler, CubemapHandler, FolderHandler, FontHandler, GSplatHandler, HierarchyHandler, HtmlHandler, JsonHandler, MaterialHandler, ModelHandler, RenderHandler, ScriptHandler, SceneHandler, ShaderHandler, SpriteHandler, TemplateHandler, TextHandler, TextureAtlasHandler, TextureHandler, SoundManager, Lightmapper, BatchManager, XrManager, AppBase, FILLMODE_FILL_WINDOW, RESOLUTION_AUTO, Picker, MeshInstance, Entity, Asset, SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, BUTTON_TRANSITION_MODE_TINT, BUTTON_TRANSITION_MODE_SPRITE_CHANGE, TONEMAP_NONE, PROJECTION_PERSPECTIVE, GAMMA_SRGB, GAMMA_NONE, XRTYPE_VR, TONEMAP_LINEAR, TONEMAP_FILMIC, TONEMAP_HEJL, TONEMAP_ACES, TONEMAP_ACES2, TONEMAP_NEUTRAL, PROJECTION_ORTHOGRAPHIC, ORIENTATION_HORIZONTAL, FITTING_NONE, FITTING_STRETCH, FITTING_SHRINK, FITTING_BOTH, ORIENTATION_VERTICAL, SHADOW_PCF3_32F, SHADOW_PCF1_16F, SHADOW_PCF1_32F, SHADOW_PCF3_16F, SHADOW_PCF5_16F, SHADOW_PCF5_32F, SHADOW_VSM_16F, SHADOW_VSM_32F, SHADOW_PCSS_32F, StandardMaterial, BLEND_NONE, CULLFACE_BACK, FRESNEL_SCHLICK, SPECOCC_AO, BLEND_NORMAL, BLEND_ADDITIVE, BLEND_ADDITIVEALPHA, BLEND_PREMULTIPLIED, BLEND_MULTIPLICATIVE, BLEND_MULTIPLICATIVE2X, BLEND_SCREEN, BLEND_MIN, BLEND_MAX, BLEND_SUBTRACTIVE, CULLFACE_NONE, CULLFACE_FRONT, CULLFACE_FRONTANDBACK, FRESNEL_NONE, SPECOCC_NONE, SPECOCC_GLOSSDEPENDENT, SCALEMODE_NONE, SCALEMODE_BLEND, SCROLL_MODE_BOUNCE, SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED, SCROLLBAR_VISIBILITY_SHOW_ALWAYS, SCROLL_MODE_CLAMP, SCROLL_MODE_INFINITE, EnvLighting, LAYERID_SKYBOX } from 'playcanvas';
|
|
2
2
|
|
|
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
|
}
|
|
@@ -487,7 +520,9 @@ const parseColor = (value, defaultValue, attribute) => {
|
|
|
487
520
|
* the value is invalid — the latter also logs a warning listing the valid names.
|
|
488
521
|
*
|
|
489
522
|
* @param value - The attribute value to parse (`null` when the attribute is absent).
|
|
490
|
-
* @param valid - The valid names: an array, or a map whose keys are the valid names.
|
|
523
|
+
* @param valid - The valid names: an array, or a map whose keys are the valid names. Only the keys
|
|
524
|
+
* are read, so the map's value type is unconstrained - engine enums are mostly numeric constants,
|
|
525
|
+
* but some (e.g. `SCALEMODE_BLEND`) are strings.
|
|
491
526
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
492
527
|
* @param attribute - The attribute name, used in the warning message.
|
|
493
528
|
* @returns The resolved enum name.
|
|
@@ -564,7 +599,10 @@ const parseTags = (value, defaultValue = []) => {
|
|
|
564
599
|
// caller's default, or a later mutation would write back through it.
|
|
565
600
|
return [...defaultValue];
|
|
566
601
|
}
|
|
567
|
-
return value
|
|
602
|
+
return value
|
|
603
|
+
.split(',')
|
|
604
|
+
.map((tag) => tag.trim())
|
|
605
|
+
.filter((tag) => tag !== '');
|
|
568
606
|
};
|
|
569
607
|
/**
|
|
570
608
|
* Parse a Vec2 attribute value. The expected format is 2 space-separated numbers (e.g. '1 2').
|
|
@@ -656,6 +694,8 @@ const getEntity = (ref) => {
|
|
|
656
694
|
return element?.entity ?? null;
|
|
657
695
|
};
|
|
658
696
|
|
|
697
|
+
/** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
|
|
698
|
+
const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
|
|
659
699
|
/**
|
|
660
700
|
* The AppElement interface provides properties and methods for manipulating
|
|
661
701
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
@@ -675,12 +715,37 @@ class AppElement extends AsyncElement {
|
|
|
675
715
|
_alpha = true;
|
|
676
716
|
_backend = 'webgpu';
|
|
677
717
|
_antialias = true;
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
718
|
+
_depthBuffer = true;
|
|
719
|
+
_stencilBuffer = true;
|
|
720
|
+
_maxPixelRatio = Infinity;
|
|
681
721
|
_loadingBar = true;
|
|
722
|
+
/**
|
|
723
|
+
* Set once the graphics options above have been handed to `createGraphicsDevice`, after which
|
|
724
|
+
* writing any of them changes nothing. Guards the warning in {@link _warnIfBooted}, and is
|
|
725
|
+
* cleared on disconnect so a re-connected element boots from its current attributes.
|
|
726
|
+
*/
|
|
727
|
+
_optionsLocked = false;
|
|
682
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
|
+
*/
|
|
683
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;
|
|
743
|
+
/**
|
|
744
|
+
* The elements backing this application's entities, keyed by the entity itself. Registered
|
|
745
|
+
* by EntityElement at creation and removed when an entity is destroyed, this joins engine
|
|
746
|
+
* scene nodes back to their owning elements by identity - never by name.
|
|
747
|
+
*/
|
|
748
|
+
_entityElements = new Map();
|
|
684
749
|
_picker = null;
|
|
685
750
|
_hasPointerListeners = {
|
|
686
751
|
pointerenter: false,
|
|
@@ -727,8 +792,16 @@ class AppElement extends AsyncElement {
|
|
|
727
792
|
super();
|
|
728
793
|
// Bind methods to maintain 'this' context
|
|
729
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
|
+
});
|
|
730
802
|
}
|
|
731
803
|
async connectedCallback() {
|
|
804
|
+
const generation = ++this._bootGeneration;
|
|
732
805
|
// Created before the first await, so the bar is visible while modules and the graphics
|
|
733
806
|
// device are created, and exists before any disconnect could need to clean it up
|
|
734
807
|
if (this._loadingBar && !this._bar) {
|
|
@@ -737,7 +810,12 @@ class AppElement extends AsyncElement {
|
|
|
737
810
|
// Get all pc-module elements that are direct children of the pc-app element
|
|
738
811
|
const moduleElements = this.querySelectorAll(':scope > pc-module');
|
|
739
812
|
// Wait for all modules to load
|
|
740
|
-
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
|
+
}
|
|
741
819
|
// Create and append the canvas to the element
|
|
742
820
|
this._canvas = document.createElement('canvas');
|
|
743
821
|
this.appendChild(this._canvas);
|
|
@@ -748,15 +826,26 @@ class AppElement extends AsyncElement {
|
|
|
748
826
|
null: ['null']
|
|
749
827
|
};
|
|
750
828
|
const deviceTypes = backendToDeviceTypes[this._backend] || [];
|
|
829
|
+
this._optionsLocked = true;
|
|
751
830
|
const device = await createGraphicsDevice(this._canvas, {
|
|
752
831
|
// @ts-ignore - alpha needs to be documented
|
|
753
832
|
alpha: this._alpha,
|
|
754
833
|
antialias: this._antialias,
|
|
755
|
-
depth: this.
|
|
834
|
+
depth: this._depthBuffer,
|
|
756
835
|
deviceTypes: deviceTypes,
|
|
757
|
-
stencil: this.
|
|
836
|
+
stencil: this._stencilBuffer
|
|
758
837
|
});
|
|
759
|
-
|
|
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
|
+
}
|
|
845
|
+
// Assigned rather than resolved to a number here: the engine caps against the live
|
|
846
|
+
// window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
|
|
847
|
+
// display when a window moves between monitors of differing density.
|
|
848
|
+
device.maxPixelRatio = this._maxPixelRatio;
|
|
760
849
|
const createOptions = new AppOptions();
|
|
761
850
|
createOptions.graphicsDevice = device;
|
|
762
851
|
createOptions.keyboard = new Keyboard(window);
|
|
@@ -829,27 +918,41 @@ class AppElement extends AsyncElement {
|
|
|
829
918
|
this._pickerCreate();
|
|
830
919
|
// Get all pc-asset elements that are direct children of the pc-app element
|
|
831
920
|
const assetElements = this.querySelectorAll(':scope > pc-asset');
|
|
832
|
-
Array.from(assetElements)
|
|
833
|
-
assetElement.
|
|
921
|
+
for (const assetElement of Array.from(assetElements)) {
|
|
922
|
+
assetElement._createAsset();
|
|
834
923
|
const asset = assetElement.asset;
|
|
835
924
|
if (asset) {
|
|
836
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
|
+
}
|
|
837
933
|
}
|
|
838
|
-
}
|
|
934
|
+
}
|
|
839
935
|
// Get all pc-material elements that are direct children of the pc-app element
|
|
840
936
|
const materialElements = this.querySelectorAll(':scope > pc-material');
|
|
841
937
|
Array.from(materialElements).forEach((materialElement) => {
|
|
842
|
-
materialElement.
|
|
938
|
+
materialElement._createMaterial();
|
|
843
939
|
});
|
|
844
940
|
// Create all entities
|
|
845
941
|
const entityElements = this.querySelectorAll('pc-entity');
|
|
846
942
|
Array.from(entityElements).forEach((entityElement) => {
|
|
847
|
-
entityElement.
|
|
943
|
+
entityElement._createEntity(app);
|
|
848
944
|
});
|
|
849
945
|
// Build hierarchy
|
|
850
946
|
entityElements.forEach((entityElement) => {
|
|
851
|
-
entityElement.
|
|
947
|
+
entityElement._buildHierarchy(app);
|
|
852
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
|
+
}
|
|
853
956
|
this._hierarchyReady = true;
|
|
854
957
|
// Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
|
|
855
958
|
// listener must be attached before preload() is called: an asset that is already loaded
|
|
@@ -866,8 +969,19 @@ class AppElement extends AsyncElement {
|
|
|
866
969
|
this._loadProgress = total === 0 ? 1 : 0;
|
|
867
970
|
this._bar?.progress(0, total);
|
|
868
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
|
+
}
|
|
869
977
|
// Load assets before starting the application
|
|
870
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
|
+
}
|
|
871
985
|
// Scope the counter to this preload pass, so a later app.preload() call by user code
|
|
872
986
|
// cannot push `loaded` past `total`
|
|
873
987
|
app.off('preload:progress', onPreloadProgress);
|
|
@@ -883,15 +997,26 @@ class AppElement extends AsyncElement {
|
|
|
883
997
|
});
|
|
884
998
|
}
|
|
885
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++;
|
|
1003
|
+
this._optionsLocked = false;
|
|
886
1004
|
this._pickerDestroy();
|
|
887
|
-
// Clean up the application
|
|
1005
|
+
// Clean up the application. Destroying it destroys every entity, whose destroy hooks
|
|
1006
|
+
// unregister them - clear() covers any entity the engine no longer reached.
|
|
888
1007
|
if (this._app) {
|
|
889
1008
|
this._app.destroy();
|
|
890
1009
|
this._app = null;
|
|
891
1010
|
}
|
|
1011
|
+
this._entityElements.clear();
|
|
892
1012
|
this._loadProgress = 0;
|
|
893
1013
|
this._bar?.destroy();
|
|
894
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();
|
|
895
1020
|
// Remove event listeners
|
|
896
1021
|
window.removeEventListener('resize', this._onWindowResize);
|
|
897
1022
|
// Remove the canvas
|
|
@@ -919,14 +1044,11 @@ class AppElement extends AsyncElement {
|
|
|
919
1044
|
this._pointerHandlers.pointermove = listener(this._onPointerMove);
|
|
920
1045
|
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
921
1046
|
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
922
|
-
//
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
// created from onpointer* attributes when their elements were first upgraded)
|
|
928
|
-
const anyListeners = Array.from(this.querySelectorAll('pc-entity'))
|
|
929
|
-
.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));
|
|
930
1052
|
if (anyListeners) {
|
|
931
1053
|
this._onPointerListenerAdded(type);
|
|
932
1054
|
}
|
|
@@ -941,6 +1063,7 @@ class AppElement extends AsyncElement {
|
|
|
941
1063
|
});
|
|
942
1064
|
}
|
|
943
1065
|
this._picker = null;
|
|
1066
|
+
this._hoveredEntity = null;
|
|
944
1067
|
this._pointerHandlers = {
|
|
945
1068
|
pointermove: null,
|
|
946
1069
|
pointerdown: null,
|
|
@@ -954,6 +1077,74 @@ class AppElement extends AsyncElement {
|
|
|
954
1077
|
pointermove: false
|
|
955
1078
|
};
|
|
956
1079
|
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Registers the element that created an entity. Called by EntityElement when it creates its
|
|
1082
|
+
* entity.
|
|
1083
|
+
*
|
|
1084
|
+
* @param entity - The entity.
|
|
1085
|
+
* @param element - The element that created it.
|
|
1086
|
+
* @internal
|
|
1087
|
+
*/
|
|
1088
|
+
_registerEntityElement(entity, element) {
|
|
1089
|
+
this._entityElements.set(entity, element);
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Removes the registration for a destroyed entity. Called by EntityElement.
|
|
1093
|
+
*
|
|
1094
|
+
* @param entity - The entity.
|
|
1095
|
+
* @internal
|
|
1096
|
+
*/
|
|
1097
|
+
_unregisterEntityElement(entity) {
|
|
1098
|
+
this._entityElements.delete(entity);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Returns the `<pc-entity>` element whose backing entity is `entity`, or `null` if the
|
|
1102
|
+
* entity was not created by an element of this application - for example, a node inside a
|
|
1103
|
+
* model's instantiated hierarchy, or an entity created through the engine API.
|
|
1104
|
+
*
|
|
1105
|
+
* @param entity - The entity to look up.
|
|
1106
|
+
* @returns The element backing the entity, or `null`.
|
|
1107
|
+
*/
|
|
1108
|
+
elementFromEntity(entity) {
|
|
1109
|
+
return this._entityElements.get(entity) ?? null;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Resolves the element that owns a picked node: the nearest node up the parent chain -
|
|
1113
|
+
* starting with the node itself - that was created by a `<pc-entity>` of this application.
|
|
1114
|
+
* A hit inside a model's instantiated hierarchy therefore resolves to the element hosting
|
|
1115
|
+
* the model.
|
|
1116
|
+
*
|
|
1117
|
+
* @param node - The picked node, or `null`.
|
|
1118
|
+
* @returns The owning element, or `null`.
|
|
1119
|
+
*/
|
|
1120
|
+
_elementFromNode(node) {
|
|
1121
|
+
while (node !== null) {
|
|
1122
|
+
const element = this._entityElements.get(node);
|
|
1123
|
+
if (element) {
|
|
1124
|
+
return element;
|
|
1125
|
+
}
|
|
1126
|
+
node = node.parent;
|
|
1127
|
+
}
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Like {@link _elementFromNode}, but skips elements without a listener for `type`, so a hit
|
|
1132
|
+
* on an unlistened child still reaches a listening ancestor.
|
|
1133
|
+
*
|
|
1134
|
+
* @param node - The picked node, or `null`.
|
|
1135
|
+
* @param type - The pointer event type a listener is required for.
|
|
1136
|
+
* @returns The nearest listening element, or `null`.
|
|
1137
|
+
*/
|
|
1138
|
+
_elementWithListener(node, type) {
|
|
1139
|
+
while (node !== null) {
|
|
1140
|
+
const element = this._entityElements.get(node);
|
|
1141
|
+
if (element?._hasListeners(type)) {
|
|
1142
|
+
return element;
|
|
1143
|
+
}
|
|
1144
|
+
node = node.parent;
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
957
1148
|
// New helper to convert CSS coordinates to canvas (picker) coordinates
|
|
958
1149
|
_getPickerCoordinates(event) {
|
|
959
1150
|
// Get the canvas' bounding rectangle in CSS pixels.
|
|
@@ -999,56 +1190,44 @@ class AppElement extends AsyncElement {
|
|
|
999
1190
|
const node = await this._pickNode(event);
|
|
1000
1191
|
if (token !== this._pickToken || !this._picker)
|
|
1001
1192
|
return;
|
|
1002
|
-
//
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
while (currentNode !== null) {
|
|
1006
|
-
const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
|
|
1007
|
-
if (entityElement) {
|
|
1008
|
-
newHoverEntity = entityElement;
|
|
1009
|
-
break;
|
|
1010
|
-
}
|
|
1011
|
-
currentNode = currentNode.parent;
|
|
1012
|
-
}
|
|
1193
|
+
// The hovered element is the nearest one up the node's parent chain, listening or not -
|
|
1194
|
+
// dispatch is gated per event type below
|
|
1195
|
+
const newHoverEntity = this._elementFromNode(node);
|
|
1013
1196
|
// Handle enter/leave events
|
|
1014
1197
|
if (this._hoveredEntity !== newHoverEntity) {
|
|
1015
|
-
if (this._hoveredEntity && this._hoveredEntity.
|
|
1198
|
+
if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
|
|
1016
1199
|
this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
|
|
1017
1200
|
}
|
|
1018
|
-
if (newHoverEntity && newHoverEntity.
|
|
1201
|
+
if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
|
|
1019
1202
|
newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
|
|
1020
1203
|
}
|
|
1021
1204
|
}
|
|
1022
1205
|
// Update hover state
|
|
1023
1206
|
this._hoveredEntity = newHoverEntity;
|
|
1024
1207
|
// Handle pointermove event
|
|
1025
|
-
if (newHoverEntity && newHoverEntity.
|
|
1208
|
+
if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
|
|
1026
1209
|
newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
|
|
1027
1210
|
}
|
|
1028
1211
|
}
|
|
1029
1212
|
async _onPointerDown(event) {
|
|
1030
1213
|
if (!this._picker || !this.app)
|
|
1031
1214
|
return;
|
|
1032
|
-
|
|
1215
|
+
const node = await this._pickNode(event);
|
|
1033
1216
|
if (!this._picker)
|
|
1034
1217
|
return; // the element disconnected while the pick was in flight
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
1039
|
-
break;
|
|
1040
|
-
}
|
|
1041
|
-
currentNode = currentNode.parent;
|
|
1218
|
+
const entityElement = this._elementWithListener(node, 'pointerdown');
|
|
1219
|
+
if (entityElement) {
|
|
1220
|
+
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
1042
1221
|
}
|
|
1043
1222
|
}
|
|
1044
1223
|
async _onPointerUp(event) {
|
|
1045
1224
|
if (!this._picker || !this.app)
|
|
1046
1225
|
return;
|
|
1047
1226
|
const node = await this._pickNode(event);
|
|
1048
|
-
if (!
|
|
1049
|
-
return;
|
|
1050
|
-
const entityElement = this.
|
|
1051
|
-
if (entityElement
|
|
1227
|
+
if (!this._picker)
|
|
1228
|
+
return; // the element disconnected while the pick was in flight
|
|
1229
|
+
const entityElement = this._elementWithListener(node, 'pointerup');
|
|
1230
|
+
if (entityElement) {
|
|
1052
1231
|
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
1053
1232
|
}
|
|
1054
1233
|
}
|
|
@@ -1056,50 +1235,64 @@ class AppElement extends AsyncElement {
|
|
|
1056
1235
|
if (!this._hasPointerListeners[type] && this._canvas) {
|
|
1057
1236
|
this._hasPointerListeners[type] = true;
|
|
1058
1237
|
// For enter/leave events, we need the move handler
|
|
1059
|
-
const handler =
|
|
1060
|
-
this._pointerHandlers.pointermove
|
|
1061
|
-
this._pointerHandlers[type];
|
|
1238
|
+
const handler = type === 'pointerenter' || type === 'pointerleave'
|
|
1239
|
+
? this._pointerHandlers.pointermove
|
|
1240
|
+
: this._pointerHandlers[type];
|
|
1062
1241
|
if (handler) {
|
|
1063
1242
|
this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
|
|
1064
1243
|
}
|
|
1065
1244
|
}
|
|
1066
1245
|
}
|
|
1067
1246
|
_onPointerListenerRemoved(type) {
|
|
1068
|
-
const hasListeners = Array.from(this.querySelectorAll('pc-entity'))
|
|
1069
|
-
.some(entity => entity.hasListeners(type));
|
|
1247
|
+
const hasListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
|
|
1070
1248
|
if (!hasListeners && this._canvas) {
|
|
1071
1249
|
this._hasPointerListeners[type] = false;
|
|
1072
|
-
const handler =
|
|
1073
|
-
this._pointerHandlers.pointermove
|
|
1074
|
-
this._pointerHandlers[type];
|
|
1250
|
+
const handler = type === 'pointerenter' || type === 'pointerleave'
|
|
1251
|
+
? this._pointerHandlers.pointermove
|
|
1252
|
+
: this._pointerHandlers[type];
|
|
1075
1253
|
if (handler) {
|
|
1076
1254
|
this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
|
|
1077
1255
|
}
|
|
1078
1256
|
}
|
|
1079
1257
|
}
|
|
1080
1258
|
/**
|
|
1081
|
-
*
|
|
1259
|
+
* Warns that a graphics option was written too late to have any effect. These options are read
|
|
1260
|
+
* once, when the element connects and creates its graphics device, so a later write updates
|
|
1261
|
+
* only the element's own property - silently, without this.
|
|
1262
|
+
*
|
|
1263
|
+
* @param name - The name of the option, as its attribute.
|
|
1264
|
+
*/
|
|
1265
|
+
_warnIfBooted(name) {
|
|
1266
|
+
if (this._optionsLocked) {
|
|
1267
|
+
console.warn(`Attribute '${name}' on <pc-app> is only read when the application boots, so this change has no effect. Set it before the element is connected, or remove and re-insert the element to reboot with the new value.`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Sets whether the frame buffer has an alpha channel, which is what lets the page show through
|
|
1272
|
+
* wherever the scene has not drawn. Read only when the application boots.
|
|
1082
1273
|
* @param value - The alpha flag.
|
|
1083
1274
|
*/
|
|
1084
1275
|
set alpha(value) {
|
|
1276
|
+
this._warnIfBooted('alpha');
|
|
1085
1277
|
this._alpha = value;
|
|
1086
1278
|
}
|
|
1087
1279
|
/**
|
|
1088
|
-
* Gets the alpha
|
|
1280
|
+
* Gets whether the frame buffer has an alpha channel.
|
|
1089
1281
|
* @returns The alpha flag.
|
|
1090
1282
|
*/
|
|
1091
1283
|
get alpha() {
|
|
1092
1284
|
return this._alpha;
|
|
1093
1285
|
}
|
|
1094
1286
|
/**
|
|
1095
|
-
* Sets the
|
|
1287
|
+
* Sets whether the frame buffer is anti-aliased. Read only when the application boots.
|
|
1096
1288
|
* @param value - The antialias flag.
|
|
1097
1289
|
*/
|
|
1098
1290
|
set antialias(value) {
|
|
1291
|
+
this._warnIfBooted('antialias');
|
|
1099
1292
|
this._antialias = value;
|
|
1100
1293
|
}
|
|
1101
1294
|
/**
|
|
1102
|
-
* Gets the
|
|
1295
|
+
* Gets whether the frame buffer is anti-aliased.
|
|
1103
1296
|
* @returns The antialias flag.
|
|
1104
1297
|
*/
|
|
1105
1298
|
get antialias() {
|
|
@@ -1107,10 +1300,11 @@ class AppElement extends AsyncElement {
|
|
|
1107
1300
|
}
|
|
1108
1301
|
/**
|
|
1109
1302
|
* Sets the graphics backend. Defaults to 'webgpu', which falls back to 'webgl2' if WebGPU
|
|
1110
|
-
* is not supported by the browser.
|
|
1303
|
+
* is not supported by the browser. Read only when the application boots.
|
|
1111
1304
|
* @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
|
|
1112
1305
|
*/
|
|
1113
1306
|
set backend(value) {
|
|
1307
|
+
this._warnIfBooted('backend');
|
|
1114
1308
|
this._backend = value;
|
|
1115
1309
|
}
|
|
1116
1310
|
/**
|
|
@@ -1121,44 +1315,20 @@ class AppElement extends AsyncElement {
|
|
|
1121
1315
|
return this._backend;
|
|
1122
1316
|
}
|
|
1123
1317
|
/**
|
|
1124
|
-
* Sets the depth
|
|
1125
|
-
*
|
|
1126
|
-
|
|
1127
|
-
set depth(value) {
|
|
1128
|
-
this._depth = value;
|
|
1129
|
-
}
|
|
1130
|
-
/**
|
|
1131
|
-
* Gets the depth flag.
|
|
1132
|
-
* @returns The depth flag.
|
|
1133
|
-
*/
|
|
1134
|
-
get depth() {
|
|
1135
|
-
return this._depth;
|
|
1136
|
-
}
|
|
1137
|
-
/**
|
|
1138
|
-
* Gets the hierarchy ready flag.
|
|
1139
|
-
* @returns The hierarchy ready flag.
|
|
1140
|
-
* @ignore
|
|
1141
|
-
*/
|
|
1142
|
-
get hierarchyReady() {
|
|
1143
|
-
return this._hierarchyReady;
|
|
1144
|
-
}
|
|
1145
|
-
/**
|
|
1146
|
-
* Sets the high resolution flag. When true, the application will render at the device's
|
|
1147
|
-
* physical resolution. When false, the application will render at CSS resolution.
|
|
1148
|
-
* @param value - The high resolution flag.
|
|
1318
|
+
* Sets whether the frame buffer has a depth buffer, which the renderer needs to resolve which
|
|
1319
|
+
* surface is nearest the camera. Read only when the application boots.
|
|
1320
|
+
* @param value - The depth buffer flag.
|
|
1149
1321
|
*/
|
|
1150
|
-
set
|
|
1151
|
-
this.
|
|
1152
|
-
|
|
1153
|
-
this.app.graphicsDevice.maxPixelRatio = value ? window.devicePixelRatio : 1;
|
|
1154
|
-
}
|
|
1322
|
+
set depthBuffer(value) {
|
|
1323
|
+
this._warnIfBooted('depth-buffer');
|
|
1324
|
+
this._depthBuffer = value;
|
|
1155
1325
|
}
|
|
1156
1326
|
/**
|
|
1157
|
-
* Gets the
|
|
1158
|
-
* @returns The
|
|
1327
|
+
* Gets whether the frame buffer has a depth buffer.
|
|
1328
|
+
* @returns The depth buffer flag.
|
|
1159
1329
|
*/
|
|
1160
|
-
get
|
|
1161
|
-
return this.
|
|
1330
|
+
get depthBuffer() {
|
|
1331
|
+
return this._depthBuffer;
|
|
1162
1332
|
}
|
|
1163
1333
|
/**
|
|
1164
1334
|
* Sets whether the application shows its built-in loading bar while it boots and preloads its
|
|
@@ -1184,21 +1354,45 @@ class AppElement extends AsyncElement {
|
|
|
1184
1354
|
return this._loadingBar;
|
|
1185
1355
|
}
|
|
1186
1356
|
/**
|
|
1187
|
-
* Sets the
|
|
1188
|
-
*
|
|
1357
|
+
* Sets the cap on the pixel ratio the application renders at. The canvas is sized by the
|
|
1358
|
+
* smaller of this value and the display's own device pixel ratio, so the default of `Infinity`
|
|
1359
|
+
* renders at full physical resolution, `1` renders at CSS resolution, and an intermediate
|
|
1360
|
+
* value such as `2` keeps a dense display sharp without paying for every one of its pixels.
|
|
1361
|
+
* Must be greater than 0. Unlike the other graphics options, this applies immediately.
|
|
1362
|
+
* @param value - The maximum pixel ratio.
|
|
1363
|
+
*/
|
|
1364
|
+
set maxPixelRatio(value) {
|
|
1365
|
+
this._maxPixelRatio = value;
|
|
1366
|
+
if (this.app) {
|
|
1367
|
+
this.app.graphicsDevice.maxPixelRatio = value;
|
|
1368
|
+
this.app.resizeCanvas();
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Gets the cap on the pixel ratio the application renders at.
|
|
1373
|
+
* @returns The maximum pixel ratio.
|
|
1374
|
+
*/
|
|
1375
|
+
get maxPixelRatio() {
|
|
1376
|
+
return this._maxPixelRatio;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Sets whether the frame buffer has a stencil buffer, which stencil-based effects and UI
|
|
1380
|
+
* masking need. Read only when the application boots.
|
|
1381
|
+
* @param value - The stencil buffer flag.
|
|
1189
1382
|
*/
|
|
1190
|
-
set
|
|
1191
|
-
this.
|
|
1383
|
+
set stencilBuffer(value) {
|
|
1384
|
+
this._warnIfBooted('stencil-buffer');
|
|
1385
|
+
this._stencilBuffer = value;
|
|
1192
1386
|
}
|
|
1193
1387
|
/**
|
|
1194
|
-
* Gets the stencil
|
|
1195
|
-
* @returns The stencil flag.
|
|
1388
|
+
* Gets whether the frame buffer has a stencil buffer.
|
|
1389
|
+
* @returns The stencil buffer flag.
|
|
1196
1390
|
*/
|
|
1197
|
-
get
|
|
1198
|
-
return this.
|
|
1391
|
+
get stencilBuffer() {
|
|
1392
|
+
return this._stencilBuffer;
|
|
1199
1393
|
}
|
|
1200
1394
|
static get observedAttributes() {
|
|
1201
|
-
return ['alpha', 'antialias', 'backend', 'depth', '
|
|
1395
|
+
return ['alpha', 'antialias', 'backend', 'depth-buffer', 'loading-bar', 'max-pixel-ratio', 'stencil-buffer'];
|
|
1202
1396
|
}
|
|
1203
1397
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
1204
1398
|
switch (name) {
|
|
@@ -1211,17 +1405,17 @@ class AppElement extends AsyncElement {
|
|
|
1211
1405
|
case 'backend':
|
|
1212
1406
|
this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgpu', name);
|
|
1213
1407
|
break;
|
|
1214
|
-
case 'depth':
|
|
1215
|
-
this.
|
|
1216
|
-
break;
|
|
1217
|
-
case 'high-resolution':
|
|
1218
|
-
this.highResolution = parseBool(newValue, true);
|
|
1408
|
+
case 'depth-buffer':
|
|
1409
|
+
this.depthBuffer = parseBool(newValue, true);
|
|
1219
1410
|
break;
|
|
1220
1411
|
case 'loading-bar':
|
|
1221
1412
|
this.loadingBar = parseBool(newValue, true);
|
|
1222
1413
|
break;
|
|
1223
|
-
case '
|
|
1224
|
-
this.
|
|
1414
|
+
case 'max-pixel-ratio':
|
|
1415
|
+
this.maxPixelRatio = parseNumber(newValue, Infinity, name);
|
|
1416
|
+
break;
|
|
1417
|
+
case 'stencil-buffer':
|
|
1418
|
+
this.stencilBuffer = parseBool(newValue, true);
|
|
1225
1419
|
break;
|
|
1226
1420
|
}
|
|
1227
1421
|
}
|
|
@@ -1290,6 +1484,11 @@ class EntityElement extends AsyncElement {
|
|
|
1290
1484
|
*/
|
|
1291
1485
|
_built = false;
|
|
1292
1486
|
_entity = null;
|
|
1487
|
+
/**
|
|
1488
|
+
* The application element this entity is registered with, cached at creation time so the
|
|
1489
|
+
* entity can be unregistered even once this element has left the DOM.
|
|
1490
|
+
*/
|
|
1491
|
+
_appElement = null;
|
|
1293
1492
|
/**
|
|
1294
1493
|
* The PlayCanvas entity instance. `null` until the element is ready, and again once it has
|
|
1295
1494
|
* been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
@@ -1299,7 +1498,14 @@ class EntityElement extends AsyncElement {
|
|
|
1299
1498
|
get entity() {
|
|
1300
1499
|
return this._entity;
|
|
1301
1500
|
}
|
|
1302
|
-
|
|
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) {
|
|
1303
1509
|
// Guard against double creation. When a subtree is inserted at runtime (e.g. cloning a
|
|
1304
1510
|
// `<template>`), an ancestor's connectedCallback eagerly creates descendant entities; the
|
|
1305
1511
|
// descendants' own connectedCallbacks would otherwise create them a second time.
|
|
@@ -1319,8 +1525,41 @@ class EntityElement extends AsyncElement {
|
|
|
1319
1525
|
if (this._tags.length > 0) {
|
|
1320
1526
|
entity.tags.add(this._tags);
|
|
1321
1527
|
}
|
|
1528
|
+
// Register with the owning application, which joins engine nodes back to elements by
|
|
1529
|
+
// identity (never by name), and hook the entity's destruction. The engine fires 'destroy'
|
|
1530
|
+
// for every entity in a destroyed subtree, so the element learns of its entity's death no
|
|
1531
|
+
// matter who causes it: this element, an ancestor, the whole application, or a user
|
|
1532
|
+
// script calling entity.destroy().
|
|
1533
|
+
this._appElement = this.closestApp;
|
|
1534
|
+
this._appElement?._registerEntityElement(entity, this);
|
|
1535
|
+
entity.once('destroy', this._onEntityDestroy, this);
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Handles the destruction of the backing entity. Resets the element so a later re-insertion
|
|
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`.
|
|
1543
|
+
*
|
|
1544
|
+
* @param entity - The entity that was destroyed.
|
|
1545
|
+
*/
|
|
1546
|
+
_onEntityDestroy(entity) {
|
|
1547
|
+
this._appElement?._unregisterEntityElement(entity);
|
|
1548
|
+
this._appElement = null;
|
|
1549
|
+
this._entity = null;
|
|
1550
|
+
this._built = false;
|
|
1551
|
+
this._resetReady();
|
|
1322
1552
|
}
|
|
1323
|
-
|
|
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) {
|
|
1324
1563
|
if (!this.entity || this._built)
|
|
1325
1564
|
return;
|
|
1326
1565
|
this._built = true;
|
|
@@ -1346,37 +1585,26 @@ class EntityElement extends AsyncElement {
|
|
|
1346
1585
|
return;
|
|
1347
1586
|
}
|
|
1348
1587
|
// If app is already running, create entity immediately
|
|
1349
|
-
if (closestApp.
|
|
1588
|
+
if (closestApp._hierarchyReady) {
|
|
1350
1589
|
const app = closestApp.app;
|
|
1351
|
-
this.
|
|
1352
|
-
this.
|
|
1590
|
+
this._createEntity(app);
|
|
1591
|
+
this._buildHierarchy(app);
|
|
1353
1592
|
// Handle any child entities that might exist
|
|
1354
1593
|
const childEntities = this.querySelectorAll('pc-entity');
|
|
1355
1594
|
childEntities.forEach((child) => {
|
|
1356
|
-
child.
|
|
1595
|
+
child._createEntity(app);
|
|
1357
1596
|
});
|
|
1358
1597
|
childEntities.forEach((child) => {
|
|
1359
|
-
child.
|
|
1598
|
+
child._buildHierarchy(app);
|
|
1360
1599
|
});
|
|
1361
1600
|
}
|
|
1362
1601
|
}
|
|
1363
1602
|
disconnectedCallback() {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
// on re-insertion, so the descendant would get a fresh entity that is never parented.
|
|
1370
|
-
const children = this.querySelectorAll('pc-entity');
|
|
1371
|
-
children.forEach((child) => {
|
|
1372
|
-
child._entity = null;
|
|
1373
|
-
child._built = false;
|
|
1374
|
-
});
|
|
1375
|
-
// Destroy the entity
|
|
1376
|
-
this.entity.destroy();
|
|
1377
|
-
this._entity = null;
|
|
1378
|
-
this._built = false;
|
|
1379
|
-
}
|
|
1603
|
+
// Destroying the entity destroys its whole subtree, and the engine fires 'destroy' for
|
|
1604
|
+
// every entity in it - so _onEntityDestroy resets this element AND every descendant
|
|
1605
|
+
// element before the descendants' own disconnectedCallbacks run. Their entities are null
|
|
1606
|
+
// by then, making this call a no-op for them.
|
|
1607
|
+
this._entity?.destroy();
|
|
1380
1608
|
}
|
|
1381
1609
|
/**
|
|
1382
1610
|
* Sets the enabled state of the entity.
|
|
@@ -1561,14 +1789,23 @@ class EntityElement extends AsyncElement {
|
|
|
1561
1789
|
}
|
|
1562
1790
|
removeEventListener(type, listener, options) {
|
|
1563
1791
|
if (this._listeners[type]) {
|
|
1564
|
-
this._listeners[type] = this._listeners[type].filter(l => l !== listener);
|
|
1792
|
+
this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
|
|
1565
1793
|
}
|
|
1566
1794
|
super.removeEventListener(type, listener, options);
|
|
1567
1795
|
if (type.startsWith('pointer')) {
|
|
1568
1796
|
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
1569
1797
|
}
|
|
1570
1798
|
}
|
|
1571
|
-
|
|
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) {
|
|
1572
1809
|
return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
|
|
1573
1810
|
}
|
|
1574
1811
|
}
|
|
@@ -1859,7 +2096,7 @@ class AssetElement extends AsyncElement {
|
|
|
1859
2096
|
const app = appElement.app;
|
|
1860
2097
|
if (!app)
|
|
1861
2098
|
return; // pc-app is re-connecting; its own boot will create this asset
|
|
1862
|
-
this.
|
|
2099
|
+
this._createAsset();
|
|
1863
2100
|
if (this.asset) {
|
|
1864
2101
|
app.assets.add(this.asset); // add() auto-loads when preload is true
|
|
1865
2102
|
if (!this.lazy) {
|
|
@@ -1867,13 +2104,15 @@ class AssetElement extends AsyncElement {
|
|
|
1867
2104
|
}
|
|
1868
2105
|
}
|
|
1869
2106
|
}
|
|
1870
|
-
// Never ready if
|
|
2107
|
+
// Never ready if _createAsset failed (unsupported asset type)
|
|
1871
2108
|
if (this.asset) {
|
|
1872
2109
|
this._onReady();
|
|
1873
2110
|
}
|
|
1874
2111
|
}
|
|
1875
2112
|
disconnectedCallback() {
|
|
1876
|
-
this.
|
|
2113
|
+
this._destroyAsset();
|
|
2114
|
+
// Re-arm readiness so a re-inserted element announces the asset it creates then
|
|
2115
|
+
this._resetReady();
|
|
1877
2116
|
}
|
|
1878
2117
|
_onAssetLoad() {
|
|
1879
2118
|
this.dispatchEvent(new Event('load'));
|
|
@@ -1883,7 +2122,14 @@ class AssetElement extends AsyncElement {
|
|
|
1883
2122
|
message: err instanceof Error ? err.message : String(err)
|
|
1884
2123
|
}));
|
|
1885
2124
|
}
|
|
1886
|
-
|
|
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() {
|
|
1887
2133
|
const id = this.getAttribute('id') || '';
|
|
1888
2134
|
const src = this.getAttribute('src') || '';
|
|
1889
2135
|
let type = this.getAttribute('type');
|
|
@@ -1973,7 +2219,7 @@ class AssetElement extends AsyncElement {
|
|
|
1973
2219
|
}
|
|
1974
2220
|
return data;
|
|
1975
2221
|
}
|
|
1976
|
-
|
|
2222
|
+
_destroyAsset() {
|
|
1977
2223
|
if (this.asset) {
|
|
1978
2224
|
// A caller that keeps the Asset alive must not dispatch on a removed element
|
|
1979
2225
|
this.asset.off('load', this._onAssetLoad, this);
|
|
@@ -2001,6 +2247,13 @@ class AssetElement extends AsyncElement {
|
|
|
2001
2247
|
get lazy() {
|
|
2002
2248
|
return this._lazy;
|
|
2003
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
|
+
*/
|
|
2004
2257
|
static get(id) {
|
|
2005
2258
|
const assetElement = document.querySelector(`pc-asset[id="${id}"]`);
|
|
2006
2259
|
return assetElement?.asset;
|
|
@@ -2026,6 +2279,14 @@ class ComponentElement extends AsyncElement {
|
|
|
2026
2279
|
_enabled = true;
|
|
2027
2280
|
_component = null;
|
|
2028
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;
|
|
2029
2290
|
/**
|
|
2030
2291
|
* Creates a new ComponentElement instance.
|
|
2031
2292
|
*
|
|
@@ -2036,11 +2297,17 @@ class ComponentElement extends AsyncElement {
|
|
|
2036
2297
|
super();
|
|
2037
2298
|
this._componentName = componentName;
|
|
2038
2299
|
}
|
|
2039
|
-
|
|
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
|
+
*/
|
|
2040
2306
|
getInitialComponentData() {
|
|
2041
2307
|
return {};
|
|
2042
2308
|
}
|
|
2043
|
-
async
|
|
2309
|
+
async _addComponent() {
|
|
2310
|
+
const generation = this._connectionGeneration;
|
|
2044
2311
|
const entityElement = this.closestEntity;
|
|
2045
2312
|
if (!entityElement) {
|
|
2046
2313
|
// A component can only exist on an entity, so an element placed outside one is inert.
|
|
@@ -2050,19 +2317,42 @@ class ComponentElement extends AsyncElement {
|
|
|
2050
2317
|
return;
|
|
2051
2318
|
}
|
|
2052
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
|
+
}
|
|
2053
2325
|
// Add the component to the entity
|
|
2054
2326
|
const data = this.getInitialComponentData();
|
|
2055
2327
|
this._component = entityElement.entity.addComponent(this._componentName, data);
|
|
2056
2328
|
}
|
|
2057
|
-
|
|
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
|
+
}
|
|
2058
2336
|
async connectedCallback() {
|
|
2337
|
+
const generation = ++this._connectionGeneration;
|
|
2059
2338
|
this._appElement = this.closestApp ?? null;
|
|
2060
2339
|
await this._appElement?.ready();
|
|
2061
|
-
|
|
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
|
+
}
|
|
2062
2350
|
this.initComponent();
|
|
2063
2351
|
this._onReady();
|
|
2064
2352
|
}
|
|
2065
2353
|
disconnectedCallback() {
|
|
2354
|
+
// Invalidate any connectedCallback still suspended on an await
|
|
2355
|
+
this._connectionGeneration++;
|
|
2066
2356
|
// Remove the component when the element is disconnected. Skip this when the owning
|
|
2067
2357
|
// application has already been destroyed — removing a <pc-app> disconnects it before
|
|
2068
2358
|
// its children, taking the component systems with it.
|
|
@@ -2071,6 +2361,7 @@ class ComponentElement extends AsyncElement {
|
|
|
2071
2361
|
}
|
|
2072
2362
|
this._component = null;
|
|
2073
2363
|
this._appElement = null;
|
|
2364
|
+
this._resetReady();
|
|
2074
2365
|
}
|
|
2075
2366
|
/**
|
|
2076
2367
|
* The PlayCanvas component instance. `null` until the element is ready, and also for an
|
|
@@ -2523,6 +2814,10 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
2523
2814
|
}
|
|
2524
2815
|
customElements.define('pc-button', ButtonComponentElement);
|
|
2525
2816
|
|
|
2817
|
+
const projections = new Map([
|
|
2818
|
+
['perspective', PROJECTION_PERSPECTIVE],
|
|
2819
|
+
['orthographic', PROJECTION_ORTHOGRAPHIC]
|
|
2820
|
+
]);
|
|
2526
2821
|
const tonemaps = new Map([
|
|
2527
2822
|
['none', TONEMAP_NONE],
|
|
2528
2823
|
['linear', TONEMAP_LINEAR],
|
|
@@ -2553,7 +2848,7 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2553
2848
|
_gamma = 'srgb';
|
|
2554
2849
|
_horizontalFov = false;
|
|
2555
2850
|
_nearClip = 0.1;
|
|
2556
|
-
|
|
2851
|
+
_projection = 'perspective';
|
|
2557
2852
|
_orthoHeight = 10;
|
|
2558
2853
|
_priority = 0;
|
|
2559
2854
|
_rect = new Vec4(0, 0, 1, 1);
|
|
@@ -2577,12 +2872,12 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2577
2872
|
gammaCorrection: this._gamma === 'srgb' ? GAMMA_SRGB : GAMMA_NONE,
|
|
2578
2873
|
horizontalFov: this._horizontalFov,
|
|
2579
2874
|
nearClip: this._nearClip,
|
|
2580
|
-
projection: this.
|
|
2875
|
+
projection: projections.get(this._projection) ?? PROJECTION_PERSPECTIVE,
|
|
2581
2876
|
orthoHeight: this._orthoHeight,
|
|
2582
2877
|
priority: this._priority,
|
|
2583
2878
|
rect: this._rect,
|
|
2584
2879
|
scissorRect: this._scissorRect,
|
|
2585
|
-
toneMapping: tonemaps.get(this._tonemap)
|
|
2880
|
+
toneMapping: tonemaps.get(this._tonemap) ?? TONEMAP_NONE
|
|
2586
2881
|
};
|
|
2587
2882
|
}
|
|
2588
2883
|
get xrAvailable() {
|
|
@@ -2824,23 +3119,6 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2824
3119
|
get nearClip() {
|
|
2825
3120
|
return this._nearClip;
|
|
2826
3121
|
}
|
|
2827
|
-
/**
|
|
2828
|
-
* Sets the orthographic projection of the camera.
|
|
2829
|
-
* @param value - The orthographic projection.
|
|
2830
|
-
*/
|
|
2831
|
-
set orthographic(value) {
|
|
2832
|
-
this._orthographic = value;
|
|
2833
|
-
if (this.component) {
|
|
2834
|
-
this.component.projection = value ? PROJECTION_ORTHOGRAPHIC : PROJECTION_PERSPECTIVE;
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
/**
|
|
2838
|
-
* Gets the orthographic projection of the camera.
|
|
2839
|
-
* @returns The orthographic projection.
|
|
2840
|
-
*/
|
|
2841
|
-
get orthographic() {
|
|
2842
|
-
return this._orthographic;
|
|
2843
|
-
}
|
|
2844
3122
|
/**
|
|
2845
3123
|
* Sets the orthographic height of the camera.
|
|
2846
3124
|
* @param value - The orthographic height.
|
|
@@ -2875,6 +3153,23 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2875
3153
|
get priority() {
|
|
2876
3154
|
return this._priority;
|
|
2877
3155
|
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Sets the projection of the camera. Use `orthoHeight` to size an orthographic projection.
|
|
3158
|
+
* @param value - The projection ('perspective' or 'orthographic').
|
|
3159
|
+
*/
|
|
3160
|
+
set projection(value) {
|
|
3161
|
+
this._projection = value;
|
|
3162
|
+
if (this.component) {
|
|
3163
|
+
this.component.projection = projections.get(value) ?? PROJECTION_PERSPECTIVE;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* Gets the projection of the camera.
|
|
3168
|
+
* @returns The projection.
|
|
3169
|
+
*/
|
|
3170
|
+
get projection() {
|
|
3171
|
+
return this._projection;
|
|
3172
|
+
}
|
|
2878
3173
|
/**
|
|
2879
3174
|
* Sets the rect of the camera.
|
|
2880
3175
|
* @param value - The rect.
|
|
@@ -2941,9 +3236,9 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2941
3236
|
'gamma',
|
|
2942
3237
|
'horizontal-fov',
|
|
2943
3238
|
'near-clip',
|
|
2944
|
-
'orthographic',
|
|
2945
3239
|
'ortho-height',
|
|
2946
3240
|
'priority',
|
|
3241
|
+
'projection',
|
|
2947
3242
|
'rect',
|
|
2948
3243
|
'scissor-rect',
|
|
2949
3244
|
'tonemap'
|
|
@@ -2988,15 +3283,15 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2988
3283
|
case 'near-clip':
|
|
2989
3284
|
this.nearClip = parseNumber(newValue, 0.1, name);
|
|
2990
3285
|
break;
|
|
2991
|
-
case 'orthographic':
|
|
2992
|
-
this.orthographic = parseBool(newValue, false);
|
|
2993
|
-
break;
|
|
2994
3286
|
case 'ortho-height':
|
|
2995
3287
|
this.orthoHeight = parseNumber(newValue, 10, name);
|
|
2996
3288
|
break;
|
|
2997
3289
|
case 'priority':
|
|
2998
3290
|
this.priority = parseNumber(newValue, 0, name);
|
|
2999
3291
|
break;
|
|
3292
|
+
case 'projection':
|
|
3293
|
+
this.projection = parseEnum(newValue, projections, 'perspective', name);
|
|
3294
|
+
break;
|
|
3000
3295
|
case 'rect':
|
|
3001
3296
|
this.rect = parseVec4(newValue, new Vec4(0, 0, 1, 1), name);
|
|
3002
3297
|
break;
|
|
@@ -3124,7 +3419,17 @@ class CollisionComponentElement extends ComponentElement {
|
|
|
3124
3419
|
return this._type;
|
|
3125
3420
|
}
|
|
3126
3421
|
static get observedAttributes() {
|
|
3127
|
-
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
|
+
];
|
|
3128
3433
|
}
|
|
3129
3434
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
3130
3435
|
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
@@ -4829,7 +5134,7 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
4829
5134
|
}
|
|
4830
5135
|
// Set all the config properties on the component
|
|
4831
5136
|
for (const key in resource) {
|
|
4832
|
-
if (
|
|
5137
|
+
if (Object.hasOwn(resource, key)) {
|
|
4833
5138
|
this.component[key] = resource[key];
|
|
4834
5139
|
}
|
|
4835
5140
|
}
|
|
@@ -4902,10 +5207,7 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
4902
5207
|
}
|
|
4903
5208
|
}
|
|
4904
5209
|
static get observedAttributes() {
|
|
4905
|
-
return [
|
|
4906
|
-
...super.observedAttributes,
|
|
4907
|
-
'asset'
|
|
4908
|
-
];
|
|
5210
|
+
return [...super.observedAttributes, 'asset'];
|
|
4909
5211
|
}
|
|
4910
5212
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
4911
5213
|
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
@@ -5065,7 +5367,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5065
5367
|
_twoSidedLighting = false;
|
|
5066
5368
|
_useFog = true;
|
|
5067
5369
|
_useLighting = true;
|
|
5068
|
-
// 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()
|
|
5069
5371
|
_useMetalness = true;
|
|
5070
5372
|
_useMetalnessSpecularColor = false;
|
|
5071
5373
|
_useSkybox = true;
|
|
@@ -5078,6 +5380,10 @@ class MaterialElement extends HTMLElement {
|
|
|
5078
5380
|
_mapHandles = new Map();
|
|
5079
5381
|
_updateScheduled = false;
|
|
5080
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
|
+
*/
|
|
5081
5387
|
material = null;
|
|
5082
5388
|
async connectedCallback() {
|
|
5083
5389
|
const appElement = this.parentElement?.closest('pc-app') ?? null;
|
|
@@ -5095,10 +5401,17 @@ class MaterialElement extends HTMLElement {
|
|
|
5095
5401
|
if (!this.material) {
|
|
5096
5402
|
if (!appElement.app)
|
|
5097
5403
|
return; // pc-app is re-connecting; its own boot will create this
|
|
5098
|
-
this.
|
|
5404
|
+
this._createMaterial();
|
|
5099
5405
|
}
|
|
5100
5406
|
}
|
|
5101
|
-
|
|
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() {
|
|
5102
5415
|
const material = new StandardMaterial();
|
|
5103
5416
|
this.material = material;
|
|
5104
5417
|
material.alphaTest = this._alphaTest;
|
|
@@ -5225,9 +5538,9 @@ class MaterialElement extends HTMLElement {
|
|
|
5225
5538
|
* warning latches and reports once per episode, clearing when the clash is resolved.
|
|
5226
5539
|
*/
|
|
5227
5540
|
_warnGlossConflict() {
|
|
5228
|
-
const quote = (names) => `'${names.join('
|
|
5229
|
-
const roughness = roughnessAliases.filter(name => this.hasAttribute(name));
|
|
5230
|
-
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));
|
|
5231
5544
|
if (roughness.length === 0 || gloss.length === 0) {
|
|
5232
5545
|
this._glossConflictWarned = false;
|
|
5233
5546
|
return;
|
|
@@ -5245,7 +5558,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5245
5558
|
* @param id - The id of the `pc-asset`, or an empty string to clear the slot.
|
|
5246
5559
|
* @param slot - The material property to write.
|
|
5247
5560
|
*/
|
|
5248
|
-
|
|
5561
|
+
_setMap(id, slot) {
|
|
5249
5562
|
// Drop any load still pending for this slot - its texture is no longer the one we want
|
|
5250
5563
|
this._mapHandles.get(slot)?.off();
|
|
5251
5564
|
this._mapHandles.delete(slot);
|
|
@@ -5339,7 +5652,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5339
5652
|
*/
|
|
5340
5653
|
set aoMap(value) {
|
|
5341
5654
|
this._aoMap = value;
|
|
5342
|
-
this.
|
|
5655
|
+
this._setMap(value, 'aoMap');
|
|
5343
5656
|
}
|
|
5344
5657
|
/**
|
|
5345
5658
|
* Gets the id of the `pc-asset` used as the ambient occlusion map.
|
|
@@ -5571,7 +5884,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5571
5884
|
*/
|
|
5572
5885
|
set diffuseMap(value) {
|
|
5573
5886
|
this._diffuseMap = value;
|
|
5574
|
-
this.
|
|
5887
|
+
this._setMap(value, 'diffuseMap');
|
|
5575
5888
|
}
|
|
5576
5889
|
/**
|
|
5577
5890
|
* Gets the id of the `pc-asset` used as the diffuse map.
|
|
@@ -5712,7 +6025,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5712
6025
|
*/
|
|
5713
6026
|
set emissiveMap(value) {
|
|
5714
6027
|
this._emissiveMap = value;
|
|
5715
|
-
this.
|
|
6028
|
+
this._setMap(value, 'emissiveMap');
|
|
5716
6029
|
}
|
|
5717
6030
|
/**
|
|
5718
6031
|
* Gets the id of the `pc-asset` used as the emissive map.
|
|
@@ -5890,7 +6203,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5890
6203
|
*/
|
|
5891
6204
|
set glossMap(value) {
|
|
5892
6205
|
this._glossMap = value;
|
|
5893
|
-
this.
|
|
6206
|
+
this._setMap(value, 'glossMap');
|
|
5894
6207
|
}
|
|
5895
6208
|
/**
|
|
5896
6209
|
* Gets the id of the `pc-asset` used as the gloss map.
|
|
@@ -5995,7 +6308,7 @@ class MaterialElement extends HTMLElement {
|
|
|
5995
6308
|
*/
|
|
5996
6309
|
set heightMap(value) {
|
|
5997
6310
|
this._heightMap = value;
|
|
5998
|
-
this.
|
|
6311
|
+
this._setMap(value, 'heightMap');
|
|
5999
6312
|
}
|
|
6000
6313
|
/**
|
|
6001
6314
|
* Gets the id of the `pc-asset` used as the height map.
|
|
@@ -6136,7 +6449,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6136
6449
|
*/
|
|
6137
6450
|
set metalnessMap(value) {
|
|
6138
6451
|
this._metalnessMap = value;
|
|
6139
|
-
this.
|
|
6452
|
+
this._setMap(value, 'metalnessMap');
|
|
6140
6453
|
}
|
|
6141
6454
|
/**
|
|
6142
6455
|
* Gets the id of the `pc-asset` used as the metalness map.
|
|
@@ -6241,7 +6554,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6241
6554
|
*/
|
|
6242
6555
|
set normalMap(value) {
|
|
6243
6556
|
this._normalMap = value;
|
|
6244
|
-
this.
|
|
6557
|
+
this._setMap(value, 'normalMap');
|
|
6245
6558
|
}
|
|
6246
6559
|
/**
|
|
6247
6560
|
* Gets the id of the `pc-asset` used as the normal map.
|
|
@@ -6329,7 +6642,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6329
6642
|
set occludeDirect(value) {
|
|
6330
6643
|
this._occludeDirect = value;
|
|
6331
6644
|
if (this.material) {
|
|
6332
|
-
// @ts-ignore see
|
|
6645
|
+
// @ts-ignore see _createMaterial() - the engine mistypes occludeDirect as a number
|
|
6333
6646
|
this.material.occludeDirect = value;
|
|
6334
6647
|
this._scheduleUpdate();
|
|
6335
6648
|
}
|
|
@@ -6421,7 +6734,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6421
6734
|
*/
|
|
6422
6735
|
set opacityMap(value) {
|
|
6423
6736
|
this._opacityMap = value;
|
|
6424
|
-
this.
|
|
6737
|
+
this._setMap(value, 'opacityMap');
|
|
6425
6738
|
}
|
|
6426
6739
|
/**
|
|
6427
6740
|
* Gets the id of the `pc-asset` used as the opacity map.
|
|
@@ -6739,6 +7052,13 @@ class MaterialElement extends HTMLElement {
|
|
|
6739
7052
|
get useTonemap() {
|
|
6740
7053
|
return this._useTonemap;
|
|
6741
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
|
+
*/
|
|
6742
7062
|
static get(id) {
|
|
6743
7063
|
const materialElement = document.querySelector(`pc-material[id="${id}"]`);
|
|
6744
7064
|
return materialElement?.material;
|
|
@@ -7377,7 +7697,18 @@ class RigidBodyComponentElement extends ComponentElement {
|
|
|
7377
7697
|
return this._type;
|
|
7378
7698
|
}
|
|
7379
7699
|
static get observedAttributes() {
|
|
7380
|
-
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
|
+
];
|
|
7381
7712
|
}
|
|
7382
7713
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
7383
7714
|
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
@@ -7414,6 +7745,14 @@ class RigidBodyComponentElement extends ComponentElement {
|
|
|
7414
7745
|
}
|
|
7415
7746
|
customElements.define('pc-rigidbody', RigidBodyComponentElement);
|
|
7416
7747
|
|
|
7748
|
+
// The engine's SCALEMODE_* constants are the strings 'none' and 'blend', so this map happens to be
|
|
7749
|
+
// an identity. It is still the right shape: it supplies parseEnum's valid-name list, it is what the
|
|
7750
|
+
// manifest generator reads the enum values from, and it keeps the attribute vocabulary independent
|
|
7751
|
+
// of constants the engine is free to change.
|
|
7752
|
+
const scaleModes = new Map([
|
|
7753
|
+
['none', SCALEMODE_NONE],
|
|
7754
|
+
['blend', SCALEMODE_BLEND]
|
|
7755
|
+
]);
|
|
7417
7756
|
/**
|
|
7418
7757
|
* The ScreenComponentElement interface provides properties and methods for manipulating
|
|
7419
7758
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-screen/ | `<pc-screen>`} elements.
|
|
@@ -7427,7 +7766,7 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
7427
7766
|
_resolution = new Vec2(640, 320);
|
|
7428
7767
|
_referenceResolution = new Vec2(640, 320);
|
|
7429
7768
|
_priority = 0;
|
|
7430
|
-
|
|
7769
|
+
_scaleMode = 'none';
|
|
7431
7770
|
_scaleBlend = 0.5;
|
|
7432
7771
|
/** @ignore */
|
|
7433
7772
|
constructor() {
|
|
@@ -7439,7 +7778,7 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
7439
7778
|
referenceResolution: this._referenceResolution,
|
|
7440
7779
|
resolution: this._resolution,
|
|
7441
7780
|
scaleBlend: this._scaleBlend,
|
|
7442
|
-
scaleMode: this.
|
|
7781
|
+
scaleMode: scaleModes.get(this._scaleMode) ?? SCALEMODE_NONE,
|
|
7443
7782
|
screenSpace: this._screenSpace
|
|
7444
7783
|
};
|
|
7445
7784
|
}
|
|
@@ -7477,23 +7816,44 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
7477
7816
|
get resolution() {
|
|
7478
7817
|
return this._resolution;
|
|
7479
7818
|
}
|
|
7819
|
+
/**
|
|
7820
|
+
* Sets how the screen's `resolution` and `referenceResolution` are weighted against each other
|
|
7821
|
+
* when `scaleMode` is `blend`, from 0 (follow the resolution) to 1 (follow the reference
|
|
7822
|
+
* resolution). Ignored while `scaleMode` is `none`.
|
|
7823
|
+
* @param value - The scale blend factor.
|
|
7824
|
+
*/
|
|
7480
7825
|
set scaleBlend(value) {
|
|
7481
7826
|
this._scaleBlend = value;
|
|
7482
7827
|
if (this.component) {
|
|
7483
7828
|
this.component.scaleBlend = this._scaleBlend;
|
|
7484
7829
|
}
|
|
7485
7830
|
}
|
|
7831
|
+
/**
|
|
7832
|
+
* Gets how the screen's resolutions are weighted against each other.
|
|
7833
|
+
* @returns The scale blend factor.
|
|
7834
|
+
*/
|
|
7486
7835
|
get scaleBlend() {
|
|
7487
7836
|
return this._scaleBlend;
|
|
7488
7837
|
}
|
|
7489
|
-
|
|
7490
|
-
|
|
7838
|
+
/**
|
|
7839
|
+
* Sets how the screen scales its contents. `none` renders at `resolution` and ignores
|
|
7840
|
+
* `referenceResolution`; `blend` scales between the two, weighted by `scaleBlend`, which is what
|
|
7841
|
+
* keeps a UI laid out at one resolution usable at another. Requires `screenSpace` - the engine
|
|
7842
|
+
* forces `none` on a world-space screen, which does not support scaling.
|
|
7843
|
+
* @param value - The scale mode ('none' or 'blend').
|
|
7844
|
+
*/
|
|
7845
|
+
set scaleMode(value) {
|
|
7846
|
+
this._scaleMode = value;
|
|
7491
7847
|
if (this.component) {
|
|
7492
|
-
this.component.scaleMode =
|
|
7848
|
+
this.component.scaleMode = scaleModes.get(value) ?? SCALEMODE_NONE;
|
|
7493
7849
|
}
|
|
7494
7850
|
}
|
|
7495
|
-
|
|
7496
|
-
|
|
7851
|
+
/**
|
|
7852
|
+
* Gets how the screen scales its contents.
|
|
7853
|
+
* @returns The scale mode.
|
|
7854
|
+
*/
|
|
7855
|
+
get scaleMode() {
|
|
7856
|
+
return this._scaleMode;
|
|
7497
7857
|
}
|
|
7498
7858
|
set screenSpace(value) {
|
|
7499
7859
|
this._screenSpace = value;
|
|
@@ -7507,12 +7867,12 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
7507
7867
|
static get observedAttributes() {
|
|
7508
7868
|
return [
|
|
7509
7869
|
...super.observedAttributes,
|
|
7510
|
-
'blend',
|
|
7511
7870
|
'screen-space',
|
|
7512
7871
|
'resolution',
|
|
7513
7872
|
'reference-resolution',
|
|
7514
7873
|
'priority',
|
|
7515
|
-
'scale-blend'
|
|
7874
|
+
'scale-blend',
|
|
7875
|
+
'scale-mode'
|
|
7516
7876
|
];
|
|
7517
7877
|
}
|
|
7518
7878
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
@@ -7530,8 +7890,8 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
7530
7890
|
case 'scale-blend':
|
|
7531
7891
|
this.scaleBlend = parseNumber(newValue, 0.5, name);
|
|
7532
7892
|
break;
|
|
7533
|
-
case '
|
|
7534
|
-
this.
|
|
7893
|
+
case 'scale-mode':
|
|
7894
|
+
this.scaleMode = parseEnum(newValue, scaleModes, 'none', name);
|
|
7535
7895
|
break;
|
|
7536
7896
|
case 'screen-space':
|
|
7537
7897
|
this.screenSpace = parseBool(newValue, false);
|
|
@@ -7653,13 +8013,7 @@ class ScrollbarComponentElement extends ComponentElement {
|
|
|
7653
8013
|
return this._handle;
|
|
7654
8014
|
}
|
|
7655
8015
|
static get observedAttributes() {
|
|
7656
|
-
return [
|
|
7657
|
-
...super.observedAttributes,
|
|
7658
|
-
'orientation',
|
|
7659
|
-
'value',
|
|
7660
|
-
'handle-size',
|
|
7661
|
-
'handle'
|
|
7662
|
-
];
|
|
8016
|
+
return [...super.observedAttributes, 'orientation', 'value', 'handle-size', 'handle'];
|
|
7663
8017
|
}
|
|
7664
8018
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
7665
8019
|
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
@@ -7754,7 +8108,8 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
7754
8108
|
return super.component;
|
|
7755
8109
|
}
|
|
7756
8110
|
/**
|
|
7757
|
-
* Sets whether horizontal
|
|
8111
|
+
* Sets whether scrolling along the horizontal axis is enabled. This is a toggle, unlike the
|
|
8112
|
+
* `orientation` of a `<pc-scrollbar>`, for which `horizontal` is one of the accepted values.
|
|
7758
8113
|
* @param value - Whether horizontal scrolling is enabled.
|
|
7759
8114
|
*/
|
|
7760
8115
|
set horizontal(value) {
|
|
@@ -7764,14 +8119,15 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
7764
8119
|
}
|
|
7765
8120
|
}
|
|
7766
8121
|
/**
|
|
7767
|
-
* Gets whether horizontal
|
|
8122
|
+
* Gets whether scrolling along the horizontal axis is enabled.
|
|
7768
8123
|
* @returns Whether horizontal scrolling is enabled.
|
|
7769
8124
|
*/
|
|
7770
8125
|
get horizontal() {
|
|
7771
8126
|
return this._horizontal;
|
|
7772
8127
|
}
|
|
7773
8128
|
/**
|
|
7774
|
-
* Sets whether vertical
|
|
8129
|
+
* Sets whether scrolling along the vertical axis is enabled. This is a toggle, unlike the
|
|
8130
|
+
* `orientation` of a `<pc-scrollbar>`, for which `vertical` is one of the accepted values.
|
|
7775
8131
|
* @param value - Whether vertical scrolling is enabled.
|
|
7776
8132
|
*/
|
|
7777
8133
|
set vertical(value) {
|
|
@@ -7781,7 +8137,7 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
7781
8137
|
}
|
|
7782
8138
|
}
|
|
7783
8139
|
/**
|
|
7784
|
-
* Gets whether vertical
|
|
8140
|
+
* Gets whether scrolling along the vertical axis is enabled.
|
|
7785
8141
|
* @returns Whether vertical scrolling is enabled.
|
|
7786
8142
|
*/
|
|
7787
8143
|
get vertical() {
|
|
@@ -7883,7 +8239,8 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
7883
8239
|
set horizontalScrollbarVisibility(value) {
|
|
7884
8240
|
this._horizontalScrollbarVisibility = value;
|
|
7885
8241
|
if (this.component) {
|
|
7886
|
-
this.component.horizontalScrollbarVisibility =
|
|
8242
|
+
this.component.horizontalScrollbarVisibility =
|
|
8243
|
+
visibilities.get(value) ?? SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
|
|
7887
8244
|
}
|
|
7888
8245
|
}
|
|
7889
8246
|
/**
|
|
@@ -7901,7 +8258,8 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
7901
8258
|
set verticalScrollbarVisibility(value) {
|
|
7902
8259
|
this._verticalScrollbarVisibility = value;
|
|
7903
8260
|
if (this.component) {
|
|
7904
|
-
this.component.verticalScrollbarVisibility =
|
|
8261
|
+
this.component.verticalScrollbarVisibility =
|
|
8262
|
+
visibilities.get(value) ?? SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
|
|
7905
8263
|
}
|
|
7906
8264
|
}
|
|
7907
8265
|
/**
|
|
@@ -8088,15 +8446,9 @@ customElements.define('pc-scrollview', ScrollViewComponentElement);
|
|
|
8088
8446
|
class ScriptElement extends AsyncElement {
|
|
8089
8447
|
_attributes = {};
|
|
8090
8448
|
_enabled = true;
|
|
8091
|
-
/**
|
|
8092
|
-
* Whether readiness has been signalled. Creation can happen more than once over an
|
|
8093
|
-
* element's life (a runtime `name` change recreates the instance), but `ready` is a
|
|
8094
|
-
* one-shot signal, so only the first successful creation fires it.
|
|
8095
|
-
*/
|
|
8096
|
-
_readySignalled = false;
|
|
8097
8449
|
/**
|
|
8098
8450
|
* The Script instance created for this element by its parent `<pc-scripts>` element.
|
|
8099
|
-
* @
|
|
8451
|
+
* @internal
|
|
8100
8452
|
*/
|
|
8101
8453
|
_script = null;
|
|
8102
8454
|
/**
|
|
@@ -8178,14 +8530,20 @@ class ScriptElement extends AsyncElement {
|
|
|
8178
8530
|
console.warn(`pc-script '${this.getAttribute('name')}' must be a direct child of pc-scripts - script not created`);
|
|
8179
8531
|
}
|
|
8180
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
|
+
}
|
|
8181
8540
|
/**
|
|
8182
8541
|
* Called by the parent `<pc-scripts>` element when the script instance has been created.
|
|
8183
|
-
*
|
|
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
|
|
8184
8545
|
*/
|
|
8185
8546
|
_onScriptCreated() {
|
|
8186
|
-
if (this._readySignalled)
|
|
8187
|
-
return;
|
|
8188
|
-
this._readySignalled = true;
|
|
8189
8547
|
this._onReady();
|
|
8190
8548
|
}
|
|
8191
8549
|
static get observedAttributes() {
|
|
@@ -8230,10 +8588,34 @@ customElements.define('pc-script', ScriptElement);
|
|
|
8230
8588
|
*/
|
|
8231
8589
|
const RESERVED_ATTRIBUTES = new Set([
|
|
8232
8590
|
...ScriptElement.observedAttributes,
|
|
8233
|
-
'accesskey',
|
|
8234
|
-
'
|
|
8235
|
-
'
|
|
8236
|
-
'
|
|
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'
|
|
8237
8619
|
]);
|
|
8238
8620
|
/**
|
|
8239
8621
|
* Checks whether a `pc-script` attribute name is reserved (and so never maps to a script
|
|
@@ -8245,18 +8627,25 @@ const RESERVED_ATTRIBUTES = new Set([
|
|
|
8245
8627
|
* @returns Whether the attribute name is reserved.
|
|
8246
8628
|
*/
|
|
8247
8629
|
const isReservedAttribute = (name) => {
|
|
8248
|
-
return RESERVED_ATTRIBUTES.has(name) ||
|
|
8630
|
+
return (RESERVED_ATTRIBUTES.has(name) ||
|
|
8249
8631
|
name.startsWith('data-') ||
|
|
8250
8632
|
name.startsWith('aria-') ||
|
|
8251
8633
|
name.startsWith('_') ||
|
|
8252
|
-
(name.startsWith('on') && name in HTMLElement.prototype);
|
|
8634
|
+
(name.startsWith('on') && name in HTMLElement.prototype));
|
|
8253
8635
|
};
|
|
8254
8636
|
/**
|
|
8255
8637
|
* Script API members that per-property attributes must never overwrite: the engine bindings and
|
|
8256
8638
|
* the (optional, so possibly undefined) lifecycle methods.
|
|
8257
8639
|
*/
|
|
8258
8640
|
const SCRIPT_API_MEMBERS = new Set([
|
|
8259
|
-
'app',
|
|
8641
|
+
'app',
|
|
8642
|
+
'entity',
|
|
8643
|
+
'destroy',
|
|
8644
|
+
'initialize',
|
|
8645
|
+
'postInitialize',
|
|
8646
|
+
'postUpdate',
|
|
8647
|
+
'swap',
|
|
8648
|
+
'update'
|
|
8260
8649
|
]);
|
|
8261
8650
|
/**
|
|
8262
8651
|
* Converts a kebab-case attribute name to the camelCase script attribute name.
|
|
@@ -8272,7 +8661,7 @@ const kebabToCamel = (name) => {
|
|
|
8272
8661
|
* @returns The kebab-case name.
|
|
8273
8662
|
*/
|
|
8274
8663
|
const camelToKebab = (name) => {
|
|
8275
|
-
return name.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
|
|
8664
|
+
return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
|
8276
8665
|
};
|
|
8277
8666
|
/**
|
|
8278
8667
|
* Resolves an `asset:` prefix to the Asset created by the `pc-asset` element with that id.
|
|
@@ -8471,7 +8860,10 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
8471
8860
|
// Only recurse into plain objects. Class instances (Vec3, Color, Asset, Entity...)
|
|
8472
8861
|
// are leaf values assigned whole, so accessor-typed script attributes receive them
|
|
8473
8862
|
// through their setters instead of having a getter's returned copy mutated.
|
|
8474
|
-
if (value &&
|
|
8863
|
+
if (value &&
|
|
8864
|
+
typeof value === 'object' &&
|
|
8865
|
+
!Array.isArray(value) &&
|
|
8866
|
+
Object.getPrototypeOf(value) === Object.prototype) {
|
|
8475
8867
|
if (!current || typeof current !== 'object') {
|
|
8476
8868
|
target[key] = {};
|
|
8477
8869
|
}
|
|
@@ -8489,7 +8881,11 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
8489
8881
|
* @returns Whether the value is a math type.
|
|
8490
8882
|
*/
|
|
8491
8883
|
isMathType(value) {
|
|
8492
|
-
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);
|
|
8493
8889
|
}
|
|
8494
8890
|
/**
|
|
8495
8891
|
* Converts a plain numeric array to the math type of `current`. A 3-element array targeting
|
|
@@ -8502,7 +8898,7 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
8502
8898
|
* @returns The converted value, or `null`.
|
|
8503
8899
|
*/
|
|
8504
8900
|
arrayToMathType(current, value, key) {
|
|
8505
|
-
if (value.every(component => typeof component === 'number' && Number.isFinite(component))) {
|
|
8901
|
+
if (value.every((component) => typeof component === 'number' && Number.isFinite(component))) {
|
|
8506
8902
|
if (current instanceof Vec2 && value.length === 2)
|
|
8507
8903
|
return new Vec2(value);
|
|
8508
8904
|
if (current instanceof Vec3 && value.length === 3)
|
|
@@ -8774,7 +9170,10 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
8774
9170
|
mutation.removedNodes.forEach((node) => {
|
|
8775
9171
|
if (node instanceof ScriptElement) {
|
|
8776
9172
|
const scriptName = node.getAttribute('name');
|
|
8777
|
-
if (scriptName &&
|
|
9173
|
+
if (scriptName &&
|
|
9174
|
+
node._script &&
|
|
9175
|
+
this.component &&
|
|
9176
|
+
this.component.get(scriptName) === node._script) {
|
|
8778
9177
|
this.destroyScript(scriptName);
|
|
8779
9178
|
}
|
|
8780
9179
|
node._script = null;
|
|
@@ -9023,18 +9422,27 @@ class SoundSlotElement extends AsyncElement {
|
|
|
9023
9422
|
* emit a misleading "must be a direct child" warning for what is an ordinary removal.
|
|
9024
9423
|
*/
|
|
9025
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;
|
|
9026
9432
|
/**
|
|
9027
9433
|
* The sound slot.
|
|
9028
9434
|
*/
|
|
9029
9435
|
soundSlot = null;
|
|
9030
9436
|
async connectedCallback() {
|
|
9437
|
+
const generation = ++this._connectionGeneration;
|
|
9031
9438
|
const soundElement = this.soundElement;
|
|
9032
9439
|
await soundElement?.ready();
|
|
9033
|
-
// The element may have been removed
|
|
9034
|
-
//
|
|
9035
|
-
// 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.
|
|
9036
9444
|
const component = soundElement?.component;
|
|
9037
|
-
if (
|
|
9445
|
+
if (generation !== this._connectionGeneration || !component) {
|
|
9038
9446
|
return;
|
|
9039
9447
|
}
|
|
9040
9448
|
const options = {
|
|
@@ -9057,12 +9465,15 @@ class SoundSlotElement extends AsyncElement {
|
|
|
9057
9465
|
this._onReady();
|
|
9058
9466
|
}
|
|
9059
9467
|
disconnectedCallback() {
|
|
9468
|
+
// Invalidate any connectedCallback still suspended on an await
|
|
9469
|
+
this._connectionGeneration++;
|
|
9060
9470
|
// Uses the cached parent rather than a fresh lookup, since parentElement is already null
|
|
9061
9471
|
// by now. The component itself is null if the parent <pc-sound> (or the whole <pc-app>) is
|
|
9062
9472
|
// being torn down — parents disconnect first and have already removed the component.
|
|
9063
9473
|
this._soundElement?.component?.removeSlot(this._name);
|
|
9064
9474
|
this._soundElement = null;
|
|
9065
9475
|
this.soundSlot = null;
|
|
9476
|
+
this._resetReady();
|
|
9066
9477
|
}
|
|
9067
9478
|
get soundElement() {
|
|
9068
9479
|
const soundElement = this.parentElement;
|
|
@@ -9462,39 +9873,85 @@ customElements.define('pc-gsplat', GSplatComponentElement);
|
|
|
9462
9873
|
class ModelElement extends AsyncElement {
|
|
9463
9874
|
_asset = '';
|
|
9464
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
|
+
}
|
|
9465
9897
|
connectedCallback() {
|
|
9466
9898
|
this._loadModel();
|
|
9467
9899
|
this._onReady();
|
|
9468
9900
|
}
|
|
9469
9901
|
disconnectedCallback() {
|
|
9902
|
+
this._loadGeneration++;
|
|
9903
|
+
this._detachLoadHandler();
|
|
9470
9904
|
this._unloadModel();
|
|
9905
|
+
this._resetReady();
|
|
9906
|
+
}
|
|
9907
|
+
_detachLoadHandler() {
|
|
9908
|
+
this._loadHandle?.off();
|
|
9909
|
+
this._loadHandle = null;
|
|
9471
9910
|
}
|
|
9472
9911
|
_instantiate(container) {
|
|
9473
|
-
|
|
9912
|
+
const generation = this._loadGeneration;
|
|
9913
|
+
const entity = container.instantiateRenderEntity();
|
|
9914
|
+
this._entity = entity;
|
|
9474
9915
|
// @ts-ignore
|
|
9475
9916
|
if (container.animations.length > 0) {
|
|
9476
|
-
|
|
9917
|
+
entity.addComponent('anim');
|
|
9477
9918
|
// @ts-ignore
|
|
9478
|
-
|
|
9919
|
+
entity.anim.assignAnimation('animation', container.animations[0].resource);
|
|
9479
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.
|
|
9480
9924
|
const parentEntityElement = this.closestEntity;
|
|
9481
9925
|
if (parentEntityElement) {
|
|
9482
9926
|
parentEntityElement.ready().then(() => {
|
|
9483
|
-
|
|
9927
|
+
if (generation !== this._loadGeneration) {
|
|
9928
|
+
return;
|
|
9929
|
+
}
|
|
9930
|
+
parentEntityElement.entity.addChild(entity);
|
|
9484
9931
|
});
|
|
9485
9932
|
}
|
|
9486
9933
|
else {
|
|
9487
9934
|
const appElement = this.closestApp;
|
|
9488
9935
|
if (appElement) {
|
|
9489
9936
|
appElement.ready().then(() => {
|
|
9490
|
-
|
|
9937
|
+
if (generation !== this._loadGeneration) {
|
|
9938
|
+
return;
|
|
9939
|
+
}
|
|
9940
|
+
appElement.app.root.addChild(entity);
|
|
9491
9941
|
});
|
|
9492
9942
|
}
|
|
9493
9943
|
}
|
|
9494
9944
|
}
|
|
9495
9945
|
async _loadModel() {
|
|
9496
9946
|
this._unloadModel();
|
|
9947
|
+
// Supersede any load already in flight - only the newest load may instantiate
|
|
9948
|
+
const generation = ++this._loadGeneration;
|
|
9949
|
+
this._detachLoadHandler();
|
|
9497
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
|
+
}
|
|
9498
9955
|
const app = appElement?.app;
|
|
9499
9956
|
const asset = AssetElement.get(this._asset);
|
|
9500
9957
|
if (!asset) {
|
|
@@ -9504,7 +9961,14 @@ class ModelElement extends AsyncElement {
|
|
|
9504
9961
|
this._instantiate(asset.resource);
|
|
9505
9962
|
}
|
|
9506
9963
|
else {
|
|
9507
|
-
|
|
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
|
+
}
|
|
9508
9972
|
this._instantiate(asset.resource);
|
|
9509
9973
|
});
|
|
9510
9974
|
app.assets.load(asset);
|
|
@@ -9606,10 +10070,17 @@ class SceneElement extends AsyncElement {
|
|
|
9606
10070
|
return;
|
|
9607
10071
|
}
|
|
9608
10072
|
this._scene = app.scene;
|
|
9609
|
-
this.
|
|
10073
|
+
this._updateSceneSettings();
|
|
9610
10074
|
this._onReady();
|
|
9611
10075
|
}
|
|
9612
|
-
|
|
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() {
|
|
9613
10084
|
if (this._scene) {
|
|
9614
10085
|
this._scene.fog.type = this._fog;
|
|
9615
10086
|
this._scene.fog.color = this._fogColor;
|
|
@@ -9771,19 +10242,38 @@ class SkyElement extends AsyncElement {
|
|
|
9771
10242
|
_center = new Vec3(0, 0.01, 0);
|
|
9772
10243
|
_intensity = 1;
|
|
9773
10244
|
_rotation = new Vec3();
|
|
9774
|
-
|
|
10245
|
+
_mipLevel = 0;
|
|
9775
10246
|
_lighting = false;
|
|
9776
10247
|
_scale = new Vec3(100, 100, 100);
|
|
9777
10248
|
_type = 'infinite';
|
|
9778
10249
|
_scene = null;
|
|
9779
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;
|
|
9780
10263
|
connectedCallback() {
|
|
9781
10264
|
this._loadSkybox();
|
|
9782
10265
|
this._onReady();
|
|
9783
10266
|
}
|
|
9784
10267
|
disconnectedCallback() {
|
|
10268
|
+
this._loadGeneration++;
|
|
10269
|
+
this._detachLoadHandler();
|
|
9785
10270
|
this._unloadSkybox();
|
|
9786
10271
|
this._appElement = null;
|
|
10272
|
+
this._resetReady();
|
|
10273
|
+
}
|
|
10274
|
+
_detachLoadHandler() {
|
|
10275
|
+
this._loadHandle?.off();
|
|
10276
|
+
this._loadHandle = null;
|
|
9787
10277
|
}
|
|
9788
10278
|
_generateSkybox(asset) {
|
|
9789
10279
|
if (!this._scene)
|
|
@@ -9791,10 +10281,17 @@ class SkyElement extends AsyncElement {
|
|
|
9791
10281
|
const source = asset.resource;
|
|
9792
10282
|
const skybox = EnvLighting.generateSkyboxCubemap(source);
|
|
9793
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();
|
|
9794
10287
|
this._scene.skybox = skybox;
|
|
9795
10288
|
if (this._lighting) {
|
|
9796
10289
|
const lighting = EnvLighting.generateLightingSource(source);
|
|
9797
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();
|
|
9798
10295
|
this._scene.envAtlas = envAtlas;
|
|
9799
10296
|
}
|
|
9800
10297
|
const layer = this._scene.layers.getLayerById(LAYERID_SKYBOX);
|
|
@@ -9805,10 +10302,17 @@ class SkyElement extends AsyncElement {
|
|
|
9805
10302
|
this._scene.sky.node.setLocalScale(this._scale);
|
|
9806
10303
|
this._scene.sky.center = this._center;
|
|
9807
10304
|
this._scene.skyboxIntensity = this._intensity;
|
|
9808
|
-
this._scene.skyboxMip = this.
|
|
10305
|
+
this._scene.skyboxMip = this._mipLevel;
|
|
9809
10306
|
}
|
|
9810
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();
|
|
9811
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
|
+
}
|
|
9812
10316
|
const app = appElement?.app;
|
|
9813
10317
|
if (!appElement || !app) {
|
|
9814
10318
|
return;
|
|
@@ -9823,7 +10327,14 @@ class SkyElement extends AsyncElement {
|
|
|
9823
10327
|
this._generateSkybox(asset);
|
|
9824
10328
|
}
|
|
9825
10329
|
else {
|
|
9826
|
-
|
|
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
|
+
}
|
|
9827
10338
|
this._generateSkybox(asset);
|
|
9828
10339
|
});
|
|
9829
10340
|
app.assets.load(asset);
|
|
@@ -9897,23 +10408,6 @@ class SkyElement extends AsyncElement {
|
|
|
9897
10408
|
get intensity() {
|
|
9898
10409
|
return this._intensity;
|
|
9899
10410
|
}
|
|
9900
|
-
/**
|
|
9901
|
-
* Sets the mip level of the skybox.
|
|
9902
|
-
* @param value - The mip level.
|
|
9903
|
-
*/
|
|
9904
|
-
set level(value) {
|
|
9905
|
-
this._level = value;
|
|
9906
|
-
if (this._scene) {
|
|
9907
|
-
this._scene.skyboxMip = this._level;
|
|
9908
|
-
}
|
|
9909
|
-
}
|
|
9910
|
-
/**
|
|
9911
|
-
* Gets the mip level of the skybox.
|
|
9912
|
-
* @returns The mip level.
|
|
9913
|
-
*/
|
|
9914
|
-
get level() {
|
|
9915
|
-
return this._level;
|
|
9916
|
-
}
|
|
9917
10411
|
/**
|
|
9918
10412
|
* Sets whether the skybox is used as a light source.
|
|
9919
10413
|
* @param value - Whether to use lighting.
|
|
@@ -9928,6 +10422,24 @@ class SkyElement extends AsyncElement {
|
|
|
9928
10422
|
get lighting() {
|
|
9929
10423
|
return this._lighting;
|
|
9930
10424
|
}
|
|
10425
|
+
/**
|
|
10426
|
+
* Sets the mip level of the skybox, where 0 is the sharpest. Raising it selects a blurrier mip,
|
|
10427
|
+
* which is how a skybox is softened without blurring the texture itself.
|
|
10428
|
+
* @param value - The mip level.
|
|
10429
|
+
*/
|
|
10430
|
+
set mipLevel(value) {
|
|
10431
|
+
this._mipLevel = value;
|
|
10432
|
+
if (this._scene) {
|
|
10433
|
+
this._scene.skyboxMip = this._mipLevel;
|
|
10434
|
+
}
|
|
10435
|
+
}
|
|
10436
|
+
/**
|
|
10437
|
+
* Gets the mip level of the skybox.
|
|
10438
|
+
* @returns The mip level.
|
|
10439
|
+
*/
|
|
10440
|
+
get mipLevel() {
|
|
10441
|
+
return this._mipLevel;
|
|
10442
|
+
}
|
|
9931
10443
|
/**
|
|
9932
10444
|
* Sets the Euler rotation of the skybox.
|
|
9933
10445
|
* @param value - The rotation.
|
|
@@ -9984,7 +10496,7 @@ class SkyElement extends AsyncElement {
|
|
|
9984
10496
|
return this._type;
|
|
9985
10497
|
}
|
|
9986
10498
|
static get observedAttributes() {
|
|
9987
|
-
return ['asset', 'center', 'intensity', '
|
|
10499
|
+
return ['asset', 'center', 'intensity', 'lighting', 'mip-level', 'rotation', 'scale', 'type'];
|
|
9988
10500
|
}
|
|
9989
10501
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
9990
10502
|
switch (name) {
|
|
@@ -9997,12 +10509,12 @@ class SkyElement extends AsyncElement {
|
|
|
9997
10509
|
case 'intensity':
|
|
9998
10510
|
this.intensity = parseNumber(newValue, 1, name);
|
|
9999
10511
|
break;
|
|
10000
|
-
case 'level':
|
|
10001
|
-
this.level = parseNumber(newValue, 0, name);
|
|
10002
|
-
break;
|
|
10003
10512
|
case 'lighting':
|
|
10004
10513
|
this.lighting = parseBool(newValue, false);
|
|
10005
10514
|
break;
|
|
10515
|
+
case 'mip-level':
|
|
10516
|
+
this.mipLevel = parseNumber(newValue, 0, name);
|
|
10517
|
+
break;
|
|
10006
10518
|
case 'rotation':
|
|
10007
10519
|
this.rotation = parseVec3(newValue, Vec3.ZERO, name);
|
|
10008
10520
|
break;
|