@playcanvas/web-components 0.9.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/README.md +18 -0
- package/dist/app.d.ts +55 -9
- package/dist/asset.d.ts +25 -1
- package/dist/async-element.d.ts +15 -2
- package/dist/components/button-component.d.ts +1 -1
- package/dist/components/camera-component.d.ts +1 -1
- package/dist/components/collision-component.d.ts +1 -1
- package/dist/components/component.d.ts +6 -5
- package/dist/components/element-component.d.ts +1 -1
- package/dist/components/gsplat-component.d.ts +1 -1
- package/dist/components/layoutchild-component.d.ts +1 -1
- package/dist/components/layoutgroup-component.d.ts +1 -1
- package/dist/components/light-component.d.ts +1 -1
- package/dist/components/particlesystem-component.d.ts +1 -1
- package/dist/components/render-component.d.ts +1 -1
- package/dist/components/rigidbody-component.d.ts +1 -1
- package/dist/components/screen-component.d.ts +1 -1
- package/dist/components/script.d.ts +8 -1
- package/dist/components/scrollbar-component.d.ts +1 -1
- package/dist/components/scrollview-component.d.ts +1 -1
- package/dist/components/sound-component.d.ts +1 -1
- package/dist/components/sound-slot.d.ts +9 -1
- package/dist/custom-elements.json +16704 -0
- package/dist/entity.d.ts +24 -5
- package/dist/loading-bar.d.ts +35 -0
- package/dist/material.d.ts +972 -4
- package/dist/model.d.ts +1 -1
- package/dist/module.d.ts +10 -0
- package/dist/{utils.d.ts → parse.d.ts} +63 -33
- package/dist/pwc.cjs +3070 -699
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +3070 -699
- 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 +2 -0
- package/dist/pwc.min.mjs.map +1 -0
- package/dist/pwc.mjs +3071 -700
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.ts +12 -4
- package/dist/sky.d.ts +1 -1
- package/dist/vscode.html-custom-data.json +1800 -0
- package/dist/web-types.json +3836 -0
- package/package.json +29 -11
- package/src/app.ts +178 -78
- package/src/asset.ts +44 -2
- package/src/async-element.ts +17 -4
- package/src/components/button-component.ts +6 -6
- package/src/components/camera-component.ts +2 -2
- package/src/components/collision-component.ts +2 -2
- package/src/components/component.ts +8 -7
- package/src/components/element-component.ts +6 -6
- package/src/components/gsplat-component.ts +3 -3
- package/src/components/layoutchild-component.ts +2 -2
- package/src/components/layoutgroup-component.ts +2 -2
- package/src/components/light-component.ts +2 -2
- package/src/components/particlesystem-component.ts +2 -2
- package/src/components/render-component.ts +10 -5
- package/src/components/rigidbody-component.ts +2 -2
- package/src/components/screen-component.ts +2 -2
- package/src/components/script-component.ts +4 -4
- package/src/components/script.ts +9 -2
- package/src/components/scrollbar-component.ts +3 -3
- package/src/components/scrollview-component.ts +6 -6
- package/src/components/sound-component.ts +2 -2
- package/src/components/sound-slot.ts +31 -9
- package/src/entity.ts +56 -22
- package/src/loading-bar.ts +122 -0
- package/src/material.ts +2402 -59
- package/src/model.ts +2 -2
- package/src/module.ts +10 -0
- package/src/{utils.ts → parse.ts} +104 -65
- package/src/scene.ts +51 -21
- package/src/sky.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcanvas/web-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"author": "PlayCanvas <support@playcanvas.com>",
|
|
5
5
|
"homepage": "https://playcanvas.com",
|
|
6
6
|
"description": "Web Components for the PlayCanvas Engine",
|
|
@@ -25,8 +25,11 @@
|
|
|
25
25
|
},
|
|
26
26
|
"main": "dist/pwc.cjs",
|
|
27
27
|
"module": "dist/pwc.mjs",
|
|
28
|
-
"
|
|
28
|
+
"unpkg": "dist/pwc.min.js",
|
|
29
|
+
"jsdelivr": "dist/pwc.min.js",
|
|
29
30
|
"types": "dist/index.d.ts",
|
|
31
|
+
"customElements": "dist/custom-elements.json",
|
|
32
|
+
"web-types": "dist/web-types.json",
|
|
30
33
|
"exports": {
|
|
31
34
|
".": {
|
|
32
35
|
"types": "./dist/index.d.ts",
|
|
@@ -44,21 +47,31 @@
|
|
|
44
47
|
"scripts": {
|
|
45
48
|
"prebuild": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
|
|
46
49
|
"build": "rollup -c",
|
|
50
|
+
"postbuild": "npm run cem",
|
|
51
|
+
"cem": "cem analyze && node utils/cem/validate.mjs",
|
|
47
52
|
"dev": "concurrently \"npm run watch\" \"npm run serve\"",
|
|
48
53
|
"docs": "typedoc",
|
|
49
|
-
"lint": "eslint examples/js examples/assets/scripts src",
|
|
54
|
+
"lint": "eslint examples/js examples/assets/scripts src test vitest.config.ts",
|
|
50
55
|
"serve": "serve",
|
|
51
|
-
"test": "
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
58
|
+
"test:coverage": "vitest run --coverage",
|
|
59
|
+
"test:unit": "vitest run --project unit",
|
|
60
|
+
"test:elements": "vitest run --project elements",
|
|
61
|
+
"test:integration": "vitest run --project integration",
|
|
52
62
|
"publint": "publint",
|
|
53
|
-
"type-check": "
|
|
54
|
-
"type-check:
|
|
63
|
+
"type-check": "npm run type-check:src && npm run type-check:test",
|
|
64
|
+
"type-check:src": "tsc --noEmit -p tsconfig.json",
|
|
65
|
+
"type-check:test": "tsc --noEmit -p tsconfig.test.json",
|
|
66
|
+
"type-check:watch": "npm run type-check:src -- --watch",
|
|
55
67
|
"watch": "rollup -c -w"
|
|
56
68
|
},
|
|
57
69
|
"peerDependencies": {
|
|
58
70
|
"playcanvas": "^2.20.1"
|
|
59
71
|
},
|
|
60
72
|
"devDependencies": {
|
|
61
|
-
"@
|
|
73
|
+
"@custom-elements-manifest/analyzer": "0.11.0",
|
|
74
|
+
"@mediapipe/tasks-vision": "1.0.1",
|
|
62
75
|
"@playcanvas/eslint-config": "2.1.0",
|
|
63
76
|
"@rollup/plugin-commonjs": "29.0.3",
|
|
64
77
|
"@rollup/plugin-node-resolve": "16.0.3",
|
|
@@ -67,20 +80,25 @@
|
|
|
67
80
|
"@tweenjs/tween.js": "25.0.0",
|
|
68
81
|
"@typescript-eslint/eslint-plugin": "8.65.0",
|
|
69
82
|
"@typescript-eslint/parser": "8.65.0",
|
|
83
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
70
84
|
"concurrently": "10.0.4",
|
|
85
|
+
"custom-element-jet-brains-integration": "1.7.0",
|
|
86
|
+
"custom-element-vs-code-integration": "1.5.0",
|
|
71
87
|
"earcut": "3.2.3",
|
|
72
88
|
"eslint": "9.39.5",
|
|
73
89
|
"eslint-import-resolver-typescript": "4.4.5",
|
|
74
|
-
"globals": "17.
|
|
75
|
-
"
|
|
90
|
+
"globals": "17.9.0",
|
|
91
|
+
"jsdom": "30.0.1",
|
|
92
|
+
"mediabunny": "1.52.2",
|
|
76
93
|
"opentype.js": "2.0.0",
|
|
77
94
|
"playcanvas": "2.21.3",
|
|
78
95
|
"publint": "0.3.22",
|
|
79
|
-
"rollup": "4.62.
|
|
96
|
+
"rollup": "4.62.4",
|
|
80
97
|
"serve": "14.2.6",
|
|
81
98
|
"tslib": "2.8.1",
|
|
82
99
|
"typedoc": "0.28.20",
|
|
83
100
|
"typedoc-plugin-mdn-links": "5.1.1",
|
|
84
|
-
"typescript": "6.0.3"
|
|
101
|
+
"typescript": "6.0.3",
|
|
102
|
+
"vitest": "4.1.10"
|
|
85
103
|
}
|
|
86
104
|
}
|
package/src/app.ts
CHANGED
|
@@ -69,15 +69,21 @@ 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
|
-
import { parseBool, parseEnum } from './
|
|
75
|
+
import { parseBool, parseEnum } from './parse';
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
78
|
* The AppElement interface provides properties and methods for manipulating
|
|
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
|
/**
|
|
@@ -87,7 +93,7 @@ class AppElement extends AsyncElement {
|
|
|
87
93
|
|
|
88
94
|
private _alpha = true;
|
|
89
95
|
|
|
90
|
-
private _backend: 'webgpu' | 'webgl2' | 'null' = '
|
|
96
|
+
private _backend: 'webgpu' | 'webgl2' | 'null' = 'webgpu';
|
|
91
97
|
|
|
92
98
|
private _antialias = true;
|
|
93
99
|
|
|
@@ -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
|
|
|
@@ -528,7 +599,8 @@ class AppElement extends AsyncElement {
|
|
|
528
599
|
}
|
|
529
600
|
|
|
530
601
|
/**
|
|
531
|
-
* Sets the graphics backend.
|
|
602
|
+
* Sets the graphics backend. Defaults to 'webgpu', which falls back to 'webgl2' if WebGPU
|
|
603
|
+
* is not supported by the browser.
|
|
532
604
|
* @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
|
|
533
605
|
*/
|
|
534
606
|
set backend(value: 'webgpu' | 'webgl2' | 'null') {
|
|
@@ -588,6 +660,31 @@ class AppElement extends AsyncElement {
|
|
|
588
660
|
return this._highResolution;
|
|
589
661
|
}
|
|
590
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
|
+
|
|
591
688
|
/**
|
|
592
689
|
* Sets the stencil flag.
|
|
593
690
|
* @param value - The stencil flag.
|
|
@@ -605,10 +702,10 @@ class AppElement extends AsyncElement {
|
|
|
605
702
|
}
|
|
606
703
|
|
|
607
704
|
static get observedAttributes() {
|
|
608
|
-
return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution'];
|
|
705
|
+
return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution', 'loading-bar'];
|
|
609
706
|
}
|
|
610
707
|
|
|
611
|
-
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
|
|
708
|
+
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
612
709
|
switch (name) {
|
|
613
710
|
case 'alpha':
|
|
614
711
|
this.alpha = parseBool(newValue, true);
|
|
@@ -617,7 +714,7 @@ class AppElement extends AsyncElement {
|
|
|
617
714
|
this.antialias = parseBool(newValue, true);
|
|
618
715
|
break;
|
|
619
716
|
case 'backend':
|
|
620
|
-
this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], '
|
|
717
|
+
this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgpu', name);
|
|
621
718
|
break;
|
|
622
719
|
case 'depth':
|
|
623
720
|
this.depth = parseBool(newValue, true);
|
|
@@ -625,6 +722,9 @@ class AppElement extends AsyncElement {
|
|
|
625
722
|
case 'high-resolution':
|
|
626
723
|
this.highResolution = parseBool(newValue, true);
|
|
627
724
|
break;
|
|
725
|
+
case 'loading-bar':
|
|
726
|
+
this.loadingBar = parseBool(newValue, true);
|
|
727
|
+
break;
|
|
628
728
|
case 'stencil':
|
|
629
729
|
this.stencil = parseBool(newValue, true);
|
|
630
730
|
break;
|
package/src/asset.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Asset, SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED } from 'playcanvas';
|
|
2
2
|
|
|
3
3
|
import { AsyncElement } from './async-element';
|
|
4
|
-
import { parseBool, parseEnum, parseNumber } from './
|
|
4
|
+
import { parseBool, parseEnum, parseNumber } from './parse';
|
|
5
5
|
import { MeshoptDecoder } from '../lib/meshopt_decoder.module.js';
|
|
6
6
|
|
|
7
7
|
const renderModes = new Map<'simple' | 'sliced' | 'tiled', number>([
|
|
@@ -16,11 +16,13 @@ const extToType = new Map([
|
|
|
16
16
|
['frag', 'shader'],
|
|
17
17
|
['glb', 'container'],
|
|
18
18
|
['glsl', 'shader'],
|
|
19
|
+
['gltf', 'container'],
|
|
19
20
|
['hdr', 'texture'],
|
|
20
21
|
['html', 'html'],
|
|
21
22
|
['jpg', 'texture'],
|
|
22
23
|
['js', 'script'],
|
|
23
24
|
['json', 'json'],
|
|
25
|
+
['ktx2', 'texture'],
|
|
24
26
|
['mp3', 'audio'],
|
|
25
27
|
['mjs', 'script'],
|
|
26
28
|
['ply', 'gsplat'],
|
|
@@ -82,6 +84,28 @@ const processBufferView = (
|
|
|
82
84
|
* while the application is running are created and registered on insertion, and begin loading
|
|
83
85
|
* immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
|
|
84
86
|
* elsewhere, or with an unsupported asset type, never become ready.
|
|
87
|
+
*
|
|
88
|
+
* Apart from `lazy`, these attributes are read once when the asset is created, so changing them
|
|
89
|
+
* later has no effect.
|
|
90
|
+
*
|
|
91
|
+
* @attribute {string} id - The identifier used to reference the asset from other elements.
|
|
92
|
+
* @attribute {string} src - The URL of the asset to load.
|
|
93
|
+
* @attribute {string} type - The asset type. Inferred from the `src` file extension when omitted.
|
|
94
|
+
* @attribute {string} data - Additional asset data, as a JSON object.
|
|
95
|
+
* @attribute {string} atlas - For a `sprite` asset, the `id` of the texture atlas asset it uses.
|
|
96
|
+
* The atlas must be declared before the sprite.
|
|
97
|
+
* @attribute {string} frame-keys - For a `sprite` asset, the atlas frame keys it uses, separated
|
|
98
|
+
* by spaces or commas.
|
|
99
|
+
* @attribute {number} pixels-per-unit - For a `sprite` asset, the number of pixels per world unit.
|
|
100
|
+
* @attribute {'simple' | 'sliced' | 'tiled'} render-mode - For a `sprite` asset, how the sprite is
|
|
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.
|
|
85
109
|
*/
|
|
86
110
|
class AssetElement extends AsyncElement {
|
|
87
111
|
private _lazy: boolean = false;
|
|
@@ -132,6 +156,16 @@ class AssetElement extends AsyncElement {
|
|
|
132
156
|
this.destroyAsset();
|
|
133
157
|
}
|
|
134
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
|
+
|
|
135
169
|
createAsset() {
|
|
136
170
|
const id = this.getAttribute('id') || '';
|
|
137
171
|
const src = this.getAttribute('src') || '';
|
|
@@ -169,6 +203,11 @@ class AssetElement extends AsyncElement {
|
|
|
169
203
|
}
|
|
170
204
|
|
|
171
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);
|
|
172
211
|
}
|
|
173
212
|
|
|
174
213
|
/**
|
|
@@ -232,6 +271,9 @@ class AssetElement extends AsyncElement {
|
|
|
232
271
|
|
|
233
272
|
destroyAsset() {
|
|
234
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);
|
|
235
277
|
// Deregister first so unload() can still notify the registry
|
|
236
278
|
this.asset.registry?.remove(this.asset);
|
|
237
279
|
this.asset.unload();
|
|
@@ -267,7 +309,7 @@ class AssetElement extends AsyncElement {
|
|
|
267
309
|
return ['lazy'];
|
|
268
310
|
}
|
|
269
311
|
|
|
270
|
-
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
|
|
312
|
+
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
271
313
|
if (name === 'lazy') {
|
|
272
314
|
this.lazy = parseBool(newValue, false);
|
|
273
315
|
}
|
package/src/async-element.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { EntityElement } from './entity';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Base class for all PlayCanvas Web Components that initialize asynchronously.
|
|
6
|
+
*
|
|
7
|
+
* @fires {CustomEvent} ready - Fired once the element is fully initialized. Bubbles and is
|
|
8
|
+
* composed.
|
|
6
9
|
*/
|
|
7
10
|
class AsyncElement extends HTMLElement {
|
|
8
11
|
private _readyPromise: Promise<void>;
|
|
@@ -17,12 +20,22 @@ class AsyncElement extends HTMLElement {
|
|
|
17
20
|
});
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
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;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
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;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
/**
|