@playcanvas/web-components 0.16.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 (105) 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-component.d.cts +33 -5
  6. package/dist/components/anim-component.d.ts +33 -5
  7. package/dist/components/{listener-component.d.cts → audio-listener-component.d.cts} +7 -5
  8. package/dist/components/{listener-component.d.ts → audio-listener-component.d.ts} +7 -5
  9. package/dist/components/button-component.d.cts +5 -1
  10. package/dist/components/button-component.d.ts +5 -1
  11. package/dist/components/camera-component.d.cts +2 -0
  12. package/dist/components/camera-component.d.ts +2 -0
  13. package/dist/components/collision-component.d.cts +5 -1
  14. package/dist/components/collision-component.d.ts +5 -1
  15. package/dist/components/component.d.cts +3 -2
  16. package/dist/components/component.d.ts +3 -2
  17. package/dist/components/element-component.d.cts +7 -0
  18. package/dist/components/element-component.d.ts +7 -0
  19. package/dist/components/gsplat-component.d.cts +2 -0
  20. package/dist/components/gsplat-component.d.ts +2 -0
  21. package/dist/components/joint-component.d.cts +2 -0
  22. package/dist/components/joint-component.d.ts +2 -0
  23. package/dist/components/{layoutchild-component.d.cts → layout-child-component.d.cts} +3 -1
  24. package/dist/components/{layoutchild-component.d.ts → layout-child-component.d.ts} +3 -1
  25. package/dist/components/{layoutgroup-component.d.cts → layout-group-component.d.cts} +3 -1
  26. package/dist/components/{layoutgroup-component.d.ts → layout-group-component.d.ts} +3 -1
  27. package/dist/components/light-component.d.cts +2 -0
  28. package/dist/components/light-component.d.ts +2 -0
  29. package/dist/components/{particlesystem-component.d.cts → particle-system-component.d.cts} +3 -1
  30. package/dist/components/{particlesystem-component.d.ts → particle-system-component.d.ts} +3 -1
  31. package/dist/components/render-component.d.cts +2 -0
  32. package/dist/components/render-component.d.ts +2 -0
  33. package/dist/components/{rigidbody-component.d.cts → rigid-body-component.d.cts} +3 -1
  34. package/dist/components/{rigidbody-component.d.ts → rigid-body-component.d.ts} +3 -1
  35. package/dist/components/screen-component.d.cts +2 -0
  36. package/dist/components/screen-component.d.ts +2 -0
  37. package/dist/components/script-component.d.cts +23 -12
  38. package/dist/components/script-component.d.ts +23 -12
  39. package/dist/components/{script.d.cts → script-instance.d.cts} +7 -7
  40. package/dist/components/{script.d.ts → script-instance.d.ts} +7 -7
  41. package/dist/components/{scrollview-component.d.cts → scroll-view-component.d.cts} +3 -1
  42. package/dist/components/{scrollview-component.d.ts → scroll-view-component.d.ts} +3 -1
  43. package/dist/components/scrollbar-component.d.cts +2 -0
  44. package/dist/components/scrollbar-component.d.ts +2 -0
  45. package/dist/components/sound-component.d.cts +3 -1
  46. package/dist/components/sound-component.d.ts +3 -1
  47. package/dist/components/sound-slot.d.cts +2 -2
  48. package/dist/components/sound-slot.d.ts +2 -2
  49. package/dist/custom-elements.json +1603 -986
  50. package/dist/entity-base.d.cts +6 -5
  51. package/dist/entity-base.d.ts +6 -5
  52. package/dist/entity-owner.d.cts +118 -0
  53. package/dist/entity-owner.d.ts +118 -0
  54. package/dist/entity.d.cts +8 -101
  55. package/dist/entity.d.ts +8 -101
  56. package/dist/index.d.cts +22 -21
  57. package/dist/index.d.ts +22 -21
  58. package/dist/model.d.cts +67 -24
  59. package/dist/model.d.ts +67 -24
  60. package/dist/pwc.cjs +526 -268
  61. package/dist/pwc.cjs.map +1 -1
  62. package/dist/pwc.js +526 -268
  63. package/dist/pwc.js.map +1 -1
  64. package/dist/pwc.min.js +1 -1
  65. package/dist/pwc.min.js.map +1 -1
  66. package/dist/pwc.min.mjs +1 -1
  67. package/dist/pwc.min.mjs.map +1 -1
  68. package/dist/pwc.mjs +523 -266
  69. package/dist/pwc.mjs.map +1 -1
  70. package/dist/vscode.html-custom-data.json +145 -90
  71. package/dist/{module.d.cts → wasm.d.cts} +6 -6
  72. package/dist/{module.d.ts → wasm.d.ts} +6 -6
  73. package/dist/web-types.json +378 -250
  74. package/package.json +7 -7
  75. package/src/app.ts +45 -35
  76. package/src/async-element.ts +14 -14
  77. package/src/components/anim-component.ts +84 -14
  78. package/src/components/{listener-component.ts → audio-listener-component.ts} +8 -6
  79. package/src/components/button-component.ts +5 -1
  80. package/src/components/camera-component.ts +2 -0
  81. package/src/components/collision-component.ts +5 -1
  82. package/src/components/component.ts +4 -3
  83. package/src/components/element-component.ts +7 -0
  84. package/src/components/gsplat-component.ts +2 -0
  85. package/src/components/joint-component.ts +2 -0
  86. package/src/components/{layoutchild-component.ts → layout-child-component.ts} +4 -2
  87. package/src/components/{layoutgroup-component.ts → layout-group-component.ts} +4 -2
  88. package/src/components/light-component.ts +2 -0
  89. package/src/components/{particlesystem-component.ts → particle-system-component.ts} +4 -2
  90. package/src/components/render-component.ts +2 -0
  91. package/src/components/{rigidbody-component.ts → rigid-body-component.ts} +4 -2
  92. package/src/components/screen-component.ts +2 -0
  93. package/src/components/script-component.ts +63 -42
  94. package/src/components/{script.ts → script-instance.ts} +14 -14
  95. package/src/components/{scrollview-component.ts → scroll-view-component.ts} +4 -2
  96. package/src/components/scrollbar-component.ts +2 -0
  97. package/src/components/sound-component.ts +4 -2
  98. package/src/components/sound-slot.ts +4 -4
  99. package/src/entity-base.ts +6 -5
  100. package/src/entity-owner.ts +298 -0
  101. package/src/entity.ts +15 -270
  102. package/src/index.ts +26 -24
  103. package/src/model.ts +159 -72
  104. package/src/node.ts +9 -5
  105. package/src/{module.ts → wasm.ts} +8 -8
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,
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,42 @@ 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
+ * @attribute {boolean} enabled - The enabled state of the model.
129
+ * @attribute {string} name - The name of the model.
130
+ * @attribute {string} position - The position of the model.
131
+ * @attribute {string} rotation - The rotation of the model.
132
+ * @attribute {string} scale - The scale of the model.
133
+ * @attribute {string} tags - The tags of the model.
134
+ * @attribute {string} onpointerenter - Script to run when the pointer moves onto the model.
135
+ * @attribute {string} onpointerleave - Script to run when the pointer moves off the model.
136
+ * @attribute {string} onpointermove - Script to run when the pointer moves over the model.
137
+ * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
138
+ * model.
139
+ * @attribute {string} onpointerup - Script to run when a pointer button is released over the
140
+ * model.
141
+ * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the model.
142
+ * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the model.
143
+ * @fires {PointerEvent} pointermove - Fired when the pointer moves over the model.
144
+ * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the model.
145
+ * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the model.
115
146
  * @fires {Event} load - Fired each time a container asset finishes instantiating, including
116
147
  * re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
117
148
  * capture-phase listener on an ancestor.
@@ -119,16 +150,16 @@ const formatHierarchy = (root: HierarchyNode, counts: ReadonlyMap<string, number
119
150
  * error in `message`. Does not bubble. The element still becomes ready — readiness means the load
120
151
  * settled, not that it succeeded.
121
152
  */
122
- class ModelElement extends AsyncElement {
153
+ class ModelElement extends EntityOwnerElement {
123
154
  private _asset = '';
124
155
 
125
- private _entity: Entity | null = null;
156
+ private _contentEntity: Entity | null = null;
126
157
 
127
158
  /**
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.
159
+ * Incremented on every new load, on disconnect, and when the host entity dies, and captured
160
+ * by a load when it starts. A load that resumes from an await or a load callback abandons
161
+ * itself if the value has moved on, so a superseded load can neither instantiate a second
162
+ * content root nor parent one under a host a newer cycle has already replaced.
132
163
  */
133
164
  private _loadGeneration = 0;
134
165
 
@@ -142,12 +173,13 @@ class ModelElement extends AsyncElement {
142
173
  private _errorHandle: EventHandle | null = null;
143
174
 
144
175
  /**
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`.
176
+ * The root entity of the instantiated model content, parented beneath the host entity.
177
+ * `null` until the container asset has loaded and been instantiated, after a failed load,
178
+ * and again once the element has been removed from the document.
179
+ * @returns The content root entity, or `null`.
148
180
  */
149
- get entity(): Entity | null {
150
- return this._entity;
181
+ get contentEntity(): Entity | null {
182
+ return this._contentEntity;
151
183
  }
152
184
 
153
185
  /**
@@ -160,12 +192,13 @@ class ModelElement extends AsyncElement {
160
192
  * is the printable form.
161
193
  *
162
194
  * The snapshot is plain data, computed afresh each call: it does not follow later changes
163
- * to the hierarchy, and mutating it changes nothing.
195
+ * to the hierarchy, and mutating it changes nothing. It covers the instantiated content
196
+ * only — the host entity the element fronts is not part of the asset's node tree.
164
197
  *
165
198
  * @returns The root of the instantiated node tree, or `null`.
166
199
  */
167
200
  hierarchy(): HierarchyNode | null {
168
- const root = this._entity;
201
+ const root = this._contentEntity;
169
202
  if (!root) {
170
203
  return null;
171
204
  }
@@ -214,19 +247,59 @@ class ModelElement extends AsyncElement {
214
247
  // A model outside an application is inert and never becomes ready, so awaiting it hangs.
215
248
  // Warn rather than fail silently, naming the parent it requires, as every other misplaced
216
249
  // element does.
217
- if (!this.closestApp) {
250
+ const closestApp = this.closestApp;
251
+ if (!closestApp) {
218
252
  const label = this._asset ? ` '${this._asset}'` : '';
219
253
  console.warn(`pc-model${label} must be a descendant of pc-app - model not created`);
220
254
  return;
221
255
  }
222
- this._loadModel();
256
+
257
+ // If the app is already running, create the host immediately; during a boot, the app's
258
+ // own sweep does it. Either way, _onBuilt starts the content load once the host is
259
+ // parented.
260
+ if (closestApp._hierarchyReady) {
261
+ const app = closestApp.app!;
262
+
263
+ this._createEntity(app);
264
+ this._buildHierarchy(app);
265
+
266
+ // A build that deferred (an unresolved pc-node above) defers the whole subtree with
267
+ // it - the node's bind sweeps it.
268
+ if (this._built) {
269
+ buildDescendantEntities(this, app);
270
+ }
271
+ }
223
272
  }
224
273
 
225
274
  disconnectedCallback() {
275
+ // Destroying the host destroys the instantiated content with it, and the destroy hook
276
+ // resets the element. The generation guard comes first so a load suspended on an await
277
+ // cannot resume against the torn-down element.
226
278
  this._loadGeneration++;
227
279
  this._detachLoadHandlers();
228
- this._unloadModel();
229
- this._resetReady();
280
+ this._entity?.destroy();
281
+ }
282
+
283
+ /**
284
+ * Starts (or restarts) the content load once the host has been parented. Readiness is not
285
+ * announced here — it tracks the content settling, not the host entering the scene graph.
286
+ */
287
+ protected override _onBuilt() {
288
+ this._loadContent();
289
+ }
290
+
291
+ /**
292
+ * Extends the owner reset for the content: the engine's destroy cascade has already taken
293
+ * the content root down with the host subtree, so only the reference and the in-flight load
294
+ * are dropped here. The next build re-creates the host and re-instantiates the content.
295
+ *
296
+ * @param entity - The host entity that was destroyed.
297
+ */
298
+ protected override _onEntityDestroy(entity: Entity) {
299
+ this._loadGeneration++;
300
+ this._detachLoadHandlers();
301
+ this._contentEntity = null;
302
+ super._onEntityDestroy(entity);
230
303
  }
231
304
 
232
305
  private _detachLoadHandlers() {
@@ -237,9 +310,9 @@ class ModelElement extends AsyncElement {
237
310
  }
238
311
 
239
312
  /**
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.
313
+ * Resolves readiness and dispatches the `load` event. Called once the instantiated content
314
+ * has been parented beneath the host the host itself is already in the scene graph by
315
+ * then, so a ready model's content always has world transforms.
243
316
  */
244
317
  private _announceLoad() {
245
318
  this._onReady();
@@ -247,46 +320,29 @@ class ModelElement extends AsyncElement {
247
320
  }
248
321
 
249
322
  private _instantiate(container: ContainerResource) {
250
- const generation = this._loadGeneration;
251
-
252
- const entity = container.instantiateRenderEntity();
253
- this._entity = entity;
323
+ const content = container.instantiateRenderEntity();
324
+ this._contentEntity = content;
325
+ this._entity!.addChild(content);
326
+ this._announceLoad();
327
+ }
254
328
 
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
- }
329
+ private _destroyContent() {
330
+ this._contentEntity?.destroy();
331
+ this._contentEntity = null;
279
332
  }
280
333
 
281
- private async _loadModel() {
282
- this._unloadModel();
334
+ private async _loadContent() {
335
+ // The old content goes down synchronously, so a reader that checks after an asset change
336
+ // never sees the outgoing hierarchy. The host survives - components and child entities
337
+ // attached to it carry over to the new content.
338
+ this._destroyContent();
283
339
 
284
340
  // Supersede any load already in flight - only the newest load may instantiate
285
341
  const generation = ++this._loadGeneration;
286
342
  this._detachLoadHandlers();
287
343
 
288
344
  // 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.
345
+ // content. A no-op on first connection, where readiness is still pending.
290
346
  this._resetReady();
291
347
 
292
348
  const appElement = this.closestApp;
@@ -302,14 +358,25 @@ class ModelElement extends AsyncElement {
302
358
  return;
303
359
  }
304
360
 
361
+ // The host may not be parented yet - a model under a pc-node that has not bound, reached
362
+ // through the asset setter. Nothing settles here: _onBuilt re-runs this load once the
363
+ // host builds.
364
+ if (!this._entity || !this._built) {
365
+ return;
366
+ }
367
+
368
+ if (this._asset === '') {
369
+ // No asset assigned is a settled selection: the element is a usable host (components
370
+ // attach, waiters resolve) with no content. Assigning an asset later re-arms.
371
+ this._onReady();
372
+ return;
373
+ }
374
+
305
375
  const asset = useAsset(this._asset);
306
376
  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
377
+ // A non-empty id that resolves to nothing is a dead end - say so rather than staying
309
378
  // silently pending.
310
- if (this._asset) {
311
- console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
312
- }
379
+ console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
313
380
  return;
314
381
  }
315
382
 
@@ -331,7 +398,7 @@ class ModelElement extends AsyncElement {
331
398
  if (generation !== this._loadGeneration) {
332
399
  return;
333
400
  }
334
- // A failed load settles readiness with a null entity, mirroring pc-asset:
401
+ // A failed load settles readiness with a null contentEntity, mirroring pc-asset:
335
402
  // readiness means the load settled, not that it succeeded.
336
403
  this.dispatchEvent(
337
404
  new ErrorEvent('error', {
@@ -343,11 +410,6 @@ class ModelElement extends AsyncElement {
343
410
  }
344
411
  }
345
412
 
346
- private _unloadModel() {
347
- this._entity?.destroy();
348
- this._entity = null;
349
- }
350
-
351
413
  /**
352
414
  * Sets the id of the `pc-asset` to use for the model.
353
415
  * @param value - The asset ID.
@@ -355,7 +417,7 @@ class ModelElement extends AsyncElement {
355
417
  set asset(value: string) {
356
418
  this._asset = value;
357
419
  if (this.isConnected) {
358
- this._loadModel();
420
+ this._loadContent();
359
421
  }
360
422
  }
361
423
 
@@ -368,7 +430,7 @@ class ModelElement extends AsyncElement {
368
430
  }
369
431
 
370
432
  static get observedAttributes() {
371
- return ['asset'];
433
+ return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
372
434
  }
373
435
 
374
436
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
@@ -376,6 +438,31 @@ class ModelElement extends AsyncElement {
376
438
  case 'asset':
377
439
  this.asset = newValue ?? '';
378
440
  break;
441
+ case 'enabled':
442
+ this.enabled = parseBool(newValue, true);
443
+ break;
444
+ case 'name':
445
+ this.name = newValue ?? 'Untitled';
446
+ break;
447
+ case 'position':
448
+ this.position = parseVec3(newValue, Vec3.ZERO, name);
449
+ break;
450
+ case 'rotation':
451
+ this.rotation = parseVec3(newValue, Vec3.ZERO, name);
452
+ break;
453
+ case 'scale':
454
+ this.scale = parseVec3(newValue, Vec3.ONE, name);
455
+ break;
456
+ case 'tags':
457
+ this.tags = parseTags(newValue);
458
+ break;
459
+ case 'onpointerenter':
460
+ case 'onpointerleave':
461
+ case 'onpointerdown':
462
+ case 'onpointerup':
463
+ case 'onpointermove':
464
+ this._updateInlineHandler(name, newValue);
465
+ break;
379
466
  }
380
467
  }
381
468
  }
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';
@@ -318,7 +318,10 @@ class NodeElement extends EntityBaseElement {
318
318
  * is retained untouched — a redundant edit must not flicker overrides through a revert.
319
319
  */
320
320
  private _rebind() {
321
- const hostEntity = this._host?.entity ?? null;
321
+ // A model fronts a host entity of its own; the names this element resolves are the
322
+ // asset's, so the search starts at the instantiated content root, not the wrapper.
323
+ const host = this._host;
324
+ const hostEntity = (host instanceof ModelElement ? host.contentEntity : host?.entity) ?? null;
322
325
 
323
326
  if (!hostEntity || !this._name) {
324
327
  // Host not instantiated (or nothing to look up yet): return to pending. An assigned
@@ -435,8 +438,9 @@ class NodeElement extends EntityBaseElement {
435
438
  this._revertOverrides();
436
439
 
437
440
  // 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) => {
441
+ // destroyed entity resets its element, which the next _buildChildren re-creates - a
442
+ // model host among them re-instantiates its content when it rebuilds.
443
+ this.querySelectorAll<EntityOwnerElement>('pc-entity, pc-model').forEach((child) => {
440
444
  if (child.closestEntity === this) {
441
445
  child.entity?.destroy();
442
446
  }
@@ -3,9 +3,9 @@ import { basisInitialize, WasmModule } from 'playcanvas';
3
3
  import { AsyncElement } from './async-element';
4
4
 
5
5
  /**
6
- * The ModuleElement interface provides properties and methods for manipulating
7
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`}
8
- * elements. The ModuleElement interface also inherits the properties and methods of the
6
+ * The WasmElement interface provides properties and methods for manipulating
7
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-wasm/ | `<pc-wasm>`}
8
+ * elements. The WasmElement interface also inherits the properties and methods of the
9
9
  * {@link AsyncElement} interface.
10
10
  *
11
11
  * The attributes are read once, when the module starts loading - on the element's first
@@ -14,7 +14,7 @@ import { AsyncElement } from './async-element';
14
14
  * modules configure engine-global state that never unloads, so readiness is not re-armed by
15
15
  * removing the element, and a re-inserted element does not load again.
16
16
  *
17
- * A `<pc-module>` without a `name` warns and never becomes ready; a containing `<pc-app>` still
17
+ * A `<pc-wasm>` without a `name` warns and never becomes ready; a containing `<pc-app>` still
18
18
  * boots.
19
19
  *
20
20
  * @attribute {string} name - The name of the WebAssembly module to configure, e.g. `Basis` or
@@ -24,7 +24,7 @@ import { AsyncElement } from './async-element';
24
24
  * @attribute {string} fallback - The URL of the module's asm.js fallback script, used when
25
25
  * WebAssembly is unavailable.
26
26
  */
27
- class ModuleElement extends AsyncElement {
27
+ class WasmElement extends AsyncElement {
28
28
  private _loadPromise: Promise<void> | null = null;
29
29
 
30
30
  connectedCallback() {
@@ -34,7 +34,7 @@ class ModuleElement extends AsyncElement {
34
34
  private async _loadModule(): Promise<void> {
35
35
  const name = this.getAttribute('name');
36
36
  if (!name) {
37
- console.warn("pc-module requires a 'name' attribute - no module was configured");
37
+ console.warn("pc-wasm requires a 'name' attribute - no module was configured");
38
38
  return;
39
39
  }
40
40
 
@@ -74,6 +74,6 @@ class ModuleElement extends AsyncElement {
74
74
  }
75
75
  }
76
76
 
77
- customElements.define('pc-module', ModuleElement);
77
+ customElements.define('pc-wasm', WasmElement);
78
78
 
79
- export { ModuleElement };
79
+ export { WasmElement };