@playcanvas/web-components 0.11.0 → 0.12.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 (82) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +31 -49
  3. package/dist/asset.d.ts +152 -12
  4. package/dist/async-element.d.ts +26 -9
  5. package/dist/components/button-component.d.ts +3 -7
  6. package/dist/components/camera-component.d.ts +3 -7
  7. package/dist/components/collision-component.d.ts +19 -7
  8. package/dist/components/component.d.ts +41 -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 +3 -7
  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 +3 -7
  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 +5824 -10581
  26. package/dist/entity-base.d.ts +67 -0
  27. package/dist/entity.d.ts +7 -48
  28. package/dist/index.d.ts +41 -1
  29. package/dist/material.d.ts +14 -13
  30. package/dist/model.d.ts +43 -5
  31. package/dist/module.d.ts +0 -6
  32. package/dist/node.d.ts +253 -0
  33. package/dist/parse.d.ts +2 -1
  34. package/dist/pwc.cjs +1830 -274
  35. package/dist/pwc.cjs.map +1 -1
  36. package/dist/pwc.js +1830 -274
  37. package/dist/pwc.js.map +1 -1
  38. package/dist/pwc.min.js +1 -1
  39. package/dist/pwc.min.js.map +1 -1
  40. package/dist/pwc.min.mjs +1 -1
  41. package/dist/pwc.min.mjs.map +1 -1
  42. package/dist/pwc.mjs +1830 -276
  43. package/dist/pwc.mjs.map +1 -1
  44. package/dist/scene.d.ts +4 -7
  45. package/dist/sky.d.ts +13 -5
  46. package/dist/vscode.html-custom-data.json +148 -26
  47. package/dist/web-types.json +894 -581
  48. package/package.json +9 -8
  49. package/src/app.ts +163 -88
  50. package/src/asset.ts +472 -36
  51. package/src/async-element.ts +39 -12
  52. package/src/components/button-component.ts +5 -9
  53. package/src/components/camera-component.ts +24 -10
  54. package/src/components/collision-component.ts +61 -15
  55. package/src/components/component.ts +151 -11
  56. package/src/components/element-component.ts +26 -30
  57. package/src/components/gsplat-component.ts +4 -9
  58. package/src/components/layoutchild-component.ts +4 -9
  59. package/src/components/layoutgroup-component.ts +14 -9
  60. package/src/components/light-component.ts +42 -12
  61. package/src/components/listener-component.ts +1 -7
  62. package/src/components/particlesystem-component.ts +7 -15
  63. package/src/components/render-component.ts +5 -10
  64. package/src/components/rigidbody-component.ts +23 -16
  65. package/src/components/screen-component.ts +5 -9
  66. package/src/components/script-component.ts +108 -46
  67. package/src/components/script.ts +38 -33
  68. package/src/components/scrollbar-component.ts +6 -16
  69. package/src/components/scrollview-component.ts +16 -11
  70. package/src/components/sound-component.ts +10 -15
  71. package/src/components/sound-slot.ts +30 -20
  72. package/src/entity-base.ts +136 -0
  73. package/src/entity.ts +47 -118
  74. package/src/index.ts +50 -1
  75. package/src/loading-bar.ts +8 -8
  76. package/src/material.ts +65 -39
  77. package/src/model.ts +140 -17
  78. package/src/module.ts +8 -7
  79. package/src/node.ts +715 -0
  80. package/src/parse.ts +62 -17
  81. package/src/scene.ts +12 -9
  82. package/src/sky.ts +50 -10
@@ -1,17 +1,20 @@
1
- import { AppElement } from './app';
2
- import { EntityElement } from './entity';
1
+ import type { AppElement } from './app';
2
+ import type { EntityBaseElement } from './entity-base';
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,31 +29,55 @@ 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
  /**
33
- * The nearest ancestor `<pc-entity>` element, or `null` if this element has no `<pc-entity>`
34
- * ancestor. The search starts at the parent, so an element never resolves to itself.
35
- * @returns The closest entity element, or `null`.
36
+ * The nearest ancestor element that fronts an entity `<pc-entity>` or `<pc-node>` — or
37
+ * `null` if this element has no such ancestor. The search starts at the parent, so an element
38
+ * never resolves to itself.
39
+ * @returns The closest entity-fronting element, or `null`.
36
40
  */
37
- get closestEntity(): EntityElement | null {
38
- return this.parentElement?.closest('pc-entity') as EntityElement | null ?? null;
41
+ get closestEntity(): EntityBaseElement | null {
42
+ return (this.parentElement?.closest('pc-entity, pc-node') as EntityBaseElement | null) ?? null;
39
43
  }
40
44
 
41
45
  /**
42
46
  * Called when the element is fully initialized and ready. Subclasses should call this when
43
47
  * they're ready. Resolves the ready promise and dispatches a bubbling, composed `ready`
44
- * event.
48
+ * event. Signals at most once per readiness cycle: a repeat call before {@link _resetReady}
49
+ * has re-armed the promise does nothing.
45
50
  */
46
51
  protected _onReady() {
52
+ if (this._readyResolved) return;
53
+ this._readyResolved = true;
47
54
  this._readyResolve();
48
55
  this.dispatchEvent(new CustomEvent('ready', { bubbles: true, composed: true }));
49
56
  }
50
57
 
58
+ /**
59
+ * Returns the ready promise to its pending state. Subclasses should call this when the
60
+ * resource their readiness announced is torn down (typically from `disconnectedCallback`),
61
+ * so that a later re-initialization can signal readiness again. Does nothing while the
62
+ * promise is still pending — an in-flight waiter carries over to the next readiness cycle
63
+ * rather than being stranded on a promise nothing will ever resolve.
64
+ */
65
+ protected _resetReady() {
66
+ if (!this._readyResolved) return;
67
+ this._readyResolved = false;
68
+ this._readyPromise = new Promise<void>((resolve) => {
69
+ this._readyResolve = resolve;
70
+ });
71
+ }
72
+
51
73
  /**
52
74
  * Returns a promise that resolves with this element when it's ready. This is the low-level
53
75
  * primitive underlying {@link whenReady}, which is the recommended way to wait for elements.
76
+ *
77
+ * Readiness tracks the element's current lifecycle: once a ready element is torn down (for
78
+ * example by removing it from the document), this returns a fresh promise that resolves when
79
+ * the element is next ready. A promise obtained earlier stays resolved — call this again
80
+ * after re-inserting an element rather than reusing a promise from before its removal.
54
81
  * @returns A promise that resolves with this element when it's ready.
55
82
  */
56
83
  ready(): Promise<this> {
@@ -63,7 +90,7 @@ class AsyncElement extends HTMLElement {
63
90
  * classes extend {@link AsyncElement}).
64
91
  */
65
92
  type AsyncElementTagName = {
66
- [K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] extends AsyncElement ? K : never
93
+ [K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] extends AsyncElement ? K : never;
67
94
  }[keyof HTMLElementTagNameMap];
68
95
 
69
96
  /**
@@ -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,25 @@
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
+
6
23
  const projections = new Map<'perspective' | 'orthographic', number>([
7
24
  ['perspective', PROJECTION_PERSPECTIVE],
8
25
  ['orthographic', PROJECTION_ORTHOGRAPHIC]
@@ -68,7 +85,7 @@ class CameraComponentElement extends ComponentElement {
68
85
  super('camera');
69
86
  }
70
87
 
71
- getInitialComponentData() {
88
+ protected getInitialComponentData() {
72
89
  return {
73
90
  clearColor: this._clearColor,
74
91
  clearColorBuffer: this._clearColorBuffer,
@@ -101,7 +118,10 @@ class CameraComponentElement extends ComponentElement {
101
118
  * @param type - The type of XR mode to start.
102
119
  * @param space - The space to start the camera in.
103
120
  */
104
- 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
+ ) {
105
125
  if (this.component && this.xrAvailable) {
106
126
  this.component.startXr(type, space, {
107
127
  callback: (err: any) => {
@@ -559,10 +579,4 @@ class CameraComponentElement extends ComponentElement {
559
579
 
560
580
  customElements.define('pc-camera', CameraComponentElement);
561
581
 
562
- declare global {
563
- interface HTMLElementTagNameMap {
564
- 'pc-camera': CameraComponentElement;
565
- }
566
- }
567
-
568
582
  export { CameraComponentElement };
@@ -1,30 +1,38 @@
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.
9
11
  * The CollisionComponentElement interface also inherits the properties and methods of the
10
12
  * {@link HTMLElement} interface.
11
13
  *
14
+ * For `type="mesh"`, the collision geometry defaults to the host entity's own render component
15
+ * (its render asset) — a collider matching the visible mesh, which is what a mesh collider on a
16
+ * glTF node means. The default resolves each time the component applies, so a `pc-node` that
17
+ * retargets or rebinds picks up the new node's geometry. An entity with no asset-backed render
18
+ * component warns, and the collider has no shape.
19
+ *
12
20
  * @category Components
13
21
  */
14
22
  class CollisionComponentElement extends ComponentElement {
15
23
  private _angularOffset: Quat = new Quat();
16
24
 
17
- private _axis: number = 1;
25
+ private _axis = 1;
18
26
 
19
- private _convexHull: boolean = false;
27
+ private _convexHull = false;
20
28
 
21
29
  private _halfExtents: Vec3 = new Vec3(0.5, 0.5, 0.5);
22
30
 
23
- private _height: number = 2;
31
+ private _height = 2;
24
32
 
25
33
  private _linearOffset: Vec3 = new Vec3();
26
34
 
27
- private _radius: number = 0.5;
35
+ private _radius = 0.5;
28
36
 
29
37
  private _type: 'box' | 'capsule' | 'compound' | 'cone' | 'cylinder' | 'mesh' | 'sphere' = 'box';
30
38
 
@@ -33,7 +41,7 @@ class CollisionComponentElement extends ComponentElement {
33
41
  super('collision');
34
42
  }
35
43
 
36
- getInitialComponentData() {
44
+ protected getInitialComponentData() {
37
45
  return {
38
46
  axis: this._axis,
39
47
  angularOffset: this._angularOffset,
@@ -46,6 +54,34 @@ class CollisionComponentElement extends ComponentElement {
46
54
  };
47
55
  }
48
56
 
57
+ protected initComponent() {
58
+ this._applyMeshGeometryDefault();
59
+ }
60
+
61
+ /**
62
+ * Defaults a mesh collider's geometry to the host entity's own render component. The
63
+ * engine's mesh collider only works with explicitly supplied geometry, and the element has
64
+ * no attribute to supply it - so the host's visible geometry, the meaning a mesh collider
65
+ * on a glTF node carries, fills the gap. Runs on every application (so a rebound `pc-node`
66
+ * recomputes it) and on a runtime switch to `type="mesh"`; an explicitly assigned
67
+ * `renderAsset` is never overwritten.
68
+ */
69
+ private _applyMeshGeometryDefault() {
70
+ const component = this.component;
71
+ if (!component || this._type !== 'mesh' || component.renderAsset !== null) {
72
+ return;
73
+ }
74
+
75
+ const asset = component.entity.render?.asset ?? null;
76
+ if (asset === null) {
77
+ console.warn(
78
+ `pc-collision type="mesh" on '${component.entity.name}' found no asset-backed render component to take geometry from - collider has no shape`
79
+ );
80
+ return;
81
+ }
82
+ component.renderAsset = asset;
83
+ }
84
+
49
85
  /**
50
86
  * Gets the underlying PlayCanvas collision component.
51
87
  * @returns The collision component.
@@ -135,6 +171,7 @@ class CollisionComponentElement extends ComponentElement {
135
171
  this._type = value;
136
172
  if (this.component) {
137
173
  this.component.type = value;
174
+ this._applyMeshGeometryDefault();
138
175
  }
139
176
  }
140
177
 
@@ -143,7 +180,17 @@ class CollisionComponentElement extends ComponentElement {
143
180
  }
144
181
 
145
182
  static get observedAttributes() {
146
- return [...super.observedAttributes, 'angular-offset', 'axis', 'convex-hull', 'half-extents', 'height', 'linear-offset', 'radius', 'type'];
183
+ return [
184
+ ...super.observedAttributes,
185
+ 'angular-offset',
186
+ 'axis',
187
+ 'convex-hull',
188
+ 'half-extents',
189
+ 'height',
190
+ 'linear-offset',
191
+ 'radius',
192
+ 'type'
193
+ ];
147
194
  }
148
195
 
149
196
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
@@ -172,7 +219,12 @@ class CollisionComponentElement extends ComponentElement {
172
219
  this.radius = parseNumber(newValue, 0.5, name);
173
220
  break;
174
221
  case 'type':
175
- this.type = parseEnum(newValue, ['box', 'capsule', 'compound', 'cone', 'cylinder', 'mesh', 'sphere'], 'box', name);
222
+ this.type = parseEnum(
223
+ newValue,
224
+ ['box', 'capsule', 'compound', 'cone', 'cylinder', 'mesh', 'sphere'],
225
+ 'box',
226
+ name
227
+ );
176
228
  break;
177
229
  }
178
230
  }
@@ -180,10 +232,4 @@ class CollisionComponentElement extends ComponentElement {
180
232
 
181
233
  customElements.define('pc-collision', CollisionComponentElement);
182
234
 
183
- declare global {
184
- interface HTMLElementTagNameMap {
185
- 'pc-collision': CollisionComponentElement;
186
- }
187
- }
188
-
189
235
  export { CollisionComponentElement };
@@ -1,7 +1,8 @@
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
+ import type { EntityBaseElement } from '../entity-base';
5
6
  import { parseBool } from '../parse';
6
7
 
7
8
  /**
@@ -18,6 +19,27 @@ class ComponentElement extends AsyncElement {
18
19
 
19
20
  private _appElement: AppElement | null = null;
20
21
 
22
+ /**
23
+ * The element hosting this component, held so the host's readiness cycles can be observed
24
+ * even after `closestEntity` would no longer resolve (during teardown).
25
+ */
26
+ private _hostElement: EntityBaseElement | null = null;
27
+
28
+ /**
29
+ * The listener re-applying this component when the host's readiness cycles. Held for
30
+ * removal on disconnect.
31
+ */
32
+ private _hostReadyListener: EventListener | null = null;
33
+
34
+ /**
35
+ * Incremented on every connect and disconnect. connectedCallback captures the value on entry
36
+ * and abandons itself wherever it resumes from an await if the value has moved on — so a
37
+ * callback whose element was removed cannot act on a torn-down tree, and one whose element
38
+ * was removed and re-inserted (which runs a callback of its own) cannot add the component a
39
+ * second time.
40
+ */
41
+ private _connectionGeneration = 0;
42
+
21
43
  /**
22
44
  * Creates a new ComponentElement instance.
23
45
  *
@@ -30,38 +52,155 @@ class ComponentElement extends AsyncElement {
30
52
  this._componentName = componentName;
31
53
  }
32
54
 
33
- // Method to be overridden by subclasses to provide initial component data
34
- getInitialComponentData() {
55
+ /**
56
+ * Returns the data the component is created with. Overridden by subclasses to supply the
57
+ * initial values of their cached properties.
58
+ *
59
+ * @returns The initial component data.
60
+ */
61
+ protected getInitialComponentData() {
35
62
  return {};
36
63
  }
37
64
 
38
- async addComponent() {
65
+ /**
66
+ * Creates the component on the host's current entity, removing it first from a previous
67
+ * entity that is still alive (a retargeted `<pc-node>` moves its decorations with it). When
68
+ * the entity already has a component of this type — a glTF node arriving with its authored
69
+ * `render` component, say — warns and leaves `component` null. The element-level warning is
70
+ * load-bearing: the engine's own duplicate-addComponent warning is Debug-stripped from
71
+ * production builds, which would otherwise leave a silent null.
72
+ */
73
+ private _applyComponent() {
74
+ const entity = this._hostElement?.entity ?? null;
75
+ if (this._component && this._component.entity === entity) {
76
+ return;
77
+ }
78
+
79
+ // A retarget leaves the previous component on a still-live entity - remove it so the
80
+ // decoration follows the element, or vanishes with a dissolved binding. A destroyed
81
+ // entity took its components with it.
82
+ const previous = this._component;
83
+ if (previous?.entity && previous.entity.c[this._componentName] === previous) {
84
+ previous.entity.removeComponent(this._componentName);
85
+ }
86
+ this._component = null;
87
+
88
+ if (!entity) {
89
+ return;
90
+ }
91
+
92
+ if (entity.c[this._componentName]) {
93
+ const label = this.id ? ` '${this.id}'` : '';
94
+ console.warn(
95
+ `${this.tagName.toLowerCase()}${label} - '${entity.name}' already has a '${this._componentName}' component - component not added`
96
+ );
97
+ return;
98
+ }
99
+
100
+ this._component = entity.addComponent(this._componentName, this.getInitialComponentData());
101
+ }
102
+
103
+ private async _addComponent() {
104
+ const generation = this._connectionGeneration;
105
+
39
106
  const entityElement = this.closestEntity;
40
107
  if (!entityElement) {
41
108
  // A component can only exist on an entity, so an element placed outside one is inert.
42
109
  // It still becomes ready (with a null `component`), so warn rather than fail silently
43
110
  const label = this.id ? ` '${this.id}'` : '';
44
- console.warn(`${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity - component not added`);
111
+ console.warn(
112
+ `${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity - component not added`
113
+ );
45
114
  return;
46
115
  }
47
116
 
48
117
  await entityElement.ready();
49
- // Add the component to the entity
50
- const data = this.getInitialComponentData();
51
- this._component = entityElement.entity!.addComponent(this._componentName, data);
118
+
119
+ // The element may have been removed, or removed and re-inserted, while the entity became
120
+ // ready — the component belongs to the connection that owns the current generation.
121
+ if (generation !== this._connectionGeneration) {
122
+ return;
123
+ }
124
+
125
+ this._hostElement = entityElement;
126
+ this._applyComponent();
127
+
128
+ // Re-apply when the host's readiness cycles without this element disconnecting: a
129
+ // `<pc-node>` rebinding after its model reloads or retargets, or a re-created entity.
130
+ // The 'ready' event bubbles, so events from descendants pass through this host - only
131
+ // the host's own cycles count. Readiness is cycled here too, so decorations one level
132
+ // down re-apply the same way.
133
+ this._hostReadyListener = (event: Event) => {
134
+ if (event.target !== this._hostElement) {
135
+ return;
136
+ }
137
+ if (generation !== this._connectionGeneration) {
138
+ return;
139
+ }
140
+ this._hostCycled();
141
+ };
142
+ entityElement.addEventListener('ready', this._hostReadyListener);
52
143
  }
53
144
 
54
- initComponent() {}
145
+ /**
146
+ * Re-evaluates this component against the host's current entity: applied to a new entity,
147
+ * moved from a still-live old one, or removed when the host no longer fronts an entity at
148
+ * all. Readiness follows - it cycles with a re-application and stays unresolved while the
149
+ * host is unbound. Called by the host-ready listener, and directly by a `<pc-node>`
150
+ * dissolving its binding: the one transition that fires no ready event to ride.
151
+ *
152
+ * @internal
153
+ */
154
+ _hostCycled() {
155
+ this._resetReady();
156
+ this._applyComponent();
157
+ if (this._hostElement?.entity) {
158
+ this.initComponent();
159
+ this._onReady();
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Configures the newly added component. Overridden by subclasses whose setup goes beyond
165
+ * the initial data — child-element handling, asset resolution and the like.
166
+ */
167
+ protected initComponent() {
168
+ // optional hook
169
+ }
55
170
 
56
171
  async connectedCallback() {
172
+ const generation = ++this._connectionGeneration;
173
+
57
174
  this._appElement = this.closestApp ?? null;
58
175
  await this._appElement?.ready();
59
- await this.addComponent();
176
+
177
+ // The element may have been removed, or removed and re-inserted, while the application
178
+ // became ready. A re-insertion runs a connectedCallback of its own, so a stale resume
179
+ // must not add the component alongside it.
180
+ if (generation !== this._connectionGeneration) {
181
+ return;
182
+ }
183
+
184
+ await this._addComponent();
185
+
186
+ if (generation !== this._connectionGeneration) {
187
+ return;
188
+ }
189
+
60
190
  this.initComponent();
61
191
  this._onReady();
62
192
  }
63
193
 
64
194
  disconnectedCallback() {
195
+ // Invalidate any connectedCallback still suspended on an await
196
+ this._connectionGeneration++;
197
+
198
+ if (this._hostElement && this._hostReadyListener) {
199
+ this._hostElement.removeEventListener('ready', this._hostReadyListener);
200
+ }
201
+ this._hostElement = null;
202
+ this._hostReadyListener = null;
203
+
65
204
  // Remove the component when the element is disconnected. Skip this when the owning
66
205
  // application has already been destroyed — removing a <pc-app> disconnects it before
67
206
  // its children, taking the component systems with it.
@@ -70,6 +209,7 @@ class ComponentElement extends AsyncElement {
70
209
  }
71
210
  this._component = null;
72
211
  this._appElement = null;
212
+ this._resetReady();
73
213
  }
74
214
 
75
215
  /**
@@ -1,9 +1,11 @@
1
- import { Color, ElementComponent, Vec2, Vec4 } from 'playcanvas';
1
+ import type { ElementComponent } from 'playcanvas';
2
+ import { Color, Vec2, Vec4 } from 'playcanvas';
2
3
 
3
4
  import { AssetElement } from '../asset';
4
- import { ComponentElement } from './component';
5
5
  import { parseBool, parseColor, parseEnum, parseNumber, parseVec2, parseVec4 } from '../parse';
6
6
 
7
+ import { ComponentElement } from './component';
8
+
7
9
  /**
8
10
  * The ElementComponentElement interface provides properties and methods for manipulating
9
11
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-element/ | `<pc-element>`} elements.
@@ -15,62 +17,62 @@ import { parseBool, parseColor, parseEnum, parseNumber, parseVec2, parseVec4 } f
15
17
  class ElementComponentElement extends ComponentElement {
16
18
  private _anchor: Vec4 = new Vec4(0.5, 0.5, 0.5, 0.5);
17
19
 
18
- private _autoWidth: boolean = true;
20
+ private _autoWidth = true;
19
21
 
20
- private _autoHeight: boolean = true;
22
+ private _autoHeight = true;
21
23
 
22
- private _autoFitWidth: boolean = false;
24
+ private _autoFitWidth = false;
23
25
 
24
- private _autoFitHeight: boolean = false;
26
+ private _autoFitHeight = false;
25
27
 
26
28
  private _color: Color = new Color(1, 1, 1, 1);
27
29
 
28
- private _enableMarkup: boolean = false;
30
+ private _enableMarkup = false;
29
31
 
30
- private _fontAsset: string = '';
32
+ private _fontAsset = '';
31
33
 
32
- private _fontSize: number = 32;
34
+ private _fontSize = 32;
33
35
 
34
- private _maxFontSize: number = 32;
36
+ private _maxFontSize = 32;
35
37
 
36
- private _minFontSize: number = 8;
38
+ private _minFontSize = 8;
37
39
 
38
- private _height: number = 0;
40
+ private _height = 0;
39
41
 
40
- private _lineHeight: number = 32;
42
+ private _lineHeight = 32;
41
43
 
42
44
  private _margin: Vec4 | null = null;
43
45
 
44
- private _mask: boolean = false;
46
+ private _mask = false;
45
47
 
46
- private _opacity: number = 1;
48
+ private _opacity = 1;
47
49
 
48
50
  private _pivot: Vec2 = new Vec2(0.5, 0.5);
49
51
 
50
52
  private _pixelsPerUnit: number | null = null;
51
53
 
52
- private _spriteAsset: string = '';
54
+ private _spriteAsset = '';
53
55
 
54
- private _spriteFrame: number = 0;
56
+ private _spriteFrame = 0;
55
57
 
56
- private _text: string = '';
58
+ private _text = '';
57
59
 
58
- private _textureAsset: string = '';
60
+ private _textureAsset = '';
59
61
 
60
62
  private _type: 'group' | 'image' | 'text' = 'group';
61
63
 
62
- private _useInput: boolean = false;
64
+ private _useInput = false;
63
65
 
64
- private _width: number = 0;
66
+ private _width = 0;
65
67
 
66
- private _wrapLines: boolean = false;
68
+ private _wrapLines = false;
67
69
 
68
70
  /** @ignore */
69
71
  constructor() {
70
72
  super('element');
71
73
  }
72
74
 
73
- initComponent() {
75
+ protected initComponent() {
74
76
  const component = this.component as any;
75
77
  if (!component) {
76
78
  return;
@@ -89,7 +91,7 @@ class ElementComponentElement extends ComponentElement {
89
91
  component._dirtifyMask?.();
90
92
  }
91
93
 
92
- getInitialComponentData() {
94
+ protected getInitialComponentData() {
93
95
  const data: Record<string, any> = {
94
96
  anchor: this._anchor,
95
97
  autoWidth: this._autoWidth,
@@ -774,10 +776,4 @@ class ElementComponentElement extends ComponentElement {
774
776
 
775
777
  customElements.define('pc-element', ElementComponentElement);
776
778
 
777
- declare global {
778
- interface HTMLElementTagNameMap {
779
- 'pc-element': ElementComponentElement;
780
- }
781
- }
782
-
783
779
  export { ElementComponentElement };