@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.
Files changed (78) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +102 -51
  3. package/dist/asset.d.ts +8 -7
  4. package/dist/async-element.d.ts +21 -5
  5. package/dist/components/button-component.d.ts +3 -7
  6. package/dist/components/camera-component.d.ts +16 -20
  7. package/dist/components/collision-component.d.ts +3 -7
  8. package/dist/components/component.d.ts +22 -4
  9. package/dist/components/element-component.d.ts +4 -8
  10. package/dist/components/gsplat-component.d.ts +2 -7
  11. package/dist/components/layoutchild-component.d.ts +2 -7
  12. package/dist/components/layoutgroup-component.d.ts +3 -7
  13. package/dist/components/light-component.d.ts +3 -7
  14. package/dist/components/listener-component.d.ts +1 -6
  15. package/dist/components/particlesystem-component.d.ts +2 -7
  16. package/dist/components/render-component.d.ts +2 -7
  17. package/dist/components/rigidbody-component.d.ts +3 -7
  18. package/dist/components/screen-component.d.ts +28 -11
  19. package/dist/components/script-component.d.ts +8 -20
  20. package/dist/components/script.d.ts +2 -22
  21. package/dist/components/scrollbar-component.d.ts +2 -7
  22. package/dist/components/scrollview-component.d.ts +9 -11
  23. package/dist/components/sound-component.d.ts +2 -7
  24. package/dist/components/sound-slot.d.ts +8 -6
  25. package/dist/custom-elements.json +5191 -10695
  26. package/dist/entity.d.ts +16 -9
  27. package/dist/index.d.ts +37 -0
  28. package/dist/material.d.ts +12 -12
  29. package/dist/model.d.ts +21 -5
  30. package/dist/module.d.ts +0 -6
  31. package/dist/parse.d.ts +6 -3
  32. package/dist/pwc.cjs +799 -287
  33. package/dist/pwc.cjs.map +1 -1
  34. package/dist/pwc.js +799 -287
  35. package/dist/pwc.js.map +1 -1
  36. package/dist/pwc.min.js +1 -1
  37. package/dist/pwc.min.js.map +1 -1
  38. package/dist/pwc.min.mjs +1 -1
  39. package/dist/pwc.min.mjs.map +1 -1
  40. package/dist/pwc.mjs +800 -288
  41. package/dist/pwc.mjs.map +1 -1
  42. package/dist/scene.d.ts +4 -7
  43. package/dist/sky.d.ts +25 -16
  44. package/dist/vscode.html-custom-data.json +65 -45
  45. package/dist/web-types.json +585 -557
  46. package/package.json +8 -7
  47. package/src/app.ts +326 -144
  48. package/src/asset.ts +34 -28
  49. package/src/async-element.ts +34 -8
  50. package/src/components/button-component.ts +5 -9
  51. package/src/components/camera-component.ts +55 -36
  52. package/src/components/collision-component.ts +26 -15
  53. package/src/components/component.ts +58 -8
  54. package/src/components/element-component.ts +26 -30
  55. package/src/components/gsplat-component.ts +4 -9
  56. package/src/components/layoutchild-component.ts +4 -9
  57. package/src/components/layoutgroup-component.ts +14 -9
  58. package/src/components/light-component.ts +42 -12
  59. package/src/components/listener-component.ts +1 -7
  60. package/src/components/particlesystem-component.ts +7 -15
  61. package/src/components/render-component.ts +5 -10
  62. package/src/components/rigidbody-component.ts +23 -16
  63. package/src/components/screen-component.ts +46 -20
  64. package/src/components/script-component.ts +108 -46
  65. package/src/components/script.ts +38 -33
  66. package/src/components/scrollbar-component.ts +6 -16
  67. package/src/components/scrollview-component.ts +22 -15
  68. package/src/components/sound-component.ts +10 -15
  69. package/src/components/sound-slot.ts +30 -20
  70. package/src/entity.ts +75 -34
  71. package/src/index.ts +45 -1
  72. package/src/loading-bar.ts +8 -8
  73. package/src/material.ts +63 -37
  74. package/src/model.ts +69 -14
  75. package/src/module.ts +8 -7
  76. package/src/parse.ts +67 -20
  77. package/src/scene.ts +12 -9
  78. package/src/sky.ts +76 -34
package/src/model.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ContainerResource, Entity } from 'playcanvas';
1
+ import type { ContainerResource, Entity, EventHandle } from 'playcanvas';
2
2
 
3
3
  import { AssetElement } from './asset';
4
4
  import { AsyncElement } from './async-element';
@@ -10,39 +10,83 @@ import { AsyncElement } from './async-element';
10
10
  * {@link HTMLElement} interface.
11
11
  */
12
12
  class ModelElement extends AsyncElement {
13
- private _asset: string = '';
13
+ private _asset = '';
14
14
 
15
15
  private _entity: Entity | null = null;
16
16
 
17
+ /**
18
+ * Incremented on every new load and on disconnect, and captured by a load when it starts. A
19
+ * load that resumes from an await or a load callback abandons itself if the value has moved
20
+ * on, so a superseded load can neither instantiate a second entity nor parent one that has
21
+ * since been destroyed.
22
+ */
23
+ private _loadGeneration = 0;
24
+
25
+ /**
26
+ * The pending asset-load subscription of the current load, if it is waiting for its asset.
27
+ * Held so that whatever supersedes the load can detach the handler from the asset, rather
28
+ * than leave it registered until the asset loads (or forever, if it never does).
29
+ */
30
+ private _loadHandle: EventHandle | null = null;
31
+
32
+ /**
33
+ * The root entity of the instantiated model. `null` until the container asset has loaded
34
+ * and been instantiated, and again once the element has been removed from the document.
35
+ * @returns The model's root entity, or `null`.
36
+ */
37
+ get entity(): Entity | null {
38
+ return this._entity;
39
+ }
40
+
17
41
  connectedCallback() {
18
42
  this._loadModel();
19
43
  this._onReady();
20
44
  }
21
45
 
22
46
  disconnectedCallback() {
47
+ this._loadGeneration++;
48
+ this._detachLoadHandler();
23
49
  this._unloadModel();
50
+ this._resetReady();
51
+ }
52
+
53
+ private _detachLoadHandler() {
54
+ this._loadHandle?.off();
55
+ this._loadHandle = null;
24
56
  }
25
57
 
26
58
  private _instantiate(container: ContainerResource) {
27
- this._entity = container.instantiateRenderEntity();
59
+ const generation = this._loadGeneration;
60
+
61
+ const entity = container.instantiateRenderEntity();
62
+ this._entity = entity;
28
63
 
29
64
  // @ts-ignore
30
65
  if (container.animations.length > 0) {
31
- this._entity.addComponent('anim');
66
+ entity.addComponent('anim');
32
67
  // @ts-ignore
33
- this._entity.anim.assignAnimation('animation', container.animations[0].resource);
68
+ entity.anim.assignAnimation('animation', container.animations[0].resource);
34
69
  }
35
70
 
71
+ // The parent's readiness re-arms when it is torn down, so these can resume in a later
72
+ // connection cycle. The entity is captured above and the generation re-checked, so a
73
+ // stale resume cannot parent an entity a newer cycle has already destroyed.
36
74
  const parentEntityElement = this.closestEntity;
37
75
  if (parentEntityElement) {
38
76
  parentEntityElement.ready().then(() => {
39
- parentEntityElement.entity!.addChild(this._entity!);
77
+ if (generation !== this._loadGeneration) {
78
+ return;
79
+ }
80
+ parentEntityElement.entity!.addChild(entity);
40
81
  });
41
82
  } else {
42
83
  const appElement = this.closestApp;
43
84
  if (appElement) {
44
85
  appElement.ready().then(() => {
45
- appElement.app!.root.addChild(this._entity!);
86
+ if (generation !== this._loadGeneration) {
87
+ return;
88
+ }
89
+ appElement.app!.root.addChild(entity);
46
90
  });
47
91
  }
48
92
  }
@@ -51,7 +95,17 @@ class ModelElement extends AsyncElement {
51
95
  private async _loadModel() {
52
96
  this._unloadModel();
53
97
 
98
+ // Supersede any load already in flight - only the newest load may instantiate
99
+ const generation = ++this._loadGeneration;
100
+ this._detachLoadHandler();
101
+
54
102
  const appElement = await this.closestApp?.ready();
103
+
104
+ // The element may have been removed, or another load started, while we waited
105
+ if (generation !== this._loadGeneration) {
106
+ return;
107
+ }
108
+
55
109
  const app = appElement?.app;
56
110
 
57
111
  const asset = AssetElement.get(this._asset);
@@ -62,7 +116,14 @@ class ModelElement extends AsyncElement {
62
116
  if (asset.loaded) {
63
117
  this._instantiate(asset.resource as ContainerResource);
64
118
  } else {
65
- asset.once('load', () => {
119
+ // The generation is re-checked even though a superseded handler is detached: the
120
+ // detach relies on how the engine's event emitter treats removal, while the check
121
+ // holds on its own.
122
+ this._loadHandle = asset.once('load', () => {
123
+ this._loadHandle = null;
124
+ if (generation !== this._loadGeneration) {
125
+ return;
126
+ }
66
127
  this._instantiate(asset.resource as ContainerResource);
67
128
  });
68
129
  app!.assets.load(asset);
@@ -108,10 +169,4 @@ class ModelElement extends AsyncElement {
108
169
 
109
170
  customElements.define('pc-model', ModelElement);
110
171
 
111
- declare global {
112
- interface HTMLElementTagNameMap {
113
- 'pc-model': ModelElement;
114
- }
115
- }
116
-
117
172
  export { ModelElement };
package/src/module.ts CHANGED
@@ -43,17 +43,18 @@ class ModuleElement extends HTMLElement {
43
43
  }
44
44
  }
45
45
 
46
- public getLoadPromise(): Promise<void> {
46
+ /**
47
+ * Returns the promise that settles when the module has loaded. Awaited by the containing
48
+ * `<pc-app>` element before it creates its graphics device.
49
+ *
50
+ * @returns The load promise.
51
+ * @internal
52
+ */
53
+ _getLoadPromise(): Promise<void> {
47
54
  return this.loadPromise;
48
55
  }
49
56
  }
50
57
 
51
58
  customElements.define('pc-module', ModuleElement);
52
59
 
53
- declare global {
54
- interface HTMLElementTagNameMap {
55
- 'pc-module': ModuleElement;
56
- }
57
- }
58
-
59
60
  export { ModuleElement };
package/src/parse.ts CHANGED
@@ -16,7 +16,8 @@
16
16
  * literal, and returns `null` instead of falling back to a default.
17
17
  */
18
18
 
19
- import { Color, Entity, Quat, Vec2, Vec3, Vec4 } from 'playcanvas';
19
+ import type { Entity } from 'playcanvas';
20
+ import { Color, Quat, Vec2, Vec3, Vec4 } from 'playcanvas';
20
21
 
21
22
  import { CSS_COLORS } from './colors';
22
23
 
@@ -31,7 +32,7 @@ import { CSS_COLORS } from './colors';
31
32
  */
32
33
  export const parseComponents = (value: string, count: number): number[] | null => {
33
34
  const components = value.trim().split(/\s+/).map(Number);
34
- if (components.length !== count || components.some(component => !Number.isFinite(component))) {
35
+ if (components.length !== count || components.some((component) => !Number.isFinite(component))) {
35
36
  return null;
36
37
  }
37
38
  return components;
@@ -77,7 +78,11 @@ export const parseBool = (value: string | null, defaultValue: boolean): boolean
77
78
  * @param attribute - The attribute name, used in the warning message.
78
79
  * @returns The parsed Color object.
79
80
  */
80
- export const parseColor = <T extends Color | null>(value: string | null, defaultValue: T, attribute: string): Color | T => {
81
+ export const parseColor = <T extends Color | null>(
82
+ value: string | null,
83
+ defaultValue: T,
84
+ attribute: string
85
+ ): Color | T => {
81
86
  if (value === null) {
82
87
  return cloneDefault(defaultValue);
83
88
  }
@@ -92,7 +97,10 @@ export const parseColor = <T extends Color | null>(value: string | null, default
92
97
  if (/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)) {
93
98
  let hex = value.slice(1);
94
99
  if (hex.length === 3 || hex.length === 4) {
95
- hex = hex.split('').map(char => char + char).join('');
100
+ hex = hex
101
+ .split('')
102
+ .map((char) => char + char)
103
+ .join('');
96
104
  }
97
105
  return new Color().fromString(`#${hex}`);
98
106
  }
@@ -103,7 +111,9 @@ export const parseColor = <T extends Color | null>(value: string | null, default
103
111
  return new Color(components);
104
112
  }
105
113
 
106
- console.warn(`Invalid value '${value}' for attribute '${attribute}'. Expected a CSS color name, a hex color or 3 or 4 space-separated numbers. Using '${defaultValue}'.`);
114
+ console.warn(
115
+ `Invalid value '${value}' for attribute '${attribute}'. Expected a CSS color name, a hex color or 3 or 4 space-separated numbers. Using '${defaultValue}'.`
116
+ );
107
117
  return cloneDefault(defaultValue);
108
118
  };
109
119
 
@@ -113,25 +123,29 @@ export const parseColor = <T extends Color | null>(value: string | null, default
113
123
  * the value is invalid — the latter also logs a warning listing the valid names.
114
124
  *
115
125
  * @param value - The attribute value to parse (`null` when the attribute is absent).
116
- * @param valid - The valid names: an array, or a map whose keys are the valid names.
126
+ * @param valid - The valid names: an array, or a map whose keys are the valid names. Only the keys
127
+ * are read, so the map's value type is unconstrained - engine enums are mostly numeric constants,
128
+ * but some (e.g. `SCALEMODE_BLEND`) are strings.
117
129
  * @param defaultValue - The value to use when the attribute is absent or invalid.
118
130
  * @param attribute - The attribute name, used in the warning message.
119
131
  * @returns The resolved enum name.
120
132
  */
121
133
  export const parseEnum = <T extends string>(
122
134
  value: string | null,
123
- valid: readonly T[] | ReadonlyMap<T, number>,
135
+ valid: readonly T[] | ReadonlyMap<T, unknown>,
124
136
  defaultValue: T,
125
137
  attribute: string
126
138
  ): T => {
127
139
  if (value === null) {
128
140
  return defaultValue;
129
141
  }
130
- const names = Array.isArray(valid) ? valid : [...(valid as ReadonlyMap<T, number>).keys()];
142
+ const names: readonly T[] = Array.isArray(valid) ? valid : [...valid.keys()];
131
143
  if (names.includes(value as T)) {
132
144
  return value as T;
133
145
  }
134
- console.warn(`Invalid value '${value}' for attribute '${attribute}'. Valid values: ${names.join(', ')}. Using '${defaultValue}'.`);
146
+ console.warn(
147
+ `Invalid value '${value}' for attribute '${attribute}'. Valid values: ${names.join(', ')}. Using '${defaultValue}'.`
148
+ );
135
149
  return defaultValue;
136
150
  };
137
151
 
@@ -145,13 +159,19 @@ export const parseEnum = <T extends string>(
145
159
  * @param attribute - The attribute name, used in the warning message.
146
160
  * @returns The parsed number.
147
161
  */
148
- export const parseNumber = <T extends number | null>(value: string | null, defaultValue: T, attribute: string): number | T => {
162
+ export const parseNumber = <T extends number | null>(
163
+ value: string | null,
164
+ defaultValue: T,
165
+ attribute: string
166
+ ): number | T => {
149
167
  if (value === null) {
150
168
  return defaultValue;
151
169
  }
152
170
  const number = value.trim() === '' ? NaN : Number(value);
153
171
  if (!Number.isFinite(number)) {
154
- console.warn(`Invalid value '${value}' for attribute '${attribute}'. Expected a finite number. Using '${defaultValue}'.`);
172
+ console.warn(
173
+ `Invalid value '${value}' for attribute '${attribute}'. Expected a finite number. Using '${defaultValue}'.`
174
+ );
155
175
  return defaultValue;
156
176
  }
157
177
  return number;
@@ -168,13 +188,19 @@ export const parseNumber = <T extends number | null>(value: string | null, defau
168
188
  * @param attribute - The attribute name, used in the warning message.
169
189
  * @returns The parsed Quat object.
170
190
  */
171
- export const parseQuat = <T extends Quat | null>(value: string | null, defaultValue: T, attribute: string): Quat | T => {
191
+ export const parseQuat = <T extends Quat | null>(
192
+ value: string | null,
193
+ defaultValue: T,
194
+ attribute: string
195
+ ): Quat | T => {
172
196
  if (value === null) {
173
197
  return cloneDefault(defaultValue);
174
198
  }
175
199
  const components = parseComponents(value, 3);
176
200
  if (!components) {
177
- console.warn(`Invalid value '${value}' for attribute '${attribute}'. Expected 3 space-separated numbers. Using '${defaultValue}'.`);
201
+ console.warn(
202
+ `Invalid value '${value}' for attribute '${attribute}'. Expected 3 space-separated numbers. Using '${defaultValue}'.`
203
+ );
178
204
  return cloneDefault(defaultValue);
179
205
  }
180
206
  return new Quat().setFromEulerAngles(components[0], components[1], components[2]);
@@ -198,7 +224,10 @@ export const parseTags = (value: string | null, defaultValue: string[] = []): st
198
224
  // caller's default, or a later mutation would write back through it.
199
225
  return [...defaultValue];
200
226
  }
201
- return value.split(',').map(tag => tag.trim()).filter(tag => tag !== '');
227
+ return value
228
+ .split(',')
229
+ .map((tag) => tag.trim())
230
+ .filter((tag) => tag !== '');
202
231
  };
203
232
 
204
233
  /**
@@ -211,13 +240,19 @@ export const parseTags = (value: string | null, defaultValue: string[] = []): st
211
240
  * @param attribute - The attribute name, used in the warning message.
212
241
  * @returns The parsed Vec2 object.
213
242
  */
214
- export const parseVec2 = <T extends Vec2 | null>(value: string | null, defaultValue: T, attribute: string): Vec2 | T => {
243
+ export const parseVec2 = <T extends Vec2 | null>(
244
+ value: string | null,
245
+ defaultValue: T,
246
+ attribute: string
247
+ ): Vec2 | T => {
215
248
  if (value === null) {
216
249
  return cloneDefault(defaultValue);
217
250
  }
218
251
  const components = parseComponents(value, 2);
219
252
  if (!components) {
220
- console.warn(`Invalid value '${value}' for attribute '${attribute}'. Expected 2 space-separated numbers. Using '${defaultValue}'.`);
253
+ console.warn(
254
+ `Invalid value '${value}' for attribute '${attribute}'. Expected 2 space-separated numbers. Using '${defaultValue}'.`
255
+ );
221
256
  return cloneDefault(defaultValue);
222
257
  }
223
258
  return new Vec2(components);
@@ -233,13 +268,19 @@ export const parseVec2 = <T extends Vec2 | null>(value: string | null, defaultVa
233
268
  * @param attribute - The attribute name, used in the warning message.
234
269
  * @returns The parsed Vec3 object.
235
270
  */
236
- export const parseVec3 = <T extends Vec3 | null>(value: string | null, defaultValue: T, attribute: string): Vec3 | T => {
271
+ export const parseVec3 = <T extends Vec3 | null>(
272
+ value: string | null,
273
+ defaultValue: T,
274
+ attribute: string
275
+ ): Vec3 | T => {
237
276
  if (value === null) {
238
277
  return cloneDefault(defaultValue);
239
278
  }
240
279
  const components = parseComponents(value, 3);
241
280
  if (!components) {
242
- console.warn(`Invalid value '${value}' for attribute '${attribute}'. Expected 3 space-separated numbers. Using '${defaultValue}'.`);
281
+ console.warn(
282
+ `Invalid value '${value}' for attribute '${attribute}'. Expected 3 space-separated numbers. Using '${defaultValue}'.`
283
+ );
243
284
  return cloneDefault(defaultValue);
244
285
  }
245
286
  return new Vec3(components);
@@ -255,13 +296,19 @@ export const parseVec3 = <T extends Vec3 | null>(value: string | null, defaultVa
255
296
  * @param attribute - The attribute name, used in the warning message.
256
297
  * @returns The parsed Vec4 object.
257
298
  */
258
- export const parseVec4 = <T extends Vec4 | null>(value: string | null, defaultValue: T, attribute: string): Vec4 | T => {
299
+ export const parseVec4 = <T extends Vec4 | null>(
300
+ value: string | null,
301
+ defaultValue: T,
302
+ attribute: string
303
+ ): Vec4 | T => {
259
304
  if (value === null) {
260
305
  return cloneDefault(defaultValue);
261
306
  }
262
307
  const components = parseComponents(value, 4);
263
308
  if (!components) {
264
- console.warn(`Invalid value '${value}' for attribute '${attribute}'. Expected 4 space-separated numbers. Using '${defaultValue}'.`);
309
+ console.warn(
310
+ `Invalid value '${value}' for attribute '${attribute}'. Expected 4 space-separated numbers. Using '${defaultValue}'.`
311
+ );
265
312
  return cloneDefault(defaultValue);
266
313
  }
267
314
  return new Vec4(components);
package/src/scene.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Color, Scene, Vec3 } from 'playcanvas';
1
+ import type { Scene } from 'playcanvas';
2
+ import { Color, Vec3 } from 'playcanvas';
2
3
 
3
4
  import { AsyncElement } from './async-element';
4
5
  import { parseColor, parseEnum, parseNumber, parseVec3 } from './parse';
@@ -77,12 +78,20 @@ class SceneElement extends AsyncElement {
77
78
  }
78
79
 
79
80
  this._scene = app.scene;
80
- this.updateSceneSettings();
81
+ this._updateSceneSettings();
81
82
 
82
83
  this._onReady();
83
84
  }
84
85
 
85
- updateSceneSettings() {
86
+ disconnectedCallback() {
87
+ // The scene belongs to the application, and removing this element - or the <pc-app>
88
+ // above it, which disconnects first - parts the two. Re-arm readiness so a re-inserted
89
+ // element announces the scene it acquires then, not the one it lost here.
90
+ this._scene = null;
91
+ this._resetReady();
92
+ }
93
+
94
+ private _updateSceneSettings() {
86
95
  if (this._scene) {
87
96
  this._scene.fog.type = this._fog;
88
97
  this._scene.fog.color = this._fogColor;
@@ -251,10 +260,4 @@ class SceneElement extends AsyncElement {
251
260
 
252
261
  customElements.define('pc-scene', SceneElement);
253
262
 
254
- declare global {
255
- interface HTMLElementTagNameMap {
256
- 'pc-scene': SceneElement;
257
- }
258
- }
259
-
260
263
  export { SceneElement };
package/src/sky.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { Asset, EnvLighting, LAYERID_SKYBOX, Quat, Scene, Texture, Vec3 } from 'playcanvas';
1
+ import type { Asset, EventHandle, Scene, Texture } from 'playcanvas';
2
+ import { EnvLighting, LAYERID_SKYBOX, Quat, Vec3 } from 'playcanvas';
2
3
 
3
- import { AppElement } from './app';
4
+ import type { AppElement } from './app';
4
5
  import { AssetElement } from './asset';
5
6
  import { AsyncElement } from './async-element';
6
7
  import { parseBool, parseEnum, parseNumber, parseVec3 } from './parse';
@@ -19,7 +20,7 @@ class SkyElement extends AsyncElement {
19
20
 
20
21
  private _rotation = new Vec3();
21
22
 
22
- private _level = 0;
23
+ private _mipLevel = 0;
23
24
 
24
25
  private _lighting = false;
25
26
 
@@ -31,14 +32,36 @@ class SkyElement extends AsyncElement {
31
32
 
32
33
  private _appElement: AppElement | null = null;
33
34
 
35
+ /**
36
+ * Incremented on every new load and on disconnect, and captured by a load when it starts. A
37
+ * load that resumes from an await or a load callback abandons itself if the value has moved
38
+ * on, so a superseded load cannot generate a skybox for a scene it no longer configures.
39
+ */
40
+ private _loadGeneration = 0;
41
+
42
+ /**
43
+ * The pending asset-load subscription of the current load, if it is waiting for its asset.
44
+ * Held so that whatever supersedes the load can detach the handler from the asset, rather
45
+ * than leave it registered until the asset loads (or forever, if it never does).
46
+ */
47
+ private _loadHandle: EventHandle | null = null;
48
+
34
49
  connectedCallback() {
35
50
  this._loadSkybox();
36
51
  this._onReady();
37
52
  }
38
53
 
39
54
  disconnectedCallback() {
55
+ this._loadGeneration++;
56
+ this._detachLoadHandler();
40
57
  this._unloadSkybox();
41
58
  this._appElement = null;
59
+ this._resetReady();
60
+ }
61
+
62
+ private _detachLoadHandler() {
63
+ this._loadHandle?.off();
64
+ this._loadHandle = null;
42
65
  }
43
66
 
44
67
  private _generateSkybox(asset: Asset) {
@@ -48,11 +71,18 @@ class SkyElement extends AsyncElement {
48
71
 
49
72
  const skybox = EnvLighting.generateSkyboxCubemap(source);
50
73
  skybox.anisotropy = 4;
74
+ // This element owns what it generated (see _unloadSkybox) - replacing a skybox from an
75
+ // earlier load must release it, not orphan it on the GPU
76
+ this._scene.skybox?.destroy();
51
77
  this._scene.skybox = skybox;
52
78
 
53
79
  if (this._lighting) {
54
80
  const lighting = EnvLighting.generateLightingSource(source);
55
81
  const envAtlas = EnvLighting.generateAtlas(lighting);
82
+ // The lighting source is an intermediate: the atlas is rendered from it and it is
83
+ // not needed afterwards
84
+ lighting.destroy();
85
+ this._scene.envAtlas?.destroy();
56
86
  this._scene.envAtlas = envAtlas;
57
87
  }
58
88
 
@@ -65,11 +95,21 @@ class SkyElement extends AsyncElement {
65
95
  this._scene.sky.node.setLocalScale(this._scale);
66
96
  this._scene.sky.center = this._center;
67
97
  this._scene.skyboxIntensity = this._intensity;
68
- this._scene.skyboxMip = this._level;
98
+ this._scene.skyboxMip = this._mipLevel;
69
99
  }
70
100
 
71
101
  private async _loadSkybox() {
102
+ // Supersede any load already in flight - only the newest load may generate the skybox
103
+ const generation = ++this._loadGeneration;
104
+ this._detachLoadHandler();
105
+
72
106
  const appElement = await this.closestApp?.ready();
107
+
108
+ // The element may have been removed, or another load started, while we waited
109
+ if (generation !== this._loadGeneration) {
110
+ return;
111
+ }
112
+
73
113
  const app = appElement?.app;
74
114
  if (!appElement || !app) {
75
115
  return;
@@ -87,7 +127,14 @@ class SkyElement extends AsyncElement {
87
127
  if (asset.loaded) {
88
128
  this._generateSkybox(asset);
89
129
  } else {
90
- asset.once('load', () => {
130
+ // The generation is re-checked even though a superseded handler is detached: the
131
+ // detach relies on how the engine's event emitter treats removal, while the check
132
+ // holds on its own.
133
+ this._loadHandle = asset.once('load', () => {
134
+ this._loadHandle = null;
135
+ if (generation !== this._loadGeneration) {
136
+ return;
137
+ }
91
138
  this._generateSkybox(asset);
92
139
  });
93
140
  app.assets.load(asset);
@@ -170,25 +217,6 @@ class SkyElement extends AsyncElement {
170
217
  return this._intensity;
171
218
  }
172
219
 
173
- /**
174
- * Sets the mip level of the skybox.
175
- * @param value - The mip level.
176
- */
177
- set level(value: number) {
178
- this._level = value;
179
- if (this._scene) {
180
- this._scene.skyboxMip = this._level;
181
- }
182
- }
183
-
184
- /**
185
- * Gets the mip level of the skybox.
186
- * @returns The mip level.
187
- */
188
- get level() {
189
- return this._level;
190
- }
191
-
192
220
  /**
193
221
  * Sets whether the skybox is used as a light source.
194
222
  * @param value - Whether to use lighting.
@@ -205,6 +233,26 @@ class SkyElement extends AsyncElement {
205
233
  return this._lighting;
206
234
  }
207
235
 
236
+ /**
237
+ * Sets the mip level of the skybox, where 0 is the sharpest. Raising it selects a blurrier mip,
238
+ * which is how a skybox is softened without blurring the texture itself.
239
+ * @param value - The mip level.
240
+ */
241
+ set mipLevel(value: number) {
242
+ this._mipLevel = value;
243
+ if (this._scene) {
244
+ this._scene.skyboxMip = this._mipLevel;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Gets the mip level of the skybox.
250
+ * @returns The mip level.
251
+ */
252
+ get mipLevel() {
253
+ return this._mipLevel;
254
+ }
255
+
208
256
  /**
209
257
  * Sets the Euler rotation of the skybox.
210
258
  * @param value - The rotation.
@@ -267,7 +315,7 @@ class SkyElement extends AsyncElement {
267
315
  }
268
316
 
269
317
  static get observedAttributes() {
270
- return ['asset', 'center', 'intensity', 'level', 'lighting', 'rotation', 'scale', 'type'];
318
+ return ['asset', 'center', 'intensity', 'lighting', 'mip-level', 'rotation', 'scale', 'type'];
271
319
  }
272
320
 
273
321
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
@@ -281,12 +329,12 @@ class SkyElement extends AsyncElement {
281
329
  case 'intensity':
282
330
  this.intensity = parseNumber(newValue, 1, name);
283
331
  break;
284
- case 'level':
285
- this.level = parseNumber(newValue, 0, name);
286
- break;
287
332
  case 'lighting':
288
333
  this.lighting = parseBool(newValue, false);
289
334
  break;
335
+ case 'mip-level':
336
+ this.mipLevel = parseNumber(newValue, 0, name);
337
+ break;
290
338
  case 'rotation':
291
339
  this.rotation = parseVec3(newValue, Vec3.ZERO, name);
292
340
  break;
@@ -302,10 +350,4 @@ class SkyElement extends AsyncElement {
302
350
 
303
351
  customElements.define('pc-sky', SkyElement);
304
352
 
305
- declare global {
306
- interface HTMLElementTagNameMap {
307
- 'pc-sky': SkyElement;
308
- }
309
- }
310
-
311
353
  export { SkyElement };