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