@playcanvas/web-components 0.10.0 → 0.11.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.
- package/dist/app.d.ts +151 -35
- package/dist/asset.d.ts +9 -0
- package/dist/async-element.d.ts +12 -2
- package/dist/components/camera-component.d.ts +13 -13
- package/dist/components/component.d.ts +5 -4
- package/dist/components/screen-component.d.ts +25 -4
- package/dist/components/scrollview-component.d.ts +6 -4
- package/dist/custom-elements.json +812 -196
- package/dist/entity.d.ts +18 -4
- package/dist/loading-bar.d.ts +35 -0
- package/dist/material.d.ts +8 -2
- package/dist/parse.d.ts +4 -2
- package/dist/pwc.cjs +661 -229
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +661 -229
- 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 +662 -230
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.ts +11 -3
- package/dist/sky.d.ts +12 -11
- package/dist/vscode.html-custom-data.json +47 -22
- package/dist/web-types.json +384 -121
- package/package.json +1 -1
- package/src/app.ts +333 -124
- package/src/asset.ts +25 -0
- package/src/async-element.ts +14 -4
- package/src/components/camera-component.ts +31 -26
- package/src/components/component.ts +6 -5
- package/src/components/screen-component.ts +42 -12
- package/src/components/scrollview-component.ts +6 -4
- package/src/entity.ts +61 -27
- package/src/loading-bar.ts +122 -0
- package/src/material.ts +10 -4
- package/src/parse.ts +5 -3
- package/src/scene.ts +49 -19
- package/src/sky.ts +26 -25
package/src/app.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
CameraComponent,
|
|
5
5
|
createGraphicsDevice,
|
|
6
6
|
ElementInput,
|
|
7
|
+
Entity,
|
|
7
8
|
FILLMODE_FILL_WINDOW,
|
|
8
9
|
GraphNode,
|
|
9
10
|
Keyboard,
|
|
@@ -69,15 +70,21 @@ import {
|
|
|
69
70
|
import { AssetElement } from './asset';
|
|
70
71
|
import { AsyncElement } from './async-element';
|
|
71
72
|
import { EntityElement } from './entity';
|
|
73
|
+
import { LoadingBar } from './loading-bar';
|
|
72
74
|
import { MaterialElement } from './material';
|
|
73
75
|
import { ModuleElement } from './module';
|
|
74
|
-
import { parseBool, parseEnum } from './parse';
|
|
76
|
+
import { parseBool, parseEnum, parseNumber } from './parse';
|
|
75
77
|
|
|
76
78
|
/**
|
|
77
79
|
* The AppElement interface provides properties and methods for manipulating
|
|
78
80
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
79
81
|
* The AppElement interface also inherits the properties and methods of the
|
|
80
82
|
* {@link HTMLElement} interface.
|
|
83
|
+
*
|
|
84
|
+
* @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
|
|
85
|
+
* `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
|
|
86
|
+
* Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
|
|
87
|
+
* not bubble.
|
|
81
88
|
*/
|
|
82
89
|
class AppElement extends AsyncElement {
|
|
83
90
|
/**
|
|
@@ -91,14 +98,32 @@ class AppElement extends AsyncElement {
|
|
|
91
98
|
|
|
92
99
|
private _antialias = true;
|
|
93
100
|
|
|
94
|
-
private
|
|
101
|
+
private _depthBuffer = true;
|
|
95
102
|
|
|
96
|
-
private
|
|
103
|
+
private _stencilBuffer = true;
|
|
97
104
|
|
|
98
|
-
private
|
|
105
|
+
private _maxPixelRatio = Infinity;
|
|
106
|
+
|
|
107
|
+
private _loadingBar = true;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Set once the graphics options above have been handed to `createGraphicsDevice`, after which
|
|
111
|
+
* writing any of them changes nothing. Guards the warning in {@link _warnIfBooted}, and is
|
|
112
|
+
* cleared on disconnect so a re-connected element boots from its current attributes.
|
|
113
|
+
*/
|
|
114
|
+
private _optionsLocked = false;
|
|
115
|
+
|
|
116
|
+
private _bar: LoadingBar | null = null;
|
|
99
117
|
|
|
100
118
|
private _hierarchyReady = false;
|
|
101
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The elements backing this application's entities, keyed by the entity itself. Registered
|
|
122
|
+
* by EntityElement at creation and removed when an entity is destroyed, this joins engine
|
|
123
|
+
* scene nodes back to their owning elements by identity - never by name.
|
|
124
|
+
*/
|
|
125
|
+
private _entityElements = new Map<GraphNode, EntityElement>();
|
|
126
|
+
|
|
102
127
|
private _picker: Picker | null = null;
|
|
103
128
|
|
|
104
129
|
private _hasPointerListeners: { [key: string]: boolean } = {
|
|
@@ -111,6 +136,9 @@ class AppElement extends AsyncElement {
|
|
|
111
136
|
|
|
112
137
|
private _hoveredEntity: EntityElement | null = null;
|
|
113
138
|
|
|
139
|
+
// Identifies the newest in-flight hover pick, so out-of-order results can be discarded
|
|
140
|
+
private _pickToken = 0;
|
|
141
|
+
|
|
114
142
|
private _pointerHandlers: { [key: string]: EventListener | null } = {
|
|
115
143
|
pointermove: null,
|
|
116
144
|
pointerdown: null,
|
|
@@ -119,13 +147,27 @@ class AppElement extends AsyncElement {
|
|
|
119
147
|
|
|
120
148
|
private _app: AppBase | null = null;
|
|
121
149
|
|
|
150
|
+
private _loadProgress = 0;
|
|
151
|
+
|
|
122
152
|
/**
|
|
123
|
-
* The PlayCanvas application instance.
|
|
124
|
-
* {@link whenReady} or the element's `ready()`
|
|
125
|
-
*
|
|
153
|
+
* The PlayCanvas application instance. `null` until the element is ready, and again once it
|
|
154
|
+
* has been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
155
|
+
* promise before accessing it.
|
|
156
|
+
* @returns The application instance, or `null`.
|
|
126
157
|
*/
|
|
127
|
-
get app(): AppBase {
|
|
128
|
-
return this._app
|
|
158
|
+
get app(): AppBase | null {
|
|
159
|
+
return this._app;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The asset preload progress of the application, as a fraction from 0 to 1. It is 0 until
|
|
164
|
+
* preloading begins (and again once the element has been removed from the document), and 1
|
|
165
|
+
* once preloading has finished — including when there was nothing to preload. Read this to
|
|
166
|
+
* initialize a loading UI; subsequent updates arrive via the `progress` event.
|
|
167
|
+
* @returns The preload progress.
|
|
168
|
+
*/
|
|
169
|
+
get loadProgress(): number {
|
|
170
|
+
return this._loadProgress;
|
|
129
171
|
}
|
|
130
172
|
|
|
131
173
|
/**
|
|
@@ -141,6 +183,12 @@ class AppElement extends AsyncElement {
|
|
|
141
183
|
}
|
|
142
184
|
|
|
143
185
|
async connectedCallback() {
|
|
186
|
+
// Created before the first await, so the bar is visible while modules and the graphics
|
|
187
|
+
// device are created, and exists before any disconnect could need to clean it up
|
|
188
|
+
if (this._loadingBar && !this._bar) {
|
|
189
|
+
this._bar = new LoadingBar(this);
|
|
190
|
+
}
|
|
191
|
+
|
|
144
192
|
// Get all pc-module elements that are direct children of the pc-app element
|
|
145
193
|
const moduleElements = this.querySelectorAll<ModuleElement>(':scope > pc-module');
|
|
146
194
|
|
|
@@ -159,15 +207,21 @@ class AppElement extends AsyncElement {
|
|
|
159
207
|
};
|
|
160
208
|
const deviceTypes = backendToDeviceTypes[this._backend] || [];
|
|
161
209
|
|
|
210
|
+
this._optionsLocked = true;
|
|
211
|
+
|
|
162
212
|
const device = await createGraphicsDevice(this._canvas, {
|
|
163
213
|
// @ts-ignore - alpha needs to be documented
|
|
164
214
|
alpha: this._alpha,
|
|
165
215
|
antialias: this._antialias,
|
|
166
|
-
depth: this.
|
|
216
|
+
depth: this._depthBuffer,
|
|
167
217
|
deviceTypes: deviceTypes,
|
|
168
|
-
stencil: this.
|
|
218
|
+
stencil: this._stencilBuffer
|
|
169
219
|
});
|
|
170
|
-
|
|
220
|
+
|
|
221
|
+
// Assigned rather than resolved to a number here: the engine caps against the live
|
|
222
|
+
// window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
|
|
223
|
+
// display when a window moves between monitors of differing density.
|
|
224
|
+
device.maxPixelRatio = this._maxPixelRatio;
|
|
171
225
|
|
|
172
226
|
const createOptions = new AppOptions();
|
|
173
227
|
createOptions.graphicsDevice = device;
|
|
@@ -234,11 +288,12 @@ class AppElement extends AsyncElement {
|
|
|
234
288
|
createOptions.batchManager = BatchManager;
|
|
235
289
|
createOptions.xr = XrManager;
|
|
236
290
|
|
|
237
|
-
|
|
238
|
-
this.app
|
|
291
|
+
const app = new AppBase(this._canvas);
|
|
292
|
+
this._app = app;
|
|
293
|
+
app.init(createOptions);
|
|
239
294
|
|
|
240
|
-
|
|
241
|
-
|
|
295
|
+
app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
|
|
296
|
+
app.setCanvasResolution(RESOLUTION_AUTO);
|
|
242
297
|
|
|
243
298
|
this._pickerCreate();
|
|
244
299
|
|
|
@@ -248,7 +303,7 @@ class AppElement extends AsyncElement {
|
|
|
248
303
|
assetElement.createAsset();
|
|
249
304
|
const asset = assetElement.asset;
|
|
250
305
|
if (asset) {
|
|
251
|
-
|
|
306
|
+
app.assets.add(asset);
|
|
252
307
|
}
|
|
253
308
|
});
|
|
254
309
|
|
|
@@ -261,20 +316,46 @@ class AppElement extends AsyncElement {
|
|
|
261
316
|
// Create all entities
|
|
262
317
|
const entityElements = this.querySelectorAll<EntityElement>('pc-entity');
|
|
263
318
|
Array.from(entityElements).forEach((entityElement) => {
|
|
264
|
-
entityElement.createEntity(
|
|
319
|
+
entityElement.createEntity(app);
|
|
265
320
|
});
|
|
266
321
|
|
|
267
322
|
// Build hierarchy
|
|
268
323
|
entityElements.forEach((entityElement) => {
|
|
269
|
-
entityElement.buildHierarchy(
|
|
324
|
+
entityElement.buildHierarchy(app);
|
|
270
325
|
});
|
|
271
326
|
|
|
272
327
|
this._hierarchyReady = true;
|
|
273
328
|
|
|
329
|
+
// Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
|
|
330
|
+
// listener must be attached before preload() is called: an asset that is already loaded
|
|
331
|
+
// ticks synchronously inside it.
|
|
332
|
+
const total = app.assets.list({ preload: true }).length;
|
|
333
|
+
let loaded = 0;
|
|
334
|
+
const onPreloadProgress = () => {
|
|
335
|
+
loaded += 1;
|
|
336
|
+
this._loadProgress = loaded / total;
|
|
337
|
+
this._bar?.progress(loaded, total);
|
|
338
|
+
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded, total }));
|
|
339
|
+
};
|
|
340
|
+
app.on('preload:progress', onPreloadProgress);
|
|
341
|
+
|
|
342
|
+
this._loadProgress = total === 0 ? 1 : 0;
|
|
343
|
+
this._bar?.progress(0, total);
|
|
344
|
+
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
|
|
345
|
+
|
|
274
346
|
// Load assets before starting the application
|
|
275
|
-
|
|
347
|
+
app.preload(() => {
|
|
348
|
+
// Scope the counter to this preload pass, so a later app.preload() call by user code
|
|
349
|
+
// cannot push `loaded` past `total`
|
|
350
|
+
app.off('preload:progress', onPreloadProgress);
|
|
351
|
+
this._loadProgress = 1;
|
|
352
|
+
|
|
276
353
|
// Start the application
|
|
277
|
-
|
|
354
|
+
app.start();
|
|
355
|
+
|
|
356
|
+
// Dismiss the bar only once a frame has actually rendered; ready fires before the
|
|
357
|
+
// first rAF tick
|
|
358
|
+
app.once('frameend', () => this._bar?.complete());
|
|
278
359
|
|
|
279
360
|
// Handle window resize to keep the canvas responsive
|
|
280
361
|
window.addEventListener('resize', this._onWindowResize);
|
|
@@ -284,13 +365,19 @@ class AppElement extends AsyncElement {
|
|
|
284
365
|
}
|
|
285
366
|
|
|
286
367
|
disconnectedCallback() {
|
|
368
|
+
this._optionsLocked = false;
|
|
287
369
|
this._pickerDestroy();
|
|
288
370
|
|
|
289
|
-
// Clean up the application
|
|
290
|
-
|
|
291
|
-
|
|
371
|
+
// Clean up the application. Destroying it destroys every entity, whose destroy hooks
|
|
372
|
+
// unregister them - clear() covers any entity the engine no longer reached.
|
|
373
|
+
if (this._app) {
|
|
374
|
+
this._app.destroy();
|
|
292
375
|
this._app = null;
|
|
293
376
|
}
|
|
377
|
+
this._entityElements.clear();
|
|
378
|
+
this._loadProgress = 0;
|
|
379
|
+
this._bar?.destroy();
|
|
380
|
+
this._bar = null;
|
|
294
381
|
|
|
295
382
|
// Remove event listeners
|
|
296
383
|
window.removeEventListener('resize', this._onWindowResize);
|
|
@@ -312,10 +399,18 @@ class AppElement extends AsyncElement {
|
|
|
312
399
|
const { width, height } = this.app!.graphicsDevice;
|
|
313
400
|
this._picker = new Picker(this.app!, width, height);
|
|
314
401
|
|
|
315
|
-
// Create bound handlers but don't attach them yet
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
402
|
+
// Create bound handlers but don't attach them yet. The handlers pick asynchronously, so
|
|
403
|
+
// each is wrapped to discard the promise - a listener must not return one, and nothing
|
|
404
|
+
// awaits the result.
|
|
405
|
+
const listener = (handler: (event: PointerEvent) => Promise<void>): EventListener => {
|
|
406
|
+
return (event: Event) => {
|
|
407
|
+
handler.call(this, event as PointerEvent);
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
this._pointerHandlers.pointermove = listener(this._onPointerMove);
|
|
412
|
+
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
413
|
+
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
319
414
|
|
|
320
415
|
// Listen for pointer listeners being added/removed
|
|
321
416
|
['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
|
|
@@ -356,6 +451,79 @@ class AppElement extends AsyncElement {
|
|
|
356
451
|
};
|
|
357
452
|
}
|
|
358
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Registers the element that created an entity. Called by EntityElement when it creates its
|
|
456
|
+
* entity.
|
|
457
|
+
*
|
|
458
|
+
* @param entity - The entity.
|
|
459
|
+
* @param element - The element that created it.
|
|
460
|
+
* @ignore
|
|
461
|
+
*/
|
|
462
|
+
_registerEntityElement(entity: Entity, element: EntityElement) {
|
|
463
|
+
this._entityElements.set(entity, element);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Removes the registration for a destroyed entity. Called by EntityElement.
|
|
468
|
+
*
|
|
469
|
+
* @param entity - The entity.
|
|
470
|
+
* @ignore
|
|
471
|
+
*/
|
|
472
|
+
_unregisterEntityElement(entity: Entity) {
|
|
473
|
+
this._entityElements.delete(entity);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Returns the `<pc-entity>` element whose backing entity is `entity`, or `null` if the
|
|
478
|
+
* entity was not created by an element of this application - for example, a node inside a
|
|
479
|
+
* model's instantiated hierarchy, or an entity created through the engine API.
|
|
480
|
+
*
|
|
481
|
+
* @param entity - The entity to look up.
|
|
482
|
+
* @returns The element backing the entity, or `null`.
|
|
483
|
+
*/
|
|
484
|
+
elementFromEntity(entity: Entity): EntityElement | null {
|
|
485
|
+
return this._entityElements.get(entity) ?? null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Resolves the element that owns a picked node: the nearest node up the parent chain -
|
|
490
|
+
* starting with the node itself - that was created by a `<pc-entity>` of this application.
|
|
491
|
+
* A hit inside a model's instantiated hierarchy therefore resolves to the element hosting
|
|
492
|
+
* the model.
|
|
493
|
+
*
|
|
494
|
+
* @param node - The picked node, or `null`.
|
|
495
|
+
* @returns The owning element, or `null`.
|
|
496
|
+
*/
|
|
497
|
+
private _elementFromNode(node: GraphNode | null): EntityElement | null {
|
|
498
|
+
while (node !== null) {
|
|
499
|
+
const element = this._entityElements.get(node);
|
|
500
|
+
if (element) {
|
|
501
|
+
return element;
|
|
502
|
+
}
|
|
503
|
+
node = node.parent;
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Like {@link _elementFromNode}, but skips elements without a listener for `type`, so a hit
|
|
510
|
+
* on an unlistened child still reaches a listening ancestor.
|
|
511
|
+
*
|
|
512
|
+
* @param node - The picked node, or `null`.
|
|
513
|
+
* @param type - The pointer event type a listener is required for.
|
|
514
|
+
* @returns The nearest listening element, or `null`.
|
|
515
|
+
*/
|
|
516
|
+
private _elementWithListener(node: GraphNode | null, type: string): EntityElement | null {
|
|
517
|
+
while (node !== null) {
|
|
518
|
+
const element = this._entityElements.get(node);
|
|
519
|
+
if (element?.hasListeners(type)) {
|
|
520
|
+
return element;
|
|
521
|
+
}
|
|
522
|
+
node = node.parent;
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
359
527
|
// New helper to convert CSS coordinates to canvas (picker) coordinates
|
|
360
528
|
private _getPickerCoordinates(event: PointerEvent): { x: number, y: number } {
|
|
361
529
|
// Get the canvas' bounding rectangle in CSS pixels.
|
|
@@ -369,32 +537,44 @@ class AppElement extends AsyncElement {
|
|
|
369
537
|
return { x, y };
|
|
370
538
|
}
|
|
371
539
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
540
|
+
/**
|
|
541
|
+
* Picks the scene under the pointer and returns the graph node that was hit, or `null`.
|
|
542
|
+
*
|
|
543
|
+
* The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
|
|
544
|
+
* supported on WebGPU, where it returns an empty selection rather than failing - which
|
|
545
|
+
* silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
|
|
546
|
+
* async variant works on both backends and does not block the main thread on a GPU read.
|
|
547
|
+
*
|
|
548
|
+
* @param event - The pointer event to pick under.
|
|
549
|
+
* @returns The graph node under the pointer, or `null` if nothing was hit.
|
|
550
|
+
*/
|
|
551
|
+
private async _pickNode(event: PointerEvent): Promise<GraphNode | null> {
|
|
375
552
|
const camera = this.app!.root.findComponent('camera') as CameraComponent;
|
|
376
|
-
if (!camera) return;
|
|
553
|
+
if (!camera) return null;
|
|
377
554
|
|
|
378
|
-
// Use the helper to convert event coordinates into canvas/picker coordinates.
|
|
379
555
|
const { x, y } = this._getPickerCoordinates(event);
|
|
380
556
|
|
|
381
|
-
this._picker
|
|
382
|
-
const selection = this._picker
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
557
|
+
this._picker!.prepare(camera, this.app!.scene);
|
|
558
|
+
const selection = await this._picker!.getSelectionAsync(x, y);
|
|
559
|
+
if (selection.length === 0) return null;
|
|
560
|
+
|
|
561
|
+
const item = selection[0];
|
|
562
|
+
return item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async _onPointerMove(event: PointerEvent) {
|
|
566
|
+
if (!this._picker || !this.app) return;
|
|
567
|
+
|
|
568
|
+
// Moves arrive faster than a pick resolves, so results can land out of order. Only the
|
|
569
|
+
// newest pick may update the hover state - an older one describes a pointer position the
|
|
570
|
+
// user has already left.
|
|
571
|
+
const token = ++this._pickToken;
|
|
572
|
+
const node = await this._pickNode(event);
|
|
573
|
+
if (token !== this._pickToken || !this._picker) return;
|
|
574
|
+
|
|
575
|
+
// The hovered element is the nearest one up the node's parent chain, listening or not -
|
|
576
|
+
// dispatch is gated per event type below
|
|
577
|
+
const newHoverEntity = this._elementFromNode(node);
|
|
398
578
|
|
|
399
579
|
// Handle enter/leave events
|
|
400
580
|
if (this._hoveredEntity !== newHoverEntity) {
|
|
@@ -415,51 +595,27 @@ class AppElement extends AsyncElement {
|
|
|
415
595
|
}
|
|
416
596
|
}
|
|
417
597
|
|
|
418
|
-
_onPointerDown(event: PointerEvent) {
|
|
598
|
+
async _onPointerDown(event: PointerEvent) {
|
|
419
599
|
if (!this._picker || !this.app) return;
|
|
420
600
|
|
|
421
|
-
const
|
|
422
|
-
if (!
|
|
423
|
-
|
|
424
|
-
// Convert the event's pointer coordinates
|
|
425
|
-
const { x, y } = this._getPickerCoordinates(event);
|
|
601
|
+
const node = await this._pickNode(event);
|
|
602
|
+
if (!this._picker) return; // the element disconnected while the pick was in flight
|
|
426
603
|
|
|
427
|
-
this.
|
|
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;
|
|
440
|
-
}
|
|
604
|
+
const entityElement = this._elementWithListener(node, 'pointerdown');
|
|
605
|
+
if (entityElement) {
|
|
606
|
+
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
441
607
|
}
|
|
442
608
|
}
|
|
443
609
|
|
|
444
|
-
_onPointerUp(event: PointerEvent) {
|
|
610
|
+
async _onPointerUp(event: PointerEvent) {
|
|
445
611
|
if (!this._picker || !this.app) return;
|
|
446
612
|
|
|
447
|
-
const
|
|
448
|
-
if (!
|
|
449
|
-
|
|
450
|
-
// Convert CSS coordinates to picker coordinates
|
|
451
|
-
const { x, y } = this._getPickerCoordinates(event);
|
|
452
|
-
|
|
453
|
-
this._picker.prepare(camera, this.app!.scene);
|
|
454
|
-
const selection = this._picker.getSelection(x, y);
|
|
613
|
+
const node = await this._pickNode(event);
|
|
614
|
+
if (!this._picker) return; // the element disconnected while the pick was in flight
|
|
455
615
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
}
|
|
616
|
+
const entityElement = this._elementWithListener(node, 'pointerup');
|
|
617
|
+
if (entityElement) {
|
|
618
|
+
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
463
619
|
}
|
|
464
620
|
}
|
|
465
621
|
|
|
@@ -496,15 +652,30 @@ class AppElement extends AsyncElement {
|
|
|
496
652
|
}
|
|
497
653
|
|
|
498
654
|
/**
|
|
499
|
-
*
|
|
655
|
+
* Warns that a graphics option was written too late to have any effect. These options are read
|
|
656
|
+
* once, when the element connects and creates its graphics device, so a later write updates
|
|
657
|
+
* only the element's own property - silently, without this.
|
|
658
|
+
*
|
|
659
|
+
* @param name - The name of the option, as its attribute.
|
|
660
|
+
*/
|
|
661
|
+
private _warnIfBooted(name: string) {
|
|
662
|
+
if (this._optionsLocked) {
|
|
663
|
+
console.warn(`Attribute '${name}' on <pc-app> is only read when the application boots, so this change has no effect. Set it before the element is connected, or remove and re-insert the element to reboot with the new value.`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Sets whether the frame buffer has an alpha channel, which is what lets the page show through
|
|
669
|
+
* wherever the scene has not drawn. Read only when the application boots.
|
|
500
670
|
* @param value - The alpha flag.
|
|
501
671
|
*/
|
|
502
672
|
set alpha(value: boolean) {
|
|
673
|
+
this._warnIfBooted('alpha');
|
|
503
674
|
this._alpha = value;
|
|
504
675
|
}
|
|
505
676
|
|
|
506
677
|
/**
|
|
507
|
-
* Gets the alpha
|
|
678
|
+
* Gets whether the frame buffer has an alpha channel.
|
|
508
679
|
* @returns The alpha flag.
|
|
509
680
|
*/
|
|
510
681
|
get alpha() {
|
|
@@ -512,15 +683,16 @@ class AppElement extends AsyncElement {
|
|
|
512
683
|
}
|
|
513
684
|
|
|
514
685
|
/**
|
|
515
|
-
* Sets the
|
|
686
|
+
* Sets whether the frame buffer is anti-aliased. Read only when the application boots.
|
|
516
687
|
* @param value - The antialias flag.
|
|
517
688
|
*/
|
|
518
689
|
set antialias(value: boolean) {
|
|
690
|
+
this._warnIfBooted('antialias');
|
|
519
691
|
this._antialias = value;
|
|
520
692
|
}
|
|
521
693
|
|
|
522
694
|
/**
|
|
523
|
-
* Gets the
|
|
695
|
+
* Gets whether the frame buffer is anti-aliased.
|
|
524
696
|
* @returns The antialias flag.
|
|
525
697
|
*/
|
|
526
698
|
get antialias() {
|
|
@@ -529,10 +701,11 @@ class AppElement extends AsyncElement {
|
|
|
529
701
|
|
|
530
702
|
/**
|
|
531
703
|
* Sets the graphics backend. Defaults to 'webgpu', which falls back to 'webgl2' if WebGPU
|
|
532
|
-
* is not supported by the browser.
|
|
704
|
+
* is not supported by the browser. Read only when the application boots.
|
|
533
705
|
* @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
|
|
534
706
|
*/
|
|
535
707
|
set backend(value: 'webgpu' | 'webgl2' | 'null') {
|
|
708
|
+
this._warnIfBooted('backend');
|
|
536
709
|
this._backend = value;
|
|
537
710
|
}
|
|
538
711
|
|
|
@@ -545,19 +718,21 @@ class AppElement extends AsyncElement {
|
|
|
545
718
|
}
|
|
546
719
|
|
|
547
720
|
/**
|
|
548
|
-
* Sets the depth
|
|
549
|
-
*
|
|
721
|
+
* Sets whether the frame buffer has a depth buffer, which the renderer needs to resolve which
|
|
722
|
+
* surface is nearest the camera. Read only when the application boots.
|
|
723
|
+
* @param value - The depth buffer flag.
|
|
550
724
|
*/
|
|
551
|
-
set
|
|
552
|
-
this.
|
|
725
|
+
set depthBuffer(value: boolean) {
|
|
726
|
+
this._warnIfBooted('depth-buffer');
|
|
727
|
+
this._depthBuffer = value;
|
|
553
728
|
}
|
|
554
729
|
|
|
555
730
|
/**
|
|
556
|
-
* Gets the depth
|
|
557
|
-
* @returns The depth flag.
|
|
731
|
+
* Gets whether the frame buffer has a depth buffer.
|
|
732
|
+
* @returns The depth buffer flag.
|
|
558
733
|
*/
|
|
559
|
-
get
|
|
560
|
-
return this.
|
|
734
|
+
get depthBuffer() {
|
|
735
|
+
return this._depthBuffer;
|
|
561
736
|
}
|
|
562
737
|
|
|
563
738
|
/**
|
|
@@ -570,43 +745,74 @@ class AppElement extends AsyncElement {
|
|
|
570
745
|
}
|
|
571
746
|
|
|
572
747
|
/**
|
|
573
|
-
* Sets the
|
|
574
|
-
*
|
|
575
|
-
*
|
|
748
|
+
* Sets whether the application shows its built-in loading bar while it boots and preloads its
|
|
749
|
+
* assets. Enabled by default; setting `false` removes the bar immediately, while setting
|
|
750
|
+
* `true` has no effect until the element is next connected. The bar can be themed with the
|
|
751
|
+
* CSS custom properties `--pc-loading-bar-color`, `--pc-loading-bar-background` and
|
|
752
|
+
* `--pc-loading-bar-height`.
|
|
753
|
+
* @param value - The loading bar flag.
|
|
754
|
+
*/
|
|
755
|
+
set loadingBar(value: boolean) {
|
|
756
|
+
this._loadingBar = value;
|
|
757
|
+
if (!value && this._bar) {
|
|
758
|
+
this._bar.destroy();
|
|
759
|
+
this._bar = null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Gets whether the application shows its built-in loading bar while it boots and preloads
|
|
765
|
+
* its assets.
|
|
766
|
+
* @returns The loading bar flag.
|
|
576
767
|
*/
|
|
577
|
-
|
|
578
|
-
this.
|
|
768
|
+
get loadingBar() {
|
|
769
|
+
return this._loadingBar;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Sets the cap on the pixel ratio the application renders at. The canvas is sized by the
|
|
774
|
+
* smaller of this value and the display's own device pixel ratio, so the default of `Infinity`
|
|
775
|
+
* renders at full physical resolution, `1` renders at CSS resolution, and an intermediate
|
|
776
|
+
* value such as `2` keeps a dense display sharp without paying for every one of its pixels.
|
|
777
|
+
* Must be greater than 0. Unlike the other graphics options, this applies immediately.
|
|
778
|
+
* @param value - The maximum pixel ratio.
|
|
779
|
+
*/
|
|
780
|
+
set maxPixelRatio(value: number) {
|
|
781
|
+
this._maxPixelRatio = value;
|
|
579
782
|
if (this.app) {
|
|
580
|
-
this.app.graphicsDevice.maxPixelRatio = value
|
|
783
|
+
this.app.graphicsDevice.maxPixelRatio = value;
|
|
784
|
+
this.app.resizeCanvas();
|
|
581
785
|
}
|
|
582
786
|
}
|
|
583
787
|
|
|
584
788
|
/**
|
|
585
|
-
* Gets the
|
|
586
|
-
* @returns The
|
|
789
|
+
* Gets the cap on the pixel ratio the application renders at.
|
|
790
|
+
* @returns The maximum pixel ratio.
|
|
587
791
|
*/
|
|
588
|
-
get
|
|
589
|
-
return this.
|
|
792
|
+
get maxPixelRatio() {
|
|
793
|
+
return this._maxPixelRatio;
|
|
590
794
|
}
|
|
591
795
|
|
|
592
796
|
/**
|
|
593
|
-
* Sets the stencil
|
|
594
|
-
*
|
|
797
|
+
* Sets whether the frame buffer has a stencil buffer, which stencil-based effects and UI
|
|
798
|
+
* masking need. Read only when the application boots.
|
|
799
|
+
* @param value - The stencil buffer flag.
|
|
595
800
|
*/
|
|
596
|
-
set
|
|
597
|
-
this.
|
|
801
|
+
set stencilBuffer(value: boolean) {
|
|
802
|
+
this._warnIfBooted('stencil-buffer');
|
|
803
|
+
this._stencilBuffer = value;
|
|
598
804
|
}
|
|
599
805
|
|
|
600
806
|
/**
|
|
601
|
-
* Gets the stencil
|
|
602
|
-
* @returns The stencil flag.
|
|
807
|
+
* Gets whether the frame buffer has a stencil buffer.
|
|
808
|
+
* @returns The stencil buffer flag.
|
|
603
809
|
*/
|
|
604
|
-
get
|
|
605
|
-
return this.
|
|
810
|
+
get stencilBuffer() {
|
|
811
|
+
return this._stencilBuffer;
|
|
606
812
|
}
|
|
607
813
|
|
|
608
814
|
static get observedAttributes() {
|
|
609
|
-
return ['alpha', 'antialias', 'backend', 'depth', '
|
|
815
|
+
return ['alpha', 'antialias', 'backend', 'depth-buffer', 'loading-bar', 'max-pixel-ratio', 'stencil-buffer'];
|
|
610
816
|
}
|
|
611
817
|
|
|
612
818
|
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
@@ -620,14 +826,17 @@ class AppElement extends AsyncElement {
|
|
|
620
826
|
case 'backend':
|
|
621
827
|
this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgpu', name);
|
|
622
828
|
break;
|
|
623
|
-
case 'depth':
|
|
624
|
-
this.
|
|
829
|
+
case 'depth-buffer':
|
|
830
|
+
this.depthBuffer = parseBool(newValue, true);
|
|
831
|
+
break;
|
|
832
|
+
case 'loading-bar':
|
|
833
|
+
this.loadingBar = parseBool(newValue, true);
|
|
625
834
|
break;
|
|
626
|
-
case '
|
|
627
|
-
this.
|
|
835
|
+
case 'max-pixel-ratio':
|
|
836
|
+
this.maxPixelRatio = parseNumber(newValue, Infinity, name);
|
|
628
837
|
break;
|
|
629
|
-
case 'stencil':
|
|
630
|
-
this.
|
|
838
|
+
case 'stencil-buffer':
|
|
839
|
+
this.stencilBuffer = parseBool(newValue, true);
|
|
631
840
|
break;
|
|
632
841
|
}
|
|
633
842
|
}
|