@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/asset.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Asset, SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED } from 'playcanvas';
2
2
 
3
+ import { MeshoptDecoder } from '../lib/meshopt_decoder.module.js';
4
+
3
5
  import { AsyncElement } from './async-element';
4
6
  import { parseBool, parseEnum, parseNumber } from './parse';
5
- import { MeshoptDecoder } from '../lib/meshopt_decoder.module.js';
6
7
 
7
8
  const renderModes = new Map<'simple' | 'sliced' | 'tiled', number>([
8
9
  ['simple', SPRITE_RENDERMODE_SIMPLE],
@@ -33,12 +34,11 @@ const extToType = new Map([
33
34
  ['webp', 'texture']
34
35
  ]);
35
36
 
36
-
37
37
  // provide buffer view callback so we can handle models compressed with MeshOptimizer
38
38
  // https://github.com/zeux/meshoptimizer
39
39
  const processBufferView = (
40
40
  gltfBuffer: any,
41
- buffers: Array<any>,
41
+ buffers: any[],
42
42
  continuation: (err: string | null, result: any) => void
43
43
  ) => {
44
44
  if (gltfBuffer.extensions && gltfBuffer.extensions.EXT_meshopt_compression) {
@@ -56,14 +56,7 @@ const processBufferView = (
56
56
  const result = new Uint8Array(count * stride);
57
57
  const source = new Uint8Array(buffer.buffer, buffer.byteOffset + byteOffset, byteLength);
58
58
 
59
- MeshoptDecoder.decodeGltfBuffer(
60
- result,
61
- count,
62
- stride,
63
- source,
64
- extensionDef.mode,
65
- extensionDef.filter
66
- );
59
+ MeshoptDecoder.decodeGltfBuffer(result, count, stride, source, extensionDef.mode, extensionDef.filter);
67
60
 
68
61
  continuation(null, result);
69
62
  });
@@ -108,7 +101,7 @@ const processBufferView = (
108
101
  * not that it succeeded.
109
102
  */
110
103
  class AssetElement extends AsyncElement {
111
- private _lazy: boolean = false;
104
+ private _lazy = false;
112
105
 
113
106
  /**
114
107
  * The asset that is loaded. Available once the element is ready — await
@@ -122,7 +115,9 @@ class AssetElement extends AsyncElement {
122
115
 
123
116
  // Assets must be direct children of pc-app (matches the boot query ':scope > pc-asset')
124
117
  if (this.parentElement !== appElement) {
125
- console.warn(`pc-asset '${this.getAttribute('id') ?? this.getAttribute('src')}' must be a direct child of pc-app - asset not created`);
118
+ console.warn(
119
+ `pc-asset '${this.getAttribute('id') ?? this.getAttribute('src')}' must be a direct child of pc-app - asset not created`
120
+ );
126
121
  return;
127
122
  }
128
123
 
@@ -137,7 +132,7 @@ class AssetElement extends AsyncElement {
137
132
  const app = appElement.app;
138
133
  if (!app) return; // pc-app is re-connecting; its own boot will create this asset
139
134
 
140
- this.createAsset();
135
+ this._createAsset();
141
136
  if (this.asset) {
142
137
  app.assets.add(this.asset); // add() auto-loads when preload is true
143
138
  if (!this.lazy) {
@@ -146,14 +141,16 @@ class AssetElement extends AsyncElement {
146
141
  }
147
142
  }
148
143
 
149
- // Never ready if createAsset failed (unsupported asset type)
144
+ // Never ready if _createAsset failed (unsupported asset type)
150
145
  if (this.asset) {
151
146
  this._onReady();
152
147
  }
153
148
  }
154
149
 
155
150
  disconnectedCallback() {
156
- this.destroyAsset();
151
+ this._destroyAsset();
152
+ // Re-arm readiness so a re-inserted element announces the asset it creates then
153
+ this._resetReady();
157
154
  }
158
155
 
159
156
  private _onAssetLoad() {
@@ -161,12 +158,21 @@ class AssetElement extends AsyncElement {
161
158
  }
162
159
 
163
160
  private _onAssetError(err: string | Error) {
164
- this.dispatchEvent(new ErrorEvent('error', {
165
- message: err instanceof Error ? err.message : String(err)
166
- }));
161
+ this.dispatchEvent(
162
+ new ErrorEvent('error', {
163
+ message: err instanceof Error ? err.message : String(err)
164
+ })
165
+ );
167
166
  }
168
167
 
169
- createAsset() {
168
+ /**
169
+ * Creates the asset from the element's attributes. Called by the containing `<pc-app>`
170
+ * element during its boot sweep, and on connection for elements inserted while the
171
+ * application is already running.
172
+ *
173
+ * @internal
174
+ */
175
+ _createAsset() {
170
176
  const id = this.getAttribute('id') || '';
171
177
  const src = this.getAttribute('src') || '';
172
178
  let type = this.getAttribute('type');
@@ -268,8 +274,7 @@ class AssetElement extends AsyncElement {
268
274
  return data;
269
275
  }
270
276
 
271
-
272
- destroyAsset() {
277
+ private _destroyAsset() {
273
278
  if (this.asset) {
274
279
  // A caller that keeps the Asset alive must not dispatch on a removed element
275
280
  this.asset.off('load', this._onAssetLoad, this);
@@ -300,6 +305,13 @@ class AssetElement extends AsyncElement {
300
305
  return this._lazy;
301
306
  }
302
307
 
308
+ /**
309
+ * Returns the {@link Asset} created by the `<pc-asset>` element with the given `id`, or
310
+ * `undefined` if there is no such element or its asset has not been created yet.
311
+ *
312
+ * @param id - The `id` of the `<pc-asset>` element.
313
+ * @returns The asset, or `undefined`.
314
+ */
303
315
  static get(id: string) {
304
316
  const assetElement = document.querySelector<AssetElement>(`pc-asset[id="${id}"]`);
305
317
  return assetElement?.asset;
@@ -318,10 +330,4 @@ class AssetElement extends AsyncElement {
318
330
 
319
331
  customElements.define('pc-asset', AssetElement);
320
332
 
321
- declare global {
322
- interface HTMLElementTagNameMap {
323
- 'pc-asset': AssetElement;
324
- }
325
- }
326
-
327
333
  export { AssetElement };
@@ -1,17 +1,20 @@
1
- import { AppElement } from './app';
2
- import { EntityElement } from './entity';
1
+ import type { AppElement } from './app';
2
+ import type { EntityElement } from './entity';
3
3
 
4
4
  /**
5
5
  * Base class for all PlayCanvas Web Components that initialize asynchronously.
6
6
  *
7
- * @fires {CustomEvent} ready - Fired once the element is fully initialized. Bubbles and is
8
- * composed.
7
+ * @fires {CustomEvent} ready - Fired when the element is fully initialized once per readiness
8
+ * cycle, so an element that is torn down and re-initialized (for example by removing and
9
+ * re-inserting it) fires it again. Bubbles and is composed.
9
10
  */
10
11
  class AsyncElement extends HTMLElement {
11
12
  private _readyPromise: Promise<void>;
12
13
 
13
14
  private _readyResolve!: () => void;
14
15
 
16
+ private _readyResolved = false;
17
+
15
18
  /** @ignore */
16
19
  constructor() {
17
20
  super();
@@ -26,7 +29,7 @@ class AsyncElement extends HTMLElement {
26
29
  * @returns The closest app element, or `null`.
27
30
  */
28
31
  get closestApp(): AppElement | null {
29
- return this.parentElement?.closest('pc-app') as AppElement | null ?? null;
32
+ return (this.parentElement?.closest('pc-app') as AppElement | null) ?? null;
30
33
  }
31
34
 
32
35
  /**
@@ -35,22 +38,45 @@ class AsyncElement extends HTMLElement {
35
38
  * @returns The closest entity element, or `null`.
36
39
  */
37
40
  get closestEntity(): EntityElement | null {
38
- return this.parentElement?.closest('pc-entity') as EntityElement | null ?? null;
41
+ return (this.parentElement?.closest('pc-entity') as EntityElement | null) ?? null;
39
42
  }
40
43
 
41
44
  /**
42
45
  * Called when the element is fully initialized and ready. Subclasses should call this when
43
46
  * they're ready. Resolves the ready promise and dispatches a bubbling, composed `ready`
44
- * event.
47
+ * event. Signals at most once per readiness cycle: a repeat call before {@link _resetReady}
48
+ * has re-armed the promise does nothing.
45
49
  */
46
50
  protected _onReady() {
51
+ if (this._readyResolved) return;
52
+ this._readyResolved = true;
47
53
  this._readyResolve();
48
54
  this.dispatchEvent(new CustomEvent('ready', { bubbles: true, composed: true }));
49
55
  }
50
56
 
57
+ /**
58
+ * Returns the ready promise to its pending state. Subclasses should call this when the
59
+ * resource their readiness announced is torn down (typically from `disconnectedCallback`),
60
+ * so that a later re-initialization can signal readiness again. Does nothing while the
61
+ * promise is still pending — an in-flight waiter carries over to the next readiness cycle
62
+ * rather than being stranded on a promise nothing will ever resolve.
63
+ */
64
+ protected _resetReady() {
65
+ if (!this._readyResolved) return;
66
+ this._readyResolved = false;
67
+ this._readyPromise = new Promise<void>((resolve) => {
68
+ this._readyResolve = resolve;
69
+ });
70
+ }
71
+
51
72
  /**
52
73
  * Returns a promise that resolves with this element when it's ready. This is the low-level
53
74
  * primitive underlying {@link whenReady}, which is the recommended way to wait for elements.
75
+ *
76
+ * Readiness tracks the element's current lifecycle: once a ready element is torn down (for
77
+ * example by removing it from the document), this returns a fresh promise that resolves when
78
+ * the element is next ready. A promise obtained earlier stays resolved — call this again
79
+ * after re-inserting an element rather than reusing a promise from before its removal.
54
80
  * @returns A promise that resolves with this element when it's ready.
55
81
  */
56
82
  ready(): Promise<this> {
@@ -63,7 +89,7 @@ class AsyncElement extends HTMLElement {
63
89
  * classes extend {@link AsyncElement}).
64
90
  */
65
91
  type AsyncElementTagName = {
66
- [K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] extends AsyncElement ? K : never
92
+ [K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] extends AsyncElement ? K : never;
67
93
  }[keyof HTMLElementTagNameMap];
68
94
 
69
95
  /**
@@ -1,9 +1,11 @@
1
- import { BUTTON_TRANSITION_MODE_SPRITE_CHANGE, BUTTON_TRANSITION_MODE_TINT, ButtonComponent, Color, Vec4 } from 'playcanvas';
1
+ import type { ButtonComponent } from 'playcanvas';
2
+ import { BUTTON_TRANSITION_MODE_SPRITE_CHANGE, BUTTON_TRANSITION_MODE_TINT, Color, Vec4 } from 'playcanvas';
2
3
 
3
4
  import { AssetElement } from '../asset';
4
- import { ComponentElement } from './component';
5
5
  import { getEntity, parseBool, parseColor, parseEnum, parseNumber, parseVec4 } from '../parse';
6
6
 
7
+ import { ComponentElement } from './component';
8
+
7
9
  const transitionModes = new Map<'tint' | 'sprite', number>([
8
10
  ['tint', BUTTON_TRANSITION_MODE_TINT],
9
11
  ['sprite', BUTTON_TRANSITION_MODE_SPRITE_CHANGE]
@@ -51,7 +53,7 @@ class ButtonComponentElement extends ComponentElement {
51
53
  super('button');
52
54
  }
53
55
 
54
- getInitialComponentData() {
56
+ protected getInitialComponentData() {
55
57
  const data: Record<string, any> = {
56
58
  active: this._active,
57
59
  hitPadding: this._hitPadding,
@@ -448,10 +450,4 @@ class ButtonComponentElement extends ComponentElement {
448
450
 
449
451
  customElements.define('pc-button', ButtonComponentElement);
450
452
 
451
- declare global {
452
- interface HTMLElementTagNameMap {
453
- 'pc-button': ButtonComponentElement;
454
- }
455
- }
456
-
457
453
  export { ButtonComponentElement };
@@ -1,8 +1,30 @@
1
- import { CameraComponent, Color, Vec4, GAMMA_NONE, GAMMA_SRGB, PROJECTION_ORTHOGRAPHIC, PROJECTION_PERSPECTIVE, TONEMAP_LINEAR, TONEMAP_FILMIC, TONEMAP_NEUTRAL, TONEMAP_ACES2, TONEMAP_ACES, TONEMAP_HEJL, TONEMAP_NONE, XRTYPE_VR } from 'playcanvas';
1
+ import type { CameraComponent } from 'playcanvas';
2
+ import {
3
+ Color,
4
+ Vec4,
5
+ GAMMA_NONE,
6
+ GAMMA_SRGB,
7
+ PROJECTION_ORTHOGRAPHIC,
8
+ PROJECTION_PERSPECTIVE,
9
+ TONEMAP_LINEAR,
10
+ TONEMAP_FILMIC,
11
+ TONEMAP_NEUTRAL,
12
+ TONEMAP_ACES2,
13
+ TONEMAP_ACES,
14
+ TONEMAP_HEJL,
15
+ TONEMAP_NONE,
16
+ XRTYPE_VR
17
+ } from 'playcanvas';
2
18
 
3
- import { ComponentElement } from './component';
4
19
  import { parseBool, parseColor, parseEnum, parseNumber, parseVec4 } from '../parse';
5
20
 
21
+ import { ComponentElement } from './component';
22
+
23
+ const projections = new Map<'perspective' | 'orthographic', number>([
24
+ ['perspective', PROJECTION_PERSPECTIVE],
25
+ ['orthographic', PROJECTION_ORTHOGRAPHIC]
26
+ ]);
27
+
6
28
  const tonemaps = new Map<'none' | 'linear' | 'filmic' | 'hejl' | 'aces' | 'aces2' | 'neutral', number>([
7
29
  ['none', TONEMAP_NONE],
8
30
  ['linear', TONEMAP_LINEAR],
@@ -46,7 +68,7 @@ class CameraComponentElement extends ComponentElement {
46
68
 
47
69
  private _nearClip = 0.1;
48
70
 
49
- private _orthographic = false;
71
+ private _projection: 'perspective' | 'orthographic' = 'perspective';
50
72
 
51
73
  private _orthoHeight = 10;
52
74
 
@@ -63,7 +85,7 @@ class CameraComponentElement extends ComponentElement {
63
85
  super('camera');
64
86
  }
65
87
 
66
- getInitialComponentData() {
88
+ protected getInitialComponentData() {
67
89
  return {
68
90
  clearColor: this._clearColor,
69
91
  clearColorBuffer: this._clearColorBuffer,
@@ -77,12 +99,12 @@ class CameraComponentElement extends ComponentElement {
77
99
  gammaCorrection: this._gamma === 'srgb' ? GAMMA_SRGB : GAMMA_NONE,
78
100
  horizontalFov: this._horizontalFov,
79
101
  nearClip: this._nearClip,
80
- projection: this._orthographic ? PROJECTION_ORTHOGRAPHIC : PROJECTION_PERSPECTIVE,
102
+ projection: projections.get(this._projection) ?? PROJECTION_PERSPECTIVE,
81
103
  orthoHeight: this._orthoHeight,
82
104
  priority: this._priority,
83
105
  rect: this._rect,
84
106
  scissorRect: this._scissorRect,
85
- toneMapping: tonemaps.get(this._tonemap)
107
+ toneMapping: tonemaps.get(this._tonemap) ?? TONEMAP_NONE
86
108
  };
87
109
  }
88
110
 
@@ -96,7 +118,10 @@ class CameraComponentElement extends ComponentElement {
96
118
  * @param type - The type of XR mode to start.
97
119
  * @param space - The space to start the camera in.
98
120
  */
99
- startXr(type: 'immersive-ar' | 'immersive-vr', space: 'bounded-floor' | 'local' | 'local-floor' | 'unbounded' | 'viewer') {
121
+ startXr(
122
+ type: 'immersive-ar' | 'immersive-vr',
123
+ space: 'bounded-floor' | 'local' | 'local-floor' | 'unbounded' | 'viewer'
124
+ ) {
100
125
  if (this.component && this.xrAvailable) {
101
126
  this.component.startXr(type, space, {
102
127
  callback: (err: any) => {
@@ -352,25 +377,6 @@ class CameraComponentElement extends ComponentElement {
352
377
  return this._nearClip;
353
378
  }
354
379
 
355
- /**
356
- * Sets the orthographic projection of the camera.
357
- * @param value - The orthographic projection.
358
- */
359
- set orthographic(value) {
360
- this._orthographic = value;
361
- if (this.component) {
362
- this.component.projection = value ? PROJECTION_ORTHOGRAPHIC : PROJECTION_PERSPECTIVE;
363
- }
364
- }
365
-
366
- /**
367
- * Gets the orthographic projection of the camera.
368
- * @returns The orthographic projection.
369
- */
370
- get orthographic(): boolean {
371
- return this._orthographic;
372
- }
373
-
374
380
  /**
375
381
  * Sets the orthographic height of the camera.
376
382
  * @param value - The orthographic height.
@@ -409,6 +415,25 @@ class CameraComponentElement extends ComponentElement {
409
415
  return this._priority;
410
416
  }
411
417
 
418
+ /**
419
+ * Sets the projection of the camera. Use `orthoHeight` to size an orthographic projection.
420
+ * @param value - The projection ('perspective' or 'orthographic').
421
+ */
422
+ set projection(value: 'perspective' | 'orthographic') {
423
+ this._projection = value;
424
+ if (this.component) {
425
+ this.component.projection = projections.get(value) ?? PROJECTION_PERSPECTIVE;
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Gets the projection of the camera.
431
+ * @returns The projection.
432
+ */
433
+ get projection() {
434
+ return this._projection;
435
+ }
436
+
412
437
  /**
413
438
  * Sets the rect of the camera.
414
439
  * @param value - The rect.
@@ -481,9 +506,9 @@ class CameraComponentElement extends ComponentElement {
481
506
  'gamma',
482
507
  'horizontal-fov',
483
508
  'near-clip',
484
- 'orthographic',
485
509
  'ortho-height',
486
510
  'priority',
511
+ 'projection',
487
512
  'rect',
488
513
  'scissor-rect',
489
514
  'tonemap'
@@ -530,15 +555,15 @@ class CameraComponentElement extends ComponentElement {
530
555
  case 'near-clip':
531
556
  this.nearClip = parseNumber(newValue, 0.1, name);
532
557
  break;
533
- case 'orthographic':
534
- this.orthographic = parseBool(newValue, false);
535
- break;
536
558
  case 'ortho-height':
537
559
  this.orthoHeight = parseNumber(newValue, 10, name);
538
560
  break;
539
561
  case 'priority':
540
562
  this.priority = parseNumber(newValue, 0, name);
541
563
  break;
564
+ case 'projection':
565
+ this.projection = parseEnum(newValue, projections, 'perspective', name);
566
+ break;
542
567
  case 'rect':
543
568
  this.rect = parseVec4(newValue, new Vec4(0, 0, 1, 1), name);
544
569
  break;
@@ -554,10 +579,4 @@ class CameraComponentElement extends ComponentElement {
554
579
 
555
580
  customElements.define('pc-camera', CameraComponentElement);
556
581
 
557
- declare global {
558
- interface HTMLElementTagNameMap {
559
- 'pc-camera': CameraComponentElement;
560
- }
561
- }
562
-
563
582
  export { CameraComponentElement };
@@ -1,8 +1,10 @@
1
- import { CollisionComponent, Quat, Vec3 } from 'playcanvas';
1
+ import type { CollisionComponent } from 'playcanvas';
2
+ import { Quat, Vec3 } from 'playcanvas';
2
3
 
3
- import { ComponentElement } from './component';
4
4
  import { parseBool, parseEnum, parseNumber, parseQuat, parseVec3 } from '../parse';
5
5
 
6
+ import { ComponentElement } from './component';
7
+
6
8
  /**
7
9
  * The CollisionComponentElement interface provides properties and methods for manipulating
8
10
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-collision/ | `<pc-collision>`} elements.
@@ -14,17 +16,17 @@ import { parseBool, parseEnum, parseNumber, parseQuat, parseVec3 } from '../pars
14
16
  class CollisionComponentElement extends ComponentElement {
15
17
  private _angularOffset: Quat = new Quat();
16
18
 
17
- private _axis: number = 1;
19
+ private _axis = 1;
18
20
 
19
- private _convexHull: boolean = false;
21
+ private _convexHull = false;
20
22
 
21
23
  private _halfExtents: Vec3 = new Vec3(0.5, 0.5, 0.5);
22
24
 
23
- private _height: number = 2;
25
+ private _height = 2;
24
26
 
25
27
  private _linearOffset: Vec3 = new Vec3();
26
28
 
27
- private _radius: number = 0.5;
29
+ private _radius = 0.5;
28
30
 
29
31
  private _type: 'box' | 'capsule' | 'compound' | 'cone' | 'cylinder' | 'mesh' | 'sphere' = 'box';
30
32
 
@@ -33,7 +35,7 @@ class CollisionComponentElement extends ComponentElement {
33
35
  super('collision');
34
36
  }
35
37
 
36
- getInitialComponentData() {
38
+ protected getInitialComponentData() {
37
39
  return {
38
40
  axis: this._axis,
39
41
  angularOffset: this._angularOffset,
@@ -143,7 +145,17 @@ class CollisionComponentElement extends ComponentElement {
143
145
  }
144
146
 
145
147
  static get observedAttributes() {
146
- return [...super.observedAttributes, 'angular-offset', 'axis', 'convex-hull', 'half-extents', 'height', 'linear-offset', 'radius', 'type'];
148
+ return [
149
+ ...super.observedAttributes,
150
+ 'angular-offset',
151
+ 'axis',
152
+ 'convex-hull',
153
+ 'half-extents',
154
+ 'height',
155
+ 'linear-offset',
156
+ 'radius',
157
+ 'type'
158
+ ];
147
159
  }
148
160
 
149
161
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
@@ -172,7 +184,12 @@ class CollisionComponentElement extends ComponentElement {
172
184
  this.radius = parseNumber(newValue, 0.5, name);
173
185
  break;
174
186
  case 'type':
175
- this.type = parseEnum(newValue, ['box', 'capsule', 'compound', 'cone', 'cylinder', 'mesh', 'sphere'], 'box', name);
187
+ this.type = parseEnum(
188
+ newValue,
189
+ ['box', 'capsule', 'compound', 'cone', 'cylinder', 'mesh', 'sphere'],
190
+ 'box',
191
+ name
192
+ );
176
193
  break;
177
194
  }
178
195
  }
@@ -180,10 +197,4 @@ class CollisionComponentElement extends ComponentElement {
180
197
 
181
198
  customElements.define('pc-collision', CollisionComponentElement);
182
199
 
183
- declare global {
184
- interface HTMLElementTagNameMap {
185
- 'pc-collision': CollisionComponentElement;
186
- }
187
- }
188
-
189
200
  export { CollisionComponentElement };
@@ -1,6 +1,6 @@
1
- import { Component } from 'playcanvas';
1
+ import type { Component } from 'playcanvas';
2
2
 
3
- import { AppElement } from '../app';
3
+ import type { AppElement } from '../app';
4
4
  import { AsyncElement } from '../async-element';
5
5
  import { parseBool } from '../parse';
6
6
 
@@ -18,6 +18,15 @@ class ComponentElement extends AsyncElement {
18
18
 
19
19
  private _appElement: AppElement | null = null;
20
20
 
21
+ /**
22
+ * Incremented on every connect and disconnect. connectedCallback captures the value on entry
23
+ * and abandons itself wherever it resumes from an await if the value has moved on — so a
24
+ * callback whose element was removed cannot act on a torn-down tree, and one whose element
25
+ * was removed and re-inserted (which runs a callback of its own) cannot add the component a
26
+ * second time.
27
+ */
28
+ private _connectionGeneration = 0;
29
+
21
30
  /**
22
31
  * Creates a new ComponentElement instance.
23
32
  *
@@ -30,38 +39,78 @@ class ComponentElement extends AsyncElement {
30
39
  this._componentName = componentName;
31
40
  }
32
41
 
33
- // Method to be overridden by subclasses to provide initial component data
34
- getInitialComponentData() {
42
+ /**
43
+ * Returns the data the component is created with. Overridden by subclasses to supply the
44
+ * initial values of their cached properties.
45
+ *
46
+ * @returns The initial component data.
47
+ */
48
+ protected getInitialComponentData() {
35
49
  return {};
36
50
  }
37
51
 
38
- async addComponent() {
52
+ private async _addComponent() {
53
+ const generation = this._connectionGeneration;
54
+
39
55
  const entityElement = this.closestEntity;
40
56
  if (!entityElement) {
41
57
  // A component can only exist on an entity, so an element placed outside one is inert.
42
58
  // It still becomes ready (with a null `component`), so warn rather than fail silently
43
59
  const label = this.id ? ` '${this.id}'` : '';
44
- console.warn(`${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity - component not added`);
60
+ console.warn(
61
+ `${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity - component not added`
62
+ );
45
63
  return;
46
64
  }
47
65
 
48
66
  await entityElement.ready();
67
+
68
+ // The element may have been removed, or removed and re-inserted, while the entity became
69
+ // ready — the component belongs to the connection that owns the current generation.
70
+ if (generation !== this._connectionGeneration) {
71
+ return;
72
+ }
73
+
49
74
  // Add the component to the entity
50
75
  const data = this.getInitialComponentData();
51
76
  this._component = entityElement.entity!.addComponent(this._componentName, data);
52
77
  }
53
78
 
54
- initComponent() {}
79
+ /**
80
+ * Configures the newly added component. Overridden by subclasses whose setup goes beyond
81
+ * the initial data — child-element handling, asset resolution and the like.
82
+ */
83
+ protected initComponent() {
84
+ // optional hook
85
+ }
55
86
 
56
87
  async connectedCallback() {
88
+ const generation = ++this._connectionGeneration;
89
+
57
90
  this._appElement = this.closestApp ?? null;
58
91
  await this._appElement?.ready();
59
- await this.addComponent();
92
+
93
+ // The element may have been removed, or removed and re-inserted, while the application
94
+ // became ready. A re-insertion runs a connectedCallback of its own, so a stale resume
95
+ // must not add the component alongside it.
96
+ if (generation !== this._connectionGeneration) {
97
+ return;
98
+ }
99
+
100
+ await this._addComponent();
101
+
102
+ if (generation !== this._connectionGeneration) {
103
+ return;
104
+ }
105
+
60
106
  this.initComponent();
61
107
  this._onReady();
62
108
  }
63
109
 
64
110
  disconnectedCallback() {
111
+ // Invalidate any connectedCallback still suspended on an await
112
+ this._connectionGeneration++;
113
+
65
114
  // Remove the component when the element is disconnected. Skip this when the owning
66
115
  // application has already been destroyed — removing a <pc-app> disconnects it before
67
116
  // its children, taking the component systems with it.
@@ -70,6 +119,7 @@ class ComponentElement extends AsyncElement {
70
119
  }
71
120
  this._component = null;
72
121
  this._appElement = null;
122
+ this._resetReady();
73
123
  }
74
124
 
75
125
  /**