@playcanvas/web-components 0.16.0 → 0.18.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 (126) hide show
  1. package/README.md +5 -3
  2. package/dist/app.d.cts +18 -13
  3. package/dist/app.d.ts +18 -13
  4. package/dist/asset.d.cts +13 -3
  5. package/dist/asset.d.ts +13 -3
  6. package/dist/async-element.d.cts +13 -13
  7. package/dist/async-element.d.ts +13 -13
  8. package/dist/components/anim-clip.d.cts +4 -0
  9. package/dist/components/anim-clip.d.ts +4 -0
  10. package/dist/components/anim-component.d.cts +38 -5
  11. package/dist/components/anim-component.d.ts +38 -5
  12. package/dist/components/audio-listener-component.d.cts +26 -0
  13. package/dist/components/audio-listener-component.d.ts +26 -0
  14. package/dist/components/button-component.d.cts +10 -1
  15. package/dist/components/button-component.d.ts +10 -1
  16. package/dist/components/camera-component.d.cts +37 -1
  17. package/dist/components/camera-component.d.ts +37 -1
  18. package/dist/components/collision-component.d.cts +9 -1
  19. package/dist/components/collision-component.d.ts +9 -1
  20. package/dist/components/component.d.cts +3 -2
  21. package/dist/components/component.d.ts +3 -2
  22. package/dist/components/element-component.d.cts +11 -0
  23. package/dist/components/element-component.d.ts +11 -0
  24. package/dist/components/gsplat-component.d.cts +6 -0
  25. package/dist/components/gsplat-component.d.ts +6 -0
  26. package/dist/components/joint-component.d.cts +14 -2
  27. package/dist/components/joint-component.d.ts +14 -2
  28. package/dist/components/{layoutchild-component.d.cts → layout-child-component.d.cts} +7 -1
  29. package/dist/components/{layoutchild-component.d.ts → layout-child-component.d.ts} +7 -1
  30. package/dist/components/{layoutgroup-component.d.cts → layout-group-component.d.cts} +7 -1
  31. package/dist/components/{layoutgroup-component.d.ts → layout-group-component.d.ts} +7 -1
  32. package/dist/components/light-component.d.cts +52 -1
  33. package/dist/components/light-component.d.ts +52 -1
  34. package/dist/components/{particlesystem-component.d.cts → particle-system-component.d.cts} +7 -1
  35. package/dist/components/{particlesystem-component.d.ts → particle-system-component.d.ts} +7 -1
  36. package/dist/components/render-component.d.cts +7 -0
  37. package/dist/components/render-component.d.ts +7 -0
  38. package/dist/components/{rigidbody-component.d.cts → rigid-body-component.d.cts} +8 -1
  39. package/dist/components/{rigidbody-component.d.ts → rigid-body-component.d.ts} +8 -1
  40. package/dist/components/screen-component.d.cts +6 -0
  41. package/dist/components/screen-component.d.ts +6 -0
  42. package/dist/components/script-component.d.cts +26 -12
  43. package/dist/components/script-component.d.ts +26 -12
  44. package/dist/components/{script.d.cts → script-instance.d.cts} +11 -7
  45. package/dist/components/{script.d.ts → script-instance.d.ts} +11 -7
  46. package/dist/components/{scrollview-component.d.cts → scroll-view-component.d.cts} +7 -1
  47. package/dist/components/{scrollview-component.d.ts → scroll-view-component.d.ts} +7 -1
  48. package/dist/components/scrollbar-component.d.cts +6 -0
  49. package/dist/components/scrollbar-component.d.ts +6 -0
  50. package/dist/components/sound-component.d.cts +7 -1
  51. package/dist/components/sound-component.d.ts +7 -1
  52. package/dist/components/sound-slot.d.cts +5 -2
  53. package/dist/components/sound-slot.d.ts +5 -2
  54. package/dist/custom-elements.json +1852 -974
  55. package/dist/entity-base.d.cts +7 -6
  56. package/dist/entity-base.d.ts +7 -6
  57. package/dist/entity-owner.d.cts +118 -0
  58. package/dist/entity-owner.d.ts +118 -0
  59. package/dist/entity.d.cts +14 -102
  60. package/dist/entity.d.ts +14 -102
  61. package/dist/index.d.cts +26 -22
  62. package/dist/index.d.ts +26 -22
  63. package/dist/material.d.cts +18 -7
  64. package/dist/material.d.ts +18 -7
  65. package/dist/model.d.cts +72 -24
  66. package/dist/model.d.ts +72 -24
  67. package/dist/node.d.cts +6 -1
  68. package/dist/node.d.ts +6 -1
  69. package/dist/pwc.cjs +807 -293
  70. package/dist/pwc.cjs.map +1 -1
  71. package/dist/pwc.js +807 -293
  72. package/dist/pwc.js.map +1 -1
  73. package/dist/pwc.min.js +1 -1
  74. package/dist/pwc.min.js.map +1 -1
  75. package/dist/pwc.min.mjs +1 -1
  76. package/dist/pwc.min.mjs.map +1 -1
  77. package/dist/pwc.mjs +805 -292
  78. package/dist/pwc.mjs.map +1 -1
  79. package/dist/scene.d.cts +3 -0
  80. package/dist/scene.d.ts +3 -0
  81. package/dist/sky.d.cts +4 -0
  82. package/dist/sky.d.ts +4 -0
  83. package/dist/vscode.html-custom-data.json +351 -121
  84. package/dist/{module.d.cts → wasm.d.cts} +10 -6
  85. package/dist/{module.d.ts → wasm.d.ts} +10 -6
  86. package/dist/web-types.json +465 -291
  87. package/package.json +7 -7
  88. package/src/app.ts +49 -35
  89. package/src/asset.ts +13 -4
  90. package/src/async-element.ts +14 -14
  91. package/src/components/anim-clip.ts +4 -0
  92. package/src/components/anim-component.ts +89 -14
  93. package/src/components/audio-listener-component.ts +36 -0
  94. package/src/components/button-component.ts +10 -1
  95. package/src/components/camera-component.ts +65 -6
  96. package/src/components/collision-component.ts +9 -1
  97. package/src/components/component.ts +4 -3
  98. package/src/components/element-component.ts +11 -0
  99. package/src/components/gsplat-component.ts +6 -0
  100. package/src/components/joint-component.ts +14 -2
  101. package/src/components/{layoutchild-component.ts → layout-child-component.ts} +8 -2
  102. package/src/components/{layoutgroup-component.ts → layout-group-component.ts} +8 -2
  103. package/src/components/light-component.ts +110 -17
  104. package/src/components/{particlesystem-component.ts → particle-system-component.ts} +8 -2
  105. package/src/components/render-component.ts +7 -0
  106. package/src/components/{rigidbody-component.ts → rigid-body-component.ts} +9 -2
  107. package/src/components/screen-component.ts +6 -0
  108. package/src/components/script-component.ts +66 -42
  109. package/src/components/{script.ts → script-instance.ts} +18 -14
  110. package/src/components/{scrollview-component.ts → scroll-view-component.ts} +8 -2
  111. package/src/components/scrollbar-component.ts +6 -0
  112. package/src/components/sound-component.ts +8 -2
  113. package/src/components/sound-slot.ts +7 -4
  114. package/src/entity-base.ts +8 -7
  115. package/src/entity-owner.ts +298 -0
  116. package/src/entity.ts +21 -271
  117. package/src/index.ts +38 -25
  118. package/src/material.ts +19 -8
  119. package/src/model.ts +164 -72
  120. package/src/node.ts +15 -6
  121. package/src/scene.ts +3 -0
  122. package/src/sky.ts +4 -0
  123. package/src/{module.ts → wasm.ts} +12 -8
  124. package/dist/components/listener-component.d.cts +0 -20
  125. package/dist/components/listener-component.d.ts +0 -20
  126. package/src/components/listener-component.ts +0 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcanvas/web-components",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "author": "PlayCanvas <support@playcanvas.com>",
5
5
  "homepage": "https://playcanvas.com",
6
6
  "description": "Web Components for the PlayCanvas Engine",
@@ -91,25 +91,25 @@
91
91
  "@rollup/plugin-terser": "1.0.0",
92
92
  "@rollup/plugin-typescript": "12.3.0",
93
93
  "@tweenjs/tween.js": "25.0.0",
94
- "@vitest/coverage-v8": "4.1.10",
95
- "concurrently": "10.0.4",
94
+ "@vitest/coverage-v8": "4.1.11",
95
+ "concurrently": "10.0.5",
96
96
  "custom-element-jet-brains-integration": "1.7.0",
97
97
  "custom-element-vs-code-integration": "1.5.0",
98
98
  "earcut": "3.2.3",
99
99
  "eslint": "9.39.5",
100
100
  "globals": "17.11.0",
101
101
  "jsdom": "30.0.1",
102
- "mediabunny": "1.53.1",
102
+ "mediabunny": "1.55.2",
103
103
  "opentype.js": "2.0.0",
104
104
  "playcanvas": "2.21.4",
105
105
  "prettier": "3.9.6",
106
- "publint": "0.3.23",
107
- "rollup": "4.62.4",
106
+ "publint": "0.3.24",
107
+ "rollup": "4.62.5",
108
108
  "serve": "14.2.6",
109
109
  "tslib": "2.8.1",
110
110
  "typedoc": "0.28.20",
111
111
  "typedoc-plugin-mdn-links": "5.1.1",
112
112
  "typescript": "6.0.3",
113
- "vitest": "4.1.10"
113
+ "vitest": "4.1.11"
114
114
  }
115
115
  }
package/src/app.ts CHANGED
@@ -66,16 +66,23 @@ import {
66
66
 
67
67
  import type { AssetElement } from './asset';
68
68
  import { AsyncElement } from './async-element';
69
- import type { EntityElement } from './entity';
70
69
  import type { EntityBaseElement } from './entity-base';
70
+ import type { EntityOwnerElement } from './entity-owner';
71
71
  import { LoadingBar } from './loading-bar';
72
72
  import type { MaterialElement } from './material';
73
- import type { ModuleElement } from './module';
74
73
  import { parseBool, parseEnum, parseNumber } from './parse';
74
+ import type { WasmElement } from './wasm';
75
75
 
76
76
  /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
77
77
  const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'] as const;
78
78
 
79
+ /**
80
+ * The event types whose listeners make an element a hover target. Hover resolution walks past
81
+ * elements listening for none of them, so a silent element never swallows an ancestor's
82
+ * enter/leave pair.
83
+ */
84
+ const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'] as const;
85
+
79
86
  /**
80
87
  * Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
81
88
  * box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
@@ -105,6 +112,10 @@ const ensureBaseStyles = () => {
105
112
  * tracked live via a ResizeObserver — so the element can be embedded at any size, resized by
106
113
  * its container, or made fullscreen with ordinary CSS such as `width: 100vw; height: 100dvh`.
107
114
  *
115
+ * @elementSummary The `<pc-app>` element creates a PlayCanvas application and the canvas it renders
116
+ * into, and is the root of every scene. It holds the `<pc-asset>`, `<pc-material>`, `<pc-wasm>` and
117
+ * `<pc-scene>` elements, and the page's CSS sizes it, as it would a `<video>`.
118
+ *
108
119
  * @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
109
120
  * `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
110
121
  * Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
@@ -163,9 +174,9 @@ class AppElement extends AsyncElement {
163
174
 
164
175
  /**
165
176
  * The elements backing this application's entities, keyed by the entity itself. Registered
166
- * by EntityElement at creation (and NodeElement at binding) and removed when an entity is
167
- * destroyed or unbound, this joins engine scene nodes back to their owning elements by
168
- * identity - never by name.
177
+ * by entity-owning elements at creation (pc-entity, and pc-model for its host) and by
178
+ * NodeElement at binding, and removed when an entity is destroyed or unbound, this joins
179
+ * engine scene nodes back to their owning elements by identity - never by name.
169
180
  */
170
181
  private _entityElements = new Map<GraphNode, EntityBaseElement>();
171
182
 
@@ -260,7 +271,7 @@ class AppElement extends AsyncElement {
260
271
  // there, leaving the element permanently unready: no canvas, no entities, no application.
261
272
  //
262
273
  // Upgrading is the fix here rather than skipping whatever has not upgraded, because a
263
- // <pc-module> is the one child that nothing else ever builds on its own behalf - skipping
274
+ // <pc-wasm> is the one child that nothing else ever builds on its own behalf - skipping
264
275
  // it would drop the wasm module the app asked for, silently and only for cloned apps.
265
276
  // Upgrading runs each descendant's connectedCallback synchronously, a few lines earlier
266
277
  // than the parser's path runs them but into the same state they see there: no application
@@ -270,11 +281,11 @@ class AppElement extends AsyncElement {
270
281
  // is left completely untouched.
271
282
  customElements.upgrade(this);
272
283
 
273
- // Get all pc-module elements that are direct children of the pc-app element
274
- const moduleElements = this.querySelectorAll<ModuleElement>(':scope > pc-module');
284
+ // Get all pc-wasm elements that are direct children of the pc-app element
285
+ const wasmElements = this.querySelectorAll<WasmElement>(':scope > pc-wasm');
275
286
 
276
287
  // Wait for all modules to load
277
- await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
288
+ await Promise.all(Array.from(wasmElements).map((element) => element._getLoadPromise()));
278
289
 
279
290
  // The element may have been removed while the modules loaded. Nothing beyond the loading
280
291
  // bar exists yet, and disconnectedCallback has already destroyed that.
@@ -462,15 +473,16 @@ class AppElement extends AsyncElement {
462
473
  materialElement._createMaterial();
463
474
  });
464
475
 
465
- // Create all entities
466
- const entityElements = this.querySelectorAll<EntityElement>('pc-entity');
467
- Array.from(entityElements).forEach((entityElement) => {
468
- entityElement._createEntity(app);
476
+ // Create all entities. pc-model joins the sweep because it owns a host entity of its
477
+ // own; its instantiated content arrives later, beneath that host.
478
+ const ownerElements = this.querySelectorAll<EntityOwnerElement>('pc-entity, pc-model');
479
+ Array.from(ownerElements).forEach((ownerElement) => {
480
+ ownerElement._createEntity(app);
469
481
  });
470
482
 
471
483
  // Build hierarchy
472
- entityElements.forEach((entityElement) => {
473
- entityElement._buildHierarchy(app);
484
+ ownerElements.forEach((ownerElement) => {
485
+ ownerElement._buildHierarchy(app);
474
486
  });
475
487
 
476
488
  // Building the hierarchy dispatched each entity's ready event synchronously, and a
@@ -602,9 +614,9 @@ class AppElement extends AsyncElement {
602
614
  // created from onpointer* attributes when their elements were first upgraded, or
603
615
  // listeners carried over from before a re-boot)
604
616
  pointerEventTypes.forEach((type) => {
605
- const anyListeners = Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')).some(
606
- (entity) => entity._hasListeners(type)
607
- );
617
+ const anyListeners = Array.from(
618
+ this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node')
619
+ ).some((entity) => entity._hasListeners(type));
608
620
  if (anyListeners) {
609
621
  this._onPointerListenerAdded(type);
610
622
  }
@@ -659,10 +671,10 @@ class AppElement extends AsyncElement {
659
671
  }
660
672
 
661
673
  /**
662
- * Returns the `<pc-entity>` or `<pc-node>` element whose backing entity is `entity`, or
663
- * `null` if the entity is not fronted by an element of this application - for example, an
664
- * unbound node inside a model's instantiated hierarchy, or an entity created through the
665
- * engine API.
674
+ * Returns the `<pc-entity>`, `<pc-model>` or `<pc-node>` element whose backing entity is
675
+ * `entity`, or `null` if the entity is not fronted by an element of this application - for
676
+ * example, an unbound node inside a model's instantiated hierarchy, or an entity created
677
+ * through the engine API.
666
678
  *
667
679
  * @param entity - The entity to look up.
668
680
  * @returns The element fronting the entity, or `null`.
@@ -672,18 +684,19 @@ class AppElement extends AsyncElement {
672
684
  }
673
685
 
674
686
  /**
675
- * Resolves the element that owns a picked node: the nearest node up the parent chain -
676
- * starting with the node itself - that is fronted by a `<pc-entity>` or `<pc-node>` of this
677
- * application. A hit inside a model's instantiated hierarchy therefore resolves to the
678
- * nearest bound `<pc-node>`, or failing that the element hosting the model.
687
+ * Resolves the element that owns hover for a picked node: the nearest node up the parent
688
+ * chain - starting with the node itself - whose element listens for any of the hover event
689
+ * types. Skipping silent elements matches {@link _elementWithListener}, so a registered
690
+ * element with no hover listeners (a `<pc-model>` host, a plain child entity) is transparent
691
+ * to hover rather than swallowing a listening ancestor's enter/leave pair.
679
692
  *
680
693
  * @param node - The picked node, or `null`.
681
- * @returns The owning element, or `null`.
694
+ * @returns The hover-owning element, or `null`.
682
695
  */
683
- private _elementFromNode(node: GraphNode | null): EntityBaseElement | null {
696
+ private _hoverTarget(node: GraphNode | null): EntityBaseElement | null {
684
697
  while (node !== null) {
685
698
  const element = this._entityElements.get(node);
686
- if (element) {
699
+ if (element && hoverEventTypes.some((type) => element._hasListeners(type))) {
687
700
  return element;
688
701
  }
689
702
  node = node.parent;
@@ -831,9 +844,10 @@ class AppElement extends AsyncElement {
831
844
  const node = await this._pickNode(event);
832
845
  if (token !== this._pickToken || !this._picker) return;
833
846
 
834
- // The hovered element is the nearest one up the node's parent chain, listening or not -
835
- // dispatch is gated per event type below
836
- const newHoverEntity = this._elementFromNode(node);
847
+ // The hovered element is the nearest one up the node's parent chain with a hover
848
+ // listener - the nearest-listener rule down/up use. Dispatch is still gated per event
849
+ // type below: having any hover listener selects the target, each event needs its own.
850
+ const newHoverEntity = this._hoverTarget(node);
837
851
 
838
852
  // Handle enter/leave events
839
853
  if (this._hoveredEntity !== newHoverEntity) {
@@ -898,9 +912,9 @@ class AppElement extends AsyncElement {
898
912
  }
899
913
 
900
914
  private _onPointerListenerRemoved(type: string) {
901
- const hasListeners = Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')).some((entity) =>
902
- entity._hasListeners(type)
903
- );
915
+ const hasListeners = Array.from(
916
+ this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node')
917
+ ).some((entity) => entity._hasListeners(type));
904
918
 
905
919
  if (!hasListeners && this._canvas) {
906
920
  this._hasPointerListeners[type] = false;
package/src/asset.ts CHANGED
@@ -26,7 +26,8 @@ const renderModes = new Map<'simple' | 'sliced' | 'tiled', number>([
26
26
  ['tiled', SPRITE_RENDERMODE_TILED]
27
27
  ]);
28
28
 
29
- type AddressMode = 'repeat' | 'clamp' | 'mirror';
29
+ /** The addressing modes for a texture asset. */
30
+ export type AddressMode = 'repeat' | 'clamp' | 'mirror';
30
31
 
31
32
  const addressModes = new Map<AddressMode, number>([
32
33
  ['repeat', ADDRESS_REPEAT],
@@ -34,7 +35,8 @@ const addressModes = new Map<AddressMode, number>([
34
35
  ['mirror', ADDRESS_MIRRORED_REPEAT]
35
36
  ]);
36
37
 
37
- type MinFilterMode =
38
+ /** The minification filter modes for a texture asset. */
39
+ export type MinFilterMode =
38
40
  'nearest' | 'linear' | 'nearest-mip-nearest' | 'linear-mip-nearest' | 'nearest-mip-linear' | 'linear-mip-linear';
39
41
 
40
42
  const minFilterModes = new Map<MinFilterMode, number>([
@@ -46,8 +48,11 @@ const minFilterModes = new Map<MinFilterMode, number>([
46
48
  ['linear-mip-linear', FILTER_LINEAR_MIPMAP_LINEAR]
47
49
  ]);
48
50
 
49
- // Magnification has no mip variants - the engine (and the GPU) only accepts these two.
50
- type MagFilterMode = 'nearest' | 'linear';
51
+ /**
52
+ * The magnification filter modes for a texture asset. Magnification has no mip variants - the
53
+ * engine (and the GPU) only accepts these two.
54
+ */
55
+ export type MagFilterMode = 'nearest' | 'linear';
51
56
 
52
57
  const magFilterModes = new Map<MagFilterMode, number>([
53
58
  ['nearest', FILTER_NEAREST],
@@ -168,6 +173,10 @@ const processBufferView = (
168
173
  * Apart from `lazy` and the texture options, these attributes are read once when the asset is
169
174
  * created, so changing them later has no effect.
170
175
  *
176
+ * @elementSummary The `<pc-asset>` element declares an asset for the application to load — a model,
177
+ * texture, font, sound, script or JSON file — under an `id` that other elements reference. Must be
178
+ * a direct child of `<pc-app>`.
179
+ *
171
180
  * @attribute {string} id - The identifier used to reference the asset from other elements.
172
181
  * @attribute {string} src - The URL of the asset to load.
173
182
  * @attribute {string} type - The asset type. Inferred from the `src` file extension when omitted.
@@ -33,13 +33,13 @@ class AsyncElement extends HTMLElement {
33
33
  }
34
34
 
35
35
  /**
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.
36
+ * The nearest ancestor element that fronts an entity — `<pc-entity>`, `<pc-model>` or
37
+ * `<pc-node>` — or `null` if this element has no such ancestor. The search starts at the
38
+ * parent, so an element never resolves to itself.
39
39
  * @returns The closest entity-fronting element, or `null`.
40
40
  */
41
41
  get closestEntity(): EntityBaseElement | null {
42
- return (this.parentElement?.closest('pc-entity, pc-node') as EntityBaseElement | null) ?? null;
42
+ return (this.parentElement?.closest('pc-entity, pc-model, pc-node') as EntityBaseElement | null) ?? null;
43
43
  }
44
44
 
45
45
  /**
@@ -95,11 +95,11 @@ type AsyncElementTagName = {
95
95
 
96
96
  /**
97
97
  * Waits for the first element matching the given tag name to be fully initialized. Note that the
98
- * promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
99
- * that is not a direct child of `<pc-scripts>`, or a `<pc-app>` that could not create a graphics
100
- * device — listen for its `error` event instead). A component element outside a `<pc-entity>` is
101
- * the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
102
- * element logs a warning naming the parent it requires.
98
+ * promise never settles if the element cannot finish initializing (for example, a `<pc-script-instance>`
99
+ * that is not a direct child of `<pc-script>`, or a `<pc-app>` that could not create a graphics
100
+ * device — listen for its `error` event instead). A component element outside an entity-fronting
101
+ * element is the exception: it still becomes ready, but its `component` is `null`. Either way, a
102
+ * misplaced element logs a warning naming the parent it requires.
103
103
  * @param target - The tag name of the element to wait for (e.g. `'pc-app'`).
104
104
  * @returns A promise that resolves with the element once it's ready.
105
105
  * @example
@@ -120,11 +120,11 @@ function whenReady<K extends AsyncElementTagName>(target: K): Promise<HTMLElemen
120
120
  function whenReady<T extends AsyncElement>(target: T): Promise<T>;
121
121
  /**
122
122
  * Waits for the first element matching the given CSS selector to be fully initialized. Note that
123
- * the promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
124
- * that is not a direct child of `<pc-scripts>`, or a `<pc-app>` that could not create a graphics
125
- * device — listen for its `error` event instead). A component element outside a `<pc-entity>` is
126
- * the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
127
- * element logs a warning naming the parent it requires.
123
+ * the promise never settles if the element cannot finish initializing (for example, a `<pc-script-instance>`
124
+ * that is not a direct child of `<pc-script>`, or a `<pc-app>` that could not create a graphics
125
+ * device — listen for its `error` event instead). A component element outside an entity-fronting
126
+ * element is the exception: it still becomes ready, but its `component` is `null`. Either way, a
127
+ * misplaced element logs a warning naming the parent it requires.
128
128
  * @param target - A CSS selector matching the element to wait for (e.g. `'#my-app'`).
129
129
  * @returns A promise that resolves with the element once it's ready.
130
130
  * @example
@@ -22,6 +22,10 @@ import { AnimComponentElement } from './anim-component';
22
22
  * named; in a multi-track source the track named `name` is chosen, falling back to the first
23
23
  * with a warning. The element becomes ready once its resolved track is assigned.
24
24
  *
25
+ * @elementSummary The `<pc-anim-clip>` element declares one named animation clip on its parent
26
+ * `<pc-anim>`, taken from the `asset` it names or, without one, from the enclosing `<pc-model>`'s
27
+ * own animations. Must be a direct child of `<pc-anim>`.
28
+ *
25
29
  * @category Components
26
30
  */
27
31
  class AnimClipElement extends AsyncElement {
@@ -1,4 +1,4 @@
1
- import type { AnimComponent, Asset, ContainerResource } from 'playcanvas';
1
+ import type { AnimComponent, Asset, ContainerResource, Entity } from 'playcanvas';
2
2
  import { ANIM_CONTROL_STATES, AnimTrack } from 'playcanvas';
3
3
 
4
4
  import { AssetElement } from '../asset';
@@ -46,6 +46,13 @@ type PlaybackState = {
46
46
  * the underlying {@link AnimComponent} (via {@link component}) for playback state beyond what
47
47
  * this element exposes.
48
48
  *
49
+ * Engine component: {@link AnimComponent} (`anim`).
50
+ *
51
+ * @elementSummary The `<pc-anim>` element plays animation clips over its entity's hierarchy, taken
52
+ * from `<pc-anim-clip>` children or from the enclosing `<pc-model>`'s own animations. The first
53
+ * clip plays automatically, and the `clip` attribute switches between them. Must be a child of a
54
+ * `<pc-entity>`, `<pc-model>` or `<pc-node>`.
55
+ *
49
56
  * @category Components
50
57
  */
51
58
  class AnimComponentElement extends ComponentElement {
@@ -71,6 +78,12 @@ class AnimComponentElement extends ComponentElement {
71
78
  */
72
79
  private _clip = '';
73
80
 
81
+ /**
82
+ * The binding root this element last assigned through {@link _applyRootBone}, distinguishing
83
+ * its own writes from a `rootBone` assigned through the engine API — which is left alone.
84
+ */
85
+ private _managedRootBone: Entity | null = null;
86
+
74
87
  /**
75
88
  * The element the model-readiness listener is attached to, held so disconnection can detach
76
89
  * it after `closestEntity` no longer resolves.
@@ -101,28 +114,76 @@ class AnimComponentElement extends ComponentElement {
101
114
  private _warnedClip: string | null = null;
102
115
 
103
116
  /**
104
- * Rebinds when a model under the host announces readiness. The engine resolves each curve
105
- * once, at the first tick after assignment, and never retries and its mesh-instance
117
+ * Rebinds when a model that is not this component's host announces readiness a sibling or
118
+ * deeper model whose content changed under the assigned clips. The engine resolves each
119
+ * curve once, at the first tick after assignment, and never retries — and its mesh-instance
106
120
  * broadcast fires before an instantiated hierarchy is parented, so a model that loads after
107
- * the clips were assigned would otherwise stay silently unbound. A re-instantiation of the
108
- * implicit clip source (the parent `<pc-model>`) means a new container, so the clip set
109
- * refreshes instead unless every clip declares its own asset, where a rebind suffices.
121
+ * the clips were assigned would otherwise stay silently unbound. The host model's own cycles
122
+ * are excluded: those re-enter through {@link initComponent}, which refreshes the whole clip
123
+ * set against the new container.
110
124
  */
111
125
  private _onModelReady = (event: Event) => {
112
126
  if (!(event.target instanceof ModelElement) || !this.component) {
113
127
  return;
114
128
  }
115
- if (event.target === this.parentElement) {
116
- const implicit = this._autoAssigned ||
117
- [...this._assignedClips.values()].some(clip => !clip.asset);
118
- if (implicit) {
119
- this._refreshClips();
120
- return;
121
- }
129
+ // The host model's own readiness cycle already re-initialized this component through
130
+ // ComponentElement's host-ready listener, which is attached first and so has run by now.
131
+ // Acting here too would resolve every track twice and capture the playhead-restore
132
+ // snapshot mid-rebuild.
133
+ if (event.target === this._modelListenerTarget) {
134
+ return;
122
135
  }
136
+ // A model cycle can replace the skeleton source's host entity (a rebuild under a
137
+ // retargeting pc-node), so the binding root is re-asserted before the rebind.
138
+ this._applyRootBone();
123
139
  this.component.rebind();
124
140
  };
125
141
 
142
+ /**
143
+ * The model whose host entity scopes this component's curve binding: the parent `pc-model`,
144
+ * or the sole `pc-model` among the parent's direct children (the arrangement where clips
145
+ * live in a library asset beside the skeleton). `null` when there is no such model, or more
146
+ * than one — an ambiguous skeleton is left to the engine's name-based resolution.
147
+ */
148
+ private _skeletonSource(): ModelElement | null {
149
+ const parent = this.parentElement;
150
+ if (parent instanceof ModelElement) {
151
+ return parent;
152
+ }
153
+ const models = parent ? parent.querySelectorAll(':scope > pc-model') : null;
154
+ return models?.length === 1 && models[0] instanceof ModelElement ? models[0] : null;
155
+ }
156
+
157
+ /**
158
+ * Keeps the component's binding root pointing at the skeleton source's host entity. The host
159
+ * wraps the instantiated content, so left at its default — the component's own entity — the
160
+ * engine binder mis-resolves curves that target the asset's root node: its fallback treats
161
+ * the graph as the asset root once the root is no longer a direct child.
162
+ *
163
+ * Authoritative in both directions for values this element assigned: a source appearing pins
164
+ * its host, and a source dissolving (the model gone, or a second model making the skeleton
165
+ * ambiguous) clears the pin rather than leaving it on a stale host. A root assigned through
166
+ * the engine API is never overwritten — the user's choice outranks the managed default.
167
+ * Writes are skipped while unchanged, because the engine setter itself triggers a rebind.
168
+ */
169
+ private _applyRootBone() {
170
+ const component = this.component;
171
+
172
+ // A non-null root this element did not assign came through the engine API. A fresh
173
+ // component starts at null, which is always reclaimable.
174
+ if (component.rootBone !== null && component.rootBone !== this._managedRootBone) {
175
+ return;
176
+ }
177
+
178
+ const host = this._skeletonSource()?.entity ?? null;
179
+ if (component.rootBone !== host) {
180
+ // The engine setter accepts null - restoring the component's own entity as the
181
+ // binding graph - but its declared type does not
182
+ component.rootBone = host as Entity;
183
+ }
184
+ this._managedRootBone = host;
185
+ }
186
+
126
187
  /** @ignore */
127
188
  constructor() {
128
189
  super('anim');
@@ -151,7 +212,16 @@ class AnimComponentElement extends ComponentElement {
151
212
  this._modelListenerTarget = host;
152
213
  }
153
214
 
154
- this._applyClips();
215
+ this._applyRootBone();
216
+
217
+ if (this.component.baseLayer) {
218
+ // The component survived the host's readiness cycle (a pc-model reloading content on
219
+ // its stable host entity). A loaded graph cannot be reassigned in place, so drop it
220
+ // and reassign from the current source, restoring the active clip and playhead.
221
+ this._refreshClips();
222
+ } else {
223
+ this._applyClips();
224
+ }
155
225
  }
156
226
 
157
227
  disconnectedCallback() {
@@ -163,6 +233,7 @@ class AnimComponentElement extends ComponentElement {
163
233
  this._sourceGeneration++;
164
234
  this._assignedClips.clear();
165
235
  this._autoAssigned = false;
236
+ this._managedRootBone = null;
166
237
 
167
238
  super.disconnectedCallback();
168
239
  }
@@ -353,6 +424,10 @@ class AnimComponentElement extends ComponentElement {
353
424
  if (!component) {
354
425
  return;
355
426
  }
427
+ // A clip-set change is also a chance for the skeleton source to have changed shape (a
428
+ // clip child appearing or leaving can accompany a model coming or going) - re-derive the
429
+ // binding root before the reassignment binds against it.
430
+ this._applyRootBone();
356
431
  const layer = component.baseLayer;
357
432
  const restore = layer ? {
358
433
  state: layer.activeState,
@@ -0,0 +1,36 @@
1
+ import type { AudioListenerComponent } from 'playcanvas';
2
+
3
+ import { ComponentElement } from './component';
4
+
5
+ /**
6
+ * The AudioListenerComponentElement interface provides properties and methods for manipulating
7
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-audio-listener/ | `<pc-audio-listener>`} elements.
8
+ * The AudioListenerComponentElement interface also inherits the properties and methods of the
9
+ * {@link HTMLElement} interface.
10
+ *
11
+ * Engine component: {@link AudioListenerComponent} (`audiolistener`).
12
+ *
13
+ * @elementSummary The `<pc-audio-listener>` element makes its entity the point from which
14
+ * positional sounds are heard, typically the entity holding the `<pc-camera>`. Must be a child of a
15
+ * `<pc-entity>`, `<pc-model>` or `<pc-node>`.
16
+ *
17
+ * @category Components
18
+ */
19
+ class AudioListenerComponentElement extends ComponentElement {
20
+ /** @ignore */
21
+ constructor() {
22
+ super('audiolistener');
23
+ }
24
+
25
+ /**
26
+ * Gets the underlying PlayCanvas audio listener component.
27
+ * @returns The audio listener component.
28
+ */
29
+ get component(): AudioListenerComponent {
30
+ return super.component as AudioListenerComponent;
31
+ }
32
+ }
33
+
34
+ customElements.define('pc-audio-listener', AudioListenerComponentElement);
35
+
36
+ export { AudioListenerComponentElement };
@@ -17,6 +17,13 @@ const transitionModes = new Map<'tint' | 'sprite', number>([
17
17
  * The ButtonComponentElement interface also inherits the properties and methods of the
18
18
  * {@link HTMLElement} interface.
19
19
  *
20
+ * Engine component: {@link ButtonComponent} (`button`).
21
+ *
22
+ * @elementSummary The `<pc-button>` element makes its entity respond to pointer input, tinting or
23
+ * swapping its image as the pointer hovers, presses and releases it. The entity also needs a
24
+ * `<pc-element>` with `use-input` set. Must be a child of a `<pc-entity>`, `<pc-model>` or
25
+ * `<pc-node>`.
26
+ *
20
27
  * @category Components
21
28
  */
22
29
  class ButtonComponentElement extends ComponentElement {
@@ -121,7 +128,9 @@ class ButtonComponentElement extends ComponentElement {
121
128
 
122
129
  /**
123
130
  * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` whose image
124
- * element is used for visual transitions. Defaults to the button's own entity.
131
+ * element is used for visual transitions. Defaults to the button's own entity — inside a
132
+ * `<pc-model>`, that is the model's host entity, so supply an explicit reference to target a
133
+ * UI entity instead.
125
134
  * @param value - The image entity reference.
126
135
  */
127
136
  set image(value: string) {