@playcanvas/web-components 0.10.1 → 0.11.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 +1 -1
- package/dist/app.d.ts +102 -51
- package/dist/asset.d.ts +8 -7
- package/dist/async-element.d.ts +21 -5
- package/dist/components/button-component.d.ts +3 -7
- package/dist/components/camera-component.d.ts +16 -20
- package/dist/components/collision-component.d.ts +3 -7
- package/dist/components/component.d.ts +22 -4
- package/dist/components/element-component.d.ts +4 -8
- package/dist/components/gsplat-component.d.ts +2 -7
- package/dist/components/layoutchild-component.d.ts +2 -7
- package/dist/components/layoutgroup-component.d.ts +3 -7
- package/dist/components/light-component.d.ts +3 -7
- package/dist/components/listener-component.d.ts +1 -6
- package/dist/components/particlesystem-component.d.ts +2 -7
- package/dist/components/render-component.d.ts +2 -7
- package/dist/components/rigidbody-component.d.ts +3 -7
- package/dist/components/screen-component.d.ts +28 -11
- package/dist/components/script-component.d.ts +8 -20
- package/dist/components/script.d.ts +2 -22
- package/dist/components/scrollbar-component.d.ts +2 -7
- package/dist/components/scrollview-component.d.ts +9 -11
- package/dist/components/sound-component.d.ts +2 -7
- package/dist/components/sound-slot.d.ts +8 -6
- package/dist/custom-elements.json +5191 -10695
- package/dist/entity.d.ts +16 -9
- package/dist/index.d.ts +37 -0
- package/dist/material.d.ts +12 -12
- package/dist/model.d.ts +21 -5
- package/dist/module.d.ts +0 -6
- package/dist/parse.d.ts +6 -3
- package/dist/pwc.cjs +799 -287
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +799 -287
- 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 +800 -288
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.ts +4 -7
- package/dist/sky.d.ts +25 -16
- package/dist/vscode.html-custom-data.json +65 -45
- package/dist/web-types.json +585 -557
- package/package.json +8 -7
- package/src/app.ts +326 -144
- package/src/asset.ts +34 -28
- package/src/async-element.ts +34 -8
- package/src/components/button-component.ts +5 -9
- package/src/components/camera-component.ts +55 -36
- package/src/components/collision-component.ts +26 -15
- package/src/components/component.ts +58 -8
- package/src/components/element-component.ts +26 -30
- package/src/components/gsplat-component.ts +4 -9
- package/src/components/layoutchild-component.ts +4 -9
- package/src/components/layoutgroup-component.ts +14 -9
- package/src/components/light-component.ts +42 -12
- package/src/components/listener-component.ts +1 -7
- package/src/components/particlesystem-component.ts +7 -15
- package/src/components/render-component.ts +5 -10
- package/src/components/rigidbody-component.ts +23 -16
- package/src/components/screen-component.ts +46 -20
- package/src/components/script-component.ts +108 -46
- package/src/components/script.ts +38 -33
- package/src/components/scrollbar-component.ts +6 -16
- package/src/components/scrollview-component.ts +22 -15
- package/src/components/sound-component.ts +10 -15
- package/src/components/sound-slot.ts +30 -20
- package/src/entity.ts +75 -34
- package/src/index.ts +45 -1
- package/src/loading-bar.ts +8 -8
- package/src/material.ts +63 -37
- package/src/model.ts +69 -14
- package/src/module.ts +8 -7
- package/src/parse.ts +67 -20
- package/src/scene.ts +12 -9
- package/src/sky.ts +76 -34
package/src/app.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import type { CameraComponent, GraphNode, GSplatComponent, Entity } from 'playcanvas';
|
|
1
2
|
import {
|
|
2
3
|
AppBase,
|
|
3
4
|
AppOptions,
|
|
4
|
-
CameraComponent,
|
|
5
5
|
createGraphicsDevice,
|
|
6
6
|
ElementInput,
|
|
7
7
|
FILLMODE_FILL_WINDOW,
|
|
8
|
-
GraphNode,
|
|
9
8
|
Keyboard,
|
|
10
9
|
Mouse,
|
|
11
10
|
Picker,
|
|
@@ -62,17 +61,19 @@ import {
|
|
|
62
61
|
SoundManager,
|
|
63
62
|
Lightmapper,
|
|
64
63
|
XrManager,
|
|
65
|
-
MeshInstance
|
|
66
|
-
GSplatComponent
|
|
64
|
+
MeshInstance
|
|
67
65
|
} from 'playcanvas';
|
|
68
66
|
|
|
69
|
-
import { AssetElement } from './asset';
|
|
67
|
+
import type { AssetElement } from './asset';
|
|
70
68
|
import { AsyncElement } from './async-element';
|
|
71
|
-
import { EntityElement } from './entity';
|
|
69
|
+
import type { EntityElement } from './entity';
|
|
72
70
|
import { LoadingBar } from './loading-bar';
|
|
73
|
-
import { MaterialElement } from './material';
|
|
74
|
-
import { ModuleElement } from './module';
|
|
75
|
-
import { parseBool, parseEnum } from './parse';
|
|
71
|
+
import type { MaterialElement } from './material';
|
|
72
|
+
import type { ModuleElement } from './module';
|
|
73
|
+
import { parseBool, parseEnum, parseNumber } from './parse';
|
|
74
|
+
|
|
75
|
+
/** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
|
|
76
|
+
const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'] as const;
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
79
|
* The AppElement interface provides properties and methods for manipulating
|
|
@@ -97,21 +98,49 @@ class AppElement extends AsyncElement {
|
|
|
97
98
|
|
|
98
99
|
private _antialias = true;
|
|
99
100
|
|
|
100
|
-
private
|
|
101
|
+
private _depthBuffer = true;
|
|
101
102
|
|
|
102
|
-
private
|
|
103
|
+
private _stencilBuffer = true;
|
|
103
104
|
|
|
104
|
-
private
|
|
105
|
+
private _maxPixelRatio = Infinity;
|
|
105
106
|
|
|
106
107
|
private _loadingBar = true;
|
|
107
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
|
+
|
|
108
116
|
private _bar: LoadingBar | null = null;
|
|
109
117
|
|
|
110
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Whether the application has created its initial entity hierarchy. Read by EntityElement to
|
|
120
|
+
* decide whether a newly connected element must create its entity itself or leave it to the
|
|
121
|
+
* boot sweep.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
_hierarchyReady = false;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Incremented on every connect and disconnect. Boot captures the value on entry and abandons
|
|
128
|
+
* itself wherever it resumes from an await if the value has moved on — so a boot whose
|
|
129
|
+
* element was removed cannot complete against a torn-down element, and a boot whose element
|
|
130
|
+
* was removed and re-inserted (which starts a boot of its own) cannot race the newer one.
|
|
131
|
+
*/
|
|
132
|
+
private _bootGeneration = 0;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The elements backing this application's entities, keyed by the entity itself. Registered
|
|
136
|
+
* by EntityElement at creation and removed when an entity is destroyed, this joins engine
|
|
137
|
+
* scene nodes back to their owning elements by identity - never by name.
|
|
138
|
+
*/
|
|
139
|
+
private _entityElements = new Map<GraphNode, EntityElement>();
|
|
111
140
|
|
|
112
141
|
private _picker: Picker | null = null;
|
|
113
142
|
|
|
114
|
-
private _hasPointerListeners:
|
|
143
|
+
private _hasPointerListeners: Record<string, boolean> = {
|
|
115
144
|
pointerenter: false,
|
|
116
145
|
pointerleave: false,
|
|
117
146
|
pointerdown: false,
|
|
@@ -124,7 +153,7 @@ class AppElement extends AsyncElement {
|
|
|
124
153
|
// Identifies the newest in-flight hover pick, so out-of-order results can be discarded
|
|
125
154
|
private _pickToken = 0;
|
|
126
155
|
|
|
127
|
-
private _pointerHandlers:
|
|
156
|
+
private _pointerHandlers: Record<string, EventListener | null> = {
|
|
128
157
|
pointermove: null,
|
|
129
158
|
pointerdown: null,
|
|
130
159
|
pointerup: null
|
|
@@ -165,9 +194,19 @@ class AppElement extends AsyncElement {
|
|
|
165
194
|
|
|
166
195
|
// Bind methods to maintain 'this' context
|
|
167
196
|
this._onWindowResize = this._onWindowResize.bind(this);
|
|
197
|
+
|
|
198
|
+
// Track pointer listeners being added to and removed from descendant entities.
|
|
199
|
+
// Registered once here rather than on every boot - the handlers no-op while there is no
|
|
200
|
+
// canvas, and a re-booted element must not stack a second set.
|
|
201
|
+
pointerEventTypes.forEach((type) => {
|
|
202
|
+
this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
|
|
203
|
+
this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
|
|
204
|
+
});
|
|
168
205
|
}
|
|
169
206
|
|
|
170
207
|
async connectedCallback() {
|
|
208
|
+
const generation = ++this._bootGeneration;
|
|
209
|
+
|
|
171
210
|
// Created before the first await, so the bar is visible while modules and the graphics
|
|
172
211
|
// device are created, and exists before any disconnect could need to clean it up
|
|
173
212
|
if (this._loadingBar && !this._bar) {
|
|
@@ -178,29 +217,49 @@ class AppElement extends AsyncElement {
|
|
|
178
217
|
const moduleElements = this.querySelectorAll<ModuleElement>(':scope > pc-module');
|
|
179
218
|
|
|
180
219
|
// Wait for all modules to load
|
|
181
|
-
await Promise.all(Array.from(moduleElements).map(module => module.
|
|
220
|
+
await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
|
|
221
|
+
|
|
222
|
+
// The element may have been removed while the modules loaded. Nothing beyond the loading
|
|
223
|
+
// bar exists yet, and disconnectedCallback has already destroyed that.
|
|
224
|
+
if (generation !== this._bootGeneration) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
182
227
|
|
|
183
228
|
// Create and append the canvas to the element
|
|
184
229
|
this._canvas = document.createElement('canvas');
|
|
185
230
|
this.appendChild(this._canvas);
|
|
186
231
|
|
|
187
232
|
// Configure device types based on backend selection
|
|
188
|
-
const backendToDeviceTypes:
|
|
233
|
+
const backendToDeviceTypes: Record<string, string[]> = {
|
|
189
234
|
webgpu: ['webgpu', 'webgl2'], // fallback to webgl2 if webgpu not available
|
|
190
235
|
webgl2: ['webgl2'],
|
|
191
236
|
null: ['null']
|
|
192
237
|
};
|
|
193
238
|
const deviceTypes = backendToDeviceTypes[this._backend] || [];
|
|
194
239
|
|
|
240
|
+
this._optionsLocked = true;
|
|
241
|
+
|
|
195
242
|
const device = await createGraphicsDevice(this._canvas, {
|
|
196
243
|
// @ts-ignore - alpha needs to be documented
|
|
197
244
|
alpha: this._alpha,
|
|
198
245
|
antialias: this._antialias,
|
|
199
|
-
depth: this.
|
|
246
|
+
depth: this._depthBuffer,
|
|
200
247
|
deviceTypes: deviceTypes,
|
|
201
|
-
stencil: this.
|
|
248
|
+
stencil: this._stencilBuffer
|
|
202
249
|
});
|
|
203
|
-
|
|
250
|
+
|
|
251
|
+
// The element may have been removed while the device was created. disconnectedCallback
|
|
252
|
+
// has already cleaned up the canvas; the device was created inside the await, so it is
|
|
253
|
+
// this boot's to release.
|
|
254
|
+
if (generation !== this._bootGeneration) {
|
|
255
|
+
device.destroy();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Assigned rather than resolved to a number here: the engine caps against the live
|
|
260
|
+
// window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
|
|
261
|
+
// display when a window moves between monitors of differing density.
|
|
262
|
+
device.maxPixelRatio = this._maxPixelRatio;
|
|
204
263
|
|
|
205
264
|
const createOptions = new AppOptions();
|
|
206
265
|
createOptions.graphicsDevice = device;
|
|
@@ -278,31 +337,47 @@ class AppElement extends AsyncElement {
|
|
|
278
337
|
|
|
279
338
|
// Get all pc-asset elements that are direct children of the pc-app element
|
|
280
339
|
const assetElements = this.querySelectorAll<AssetElement>(':scope > pc-asset');
|
|
281
|
-
Array.from(assetElements)
|
|
282
|
-
assetElement.
|
|
340
|
+
for (const assetElement of Array.from(assetElements)) {
|
|
341
|
+
assetElement._createAsset();
|
|
283
342
|
const asset = assetElement.asset;
|
|
284
343
|
if (asset) {
|
|
285
344
|
app.assets.add(asset);
|
|
345
|
+
|
|
346
|
+
// Adding a fileless asset (one built purely from data, such as a sprite)
|
|
347
|
+
// completes it synchronously, dispatching the element's load event - whose
|
|
348
|
+
// listeners may have removed this element. Stop before the next addition
|
|
349
|
+
// reaches the destroyed registry, and before orphan entities are created.
|
|
350
|
+
if (generation !== this._bootGeneration) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
286
353
|
}
|
|
287
|
-
}
|
|
354
|
+
}
|
|
288
355
|
|
|
289
356
|
// Get all pc-material elements that are direct children of the pc-app element
|
|
290
357
|
const materialElements = this.querySelectorAll<MaterialElement>(':scope > pc-material');
|
|
291
358
|
Array.from(materialElements).forEach((materialElement) => {
|
|
292
|
-
materialElement.
|
|
359
|
+
materialElement._createMaterial();
|
|
293
360
|
});
|
|
294
361
|
|
|
295
362
|
// Create all entities
|
|
296
363
|
const entityElements = this.querySelectorAll<EntityElement>('pc-entity');
|
|
297
364
|
Array.from(entityElements).forEach((entityElement) => {
|
|
298
|
-
entityElement.
|
|
365
|
+
entityElement._createEntity(app);
|
|
299
366
|
});
|
|
300
367
|
|
|
301
368
|
// Build hierarchy
|
|
302
369
|
entityElements.forEach((entityElement) => {
|
|
303
|
-
entityElement.
|
|
370
|
+
entityElement._buildHierarchy(app);
|
|
304
371
|
});
|
|
305
372
|
|
|
373
|
+
// Building the hierarchy dispatched each entity's ready event synchronously, and a
|
|
374
|
+
// listener may have removed the element. The sweep itself degrades safely - destroying
|
|
375
|
+
// the application nulls every element's entity, so the remaining builds no-op - but the
|
|
376
|
+
// teardown's reset must not be overwritten here.
|
|
377
|
+
if (generation !== this._bootGeneration) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
306
381
|
this._hierarchyReady = true;
|
|
307
382
|
|
|
308
383
|
// Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
|
|
@@ -322,8 +397,21 @@ class AppElement extends AsyncElement {
|
|
|
322
397
|
this._bar?.progress(0, total);
|
|
323
398
|
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
|
|
324
399
|
|
|
400
|
+
// The progress dispatch above ran listeners synchronously, and one may have removed the
|
|
401
|
+
// element. The application is already destroyed - it must not be asked to preload.
|
|
402
|
+
if (generation !== this._bootGeneration) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
325
406
|
// Load assets before starting the application
|
|
326
407
|
app.preload(() => {
|
|
408
|
+
// The element may have been removed while assets loaded. The application is already
|
|
409
|
+
// destroyed, so it must not be started — and readiness must not be signaled for a
|
|
410
|
+
// boot that no longer owns the element.
|
|
411
|
+
if (generation !== this._bootGeneration) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
327
415
|
// Scope the counter to this preload pass, so a later app.preload() call by user code
|
|
328
416
|
// cannot push `loaded` past `total`
|
|
329
417
|
app.off('preload:progress', onPreloadProgress);
|
|
@@ -344,17 +432,30 @@ class AppElement extends AsyncElement {
|
|
|
344
432
|
}
|
|
345
433
|
|
|
346
434
|
disconnectedCallback() {
|
|
435
|
+
// Invalidate any boot still in flight, so it abandons itself when it next resumes
|
|
436
|
+
// instead of completing against a torn-down element.
|
|
437
|
+
this._bootGeneration++;
|
|
438
|
+
|
|
439
|
+
this._optionsLocked = false;
|
|
347
440
|
this._pickerDestroy();
|
|
348
441
|
|
|
349
|
-
// Clean up the application
|
|
442
|
+
// Clean up the application. Destroying it destroys every entity, whose destroy hooks
|
|
443
|
+
// unregister them - clear() covers any entity the engine no longer reached.
|
|
350
444
|
if (this._app) {
|
|
351
445
|
this._app.destroy();
|
|
352
446
|
this._app = null;
|
|
353
447
|
}
|
|
448
|
+
this._entityElements.clear();
|
|
354
449
|
this._loadProgress = 0;
|
|
355
450
|
this._bar?.destroy();
|
|
356
451
|
this._bar = null;
|
|
357
452
|
|
|
453
|
+
// Return the element to its pre-boot state, so re-inserting it boots afresh: descendants
|
|
454
|
+
// must neither see a hierarchy that no longer exists nor resume against a readiness that
|
|
455
|
+
// no longer holds.
|
|
456
|
+
this._hierarchyReady = false;
|
|
457
|
+
this._resetReady();
|
|
458
|
+
|
|
358
459
|
// Remove event listeners
|
|
359
460
|
window.removeEventListener('resize', this._onWindowResize);
|
|
360
461
|
|
|
@@ -365,13 +466,13 @@ class AppElement extends AsyncElement {
|
|
|
365
466
|
}
|
|
366
467
|
}
|
|
367
468
|
|
|
368
|
-
_onWindowResize() {
|
|
469
|
+
private _onWindowResize() {
|
|
369
470
|
if (this.app) {
|
|
370
471
|
this.app.resizeCanvas();
|
|
371
472
|
}
|
|
372
473
|
}
|
|
373
474
|
|
|
374
|
-
_pickerCreate() {
|
|
475
|
+
private _pickerCreate() {
|
|
375
476
|
const { width, height } = this.app!.graphicsDevice;
|
|
376
477
|
this._picker = new Picker(this.app!, width, height);
|
|
377
478
|
|
|
@@ -388,22 +489,20 @@ class AppElement extends AsyncElement {
|
|
|
388
489
|
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
389
490
|
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
390
491
|
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const anyListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity'))
|
|
399
|
-
.some(entity => entity.hasListeners(type));
|
|
492
|
+
// Attach canvas handlers for listeners registered before this boot (e.g. handlers
|
|
493
|
+
// created from onpointer* attributes when their elements were first upgraded, or
|
|
494
|
+
// listeners carried over from before a re-boot)
|
|
495
|
+
pointerEventTypes.forEach((type) => {
|
|
496
|
+
const anyListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity')).some((entity) =>
|
|
497
|
+
entity._hasListeners(type)
|
|
498
|
+
);
|
|
400
499
|
if (anyListeners) {
|
|
401
500
|
this._onPointerListenerAdded(type);
|
|
402
501
|
}
|
|
403
502
|
});
|
|
404
503
|
}
|
|
405
504
|
|
|
406
|
-
_pickerDestroy() {
|
|
505
|
+
private _pickerDestroy() {
|
|
407
506
|
if (this._canvas) {
|
|
408
507
|
Object.entries(this._pointerHandlers).forEach(([type, handler]) => {
|
|
409
508
|
if (handler) {
|
|
@@ -413,6 +512,7 @@ class AppElement extends AsyncElement {
|
|
|
413
512
|
}
|
|
414
513
|
|
|
415
514
|
this._picker = null;
|
|
515
|
+
this._hoveredEntity = null;
|
|
416
516
|
this._pointerHandlers = {
|
|
417
517
|
pointermove: null,
|
|
418
518
|
pointerdown: null,
|
|
@@ -427,8 +527,81 @@ class AppElement extends AsyncElement {
|
|
|
427
527
|
};
|
|
428
528
|
}
|
|
429
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Registers the element that created an entity. Called by EntityElement when it creates its
|
|
532
|
+
* entity.
|
|
533
|
+
*
|
|
534
|
+
* @param entity - The entity.
|
|
535
|
+
* @param element - The element that created it.
|
|
536
|
+
* @internal
|
|
537
|
+
*/
|
|
538
|
+
_registerEntityElement(entity: Entity, element: EntityElement) {
|
|
539
|
+
this._entityElements.set(entity, element);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Removes the registration for a destroyed entity. Called by EntityElement.
|
|
544
|
+
*
|
|
545
|
+
* @param entity - The entity.
|
|
546
|
+
* @internal
|
|
547
|
+
*/
|
|
548
|
+
_unregisterEntityElement(entity: Entity) {
|
|
549
|
+
this._entityElements.delete(entity);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Returns the `<pc-entity>` element whose backing entity is `entity`, or `null` if the
|
|
554
|
+
* entity was not created by an element of this application - for example, a node inside a
|
|
555
|
+
* model's instantiated hierarchy, or an entity created through the engine API.
|
|
556
|
+
*
|
|
557
|
+
* @param entity - The entity to look up.
|
|
558
|
+
* @returns The element backing the entity, or `null`.
|
|
559
|
+
*/
|
|
560
|
+
elementFromEntity(entity: Entity): EntityElement | null {
|
|
561
|
+
return this._entityElements.get(entity) ?? null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Resolves the element that owns a picked node: the nearest node up the parent chain -
|
|
566
|
+
* starting with the node itself - that was created by a `<pc-entity>` of this application.
|
|
567
|
+
* A hit inside a model's instantiated hierarchy therefore resolves to the element hosting
|
|
568
|
+
* the model.
|
|
569
|
+
*
|
|
570
|
+
* @param node - The picked node, or `null`.
|
|
571
|
+
* @returns The owning element, or `null`.
|
|
572
|
+
*/
|
|
573
|
+
private _elementFromNode(node: GraphNode | null): EntityElement | null {
|
|
574
|
+
while (node !== null) {
|
|
575
|
+
const element = this._entityElements.get(node);
|
|
576
|
+
if (element) {
|
|
577
|
+
return element;
|
|
578
|
+
}
|
|
579
|
+
node = node.parent;
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Like {@link _elementFromNode}, but skips elements without a listener for `type`, so a hit
|
|
586
|
+
* on an unlistened child still reaches a listening ancestor.
|
|
587
|
+
*
|
|
588
|
+
* @param node - The picked node, or `null`.
|
|
589
|
+
* @param type - The pointer event type a listener is required for.
|
|
590
|
+
* @returns The nearest listening element, or `null`.
|
|
591
|
+
*/
|
|
592
|
+
private _elementWithListener(node: GraphNode | null, type: string): EntityElement | null {
|
|
593
|
+
while (node !== null) {
|
|
594
|
+
const element = this._entityElements.get(node);
|
|
595
|
+
if (element?._hasListeners(type)) {
|
|
596
|
+
return element;
|
|
597
|
+
}
|
|
598
|
+
node = node.parent;
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
|
|
430
603
|
// New helper to convert CSS coordinates to canvas (picker) coordinates
|
|
431
|
-
private _getPickerCoordinates(event: PointerEvent): { x: number
|
|
604
|
+
private _getPickerCoordinates(event: PointerEvent): { x: number; y: number } {
|
|
432
605
|
// Get the canvas' bounding rectangle in CSS pixels.
|
|
433
606
|
const canvasRect = this._canvas!.getBoundingClientRect();
|
|
434
607
|
// Compute scale factors based on canvas actual resolution vs. its CSS display size.
|
|
@@ -465,7 +638,7 @@ class AppElement extends AsyncElement {
|
|
|
465
638
|
return item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
|
|
466
639
|
}
|
|
467
640
|
|
|
468
|
-
async _onPointerMove(event: PointerEvent) {
|
|
641
|
+
private async _onPointerMove(event: PointerEvent) {
|
|
469
642
|
if (!this._picker || !this.app) return;
|
|
470
643
|
|
|
471
644
|
// Moves arrive faster than a pick resolves, so results can land out of order. Only the
|
|
@@ -475,24 +648,16 @@ class AppElement extends AsyncElement {
|
|
|
475
648
|
const node = await this._pickNode(event);
|
|
476
649
|
if (token !== this._pickToken || !this._picker) return;
|
|
477
650
|
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
while (currentNode !== null) {
|
|
482
|
-
const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
|
|
483
|
-
if (entityElement) {
|
|
484
|
-
newHoverEntity = entityElement;
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
currentNode = currentNode.parent;
|
|
488
|
-
}
|
|
651
|
+
// The hovered element is the nearest one up the node's parent chain, listening or not -
|
|
652
|
+
// dispatch is gated per event type below
|
|
653
|
+
const newHoverEntity = this._elementFromNode(node);
|
|
489
654
|
|
|
490
655
|
// Handle enter/leave events
|
|
491
656
|
if (this._hoveredEntity !== newHoverEntity) {
|
|
492
|
-
if (this._hoveredEntity && this._hoveredEntity.
|
|
657
|
+
if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
|
|
493
658
|
this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
|
|
494
659
|
}
|
|
495
|
-
if (newHoverEntity && newHoverEntity.
|
|
660
|
+
if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
|
|
496
661
|
newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
|
|
497
662
|
}
|
|
498
663
|
}
|
|
@@ -501,81 +666,103 @@ class AppElement extends AsyncElement {
|
|
|
501
666
|
this._hoveredEntity = newHoverEntity;
|
|
502
667
|
|
|
503
668
|
// Handle pointermove event
|
|
504
|
-
if (newHoverEntity && newHoverEntity.
|
|
669
|
+
if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
|
|
505
670
|
newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
|
|
506
671
|
}
|
|
507
672
|
}
|
|
508
673
|
|
|
509
|
-
async _onPointerDown(event: PointerEvent) {
|
|
674
|
+
private async _onPointerDown(event: PointerEvent) {
|
|
510
675
|
if (!this._picker || !this.app) return;
|
|
511
676
|
|
|
512
|
-
|
|
677
|
+
const node = await this._pickNode(event);
|
|
513
678
|
if (!this._picker) return; // the element disconnected while the pick was in flight
|
|
514
679
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
519
|
-
break;
|
|
520
|
-
}
|
|
521
|
-
currentNode = currentNode.parent;
|
|
680
|
+
const entityElement = this._elementWithListener(node, 'pointerdown');
|
|
681
|
+
if (entityElement) {
|
|
682
|
+
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
522
683
|
}
|
|
523
684
|
}
|
|
524
685
|
|
|
525
|
-
async _onPointerUp(event: PointerEvent) {
|
|
686
|
+
private async _onPointerUp(event: PointerEvent) {
|
|
526
687
|
if (!this._picker || !this.app) return;
|
|
527
688
|
|
|
528
689
|
const node = await this._pickNode(event);
|
|
529
|
-
if (!
|
|
690
|
+
if (!this._picker) return; // the element disconnected while the pick was in flight
|
|
530
691
|
|
|
531
|
-
const entityElement = this.
|
|
532
|
-
if (entityElement
|
|
692
|
+
const entityElement = this._elementWithListener(node, 'pointerup');
|
|
693
|
+
if (entityElement) {
|
|
533
694
|
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
534
695
|
}
|
|
535
696
|
}
|
|
536
697
|
|
|
537
|
-
_onPointerListenerAdded(type: string) {
|
|
698
|
+
private _onPointerListenerAdded(type: string) {
|
|
538
699
|
if (!this._hasPointerListeners[type] && this._canvas) {
|
|
539
700
|
this._hasPointerListeners[type] = true;
|
|
540
701
|
|
|
541
702
|
// For enter/leave events, we need the move handler
|
|
542
|
-
const handler =
|
|
543
|
-
|
|
544
|
-
|
|
703
|
+
const handler =
|
|
704
|
+
type === 'pointerenter' || type === 'pointerleave'
|
|
705
|
+
? this._pointerHandlers.pointermove
|
|
706
|
+
: this._pointerHandlers[type];
|
|
545
707
|
|
|
546
708
|
if (handler) {
|
|
547
|
-
this._canvas.addEventListener(
|
|
709
|
+
this._canvas.addEventListener(
|
|
710
|
+
type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type,
|
|
711
|
+
handler
|
|
712
|
+
);
|
|
548
713
|
}
|
|
549
714
|
}
|
|
550
715
|
}
|
|
551
716
|
|
|
552
|
-
_onPointerListenerRemoved(type: string) {
|
|
553
|
-
const hasListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity'))
|
|
554
|
-
|
|
717
|
+
private _onPointerListenerRemoved(type: string) {
|
|
718
|
+
const hasListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity')).some((entity) =>
|
|
719
|
+
entity._hasListeners(type)
|
|
720
|
+
);
|
|
555
721
|
|
|
556
722
|
if (!hasListeners && this._canvas) {
|
|
557
723
|
this._hasPointerListeners[type] = false;
|
|
558
724
|
|
|
559
|
-
const handler =
|
|
560
|
-
|
|
561
|
-
|
|
725
|
+
const handler =
|
|
726
|
+
type === 'pointerenter' || type === 'pointerleave'
|
|
727
|
+
? this._pointerHandlers.pointermove
|
|
728
|
+
: this._pointerHandlers[type];
|
|
562
729
|
|
|
563
730
|
if (handler) {
|
|
564
|
-
this._canvas.removeEventListener(
|
|
731
|
+
this._canvas.removeEventListener(
|
|
732
|
+
type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type,
|
|
733
|
+
handler
|
|
734
|
+
);
|
|
565
735
|
}
|
|
566
736
|
}
|
|
567
737
|
}
|
|
568
738
|
|
|
569
739
|
/**
|
|
570
|
-
*
|
|
740
|
+
* Warns that a graphics option was written too late to have any effect. These options are read
|
|
741
|
+
* once, when the element connects and creates its graphics device, so a later write updates
|
|
742
|
+
* only the element's own property - silently, without this.
|
|
743
|
+
*
|
|
744
|
+
* @param name - The name of the option, as its attribute.
|
|
745
|
+
*/
|
|
746
|
+
private _warnIfBooted(name: string) {
|
|
747
|
+
if (this._optionsLocked) {
|
|
748
|
+
console.warn(
|
|
749
|
+
`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.`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Sets whether the frame buffer has an alpha channel, which is what lets the page show through
|
|
756
|
+
* wherever the scene has not drawn. Read only when the application boots.
|
|
571
757
|
* @param value - The alpha flag.
|
|
572
758
|
*/
|
|
573
759
|
set alpha(value: boolean) {
|
|
760
|
+
this._warnIfBooted('alpha');
|
|
574
761
|
this._alpha = value;
|
|
575
762
|
}
|
|
576
763
|
|
|
577
764
|
/**
|
|
578
|
-
* Gets the alpha
|
|
765
|
+
* Gets whether the frame buffer has an alpha channel.
|
|
579
766
|
* @returns The alpha flag.
|
|
580
767
|
*/
|
|
581
768
|
get alpha() {
|
|
@@ -583,15 +770,16 @@ class AppElement extends AsyncElement {
|
|
|
583
770
|
}
|
|
584
771
|
|
|
585
772
|
/**
|
|
586
|
-
* Sets the
|
|
773
|
+
* Sets whether the frame buffer is anti-aliased. Read only when the application boots.
|
|
587
774
|
* @param value - The antialias flag.
|
|
588
775
|
*/
|
|
589
776
|
set antialias(value: boolean) {
|
|
777
|
+
this._warnIfBooted('antialias');
|
|
590
778
|
this._antialias = value;
|
|
591
779
|
}
|
|
592
780
|
|
|
593
781
|
/**
|
|
594
|
-
* Gets the
|
|
782
|
+
* Gets whether the frame buffer is anti-aliased.
|
|
595
783
|
* @returns The antialias flag.
|
|
596
784
|
*/
|
|
597
785
|
get antialias() {
|
|
@@ -600,10 +788,11 @@ class AppElement extends AsyncElement {
|
|
|
600
788
|
|
|
601
789
|
/**
|
|
602
790
|
* Sets the graphics backend. Defaults to 'webgpu', which falls back to 'webgl2' if WebGPU
|
|
603
|
-
* is not supported by the browser.
|
|
791
|
+
* is not supported by the browser. Read only when the application boots.
|
|
604
792
|
* @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
|
|
605
793
|
*/
|
|
606
794
|
set backend(value: 'webgpu' | 'webgl2' | 'null') {
|
|
795
|
+
this._warnIfBooted('backend');
|
|
607
796
|
this._backend = value;
|
|
608
797
|
}
|
|
609
798
|
|
|
@@ -616,48 +805,21 @@ class AppElement extends AsyncElement {
|
|
|
616
805
|
}
|
|
617
806
|
|
|
618
807
|
/**
|
|
619
|
-
* Sets the depth
|
|
620
|
-
*
|
|
808
|
+
* Sets whether the frame buffer has a depth buffer, which the renderer needs to resolve which
|
|
809
|
+
* surface is nearest the camera. Read only when the application boots.
|
|
810
|
+
* @param value - The depth buffer flag.
|
|
621
811
|
*/
|
|
622
|
-
set
|
|
623
|
-
this.
|
|
812
|
+
set depthBuffer(value: boolean) {
|
|
813
|
+
this._warnIfBooted('depth-buffer');
|
|
814
|
+
this._depthBuffer = value;
|
|
624
815
|
}
|
|
625
816
|
|
|
626
817
|
/**
|
|
627
|
-
* Gets the depth
|
|
628
|
-
* @returns The depth flag.
|
|
818
|
+
* Gets whether the frame buffer has a depth buffer.
|
|
819
|
+
* @returns The depth buffer flag.
|
|
629
820
|
*/
|
|
630
|
-
get
|
|
631
|
-
return this.
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Gets the hierarchy ready flag.
|
|
636
|
-
* @returns The hierarchy ready flag.
|
|
637
|
-
* @ignore
|
|
638
|
-
*/
|
|
639
|
-
get hierarchyReady() {
|
|
640
|
-
return this._hierarchyReady;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Sets the high resolution flag. When true, the application will render at the device's
|
|
645
|
-
* physical resolution. When false, the application will render at CSS resolution.
|
|
646
|
-
* @param value - The high resolution flag.
|
|
647
|
-
*/
|
|
648
|
-
set highResolution(value: boolean) {
|
|
649
|
-
this._highResolution = value;
|
|
650
|
-
if (this.app) {
|
|
651
|
-
this.app.graphicsDevice.maxPixelRatio = value ? window.devicePixelRatio : 1;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Gets the high resolution flag.
|
|
657
|
-
* @returns The high resolution flag.
|
|
658
|
-
*/
|
|
659
|
-
get highResolution() {
|
|
660
|
-
return this._highResolution;
|
|
821
|
+
get depthBuffer() {
|
|
822
|
+
return this._depthBuffer;
|
|
661
823
|
}
|
|
662
824
|
|
|
663
825
|
/**
|
|
@@ -686,23 +848,49 @@ class AppElement extends AsyncElement {
|
|
|
686
848
|
}
|
|
687
849
|
|
|
688
850
|
/**
|
|
689
|
-
* Sets the
|
|
690
|
-
*
|
|
851
|
+
* Sets the cap on the pixel ratio the application renders at. The canvas is sized by the
|
|
852
|
+
* smaller of this value and the display's own device pixel ratio, so the default of `Infinity`
|
|
853
|
+
* renders at full physical resolution, `1` renders at CSS resolution, and an intermediate
|
|
854
|
+
* value such as `2` keeps a dense display sharp without paying for every one of its pixels.
|
|
855
|
+
* Must be greater than 0. Unlike the other graphics options, this applies immediately.
|
|
856
|
+
* @param value - The maximum pixel ratio.
|
|
691
857
|
*/
|
|
692
|
-
set
|
|
693
|
-
this.
|
|
858
|
+
set maxPixelRatio(value: number) {
|
|
859
|
+
this._maxPixelRatio = value;
|
|
860
|
+
if (this.app) {
|
|
861
|
+
this.app.graphicsDevice.maxPixelRatio = value;
|
|
862
|
+
this.app.resizeCanvas();
|
|
863
|
+
}
|
|
694
864
|
}
|
|
695
865
|
|
|
696
866
|
/**
|
|
697
|
-
* Gets the
|
|
698
|
-
* @returns The
|
|
867
|
+
* Gets the cap on the pixel ratio the application renders at.
|
|
868
|
+
* @returns The maximum pixel ratio.
|
|
699
869
|
*/
|
|
700
|
-
get
|
|
701
|
-
return this.
|
|
870
|
+
get maxPixelRatio() {
|
|
871
|
+
return this._maxPixelRatio;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Sets whether the frame buffer has a stencil buffer, which stencil-based effects and UI
|
|
876
|
+
* masking need. Read only when the application boots.
|
|
877
|
+
* @param value - The stencil buffer flag.
|
|
878
|
+
*/
|
|
879
|
+
set stencilBuffer(value: boolean) {
|
|
880
|
+
this._warnIfBooted('stencil-buffer');
|
|
881
|
+
this._stencilBuffer = value;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Gets whether the frame buffer has a stencil buffer.
|
|
886
|
+
* @returns The stencil buffer flag.
|
|
887
|
+
*/
|
|
888
|
+
get stencilBuffer() {
|
|
889
|
+
return this._stencilBuffer;
|
|
702
890
|
}
|
|
703
891
|
|
|
704
892
|
static get observedAttributes() {
|
|
705
|
-
return ['alpha', 'antialias', 'backend', 'depth', '
|
|
893
|
+
return ['alpha', 'antialias', 'backend', 'depth-buffer', 'loading-bar', 'max-pixel-ratio', 'stencil-buffer'];
|
|
706
894
|
}
|
|
707
895
|
|
|
708
896
|
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
@@ -716,17 +904,17 @@ class AppElement extends AsyncElement {
|
|
|
716
904
|
case 'backend':
|
|
717
905
|
this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgpu', name);
|
|
718
906
|
break;
|
|
719
|
-
case 'depth':
|
|
720
|
-
this.
|
|
721
|
-
break;
|
|
722
|
-
case 'high-resolution':
|
|
723
|
-
this.highResolution = parseBool(newValue, true);
|
|
907
|
+
case 'depth-buffer':
|
|
908
|
+
this.depthBuffer = parseBool(newValue, true);
|
|
724
909
|
break;
|
|
725
910
|
case 'loading-bar':
|
|
726
911
|
this.loadingBar = parseBool(newValue, true);
|
|
727
912
|
break;
|
|
728
|
-
case '
|
|
729
|
-
this.
|
|
913
|
+
case 'max-pixel-ratio':
|
|
914
|
+
this.maxPixelRatio = parseNumber(newValue, Infinity, name);
|
|
915
|
+
break;
|
|
916
|
+
case 'stencil-buffer':
|
|
917
|
+
this.stencilBuffer = parseBool(newValue, true);
|
|
730
918
|
break;
|
|
731
919
|
}
|
|
732
920
|
}
|
|
@@ -734,10 +922,4 @@ class AppElement extends AsyncElement {
|
|
|
734
922
|
|
|
735
923
|
customElements.define('pc-app', AppElement);
|
|
736
924
|
|
|
737
|
-
declare global {
|
|
738
|
-
interface HTMLElementTagNameMap {
|
|
739
|
-
'pc-app': AppElement;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
925
|
export { AppElement };
|