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