@playcanvas/web-components 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +5 -3
  2. package/dist/app.d.cts +18 -13
  3. package/dist/app.d.ts +18 -13
  4. package/dist/asset.d.cts +13 -3
  5. package/dist/asset.d.ts +13 -3
  6. package/dist/async-element.d.cts +13 -13
  7. package/dist/async-element.d.ts +13 -13
  8. package/dist/components/anim-clip.d.cts +4 -0
  9. package/dist/components/anim-clip.d.ts +4 -0
  10. package/dist/components/anim-component.d.cts +38 -5
  11. package/dist/components/anim-component.d.ts +38 -5
  12. package/dist/components/audio-listener-component.d.cts +26 -0
  13. package/dist/components/audio-listener-component.d.ts +26 -0
  14. package/dist/components/button-component.d.cts +10 -1
  15. package/dist/components/button-component.d.ts +10 -1
  16. package/dist/components/camera-component.d.cts +37 -1
  17. package/dist/components/camera-component.d.ts +37 -1
  18. package/dist/components/collision-component.d.cts +9 -1
  19. package/dist/components/collision-component.d.ts +9 -1
  20. package/dist/components/component.d.cts +3 -2
  21. package/dist/components/component.d.ts +3 -2
  22. package/dist/components/element-component.d.cts +11 -0
  23. package/dist/components/element-component.d.ts +11 -0
  24. package/dist/components/gsplat-component.d.cts +6 -0
  25. package/dist/components/gsplat-component.d.ts +6 -0
  26. package/dist/components/joint-component.d.cts +14 -2
  27. package/dist/components/joint-component.d.ts +14 -2
  28. package/dist/components/{layoutchild-component.d.cts → layout-child-component.d.cts} +7 -1
  29. package/dist/components/{layoutchild-component.d.ts → layout-child-component.d.ts} +7 -1
  30. package/dist/components/{layoutgroup-component.d.cts → layout-group-component.d.cts} +7 -1
  31. package/dist/components/{layoutgroup-component.d.ts → layout-group-component.d.ts} +7 -1
  32. package/dist/components/light-component.d.cts +52 -1
  33. package/dist/components/light-component.d.ts +52 -1
  34. package/dist/components/{particlesystem-component.d.cts → particle-system-component.d.cts} +7 -1
  35. package/dist/components/{particlesystem-component.d.ts → particle-system-component.d.ts} +7 -1
  36. package/dist/components/render-component.d.cts +7 -0
  37. package/dist/components/render-component.d.ts +7 -0
  38. package/dist/components/{rigidbody-component.d.cts → rigid-body-component.d.cts} +8 -1
  39. package/dist/components/{rigidbody-component.d.ts → rigid-body-component.d.ts} +8 -1
  40. package/dist/components/screen-component.d.cts +6 -0
  41. package/dist/components/screen-component.d.ts +6 -0
  42. package/dist/components/script-component.d.cts +26 -12
  43. package/dist/components/script-component.d.ts +26 -12
  44. package/dist/components/{script.d.cts → script-instance.d.cts} +11 -7
  45. package/dist/components/{script.d.ts → script-instance.d.ts} +11 -7
  46. package/dist/components/{scrollview-component.d.cts → scroll-view-component.d.cts} +7 -1
  47. package/dist/components/{scrollview-component.d.ts → scroll-view-component.d.ts} +7 -1
  48. package/dist/components/scrollbar-component.d.cts +6 -0
  49. package/dist/components/scrollbar-component.d.ts +6 -0
  50. package/dist/components/sound-component.d.cts +7 -1
  51. package/dist/components/sound-component.d.ts +7 -1
  52. package/dist/components/sound-slot.d.cts +5 -2
  53. package/dist/components/sound-slot.d.ts +5 -2
  54. package/dist/custom-elements.json +1852 -974
  55. package/dist/entity-base.d.cts +7 -6
  56. package/dist/entity-base.d.ts +7 -6
  57. package/dist/entity-owner.d.cts +118 -0
  58. package/dist/entity-owner.d.ts +118 -0
  59. package/dist/entity.d.cts +14 -102
  60. package/dist/entity.d.ts +14 -102
  61. package/dist/index.d.cts +26 -22
  62. package/dist/index.d.ts +26 -22
  63. package/dist/material.d.cts +18 -7
  64. package/dist/material.d.ts +18 -7
  65. package/dist/model.d.cts +72 -24
  66. package/dist/model.d.ts +72 -24
  67. package/dist/node.d.cts +6 -1
  68. package/dist/node.d.ts +6 -1
  69. package/dist/pwc.cjs +807 -293
  70. package/dist/pwc.cjs.map +1 -1
  71. package/dist/pwc.js +807 -293
  72. package/dist/pwc.js.map +1 -1
  73. package/dist/pwc.min.js +1 -1
  74. package/dist/pwc.min.js.map +1 -1
  75. package/dist/pwc.min.mjs +1 -1
  76. package/dist/pwc.min.mjs.map +1 -1
  77. package/dist/pwc.mjs +805 -292
  78. package/dist/pwc.mjs.map +1 -1
  79. package/dist/scene.d.cts +3 -0
  80. package/dist/scene.d.ts +3 -0
  81. package/dist/sky.d.cts +4 -0
  82. package/dist/sky.d.ts +4 -0
  83. package/dist/vscode.html-custom-data.json +351 -121
  84. package/dist/{module.d.cts → wasm.d.cts} +10 -6
  85. package/dist/{module.d.ts → wasm.d.ts} +10 -6
  86. package/dist/web-types.json +465 -291
  87. package/package.json +7 -7
  88. package/src/app.ts +49 -35
  89. package/src/asset.ts +13 -4
  90. package/src/async-element.ts +14 -14
  91. package/src/components/anim-clip.ts +4 -0
  92. package/src/components/anim-component.ts +89 -14
  93. package/src/components/audio-listener-component.ts +36 -0
  94. package/src/components/button-component.ts +10 -1
  95. package/src/components/camera-component.ts +65 -6
  96. package/src/components/collision-component.ts +9 -1
  97. package/src/components/component.ts +4 -3
  98. package/src/components/element-component.ts +11 -0
  99. package/src/components/gsplat-component.ts +6 -0
  100. package/src/components/joint-component.ts +14 -2
  101. package/src/components/{layoutchild-component.ts → layout-child-component.ts} +8 -2
  102. package/src/components/{layoutgroup-component.ts → layout-group-component.ts} +8 -2
  103. package/src/components/light-component.ts +110 -17
  104. package/src/components/{particlesystem-component.ts → particle-system-component.ts} +8 -2
  105. package/src/components/render-component.ts +7 -0
  106. package/src/components/{rigidbody-component.ts → rigid-body-component.ts} +9 -2
  107. package/src/components/screen-component.ts +6 -0
  108. package/src/components/script-component.ts +66 -42
  109. package/src/components/{script.ts → script-instance.ts} +18 -14
  110. package/src/components/{scrollview-component.ts → scroll-view-component.ts} +8 -2
  111. package/src/components/scrollbar-component.ts +6 -0
  112. package/src/components/sound-component.ts +8 -2
  113. package/src/components/sound-slot.ts +7 -4
  114. package/src/entity-base.ts +8 -7
  115. package/src/entity-owner.ts +298 -0
  116. package/src/entity.ts +21 -271
  117. package/src/index.ts +38 -25
  118. package/src/material.ts +19 -8
  119. package/src/model.ts +164 -72
  120. package/src/node.ts +15 -6
  121. package/src/scene.ts +3 -0
  122. package/src/sky.ts +4 -0
  123. package/src/{module.ts → wasm.ts} +12 -8
  124. package/dist/components/listener-component.d.cts +0 -20
  125. package/dist/components/listener-component.d.ts +0 -20
  126. package/src/components/listener-component.ts +0 -30
package/src/index.ts CHANGED
@@ -9,38 +9,39 @@
9
9
 
10
10
  /* eslint-disable import-x/order */
11
11
 
12
- // Note that order matters here (e.g. pc-entity must be defined before components)
12
+ // Note that order matters here (e.g. pc-entity and pc-model must be defined before components)
13
13
  import { AsyncElement, whenReady } from './async-element';
14
- import { ModuleElement } from './module';
14
+ import { WasmElement } from './wasm';
15
15
  import { AppElement } from './app';
16
16
  import { EntityElement } from './entity';
17
+ import { ModelElement } from './model';
17
18
  import { AssetElement } from './asset';
18
19
  import { AnimComponentElement } from './components/anim-component';
19
20
  import { AnimClipElement } from './components/anim-clip';
20
- import { ListenerComponentElement } from './components/listener-component';
21
+ import { AudioListenerComponentElement } from './components/audio-listener-component';
21
22
  import { ButtonComponentElement } from './components/button-component';
22
23
  import { CameraComponentElement } from './components/camera-component';
23
24
  import { CollisionComponentElement } from './components/collision-component';
24
25
  import { ComponentElement } from './components/component';
25
26
  import { ElementComponentElement } from './components/element-component';
26
27
  import { JointComponentElement } from './components/joint-component';
27
- import { LayoutChildComponentElement } from './components/layoutchild-component';
28
- import { LayoutGroupComponentElement } from './components/layoutgroup-component';
28
+ import { LayoutChildComponentElement } from './components/layout-child-component';
29
+ import { LayoutGroupComponentElement } from './components/layout-group-component';
29
30
  import { LightComponentElement } from './components/light-component';
30
- import { ParticleSystemComponentElement } from './components/particlesystem-component';
31
+ import { ParticleSystemComponentElement } from './components/particle-system-component';
31
32
  import { RenderComponentElement } from './components/render-component';
32
- import { RigidBodyComponentElement } from './components/rigidbody-component';
33
+ import { RigidBodyComponentElement } from './components/rigid-body-component';
33
34
  import { ScreenComponentElement } from './components/screen-component';
34
35
  import { ScrollbarComponentElement } from './components/scrollbar-component';
35
- import { ScrollViewComponentElement } from './components/scrollview-component';
36
+ import { ScrollViewComponentElement } from './components/scroll-view-component';
36
37
  import { ScriptComponentElement } from './components/script-component';
37
- import { ScriptElement } from './components/script';
38
+ import { ScriptInstanceElement } from './components/script-instance';
38
39
  import { SoundComponentElement } from './components/sound-component';
39
40
  import { SoundSlotElement } from './components/sound-slot';
40
41
  import { GSplatComponentElement } from './components/gsplat-component';
41
42
  import { EntityBaseElement } from './entity-base';
43
+ import { EntityOwnerElement } from './entity-owner';
42
44
  import { MaterialElement } from './material';
43
- import { ModelElement } from './model';
44
45
  import { NodeElement } from './node';
45
46
  import { SceneElement } from './scene';
46
47
  import { SkyElement } from './sky';
@@ -71,32 +72,32 @@ declare global {
71
72
  'pc-entity': EntityElement;
72
73
  'pc-gsplat': GSplatComponentElement;
73
74
  'pc-joint': JointComponentElement;
74
- 'pc-layoutchild': LayoutChildComponentElement;
75
- 'pc-layoutgroup': LayoutGroupComponentElement;
75
+ 'pc-layout-child': LayoutChildComponentElement;
76
+ 'pc-layout-group': LayoutGroupComponentElement;
76
77
  'pc-light': LightComponentElement;
77
- 'pc-listener': ListenerComponentElement;
78
+ 'pc-audio-listener': AudioListenerComponentElement;
78
79
  'pc-material': MaterialElement;
79
80
  'pc-model': ModelElement;
80
- 'pc-module': ModuleElement;
81
+ 'pc-wasm': WasmElement;
81
82
  'pc-node': NodeElement;
82
- 'pc-particles': ParticleSystemComponentElement;
83
+ 'pc-particle-system': ParticleSystemComponentElement;
83
84
  'pc-render': RenderComponentElement;
84
- 'pc-rigidbody': RigidBodyComponentElement;
85
+ 'pc-rigid-body': RigidBodyComponentElement;
85
86
  'pc-scene': SceneElement;
86
87
  'pc-screen': ScreenComponentElement;
87
- 'pc-script': ScriptElement;
88
- 'pc-scripts': ScriptComponentElement;
88
+ 'pc-script-instance': ScriptInstanceElement;
89
+ 'pc-script': ScriptComponentElement;
89
90
  'pc-scrollbar': ScrollbarComponentElement;
90
- 'pc-scrollview': ScrollViewComponentElement;
91
+ 'pc-scroll-view': ScrollViewComponentElement;
91
92
  'pc-sky': SkyElement;
92
- 'pc-sound': SoundSlotElement;
93
- 'pc-sounds': SoundComponentElement;
93
+ 'pc-sound-slot': SoundSlotElement;
94
+ 'pc-sound': SoundComponentElement;
94
95
  }
95
96
  }
96
97
 
97
98
  export {
98
99
  AsyncElement,
99
- ModuleElement,
100
+ WasmElement,
100
101
  AppElement,
101
102
  EntityElement,
102
103
  AssetElement,
@@ -112,18 +113,19 @@ export {
112
113
  LayoutGroupComponentElement,
113
114
  ParticleSystemComponentElement,
114
115
  LightComponentElement,
115
- ListenerComponentElement,
116
+ AudioListenerComponentElement,
116
117
  RenderComponentElement,
117
118
  RigidBodyComponentElement,
118
119
  ScreenComponentElement,
119
120
  ScrollbarComponentElement,
120
121
  ScrollViewComponentElement,
121
122
  ScriptComponentElement,
122
- ScriptElement,
123
+ ScriptInstanceElement,
123
124
  SoundComponentElement,
124
125
  SoundSlotElement,
125
126
  GSplatComponentElement,
126
127
  EntityBaseElement,
128
+ EntityOwnerElement,
127
129
  MaterialElement,
128
130
  ModelElement,
129
131
  NodeElement,
@@ -132,6 +134,17 @@ export {
132
134
  whenReady
133
135
  };
134
136
 
137
+ export type { AddressMode, MagFilterMode, MinFilterMode } from './asset';
135
138
  export type { AsyncElementTagName } from './async-element';
139
+ export type { JointType, MotionMode } from './components/joint-component';
140
+ export type {
141
+ BlendType,
142
+ ColorChannel,
143
+ CullMode,
144
+ FresnelModel,
145
+ OccludeSpecular,
146
+ OpacityDither,
147
+ ScalarChannel
148
+ } from './material';
136
149
  export type { HierarchyMaterial, HierarchyNode } from './model';
137
- export type { MaterialOverrides } from './node';
150
+ export type { MaterialOverrides, NodeBindingState } from './node';
package/src/material.ts CHANGED
@@ -29,7 +29,8 @@ import type { AppElement } from './app';
29
29
  import { useAsset } from './asset';
30
30
  import { parseBool, parseColor, parseEnum, parseNumber, parseVec2 } from './parse';
31
31
 
32
- type BlendType =
32
+ /** The blend modes for a material. */
33
+ export type BlendType =
33
34
  | 'none'
34
35
  | 'normal'
35
36
  | 'additive'
@@ -56,7 +57,8 @@ const blendTypes = new Map<BlendType, number>([
56
57
  ['subtractive', BLEND_SUBTRACTIVE]
57
58
  ]);
58
59
 
59
- type CullMode = 'none' | 'back' | 'front' | 'front-and-back';
60
+ /** The face culling modes for a material. */
61
+ export type CullMode = 'none' | 'back' | 'front' | 'front-and-back';
60
62
 
61
63
  const cullModes = new Map<CullMode, number>([
62
64
  ['none', CULLFACE_NONE],
@@ -65,14 +67,16 @@ const cullModes = new Map<CullMode, number>([
65
67
  ['front-and-back', CULLFACE_FRONTANDBACK]
66
68
  ]);
67
69
 
68
- type FresnelModel = 'none' | 'schlick';
70
+ /** The Fresnel models for a material. */
71
+ export type FresnelModel = 'none' | 'schlick';
69
72
 
70
73
  const fresnelModels = new Map<FresnelModel, number>([
71
74
  ['none', FRESNEL_NONE],
72
75
  ['schlick', FRESNEL_SCHLICK]
73
76
  ]);
74
77
 
75
- type OccludeSpecular = 'none' | 'ao' | 'gloss-dependent';
78
+ /** The specular occlusion modes for a material. */
79
+ export type OccludeSpecular = 'none' | 'ao' | 'gloss-dependent';
76
80
 
77
81
  const occludeSpeculars = new Map<OccludeSpecular, number>([
78
82
  ['none', SPECOCC_NONE],
@@ -80,17 +84,20 @@ const occludeSpeculars = new Map<OccludeSpecular, number>([
80
84
  ['gloss-dependent', SPECOCC_GLOSSDEPENDENT]
81
85
  ]);
82
86
 
87
+ /** The opacity dithering modes for a material. */
88
+ export type OpacityDither = 'none' | 'bayer8' | 'bluenoise' | 'ignnoise';
89
+
83
90
  // The DITHER_* constants are strings whose values are exactly these names, so a parsed value is
84
91
  // assigned to the material unchanged rather than mapped through a table.
85
- type OpacityDither = 'none' | 'bayer8' | 'bluenoise' | 'ignnoise';
86
-
87
92
  const opacityDithers: OpacityDither[] = ['none', 'bayer8', 'bluenoise', 'ignnoise'];
88
93
 
89
- type ColorChannel = 'r' | 'g' | 'b' | 'a' | 'rgb';
94
+ /** The texture channels a color map can sample. */
95
+ export type ColorChannel = 'r' | 'g' | 'b' | 'a' | 'rgb';
90
96
 
91
97
  const colorChannels: ColorChannel[] = ['r', 'g', 'b', 'a', 'rgb'];
92
98
 
93
- type ScalarChannel = 'r' | 'g' | 'b' | 'a';
99
+ /** The texture channels a scalar map can sample. */
100
+ export type ScalarChannel = 'r' | 'g' | 'b' | 'a';
94
101
 
95
102
  const scalarChannels: ScalarChannel[] = ['r', 'g', 'b', 'a'];
96
103
 
@@ -136,6 +143,10 @@ type TextureSlot =
136
143
  * The two aliases are documented here rather than on an accessor, because they resolve to the
137
144
  * `gloss` properties and would otherwise inherit gloss's description - which reads inverted.
138
145
  *
146
+ * @elementSummary The `<pc-material>` element defines a physically based material, which
147
+ * `<pc-render>` elements apply by naming its `id`. It is metal/rough by default, with `metalness`
148
+ * starting at 0. Must be a direct child of `<pc-app>`.
149
+ *
139
150
  * @attribute {number} roughness - The roughness of the material, from 0 (shiny) to 1 (rough). An
140
151
  * alias for `gloss` that also inverts it, so do not combine it with the `gloss` attributes.
141
152
  * @attribute {string} roughness-map - The id of the `pc-asset` to use as the roughness map. An
package/src/model.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { ContainerResource, Entity, EventHandle } from 'playcanvas';
2
+ import { Vec3 } from 'playcanvas';
2
3
 
3
4
  import { useAsset } from './asset';
4
- import { AsyncElement } from './async-element';
5
+ import { POINTER_ATTRIBUTES } from './entity-base';
6
+ import { buildDescendantEntities, EntityOwnerElement } from './entity-owner';
7
+ import { parseBool, parseTags, parseVec3 } from './parse';
5
8
 
6
9
  /**
7
10
  * One material assignment of a {@link HierarchyNode} with a render component: a mesh instance's
@@ -104,14 +107,47 @@ const formatHierarchy = (root: HierarchyNode, counts: ReadonlyMap<string, number
104
107
  * The ModelElement interface also inherits the properties and methods of the
105
108
  * {@link HTMLElement} interface.
106
109
  *
107
- * The element becomes ready once its container asset has loaded and the instantiated hierarchy has
108
- * been added to the scene `entity` is non-null by then. A failed load also settles readiness,
109
- * with `entity` remaining `null`: readiness means the load settled, not that it succeeded — listen
110
- * for `error`, or check `entity`, to tell the outcomes apart. Changing `asset` re-arms readiness
111
- * and instantiates anew, so a `ready()` obtained after the change resolves against the new
112
- * hierarchy. A `pc-model` outside a `pc-app`, or referencing an unknown asset id, warns and never
113
- * becomes ready.
110
+ * The element creates and fronts a stable host entity: `entity` is that host, created when the
111
+ * application builds its hierarchy and kept across `asset` changes, so the element's transform
112
+ * and tags are instance placement that composes with whatever transform the asset authored on
113
+ * its root. The instantiated content is parented beneath the host and exposed as
114
+ * {@link contentEntity}.
114
115
  *
116
+ * The element becomes ready once its current asset selection has settled: the container asset
117
+ * has loaded and its content root has been parented beneath the host, the load has failed
118
+ * (`contentEntity` stays `null` — listen for `error`, or check `contentEntity`, to tell the
119
+ * outcomes apart), or no asset is assigned at all. Changing `asset` re-arms readiness and
120
+ * instantiates anew, so a `ready()` obtained after the change resolves against the new content.
121
+ * A `pc-model` outside a `pc-app`, or referencing an unknown asset id, warns and never becomes
122
+ * ready.
123
+ *
124
+ * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
125
+ * intersects the model's geometry, exactly as for `<pc-entity>` — a hit on a content node that no
126
+ * `pc-node` fronts resolves to this element.
127
+ *
128
+ * @elementSummary The `<pc-model>` element instantiates a 3D model from a container asset
129
+ * (typically a GLB) beneath an entity of its own, so the element's transform and tags place the
130
+ * instance in the scene. Its `<pc-node>` children override what the asset authored. Place it in the
131
+ * `<pc-scene>`, or nest it under a `<pc-entity>`, another `<pc-model>` or a `<pc-node>`.
132
+ *
133
+ * @attribute {boolean} enabled - The enabled state of the model.
134
+ * @attribute {string} name - The name of the model.
135
+ * @attribute {string} position - The position of the model.
136
+ * @attribute {string} rotation - The rotation of the model.
137
+ * @attribute {string} scale - The scale of the model.
138
+ * @attribute {string} tags - The tags of the model.
139
+ * @attribute {string} onpointerenter - Script to run when the pointer moves onto the model.
140
+ * @attribute {string} onpointerleave - Script to run when the pointer moves off the model.
141
+ * @attribute {string} onpointermove - Script to run when the pointer moves over the model.
142
+ * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
143
+ * model.
144
+ * @attribute {string} onpointerup - Script to run when a pointer button is released over the
145
+ * model.
146
+ * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the model.
147
+ * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the model.
148
+ * @fires {PointerEvent} pointermove - Fired when the pointer moves over the model.
149
+ * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the model.
150
+ * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the model.
115
151
  * @fires {Event} load - Fired each time a container asset finishes instantiating, including
116
152
  * re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
117
153
  * capture-phase listener on an ancestor.
@@ -119,16 +155,16 @@ const formatHierarchy = (root: HierarchyNode, counts: ReadonlyMap<string, number
119
155
  * error in `message`. Does not bubble. The element still becomes ready — readiness means the load
120
156
  * settled, not that it succeeded.
121
157
  */
122
- class ModelElement extends AsyncElement {
158
+ class ModelElement extends EntityOwnerElement {
123
159
  private _asset = '';
124
160
 
125
- private _entity: Entity | null = null;
161
+ private _contentEntity: Entity | null = null;
126
162
 
127
163
  /**
128
- * Incremented on every new load and on disconnect, and captured by a load when it starts. A
129
- * load that resumes from an await or a load callback abandons itself if the value has moved
130
- * on, so a superseded load can neither instantiate a second entity nor parent one that has
131
- * since been destroyed.
164
+ * Incremented on every new load, on disconnect, and when the host entity dies, and captured
165
+ * by a load when it starts. A load that resumes from an await or a load callback abandons
166
+ * itself if the value has moved on, so a superseded load can neither instantiate a second
167
+ * content root nor parent one under a host a newer cycle has already replaced.
132
168
  */
133
169
  private _loadGeneration = 0;
134
170
 
@@ -142,12 +178,13 @@ class ModelElement extends AsyncElement {
142
178
  private _errorHandle: EventHandle | null = null;
143
179
 
144
180
  /**
145
- * The root entity of the instantiated model. `null` until the container asset has loaded
146
- * and been instantiated, and again once the element has been removed from the document.
147
- * @returns The model's root entity, or `null`.
181
+ * The root entity of the instantiated model content, parented beneath the host entity.
182
+ * `null` until the container asset has loaded and been instantiated, after a failed load,
183
+ * and again once the element has been removed from the document.
184
+ * @returns The content root entity, or `null`.
148
185
  */
149
- get entity(): Entity | null {
150
- return this._entity;
186
+ get contentEntity(): Entity | null {
187
+ return this._contentEntity;
151
188
  }
152
189
 
153
190
  /**
@@ -160,12 +197,13 @@ class ModelElement extends AsyncElement {
160
197
  * is the printable form.
161
198
  *
162
199
  * The snapshot is plain data, computed afresh each call: it does not follow later changes
163
- * to the hierarchy, and mutating it changes nothing.
200
+ * to the hierarchy, and mutating it changes nothing. It covers the instantiated content
201
+ * only — the host entity the element fronts is not part of the asset's node tree.
164
202
  *
165
203
  * @returns The root of the instantiated node tree, or `null`.
166
204
  */
167
205
  hierarchy(): HierarchyNode | null {
168
- const root = this._entity;
206
+ const root = this._contentEntity;
169
207
  if (!root) {
170
208
  return null;
171
209
  }
@@ -214,19 +252,59 @@ class ModelElement extends AsyncElement {
214
252
  // A model outside an application is inert and never becomes ready, so awaiting it hangs.
215
253
  // Warn rather than fail silently, naming the parent it requires, as every other misplaced
216
254
  // element does.
217
- if (!this.closestApp) {
255
+ const closestApp = this.closestApp;
256
+ if (!closestApp) {
218
257
  const label = this._asset ? ` '${this._asset}'` : '';
219
258
  console.warn(`pc-model${label} must be a descendant of pc-app - model not created`);
220
259
  return;
221
260
  }
222
- this._loadModel();
261
+
262
+ // If the app is already running, create the host immediately; during a boot, the app's
263
+ // own sweep does it. Either way, _onBuilt starts the content load once the host is
264
+ // parented.
265
+ if (closestApp._hierarchyReady) {
266
+ const app = closestApp.app!;
267
+
268
+ this._createEntity(app);
269
+ this._buildHierarchy(app);
270
+
271
+ // A build that deferred (an unresolved pc-node above) defers the whole subtree with
272
+ // it - the node's bind sweeps it.
273
+ if (this._built) {
274
+ buildDescendantEntities(this, app);
275
+ }
276
+ }
223
277
  }
224
278
 
225
279
  disconnectedCallback() {
280
+ // Destroying the host destroys the instantiated content with it, and the destroy hook
281
+ // resets the element. The generation guard comes first so a load suspended on an await
282
+ // cannot resume against the torn-down element.
226
283
  this._loadGeneration++;
227
284
  this._detachLoadHandlers();
228
- this._unloadModel();
229
- this._resetReady();
285
+ this._entity?.destroy();
286
+ }
287
+
288
+ /**
289
+ * Starts (or restarts) the content load once the host has been parented. Readiness is not
290
+ * announced here — it tracks the content settling, not the host entering the scene graph.
291
+ */
292
+ protected override _onBuilt() {
293
+ this._loadContent();
294
+ }
295
+
296
+ /**
297
+ * Extends the owner reset for the content: the engine's destroy cascade has already taken
298
+ * the content root down with the host subtree, so only the reference and the in-flight load
299
+ * are dropped here. The next build re-creates the host and re-instantiates the content.
300
+ *
301
+ * @param entity - The host entity that was destroyed.
302
+ */
303
+ protected override _onEntityDestroy(entity: Entity) {
304
+ this._loadGeneration++;
305
+ this._detachLoadHandlers();
306
+ this._contentEntity = null;
307
+ super._onEntityDestroy(entity);
230
308
  }
231
309
 
232
310
  private _detachLoadHandlers() {
@@ -237,9 +315,9 @@ class ModelElement extends AsyncElement {
237
315
  }
238
316
 
239
317
  /**
240
- * Resolves readiness and dispatches the `load` event. Called once the instantiated hierarchy
241
- * has been parented — readiness means "in the scene graph", matching `pc-entity`, so a ready
242
- * model's entity always has world transforms.
318
+ * Resolves readiness and dispatches the `load` event. Called once the instantiated content
319
+ * has been parented beneath the host the host itself is already in the scene graph by
320
+ * then, so a ready model's content always has world transforms.
243
321
  */
244
322
  private _announceLoad() {
245
323
  this._onReady();
@@ -247,46 +325,29 @@ class ModelElement extends AsyncElement {
247
325
  }
248
326
 
249
327
  private _instantiate(container: ContainerResource) {
250
- const generation = this._loadGeneration;
251
-
252
- const entity = container.instantiateRenderEntity();
253
- this._entity = entity;
328
+ const content = container.instantiateRenderEntity();
329
+ this._contentEntity = content;
330
+ this._entity!.addChild(content);
331
+ this._announceLoad();
332
+ }
254
333
 
255
- // The parent's readiness re-arms when it is torn down, so these can resume in a later
256
- // connection cycle. The entity is captured above and the generation re-checked, so a
257
- // stale resume cannot parent an entity a newer cycle has already destroyed.
258
- const parentEntityElement = this.closestEntity;
259
- if (parentEntityElement) {
260
- parentEntityElement.ready().then(() => {
261
- if (generation !== this._loadGeneration) {
262
- return;
263
- }
264
- parentEntityElement.entity!.addChild(entity);
265
- this._announceLoad();
266
- });
267
- } else {
268
- const appElement = this.closestApp;
269
- if (appElement) {
270
- appElement.ready().then(() => {
271
- if (generation !== this._loadGeneration) {
272
- return;
273
- }
274
- appElement.app!.root.addChild(entity);
275
- this._announceLoad();
276
- });
277
- }
278
- }
334
+ private _destroyContent() {
335
+ this._contentEntity?.destroy();
336
+ this._contentEntity = null;
279
337
  }
280
338
 
281
- private async _loadModel() {
282
- this._unloadModel();
339
+ private async _loadContent() {
340
+ // The old content goes down synchronously, so a reader that checks after an asset change
341
+ // never sees the outgoing hierarchy. The host survives - components and child entities
342
+ // attached to it carry over to the new content.
343
+ this._destroyContent();
283
344
 
284
345
  // Supersede any load already in flight - only the newest load may instantiate
285
346
  const generation = ++this._loadGeneration;
286
347
  this._detachLoadHandlers();
287
348
 
288
349
  // Re-arm readiness so a waiter obtained after an asset change resolves against the new
289
- // hierarchy. A no-op on first connection, where readiness is still pending.
350
+ // content. A no-op on first connection, where readiness is still pending.
290
351
  this._resetReady();
291
352
 
292
353
  const appElement = this.closestApp;
@@ -302,14 +363,25 @@ class ModelElement extends AsyncElement {
302
363
  return;
303
364
  }
304
365
 
366
+ // The host may not be parented yet - a model under a pc-node that has not bound, reached
367
+ // through the asset setter. Nothing settles here: _onBuilt re-runs this load once the
368
+ // host builds.
369
+ if (!this._entity || !this._built) {
370
+ return;
371
+ }
372
+
373
+ if (this._asset === '') {
374
+ // No asset assigned is a settled selection: the element is a usable host (components
375
+ // attach, waiters resolve) with no content. Assigning an asset later re-arms.
376
+ this._onReady();
377
+ return;
378
+ }
379
+
305
380
  const asset = useAsset(this._asset);
306
381
  if (!asset) {
307
- // An empty id is a legitimate transient (the asset may be assigned later); a
308
- // non-empty one that resolves to nothing is a dead end - say so rather than staying
382
+ // A non-empty id that resolves to nothing is a dead end - say so rather than staying
309
383
  // silently pending.
310
- if (this._asset) {
311
- console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
312
- }
384
+ console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
313
385
  return;
314
386
  }
315
387
 
@@ -331,7 +403,7 @@ class ModelElement extends AsyncElement {
331
403
  if (generation !== this._loadGeneration) {
332
404
  return;
333
405
  }
334
- // A failed load settles readiness with a null entity, mirroring pc-asset:
406
+ // A failed load settles readiness with a null contentEntity, mirroring pc-asset:
335
407
  // readiness means the load settled, not that it succeeded.
336
408
  this.dispatchEvent(
337
409
  new ErrorEvent('error', {
@@ -343,11 +415,6 @@ class ModelElement extends AsyncElement {
343
415
  }
344
416
  }
345
417
 
346
- private _unloadModel() {
347
- this._entity?.destroy();
348
- this._entity = null;
349
- }
350
-
351
418
  /**
352
419
  * Sets the id of the `pc-asset` to use for the model.
353
420
  * @param value - The asset ID.
@@ -355,7 +422,7 @@ class ModelElement extends AsyncElement {
355
422
  set asset(value: string) {
356
423
  this._asset = value;
357
424
  if (this.isConnected) {
358
- this._loadModel();
425
+ this._loadContent();
359
426
  }
360
427
  }
361
428
 
@@ -368,7 +435,7 @@ class ModelElement extends AsyncElement {
368
435
  }
369
436
 
370
437
  static get observedAttributes() {
371
- return ['asset'];
438
+ return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
372
439
  }
373
440
 
374
441
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
@@ -376,6 +443,31 @@ class ModelElement extends AsyncElement {
376
443
  case 'asset':
377
444
  this.asset = newValue ?? '';
378
445
  break;
446
+ case 'enabled':
447
+ this.enabled = parseBool(newValue, true);
448
+ break;
449
+ case 'name':
450
+ this.name = newValue ?? 'Untitled';
451
+ break;
452
+ case 'position':
453
+ this.position = parseVec3(newValue, Vec3.ZERO, name);
454
+ break;
455
+ case 'rotation':
456
+ this.rotation = parseVec3(newValue, Vec3.ZERO, name);
457
+ break;
458
+ case 'scale':
459
+ this.scale = parseVec3(newValue, Vec3.ONE, name);
460
+ break;
461
+ case 'tags':
462
+ this.tags = parseTags(newValue);
463
+ break;
464
+ case 'onpointerenter':
465
+ case 'onpointerleave':
466
+ case 'onpointerdown':
467
+ case 'onpointerup':
468
+ case 'onpointermove':
469
+ this._updateInlineHandler(name, newValue);
470
+ break;
379
471
  }
380
472
  }
381
473
  }
package/src/node.ts CHANGED
@@ -2,9 +2,9 @@ import type { Entity, EventHandle, GraphNode, Material, MeshInstance, Quat, Rend
2
2
  import { Vec3 } from 'playcanvas';
3
3
 
4
4
  import { ComponentElement } from './components/component';
5
- import { buildDescendantEntities } from './entity';
6
- import type { EntityElement } from './entity';
7
5
  import { EntityBaseElement, POINTER_ATTRIBUTES } from './entity-base';
6
+ import { buildDescendantEntities } from './entity-owner';
7
+ import type { EntityOwnerElement } from './entity-owner';
8
8
  import { MaterialElement } from './material';
9
9
  import { ModelElement } from './model';
10
10
  import { parseBool, parseTags, parseVec3 } from './parse';
@@ -15,7 +15,7 @@ import { parseBool, parseTags, parseVec3 } from './parse';
15
15
  * `missing`/`ambiguous`/`duplicate` when resolution failed — each accompanied by a warning
16
16
  * naming the cause.
17
17
  */
18
- type NodeBindingState = 'pending' | 'bound' | 'missing' | 'ambiguous' | 'duplicate';
18
+ export type NodeBindingState = 'pending' | 'bound' | 'missing' | 'ambiguous' | 'duplicate';
19
19
 
20
20
  /**
21
21
  * The authored values a bound node's overrides displaced, captured per property when the first
@@ -164,6 +164,11 @@ const levenshtein = (a: string, b: string): number => {
164
164
  * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
165
165
  * intersects the bound node's geometry, exactly as for `<pc-entity>`.
166
166
  *
167
+ * @elementSummary The `<pc-node>` element binds to a node inside the hierarchy a `<pc-model>`
168
+ * instantiated and declares overrides against it: a transform, an enabled state, tags, components
169
+ * to add, or content to attach. Its `name` is a lookup, never a rename. Must be a descendant of
170
+ * `<pc-model>`.
171
+ *
167
172
  * @attribute {string} name - The name of the node to bind, resolved within the nearest ancestor
168
173
  * `pc-model` (or `pc-node`) once it has instantiated.
169
174
  * @attribute {number} index - Which match to bind when `name` matches more than one node,
@@ -318,7 +323,10 @@ class NodeElement extends EntityBaseElement {
318
323
  * is retained untouched — a redundant edit must not flicker overrides through a revert.
319
324
  */
320
325
  private _rebind() {
321
- const hostEntity = this._host?.entity ?? null;
326
+ // A model fronts a host entity of its own; the names this element resolves are the
327
+ // asset's, so the search starts at the instantiated content root, not the wrapper.
328
+ const host = this._host;
329
+ const hostEntity = (host instanceof ModelElement ? host.contentEntity : host?.entity) ?? null;
322
330
 
323
331
  if (!hostEntity || !this._name) {
324
332
  // Host not instantiated (or nothing to look up yet): return to pending. An assigned
@@ -435,8 +443,9 @@ class NodeElement extends EntityBaseElement {
435
443
  this._revertOverrides();
436
444
 
437
445
  // Attachment points anchor to the bound node, so they cannot outlive the binding. Each
438
- // destroyed entity resets its element, which the next _buildChildren re-creates.
439
- this.querySelectorAll<EntityElement>('pc-entity').forEach((child) => {
446
+ // destroyed entity resets its element, which the next _buildChildren re-creates - a
447
+ // model host among them re-instantiates its content when it rebuilds.
448
+ this.querySelectorAll<EntityOwnerElement>('pc-entity, pc-model').forEach((child) => {
440
449
  if (child.closestEntity === this) {
441
450
  child.entity?.destroy();
442
451
  }
package/src/scene.ts CHANGED
@@ -9,6 +9,9 @@ import { parseColor, parseEnum, parseNumber, parseVec3 } from './parse';
9
9
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scene/ | `<pc-scene>`} elements.
10
10
  * The SceneElement interface also inherits the properties and methods of the
11
11
  * {@link HTMLElement} interface.
12
+ *
13
+ * @elementSummary The `<pc-scene>` element holds the entity hierarchy the application renders,
14
+ * along with the scene-wide fog and gravity settings. Must be a direct child of `<pc-app>`.
12
15
  */
13
16
  class SceneElement extends AsyncElement {
14
17
  /**
package/src/sky.ts CHANGED
@@ -10,6 +10,10 @@ import { parseBool, parseEnum, parseNumber, parseVec3 } from './parse';
10
10
  * The SkyElement interface provides properties and methods for manipulating
11
11
  * `<pc-sky>` elements. The SkyElement interface also inherits the properties and
12
12
  * methods of the {@link HTMLElement} interface.
13
+ *
14
+ * @elementSummary The `<pc-sky>` element renders a skybox from a texture asset, projected as an
15
+ * infinite background, a box or a dome, and optionally lights the scene from it. Must be a direct
16
+ * child of `<pc-scene>`.
13
17
  */
14
18
  class SkyElement extends AsyncElement {
15
19
  private _asset = '';