@playcanvas/web-components 0.15.0 → 0.17.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 (108) hide show
  1. package/dist/app.d.cts +14 -13
  2. package/dist/app.d.ts +14 -13
  3. package/dist/async-element.d.cts +13 -13
  4. package/dist/async-element.d.ts +13 -13
  5. package/dist/components/anim-clip.d.cts +127 -0
  6. package/dist/components/anim-clip.d.ts +127 -0
  7. package/dist/components/anim-component.d.cts +235 -0
  8. package/dist/components/anim-component.d.ts +235 -0
  9. package/dist/components/{listener-component.d.cts → audio-listener-component.d.cts} +7 -5
  10. package/dist/components/{listener-component.d.ts → audio-listener-component.d.ts} +7 -5
  11. package/dist/components/button-component.d.cts +5 -1
  12. package/dist/components/button-component.d.ts +5 -1
  13. package/dist/components/camera-component.d.cts +2 -0
  14. package/dist/components/camera-component.d.ts +2 -0
  15. package/dist/components/collision-component.d.cts +5 -1
  16. package/dist/components/collision-component.d.ts +5 -1
  17. package/dist/components/component.d.cts +3 -2
  18. package/dist/components/component.d.ts +3 -2
  19. package/dist/components/element-component.d.cts +7 -0
  20. package/dist/components/element-component.d.ts +7 -0
  21. package/dist/components/gsplat-component.d.cts +2 -0
  22. package/dist/components/gsplat-component.d.ts +2 -0
  23. package/dist/components/joint-component.d.cts +2 -0
  24. package/dist/components/joint-component.d.ts +2 -0
  25. package/dist/components/{layoutchild-component.d.cts → layout-child-component.d.cts} +3 -1
  26. package/dist/components/{layoutchild-component.d.ts → layout-child-component.d.ts} +3 -1
  27. package/dist/components/{layoutgroup-component.d.cts → layout-group-component.d.cts} +3 -1
  28. package/dist/components/{layoutgroup-component.d.ts → layout-group-component.d.ts} +3 -1
  29. package/dist/components/light-component.d.cts +2 -0
  30. package/dist/components/light-component.d.ts +2 -0
  31. package/dist/components/{particlesystem-component.d.cts → particle-system-component.d.cts} +3 -1
  32. package/dist/components/{particlesystem-component.d.ts → particle-system-component.d.ts} +3 -1
  33. package/dist/components/render-component.d.cts +2 -0
  34. package/dist/components/render-component.d.ts +2 -0
  35. package/dist/components/{rigidbody-component.d.cts → rigid-body-component.d.cts} +3 -1
  36. package/dist/components/{rigidbody-component.d.ts → rigid-body-component.d.ts} +3 -1
  37. package/dist/components/screen-component.d.cts +2 -0
  38. package/dist/components/screen-component.d.ts +2 -0
  39. package/dist/components/script-component.d.cts +23 -12
  40. package/dist/components/script-component.d.ts +23 -12
  41. package/dist/components/{script.d.cts → script-instance.d.cts} +7 -7
  42. package/dist/components/{script.d.ts → script-instance.d.ts} +7 -7
  43. package/dist/components/{scrollview-component.d.cts → scroll-view-component.d.cts} +3 -1
  44. package/dist/components/{scrollview-component.d.ts → scroll-view-component.d.ts} +3 -1
  45. package/dist/components/scrollbar-component.d.cts +2 -0
  46. package/dist/components/scrollbar-component.d.ts +2 -0
  47. package/dist/components/sound-component.d.cts +3 -1
  48. package/dist/components/sound-component.d.ts +3 -1
  49. package/dist/components/sound-slot.d.cts +2 -2
  50. package/dist/components/sound-slot.d.ts +2 -2
  51. package/dist/custom-elements.json +3154 -2005
  52. package/dist/entity-base.d.cts +6 -5
  53. package/dist/entity-base.d.ts +6 -5
  54. package/dist/entity-owner.d.cts +118 -0
  55. package/dist/entity-owner.d.ts +118 -0
  56. package/dist/entity.d.cts +8 -101
  57. package/dist/entity.d.ts +8 -101
  58. package/dist/index.d.cts +26 -21
  59. package/dist/index.d.ts +26 -21
  60. package/dist/model.d.cts +67 -24
  61. package/dist/model.d.ts +67 -24
  62. package/dist/pwc.cjs +1766 -619
  63. package/dist/pwc.cjs.map +1 -1
  64. package/dist/pwc.js +1766 -619
  65. package/dist/pwc.js.map +1 -1
  66. package/dist/pwc.min.js +1 -1
  67. package/dist/pwc.min.js.map +1 -1
  68. package/dist/pwc.min.mjs +1 -1
  69. package/dist/pwc.min.mjs.map +1 -1
  70. package/dist/pwc.mjs +1762 -618
  71. package/dist/pwc.mjs.map +1 -1
  72. package/dist/vscode.html-custom-data.json +203 -89
  73. package/dist/{module.d.cts → wasm.d.cts} +6 -6
  74. package/dist/{module.d.ts → wasm.d.ts} +6 -6
  75. package/dist/web-types.json +556 -273
  76. package/package.json +7 -7
  77. package/src/app.ts +45 -35
  78. package/src/async-element.ts +14 -14
  79. package/src/components/anim-clip.ts +395 -0
  80. package/src/components/anim-component.ts +719 -0
  81. package/src/components/{listener-component.ts → audio-listener-component.ts} +8 -6
  82. package/src/components/button-component.ts +5 -1
  83. package/src/components/camera-component.ts +2 -0
  84. package/src/components/collision-component.ts +5 -1
  85. package/src/components/component.ts +4 -3
  86. package/src/components/element-component.ts +7 -0
  87. package/src/components/gsplat-component.ts +2 -0
  88. package/src/components/joint-component.ts +2 -0
  89. package/src/components/{layoutchild-component.ts → layout-child-component.ts} +4 -2
  90. package/src/components/{layoutgroup-component.ts → layout-group-component.ts} +4 -2
  91. package/src/components/light-component.ts +2 -0
  92. package/src/components/{particlesystem-component.ts → particle-system-component.ts} +4 -2
  93. package/src/components/render-component.ts +2 -0
  94. package/src/components/{rigidbody-component.ts → rigid-body-component.ts} +4 -2
  95. package/src/components/screen-component.ts +2 -0
  96. package/src/components/script-component.ts +63 -42
  97. package/src/components/{script.ts → script-instance.ts} +14 -14
  98. package/src/components/{scrollview-component.ts → scroll-view-component.ts} +4 -2
  99. package/src/components/scrollbar-component.ts +2 -0
  100. package/src/components/sound-component.ts +4 -2
  101. package/src/components/sound-slot.ts +4 -4
  102. package/src/entity-base.ts +6 -5
  103. package/src/entity-owner.ts +298 -0
  104. package/src/entity.ts +15 -270
  105. package/src/index.ts +32 -24
  106. package/src/model.ts +159 -79
  107. package/src/node.ts +9 -5
  108. package/src/{module.ts → wasm.ts} +8 -8
package/dist/pwc.cjs CHANGED
@@ -29,13 +29,13 @@ class AsyncElement extends HTMLElement {
29
29
  return this.parentElement?.closest('pc-app') ?? null;
30
30
  }
31
31
  /**
32
- * The nearest ancestor element that fronts an entity — `<pc-entity>` or `<pc-node>` or
33
- * `null` if this element has no such ancestor. The search starts at the parent, so an element
34
- * never resolves to itself.
32
+ * The nearest ancestor element that fronts an entity — `<pc-entity>`, `<pc-model>` or
33
+ * `<pc-node>` — or `null` if this element has no such ancestor. The search starts at the
34
+ * parent, so an element never resolves to itself.
35
35
  * @returns The closest entity-fronting element, or `null`.
36
36
  */
37
37
  get closestEntity() {
38
- return this.parentElement?.closest('pc-entity, pc-node') ?? null;
38
+ return this.parentElement?.closest('pc-entity, pc-model, pc-node') ?? null;
39
39
  }
40
40
  /**
41
41
  * Called when the element is fully initialized and ready. Subclasses should call this when
@@ -109,9 +109,9 @@ async function whenReady(target) {
109
109
  }
110
110
 
111
111
  /**
112
- * The ModuleElement interface provides properties and methods for manipulating
113
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`}
114
- * elements. The ModuleElement interface also inherits the properties and methods of the
112
+ * The WasmElement interface provides properties and methods for manipulating
113
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-wasm/ | `<pc-wasm>`}
114
+ * elements. The WasmElement interface also inherits the properties and methods of the
115
115
  * {@link AsyncElement} interface.
116
116
  *
117
117
  * The attributes are read once, when the module starts loading - on the element's first
@@ -120,7 +120,7 @@ async function whenReady(target) {
120
120
  * modules configure engine-global state that never unloads, so readiness is not re-armed by
121
121
  * removing the element, and a re-inserted element does not load again.
122
122
  *
123
- * A `<pc-module>` without a `name` warns and never becomes ready; a containing `<pc-app>` still
123
+ * A `<pc-wasm>` without a `name` warns and never becomes ready; a containing `<pc-app>` still
124
124
  * boots.
125
125
  *
126
126
  * @attribute {string} name - The name of the WebAssembly module to configure, e.g. `Basis` or
@@ -130,7 +130,7 @@ async function whenReady(target) {
130
130
  * @attribute {string} fallback - The URL of the module's asm.js fallback script, used when
131
131
  * WebAssembly is unavailable.
132
132
  */
133
- class ModuleElement extends AsyncElement {
133
+ class WasmElement extends AsyncElement {
134
134
  _loadPromise = null;
135
135
  connectedCallback() {
136
136
  this._getLoadPromise();
@@ -138,7 +138,7 @@ class ModuleElement extends AsyncElement {
138
138
  async _loadModule() {
139
139
  const name = this.getAttribute('name');
140
140
  if (!name) {
141
- console.warn("pc-module requires a 'name' attribute - no module was configured");
141
+ console.warn("pc-wasm requires a 'name' attribute - no module was configured");
142
142
  return;
143
143
  }
144
144
  const config = {
@@ -173,7 +173,7 @@ class ModuleElement extends AsyncElement {
173
173
  return this._loadPromise;
174
174
  }
175
175
  }
176
- customElements.define('pc-module', ModuleElement);
176
+ customElements.define('pc-wasm', WasmElement);
177
177
 
178
178
  /** Covers the 0.2s opacity transition; jsdom never fires transitionend, so removal is timed. */
179
179
  const REMOVAL_DELAY_MS = 250;
@@ -730,6 +730,12 @@ const getEntity = (ref) => {
730
730
 
731
731
  /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
732
732
  const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
733
+ /**
734
+ * The event types whose listeners make an element a hover target. Hover resolution walks past
735
+ * elements listening for none of them, so a silent element never swallows an ancestor's
736
+ * enter/leave pair.
737
+ */
738
+ const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'];
733
739
  /**
734
740
  * Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
735
741
  * box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
@@ -804,9 +810,9 @@ class AppElement extends AsyncElement {
804
810
  _bootGeneration = 0;
805
811
  /**
806
812
  * The elements backing this application's entities, keyed by the entity itself. Registered
807
- * by EntityElement at creation (and NodeElement at binding) and removed when an entity is
808
- * destroyed or unbound, this joins engine scene nodes back to their owning elements by
809
- * identity - never by name.
813
+ * by entity-owning elements at creation (pc-entity, and pc-model for its host) and by
814
+ * NodeElement at binding, and removed when an entity is destroyed or unbound, this joins
815
+ * engine scene nodes back to their owning elements by identity - never by name.
810
816
  */
811
817
  _entityElements = new Map();
812
818
  _picker = null;
@@ -885,7 +891,7 @@ class AppElement extends AsyncElement {
885
891
  // there, leaving the element permanently unready: no canvas, no entities, no application.
886
892
  //
887
893
  // Upgrading is the fix here rather than skipping whatever has not upgraded, because a
888
- // <pc-module> is the one child that nothing else ever builds on its own behalf - skipping
894
+ // <pc-wasm> is the one child that nothing else ever builds on its own behalf - skipping
889
895
  // it would drop the wasm module the app asked for, silently and only for cloned apps.
890
896
  // Upgrading runs each descendant's connectedCallback synchronously, a few lines earlier
891
897
  // than the parser's path runs them but into the same state they see there: no application
@@ -894,10 +900,10 @@ class AppElement extends AsyncElement {
894
900
  // as any other disconnect is. An already-upgraded subtree - every other insertion path -
895
901
  // is left completely untouched.
896
902
  customElements.upgrade(this);
897
- // Get all pc-module elements that are direct children of the pc-app element
898
- const moduleElements = this.querySelectorAll(':scope > pc-module');
903
+ // Get all pc-wasm elements that are direct children of the pc-app element
904
+ const wasmElements = this.querySelectorAll(':scope > pc-wasm');
899
905
  // Wait for all modules to load
900
- await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
906
+ await Promise.all(Array.from(wasmElements).map((element) => element._getLoadPromise()));
901
907
  // The element may have been removed while the modules loaded. Nothing beyond the loading
902
908
  // bar exists yet, and disconnectedCallback has already destroyed that.
903
909
  if (generation !== this._bootGeneration) {
@@ -1067,14 +1073,15 @@ class AppElement extends AsyncElement {
1067
1073
  Array.from(materialElements).forEach((materialElement) => {
1068
1074
  materialElement._createMaterial();
1069
1075
  });
1070
- // Create all entities
1071
- const entityElements = this.querySelectorAll('pc-entity');
1072
- Array.from(entityElements).forEach((entityElement) => {
1073
- entityElement._createEntity(app);
1076
+ // Create all entities. pc-model joins the sweep because it owns a host entity of its
1077
+ // own; its instantiated content arrives later, beneath that host.
1078
+ const ownerElements = this.querySelectorAll('pc-entity, pc-model');
1079
+ Array.from(ownerElements).forEach((ownerElement) => {
1080
+ ownerElement._createEntity(app);
1074
1081
  });
1075
1082
  // Build hierarchy
1076
- entityElements.forEach((entityElement) => {
1077
- entityElement._buildHierarchy(app);
1083
+ ownerElements.forEach((ownerElement) => {
1084
+ ownerElement._buildHierarchy(app);
1078
1085
  });
1079
1086
  // Building the hierarchy dispatched each entity's ready event synchronously, and a
1080
1087
  // listener may have removed the element. The sweep itself degrades safely - destroying
@@ -1185,7 +1192,7 @@ class AppElement extends AsyncElement {
1185
1192
  // created from onpointer* attributes when their elements were first upgraded, or
1186
1193
  // listeners carried over from before a re-boot)
1187
1194
  pointerEventTypes.forEach((type) => {
1188
- const anyListeners = Array.from(this.querySelectorAll('pc-entity, pc-node')).some((entity) => entity._hasListeners(type));
1195
+ const anyListeners = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node')).some((entity) => entity._hasListeners(type));
1189
1196
  if (anyListeners) {
1190
1197
  this._onPointerListenerAdded(type);
1191
1198
  }
@@ -1235,10 +1242,10 @@ class AppElement extends AsyncElement {
1235
1242
  this._entityElements.delete(entity);
1236
1243
  }
1237
1244
  /**
1238
- * Returns the `<pc-entity>` or `<pc-node>` element whose backing entity is `entity`, or
1239
- * `null` if the entity is not fronted by an element of this application - for example, an
1240
- * unbound node inside a model's instantiated hierarchy, or an entity created through the
1241
- * engine API.
1245
+ * Returns the `<pc-entity>`, `<pc-model>` or `<pc-node>` element whose backing entity is
1246
+ * `entity`, or `null` if the entity is not fronted by an element of this application - for
1247
+ * example, an unbound node inside a model's instantiated hierarchy, or an entity created
1248
+ * through the engine API.
1242
1249
  *
1243
1250
  * @param entity - The entity to look up.
1244
1251
  * @returns The element fronting the entity, or `null`.
@@ -1247,18 +1254,19 @@ class AppElement extends AsyncElement {
1247
1254
  return this._entityElements.get(entity) ?? null;
1248
1255
  }
1249
1256
  /**
1250
- * Resolves the element that owns a picked node: the nearest node up the parent chain -
1251
- * starting with the node itself - that is fronted by a `<pc-entity>` or `<pc-node>` of this
1252
- * application. A hit inside a model's instantiated hierarchy therefore resolves to the
1253
- * nearest bound `<pc-node>`, or failing that the element hosting the model.
1257
+ * Resolves the element that owns hover for a picked node: the nearest node up the parent
1258
+ * chain - starting with the node itself - whose element listens for any of the hover event
1259
+ * types. Skipping silent elements matches {@link _elementWithListener}, so a registered
1260
+ * element with no hover listeners (a `<pc-model>` host, a plain child entity) is transparent
1261
+ * to hover rather than swallowing a listening ancestor's enter/leave pair.
1254
1262
  *
1255
1263
  * @param node - The picked node, or `null`.
1256
- * @returns The owning element, or `null`.
1264
+ * @returns The hover-owning element, or `null`.
1257
1265
  */
1258
- _elementFromNode(node) {
1266
+ _hoverTarget(node) {
1259
1267
  while (node !== null) {
1260
1268
  const element = this._entityElements.get(node);
1261
- if (element) {
1269
+ if (element && hoverEventTypes.some((type) => element._hasListeners(type))) {
1262
1270
  return element;
1263
1271
  }
1264
1272
  node = node.parent;
@@ -1394,9 +1402,10 @@ class AppElement extends AsyncElement {
1394
1402
  const node = await this._pickNode(event);
1395
1403
  if (token !== this._pickToken || !this._picker)
1396
1404
  return;
1397
- // The hovered element is the nearest one up the node's parent chain, listening or not -
1398
- // dispatch is gated per event type below
1399
- const newHoverEntity = this._elementFromNode(node);
1405
+ // The hovered element is the nearest one up the node's parent chain with a hover
1406
+ // listener - the nearest-listener rule down/up use. Dispatch is still gated per event
1407
+ // type below: having any hover listener selects the target, each event needs its own.
1408
+ const newHoverEntity = this._hoverTarget(node);
1400
1409
  // Handle enter/leave events
1401
1410
  if (this._hoveredEntity !== newHoverEntity) {
1402
1411
  if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
@@ -1448,7 +1457,7 @@ class AppElement extends AsyncElement {
1448
1457
  }
1449
1458
  }
1450
1459
  _onPointerListenerRemoved(type) {
1451
- const hasListeners = Array.from(this.querySelectorAll('pc-entity, pc-node')).some((entity) => entity._hasListeners(type));
1460
+ const hasListeners = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node')).some((entity) => entity._hasListeners(type));
1452
1461
  if (!hasListeners && this._canvas) {
1453
1462
  this._hasPointerListeners[type] = false;
1454
1463
  const handler = type === 'pointerenter' || type === 'pointerleave'
@@ -1639,11 +1648,12 @@ const POINTER_ATTRIBUTES = [
1639
1648
  'onpointermove'
1640
1649
  ];
1641
1650
  /**
1642
- * The base class for elements that front an engine {@link Entity}: `<pc-entity>`, which creates
1643
- * one, and `<pc-node>`, which binds to one inside a model's instantiated hierarchy. It carries
1644
- * what both need — the `entity` contract, registration with the owning application (which joins
1645
- * picked scene nodes back to elements by identity, never by name), and the pointer listener
1646
- * bookkeeping that lets the application lazily attach its canvas handlers.
1651
+ * The base class for elements that front an engine {@link Entity}: `<pc-entity>` and
1652
+ * `<pc-model>`, which create one, and `<pc-node>`, which binds to one inside a model's
1653
+ * instantiated hierarchy. It carries what all of them need — the `entity` contract, registration
1654
+ * with the owning application (which joins picked scene nodes back to elements by identity,
1655
+ * never by name), and the pointer listener bookkeeping that lets the application lazily attach
1656
+ * its canvas handlers.
1647
1657
  */
1648
1658
  class EntityBaseElement extends AsyncElement {
1649
1659
  _entity = null;
@@ -1746,17 +1756,17 @@ class EntityBaseElement extends AsyncElement {
1746
1756
  }
1747
1757
 
1748
1758
  /**
1749
- * Creates and parents the entities of every descendant `<pc-entity>` of `root`, in two passes so
1750
- * that no parent's existence depends on document order. Called wherever a subtree could not build
1751
- * itself: an element inserted into an application that is already running, and a `<pc-node>` whose
1752
- * children waited for it to bind.
1759
+ * Creates and parents the entities of every descendant entity-owning element of `root`, in two
1760
+ * passes so that no parent's existence depends on document order. Called wherever a subtree could
1761
+ * not build itself: an element inserted into an application that is already running, and a
1762
+ * `<pc-node>` whose children waited for it to bind.
1753
1763
  *
1754
1764
  * Descendants that are not yet custom elements are skipped, because there is nothing useful to do
1755
1765
  * for them and reaching for `_createEntity` would throw. A subtree cloned from a `<template>`
1756
1766
  * arrives entirely unupgraded — template content lives in an inert document, where custom element
1757
1767
  * definitions are never looked up — and appending the clone upgrades its elements in tree order,
1758
1768
  * an element before its descendants. So a sweep from an element's own `connectedCallback` sees
1759
- * plain `HTMLElement`s below it. Each becomes an `EntityElement` moments later and its own
1769
+ * plain `HTMLElement`s below it. Each becomes an entity-owning element moments later and its own
1760
1770
  * `connectedCallback` creates and parents it, by which time the ancestor it parents under has its
1761
1771
  * entity — the same guarantee tree order gives this sweep.
1762
1772
  *
@@ -1765,35 +1775,18 @@ class EntityBaseElement extends AsyncElement {
1765
1775
  * @internal
1766
1776
  */
1767
1777
  const buildDescendantEntities = (root, app) => {
1768
- const children = Array.from(root.querySelectorAll('pc-entity')).filter((child) => child instanceof EntityElement);
1778
+ const children = Array.from(root.querySelectorAll('pc-entity, pc-model')).filter((child) => child instanceof EntityOwnerElement);
1769
1779
  children.forEach((child) => child._createEntity(app));
1770
1780
  children.forEach((child) => child._buildHierarchy(app));
1771
1781
  };
1772
1782
  /**
1773
- * The EntityElement interface provides properties and methods for manipulating
1774
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | `<pc-entity>`} elements.
1775
- * The EntityElement interface also inherits the properties and methods of the
1776
- * {@link HTMLElement} interface.
1777
- *
1778
- * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
1779
- * intersects this entity's geometry. They are only generated while the entity has a listener for
1780
- * them, registered either with {@link addEventListener} or with the matching inline `onpointer*`
1781
- * attribute.
1782
- *
1783
- * @attribute {string} onpointerenter - Script to run when the pointer moves onto the entity.
1784
- * @attribute {string} onpointerleave - Script to run when the pointer moves off the entity.
1785
- * @attribute {string} onpointermove - Script to run when the pointer moves over the entity.
1786
- * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
1787
- * entity.
1788
- * @attribute {string} onpointerup - Script to run when a pointer button is released over the
1789
- * entity.
1790
- * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the entity.
1791
- * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the entity.
1792
- * @fires {PointerEvent} pointermove - Fired when the pointer moves over the entity.
1793
- * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
1794
- * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
1783
+ * The base class for elements that create and own their backing entity: `<pc-entity>` and
1784
+ * `<pc-model>`, whose host entity carries the same authored properties. It carries the cached
1785
+ * property state, entity creation and parenting, and the reset that follows the entity's
1786
+ * destruction. `<pc-node>` sits outside this class: it borrows an entity a model instantiated,
1787
+ * and its properties are nullable overrides rather than owned values.
1795
1788
  */
1796
- class EntityElement extends EntityBaseElement {
1789
+ class EntityOwnerElement extends EntityBaseElement {
1797
1790
  /**
1798
1791
  * Whether the entity is enabled.
1799
1792
  */
@@ -1819,7 +1812,8 @@ class EntityElement extends EntityBaseElement {
1819
1812
  */
1820
1813
  _tags = [];
1821
1814
  /**
1822
- * Whether the hierarchy has been built for this entity.
1815
+ * Whether the hierarchy has been built for this entity — set once {@link _buildHierarchy} has
1816
+ * parented it. Read by subclasses that gate work on the entity being in the scene graph.
1823
1817
  */
1824
1818
  _built = false;
1825
1819
  /**
@@ -1872,10 +1866,10 @@ class EntityElement extends EntityBaseElement {
1872
1866
  this._resetReady();
1873
1867
  }
1874
1868
  /**
1875
- * Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` or
1876
- * `<pc-node>` when there is one, and under the application root otherwise. Called by the
1877
- * containing `<pc-app>` element once a sweep has created every entity, so a parent's
1878
- * existence never depends on document order.
1869
+ * Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>`,
1870
+ * `<pc-model>` or `<pc-node>` when there is one, and under the application root otherwise.
1871
+ * Called by the containing `<pc-app>` element once a sweep has created every entity, so a
1872
+ * parent's existence never depends on document order.
1879
1873
  *
1880
1874
  * @param app - The application whose root adopts parentless entities.
1881
1875
  * @internal
@@ -1890,6 +1884,13 @@ class EntityElement extends EntityBaseElement {
1890
1884
  if (closestEntity && !closestEntity.entity) {
1891
1885
  return;
1892
1886
  }
1887
+ // An owner that exists but is not yet parented is itself deferred behind an unresolved
1888
+ // node further up. Building beneath it would announce readiness for an entity that is
1889
+ // not in the scene graph; stay unbuilt with it - the node's bind sweeps the whole
1890
+ // subtree, ancestors before descendants.
1891
+ if (closestEntity instanceof EntityOwnerElement && !closestEntity._built) {
1892
+ return;
1893
+ }
1893
1894
  this._built = true;
1894
1895
  if (closestEntity?.entity) {
1895
1896
  closestEntity.entity.addChild(this.entity);
@@ -1897,35 +1898,16 @@ class EntityElement extends EntityBaseElement {
1897
1898
  else {
1898
1899
  app.root.addChild(this.entity);
1899
1900
  }
1900
- this._onReady();
1901
- }
1902
- connectedCallback() {
1903
- // Wait for app to be ready
1904
- const closestApp = this.closestApp;
1905
- if (!closestApp) {
1906
- // An entity outside an application is inert and never becomes ready, so awaiting it
1907
- // hangs. Warn rather than fail silently, naming the parent it requires, as every other
1908
- // misplaced element does.
1909
- const name = this.getAttribute('name');
1910
- const label = name ? ` '${name}'` : '';
1911
- console.warn(`pc-entity${label} must be a descendant of pc-app - entity not created`);
1912
- return;
1913
- }
1914
- // If app is already running, create entity immediately
1915
- if (closestApp._hierarchyReady) {
1916
- const app = closestApp.app;
1917
- this._createEntity(app);
1918
- this._buildHierarchy(app);
1919
- // Handle any child entities that might exist
1920
- buildDescendantEntities(this, app);
1921
- }
1901
+ this._onBuilt();
1922
1902
  }
1923
- disconnectedCallback() {
1924
- // Destroying the entity destroys its whole subtree, and the engine fires 'destroy' for
1925
- // every entity in it - so _onEntityDestroy resets this element AND every descendant
1926
- // element before the descendants' own disconnectedCallbacks run. Their entities are null
1927
- // by then, making this call a no-op for them.
1928
- this._entity?.destroy();
1903
+ /**
1904
+ * Called by {@link _buildHierarchy} once the backing entity has been parented exactly once
1905
+ * per build cycle. The default announces readiness, which is what a parented `<pc-entity>`
1906
+ * means; `<pc-model>` overrides it to start loading content instead, because its readiness
1907
+ * tracks the content settling rather than the host entering the scene graph.
1908
+ */
1909
+ _onBuilt() {
1910
+ this._onReady();
1929
1911
  }
1930
1912
  /**
1931
1913
  * Sets the enabled state of the entity.
@@ -2030,6 +2012,70 @@ class EntityElement extends EntityBaseElement {
2030
2012
  get tags() {
2031
2013
  return this._tags;
2032
2014
  }
2015
+ }
2016
+
2017
+ /**
2018
+ * The EntityElement interface provides properties and methods for manipulating
2019
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | `<pc-entity>`} elements.
2020
+ * The EntityElement interface also inherits the properties and methods of the
2021
+ * {@link HTMLElement} interface.
2022
+ *
2023
+ * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
2024
+ * intersects this entity's geometry. They are only generated while the entity has a listener for
2025
+ * them, registered either with {@link addEventListener} or with the matching inline `onpointer*`
2026
+ * attribute.
2027
+ *
2028
+ * @attribute {boolean} enabled - The enabled state of the entity.
2029
+ * @attribute {string} name - The name of the entity.
2030
+ * @attribute {string} position - The position of the entity.
2031
+ * @attribute {string} rotation - The rotation of the entity.
2032
+ * @attribute {string} scale - The scale of the entity.
2033
+ * @attribute {string} tags - The tags of the entity.
2034
+ * @attribute {string} onpointerenter - Script to run when the pointer moves onto the entity.
2035
+ * @attribute {string} onpointerleave - Script to run when the pointer moves off the entity.
2036
+ * @attribute {string} onpointermove - Script to run when the pointer moves over the entity.
2037
+ * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
2038
+ * entity.
2039
+ * @attribute {string} onpointerup - Script to run when a pointer button is released over the
2040
+ * entity.
2041
+ * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the entity.
2042
+ * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the entity.
2043
+ * @fires {PointerEvent} pointermove - Fired when the pointer moves over the entity.
2044
+ * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
2045
+ * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
2046
+ */
2047
+ class EntityElement extends EntityOwnerElement {
2048
+ connectedCallback() {
2049
+ // Wait for app to be ready
2050
+ const closestApp = this.closestApp;
2051
+ if (!closestApp) {
2052
+ // An entity outside an application is inert and never becomes ready, so awaiting it
2053
+ // hangs. Warn rather than fail silently, naming the parent it requires, as every other
2054
+ // misplaced element does.
2055
+ const name = this.getAttribute('name');
2056
+ const label = name ? ` '${name}'` : '';
2057
+ console.warn(`pc-entity${label} must be a descendant of pc-app - entity not created`);
2058
+ return;
2059
+ }
2060
+ // If app is already running, create entity immediately
2061
+ if (closestApp._hierarchyReady) {
2062
+ const app = closestApp.app;
2063
+ this._createEntity(app);
2064
+ this._buildHierarchy(app);
2065
+ // Handle any child entities that might exist. A build that deferred (an unresolved
2066
+ // pc-node above) defers the whole subtree with it - the node's bind sweeps it.
2067
+ if (this._built) {
2068
+ buildDescendantEntities(this, app);
2069
+ }
2070
+ }
2071
+ }
2072
+ disconnectedCallback() {
2073
+ // Destroying the entity destroys its whole subtree, and the engine fires 'destroy' for
2074
+ // every entity in it - so _onEntityDestroy resets this element AND every descendant
2075
+ // element before the descendants' own disconnectedCallbacks run. Their entities are null
2076
+ // by then, making this call a no-op for them.
2077
+ this._entity?.destroy();
2078
+ }
2033
2079
  static get observedAttributes() {
2034
2080
  return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
2035
2081
  }
@@ -2927,223 +2973,1546 @@ const useAsset = (id) => {
2927
2973
  };
2928
2974
 
2929
2975
  /**
2930
- * Represents a component in the PlayCanvas engine.
2976
+ * Formats one line of the printable hierarchy: the node's name, an `[index]` marker when the
2977
+ * name is shared by several nodes in the model, the attached component types, and the material
2978
+ * names of a render component.
2931
2979
  *
2932
- * @category Components
2980
+ * @param node - The node to format.
2981
+ * @param counts - The number of nodes bearing each name.
2982
+ * @returns The formatted line.
2933
2983
  */
2934
- class ComponentElement extends AsyncElement {
2935
- _componentName;
2936
- _enabled = true;
2937
- _component = null;
2938
- _appElement = null;
2984
+ const formatNode = (node, counts) => {
2985
+ const index = (counts.get(node.name) ?? 0) > 1 ? ` [${node.index}]` : '';
2986
+ const components = node.components.length > 0 ? ` (${node.components.join(', ')})` : '';
2987
+ // Braces rather than brackets: `[N]` already means a match index on this line
2988
+ const materials = node.materials.length > 0 ? ` {${node.materials.map((slot) => slot.name ?? 'null').join(', ')}}` : '';
2989
+ return `${node.name}${index}${components}${materials}`;
2990
+ };
2991
+ /**
2992
+ * Formats the printable form of a hierarchy subtree.
2993
+ *
2994
+ * @param root - The subtree root.
2995
+ * @param counts - The number of nodes bearing each name.
2996
+ * @returns The tree, one line per node.
2997
+ */
2998
+ const formatHierarchy = (root, counts) => {
2999
+ const lines = [formatNode(root, counts)];
3000
+ const walk = (node, prefix) => {
3001
+ node.children.forEach((child, i) => {
3002
+ const last = i === node.children.length - 1;
3003
+ lines.push(`${prefix}${last ? '└─ ' : '├─ '}${formatNode(child, counts)}`);
3004
+ walk(child, `${prefix}${last ? ' ' : '│ '}`);
3005
+ });
3006
+ };
3007
+ walk(root, '');
3008
+ return lines.join('\n');
3009
+ };
3010
+ /**
3011
+ * The ModelElement interface provides properties and methods for manipulating
3012
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | `<pc-model>`} elements.
3013
+ * The ModelElement interface also inherits the properties and methods of the
3014
+ * {@link HTMLElement} interface.
3015
+ *
3016
+ * The element creates and fronts a stable host entity: `entity` is that host, created when the
3017
+ * application builds its hierarchy and kept across `asset` changes, so the element's transform
3018
+ * and tags are instance placement that composes with whatever transform the asset authored on
3019
+ * its root. The instantiated content is parented beneath the host and exposed as
3020
+ * {@link contentEntity}.
3021
+ *
3022
+ * The element becomes ready once its current asset selection has settled: the container asset
3023
+ * has loaded and its content root has been parented beneath the host, the load has failed
3024
+ * (`contentEntity` stays `null` — listen for `error`, or check `contentEntity`, to tell the
3025
+ * outcomes apart), or no asset is assigned at all. Changing `asset` re-arms readiness and
3026
+ * instantiates anew, so a `ready()` obtained after the change resolves against the new content.
3027
+ * A `pc-model` outside a `pc-app`, or referencing an unknown asset id, warns and never becomes
3028
+ * ready.
3029
+ *
3030
+ * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
3031
+ * intersects the model's geometry, exactly as for `<pc-entity>` — a hit on a content node that no
3032
+ * `pc-node` fronts resolves to this element.
3033
+ *
3034
+ * @attribute {boolean} enabled - The enabled state of the model.
3035
+ * @attribute {string} name - The name of the model.
3036
+ * @attribute {string} position - The position of the model.
3037
+ * @attribute {string} rotation - The rotation of the model.
3038
+ * @attribute {string} scale - The scale of the model.
3039
+ * @attribute {string} tags - The tags of the model.
3040
+ * @attribute {string} onpointerenter - Script to run when the pointer moves onto the model.
3041
+ * @attribute {string} onpointerleave - Script to run when the pointer moves off the model.
3042
+ * @attribute {string} onpointermove - Script to run when the pointer moves over the model.
3043
+ * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
3044
+ * model.
3045
+ * @attribute {string} onpointerup - Script to run when a pointer button is released over the
3046
+ * model.
3047
+ * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the model.
3048
+ * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the model.
3049
+ * @fires {PointerEvent} pointermove - Fired when the pointer moves over the model.
3050
+ * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the model.
3051
+ * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the model.
3052
+ * @fires {Event} load - Fired each time a container asset finishes instantiating, including
3053
+ * re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
3054
+ * capture-phase listener on an ancestor.
3055
+ * @fires {ErrorEvent} error - Fired when the container asset fails to load, with the engine's
3056
+ * error in `message`. Does not bubble. The element still becomes ready — readiness means the load
3057
+ * settled, not that it succeeded.
3058
+ */
3059
+ class ModelElement extends EntityOwnerElement {
3060
+ _asset = '';
3061
+ _contentEntity = null;
3062
+ /**
3063
+ * Incremented on every new load, on disconnect, and when the host entity dies, and captured
3064
+ * by a load when it starts. A load that resumes from an await or a load callback abandons
3065
+ * itself if the value has moved on, so a superseded load can neither instantiate a second
3066
+ * content root nor parent one under a host a newer cycle has already replaced.
3067
+ */
3068
+ _loadGeneration = 0;
3069
+ /**
3070
+ * The pending asset subscriptions of the current load, if it is waiting for its asset. Held
3071
+ * so that whatever supersedes the load can detach the handlers from the asset, rather than
3072
+ * leave them registered until the asset settles (or forever, if it never does).
3073
+ */
3074
+ _loadHandle = null;
3075
+ _errorHandle = null;
3076
+ /**
3077
+ * The root entity of the instantiated model content, parented beneath the host entity.
3078
+ * `null` until the container asset has loaded and been instantiated, after a failed load,
3079
+ * and again once the element has been removed from the document.
3080
+ * @returns The content root entity, or `null`.
3081
+ */
3082
+ get contentEntity() {
3083
+ return this._contentEntity;
3084
+ }
3085
+ /**
3086
+ * Returns a snapshot of the instantiated node tree, or `null` while there is none (the
3087
+ * container asset has not loaded, or the element has left the document). One call grounds a
3088
+ * session — a browser console, a test, an agent — in the vocabulary `pc-node` binding
3089
+ * resolves against: the instantiated names ({@link HierarchyNode.name}), paths, match
3090
+ * indices, attached component types and the material assignments of render components
3091
+ * ({@link HierarchyNode.materials}). `String(...)` of the result, or of any node in it,
3092
+ * is the printable form.
3093
+ *
3094
+ * The snapshot is plain data, computed afresh each call: it does not follow later changes
3095
+ * to the hierarchy, and mutating it changes nothing. It covers the instantiated content
3096
+ * only — the host entity the element fronts is not part of the asset's node tree.
3097
+ *
3098
+ * @returns The root of the instantiated node tree, or `null`.
3099
+ */
3100
+ hierarchy() {
3101
+ const root = this._contentEntity;
3102
+ if (!root) {
3103
+ return null;
3104
+ }
3105
+ // Ordinals are assigned in the traversal resolution searches — pre-order depth-first
3106
+ // from the model root, the root itself included — so each node's index is exactly what
3107
+ // a pc-node's index attribute selects. Once the walk completes, the map holds the total
3108
+ // count per name, which is what the printable form reads to annotate only shared names.
3109
+ const ordinals = new Map();
3110
+ const describe = (entity, pathBelowRoot) => {
3111
+ const index = ordinals.get(entity.name) ?? 0;
3112
+ ordinals.set(entity.name, index + 1);
3113
+ const node = {
3114
+ name: entity.name,
3115
+ // The root has no path below itself; its own name stands in, as it does for
3116
+ // the path a pc-node bound to the root reports.
3117
+ path: pathBelowRoot || entity.name,
3118
+ index,
3119
+ // A plain GraphNode grafted into the hierarchy has no component storage
3120
+ components: Object.keys(entity.c ?? {}).sort(),
3121
+ materials: (entity.render?.meshInstances ?? []).map((meshInstance, slot) => ({
3122
+ index: slot,
3123
+ name: meshInstance.material?.name ?? null
3124
+ })),
3125
+ children: entity.children.map((child) => describe(child, pathBelowRoot ? `${pathBelowRoot}/${child.name}` : child.name))
3126
+ };
3127
+ // Non-enumerable, keeping the snapshot plain data under JSON.stringify, spreads and
3128
+ // key enumeration. Deferred to call time, by which the ordinal map holds its totals.
3129
+ Object.defineProperty(node, 'toString', {
3130
+ enumerable: false,
3131
+ value: () => formatHierarchy(node, ordinals)
3132
+ });
3133
+ return node;
3134
+ };
3135
+ return describe(root, '');
3136
+ }
3137
+ connectedCallback() {
3138
+ // A model outside an application is inert and never becomes ready, so awaiting it hangs.
3139
+ // Warn rather than fail silently, naming the parent it requires, as every other misplaced
3140
+ // element does.
3141
+ const closestApp = this.closestApp;
3142
+ if (!closestApp) {
3143
+ const label = this._asset ? ` '${this._asset}'` : '';
3144
+ console.warn(`pc-model${label} must be a descendant of pc-app - model not created`);
3145
+ return;
3146
+ }
3147
+ // If the app is already running, create the host immediately; during a boot, the app's
3148
+ // own sweep does it. Either way, _onBuilt starts the content load once the host is
3149
+ // parented.
3150
+ if (closestApp._hierarchyReady) {
3151
+ const app = closestApp.app;
3152
+ this._createEntity(app);
3153
+ this._buildHierarchy(app);
3154
+ // A build that deferred (an unresolved pc-node above) defers the whole subtree with
3155
+ // it - the node's bind sweeps it.
3156
+ if (this._built) {
3157
+ buildDescendantEntities(this, app);
3158
+ }
3159
+ }
3160
+ }
3161
+ disconnectedCallback() {
3162
+ // Destroying the host destroys the instantiated content with it, and the destroy hook
3163
+ // resets the element. The generation guard comes first so a load suspended on an await
3164
+ // cannot resume against the torn-down element.
3165
+ this._loadGeneration++;
3166
+ this._detachLoadHandlers();
3167
+ this._entity?.destroy();
3168
+ }
3169
+ /**
3170
+ * Starts (or restarts) the content load once the host has been parented. Readiness is not
3171
+ * announced here — it tracks the content settling, not the host entering the scene graph.
3172
+ */
3173
+ _onBuilt() {
3174
+ this._loadContent();
3175
+ }
3176
+ /**
3177
+ * Extends the owner reset for the content: the engine's destroy cascade has already taken
3178
+ * the content root down with the host subtree, so only the reference and the in-flight load
3179
+ * are dropped here. The next build re-creates the host and re-instantiates the content.
3180
+ *
3181
+ * @param entity - The host entity that was destroyed.
3182
+ */
3183
+ _onEntityDestroy(entity) {
3184
+ this._loadGeneration++;
3185
+ this._detachLoadHandlers();
3186
+ this._contentEntity = null;
3187
+ super._onEntityDestroy(entity);
3188
+ }
3189
+ _detachLoadHandlers() {
3190
+ this._loadHandle?.off();
3191
+ this._loadHandle = null;
3192
+ this._errorHandle?.off();
3193
+ this._errorHandle = null;
3194
+ }
3195
+ /**
3196
+ * Resolves readiness and dispatches the `load` event. Called once the instantiated content
3197
+ * has been parented beneath the host — the host itself is already in the scene graph by
3198
+ * then, so a ready model's content always has world transforms.
3199
+ */
3200
+ _announceLoad() {
3201
+ this._onReady();
3202
+ this.dispatchEvent(new Event('load'));
3203
+ }
3204
+ _instantiate(container) {
3205
+ const content = container.instantiateRenderEntity();
3206
+ this._contentEntity = content;
3207
+ this._entity.addChild(content);
3208
+ this._announceLoad();
3209
+ }
3210
+ _destroyContent() {
3211
+ this._contentEntity?.destroy();
3212
+ this._contentEntity = null;
3213
+ }
3214
+ async _loadContent() {
3215
+ // The old content goes down synchronously, so a reader that checks after an asset change
3216
+ // never sees the outgoing hierarchy. The host survives - components and child entities
3217
+ // attached to it carry over to the new content.
3218
+ this._destroyContent();
3219
+ // Supersede any load already in flight - only the newest load may instantiate
3220
+ const generation = ++this._loadGeneration;
3221
+ this._detachLoadHandlers();
3222
+ // Re-arm readiness so a waiter obtained after an asset change resolves against the new
3223
+ // content. A no-op on first connection, where readiness is still pending.
3224
+ this._resetReady();
3225
+ const appElement = this.closestApp;
3226
+ if (!appElement) {
3227
+ // Outside pc-app; connectedCallback already warned. Reached through the asset setter.
3228
+ return;
3229
+ }
3230
+ await appElement.ready();
3231
+ // The element may have been removed, or another load started, while we waited
3232
+ if (generation !== this._loadGeneration) {
3233
+ return;
3234
+ }
3235
+ // The host may not be parented yet - a model under a pc-node that has not bound, reached
3236
+ // through the asset setter. Nothing settles here: _onBuilt re-runs this load once the
3237
+ // host builds.
3238
+ if (!this._entity || !this._built) {
3239
+ return;
3240
+ }
3241
+ if (this._asset === '') {
3242
+ // No asset assigned is a settled selection: the element is a usable host (components
3243
+ // attach, waiters resolve) with no content. Assigning an asset later re-arms.
3244
+ this._onReady();
3245
+ return;
3246
+ }
3247
+ const asset = useAsset(this._asset);
3248
+ if (!asset) {
3249
+ // A non-empty id that resolves to nothing is a dead end - say so rather than staying
3250
+ // silently pending.
3251
+ console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
3252
+ return;
3253
+ }
3254
+ if (asset.loaded) {
3255
+ this._instantiate(asset.resource);
3256
+ }
3257
+ else {
3258
+ // The generation is re-checked even though a superseded handler is detached: the
3259
+ // detach relies on how the engine's event emitter treats removal, while the check
3260
+ // holds on its own. Whichever of load/error fires first detaches the other.
3261
+ this._loadHandle = asset.once('load', () => {
3262
+ this._detachLoadHandlers();
3263
+ if (generation !== this._loadGeneration) {
3264
+ return;
3265
+ }
3266
+ this._instantiate(asset.resource);
3267
+ });
3268
+ this._errorHandle = asset.once('error', (err) => {
3269
+ this._detachLoadHandlers();
3270
+ if (generation !== this._loadGeneration) {
3271
+ return;
3272
+ }
3273
+ // A failed load settles readiness with a null contentEntity, mirroring pc-asset:
3274
+ // readiness means the load settled, not that it succeeded.
3275
+ this.dispatchEvent(new ErrorEvent('error', {
3276
+ message: err instanceof Error ? err.message : String(err)
3277
+ }));
3278
+ this._onReady();
3279
+ });
3280
+ }
3281
+ }
3282
+ /**
3283
+ * Sets the id of the `pc-asset` to use for the model.
3284
+ * @param value - The asset ID.
3285
+ */
3286
+ set asset(value) {
3287
+ this._asset = value;
3288
+ if (this.isConnected) {
3289
+ this._loadContent();
3290
+ }
3291
+ }
3292
+ /**
3293
+ * Gets the id of the `pc-asset` to use for the model.
3294
+ * @returns The asset ID.
3295
+ */
3296
+ get asset() {
3297
+ return this._asset;
3298
+ }
3299
+ static get observedAttributes() {
3300
+ return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
3301
+ }
3302
+ attributeChangedCallback(name, _oldValue, newValue) {
3303
+ switch (name) {
3304
+ case 'asset':
3305
+ this.asset = newValue ?? '';
3306
+ break;
3307
+ case 'enabled':
3308
+ this.enabled = parseBool(newValue, true);
3309
+ break;
3310
+ case 'name':
3311
+ this.name = newValue ?? 'Untitled';
3312
+ break;
3313
+ case 'position':
3314
+ this.position = parseVec3(newValue, playcanvas.Vec3.ZERO, name);
3315
+ break;
3316
+ case 'rotation':
3317
+ this.rotation = parseVec3(newValue, playcanvas.Vec3.ZERO, name);
3318
+ break;
3319
+ case 'scale':
3320
+ this.scale = parseVec3(newValue, playcanvas.Vec3.ONE, name);
3321
+ break;
3322
+ case 'tags':
3323
+ this.tags = parseTags(newValue);
3324
+ break;
3325
+ case 'onpointerenter':
3326
+ case 'onpointerleave':
3327
+ case 'onpointerdown':
3328
+ case 'onpointerup':
3329
+ case 'onpointermove':
3330
+ this._updateInlineHandler(name, newValue);
3331
+ break;
3332
+ }
3333
+ }
3334
+ }
3335
+ customElements.define('pc-model', ModelElement);
3336
+
3337
+ /**
3338
+ * Represents a component in the PlayCanvas engine.
3339
+ *
3340
+ * @category Components
3341
+ */
3342
+ class ComponentElement extends AsyncElement {
3343
+ _componentName;
3344
+ _enabled = true;
3345
+ _component = null;
3346
+ _appElement = null;
3347
+ /**
3348
+ * The element hosting this component, held so the host's readiness cycles can be observed
3349
+ * even after `closestEntity` would no longer resolve (during teardown).
3350
+ */
3351
+ _hostElement = null;
3352
+ /**
3353
+ * The listener re-applying this component when the host's readiness cycles. Held for
3354
+ * removal on disconnect.
3355
+ */
3356
+ _hostReadyListener = null;
3357
+ /**
3358
+ * Incremented on every connect and disconnect. connectedCallback captures the value on entry
3359
+ * and abandons itself wherever it resumes from an await if the value has moved on — so a
3360
+ * callback whose element was removed cannot act on a torn-down tree, and one whose element
3361
+ * was removed and re-inserted (which runs a callback of its own) cannot add the component a
3362
+ * second time.
3363
+ */
3364
+ _connectionGeneration = 0;
3365
+ /**
3366
+ * Creates a new ComponentElement instance.
3367
+ *
3368
+ * @param componentName - The name of the component.
3369
+ * @ignore
3370
+ */
3371
+ constructor(componentName) {
3372
+ super();
3373
+ this._componentName = componentName;
3374
+ }
3375
+ /**
3376
+ * Returns the data the component is created with. Overridden by subclasses to supply the
3377
+ * initial values of their cached properties.
3378
+ *
3379
+ * @returns The initial component data.
3380
+ */
3381
+ getInitialComponentData() {
3382
+ return {};
3383
+ }
3384
+ /**
3385
+ * Creates the component on the host's current entity, removing it first from a previous
3386
+ * entity that is still alive (a retargeted `<pc-node>` moves its decorations with it). When
3387
+ * the entity already has a component of this type — a glTF node arriving with its authored
3388
+ * `render` component, say — warns and leaves `component` null. The element-level warning is
3389
+ * load-bearing: the engine's own duplicate-addComponent warning is Debug-stripped from
3390
+ * production builds, which would otherwise leave a silent null.
3391
+ */
3392
+ _applyComponent() {
3393
+ const entity = this._hostElement?.entity ?? null;
3394
+ if (this._component && this._component.entity === entity) {
3395
+ return;
3396
+ }
3397
+ // A retarget leaves the previous component on a still-live entity - remove it so the
3398
+ // decoration follows the element, or vanishes with a dissolved binding. A destroyed
3399
+ // entity took its components with it.
3400
+ const previous = this._component;
3401
+ if (previous?.entity && previous.entity.c[this._componentName] === previous) {
3402
+ previous.entity.removeComponent(this._componentName);
3403
+ }
3404
+ this._component = null;
3405
+ if (!entity) {
3406
+ return;
3407
+ }
3408
+ if (entity.c[this._componentName]) {
3409
+ const label = this.id ? ` '${this.id}'` : '';
3410
+ console.warn(`${this.tagName.toLowerCase()}${label} - '${entity.name}' already has a '${this._componentName}' component - component not added`);
3411
+ return;
3412
+ }
3413
+ this._component = entity.addComponent(this._componentName, this.getInitialComponentData());
3414
+ }
3415
+ async _addComponent() {
3416
+ const generation = this._connectionGeneration;
3417
+ const entityElement = this.closestEntity;
3418
+ if (!entityElement) {
3419
+ // A component can only exist on an entity, so an element placed outside one is inert.
3420
+ // It still becomes ready (with a null `component`), so warn rather than fail silently
3421
+ const label = this.id ? ` '${this.id}'` : '';
3422
+ console.warn(`${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity, pc-model or pc-node - component not added`);
3423
+ return;
3424
+ }
3425
+ await entityElement.ready();
3426
+ // The element may have been removed, or removed and re-inserted, while the entity became
3427
+ // ready — the component belongs to the connection that owns the current generation.
3428
+ if (generation !== this._connectionGeneration) {
3429
+ return;
3430
+ }
3431
+ this._hostElement = entityElement;
3432
+ this._applyComponent();
3433
+ // Re-apply when the host's readiness cycles without this element disconnecting: a
3434
+ // `<pc-node>` rebinding after its model reloads or retargets, or a re-created entity.
3435
+ // The 'ready' event bubbles, so events from descendants pass through this host - only
3436
+ // the host's own cycles count. Readiness is cycled here too, so decorations one level
3437
+ // down re-apply the same way.
3438
+ this._hostReadyListener = (event) => {
3439
+ if (event.target !== this._hostElement) {
3440
+ return;
3441
+ }
3442
+ if (generation !== this._connectionGeneration) {
3443
+ return;
3444
+ }
3445
+ this._hostCycled();
3446
+ };
3447
+ entityElement.addEventListener('ready', this._hostReadyListener);
3448
+ }
3449
+ /**
3450
+ * Re-evaluates this component against the host's current entity: applied to a new entity,
3451
+ * moved from a still-live old one, or removed when the host no longer fronts an entity at
3452
+ * all. Readiness follows - it cycles with a re-application and stays unresolved while the
3453
+ * host is unbound. Called by the host-ready listener, and directly by a `<pc-node>`
3454
+ * dissolving its binding: the one transition that fires no ready event to ride.
3455
+ *
3456
+ * @internal
3457
+ */
3458
+ _hostCycled() {
3459
+ this._resetReady();
3460
+ this._applyComponent();
3461
+ if (this._hostElement?.entity) {
3462
+ this.initComponent();
3463
+ this._onReady();
3464
+ }
3465
+ }
3466
+ /**
3467
+ * Configures the newly added component. Overridden by subclasses whose setup goes beyond
3468
+ * the initial data — child-element handling, asset resolution and the like.
3469
+ */
3470
+ initComponent() {
3471
+ // optional hook
3472
+ }
3473
+ async connectedCallback() {
3474
+ const generation = ++this._connectionGeneration;
3475
+ this._appElement = this.closestApp ?? null;
3476
+ await this._appElement?.ready();
3477
+ // The element may have been removed, or removed and re-inserted, while the application
3478
+ // became ready. A re-insertion runs a connectedCallback of its own, so a stale resume
3479
+ // must not add the component alongside it.
3480
+ if (generation !== this._connectionGeneration) {
3481
+ return;
3482
+ }
3483
+ await this._addComponent();
3484
+ if (generation !== this._connectionGeneration) {
3485
+ return;
3486
+ }
3487
+ this.initComponent();
3488
+ this._onReady();
3489
+ }
3490
+ disconnectedCallback() {
3491
+ // Invalidate any connectedCallback still suspended on an await
3492
+ this._connectionGeneration++;
3493
+ if (this._hostElement && this._hostReadyListener) {
3494
+ this._hostElement.removeEventListener('ready', this._hostReadyListener);
3495
+ }
3496
+ this._hostElement = null;
3497
+ this._hostReadyListener = null;
3498
+ // Remove the component when the element is disconnected. Skip this when the owning
3499
+ // application has already been destroyed — removing a <pc-app> disconnects it before
3500
+ // its children, taking the component systems with it.
3501
+ if (this._appElement?.app && this._component?.entity) {
3502
+ this._component.entity.removeComponent(this._componentName);
3503
+ }
3504
+ this._component = null;
3505
+ this._appElement = null;
3506
+ this._resetReady();
3507
+ }
3508
+ /**
3509
+ * The PlayCanvas component instance. `null` until the element is ready, and also for an
3510
+ * element that is not a descendant of an entity-fronting element (`<pc-entity>`,
3511
+ * `<pc-model>` or `<pc-node>`) — await {@link whenReady} or the element's `ready()` promise
3512
+ * before accessing it.
3513
+ * @returns The component instance, or `null`.
3514
+ */
3515
+ get component() {
3516
+ return this._component;
3517
+ }
3518
+ /**
3519
+ * Sets the enabled state of the component.
3520
+ * @param value - The enabled state of the component.
3521
+ */
3522
+ set enabled(value) {
3523
+ this._enabled = value;
3524
+ if (this.component) {
3525
+ this.component.enabled = value;
3526
+ }
3527
+ }
3528
+ /**
3529
+ * Gets the enabled state of the component.
3530
+ * @returns The enabled state of the component.
3531
+ */
3532
+ get enabled() {
3533
+ return this._enabled;
3534
+ }
3535
+ static get observedAttributes() {
3536
+ return ['enabled'];
3537
+ }
3538
+ attributeChangedCallback(name, _oldValue, newValue) {
3539
+ switch (name) {
3540
+ case 'enabled':
3541
+ this.enabled = parseBool(newValue, true);
3542
+ break;
3543
+ }
3544
+ }
3545
+ }
3546
+
3547
+ /**
3548
+ * The AnimComponentElement interface provides properties and methods for manipulating
3549
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim/ | `<pc-anim>`} elements.
3550
+ * The AnimComponentElement interface also inherits the properties and methods of the
3551
+ * {@link HTMLElement} interface.
3552
+ *
3553
+ * The element drives animation clips over the host entity's hierarchy. Clips come from
3554
+ * `<pc-anim-clip>` children — or, when the element is a direct child of a `<pc-model>` and
3555
+ * declares no clips, every animation of that model's container asset is assigned, named by track
3556
+ * name, in container order. The first clip plays automatically (opt out with `activate="false"`);
3557
+ * switch clips declaratively through the `clip` attribute, or imperatively through {@link play}
3558
+ * and {@link transition}. Tracks bind to scene nodes by name, so any hierarchy whose node names
3559
+ * match a clip's curves can be animated — a model's skeleton is simply the common case.
3560
+ *
3561
+ * The engine reports no clip completion: a non-looping clip holds its last pose silently. Poll
3562
+ * the underlying {@link AnimComponent} (via {@link component}) for playback state beyond what
3563
+ * this element exposes.
3564
+ *
3565
+ * Engine component: {@link AnimComponent} (`anim`).
3566
+ *
3567
+ * @category Components
3568
+ */
3569
+ class AnimComponentElement extends ComponentElement {
3570
+ /**
3571
+ * Whether playback starts automatically once a clip is assigned.
3572
+ */
3573
+ _activate = true;
3574
+ /**
3575
+ * The clip elements whose states are currently assigned, by clip name. The single writer of
3576
+ * a state: a later clip child re-using an adopted name is rejected as a duplicate.
3577
+ */
3578
+ _assignedClips = new Map();
3579
+ /**
3580
+ * Whether the current clip set was auto-assigned from the enclosing model rather than
3581
+ * declared by clip children.
3582
+ */
3583
+ _autoAssigned = false;
3584
+ /**
3585
+ * The name of the active clip.
3586
+ */
3587
+ _clip = '';
3588
+ /**
3589
+ * The binding root this element last assigned through {@link _applyRootBone}, distinguishing
3590
+ * its own writes from a `rootBone` assigned through the engine API — which is left alone.
3591
+ */
3592
+ _managedRootBone = null;
3593
+ /**
3594
+ * The element the model-readiness listener is attached to, held so disconnection can detach
3595
+ * it after `closestEntity` no longer resolves.
3596
+ */
3597
+ _modelListenerTarget = null;
3598
+ /**
3599
+ * Incremented whenever the clip source changes, and captured by an auto-assign pass on
3600
+ * entry — a pass resuming from an await abandons itself if the value has moved on, so a
3601
+ * superseded pass cannot assign clips alongside declared children or a newer pass.
3602
+ */
3603
+ _sourceGeneration = 0;
3604
+ /**
3605
+ * The playback speed multiplier applied across all clips.
3606
+ */
3607
+ _speed = 1;
3608
+ /**
3609
+ * The cross-fade duration of declarative clip switches, in seconds.
3610
+ */
3611
+ _transitionTime = 0;
3612
+ /**
3613
+ * The unknown clip name already warned about, so a repeated selection of the same missing
3614
+ * name complains once.
3615
+ */
3616
+ _warnedClip = null;
3617
+ /**
3618
+ * Rebinds when a model that is not this component's host announces readiness — a sibling or
3619
+ * deeper model whose content changed under the assigned clips. The engine resolves each
3620
+ * curve once, at the first tick after assignment, and never retries — and its mesh-instance
3621
+ * broadcast fires before an instantiated hierarchy is parented, so a model that loads after
3622
+ * the clips were assigned would otherwise stay silently unbound. The host model's own cycles
3623
+ * are excluded: those re-enter through {@link initComponent}, which refreshes the whole clip
3624
+ * set against the new container.
3625
+ */
3626
+ _onModelReady = (event) => {
3627
+ if (!(event.target instanceof ModelElement) || !this.component) {
3628
+ return;
3629
+ }
3630
+ // The host model's own readiness cycle already re-initialized this component through
3631
+ // ComponentElement's host-ready listener, which is attached first and so has run by now.
3632
+ // Acting here too would resolve every track twice and capture the playhead-restore
3633
+ // snapshot mid-rebuild.
3634
+ if (event.target === this._modelListenerTarget) {
3635
+ return;
3636
+ }
3637
+ // A model cycle can replace the skeleton source's host entity (a rebuild under a
3638
+ // retargeting pc-node), so the binding root is re-asserted before the rebind.
3639
+ this._applyRootBone();
3640
+ this.component.rebind();
3641
+ };
3642
+ /**
3643
+ * The model whose host entity scopes this component's curve binding: the parent `pc-model`,
3644
+ * or the sole `pc-model` among the parent's direct children (the arrangement where clips
3645
+ * live in a library asset beside the skeleton). `null` when there is no such model, or more
3646
+ * than one — an ambiguous skeleton is left to the engine's name-based resolution.
3647
+ */
3648
+ _skeletonSource() {
3649
+ const parent = this.parentElement;
3650
+ if (parent instanceof ModelElement) {
3651
+ return parent;
3652
+ }
3653
+ const models = parent ? parent.querySelectorAll(':scope > pc-model') : null;
3654
+ return models?.length === 1 && models[0] instanceof ModelElement ? models[0] : null;
3655
+ }
3656
+ /**
3657
+ * Keeps the component's binding root pointing at the skeleton source's host entity. The host
3658
+ * wraps the instantiated content, so left at its default — the component's own entity — the
3659
+ * engine binder mis-resolves curves that target the asset's root node: its fallback treats
3660
+ * the graph as the asset root once the root is no longer a direct child.
3661
+ *
3662
+ * Authoritative in both directions for values this element assigned: a source appearing pins
3663
+ * its host, and a source dissolving (the model gone, or a second model making the skeleton
3664
+ * ambiguous) clears the pin rather than leaving it on a stale host. A root assigned through
3665
+ * the engine API is never overwritten — the user's choice outranks the managed default.
3666
+ * Writes are skipped while unchanged, because the engine setter itself triggers a rebind.
3667
+ */
3668
+ _applyRootBone() {
3669
+ const component = this.component;
3670
+ // A non-null root this element did not assign came through the engine API. A fresh
3671
+ // component starts at null, which is always reclaimable.
3672
+ if (component.rootBone !== null && component.rootBone !== this._managedRootBone) {
3673
+ return;
3674
+ }
3675
+ const host = this._skeletonSource()?.entity ?? null;
3676
+ if (component.rootBone !== host) {
3677
+ // The engine setter accepts null - restoring the component's own entity as the
3678
+ // binding graph - but its declared type does not
3679
+ component.rootBone = host;
3680
+ }
3681
+ this._managedRootBone = host;
3682
+ }
3683
+ /** @ignore */
3684
+ constructor() {
3685
+ super('anim');
3686
+ }
3687
+ getInitialComponentData() {
3688
+ // The engine assigns creation data in key order and `activate` gates playback, so it
3689
+ // must precede any future key that builds layers (e.g. a state graph)
3690
+ return {
3691
+ activate: this._activate,
3692
+ speed: this._speed
3693
+ };
3694
+ }
3695
+ initComponent() {
3696
+ if (!this.component) {
3697
+ return;
3698
+ }
3699
+ // A host readiness cycle can re-run this. An identical re-add is deduped by the DOM;
3700
+ // the explicit swap handles the listener target changing across connections.
3701
+ const host = this.closestEntity;
3702
+ if (host && host !== this._modelListenerTarget) {
3703
+ this._modelListenerTarget?.removeEventListener('ready', this._onModelReady);
3704
+ host.addEventListener('ready', this._onModelReady);
3705
+ this._modelListenerTarget = host;
3706
+ }
3707
+ this._applyRootBone();
3708
+ if (this.component.baseLayer) {
3709
+ // The component survived the host's readiness cycle (a pc-model reloading content on
3710
+ // its stable host entity). A loaded graph cannot be reassigned in place, so drop it
3711
+ // and reassign from the current source, restoring the active clip and playhead.
3712
+ this._refreshClips();
3713
+ }
3714
+ else {
3715
+ this._applyClips();
3716
+ }
3717
+ }
3718
+ disconnectedCallback() {
3719
+ this._modelListenerTarget?.removeEventListener('ready', this._onModelReady);
3720
+ this._modelListenerTarget = null;
3721
+ // Invalidate any auto-assign still awaiting its model, and drop the adoption
3722
+ // bookkeeping so a reconnection starts clean
3723
+ this._sourceGeneration++;
3724
+ this._assignedClips.clear();
3725
+ this._autoAssigned = false;
3726
+ this._managedRootBone = null;
3727
+ super.disconnectedCallback();
3728
+ }
3729
+ /**
3730
+ * The clip children in DOM order. Read afresh each pass — the DOM is the single source of
3731
+ * truth for the declared clip set.
3732
+ */
3733
+ _clipElements() {
3734
+ return Array.from(this.querySelectorAll(':scope > pc-anim-clip'));
3735
+ }
3736
+ /**
3737
+ * Assigns a clip's state. Until the clip's real track resolves, the engine's own placeholder
3738
+ * track stands in — it keeps the layer playable, so `activate` can start playback and the
3739
+ * declared `clip` selection can apply before any asset has loaded.
3740
+ */
3741
+ _assignClip(clip) {
3742
+ this.component.assignAnimation(clip.name, clip._track ?? playcanvas.AnimTrack.EMPTY, undefined, clip.speed, clip.loop);
3743
+ }
3744
+ /**
3745
+ * Validates a clip child and, when valid, assigns its state and starts its track resolution.
3746
+ *
3747
+ * @param clip - The clip element.
3748
+ * @returns Whether the clip was adopted.
3749
+ */
3750
+ _adoptClip(clip) {
3751
+ const name = clip.name;
3752
+ if (!name) {
3753
+ clip._markInvalid('pc-anim-clip must have a name - clip not assigned');
3754
+ return false;
3755
+ }
3756
+ if (name.indexOf('.') !== -1) {
3757
+ clip._markInvalid(`pc-anim-clip '${name}' - '.' in a clip name is reserved for blend tree paths - clip not assigned`);
3758
+ return false;
3759
+ }
3760
+ if (this._assignedClips.has(name)) {
3761
+ clip._markInvalid(`pc-anim-clip '${name}' - an earlier clip already uses this name - clip not assigned`);
3762
+ return false;
3763
+ }
3764
+ this._assignedClips.set(name, clip);
3765
+ this._assignClip(clip);
3766
+ clip._resolveTrack(this);
3767
+ return true;
3768
+ }
3769
+ /**
3770
+ * Assigns the current clip set: the declared clip children when there are any, otherwise the
3771
+ * enclosing model's clips. Runs against a fresh component after a host cycle, so the
3772
+ * adoption bookkeeping rebuilds from scratch.
3773
+ */
3774
+ _applyClips(restore) {
3775
+ if (!this.component) {
3776
+ return;
3777
+ }
3778
+ this._sourceGeneration++;
3779
+ this._assignedClips.clear();
3780
+ this._autoAssigned = false;
3781
+ const clips = this._clipElements();
3782
+ if (clips.length === 0) {
3783
+ this._kickAutoAssign(restore);
3784
+ return;
3785
+ }
3786
+ for (const clip of clips) {
3787
+ this._adoptClip(clip);
3788
+ }
3789
+ this._applySelection(restore);
3790
+ }
3791
+ /**
3792
+ * Assigns every clip of the enclosing model's container, named by track name, in container
3793
+ * order. Names the engine cannot host — dotted (reserved for blend tree paths) or already
3794
+ * taken — are skipped with a warning naming each.
3795
+ */
3796
+ async _kickAutoAssign(restore) {
3797
+ const generation = this._sourceGeneration;
3798
+ const model = this.parentElement;
3799
+ if (!(model instanceof ModelElement)) {
3800
+ // Not inside a model: an empty component, driven through the JS API
3801
+ return;
3802
+ }
3803
+ await model.ready();
3804
+ // The source may have changed while the model loaded - a declared clip child appearing
3805
+ // flips the element over to declared mode, and wins
3806
+ const component = this.component;
3807
+ if (generation !== this._sourceGeneration || !component || this._clipElements().length > 0) {
3808
+ return;
3809
+ }
3810
+ const container = AssetElement.get(model.asset)?.resource;
3811
+ if (!container) {
3812
+ // The load failed; the model already reported it
3813
+ return;
3814
+ }
3815
+ const label = this.id ? ` '${this.id}'` : '';
3816
+ if (container.animations.length === 0) {
3817
+ console.warn(`pc-anim${label} - model '${model.asset}' has no animations`);
3818
+ return;
3819
+ }
3820
+ const seen = new Set();
3821
+ for (const animationAsset of container.animations) {
3822
+ const track = animationAsset.resource;
3823
+ if (!(track instanceof playcanvas.AnimTrack)) {
3824
+ continue;
3825
+ }
3826
+ if (track.name.indexOf('.') !== -1) {
3827
+ console.warn(`pc-anim${label} - track '${track.name}' - '.' in a clip name is reserved for blend tree paths - track skipped`);
3828
+ continue;
3829
+ }
3830
+ if (seen.has(track.name)) {
3831
+ console.warn(`pc-anim${label} - duplicate track name '${track.name}' - track skipped`);
3832
+ continue;
3833
+ }
3834
+ seen.add(track.name);
3835
+ component.assignAnimation(track.name, track);
3836
+ }
3837
+ this._autoAssigned = seen.size > 0;
3838
+ this._applySelection(restore);
3839
+ }
3840
+ /**
3841
+ * Applies the active-clip selection: the declared `clip` when it names an assigned state,
3842
+ * else a captured pre-rebuild state when it survived, else the engine's default (the first
3843
+ * assigned clip). A restore also reinstates the playhead and both playing flags exactly as
3844
+ * captured — the reassignment that preceded it set both to the `activate` outcome, which is
3845
+ * not necessarily the state the rebuild interrupted.
3846
+ */
3847
+ _applySelection(restore) {
3848
+ const component = this.component;
3849
+ const layer = component ? component.baseLayer : null;
3850
+ if (!component || !layer) {
3851
+ return;
3852
+ }
3853
+ if (this._clip && !layer.states.includes(this._clip)) {
3854
+ this._warnUnknownClip(this._clip);
3855
+ }
3856
+ let target = null;
3857
+ if (this._clip && layer.states.includes(this._clip)) {
3858
+ target = this._clip;
3859
+ }
3860
+ else if (restore && layer.states.includes(restore.state)) {
3861
+ target = restore.state;
3862
+ }
3863
+ if (target && layer.activeState !== target) {
3864
+ layer.play(target);
3865
+ }
3866
+ if (restore) {
3867
+ if (target === restore.state) {
3868
+ layer.activeStateCurrentTime = restore.time;
3869
+ }
3870
+ layer.playing = restore.layerPlaying;
3871
+ component.playing = restore.playing;
3872
+ }
3873
+ }
3874
+ _warnUnknownClip(name) {
3875
+ if (this._warnedClip === name) {
3876
+ return;
3877
+ }
3878
+ this._warnedClip = name;
3879
+ const label = this.id ? ` '${this.id}'` : '';
3880
+ console.warn(`pc-anim${label} has no clip named '${name}' - selection unchanged`);
3881
+ }
3882
+ /**
3883
+ * Rebuilds the clip set from the DOM, restoring the active clip and playhead when they
3884
+ * survive the rebuild. The engine cannot remove a state from a loaded graph (unassigning
3885
+ * only empties the state's tracks), so removals, renames and source changes drop the whole
3886
+ * graph and reassign.
3887
+ *
3888
+ * @internal
3889
+ */
3890
+ _refreshClips() {
3891
+ const component = this.component;
3892
+ if (!component) {
3893
+ return;
3894
+ }
3895
+ // A clip-set change is also a chance for the skeleton source to have changed shape (a
3896
+ // clip child appearing or leaving can accompany a model coming or going) - re-derive the
3897
+ // binding root before the reassignment binds against it.
3898
+ this._applyRootBone();
3899
+ const layer = component.baseLayer;
3900
+ const restore = layer ? {
3901
+ state: layer.activeState,
3902
+ time: layer.activeStateCurrentTime,
3903
+ playing: component.playing,
3904
+ layerPlaying: layer.playing
3905
+ } : undefined;
3906
+ component.removeStateGraph();
3907
+ this._applyClips(restore);
3908
+ }
3909
+ /**
3910
+ * Adopts a clip child announced by its connectedCallback. The initComponent sweep adopts
3911
+ * children already present, so this is a no-op for those; it serves clips appended later,
3912
+ * and flips an auto-assigned element over to its declared children — declared clips win.
3913
+ *
3914
+ * @param clip - The clip element.
3915
+ * @internal
3916
+ */
3917
+ _registerClip(clip) {
3918
+ if (!this.component) {
3919
+ return;
3920
+ }
3921
+ if (this._autoAssigned) {
3922
+ this._refreshClips();
3923
+ return;
3924
+ }
3925
+ if (this._assignedClips.get(clip.name) === clip) {
3926
+ return;
3927
+ }
3928
+ // A clip child appearing supersedes an auto-assign still awaiting its model
3929
+ this._sourceGeneration++;
3930
+ if (this._adoptClip(clip)) {
3931
+ this._applySelection();
3932
+ }
3933
+ }
3934
+ /**
3935
+ * Releases a disconnected clip child. Rebuilds the set — a state cannot be removed from a
3936
+ * live graph — and the removal of the last child inside a `<pc-model>` flips the element
3937
+ * back to auto-assigning the model's clips.
3938
+ *
3939
+ * @param clip - The clip element.
3940
+ * @internal
3941
+ */
3942
+ _unregisterClip(clip) {
3943
+ if (!this.component) {
3944
+ // The whole subtree is coming down (parents disconnect first) - nothing to rebuild
3945
+ return;
3946
+ }
3947
+ if (this._assignedClips.get(clip.name) !== clip) {
3948
+ // The clip never held a state (invalid or duplicate name)
3949
+ return;
3950
+ }
3951
+ this._refreshClips();
3952
+ }
3953
+ /**
3954
+ * Swaps a clip's resolved track in for the placeholder (or for its previous track after an
3955
+ * asset change). A swap of the active clip restarts it: the engine preserves the playhead
3956
+ * through a track replacement, which would land mid-way into unrelated animation.
3957
+ *
3958
+ * @param clip - The clip element.
3959
+ * @returns Whether the clip still owns its state — the resolution may have been superseded
3960
+ * by a rebuild that dropped it.
3961
+ * @internal
3962
+ */
3963
+ _onClipResolved(clip) {
3964
+ const component = this.component;
3965
+ if (!component || this._assignedClips.get(clip.name) !== clip) {
3966
+ return false;
3967
+ }
3968
+ this._assignClip(clip);
3969
+ const layer = component.baseLayer;
3970
+ if (layer && layer.activeState === clip.name) {
3971
+ layer.play(clip.name);
3972
+ }
3973
+ return true;
3974
+ }
3975
+ /**
3976
+ * Applies a clip's changed speed or loop. The engine bakes both into the playback state it
3977
+ * creates on state entry, so a live change re-enters the state and restores the playhead.
3978
+ *
3979
+ * @param clip - The clip element.
3980
+ * @internal
3981
+ */
3982
+ _onClipParamsChanged(clip) {
3983
+ const component = this.component;
3984
+ if (!component || this._assignedClips.get(clip.name) !== clip) {
3985
+ return;
3986
+ }
3987
+ this._assignClip(clip);
3988
+ const layer = component.baseLayer;
3989
+ if (layer && layer.activeState === clip.name) {
3990
+ const time = layer.activeStateCurrentTime;
3991
+ layer.play(clip.name);
3992
+ layer.activeStateCurrentTime = time;
3993
+ }
3994
+ }
3995
+ /**
3996
+ * Resumes playback, optionally switching to a named clip first (a hard cut). A name that
3997
+ * matches no clip leaves the selection unchanged.
3998
+ *
3999
+ * @param name - The name of the clip to play. Resumes the current clip when omitted.
4000
+ */
4001
+ play(name) {
4002
+ const component = this.component;
4003
+ const layer = component ? component.baseLayer : null;
4004
+ if (!component || !layer) {
4005
+ return;
4006
+ }
4007
+ // layer.play sets the layer controller's playing flag; the component's is the system
4008
+ // gate. Setting both is what makes this a resume regardless of how playback stopped.
4009
+ if (name !== undefined) {
4010
+ if (!layer.states.includes(name)) {
4011
+ return;
4012
+ }
4013
+ layer.play(name);
4014
+ }
4015
+ else {
4016
+ layer.play();
4017
+ }
4018
+ component.playing = true;
4019
+ }
4020
+ /**
4021
+ * Pauses playback, preserving the playhead — {@link play} resumes from where it stopped.
4022
+ */
4023
+ pause() {
4024
+ if (!this.component) {
4025
+ return;
4026
+ }
4027
+ // Only the component flag - the single gate the system tick reads - is cleared. The
4028
+ // layer controller's flag is left as-is so a pause is exactly reversible, whether
4029
+ // resumed through play() (which sets both) or through the component API directly.
4030
+ this.component.playing = false;
4031
+ }
4032
+ /**
4033
+ * Cross-fades to a named clip and ensures playback is running. A name that matches no clip
4034
+ * leaves the selection unchanged.
4035
+ *
4036
+ * @param name - The name of the clip to fade to.
4037
+ * @param time - The fade duration in seconds. Defaults to the `transition-time` attribute.
4038
+ */
4039
+ transition(name, time) {
4040
+ const component = this.component;
4041
+ const layer = component ? component.baseLayer : null;
4042
+ if (!component || !layer || !layer.states.includes(name)) {
4043
+ return;
4044
+ }
4045
+ layer.transition(name, Math.max(0, time ?? this._transitionTime));
4046
+ layer.playing = true;
4047
+ component.playing = true;
4048
+ }
4049
+ /**
4050
+ * Gets the underlying PlayCanvas anim component.
4051
+ * @returns The anim component.
4052
+ */
4053
+ get component() {
4054
+ return super.component;
4055
+ }
4056
+ /**
4057
+ * Gets the names of the assigned clips.
4058
+ * @returns The clip names, in assignment order.
4059
+ */
4060
+ get clips() {
4061
+ const layer = this.component ? this.component.baseLayer : null;
4062
+ return layer ? layer.states.filter(state => !playcanvas.ANIM_CONTROL_STATES.includes(state)) : [];
4063
+ }
4064
+ /**
4065
+ * Sets whether playback starts automatically once a clip is assigned. Defaults to `true`.
4066
+ * Applies when clips are assigned — it does not stop a clip that is already playing.
4067
+ * @param value - Whether playback starts automatically.
4068
+ */
4069
+ set activate(value) {
4070
+ this._activate = value;
4071
+ if (this.component) {
4072
+ this.component.activate = value;
4073
+ }
4074
+ }
4075
+ /**
4076
+ * Gets whether playback starts automatically once a clip is assigned.
4077
+ * @returns Whether playback starts automatically.
4078
+ */
4079
+ get activate() {
4080
+ return this._activate;
4081
+ }
4082
+ /**
4083
+ * Sets the name of the active clip. Changing it switches playback, cross-fading over
4084
+ * `transition-time` seconds (a hard cut at 0). An empty value leaves the current clip
4085
+ * playing; a name that matches no clip warns and leaves the selection unchanged.
4086
+ * @param value - The name of the active clip.
4087
+ */
4088
+ set clip(value) {
4089
+ this._clip = value;
4090
+ const component = this.component;
4091
+ const layer = component ? component.baseLayer : null;
4092
+ if (!component || !layer || !value) {
4093
+ return;
4094
+ }
4095
+ if (!layer.states.includes(value)) {
4096
+ this._warnUnknownClip(value);
4097
+ return;
4098
+ }
4099
+ if (layer.activeState === value) {
4100
+ return;
4101
+ }
4102
+ if (this._transitionTime > 0) {
4103
+ this.transition(value);
4104
+ }
4105
+ else {
4106
+ this.play(value);
4107
+ }
4108
+ }
4109
+ /**
4110
+ * Gets the name of the active clip.
4111
+ * @returns The name of the active clip.
4112
+ */
4113
+ get clip() {
4114
+ return this._clip;
4115
+ }
4116
+ /**
4117
+ * Sets the playback speed multiplier applied across all clips, where 0 freezes playback.
4118
+ * Defaults to 1.
4119
+ * @param value - The playback speed multiplier.
4120
+ */
4121
+ set speed(value) {
4122
+ this._speed = value;
4123
+ if (this.component) {
4124
+ this.component.speed = value;
4125
+ }
4126
+ }
4127
+ /**
4128
+ * Gets the playback speed multiplier applied across all clips.
4129
+ * @returns The playback speed multiplier.
4130
+ */
4131
+ get speed() {
4132
+ return this._speed;
4133
+ }
4134
+ /**
4135
+ * Sets the cross-fade duration of clip switches made through the `clip` attribute, in
4136
+ * seconds. Defaults to 0 (a hard cut).
4137
+ * @param value - The cross-fade duration in seconds.
4138
+ */
4139
+ set transitionTime(value) {
4140
+ this._transitionTime = value;
4141
+ }
4142
+ /**
4143
+ * Gets the cross-fade duration of clip switches made through the `clip` attribute.
4144
+ * @returns The cross-fade duration in seconds.
4145
+ */
4146
+ get transitionTime() {
4147
+ return this._transitionTime;
4148
+ }
4149
+ static get observedAttributes() {
4150
+ return [...super.observedAttributes, 'activate', 'clip', 'speed', 'transition-time'];
4151
+ }
4152
+ attributeChangedCallback(name, _oldValue, newValue) {
4153
+ super.attributeChangedCallback(name, _oldValue, newValue);
4154
+ switch (name) {
4155
+ case 'activate':
4156
+ this.activate = parseBool(newValue, true);
4157
+ break;
4158
+ case 'clip':
4159
+ this.clip = newValue ?? '';
4160
+ break;
4161
+ case 'speed':
4162
+ this.speed = parseNumber(newValue, 1, name);
4163
+ break;
4164
+ case 'transition-time':
4165
+ this.transitionTime = parseNumber(newValue, 0, name);
4166
+ break;
4167
+ }
4168
+ }
4169
+ }
4170
+ customElements.define('pc-anim', AnimComponentElement);
4171
+
4172
+ /**
4173
+ * The AnimClipElement interface provides properties and methods for manipulating
4174
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim-clip/ | `<pc-anim-clip>`}
4175
+ * elements. The AnimClipElement interface also inherits the properties and methods of the
4176
+ * {@link HTMLElement} interface.
4177
+ *
4178
+ * A clip declares one named animation on its parent `<pc-anim>`. `name` is both the clip's name
4179
+ * and the track looked up in the clip's source: an explicit `asset` (a `container`, an
4180
+ * `animation` `.glb`, or an `animclip` JSON), or, without one, the container of the `<pc-model>`
4181
+ * enclosing the parent `<pc-anim>`. A source holding a single track supplies it whatever it is
4182
+ * named; in a multi-track source the track named `name` is chosen, falling back to the first
4183
+ * with a warning. The element becomes ready once its resolved track is assigned.
4184
+ *
4185
+ * @category Components
4186
+ */
4187
+ class AnimClipElement extends AsyncElement {
2939
4188
  /**
2940
- * The element hosting this component, held so the host's readiness cycles can be observed
2941
- * even after `closestEntity` would no longer resolve (during teardown).
4189
+ * The `<pc-anim>` this clip was adopted by, captured when the parent adopts the clip and on
4190
+ * connection.
4191
+ *
4192
+ * `disconnectedCallback` cannot rediscover it: by the time the element is disconnected its
4193
+ * `parentElement` is already `null`, so a lookup would both fail to find the component and
4194
+ * emit a misleading "must be a direct child" warning for what is an ordinary removal.
2942
4195
  */
2943
- _hostElement = null;
4196
+ _animElement = null;
4197
+ _asset = '';
2944
4198
  /**
2945
- * The listener re-applying this component when the host's readiness cycles. Held for
2946
- * removal on disconnect.
4199
+ * Incremented on every connect and disconnect, and captured by connectedCallback on entry —
4200
+ * a resume from an await abandons itself if the value has moved on, so a stale callback can
4201
+ * neither act on a torn-down tree nor register its clip alongside a re-inserted element's
4202
+ * own callback.
2947
4203
  */
2948
- _hostReadyListener = null;
4204
+ _connectionGeneration = 0;
4205
+ _errorHandle = null;
2949
4206
  /**
2950
- * Incremented on every connect and disconnect. connectedCallback captures the value on entry
2951
- * and abandons itself wherever it resumes from an await if the value has moved on — so a
2952
- * callback whose element was removed cannot act on a torn-down tree, and one whose element
2953
- * was removed and re-inserted (which runs a callback of its own) cannot add the component a
2954
- * second time.
4207
+ * Incremented on every track resolution and on disconnect, and captured by a resolution when
4208
+ * it starts. A resolution that resumes from an await or an asset callback abandons itself if
4209
+ * the value has moved on, so a superseded resolution cannot hand a stale track to the parent.
2955
4210
  */
2956
- _connectionGeneration = 0;
4211
+ _loadGeneration = 0;
2957
4212
  /**
2958
- * Creates a new ComponentElement instance.
4213
+ * The pending asset subscriptions of the current resolution, if it is waiting for its asset.
4214
+ * Held so that whatever supersedes the resolution can detach the handlers from the asset,
4215
+ * rather than leave them registered until the asset settles (or forever, if it never does).
4216
+ */
4217
+ _loadHandle = null;
4218
+ _loop = true;
4219
+ _name = '';
4220
+ _speed = 1;
4221
+ /**
4222
+ * The source complaint already made — the asset id it was made for, or `''` for the
4223
+ * no-asset-no-model case — so re-resolutions (host cycles, model reloads) do not repeat it.
4224
+ */
4225
+ _warnedSource = null;
4226
+ /**
4227
+ * Whether the owning `<pc-anim>` already rejected this clip's name — its sweeps re-run on
4228
+ * host cycles and must not repeat the complaint.
4229
+ */
4230
+ _warnedInvalid = false;
4231
+ /**
4232
+ * The clip's resolved track. `null` until resolution completes, during which the owning
4233
+ * `<pc-anim>` assigns the engine's placeholder track in its stead.
2959
4234
  *
2960
- * @param componentName - The name of the component.
2961
- * @ignore
4235
+ * @internal
2962
4236
  */
2963
- constructor(componentName) {
2964
- super();
2965
- this._componentName = componentName;
4237
+ _track = null;
4238
+ async connectedCallback() {
4239
+ const generation = ++this._connectionGeneration;
4240
+ const animElement = this.animElement;
4241
+ await animElement?.ready();
4242
+ // The element may have been removed (perhaps re-inserted, which runs a callback of its
4243
+ // own), or its parent torn down, while we were waiting. A <pc-app> disconnects before
4244
+ // its children, so by the time we resume the component can already be gone - see the
4245
+ // matching guard in disconnectedCallback below.
4246
+ const component = animElement ? animElement.component : null;
4247
+ if (generation !== this._connectionGeneration || !animElement || !component) {
4248
+ return;
4249
+ }
4250
+ this._animElement = animElement;
4251
+ animElement._registerClip(this);
4252
+ }
4253
+ disconnectedCallback() {
4254
+ // Invalidate any connectedCallback or track resolution still suspended on an await
4255
+ this._connectionGeneration++;
4256
+ this._loadGeneration++;
4257
+ this._detachLoadHandlers();
4258
+ // Uses the cached parent rather than a fresh lookup, since parentElement is already null
4259
+ // by now. The component itself is null if the whole <pc-app> is being torn down —
4260
+ // parents disconnect first and have already removed the component.
4261
+ this._animElement?._unregisterClip(this);
4262
+ this._animElement = null;
4263
+ this._track = null;
4264
+ this._resetReady();
4265
+ }
4266
+ get animElement() {
4267
+ const animElement = this.parentElement;
4268
+ if (!(animElement instanceof AnimComponentElement)) {
4269
+ const label = this._name ? ` '${this._name}'` : '';
4270
+ console.warn(`pc-anim-clip${label} must be a direct child of a pc-anim element`);
4271
+ return null;
4272
+ }
4273
+ return animElement;
4274
+ }
4275
+ _detachLoadHandlers() {
4276
+ this._loadHandle?.off();
4277
+ this._loadHandle = null;
4278
+ this._errorHandle?.off();
4279
+ this._errorHandle = null;
2966
4280
  }
2967
4281
  /**
2968
- * Returns the data the component is created with. Overridden by subclasses to supply the
2969
- * initial values of their cached properties.
4282
+ * Reports a name-validation failure from the owning `<pc-anim>`, once per name value.
2970
4283
  *
2971
- * @returns The initial component data.
4284
+ * @param message - The complaint.
4285
+ * @internal
2972
4286
  */
2973
- getInitialComponentData() {
2974
- return {};
4287
+ _markInvalid(message) {
4288
+ if (this._warnedInvalid) {
4289
+ return;
4290
+ }
4291
+ this._warnedInvalid = true;
4292
+ console.warn(message);
2975
4293
  }
2976
4294
  /**
2977
- * Creates the component on the host's current entity, removing it first from a previous
2978
- * entity that is still alive (a retargeted `<pc-node>` moves its decorations with it). When
2979
- * the entity already has a component of this type a glTF node arriving with its authored
2980
- * `render` component, say — warns and leaves `component` null. The element-level warning is
2981
- * load-bearing: the engine's own duplicate-addComponent warning is Debug-stripped from
2982
- * production builds, which would otherwise leave a silent null.
4295
+ * Resolves the clip's track from its source and hands it to the owning `<pc-anim>`. Called
4296
+ * by the parent whenever the clip is (re)adopted, and again when the source changes; a newer
4297
+ * resolution supersedes one still in flight. The element becomes ready once the resolved
4298
+ * track is assigned.
4299
+ *
4300
+ * @param animElement - The owning `<pc-anim>`.
4301
+ * @internal
2983
4302
  */
2984
- _applyComponent() {
2985
- const entity = this._hostElement?.entity ?? null;
2986
- if (this._component && this._component.entity === entity) {
4303
+ async _resolveTrack(animElement) {
4304
+ this._animElement = animElement;
4305
+ const generation = ++this._loadGeneration;
4306
+ this._detachLoadHandlers();
4307
+ if (this._asset) {
4308
+ const asset = useAsset(this._asset);
4309
+ if (!asset) {
4310
+ this._warnSource(`pc-anim-clip '${this._name}' could not find asset '${this._asset}' - clip not assigned`);
4311
+ return;
4312
+ }
4313
+ if (asset.loaded) {
4314
+ this._extractTrack(asset, `asset '${this._asset}'`);
4315
+ return;
4316
+ }
4317
+ // Whichever of load/error fires first detaches the other. The generation is
4318
+ // re-checked even though a superseded handler is detached: the detach relies on how
4319
+ // the engine's event emitter treats removal, while the check holds on its own.
4320
+ this._loadHandle = asset.once('load', () => {
4321
+ this._detachLoadHandlers();
4322
+ if (generation !== this._loadGeneration) {
4323
+ return;
4324
+ }
4325
+ this._extractTrack(asset, `asset '${this._asset}'`);
4326
+ });
4327
+ this._errorHandle = asset.once('error', () => {
4328
+ this._detachLoadHandlers();
4329
+ if (generation !== this._loadGeneration) {
4330
+ return;
4331
+ }
4332
+ this._warnSource(`pc-anim-clip '${this._name}' - asset '${this._asset}' failed to load - clip not assigned`);
4333
+ });
2987
4334
  return;
2988
4335
  }
2989
- // A retarget leaves the previous component on a still-live entity - remove it so the
2990
- // decoration follows the element, or vanishes with a dissolved binding. A destroyed
2991
- // entity took its components with it.
2992
- const previous = this._component;
2993
- if (previous?.entity && previous.entity.c[this._componentName] === previous) {
2994
- previous.entity.removeComponent(this._componentName);
4336
+ const model = animElement.parentElement;
4337
+ if (!(model instanceof ModelElement)) {
4338
+ this._warnSource(`pc-anim-clip '${this._name}' has no asset and no enclosing pc-model - clip not assigned`);
4339
+ return;
2995
4340
  }
2996
- this._component = null;
2997
- if (!entity) {
4341
+ await model.ready();
4342
+ if (generation !== this._loadGeneration) {
2998
4343
  return;
2999
4344
  }
3000
- if (entity.c[this._componentName]) {
3001
- const label = this.id ? ` '${this.id}'` : '';
3002
- console.warn(`${this.tagName.toLowerCase()}${label} - '${entity.name}' already has a '${this._componentName}' component - component not added`);
4345
+ const asset = AssetElement.get(model.asset);
4346
+ if (!asset?.resource) {
4347
+ // The model's load failed; it already reported the error
3003
4348
  return;
3004
4349
  }
3005
- this._component = entity.addComponent(this._componentName, this.getInitialComponentData());
4350
+ this._extractTrack(asset, `model '${model.asset}'`);
3006
4351
  }
3007
- async _addComponent() {
3008
- const generation = this._connectionGeneration;
3009
- const entityElement = this.closestEntity;
3010
- if (!entityElement) {
3011
- // A component can only exist on an entity, so an element placed outside one is inert.
3012
- // It still becomes ready (with a null `component`), so warn rather than fail silently
3013
- const label = this.id ? ` '${this.id}'` : '';
3014
- console.warn(`${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity - component not added`);
4352
+ /**
4353
+ * Complains about the clip's source, once per source value — resolutions re-run on host
4354
+ * cycles and model reloads, and must not repeat the complaint.
4355
+ */
4356
+ _warnSource(message) {
4357
+ if (this._warnedSource === this._asset) {
3015
4358
  return;
3016
4359
  }
3017
- await entityElement.ready();
3018
- // The element may have been removed, or removed and re-inserted, while the entity became
3019
- // ready — the component belongs to the connection that owns the current generation.
3020
- if (generation !== this._connectionGeneration) {
4360
+ this._warnedSource = this._asset;
4361
+ console.warn(message);
4362
+ }
4363
+ /**
4364
+ * Picks the clip's track out of a loaded source asset: the track named `name`, or a lone
4365
+ * track whatever it is named, or the first of several with a warning.
4366
+ *
4367
+ * @param asset - The loaded source asset.
4368
+ * @param source - How warnings name the source.
4369
+ */
4370
+ _extractTrack(asset, source) {
4371
+ const label = `pc-anim-clip '${this._name}'`;
4372
+ // Widened: the engine registers an 'animclip' handler but omits the type from the
4373
+ // Asset.type union
4374
+ const type = asset.type;
4375
+ let candidates;
4376
+ switch (type) {
4377
+ case 'container':
4378
+ candidates = asset.resource.animations.map((animationAsset) => animationAsset.resource);
4379
+ break;
4380
+ case 'animation':
4381
+ candidates = asset.resources;
4382
+ break;
4383
+ case 'animclip':
4384
+ candidates = [asset.resource];
4385
+ break;
4386
+ default:
4387
+ this._warnSource(`${label} - ${source} has type '${asset.type}', expected 'container', 'animation' or 'animclip' - clip not assigned`);
4388
+ return;
4389
+ }
4390
+ // A JSON 'animation' asset parses to the engine's legacy Animation class, which the anim
4391
+ // system rejects - only real AnimTracks qualify
4392
+ const tracks = candidates.filter((candidate) => candidate instanceof playcanvas.AnimTrack);
4393
+ if (tracks.length === 0) {
4394
+ this._warnSource(`${label} - ${source} contains no usable animation track - clip not assigned`);
3021
4395
  return;
3022
4396
  }
3023
- this._hostElement = entityElement;
3024
- this._applyComponent();
3025
- // Re-apply when the host's readiness cycles without this element disconnecting: a
3026
- // `<pc-node>` rebinding after its model reloads or retargets, or a re-created entity.
3027
- // The 'ready' event bubbles, so events from descendants pass through this host - only
3028
- // the host's own cycles count. Readiness is cycled here too, so decorations one level
3029
- // down re-apply the same way.
3030
- this._hostReadyListener = (event) => {
3031
- if (event.target !== this._hostElement) {
3032
- return;
3033
- }
3034
- if (generation !== this._connectionGeneration) {
3035
- return;
4397
+ let track = tracks.find((candidate) => candidate.name === this._name);
4398
+ if (!track) {
4399
+ track = tracks[0];
4400
+ if (tracks.length > 1) {
4401
+ console.warn(`${label} - no track named '${this._name}' in ${source} - using '${track.name}' (available: ${tracks.map((candidate) => candidate.name).join(', ')})`);
3036
4402
  }
3037
- this._hostCycled();
3038
- };
3039
- entityElement.addEventListener('ready', this._hostReadyListener);
4403
+ }
4404
+ this._track = track;
4405
+ if (this._animElement?._onClipResolved(this)) {
4406
+ this._onReady();
4407
+ }
3040
4408
  }
3041
4409
  /**
3042
- * Re-evaluates this component against the host's current entity: applied to a new entity,
3043
- * moved from a still-live old one, or removed when the host no longer fronts an entity at
3044
- * all. Readiness follows - it cycles with a re-application and stays unresolved while the
3045
- * host is unbound. Called by the host-ready listener, and directly by a `<pc-node>`
3046
- * dissolving its binding: the one transition that fires no ready event to ride.
3047
- *
3048
- * @internal
4410
+ * Sets the id of the `pc-asset` supplying the clip's track: a `container`, an `animation`
4411
+ * `.glb`, or an `animclip` JSON. When empty, the track comes from the container of the
4412
+ * `<pc-model>` enclosing the parent `<pc-anim>`.
4413
+ * @param value - The asset id.
3049
4414
  */
3050
- _hostCycled() {
3051
- this._resetReady();
3052
- this._applyComponent();
3053
- if (this._hostElement?.entity) {
3054
- this.initComponent();
3055
- this._onReady();
4415
+ set asset(value) {
4416
+ this._asset = value;
4417
+ this._warnedSource = null;
4418
+ if (this._animElement) {
4419
+ this._resetReady();
4420
+ this._track = null;
4421
+ this._resolveTrack(this._animElement);
3056
4422
  }
3057
4423
  }
3058
4424
  /**
3059
- * Configures the newly added component. Overridden by subclasses whose setup goes beyond
3060
- * the initial data — child-element handling, asset resolution and the like.
4425
+ * Gets the id of the `pc-asset` supplying the clip's track.
4426
+ * @returns The asset id.
3061
4427
  */
3062
- initComponent() {
3063
- // optional hook
4428
+ get asset() {
4429
+ return this._asset;
3064
4430
  }
3065
- async connectedCallback() {
3066
- const generation = ++this._connectionGeneration;
3067
- this._appElement = this.closestApp ?? null;
3068
- await this._appElement?.ready();
3069
- // The element may have been removed, or removed and re-inserted, while the application
3070
- // became ready. A re-insertion runs a connectedCallback of its own, so a stale resume
3071
- // must not add the component alongside it.
3072
- if (generation !== this._connectionGeneration) {
3073
- return;
3074
- }
3075
- await this._addComponent();
3076
- if (generation !== this._connectionGeneration) {
3077
- return;
3078
- }
3079
- this.initComponent();
3080
- this._onReady();
4431
+ /**
4432
+ * Sets whether the clip loops. A non-looping clip holds its last pose when it ends — the
4433
+ * engine reports no completion. Defaults to `true`.
4434
+ * @param value - Whether the clip loops.
4435
+ */
4436
+ set loop(value) {
4437
+ this._loop = value;
4438
+ this._animElement?._onClipParamsChanged(this);
3081
4439
  }
3082
- disconnectedCallback() {
3083
- // Invalidate any connectedCallback still suspended on an await
3084
- this._connectionGeneration++;
3085
- if (this._hostElement && this._hostReadyListener) {
3086
- this._hostElement.removeEventListener('ready', this._hostReadyListener);
3087
- }
3088
- this._hostElement = null;
3089
- this._hostReadyListener = null;
3090
- // Remove the component when the element is disconnected. Skip this when the owning
3091
- // application has already been destroyed removing a <pc-app> disconnects it before
3092
- // its children, taking the component systems with it.
3093
- if (this._appElement?.app && this._component?.entity) {
3094
- this._component.entity.removeComponent(this._componentName);
4440
+ /**
4441
+ * Gets whether the clip loops.
4442
+ * @returns Whether the clip loops.
4443
+ */
4444
+ get loop() {
4445
+ return this._loop;
4446
+ }
4447
+ /**
4448
+ * Sets the name of the clip: the name it is played by, and the track looked up in the
4449
+ * clip's source. Names must be unique within a `<pc-anim>` and must not contain `.`.
4450
+ * @param value - The clip name.
4451
+ */
4452
+ set name(value) {
4453
+ this._name = value;
4454
+ this._warnedInvalid = false;
4455
+ if (this._animElement) {
4456
+ this._resetReady();
4457
+ this._animElement._refreshClips();
3095
4458
  }
3096
- this._component = null;
3097
- this._appElement = null;
3098
- this._resetReady();
3099
4459
  }
3100
4460
  /**
3101
- * The PlayCanvas component instance. `null` until the element is ready, and also for an
3102
- * element that is not a descendant of a `<pc-entity>` — await {@link whenReady} or the
3103
- * element's `ready()` promise before accessing it.
3104
- * @returns The component instance, or `null`.
4461
+ * Gets the name of the clip.
4462
+ * @returns The clip name.
3105
4463
  */
3106
- get component() {
3107
- return this._component;
4464
+ get name() {
4465
+ return this._name;
3108
4466
  }
3109
4467
  /**
3110
- * Sets the enabled state of the component.
3111
- * @param value - The enabled state of the component.
4468
+ * Sets the playback speed of the clip, where negative values play it backwards. Applies
4469
+ * immediately, preserving the playhead. Defaults to 1.
4470
+ * @param value - The playback speed.
3112
4471
  */
3113
- set enabled(value) {
3114
- this._enabled = value;
3115
- if (this.component) {
3116
- this.component.enabled = value;
3117
- }
4472
+ set speed(value) {
4473
+ this._speed = value;
4474
+ this._animElement?._onClipParamsChanged(this);
3118
4475
  }
3119
4476
  /**
3120
- * Gets the enabled state of the component.
3121
- * @returns The enabled state of the component.
4477
+ * Gets the playback speed of the clip.
4478
+ * @returns The playback speed.
3122
4479
  */
3123
- get enabled() {
3124
- return this._enabled;
4480
+ get speed() {
4481
+ return this._speed;
3125
4482
  }
3126
4483
  static get observedAttributes() {
3127
- return ['enabled'];
4484
+ return ['asset', 'loop', 'name', 'speed'];
3128
4485
  }
3129
4486
  attributeChangedCallback(name, _oldValue, newValue) {
3130
4487
  switch (name) {
3131
- case 'enabled':
3132
- this.enabled = parseBool(newValue, true);
4488
+ case 'asset':
4489
+ this.asset = newValue ?? '';
4490
+ break;
4491
+ case 'loop':
4492
+ this.loop = parseBool(newValue, true);
4493
+ break;
4494
+ case 'name':
4495
+ this.name = newValue ?? '';
4496
+ break;
4497
+ case 'speed':
4498
+ this.speed = parseNumber(newValue, 1, name);
3133
4499
  break;
3134
4500
  }
3135
4501
  }
3136
4502
  }
4503
+ customElements.define('pc-anim-clip', AnimClipElement);
3137
4504
 
3138
4505
  /**
3139
- * The ListenerComponentElement interface provides properties and methods for manipulating
3140
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-listener/ | `<pc-listener>`} elements.
3141
- * The ListenerComponentElement interface also inherits the properties and methods of the
4506
+ * The AudioListenerComponentElement interface provides properties and methods for manipulating
4507
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-audio-listener/ | `<pc-audio-listener>`} elements.
4508
+ * The AudioListenerComponentElement interface also inherits the properties and methods of the
3142
4509
  * {@link HTMLElement} interface.
3143
4510
  *
4511
+ * Engine component: {@link AudioListenerComponent} (`audiolistener`).
4512
+ *
3144
4513
  * @category Components
3145
4514
  */
3146
- class ListenerComponentElement extends ComponentElement {
4515
+ class AudioListenerComponentElement extends ComponentElement {
3147
4516
  /** @ignore */
3148
4517
  constructor() {
3149
4518
  super('audiolistener');
@@ -3156,7 +4525,7 @@ class ListenerComponentElement extends ComponentElement {
3156
4525
  return super.component;
3157
4526
  }
3158
4527
  }
3159
- customElements.define('pc-listener', ListenerComponentElement);
4528
+ customElements.define('pc-audio-listener', AudioListenerComponentElement);
3160
4529
 
3161
4530
  const transitionModes = new Map([
3162
4531
  ['tint', playcanvas.BUTTON_TRANSITION_MODE_TINT],
@@ -3168,6 +4537,8 @@ const transitionModes = new Map([
3168
4537
  * The ButtonComponentElement interface also inherits the properties and methods of the
3169
4538
  * {@link HTMLElement} interface.
3170
4539
  *
4540
+ * Engine component: {@link ButtonComponent} (`button`).
4541
+ *
3171
4542
  * @category Components
3172
4543
  */
3173
4544
  class ButtonComponentElement extends ComponentElement {
@@ -3248,7 +4619,9 @@ class ButtonComponentElement extends ComponentElement {
3248
4619
  }
3249
4620
  /**
3250
4621
  * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` whose image
3251
- * element is used for visual transitions. Defaults to the button's own entity.
4622
+ * element is used for visual transitions. Defaults to the button's own entity — inside a
4623
+ * `<pc-model>`, that is the model's host entity, so supply an explicit reference to target a
4624
+ * UI entity instead.
3252
4625
  * @param value - The image entity reference.
3253
4626
  */
3254
4627
  set image(value) {
@@ -3567,6 +4940,8 @@ const tonemaps = new Map([
3567
4940
  * The CameraComponentElement interface also inherits the properties and methods of the
3568
4941
  * {@link HTMLElement} interface.
3569
4942
  *
4943
+ * Engine component: {@link CameraComponent} (`camera`).
4944
+ *
3570
4945
  * @category Components
3571
4946
  */
3572
4947
  class CameraComponentElement extends ComponentElement {
@@ -4052,6 +5427,8 @@ customElements.define('pc-camera', CameraComponentElement);
4052
5427
  * retargets or rebinds picks up the new node's geometry. An entity with no asset-backed render
4053
5428
  * component warns, and the collider has no shape.
4054
5429
  *
5430
+ * Engine component: {@link CollisionComponent} (`collision`).
5431
+ *
4055
5432
  * @category Components
4056
5433
  */
4057
5434
  class CollisionComponentElement extends ComponentElement {
@@ -4088,7 +5465,9 @@ class CollisionComponentElement extends ComponentElement {
4088
5465
  * no attribute to supply it - so the host's visible geometry, the meaning a mesh collider
4089
5466
  * on a glTF node carries, fills the gap. Runs on every application (so a rebound `pc-node`
4090
5467
  * recomputes it) and on a runtime switch to `type="mesh"`; an explicitly assigned
4091
- * `renderAsset` is never overwritten.
5468
+ * `renderAsset` is never overwritten. A `pc-model` host entity never carries a render
5469
+ * component (the instantiated content beneath it does), so a mesh collider that should take
5470
+ * an asset's geometry belongs on a bound `pc-node`.
4092
5471
  */
4093
5472
  _applyMeshGeometryDefault() {
4094
5473
  const component = this.component;
@@ -4233,6 +5612,13 @@ customElements.define('pc-collision', CollisionComponentElement);
4233
5612
  * The ElementComponentElement interface also inherits the properties and methods of the
4234
5613
  * {@link HTMLElement} interface.
4235
5614
  *
5615
+ * Despite the name, this is not a base class or a generic wrapper: it is the engine's 2D UI
5616
+ * component, which gives its host entity a rectangle in a `<pc-screen>` hierarchy that draws
5617
+ * either an image, a line of text or nothing (`type="image"`, `"text"` or `"group"`). The tag
5618
+ * spells the engine component it adds, as every component element does.
5619
+ *
5620
+ * Engine component: {@link ElementComponent} (`element`).
5621
+ *
4236
5622
  * @category Components
4237
5623
  */
4238
5624
  class ElementComponentElement extends ComponentElement {
@@ -4921,6 +6307,8 @@ customElements.define('pc-element', ElementComponentElement);
4921
6307
  * constraint breaks. A broken joint no longer constrains its bodies; calling `refreshFrames()` on
4922
6308
  * the underlying component re-attaches it. Bubbles and is composed.
4923
6309
  *
6310
+ * Engine component: {@link JointComponent} (`joint`).
6311
+ *
4924
6312
  * @category Components
4925
6313
  */
4926
6314
  class JointComponentElement extends ComponentElement {
@@ -5779,10 +7167,12 @@ customElements.define('pc-joint', JointComponentElement);
5779
7167
 
5780
7168
  /**
5781
7169
  * The LayoutChildComponentElement interface provides properties and methods for manipulating
5782
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layoutchild/ | `<pc-layoutchild>`} elements.
7170
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layout-child/ | `<pc-layout-child>`} elements.
5783
7171
  * The LayoutChildComponentElement interface also inherits the properties and methods of the
5784
7172
  * {@link HTMLElement} interface.
5785
7173
  *
7174
+ * Engine component: {@link LayoutChildComponent} (`layoutchild`).
7175
+ *
5786
7176
  * @category Components
5787
7177
  */
5788
7178
  class LayoutChildComponentElement extends ComponentElement {
@@ -5975,7 +7365,7 @@ class LayoutChildComponentElement extends ComponentElement {
5975
7365
  }
5976
7366
  }
5977
7367
  }
5978
- customElements.define('pc-layoutchild', LayoutChildComponentElement);
7368
+ customElements.define('pc-layout-child', LayoutChildComponentElement);
5979
7369
 
5980
7370
  const orientations$1 = new Map([
5981
7371
  ['horizontal', playcanvas.ORIENTATION_HORIZONTAL],
@@ -5989,10 +7379,12 @@ const fittings = new Map([
5989
7379
  ]);
5990
7380
  /**
5991
7381
  * The LayoutGroupComponentElement interface provides properties and methods for manipulating
5992
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layoutgroup/ | `<pc-layoutgroup>`} elements.
7382
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layout-group/ | `<pc-layout-group>`} elements.
5993
7383
  * The LayoutGroupComponentElement interface also inherits the properties and methods of the
5994
7384
  * {@link HTMLElement} interface.
5995
7385
  *
7386
+ * Engine component: {@link LayoutGroupComponent} (`layoutgroup`).
7387
+ *
5996
7388
  * @category Components
5997
7389
  */
5998
7390
  class LayoutGroupComponentElement extends ComponentElement {
@@ -6232,7 +7624,7 @@ class LayoutGroupComponentElement extends ComponentElement {
6232
7624
  }
6233
7625
  }
6234
7626
  }
6235
- customElements.define('pc-layoutgroup', LayoutGroupComponentElement);
7627
+ customElements.define('pc-layout-group', LayoutGroupComponentElement);
6236
7628
 
6237
7629
  const shadowTypes = new Map([
6238
7630
  ['pcf1-16f', playcanvas.SHADOW_PCF1_16F],
@@ -6251,6 +7643,8 @@ const shadowTypes = new Map([
6251
7643
  * The LightComponentElement interface also inherits the properties and methods of the
6252
7644
  * {@link HTMLElement} interface.
6253
7645
  *
7646
+ * Engine component: {@link LightComponent} (`light`).
7647
+ *
6254
7648
  * @category Components
6255
7649
  */
6256
7650
  class LightComponentElement extends ComponentElement {
@@ -6732,10 +8126,12 @@ customElements.define('pc-light', LightComponentElement);
6732
8126
 
6733
8127
  /**
6734
8128
  * The ParticleSystemComponentElement interface provides properties and methods for manipulating
6735
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-particles/ | `<pc-particles>`} elements.
8129
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-particle-system/ | `<pc-particle-system>`} elements.
6736
8130
  * The ParticleSystemComponentElement interface also inherits the properties and methods of the
6737
8131
  * {@link HTMLElement} interface.
6738
8132
  *
8133
+ * Engine component: {@link ParticleSystemComponent} (`particlesystem`).
8134
+ *
6739
8135
  * @category Components
6740
8136
  */
6741
8137
  class ParticleSystemComponentElement extends ComponentElement {
@@ -6854,7 +8250,7 @@ class ParticleSystemComponentElement extends ComponentElement {
6854
8250
  }
6855
8251
  }
6856
8252
  }
6857
- customElements.define('pc-particles', ParticleSystemComponentElement);
8253
+ customElements.define('pc-particle-system', ParticleSystemComponentElement);
6858
8254
 
6859
8255
  const blendTypes = new Map([
6860
8256
  ['none', playcanvas.BLEND_NONE],
@@ -9086,6 +10482,8 @@ customElements.define('pc-material', MaterialElement);
9086
10482
  * to `box`). It does not cover the engine's `asset` render type, since there is no way to supply
9087
10483
  * a render asset here — use `pc-model` for glTF content instead.
9088
10484
  *
10485
+ * Engine component: {@link RenderComponent} (`render`).
10486
+ *
9089
10487
  * @category Components
9090
10488
  */
9091
10489
  class RenderComponentElement extends ComponentElement {
@@ -9210,10 +10608,12 @@ customElements.define('pc-render', RenderComponentElement);
9210
10608
 
9211
10609
  /**
9212
10610
  * The RigidBodyComponentElement interface provides properties and methods for manipulating
9213
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-rigidbody/ | `<pc-rigidbody>`} elements.
10611
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-rigid-body/ | `<pc-rigid-body>`} elements.
9214
10612
  * The RigidBodyComponentElement interface also inherits the properties and methods of the
9215
10613
  * {@link HTMLElement} interface.
9216
10614
  *
10615
+ * Engine component: {@link RigidBodyComponent} (`rigidbody`).
10616
+ *
9217
10617
  * @category Components
9218
10618
  */
9219
10619
  class RigidBodyComponentElement extends ComponentElement {
@@ -9405,7 +10805,7 @@ class RigidBodyComponentElement extends ComponentElement {
9405
10805
  }
9406
10806
  }
9407
10807
  }
9408
- customElements.define('pc-rigidbody', RigidBodyComponentElement);
10808
+ customElements.define('pc-rigid-body', RigidBodyComponentElement);
9409
10809
 
9410
10810
  // The engine's SCALEMODE_* constants are the strings 'none' and 'blend', so this map happens to be
9411
10811
  // an identity. It is still the right shape: it supplies parseEnum's valid-name list, it is what the
@@ -9421,6 +10821,8 @@ const scaleModes = new Map([
9421
10821
  * The ScreenComponentElement interface also inherits the properties and methods of the
9422
10822
  * {@link HTMLElement} interface.
9423
10823
  *
10824
+ * Engine component: {@link ScreenComponent} (`screen`).
10825
+ *
9424
10826
  * @category Components
9425
10827
  */
9426
10828
  class ScreenComponentElement extends ComponentElement {
@@ -9573,6 +10975,8 @@ const orientations = new Map([
9573
10975
  * The ScrollbarComponentElement interface also inherits the properties and methods of the
9574
10976
  * {@link HTMLElement} interface.
9575
10977
  *
10978
+ * Engine component: {@link ScrollbarComponent} (`scrollbar`).
10979
+ *
9576
10980
  * @category Components
9577
10981
  */
9578
10982
  class ScrollbarComponentElement extends ComponentElement {
@@ -9708,10 +11112,12 @@ const visibilities = new Map([
9708
11112
  ]);
9709
11113
  /**
9710
11114
  * The ScrollViewComponentElement interface provides properties and methods for manipulating
9711
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scrollview/ | `<pc-scrollview>`} elements.
11115
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scroll-view/ | `<pc-scroll-view>`} elements.
9712
11116
  * The ScrollViewComponentElement interface also inherits the properties and methods of the
9713
11117
  * {@link HTMLElement} interface.
9714
11118
  *
11119
+ * Engine component: {@link ScrollViewComponent} (`scrollview`).
11120
+ *
9715
11121
  * @category Components
9716
11122
  */
9717
11123
  class ScrollViewComponentElement extends ComponentElement {
@@ -10070,11 +11476,11 @@ class ScrollViewComponentElement extends ComponentElement {
10070
11476
  }
10071
11477
  }
10072
11478
  }
10073
- customElements.define('pc-scrollview', ScrollViewComponentElement);
11479
+ customElements.define('pc-scroll-view', ScrollViewComponentElement);
10074
11480
 
10075
11481
  /**
10076
- * The ScriptElement interface provides properties and methods for manipulating
10077
- * `<pc-script>` elements. The ScriptElement interface also inherits the properties and
11482
+ * The ScriptInstanceElement interface provides properties and methods for manipulating
11483
+ * `<pc-script-instance>` elements. The ScriptInstanceElement interface also inherits the properties and
10078
11484
  * methods of the {@link AsyncElement} interface.
10079
11485
  *
10080
11486
  * Script attributes can be supplied through two channels:
@@ -10096,7 +11502,7 @@ customElements.define('pc-scrollview', ScrollViewComponentElement);
10096
11502
  * new-name one, re-applying both attribute channels to it.
10097
11503
  *
10098
11504
  * The element becomes ready once its script instance has been created by the parent
10099
- * `<pc-scripts>` element.
11505
+ * `<pc-script>` element.
10100
11506
  *
10101
11507
  * @fires {CustomEvent} scriptattributeschange - Fired when the script's attributes change. The
10102
11508
  * `detail` carries the new `attributes` object. Bubbles.
@@ -10105,11 +11511,11 @@ customElements.define('pc-scrollview', ScrollViewComponentElement);
10105
11511
  * @fires {CustomEvent} scriptnamechange - Fired when the script is renamed on a live element. The
10106
11512
  * `detail` carries `oldName` and `newName`. Bubbles.
10107
11513
  */
10108
- class ScriptElement extends AsyncElement {
11514
+ class ScriptInstanceElement extends AsyncElement {
10109
11515
  _attributes = {};
10110
11516
  _enabled = true;
10111
11517
  /**
10112
- * The Script instance created for this element by its parent `<pc-scripts>` element.
11518
+ * The Script instance created for this element by its parent `<pc-script>` element.
10113
11519
  * @internal
10114
11520
  */
10115
11521
  _script = null;
@@ -10155,11 +11561,11 @@ class ScriptElement extends AsyncElement {
10155
11561
  }
10156
11562
  /**
10157
11563
  * Sets the name of the script to create. The `name` attribute is the single source of truth
10158
- * (it is what the parent `<pc-scripts>` element reads when creating the instance), so the
11564
+ * (it is what the parent `<pc-script>` element reads when creating the instance), so the
10159
11565
  * property writes through to it — assigning before insertion works as expected:
10160
11566
  *
10161
11567
  * ```js
10162
- * const script = document.createElement('pc-script');
11568
+ * const script = document.createElement('pc-script-instance');
10163
11569
  * script.name = 'rotate';
10164
11570
  * scriptsElement.appendChild(script);
10165
11571
  * await script.ready();
@@ -10186,10 +11592,10 @@ class ScriptElement extends AsyncElement {
10186
11592
  return this._script;
10187
11593
  }
10188
11594
  connectedCallback() {
10189
- // Script instances are created by the parent pc-scripts element, so an element placed
11595
+ // Script instances are created by the parent pc-script element, so an element placed
10190
11596
  // anywhere else is inert and never becomes ready - warn rather than hang silently
10191
- if (this.parentElement?.tagName !== 'PC-SCRIPTS') {
10192
- console.warn(`pc-script '${this.getAttribute('name')}' must be a direct child of pc-scripts - script not created`);
11597
+ if (this.parentElement?.tagName !== 'PC-SCRIPT') {
11598
+ console.warn(`pc-script-instance '${this.getAttribute('name')}' must be a direct child of pc-script - script not created`);
10193
11599
  }
10194
11600
  }
10195
11601
  disconnectedCallback() {
@@ -10200,7 +11606,7 @@ class ScriptElement extends AsyncElement {
10200
11606
  this._resetReady();
10201
11607
  }
10202
11608
  /**
10203
- * Called by the parent `<pc-scripts>` element when the script instance has been created.
11609
+ * Called by the parent `<pc-script>` element when the script instance has been created.
10204
11610
  * Creation can happen more than once per connection (a runtime `name` change recreates the
10205
11611
  * instance), but `_onReady` signals readiness at most once per cycle.
10206
11612
  * @internal
@@ -10222,7 +11628,7 @@ class ScriptElement extends AsyncElement {
10222
11628
  this.scriptAttributes = JSON.parse(newValue);
10223
11629
  }
10224
11630
  catch (error) {
10225
- console.warn(`Invalid 'attributes' JSON on pc-script '${this.getAttribute('name')}': ${error.message}`);
11631
+ console.warn(`Invalid 'attributes' JSON on pc-script-instance '${this.getAttribute('name')}': ${error.message}`);
10226
11632
  }
10227
11633
  break;
10228
11634
  case 'enabled':
@@ -10242,14 +11648,14 @@ class ScriptElement extends AsyncElement {
10242
11648
  }
10243
11649
  }
10244
11650
  }
10245
- customElements.define('pc-script', ScriptElement);
11651
+ customElements.define('pc-script-instance', ScriptInstanceElement);
10246
11652
 
10247
11653
  /**
10248
- * Attributes on `pc-script` that never map to script attributes: the element's own API (derived
11654
+ * Attributes on `pc-script-instance` that never map to script attributes: the element's own API (derived
10249
11655
  * from its observed attributes) plus reserved and global HTML attribute names.
10250
11656
  */
10251
11657
  const RESERVED_ATTRIBUTES = new Set([
10252
- ...ScriptElement.observedAttributes,
11658
+ ...ScriptInstanceElement.observedAttributes,
10253
11659
  'accesskey',
10254
11660
  'autocapitalize',
10255
11661
  'autofocus',
@@ -10280,7 +11686,7 @@ const RESERVED_ATTRIBUTES = new Set([
10280
11686
  'translate'
10281
11687
  ]);
10282
11688
  /**
10283
- * Checks whether a `pc-script` attribute name is reserved (and so never maps to a script
11689
+ * Checks whether a `pc-script-instance` attribute name is reserved (and so never maps to a script
10284
11690
  * attribute). Reserved names are the element's own API, global HTML attribute names, `data-*`
10285
11691
  * and `aria-*` attributes, names starting with `_` (framework-stamped attributes), and real
10286
11692
  * inline event handler names (`onclick` etc. — detected via the platform, so script attributes
@@ -10436,10 +11842,12 @@ const findCaseMatch = (script, key) => {
10436
11842
  };
10437
11843
  /**
10438
11844
  * The ScriptComponentElement interface provides properties and methods for manipulating
10439
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scripts/ | `<pc-scripts>`} elements.
11845
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-script/ | `<pc-script>`} elements.
10440
11846
  * The ScriptComponentElement interface also inherits the properties and methods of the
10441
11847
  * {@link HTMLElement} interface.
10442
11848
  *
11849
+ * Engine component: {@link ScriptComponent} (`script`).
11850
+ *
10443
11851
  * @category Components
10444
11852
  */
10445
11853
  class ScriptComponentElement extends ComponentElement {
@@ -10456,14 +11864,23 @@ class ScriptComponentElement extends ComponentElement {
10456
11864
  }
10457
11865
  connectedCallback() {
10458
11866
  // (Re-)observe on every connection - disconnectedCallback disconnects the observer.
10459
- // Attribute changes on child pc-script elements are watched here too: per-property
11867
+ // Attribute changes on child pc-script-instance elements are watched here too: per-property
10460
11868
  // script attributes are not statically known, so they cannot use observedAttributes.
10461
11869
  this.observer.observe(this, { childList: true, subtree: true, attributes: true });
10462
11870
  return super.connectedCallback();
10463
11871
  }
10464
11872
  initComponent() {
10465
- // Handle initial script elements
10466
- this.querySelectorAll(':scope > pc-script').forEach((scriptElement) => {
11873
+ this.querySelectorAll(':scope > pc-script-instance').forEach((scriptElement) => {
11874
+ // A host readiness cycle re-runs this against a component that can have survived it
11875
+ // (a pc-model reloading content on its stable host entity). The engine rejects a
11876
+ // duplicate create - returning null, silently in production builds - which would
11877
+ // skip attribute application entirely. A surviving instance is re-asserted instead,
11878
+ // so both cycle outcomes leave the component reflecting the element's declared state.
11879
+ const script = this.scriptFor(scriptElement);
11880
+ if (script) {
11881
+ this.applyDeclaredState(script, scriptElement);
11882
+ return;
11883
+ }
10467
11884
  this.createScript(scriptElement);
10468
11885
  });
10469
11886
  }
@@ -10597,9 +12014,9 @@ class ScriptComponentElement extends ComponentElement {
10597
12014
  this.mergeDeep(script, converted);
10598
12015
  }
10599
12016
  /**
10600
- * Returns the camelCase keys of the per-property attributes present on a `pc-script`
12017
+ * Returns the camelCase keys of the per-property attributes present on a `pc-script-instance`
10601
12018
  * element.
10602
- * @param scriptElement - The `pc-script` element.
12019
+ * @param scriptElement - The `pc-script-instance` element.
10603
12020
  * @returns The camelCase keys.
10604
12021
  */
10605
12022
  inlineKeys(scriptElement) {
@@ -10612,10 +12029,10 @@ class ScriptComponentElement extends ComponentElement {
10612
12029
  return keys;
10613
12030
  }
10614
12031
  /**
10615
- * Resolves the script instance owned by a `pc-script` element. Returns `null` when the
12032
+ * Resolves the script instance owned by a `pc-script-instance` element. Returns `null` when the
10616
12033
  * element has no created script, or when its name resolves to a script created by a
10617
12034
  * different element (e.g. a duplicate-named sibling).
10618
- * @param scriptElement - The `pc-script` element.
12035
+ * @param scriptElement - The `pc-script-instance` element.
10619
12036
  * @returns The owned script, or `null`.
10620
12037
  */
10621
12038
  scriptFor(scriptElement) {
@@ -10644,7 +12061,7 @@ class ScriptComponentElement extends ComponentElement {
10644
12061
  }
10645
12062
  }
10646
12063
  /**
10647
- * Handles a runtime `name` change on a child `pc-script`, swapping the engine script instance
12064
+ * Handles a runtime `name` change on a child `pc-script-instance`, swapping the engine script instance
10648
12065
  * to match. Without this the element would keep pointing at the old-name instance: the old
10649
12066
  * script would go on running while every subsequent update (attribute changes, enable
10650
12067
  * changes, destruction on removal) resolved the new name and silently no-opped.
@@ -10655,8 +12072,8 @@ class ScriptComponentElement extends ComponentElement {
10655
12072
  */
10656
12073
  handleScriptNameChange(event) {
10657
12074
  const scriptElement = event.target;
10658
- // Only direct children are managed, matching initComponent's ':scope > pc-script'
10659
- // contract - the event bubbles, so a deeper pc-script must not be created here
12075
+ // Only direct children are managed, matching initComponent's ':scope > pc-script-instance'
12076
+ // contract - the event bubbles, so a deeper pc-script-instance must not be created here
10660
12077
  if (scriptElement.parentElement !== this)
10661
12078
  return;
10662
12079
  // Before the component exists there is nothing to swap: initComponent creates from
@@ -10673,11 +12090,11 @@ class ScriptComponentElement extends ComponentElement {
10673
12090
  this.createScript(scriptElement);
10674
12091
  }
10675
12092
  /**
10676
- * Creates the script instance for a `pc-script` element. The instance is created disabled,
12093
+ * Creates the script instance for a `pc-script-instance` element. The instance is created disabled,
10677
12094
  * the element's converted attributes are merged over the instance's defaults (which is what
10678
12095
  * allows plain numeric arrays to be typed against those defaults), and only then is the
10679
12096
  * declared enabled state applied — so `initialize()` runs with every attribute in place.
10680
- * @param scriptElement - The `pc-script` element to create the script instance for.
12097
+ * @param scriptElement - The `pc-script-instance` element to create the script instance for.
10681
12098
  * @returns The created script, or `null`.
10682
12099
  */
10683
12100
  createScript(scriptElement) {
@@ -10688,21 +12105,30 @@ class ScriptComponentElement extends ComponentElement {
10688
12105
  if (!script)
10689
12106
  return null;
10690
12107
  scriptElement._script = script;
10691
- // The JSON blob first with per-property-shadowed keys stripped, then the per-property
10692
- // attributes: each property is written exactly once and individual attributes win
12108
+ this.applyDeclaredState(script, scriptElement);
12109
+ scriptElement._onScriptCreated();
12110
+ return script;
12111
+ }
12112
+ /**
12113
+ * Applies a `pc-script-instance` element's declared state to a script instance: the `attributes` JSON
12114
+ * first with per-property-shadowed keys stripped, then the per-property attributes — each
12115
+ * property is written exactly once and individual attributes win — and finally the declared
12116
+ * enabled state, so `initialize()` runs with every attribute in place.
12117
+ * @param script - The script instance.
12118
+ * @param scriptElement - The `pc-script-instance` element holding the declared state.
12119
+ */
12120
+ applyDeclaredState(script, scriptElement) {
10693
12121
  this.applyAttributes(script, scriptElement.scriptAttributes, this.inlineKeys(scriptElement));
10694
12122
  this.applyInlineAttributes(script, scriptElement);
10695
12123
  script.enabled = scriptElement.enabled;
10696
- scriptElement._onScriptCreated();
10697
- return script;
10698
12124
  }
10699
12125
  /**
10700
- * Applies the per-property attributes present on a `pc-script` element — any attribute that
12126
+ * Applies the per-property attributes present on a `pc-script-instance` element — any attribute that
10701
12127
  * is not part of the element's own API or a reserved HTML attribute name. These are applied
10702
12128
  * after the `attributes` JSON, so an individual attribute always takes precedence over the
10703
12129
  * blob.
10704
12130
  * @param script - The script to apply the attributes to.
10705
- * @param scriptElement - The `pc-script` element holding the attributes.
12131
+ * @param scriptElement - The `pc-script-instance` element holding the attributes.
10706
12132
  */
10707
12133
  applyInlineAttributes(script, scriptElement) {
10708
12134
  const scriptName = scriptElement.getAttribute('name') ?? '';
@@ -10713,10 +12139,10 @@ class ScriptComponentElement extends ComponentElement {
10713
12139
  }
10714
12140
  }
10715
12141
  /**
10716
- * Applies a single per-property attribute change to the script of a `pc-script` element.
12142
+ * Applies a single per-property attribute change to the script of a `pc-script-instance` element.
10717
12143
  * When the attribute has been removed, the value from the `attributes` JSON (if any) takes
10718
12144
  * effect again.
10719
- * @param scriptElement - The `pc-script` element whose attribute changed.
12145
+ * @param scriptElement - The `pc-script-instance` element whose attribute changed.
10720
12146
  * @param attributeName - The name of the changed attribute.
10721
12147
  */
10722
12148
  applyScriptProperty(scriptElement, attributeName) {
@@ -10752,7 +12178,7 @@ class ScriptComponentElement extends ComponentElement {
10752
12178
  try {
10753
12179
  const current = script[key];
10754
12180
  if (typeof current === 'function' || SCRIPT_API_MEMBERS.has(key)) {
10755
- console.warn(`Ignoring attribute '${attributeName}' on pc-script '${scriptName}' - '${key}' is part of the Script API.`);
12181
+ console.warn(`Ignoring attribute '${attributeName}' on pc-script-instance '${scriptName}' - '${key}' is part of the Script API.`);
10756
12182
  return;
10757
12183
  }
10758
12184
  if (typeof current === 'string') {
@@ -10808,10 +12234,10 @@ class ScriptComponentElement extends ComponentElement {
10808
12234
  }
10809
12235
  handleMutations(mutations) {
10810
12236
  for (const mutation of mutations) {
10811
- // Handle per-property attribute changes on child pc-script elements
12237
+ // Handle per-property attribute changes on child pc-script-instance elements
10812
12238
  if (mutation.type === 'attributes') {
10813
12239
  const target = mutation.target;
10814
- if (target instanceof ScriptElement &&
12240
+ if (target instanceof ScriptInstanceElement &&
10815
12241
  target.parentElement === this &&
10816
12242
  mutation.attributeName &&
10817
12243
  !isReservedAttribute(mutation.attributeName)) {
@@ -10821,16 +12247,16 @@ class ScriptComponentElement extends ComponentElement {
10821
12247
  }
10822
12248
  // Only direct children are managed - the observer watches the subtree for attribute
10823
12249
  // changes, but deeper childList records must not create or destroy scripts
10824
- // (matching initComponent's ':scope > pc-script' contract)
12250
+ // (matching initComponent's ':scope > pc-script-instance' contract)
10825
12251
  if (mutation.target !== this) {
10826
12252
  continue;
10827
12253
  }
10828
- // Handle removed nodes first, so that replacing a pc-script with a same-named one
12254
+ // Handle removed nodes first, so that replacing a pc-script-instance with a same-named one
10829
12255
  // destroys the old script before the replacement is created. Only destroy a script
10830
12256
  // this element actually owns - a duplicate-named element whose own create() failed
10831
12257
  // must not take down the live script on removal.
10832
12258
  mutation.removedNodes.forEach((node) => {
10833
- if (node instanceof ScriptElement) {
12259
+ if (node instanceof ScriptInstanceElement) {
10834
12260
  const scriptName = node.getAttribute('name');
10835
12261
  if (scriptName &&
10836
12262
  node._script &&
@@ -10843,7 +12269,7 @@ class ScriptComponentElement extends ComponentElement {
10843
12269
  });
10844
12270
  // Handle added nodes
10845
12271
  mutation.addedNodes.forEach((node) => {
10846
- if (node instanceof ScriptElement) {
12272
+ if (node instanceof ScriptInstanceElement) {
10847
12273
  this.createScript(node);
10848
12274
  }
10849
12275
  });
@@ -10861,14 +12287,16 @@ class ScriptComponentElement extends ComponentElement {
10861
12287
  return super.component;
10862
12288
  }
10863
12289
  }
10864
- customElements.define('pc-scripts', ScriptComponentElement);
12290
+ customElements.define('pc-script', ScriptComponentElement);
10865
12291
 
10866
12292
  /**
10867
12293
  * The SoundComponentElement interface provides properties and methods for manipulating
10868
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-sounds/ | `<pc-sounds>`} elements.
12294
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-sound/ | `<pc-sound>`} elements.
10869
12295
  * The SoundComponentElement interface also inherits the properties and methods of the
10870
12296
  * {@link HTMLElement} interface.
10871
12297
  *
12298
+ * Engine component: {@link SoundComponent} (`sound`).
12299
+ *
10872
12300
  * @category Components
10873
12301
  */
10874
12302
  class SoundComponentElement extends ComponentElement {
@@ -11059,11 +12487,11 @@ class SoundComponentElement extends ComponentElement {
11059
12487
  }
11060
12488
  }
11061
12489
  }
11062
- customElements.define('pc-sounds', SoundComponentElement);
12490
+ customElements.define('pc-sound', SoundComponentElement);
11063
12491
 
11064
12492
  /**
11065
12493
  * The SoundSlotElement interface provides properties and methods for manipulating
11066
- * `<pc-sound>` elements. The SoundSlotElement interface also inherits the properties and
12494
+ * `<pc-sound-slot>` elements. The SoundSlotElement interface also inherits the properties and
11067
12495
  * methods of the {@link AsyncElement} interface.
11068
12496
  */
11069
12497
  class SoundSlotElement extends AsyncElement {
@@ -11077,7 +12505,7 @@ class SoundSlotElement extends AsyncElement {
11077
12505
  _startTime = 0;
11078
12506
  _volume = 1;
11079
12507
  /**
11080
- * The `<pc-sounds>` this slot was added to, captured at connect time.
12508
+ * The `<pc-sound>` this slot was added to, captured at connect time.
11081
12509
  *
11082
12510
  * `disconnectedCallback` cannot rediscover it: by the time the element is disconnected its
11083
12511
  * `parentElement` is already `null`, so a lookup would both fail to find the component and
@@ -11140,7 +12568,7 @@ class SoundSlotElement extends AsyncElement {
11140
12568
  get soundElement() {
11141
12569
  const soundElement = this.parentElement;
11142
12570
  if (!(soundElement instanceof SoundComponentElement)) {
11143
- console.warn('pc-sound must be a direct child of a pc-sounds element');
12571
+ console.warn('pc-sound-slot must be a direct child of a pc-sound element');
11144
12572
  return null;
11145
12573
  }
11146
12574
  return soundElement;
@@ -11336,7 +12764,7 @@ class SoundSlotElement extends AsyncElement {
11336
12764
  }
11337
12765
  }
11338
12766
  }
11339
- customElements.define('pc-sound', SoundSlotElement);
12767
+ customElements.define('pc-sound-slot', SoundSlotElement);
11340
12768
 
11341
12769
  /**
11342
12770
  * The GSplatComponentElement interface provides properties and methods for manipulating
@@ -11344,6 +12772,8 @@ customElements.define('pc-sound', SoundSlotElement);
11344
12772
  * The GSplatComponentElement interface also inherits the properties and methods of the
11345
12773
  * {@link HTMLElement} interface.
11346
12774
  *
12775
+ * Engine component: {@link GSplatComponent} (`gsplat`).
12776
+ *
11347
12777
  * @category Components
11348
12778
  */
11349
12779
  class GSplatComponentElement extends ComponentElement {
@@ -11526,296 +12956,6 @@ class GSplatComponentElement extends ComponentElement {
11526
12956
  }
11527
12957
  customElements.define('pc-gsplat', GSplatComponentElement);
11528
12958
 
11529
- /**
11530
- * Formats one line of the printable hierarchy: the node's name, an `[index]` marker when the
11531
- * name is shared by several nodes in the model, the attached component types, and the material
11532
- * names of a render component.
11533
- *
11534
- * @param node - The node to format.
11535
- * @param counts - The number of nodes bearing each name.
11536
- * @returns The formatted line.
11537
- */
11538
- const formatNode = (node, counts) => {
11539
- const index = (counts.get(node.name) ?? 0) > 1 ? ` [${node.index}]` : '';
11540
- const components = node.components.length > 0 ? ` (${node.components.join(', ')})` : '';
11541
- // Braces rather than brackets: `[N]` already means a match index on this line
11542
- const materials = node.materials.length > 0 ? ` {${node.materials.map((slot) => slot.name ?? 'null').join(', ')}}` : '';
11543
- return `${node.name}${index}${components}${materials}`;
11544
- };
11545
- /**
11546
- * Formats the printable form of a hierarchy subtree.
11547
- *
11548
- * @param root - The subtree root.
11549
- * @param counts - The number of nodes bearing each name.
11550
- * @returns The tree, one line per node.
11551
- */
11552
- const formatHierarchy = (root, counts) => {
11553
- const lines = [formatNode(root, counts)];
11554
- const walk = (node, prefix) => {
11555
- node.children.forEach((child, i) => {
11556
- const last = i === node.children.length - 1;
11557
- lines.push(`${prefix}${last ? '└─ ' : '├─ '}${formatNode(child, counts)}`);
11558
- walk(child, `${prefix}${last ? ' ' : '│ '}`);
11559
- });
11560
- };
11561
- walk(root, '');
11562
- return lines.join('\n');
11563
- };
11564
- /**
11565
- * The ModelElement interface provides properties and methods for manipulating
11566
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | `<pc-model>`} elements.
11567
- * The ModelElement interface also inherits the properties and methods of the
11568
- * {@link HTMLElement} interface.
11569
- *
11570
- * The element becomes ready once its container asset has loaded and the instantiated hierarchy has
11571
- * been added to the scene — `entity` is non-null by then. A failed load also settles readiness,
11572
- * with `entity` remaining `null`: readiness means the load settled, not that it succeeded — listen
11573
- * for `error`, or check `entity`, to tell the outcomes apart. Changing `asset` re-arms readiness
11574
- * and instantiates anew, so a `ready()` obtained after the change resolves against the new
11575
- * hierarchy. A `pc-model` outside a `pc-app`, or referencing an unknown asset id, warns and never
11576
- * becomes ready.
11577
- *
11578
- * @fires {Event} load - Fired each time a container asset finishes instantiating, including
11579
- * re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
11580
- * capture-phase listener on an ancestor.
11581
- * @fires {ErrorEvent} error - Fired when the container asset fails to load, with the engine's
11582
- * error in `message`. Does not bubble. The element still becomes ready — readiness means the load
11583
- * settled, not that it succeeded.
11584
- */
11585
- class ModelElement extends AsyncElement {
11586
- _asset = '';
11587
- _entity = null;
11588
- /**
11589
- * Incremented on every new load and on disconnect, and captured by a load when it starts. A
11590
- * load that resumes from an await or a load callback abandons itself if the value has moved
11591
- * on, so a superseded load can neither instantiate a second entity nor parent one that has
11592
- * since been destroyed.
11593
- */
11594
- _loadGeneration = 0;
11595
- /**
11596
- * The pending asset subscriptions of the current load, if it is waiting for its asset. Held
11597
- * so that whatever supersedes the load can detach the handlers from the asset, rather than
11598
- * leave them registered until the asset settles (or forever, if it never does).
11599
- */
11600
- _loadHandle = null;
11601
- _errorHandle = null;
11602
- /**
11603
- * The root entity of the instantiated model. `null` until the container asset has loaded
11604
- * and been instantiated, and again once the element has been removed from the document.
11605
- * @returns The model's root entity, or `null`.
11606
- */
11607
- get entity() {
11608
- return this._entity;
11609
- }
11610
- /**
11611
- * Returns a snapshot of the instantiated node tree, or `null` while there is none (the
11612
- * container asset has not loaded, or the element has left the document). One call grounds a
11613
- * session — a browser console, a test, an agent — in the vocabulary `pc-node` binding
11614
- * resolves against: the instantiated names ({@link HierarchyNode.name}), paths, match
11615
- * indices, attached component types and the material assignments of render components
11616
- * ({@link HierarchyNode.materials}). `String(...)` of the result, or of any node in it,
11617
- * is the printable form.
11618
- *
11619
- * The snapshot is plain data, computed afresh each call: it does not follow later changes
11620
- * to the hierarchy, and mutating it changes nothing.
11621
- *
11622
- * @returns The root of the instantiated node tree, or `null`.
11623
- */
11624
- hierarchy() {
11625
- const root = this._entity;
11626
- if (!root) {
11627
- return null;
11628
- }
11629
- // Ordinals are assigned in the traversal resolution searches — pre-order depth-first
11630
- // from the model root, the root itself included — so each node's index is exactly what
11631
- // a pc-node's index attribute selects. Once the walk completes, the map holds the total
11632
- // count per name, which is what the printable form reads to annotate only shared names.
11633
- const ordinals = new Map();
11634
- const describe = (entity, pathBelowRoot) => {
11635
- const index = ordinals.get(entity.name) ?? 0;
11636
- ordinals.set(entity.name, index + 1);
11637
- const node = {
11638
- name: entity.name,
11639
- // The root has no path below itself; its own name stands in, as it does for
11640
- // the path a pc-node bound to the root reports.
11641
- path: pathBelowRoot || entity.name,
11642
- index,
11643
- // A plain GraphNode grafted into the hierarchy has no component storage
11644
- components: Object.keys(entity.c ?? {}).sort(),
11645
- materials: (entity.render?.meshInstances ?? []).map((meshInstance, slot) => ({
11646
- index: slot,
11647
- name: meshInstance.material?.name ?? null
11648
- })),
11649
- children: entity.children.map((child) => describe(child, pathBelowRoot ? `${pathBelowRoot}/${child.name}` : child.name))
11650
- };
11651
- // Non-enumerable, keeping the snapshot plain data under JSON.stringify, spreads and
11652
- // key enumeration. Deferred to call time, by which the ordinal map holds its totals.
11653
- Object.defineProperty(node, 'toString', {
11654
- enumerable: false,
11655
- value: () => formatHierarchy(node, ordinals)
11656
- });
11657
- return node;
11658
- };
11659
- return describe(root, '');
11660
- }
11661
- connectedCallback() {
11662
- // A model outside an application is inert and never becomes ready, so awaiting it hangs.
11663
- // Warn rather than fail silently, naming the parent it requires, as every other misplaced
11664
- // element does.
11665
- if (!this.closestApp) {
11666
- const label = this._asset ? ` '${this._asset}'` : '';
11667
- console.warn(`pc-model${label} must be a descendant of pc-app - model not created`);
11668
- return;
11669
- }
11670
- this._loadModel();
11671
- }
11672
- disconnectedCallback() {
11673
- this._loadGeneration++;
11674
- this._detachLoadHandlers();
11675
- this._unloadModel();
11676
- this._resetReady();
11677
- }
11678
- _detachLoadHandlers() {
11679
- this._loadHandle?.off();
11680
- this._loadHandle = null;
11681
- this._errorHandle?.off();
11682
- this._errorHandle = null;
11683
- }
11684
- /**
11685
- * Resolves readiness and dispatches the `load` event. Called once the instantiated hierarchy
11686
- * has been parented — readiness means "in the scene graph", matching `pc-entity`, so a ready
11687
- * model's entity always has world transforms.
11688
- */
11689
- _announceLoad() {
11690
- this._onReady();
11691
- this.dispatchEvent(new Event('load'));
11692
- }
11693
- _instantiate(container) {
11694
- const generation = this._loadGeneration;
11695
- const entity = container.instantiateRenderEntity();
11696
- this._entity = entity;
11697
- // @ts-ignore
11698
- if (container.animations.length > 0) {
11699
- entity.addComponent('anim');
11700
- // @ts-ignore
11701
- entity.anim.assignAnimation('animation', container.animations[0].resource);
11702
- }
11703
- // The parent's readiness re-arms when it is torn down, so these can resume in a later
11704
- // connection cycle. The entity is captured above and the generation re-checked, so a
11705
- // stale resume cannot parent an entity a newer cycle has already destroyed.
11706
- const parentEntityElement = this.closestEntity;
11707
- if (parentEntityElement) {
11708
- parentEntityElement.ready().then(() => {
11709
- if (generation !== this._loadGeneration) {
11710
- return;
11711
- }
11712
- parentEntityElement.entity.addChild(entity);
11713
- this._announceLoad();
11714
- });
11715
- }
11716
- else {
11717
- const appElement = this.closestApp;
11718
- if (appElement) {
11719
- appElement.ready().then(() => {
11720
- if (generation !== this._loadGeneration) {
11721
- return;
11722
- }
11723
- appElement.app.root.addChild(entity);
11724
- this._announceLoad();
11725
- });
11726
- }
11727
- }
11728
- }
11729
- async _loadModel() {
11730
- this._unloadModel();
11731
- // Supersede any load already in flight - only the newest load may instantiate
11732
- const generation = ++this._loadGeneration;
11733
- this._detachLoadHandlers();
11734
- // Re-arm readiness so a waiter obtained after an asset change resolves against the new
11735
- // hierarchy. A no-op on first connection, where readiness is still pending.
11736
- this._resetReady();
11737
- const appElement = this.closestApp;
11738
- if (!appElement) {
11739
- // Outside pc-app; connectedCallback already warned. Reached through the asset setter.
11740
- return;
11741
- }
11742
- await appElement.ready();
11743
- // The element may have been removed, or another load started, while we waited
11744
- if (generation !== this._loadGeneration) {
11745
- return;
11746
- }
11747
- const asset = useAsset(this._asset);
11748
- if (!asset) {
11749
- // An empty id is a legitimate transient (the asset may be assigned later); a
11750
- // non-empty one that resolves to nothing is a dead end - say so rather than staying
11751
- // silently pending.
11752
- if (this._asset) {
11753
- console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
11754
- }
11755
- return;
11756
- }
11757
- if (asset.loaded) {
11758
- this._instantiate(asset.resource);
11759
- }
11760
- else {
11761
- // The generation is re-checked even though a superseded handler is detached: the
11762
- // detach relies on how the engine's event emitter treats removal, while the check
11763
- // holds on its own. Whichever of load/error fires first detaches the other.
11764
- this._loadHandle = asset.once('load', () => {
11765
- this._detachLoadHandlers();
11766
- if (generation !== this._loadGeneration) {
11767
- return;
11768
- }
11769
- this._instantiate(asset.resource);
11770
- });
11771
- this._errorHandle = asset.once('error', (err) => {
11772
- this._detachLoadHandlers();
11773
- if (generation !== this._loadGeneration) {
11774
- return;
11775
- }
11776
- // A failed load settles readiness with a null entity, mirroring pc-asset:
11777
- // readiness means the load settled, not that it succeeded.
11778
- this.dispatchEvent(new ErrorEvent('error', {
11779
- message: err instanceof Error ? err.message : String(err)
11780
- }));
11781
- this._onReady();
11782
- });
11783
- }
11784
- }
11785
- _unloadModel() {
11786
- this._entity?.destroy();
11787
- this._entity = null;
11788
- }
11789
- /**
11790
- * Sets the id of the `pc-asset` to use for the model.
11791
- * @param value - The asset ID.
11792
- */
11793
- set asset(value) {
11794
- this._asset = value;
11795
- if (this.isConnected) {
11796
- this._loadModel();
11797
- }
11798
- }
11799
- /**
11800
- * Gets the id of the `pc-asset` to use for the model.
11801
- * @returns The asset ID.
11802
- */
11803
- get asset() {
11804
- return this._asset;
11805
- }
11806
- static get observedAttributes() {
11807
- return ['asset'];
11808
- }
11809
- attributeChangedCallback(name, _oldValue, newValue) {
11810
- switch (name) {
11811
- case 'asset':
11812
- this.asset = newValue ?? '';
11813
- break;
11814
- }
11815
- }
11816
- }
11817
- customElements.define('pc-model', ModelElement);
11818
-
11819
12959
  /**
11820
12960
  * Parses one mapping into its valid rules, warning for each entry that is not one: an unknown
11821
12961
  * or missing selector prefix, an empty `name:` value, an `index:` value that is not a
@@ -12055,7 +13195,10 @@ class NodeElement extends EntityBaseElement {
12055
13195
  * is retained untouched — a redundant edit must not flicker overrides through a revert.
12056
13196
  */
12057
13197
  _rebind() {
12058
- const hostEntity = this._host?.entity ?? null;
13198
+ // A model fronts a host entity of its own; the names this element resolves are the
13199
+ // asset's, so the search starts at the instantiated content root, not the wrapper.
13200
+ const host = this._host;
13201
+ const hostEntity = (host instanceof ModelElement ? host.contentEntity : host?.entity) ?? null;
12059
13202
  if (!hostEntity || !this._name) {
12060
13203
  // Host not instantiated (or nothing to look up yet): return to pending. An assigned
12061
13204
  // name arriving later, or the host's next cycle, resolves it.
@@ -12152,8 +13295,9 @@ class NodeElement extends EntityBaseElement {
12152
13295
  }
12153
13296
  this._revertOverrides();
12154
13297
  // Attachment points anchor to the bound node, so they cannot outlive the binding. Each
12155
- // destroyed entity resets its element, which the next _buildChildren re-creates.
12156
- this.querySelectorAll('pc-entity').forEach((child) => {
13298
+ // destroyed entity resets its element, which the next _buildChildren re-creates - a
13299
+ // model host among them re-instantiates its content when it rebuilds.
13300
+ this.querySelectorAll('pc-entity, pc-model').forEach((child) => {
12157
13301
  if (child.closestEntity === this) {
12158
13302
  child.entity?.destroy();
12159
13303
  }
@@ -13179,9 +14323,12 @@ class SkyElement extends AsyncElement {
13179
14323
  }
13180
14324
  customElements.define('pc-sky', SkyElement);
13181
14325
 
14326
+ exports.AnimClipElement = AnimClipElement;
14327
+ exports.AnimComponentElement = AnimComponentElement;
13182
14328
  exports.AppElement = AppElement;
13183
14329
  exports.AssetElement = AssetElement;
13184
14330
  exports.AsyncElement = AsyncElement;
14331
+ exports.AudioListenerComponentElement = AudioListenerComponentElement;
13185
14332
  exports.ButtonComponentElement = ButtonComponentElement;
13186
14333
  exports.CameraComponentElement = CameraComponentElement;
13187
14334
  exports.CollisionComponentElement = CollisionComponentElement;
@@ -13189,15 +14336,14 @@ exports.ComponentElement = ComponentElement;
13189
14336
  exports.ElementComponentElement = ElementComponentElement;
13190
14337
  exports.EntityBaseElement = EntityBaseElement;
13191
14338
  exports.EntityElement = EntityElement;
14339
+ exports.EntityOwnerElement = EntityOwnerElement;
13192
14340
  exports.GSplatComponentElement = GSplatComponentElement;
13193
14341
  exports.JointComponentElement = JointComponentElement;
13194
14342
  exports.LayoutChildComponentElement = LayoutChildComponentElement;
13195
14343
  exports.LayoutGroupComponentElement = LayoutGroupComponentElement;
13196
14344
  exports.LightComponentElement = LightComponentElement;
13197
- exports.ListenerComponentElement = ListenerComponentElement;
13198
14345
  exports.MaterialElement = MaterialElement;
13199
14346
  exports.ModelElement = ModelElement;
13200
- exports.ModuleElement = ModuleElement;
13201
14347
  exports.NodeElement = NodeElement;
13202
14348
  exports.ParticleSystemComponentElement = ParticleSystemComponentElement;
13203
14349
  exports.RenderComponentElement = RenderComponentElement;
@@ -13205,11 +14351,12 @@ exports.RigidBodyComponentElement = RigidBodyComponentElement;
13205
14351
  exports.SceneElement = SceneElement;
13206
14352
  exports.ScreenComponentElement = ScreenComponentElement;
13207
14353
  exports.ScriptComponentElement = ScriptComponentElement;
13208
- exports.ScriptElement = ScriptElement;
14354
+ exports.ScriptInstanceElement = ScriptInstanceElement;
13209
14355
  exports.ScrollViewComponentElement = ScrollViewComponentElement;
13210
14356
  exports.ScrollbarComponentElement = ScrollbarComponentElement;
13211
14357
  exports.SkyElement = SkyElement;
13212
14358
  exports.SoundComponentElement = SoundComponentElement;
13213
14359
  exports.SoundSlotElement = SoundSlotElement;
14360
+ exports.WasmElement = WasmElement;
13214
14361
  exports.whenReady = whenReady;
13215
14362
  //# sourceMappingURL=pwc.cjs.map