@playcanvas/web-components 0.20.0 → 0.21.0

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 (100) hide show
  1. package/dist/app.d.cts +5 -113
  2. package/dist/app.d.ts +5 -113
  3. package/dist/asset-binding.d.cts +1 -0
  4. package/dist/asset-binding.d.ts +1 -0
  5. package/dist/components/anim-clip.d.cts +8 -10
  6. package/dist/components/anim-clip.d.ts +8 -10
  7. package/dist/components/anim-component.d.cts +5 -4
  8. package/dist/components/anim-component.d.ts +5 -4
  9. package/dist/components/audio-listener-component.d.cts +5 -4
  10. package/dist/components/audio-listener-component.d.ts +5 -4
  11. package/dist/components/button-component.d.cts +5 -4
  12. package/dist/components/button-component.d.ts +5 -4
  13. package/dist/components/camera-component.d.cts +5 -4
  14. package/dist/components/camera-component.d.ts +5 -4
  15. package/dist/components/collision-component.d.cts +5 -4
  16. package/dist/components/collision-component.d.ts +5 -4
  17. package/dist/components/component.d.cts +2 -2
  18. package/dist/components/component.d.ts +2 -2
  19. package/dist/components/element-component.d.cts +5 -4
  20. package/dist/components/element-component.d.ts +5 -4
  21. package/dist/components/gsplat-component.d.cts +5 -4
  22. package/dist/components/gsplat-component.d.ts +5 -4
  23. package/dist/components/joint-component.d.cts +5 -4
  24. package/dist/components/joint-component.d.ts +5 -4
  25. package/dist/components/layout-child-component.d.cts +5 -4
  26. package/dist/components/layout-child-component.d.ts +5 -4
  27. package/dist/components/layout-group-component.d.cts +5 -4
  28. package/dist/components/layout-group-component.d.ts +5 -4
  29. package/dist/components/light-component.d.cts +5 -4
  30. package/dist/components/light-component.d.ts +5 -4
  31. package/dist/components/particle-system-component.d.cts +22 -5
  32. package/dist/components/particle-system-component.d.ts +22 -5
  33. package/dist/components/render-component.d.cts +5 -4
  34. package/dist/components/render-component.d.ts +5 -4
  35. package/dist/components/rigid-body-component.d.cts +5 -4
  36. package/dist/components/rigid-body-component.d.ts +5 -4
  37. package/dist/components/screen-component.d.cts +5 -4
  38. package/dist/components/screen-component.d.ts +5 -4
  39. package/dist/components/script-component.d.cts +5 -4
  40. package/dist/components/script-component.d.ts +5 -4
  41. package/dist/components/scroll-view-component.d.cts +5 -4
  42. package/dist/components/scroll-view-component.d.ts +5 -4
  43. package/dist/components/scrollbar-component.d.cts +5 -4
  44. package/dist/components/scrollbar-component.d.ts +5 -4
  45. package/dist/components/sound-component.d.cts +5 -4
  46. package/dist/components/sound-component.d.ts +5 -4
  47. package/dist/custom-elements.json +38 -38
  48. package/dist/entity-reference.d.cts +13 -0
  49. package/dist/entity-reference.d.ts +13 -0
  50. package/dist/material.d.cts +8 -6
  51. package/dist/material.d.ts +8 -6
  52. package/dist/model.d.cts +8 -9
  53. package/dist/model.d.ts +8 -9
  54. package/dist/parse.d.cts +0 -8
  55. package/dist/parse.d.ts +0 -8
  56. package/dist/pointer-controller.d.cts +1 -0
  57. package/dist/pointer-controller.d.ts +1 -0
  58. package/dist/pwc.cjs +1360 -1186
  59. package/dist/pwc.cjs.map +1 -1
  60. package/dist/pwc.js +1360 -1186
  61. package/dist/pwc.js.map +1 -1
  62. package/dist/pwc.min.js +1 -1
  63. package/dist/pwc.min.js.map +1 -1
  64. package/dist/pwc.min.mjs +1 -1
  65. package/dist/pwc.min.mjs.map +1 -1
  66. package/dist/pwc.mjs +1361 -1187
  67. package/dist/pwc.mjs.map +1 -1
  68. package/dist/sky.d.cts +6 -7
  69. package/dist/sky.d.ts +6 -7
  70. package/dist/web-types.json +39 -39
  71. package/package.json +2 -2
  72. package/src/app.ts +16 -451
  73. package/src/asset-binding.ts +141 -0
  74. package/src/components/anim-clip.ts +22 -45
  75. package/src/components/anim-component.ts +10 -6
  76. package/src/components/audio-listener-component.ts +6 -5
  77. package/src/components/button-component.ts +8 -6
  78. package/src/components/camera-component.ts +6 -5
  79. package/src/components/collision-component.ts +6 -5
  80. package/src/components/component.ts +6 -4
  81. package/src/components/element-component.ts +6 -5
  82. package/src/components/gsplat-component.ts +6 -5
  83. package/src/components/joint-component.ts +8 -6
  84. package/src/components/layout-child-component.ts +6 -5
  85. package/src/components/layout-group-component.ts +6 -5
  86. package/src/components/light-component.ts +6 -5
  87. package/src/components/particle-system-component.ts +56 -30
  88. package/src/components/render-component.ts +6 -5
  89. package/src/components/rigid-body-component.ts +6 -5
  90. package/src/components/screen-component.ts +6 -5
  91. package/src/components/script-component.ts +8 -10
  92. package/src/components/scroll-view-component.ts +8 -6
  93. package/src/components/scrollbar-component.ts +8 -6
  94. package/src/components/sound-component.ts +6 -5
  95. package/src/entity-reference.ts +237 -0
  96. package/src/material.ts +25 -29
  97. package/src/model.ts +24 -48
  98. package/src/parse.ts +0 -232
  99. package/src/pointer-controller.ts +550 -0
  100. package/src/sky.ts +19 -30
@@ -1,7 +1,8 @@
1
- import type { Asset, EventHandle } from 'playcanvas';
1
+ import type { Asset } from 'playcanvas';
2
2
  import { AnimTrack } from 'playcanvas';
3
3
 
4
- import { AssetElement, useAsset } from '../asset';
4
+ import { AssetElement } from '../asset';
5
+ import { AssetBinding } from '../asset-binding';
5
6
  import { AsyncElement } from '../async-element';
6
7
  import { ModelElement } from '../model';
7
8
  import { parseBool, parseNumber } from '../parse';
@@ -39,6 +40,12 @@ class AnimClipElement extends AsyncElement {
39
40
 
40
41
  private _asset = '';
41
42
 
43
+ /**
44
+ * Watches the current source asset while it loads. Starting a new resolution or
45
+ * disconnecting cancels it, so a superseded source can never hand its track to the parent.
46
+ */
47
+ private _binding = new AssetBinding();
48
+
42
49
  /**
43
50
  * Incremented on every connect and disconnect, and captured by connectedCallback on entry —
44
51
  * a resume from an await abandons itself if the value has moved on, so a stale callback can
@@ -47,22 +54,14 @@ class AnimClipElement extends AsyncElement {
47
54
  */
48
55
  private _connectionGeneration = 0;
49
56
 
50
- private _errorHandle: EventHandle | null = null;
51
-
52
57
  /**
53
58
  * Incremented on every track resolution and on disconnect, and captured by a resolution when
54
- * it starts. A resolution that resumes from an await or an asset callback abandons itself if
55
- * the value has moved on, so a superseded resolution cannot hand a stale track to the parent.
59
+ * it starts. A resolution that resumes from an await abandons itself if the value has moved
60
+ * on, so a superseded resolution cannot hand a stale track to the parent. The asset
61
+ * subscription itself is guarded by the binding above.
56
62
  */
57
63
  private _loadGeneration = 0;
58
64
 
59
- /**
60
- * The pending asset subscriptions of the current resolution, if it is waiting for its asset.
61
- * Held so that whatever supersedes the resolution can detach the handlers from the asset,
62
- * rather than leave them registered until the asset settles (or forever, if it never does).
63
- */
64
- private _loadHandle: EventHandle | null = null;
65
-
66
65
  private _loop = true;
67
66
 
68
67
  private _name = '';
@@ -112,7 +111,7 @@ class AnimClipElement extends AsyncElement {
112
111
  // Invalidate any connectedCallback or track resolution still suspended on an await
113
112
  this._connectionGeneration++;
114
113
  this._loadGeneration++;
115
- this._detachLoadHandlers();
114
+ this._binding.cancel();
116
115
 
117
116
  // Uses the cached parent rather than a fresh lookup, since parentElement is already null
118
117
  // by now. The component itself is null if the whole <pc-app> is being torn down —
@@ -135,13 +134,6 @@ class AnimClipElement extends AsyncElement {
135
134
  return animElement;
136
135
  }
137
136
 
138
- private _detachLoadHandlers() {
139
- this._loadHandle?.off();
140
- this._loadHandle = null;
141
- this._errorHandle?.off();
142
- this._errorHandle = null;
143
- }
144
-
145
137
  /**
146
138
  * Reports a name-validation failure from the owning `<pc-anim>`, once per name value.
147
139
  *
@@ -169,35 +161,20 @@ class AnimClipElement extends AsyncElement {
169
161
  this._animElement = animElement;
170
162
 
171
163
  const generation = ++this._loadGeneration;
172
- this._detachLoadHandlers();
164
+ this._binding.cancel();
173
165
 
174
166
  if (this._asset) {
175
- const asset = useAsset(this._asset);
167
+ // Every path that moves _loadGeneration also rebinds or cancels the binding, so a
168
+ // delivery below is always current.
169
+ const asset = this._binding.bind(this._asset, {
170
+ load: (loaded) => this._extractTrack(loaded, `asset '${this._asset}'`),
171
+ error: () => {
172
+ this._warnSource(`pc-anim-clip '${this._name}' - asset '${this._asset}' failed to load - clip not assigned`);
173
+ }
174
+ });
176
175
  if (!asset) {
177
176
  this._warnSource(`pc-anim-clip '${this._name}' could not find asset '${this._asset}' - clip not assigned`);
178
- return;
179
- }
180
- if (asset.loaded) {
181
- this._extractTrack(asset, `asset '${this._asset}'`);
182
- return;
183
177
  }
184
- // Whichever of load/error fires first detaches the other. The generation is
185
- // re-checked even though a superseded handler is detached: the detach relies on how
186
- // the engine's event emitter treats removal, while the check holds on its own.
187
- this._loadHandle = asset.once('load', () => {
188
- this._detachLoadHandlers();
189
- if (generation !== this._loadGeneration) {
190
- return;
191
- }
192
- this._extractTrack(asset, `asset '${this._asset}'`);
193
- });
194
- this._errorHandle = asset.once('error', () => {
195
- this._detachLoadHandlers();
196
- if (generation !== this._loadGeneration) {
197
- return;
198
- }
199
- this._warnSource(`pc-anim-clip '${this._name}' - asset '${this._asset}' failed to load - clip not assigned`);
200
- });
201
178
  return;
202
179
  }
203
180
 
@@ -55,7 +55,7 @@ type PlaybackState = {
55
55
  *
56
56
  * @category Components
57
57
  */
58
- class AnimComponentElement extends ComponentElement {
58
+ class AnimComponentElement extends ComponentElement<AnimComponent> {
59
59
  /**
60
60
  * Whether playback starts automatically once a clip is assigned.
61
61
  */
@@ -168,6 +168,9 @@ class AnimComponentElement extends ComponentElement {
168
168
  */
169
169
  private _applyRootBone() {
170
170
  const component = this.component;
171
+ if (!component) {
172
+ return;
173
+ }
171
174
 
172
175
  // A non-null root this element did not assign came through the engine API. A fresh
173
176
  // component starts at null, which is always reclaimable.
@@ -252,7 +255,7 @@ class AnimComponentElement extends ComponentElement {
252
255
  * declared `clip` selection can apply before any asset has loaded.
253
256
  */
254
257
  private _assignClip(clip: AnimClipElement) {
255
- this.component.assignAnimation(clip.name, clip._track ?? AnimTrack.EMPTY, undefined, clip.speed, clip.loop);
258
+ this.component?.assignAnimation(clip.name, clip._track ?? AnimTrack.EMPTY, undefined, clip.speed, clip.loop);
256
259
  }
257
260
 
258
261
  /**
@@ -586,11 +589,12 @@ class AnimComponentElement extends ComponentElement {
586
589
  }
587
590
 
588
591
  /**
589
- * Gets the underlying PlayCanvas anim component.
590
- * @returns The anim component.
592
+ * Gets the underlying PlayCanvas anim component. `null` until the element is
593
+ * ready see {@link ComponentElement.component}.
594
+ * @returns The anim component, or `null`.
591
595
  */
592
- get component(): AnimComponent {
593
- return super.component as AnimComponent;
596
+ get component(): AnimComponent | null {
597
+ return super.component;
594
598
  }
595
599
 
596
600
  /**
@@ -16,18 +16,19 @@ import { ComponentElement } from './component';
16
16
  *
17
17
  * @category Components
18
18
  */
19
- class AudioListenerComponentElement extends ComponentElement {
19
+ class AudioListenerComponentElement extends ComponentElement<AudioListenerComponent> {
20
20
  /** @ignore */
21
21
  constructor() {
22
22
  super('audiolistener');
23
23
  }
24
24
 
25
25
  /**
26
- * Gets the underlying PlayCanvas audio listener component.
27
- * @returns The audio listener component.
26
+ * Gets the underlying PlayCanvas audio listener component. `null` until the element is
27
+ * ready see {@link ComponentElement.component}.
28
+ * @returns The audio listener component, or `null`.
28
29
  */
29
- get component(): AudioListenerComponent {
30
- return super.component as AudioListenerComponent;
30
+ get component(): AudioListenerComponent | null {
31
+ return super.component;
31
32
  }
32
33
  }
33
34
 
@@ -2,7 +2,8 @@ import type { ButtonComponent } from 'playcanvas';
2
2
  import { BUTTON_TRANSITION_MODE_SPRITE_CHANGE, BUTTON_TRANSITION_MODE_TINT, Color, Vec4 } from 'playcanvas';
3
3
 
4
4
  import { useAsset } from '../asset';
5
- import { parseBool, parseColor, parseEnum, parseNumber, parseVec4, resolveEntity } from '../parse';
5
+ import { resolveEntity } from '../entity-reference';
6
+ import { parseBool, parseColor, parseEnum, parseNumber, parseVec4 } from '../parse';
6
7
 
7
8
  import { ComponentElement } from './component';
8
9
 
@@ -26,7 +27,7 @@ const transitionModes = new Map<'tint' | 'sprite', number>([
26
27
  *
27
28
  * @category Components
28
29
  */
29
- class ButtonComponentElement extends ComponentElement {
30
+ class ButtonComponentElement extends ComponentElement<ButtonComponent> {
30
31
  private _active = true;
31
32
 
32
33
  private _image = '';
@@ -102,11 +103,12 @@ class ButtonComponentElement extends ComponentElement {
102
103
  }
103
104
 
104
105
  /**
105
- * Gets the underlying PlayCanvas button component.
106
- * @returns The button component.
106
+ * Gets the underlying PlayCanvas button component. `null` until the element is
107
+ * ready see {@link ComponentElement.component}.
108
+ * @returns The button component, or `null`.
107
109
  */
108
- get component(): ButtonComponent {
109
- return super.component as ButtonComponent;
110
+ get component(): ButtonComponent | null {
111
+ return super.component;
110
112
  }
111
113
 
112
114
  /**
@@ -50,7 +50,7 @@ const tonemaps = new Map<'none' | 'linear' | 'filmic' | 'hejl' | 'aces' | 'aces2
50
50
  *
51
51
  * @category Components
52
52
  */
53
- class CameraComponentElement extends ComponentElement {
53
+ class CameraComponentElement extends ComponentElement<CameraComponent> {
54
54
  private _clearColor = new Color(0.75, 0.75, 0.75, 1);
55
55
 
56
56
  private _clearColorBuffer = true;
@@ -177,11 +177,12 @@ class CameraComponentElement extends ComponentElement {
177
177
  }
178
178
 
179
179
  /**
180
- * Gets the underlying PlayCanvas camera component.
181
- * @returns The camera component.
180
+ * Gets the underlying PlayCanvas camera component. `null` until the element is
181
+ * ready see {@link ComponentElement.component}.
182
+ * @returns The camera component, or `null`.
182
183
  */
183
- get component(): CameraComponent {
184
- return super.component as CameraComponent;
184
+ get component(): CameraComponent | null {
185
+ return super.component;
185
186
  }
186
187
 
187
188
  /**
@@ -25,7 +25,7 @@ import { ComponentElement } from './component';
25
25
  *
26
26
  * @category Components
27
27
  */
28
- class CollisionComponentElement extends ComponentElement {
28
+ class CollisionComponentElement extends ComponentElement<CollisionComponent> {
29
29
  private _angularOffset: Quat = new Quat();
30
30
 
31
31
  private _axis = 1;
@@ -91,11 +91,12 @@ class CollisionComponentElement extends ComponentElement {
91
91
  }
92
92
 
93
93
  /**
94
- * Gets the underlying PlayCanvas collision component.
95
- * @returns The collision component.
94
+ * Gets the underlying PlayCanvas collision component. `null` until the element is
95
+ * ready see {@link ComponentElement.component}.
96
+ * @returns The collision component, or `null`.
96
97
  */
97
- get component(): CollisionComponent {
98
- return super.component as CollisionComponent;
98
+ get component(): CollisionComponent | null {
99
+ return super.component;
99
100
  }
100
101
 
101
102
  set angularOffset(value: Quat) {
@@ -10,12 +10,12 @@ import { parseBool } from '../parse';
10
10
  *
11
11
  * @category Components
12
12
  */
13
- class ComponentElement extends AsyncElement {
13
+ class ComponentElement<T extends Component = Component> extends AsyncElement {
14
14
  private _componentName: string;
15
15
 
16
16
  private _enabled = true;
17
17
 
18
- private _component: Component | null = null;
18
+ private _component: T | null = null;
19
19
 
20
20
  private _appElement: AppElement | null = null;
21
21
 
@@ -97,7 +97,9 @@ class ComponentElement extends AsyncElement {
97
97
  return;
98
98
  }
99
99
 
100
- this._component = entity.addComponent(this._componentName, this.getInitialComponentData());
100
+ // The name passed by the subclass selects the engine system that creates its T - a
101
+ // pairing the type system cannot express, so it is asserted this once
102
+ this._component = entity.addComponent(this._componentName, this.getInitialComponentData()) as T | null;
101
103
  }
102
104
 
103
105
  private async _addComponent() {
@@ -219,7 +221,7 @@ class ComponentElement extends AsyncElement {
219
221
  * before accessing it.
220
222
  * @returns The component instance, or `null`.
221
223
  */
222
- get component(): Component | null {
224
+ get component(): T | null {
223
225
  return this._component;
224
226
  }
225
227
 
@@ -25,7 +25,7 @@ import { ComponentElement } from './component';
25
25
  *
26
26
  * @category Components
27
27
  */
28
- class ElementComponentElement extends ComponentElement {
28
+ class ElementComponentElement extends ComponentElement<ElementComponent> {
29
29
  private _anchor: Vec4 = new Vec4(0.5, 0.5, 0.5, 0.5);
30
30
 
31
31
  private _autoWidth = true;
@@ -158,11 +158,12 @@ class ElementComponentElement extends ComponentElement {
158
158
  }
159
159
 
160
160
  /**
161
- * Gets the underlying PlayCanvas element component.
162
- * @returns The element component.
161
+ * Gets the underlying PlayCanvas element component. `null` until the element is
162
+ * ready see {@link ComponentElement.component}.
163
+ * @returns The element component, or `null`.
163
164
  */
164
- get component(): ElementComponent {
165
- return super.component as ElementComponent;
165
+ get component(): ElementComponent | null {
166
+ return super.component;
166
167
  }
167
168
 
168
169
  /**
@@ -19,7 +19,7 @@ import { ComponentElement } from './component';
19
19
  *
20
20
  * @category Components
21
21
  */
22
- class GSplatComponentElement extends ComponentElement {
22
+ class GSplatComponentElement extends ComponentElement<GSplatComponent> {
23
23
  private _asset = '';
24
24
 
25
25
  private _castShadows = false;
@@ -49,11 +49,12 @@ class GSplatComponentElement extends ComponentElement {
49
49
  }
50
50
 
51
51
  /**
52
- * Gets the underlying PlayCanvas gsplat component.
53
- * @returns The gsplat component.
52
+ * Gets the underlying PlayCanvas gsplat component. `null` until the element is
53
+ * ready see {@link ComponentElement.component}.
54
+ * @returns The gsplat component, or `null`.
54
55
  */
55
- get component(): GSplatComponent {
56
- return super.component as GSplatComponent;
56
+ get component(): GSplatComponent | null {
57
+ return super.component;
57
58
  }
58
59
 
59
60
  /**
@@ -1,7 +1,8 @@
1
1
  import type { JointComponent } from 'playcanvas';
2
2
  import { Vec2, Vec3 } from 'playcanvas';
3
3
 
4
- import { parseBool, parseEnum, parseNumber, parseVec2, parseVec3, resolveEntity } from '../parse';
4
+ import { resolveEntity } from '../entity-reference';
5
+ import { parseBool, parseEnum, parseNumber, parseVec2, parseVec3 } from '../parse';
5
6
 
6
7
  import { ComponentElement } from './component';
7
8
 
@@ -45,7 +46,7 @@ export type MotionMode = 'locked' | 'limited' | 'free';
45
46
  *
46
47
  * @category Components
47
48
  */
48
- class JointComponentElement extends ComponentElement {
49
+ class JointComponentElement extends ComponentElement<JointComponent> {
49
50
  /**
50
51
  * The spring damping of the joint per angular axis.
51
52
  */
@@ -254,11 +255,12 @@ class JointComponentElement extends ComponentElement {
254
255
  }
255
256
 
256
257
  /**
257
- * Gets the underlying PlayCanvas joint component.
258
- * @returns The joint component.
258
+ * Gets the underlying PlayCanvas joint component. `null` until the element is
259
+ * ready see {@link ComponentElement.component}.
260
+ * @returns The joint component, or `null`.
259
261
  */
260
- get component(): JointComponent {
261
- return super.component as JointComponent;
262
+ get component(): JointComponent | null {
263
+ return super.component;
262
264
  }
263
265
 
264
266
  /**
@@ -18,7 +18,7 @@ import { ComponentElement } from './component';
18
18
  *
19
19
  * @category Components
20
20
  */
21
- class LayoutChildComponentElement extends ComponentElement {
21
+ class LayoutChildComponentElement extends ComponentElement<LayoutChildComponent> {
22
22
  private _minWidth = 0;
23
23
 
24
24
  private _minHeight = 0;
@@ -51,11 +51,12 @@ class LayoutChildComponentElement extends ComponentElement {
51
51
  }
52
52
 
53
53
  /**
54
- * Gets the underlying PlayCanvas layout child component.
55
- * @returns The layout child component.
54
+ * Gets the underlying PlayCanvas layout child component. `null` until the element is
55
+ * ready see {@link ComponentElement.component}.
56
+ * @returns The layout child component, or `null`.
56
57
  */
57
- get component(): LayoutChildComponent {
58
- return super.component as LayoutChildComponent;
58
+ get component(): LayoutChildComponent | null {
59
+ return super.component;
59
60
  }
60
61
 
61
62
  /**
@@ -40,7 +40,7 @@ const fittings = new Map<'none' | 'stretch' | 'shrink' | 'both', number>([
40
40
  *
41
41
  * @category Components
42
42
  */
43
- class LayoutGroupComponentElement extends ComponentElement {
43
+ class LayoutGroupComponentElement extends ComponentElement<LayoutGroupComponent> {
44
44
  private _orientation: 'horizontal' | 'vertical' = 'horizontal';
45
45
 
46
46
  private _reverseX = false;
@@ -79,11 +79,12 @@ class LayoutGroupComponentElement extends ComponentElement {
79
79
  }
80
80
 
81
81
  /**
82
- * Gets the underlying PlayCanvas layout group component.
83
- * @returns The layout group component.
82
+ * Gets the underlying PlayCanvas layout group component. `null` until the element is
83
+ * ready see {@link ComponentElement.component}.
84
+ * @returns The layout group component, or `null`.
84
85
  */
85
- get component(): LayoutGroupComponent {
86
- return super.component as LayoutGroupComponent;
86
+ get component(): LayoutGroupComponent | null {
87
+ return super.component;
87
88
  }
88
89
 
89
90
  /**
@@ -45,7 +45,7 @@ const shadowTypes = new Map<
45
45
  *
46
46
  * @category Components
47
47
  */
48
- class LightComponentElement extends ComponentElement {
48
+ class LightComponentElement extends ComponentElement<LightComponent> {
49
49
  private _cascadeBlend = 0;
50
50
 
51
51
  private _cascadeDistribution = 0.5;
@@ -132,11 +132,12 @@ class LightComponentElement extends ComponentElement {
132
132
  }
133
133
 
134
134
  /**
135
- * Gets the underlying PlayCanvas light component.
136
- * @returns The light component.
135
+ * Gets the underlying PlayCanvas light component. `null` until the element is
136
+ * ready see {@link ComponentElement.component}.
137
+ * @returns The light component, or `null`.
137
138
  */
138
- get component(): LightComponent {
139
- return super.component as LightComponent;
139
+ get component(): LightComponent | null {
140
+ return super.component;
140
141
  }
141
142
 
142
143
  /**
@@ -1,6 +1,7 @@
1
- import type { ParticleSystemComponent } from 'playcanvas';
1
+ import type { Asset, ParticleSystemComponent } from 'playcanvas';
2
2
 
3
3
  import { useAsset } from '../asset';
4
+ import { AssetBinding } from '../asset-binding';
4
5
 
5
6
  import { ComponentElement } from './component';
6
7
 
@@ -18,9 +19,17 @@ import { ComponentElement } from './component';
18
19
  *
19
20
  * @category Components
20
21
  */
21
- class ParticleSystemComponentElement extends ComponentElement {
22
+ class ParticleSystemComponentElement extends ComponentElement<ParticleSystemComponent> {
22
23
  private _asset = '';
23
24
 
25
+ /**
26
+ * The subscription to the current config asset while its load is in flight. Rebinding
27
+ * supersedes it and disconnect cancels it, so a superseded config — an earlier asset that
28
+ * finishes loading after its replacement, or a callback left behind by a previous
29
+ * connection — can never configure the component.
30
+ */
31
+ private _binding = new AssetBinding();
32
+
24
33
  /** @ignore */
25
34
  constructor() {
26
35
  super('particlesystem');
@@ -28,28 +37,54 @@ class ParticleSystemComponentElement extends ComponentElement {
28
37
 
29
38
  protected getInitialComponentData() {
30
39
  const asset = useAsset(this._asset);
31
- // A lazy config has no resource yet - _loadAsset applies it once the load completes
40
+ // A lazy config has no resource yet - the config binding applies it once the load
41
+ // completes
32
42
  if (!asset || !asset.resource) {
33
43
  return {};
34
44
  }
35
45
 
36
- if ((asset.resource as any).colorMapAsset) {
37
- const id = (asset.resource as any).colorMapAsset;
38
- const colorMapAsset = useAsset(id)?.id;
39
- if (colorMapAsset) {
40
- (asset.resource as any).colorMapAsset = colorMapAsset;
41
- }
46
+ this._resolveColorMap(asset.resource);
47
+ return asset.resource;
48
+ }
49
+
50
+ protected initComponent() {
51
+ // A loaded config already arrived through getInitialComponentData - the binding is only
52
+ // needed for a load still in flight. Resolution here also starts a lazy config's load.
53
+ const asset = useAsset(this._asset);
54
+ if (asset && !asset.loaded) {
55
+ this._bindConfig();
42
56
  }
57
+ }
43
58
 
44
- return asset.resource;
59
+ disconnectedCallback() {
60
+ // The binding dies with the connection, so a config that finishes loading later cannot
61
+ // configure the component a reconnection creates - that connection binds afresh.
62
+ this._binding.cancel();
63
+ super.disconnectedCallback();
45
64
  }
46
65
 
47
66
  /**
48
- * Gets the underlying PlayCanvas particle system component.
49
- * @returns The particle system component.
67
+ * Gets the underlying PlayCanvas particle system component. `null` until the element is
68
+ * ready see {@link ComponentElement.component}.
69
+ * @returns The particle system component, or `null`.
50
70
  */
51
- get component(): ParticleSystemComponent {
52
- return super.component as ParticleSystemComponent;
71
+ get component(): ParticleSystemComponent | null {
72
+ return super.component;
73
+ }
74
+
75
+ /**
76
+ * Rewrites the config's `colorMapAsset` from the `pc-asset` id it is authored with to the
77
+ * engine asset id the component resolves, starting the texture's load if it is lazy. The
78
+ * rewrite is in place, so a config applied again — a host cycle, a reconnection — is already
79
+ * resolved and passes through unchanged.
80
+ */
81
+ private _resolveColorMap(resource: any) {
82
+ if (resource.colorMapAsset) {
83
+ const colorMapAsset = useAsset(resource.colorMapAsset)?.id;
84
+ if (colorMapAsset) {
85
+ resource.colorMapAsset = colorMapAsset;
86
+ }
87
+ }
53
88
  }
54
89
 
55
90
  private applyConfig(resource: any) {
@@ -57,6 +92,8 @@ class ParticleSystemComponentElement extends ComponentElement {
57
92
  return;
58
93
  }
59
94
 
95
+ this._resolveColorMap(resource);
96
+
60
97
  // Set all the config properties on the component
61
98
  for (const key in resource) {
62
99
  if (Object.hasOwn(resource, key)) {
@@ -65,21 +102,10 @@ class ParticleSystemComponentElement extends ComponentElement {
65
102
  }
66
103
  }
67
104
 
68
- private async _loadAsset() {
69
- await this.closestApp?.ready();
70
-
71
- const asset = useAsset(this._asset);
72
- if (!asset) {
73
- return;
74
- }
75
-
76
- if (asset.loaded) {
77
- this.applyConfig(asset.resource);
78
- } else {
79
- asset.once('load', () => {
80
- this.applyConfig(asset.resource);
81
- });
82
- }
105
+ private _bindConfig() {
106
+ this._binding.bind(this._asset, {
107
+ load: (asset: Asset) => this.applyConfig(asset.resource)
108
+ });
83
109
  }
84
110
 
85
111
  /**
@@ -89,7 +115,7 @@ class ParticleSystemComponentElement extends ComponentElement {
89
115
  set asset(value: string) {
90
116
  this._asset = value;
91
117
  if (this.isConnected) {
92
- this._loadAsset();
118
+ this._bindConfig();
93
119
  }
94
120
  }
95
121
 
@@ -24,7 +24,7 @@ import { ComponentElement } from './component';
24
24
  *
25
25
  * @category Components
26
26
  */
27
- class RenderComponentElement extends ComponentElement {
27
+ class RenderComponentElement extends ComponentElement<RenderComponent> {
28
28
  private _castShadows = true;
29
29
 
30
30
  private _material = '';
@@ -48,11 +48,12 @@ class RenderComponentElement extends ComponentElement {
48
48
  }
49
49
 
50
50
  /**
51
- * Gets the underlying PlayCanvas render component.
52
- * @returns The render component.
51
+ * Gets the underlying PlayCanvas render component. `null` until the element is
52
+ * ready see {@link ComponentElement.component}.
53
+ * @returns The render component, or `null`.
53
54
  */
54
- get component(): RenderComponent {
55
- return super.component as RenderComponent;
55
+ get component(): RenderComponent | null {
56
+ return super.component;
56
57
  }
57
58
 
58
59
  /**