@playcanvas/web-components 0.10.0 → 0.10.1
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.
- package/dist/app.d.ts +52 -7
- package/dist/asset.d.ts +9 -0
- package/dist/async-element.d.ts +12 -2
- package/dist/components/component.d.ts +5 -4
- package/dist/custom-elements.json +583 -110
- package/dist/entity.d.ts +5 -4
- package/dist/loading-bar.d.ts +35 -0
- package/dist/material.d.ts +8 -2
- package/dist/pwc.cjs +385 -105
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +385 -105
- package/dist/pwc.js.map +1 -1
- package/dist/pwc.min.js +1 -1
- package/dist/pwc.min.js.map +1 -1
- package/dist/pwc.min.mjs +1 -1
- package/dist/pwc.min.mjs.map +1 -1
- package/dist/pwc.mjs +385 -105
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.ts +11 -3
- package/dist/vscode.html-custom-data.json +8 -3
- package/dist/web-types.json +322 -78
- package/package.json +1 -1
- package/src/app.ts +172 -73
- package/src/asset.ts +25 -0
- package/src/async-element.ts +14 -4
- package/src/components/component.ts +6 -5
- package/src/entity.ts +34 -18
- package/src/loading-bar.ts +122 -0
- package/src/material.ts +10 -4
- package/src/scene.ts +49 -19
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
import { AssetElement } from './asset';
|
|
70
70
|
import { AsyncElement } from './async-element';
|
|
71
71
|
import { EntityElement } from './entity';
|
|
72
|
+
import { LoadingBar } from './loading-bar';
|
|
72
73
|
import { MaterialElement } from './material';
|
|
73
74
|
import { ModuleElement } from './module';
|
|
74
75
|
import { parseBool, parseEnum } from './parse';
|
|
@@ -78,6 +79,11 @@ import { parseBool, parseEnum } from './parse';
|
|
|
78
79
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
79
80
|
* The AppElement interface also inherits the properties and methods of the
|
|
80
81
|
* {@link HTMLElement} interface.
|
|
82
|
+
*
|
|
83
|
+
* @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
|
|
84
|
+
* `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
|
|
85
|
+
* Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
|
|
86
|
+
* not bubble.
|
|
81
87
|
*/
|
|
82
88
|
class AppElement extends AsyncElement {
|
|
83
89
|
/**
|
|
@@ -97,6 +103,10 @@ class AppElement extends AsyncElement {
|
|
|
97
103
|
|
|
98
104
|
private _highResolution = true;
|
|
99
105
|
|
|
106
|
+
private _loadingBar = true;
|
|
107
|
+
|
|
108
|
+
private _bar: LoadingBar | null = null;
|
|
109
|
+
|
|
100
110
|
private _hierarchyReady = false;
|
|
101
111
|
|
|
102
112
|
private _picker: Picker | null = null;
|
|
@@ -111,6 +121,9 @@ class AppElement extends AsyncElement {
|
|
|
111
121
|
|
|
112
122
|
private _hoveredEntity: EntityElement | null = null;
|
|
113
123
|
|
|
124
|
+
// Identifies the newest in-flight hover pick, so out-of-order results can be discarded
|
|
125
|
+
private _pickToken = 0;
|
|
126
|
+
|
|
114
127
|
private _pointerHandlers: { [key: string]: EventListener | null } = {
|
|
115
128
|
pointermove: null,
|
|
116
129
|
pointerdown: null,
|
|
@@ -119,13 +132,27 @@ class AppElement extends AsyncElement {
|
|
|
119
132
|
|
|
120
133
|
private _app: AppBase | null = null;
|
|
121
134
|
|
|
135
|
+
private _loadProgress = 0;
|
|
136
|
+
|
|
122
137
|
/**
|
|
123
|
-
* The PlayCanvas application instance.
|
|
124
|
-
* {@link whenReady} or the element's `ready()`
|
|
125
|
-
*
|
|
138
|
+
* The PlayCanvas application instance. `null` until the element is ready, and again once it
|
|
139
|
+
* has been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
140
|
+
* promise before accessing it.
|
|
141
|
+
* @returns The application instance, or `null`.
|
|
126
142
|
*/
|
|
127
|
-
get app(): AppBase {
|
|
128
|
-
return this._app
|
|
143
|
+
get app(): AppBase | null {
|
|
144
|
+
return this._app;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The asset preload progress of the application, as a fraction from 0 to 1. It is 0 until
|
|
149
|
+
* preloading begins (and again once the element has been removed from the document), and 1
|
|
150
|
+
* once preloading has finished — including when there was nothing to preload. Read this to
|
|
151
|
+
* initialize a loading UI; subsequent updates arrive via the `progress` event.
|
|
152
|
+
* @returns The preload progress.
|
|
153
|
+
*/
|
|
154
|
+
get loadProgress(): number {
|
|
155
|
+
return this._loadProgress;
|
|
129
156
|
}
|
|
130
157
|
|
|
131
158
|
/**
|
|
@@ -141,6 +168,12 @@ class AppElement extends AsyncElement {
|
|
|
141
168
|
}
|
|
142
169
|
|
|
143
170
|
async connectedCallback() {
|
|
171
|
+
// Created before the first await, so the bar is visible while modules and the graphics
|
|
172
|
+
// device are created, and exists before any disconnect could need to clean it up
|
|
173
|
+
if (this._loadingBar && !this._bar) {
|
|
174
|
+
this._bar = new LoadingBar(this);
|
|
175
|
+
}
|
|
176
|
+
|
|
144
177
|
// Get all pc-module elements that are direct children of the pc-app element
|
|
145
178
|
const moduleElements = this.querySelectorAll<ModuleElement>(':scope > pc-module');
|
|
146
179
|
|
|
@@ -234,11 +267,12 @@ class AppElement extends AsyncElement {
|
|
|
234
267
|
createOptions.batchManager = BatchManager;
|
|
235
268
|
createOptions.xr = XrManager;
|
|
236
269
|
|
|
237
|
-
|
|
238
|
-
this.app
|
|
270
|
+
const app = new AppBase(this._canvas);
|
|
271
|
+
this._app = app;
|
|
272
|
+
app.init(createOptions);
|
|
239
273
|
|
|
240
|
-
|
|
241
|
-
|
|
274
|
+
app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
|
|
275
|
+
app.setCanvasResolution(RESOLUTION_AUTO);
|
|
242
276
|
|
|
243
277
|
this._pickerCreate();
|
|
244
278
|
|
|
@@ -248,7 +282,7 @@ class AppElement extends AsyncElement {
|
|
|
248
282
|
assetElement.createAsset();
|
|
249
283
|
const asset = assetElement.asset;
|
|
250
284
|
if (asset) {
|
|
251
|
-
|
|
285
|
+
app.assets.add(asset);
|
|
252
286
|
}
|
|
253
287
|
});
|
|
254
288
|
|
|
@@ -261,20 +295,46 @@ class AppElement extends AsyncElement {
|
|
|
261
295
|
// Create all entities
|
|
262
296
|
const entityElements = this.querySelectorAll<EntityElement>('pc-entity');
|
|
263
297
|
Array.from(entityElements).forEach((entityElement) => {
|
|
264
|
-
entityElement.createEntity(
|
|
298
|
+
entityElement.createEntity(app);
|
|
265
299
|
});
|
|
266
300
|
|
|
267
301
|
// Build hierarchy
|
|
268
302
|
entityElements.forEach((entityElement) => {
|
|
269
|
-
entityElement.buildHierarchy(
|
|
303
|
+
entityElement.buildHierarchy(app);
|
|
270
304
|
});
|
|
271
305
|
|
|
272
306
|
this._hierarchyReady = true;
|
|
273
307
|
|
|
308
|
+
// Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
|
|
309
|
+
// listener must be attached before preload() is called: an asset that is already loaded
|
|
310
|
+
// ticks synchronously inside it.
|
|
311
|
+
const total = app.assets.list({ preload: true }).length;
|
|
312
|
+
let loaded = 0;
|
|
313
|
+
const onPreloadProgress = () => {
|
|
314
|
+
loaded += 1;
|
|
315
|
+
this._loadProgress = loaded / total;
|
|
316
|
+
this._bar?.progress(loaded, total);
|
|
317
|
+
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded, total }));
|
|
318
|
+
};
|
|
319
|
+
app.on('preload:progress', onPreloadProgress);
|
|
320
|
+
|
|
321
|
+
this._loadProgress = total === 0 ? 1 : 0;
|
|
322
|
+
this._bar?.progress(0, total);
|
|
323
|
+
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
|
|
324
|
+
|
|
274
325
|
// Load assets before starting the application
|
|
275
|
-
|
|
326
|
+
app.preload(() => {
|
|
327
|
+
// Scope the counter to this preload pass, so a later app.preload() call by user code
|
|
328
|
+
// cannot push `loaded` past `total`
|
|
329
|
+
app.off('preload:progress', onPreloadProgress);
|
|
330
|
+
this._loadProgress = 1;
|
|
331
|
+
|
|
276
332
|
// Start the application
|
|
277
|
-
|
|
333
|
+
app.start();
|
|
334
|
+
|
|
335
|
+
// Dismiss the bar only once a frame has actually rendered; ready fires before the
|
|
336
|
+
// first rAF tick
|
|
337
|
+
app.once('frameend', () => this._bar?.complete());
|
|
278
338
|
|
|
279
339
|
// Handle window resize to keep the canvas responsive
|
|
280
340
|
window.addEventListener('resize', this._onWindowResize);
|
|
@@ -287,10 +347,13 @@ class AppElement extends AsyncElement {
|
|
|
287
347
|
this._pickerDestroy();
|
|
288
348
|
|
|
289
349
|
// Clean up the application
|
|
290
|
-
if (this.
|
|
291
|
-
this.
|
|
350
|
+
if (this._app) {
|
|
351
|
+
this._app.destroy();
|
|
292
352
|
this._app = null;
|
|
293
353
|
}
|
|
354
|
+
this._loadProgress = 0;
|
|
355
|
+
this._bar?.destroy();
|
|
356
|
+
this._bar = null;
|
|
294
357
|
|
|
295
358
|
// Remove event listeners
|
|
296
359
|
window.removeEventListener('resize', this._onWindowResize);
|
|
@@ -312,10 +375,18 @@ class AppElement extends AsyncElement {
|
|
|
312
375
|
const { width, height } = this.app!.graphicsDevice;
|
|
313
376
|
this._picker = new Picker(this.app!, width, height);
|
|
314
377
|
|
|
315
|
-
// Create bound handlers but don't attach them yet
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
378
|
+
// Create bound handlers but don't attach them yet. The handlers pick asynchronously, so
|
|
379
|
+
// each is wrapped to discard the promise - a listener must not return one, and nothing
|
|
380
|
+
// awaits the result.
|
|
381
|
+
const listener = (handler: (event: PointerEvent) => Promise<void>): EventListener => {
|
|
382
|
+
return (event: Event) => {
|
|
383
|
+
handler.call(this, event as PointerEvent);
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
this._pointerHandlers.pointermove = listener(this._onPointerMove);
|
|
388
|
+
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
389
|
+
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
319
390
|
|
|
320
391
|
// Listen for pointer listeners being added/removed
|
|
321
392
|
['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
|
|
@@ -369,31 +440,51 @@ class AppElement extends AsyncElement {
|
|
|
369
440
|
return { x, y };
|
|
370
441
|
}
|
|
371
442
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
443
|
+
/**
|
|
444
|
+
* Picks the scene under the pointer and returns the graph node that was hit, or `null`.
|
|
445
|
+
*
|
|
446
|
+
* The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
|
|
447
|
+
* supported on WebGPU, where it returns an empty selection rather than failing - which
|
|
448
|
+
* silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
|
|
449
|
+
* async variant works on both backends and does not block the main thread on a GPU read.
|
|
450
|
+
*
|
|
451
|
+
* @param event - The pointer event to pick under.
|
|
452
|
+
* @returns The graph node under the pointer, or `null` if nothing was hit.
|
|
453
|
+
*/
|
|
454
|
+
private async _pickNode(event: PointerEvent): Promise<GraphNode | null> {
|
|
375
455
|
const camera = this.app!.root.findComponent('camera') as CameraComponent;
|
|
376
|
-
if (!camera) return;
|
|
456
|
+
if (!camera) return null;
|
|
377
457
|
|
|
378
|
-
// Use the helper to convert event coordinates into canvas/picker coordinates.
|
|
379
458
|
const { x, y } = this._getPickerCoordinates(event);
|
|
380
459
|
|
|
381
|
-
this._picker
|
|
382
|
-
const selection = this._picker
|
|
460
|
+
this._picker!.prepare(camera, this.app!.scene);
|
|
461
|
+
const selection = await this._picker!.getSelectionAsync(x, y);
|
|
462
|
+
if (selection.length === 0) return null;
|
|
463
|
+
|
|
464
|
+
const item = selection[0];
|
|
465
|
+
return item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async _onPointerMove(event: PointerEvent) {
|
|
469
|
+
if (!this._picker || !this.app) return;
|
|
470
|
+
|
|
471
|
+
// Moves arrive faster than a pick resolves, so results can land out of order. Only the
|
|
472
|
+
// newest pick may update the hover state - an older one describes a pointer position the
|
|
473
|
+
// user has already left.
|
|
474
|
+
const token = ++this._pickToken;
|
|
475
|
+
const node = await this._pickNode(event);
|
|
476
|
+
if (token !== this._pickToken || !this._picker) return;
|
|
383
477
|
|
|
384
478
|
// Get the currently hovered entity by walking up the hierarchy
|
|
385
479
|
let newHoverEntity: EntityElement | null = null;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
newHoverEntity = entityElement;
|
|
393
|
-
break;
|
|
394
|
-
}
|
|
395
|
-
currentNode = currentNode.parent;
|
|
480
|
+
let currentNode = node;
|
|
481
|
+
while (currentNode !== null) {
|
|
482
|
+
const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
|
|
483
|
+
if (entityElement) {
|
|
484
|
+
newHoverEntity = entityElement;
|
|
485
|
+
break;
|
|
396
486
|
}
|
|
487
|
+
currentNode = currentNode.parent;
|
|
397
488
|
}
|
|
398
489
|
|
|
399
490
|
// Handle enter/leave events
|
|
@@ -415,51 +506,31 @@ class AppElement extends AsyncElement {
|
|
|
415
506
|
}
|
|
416
507
|
}
|
|
417
508
|
|
|
418
|
-
_onPointerDown(event: PointerEvent) {
|
|
509
|
+
async _onPointerDown(event: PointerEvent) {
|
|
419
510
|
if (!this._picker || !this.app) return;
|
|
420
511
|
|
|
421
|
-
|
|
422
|
-
if (!
|
|
512
|
+
let currentNode = await this._pickNode(event);
|
|
513
|
+
if (!this._picker) return; // the element disconnected while the pick was in flight
|
|
423
514
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (selection.length > 0) {
|
|
431
|
-
const item = selection[0];
|
|
432
|
-
let currentNode: GraphNode | null = item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
|
|
433
|
-
while (currentNode !== null) {
|
|
434
|
-
const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
|
|
435
|
-
if (entityElement && entityElement.hasListeners('pointerdown')) {
|
|
436
|
-
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
437
|
-
break;
|
|
438
|
-
}
|
|
439
|
-
currentNode = currentNode.parent;
|
|
515
|
+
while (currentNode !== null) {
|
|
516
|
+
const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
|
|
517
|
+
if (entityElement && entityElement.hasListeners('pointerdown')) {
|
|
518
|
+
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
519
|
+
break;
|
|
440
520
|
}
|
|
521
|
+
currentNode = currentNode.parent;
|
|
441
522
|
}
|
|
442
523
|
}
|
|
443
524
|
|
|
444
|
-
_onPointerUp(event: PointerEvent) {
|
|
525
|
+
async _onPointerUp(event: PointerEvent) {
|
|
445
526
|
if (!this._picker || !this.app) return;
|
|
446
527
|
|
|
447
|
-
const
|
|
448
|
-
if (!
|
|
449
|
-
|
|
450
|
-
// Convert CSS coordinates to picker coordinates
|
|
451
|
-
const { x, y } = this._getPickerCoordinates(event);
|
|
528
|
+
const node = await this._pickNode(event);
|
|
529
|
+
if (!node || !this._picker) return;
|
|
452
530
|
|
|
453
|
-
this.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (selection.length > 0) {
|
|
457
|
-
const item = selection[0];
|
|
458
|
-
const node = item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
|
|
459
|
-
const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`) as EntityElement;
|
|
460
|
-
if (entityElement && entityElement.hasListeners('pointerup')) {
|
|
461
|
-
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
462
|
-
}
|
|
531
|
+
const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`) as EntityElement;
|
|
532
|
+
if (entityElement && entityElement.hasListeners('pointerup')) {
|
|
533
|
+
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
463
534
|
}
|
|
464
535
|
}
|
|
465
536
|
|
|
@@ -589,6 +660,31 @@ class AppElement extends AsyncElement {
|
|
|
589
660
|
return this._highResolution;
|
|
590
661
|
}
|
|
591
662
|
|
|
663
|
+
/**
|
|
664
|
+
* Sets whether the application shows its built-in loading bar while it boots and preloads its
|
|
665
|
+
* assets. Enabled by default; setting `false` removes the bar immediately, while setting
|
|
666
|
+
* `true` has no effect until the element is next connected. The bar can be themed with the
|
|
667
|
+
* CSS custom properties `--pc-loading-bar-color`, `--pc-loading-bar-background` and
|
|
668
|
+
* `--pc-loading-bar-height`.
|
|
669
|
+
* @param value - The loading bar flag.
|
|
670
|
+
*/
|
|
671
|
+
set loadingBar(value: boolean) {
|
|
672
|
+
this._loadingBar = value;
|
|
673
|
+
if (!value && this._bar) {
|
|
674
|
+
this._bar.destroy();
|
|
675
|
+
this._bar = null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Gets whether the application shows its built-in loading bar while it boots and preloads
|
|
681
|
+
* its assets.
|
|
682
|
+
* @returns The loading bar flag.
|
|
683
|
+
*/
|
|
684
|
+
get loadingBar() {
|
|
685
|
+
return this._loadingBar;
|
|
686
|
+
}
|
|
687
|
+
|
|
592
688
|
/**
|
|
593
689
|
* Sets the stencil flag.
|
|
594
690
|
* @param value - The stencil flag.
|
|
@@ -606,7 +702,7 @@ class AppElement extends AsyncElement {
|
|
|
606
702
|
}
|
|
607
703
|
|
|
608
704
|
static get observedAttributes() {
|
|
609
|
-
return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution'];
|
|
705
|
+
return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution', 'loading-bar'];
|
|
610
706
|
}
|
|
611
707
|
|
|
612
708
|
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
@@ -626,6 +722,9 @@ class AppElement extends AsyncElement {
|
|
|
626
722
|
case 'high-resolution':
|
|
627
723
|
this.highResolution = parseBool(newValue, true);
|
|
628
724
|
break;
|
|
725
|
+
case 'loading-bar':
|
|
726
|
+
this.loadingBar = parseBool(newValue, true);
|
|
727
|
+
break;
|
|
629
728
|
case 'stencil':
|
|
630
729
|
this.stencil = parseBool(newValue, true);
|
|
631
730
|
break;
|
package/src/asset.ts
CHANGED
|
@@ -99,6 +99,13 @@ const processBufferView = (
|
|
|
99
99
|
* @attribute {number} pixels-per-unit - For a `sprite` asset, the number of pixels per world unit.
|
|
100
100
|
* @attribute {'simple' | 'sliced' | 'tiled'} render-mode - For a `sprite` asset, how the sprite is
|
|
101
101
|
* rendered when resized.
|
|
102
|
+
*
|
|
103
|
+
* @fires {Event} load - Fired each time the asset finishes loading, including a `lazy` asset
|
|
104
|
+
* loaded later and any subsequent reloads. Does not bubble — listen on this element, or use a
|
|
105
|
+
* capture-phase listener on an ancestor to observe every asset.
|
|
106
|
+
* @fires {ErrorEvent} error - Fired when the asset fails to load, with the engine's error in
|
|
107
|
+
* `message`. Does not bubble. The element still becomes ready — readiness means the load settled,
|
|
108
|
+
* not that it succeeded.
|
|
102
109
|
*/
|
|
103
110
|
class AssetElement extends AsyncElement {
|
|
104
111
|
private _lazy: boolean = false;
|
|
@@ -149,6 +156,16 @@ class AssetElement extends AsyncElement {
|
|
|
149
156
|
this.destroyAsset();
|
|
150
157
|
}
|
|
151
158
|
|
|
159
|
+
private _onAssetLoad() {
|
|
160
|
+
this.dispatchEvent(new Event('load'));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private _onAssetError(err: string | Error) {
|
|
164
|
+
this.dispatchEvent(new ErrorEvent('error', {
|
|
165
|
+
message: err instanceof Error ? err.message : String(err)
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
152
169
|
createAsset() {
|
|
153
170
|
const id = this.getAttribute('id') || '';
|
|
154
171
|
const src = this.getAttribute('src') || '';
|
|
@@ -186,6 +203,11 @@ class AssetElement extends AsyncElement {
|
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
this.asset.preload = !this._lazy;
|
|
206
|
+
|
|
207
|
+
// Forward the engine asset's load outcome as DOM events on this element, like <img>.
|
|
208
|
+
// Attached before the asset joins the registry, which is what starts a preloaded load.
|
|
209
|
+
this.asset.on('load', this._onAssetLoad, this);
|
|
210
|
+
this.asset.on('error', this._onAssetError, this);
|
|
189
211
|
}
|
|
190
212
|
|
|
191
213
|
/**
|
|
@@ -249,6 +271,9 @@ class AssetElement extends AsyncElement {
|
|
|
249
271
|
|
|
250
272
|
destroyAsset() {
|
|
251
273
|
if (this.asset) {
|
|
274
|
+
// A caller that keeps the Asset alive must not dispatch on a removed element
|
|
275
|
+
this.asset.off('load', this._onAssetLoad, this);
|
|
276
|
+
this.asset.off('error', this._onAssetError, this);
|
|
252
277
|
// Deregister first so unload() can still notify the registry
|
|
253
278
|
this.asset.registry?.remove(this.asset);
|
|
254
279
|
this.asset.unload();
|
package/src/async-element.ts
CHANGED
|
@@ -20,12 +20,22 @@ class AsyncElement extends HTMLElement {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The nearest ancestor `<pc-app>` element, or `null` if this element has no `<pc-app>`
|
|
25
|
+
* ancestor. The search starts at the parent, so an element never resolves to itself.
|
|
26
|
+
* @returns The closest app element, or `null`.
|
|
27
|
+
*/
|
|
28
|
+
get closestApp(): AppElement | null {
|
|
29
|
+
return this.parentElement?.closest('pc-app') as AppElement | null ?? null;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The nearest ancestor `<pc-entity>` element, or `null` if this element has no `<pc-entity>`
|
|
34
|
+
* ancestor. The search starts at the parent, so an element never resolves to itself.
|
|
35
|
+
* @returns The closest entity element, or `null`.
|
|
36
|
+
*/
|
|
37
|
+
get closestEntity(): EntityElement | null {
|
|
38
|
+
return this.parentElement?.closest('pc-entity') as EntityElement | null ?? null;
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
/**
|
|
@@ -73,12 +73,13 @@ class ComponentElement extends AsyncElement {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* The PlayCanvas component instance.
|
|
77
|
-
* {@link whenReady} or the
|
|
78
|
-
*
|
|
76
|
+
* The PlayCanvas component instance. `null` until the element is ready, and also for an
|
|
77
|
+
* element that is not a descendant of a `<pc-entity>` — await {@link whenReady} or the
|
|
78
|
+
* element's `ready()` promise before accessing it.
|
|
79
|
+
* @returns The component instance, or `null`.
|
|
79
80
|
*/
|
|
80
|
-
get component(): Component {
|
|
81
|
-
return this._component
|
|
81
|
+
get component(): Component | null {
|
|
82
|
+
return this._component;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/**
|
package/src/entity.ts
CHANGED
|
@@ -76,12 +76,13 @@ class EntityElement extends AsyncElement {
|
|
|
76
76
|
private _entity: Entity | null = null;
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* The PlayCanvas entity instance.
|
|
80
|
-
* {@link whenReady} or the element's `ready()`
|
|
81
|
-
*
|
|
79
|
+
* The PlayCanvas entity instance. `null` until the element is ready, and again once it has
|
|
80
|
+
* been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
81
|
+
* promise before accessing it.
|
|
82
|
+
* @returns The entity instance, or `null`.
|
|
82
83
|
*/
|
|
83
|
-
get entity(): Entity {
|
|
84
|
-
return this._entity
|
|
84
|
+
get entity(): Entity | null {
|
|
85
|
+
return this._entity;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
createEntity(app: AppBase) {
|
|
@@ -92,18 +93,20 @@ class EntityElement extends AsyncElement {
|
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
//
|
|
96
|
-
|
|
96
|
+
// Seed from the cached fields rather than re-reading the attributes. Every observed
|
|
97
|
+
// attribute is routed through its property setter by attributeChangedCallback, so the field
|
|
98
|
+
// already holds the parsed attribute value - and it also holds anything assigned through the
|
|
99
|
+
// property API before the app booted, which reading the attribute back would discard.
|
|
100
|
+
const entity = new Entity(this._name, app);
|
|
97
101
|
this._entity = entity;
|
|
98
102
|
|
|
99
|
-
entity.enabled =
|
|
100
|
-
entity.setLocalPosition(
|
|
101
|
-
entity.setLocalEulerAngles(
|
|
102
|
-
entity.setLocalScale(
|
|
103
|
+
entity.enabled = this._enabled;
|
|
104
|
+
entity.setLocalPosition(this._position);
|
|
105
|
+
entity.setLocalEulerAngles(this._rotation);
|
|
106
|
+
entity.setLocalScale(this._scale);
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
entity.tags.add(tags);
|
|
108
|
+
if (this._tags.length > 0) {
|
|
109
|
+
entity.tags.add(this._tags);
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -124,7 +127,15 @@ class EntityElement extends AsyncElement {
|
|
|
124
127
|
connectedCallback() {
|
|
125
128
|
// Wait for app to be ready
|
|
126
129
|
const closestApp = this.closestApp;
|
|
127
|
-
if (!closestApp)
|
|
130
|
+
if (!closestApp) {
|
|
131
|
+
// An entity outside an application is inert and never becomes ready, so awaiting it
|
|
132
|
+
// hangs. Warn rather than fail silently, naming the parent it requires, as every other
|
|
133
|
+
// misplaced element does.
|
|
134
|
+
const name = this.getAttribute('name');
|
|
135
|
+
const label = name ? ` '${name}'` : '';
|
|
136
|
+
console.warn(`pc-entity${label} must be a descendant of pc-app - entity not created`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
128
139
|
|
|
129
140
|
// If app is already running, create entity immediately
|
|
130
141
|
if (closestApp.hierarchyReady) {
|
|
@@ -146,10 +157,15 @@ class EntityElement extends AsyncElement {
|
|
|
146
157
|
|
|
147
158
|
disconnectedCallback() {
|
|
148
159
|
if (this.entity) {
|
|
149
|
-
// Notify all children that their entities are about to become invalid
|
|
150
|
-
|
|
160
|
+
// Notify all children that their entities are about to become invalid. Both fields have
|
|
161
|
+
// to be reset here, not just _entity: a descendant's own disconnectedCallback runs after
|
|
162
|
+
// this one and skips its reset behind the `if (this.entity)` guard, because we have
|
|
163
|
+
// already nulled the entity it tests. Leaving _built set would make buildHierarchy bail
|
|
164
|
+
// on re-insertion, so the descendant would get a fresh entity that is never parented.
|
|
165
|
+
const children = this.querySelectorAll<EntityElement>('pc-entity');
|
|
151
166
|
children.forEach((child) => {
|
|
152
|
-
|
|
167
|
+
child._entity = null;
|
|
168
|
+
child._built = false;
|
|
153
169
|
});
|
|
154
170
|
|
|
155
171
|
// Destroy the entity
|