@playcanvas/web-components 0.12.0 → 0.13.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.cts +281 -0
- package/dist/app.d.ts +61 -3
- package/dist/asset.d.cts +224 -0
- package/dist/asset.d.ts +8 -2
- package/dist/async-element.d.cts +103 -0
- package/dist/async-element.d.ts +6 -4
- package/dist/colors.d.cts +1 -0
- package/dist/components/button-component.d.cts +186 -0
- package/dist/components/button-component.d.ts +1 -1
- package/dist/components/camera-component.d.cts +253 -0
- package/dist/components/camera-component.d.ts +1 -1
- package/dist/components/collision-component.d.cts +73 -0
- package/dist/components/collision-component.d.ts +1 -1
- package/dist/components/component.d.cts +82 -0
- package/dist/components/component.d.ts +1 -1
- package/dist/components/element-component.d.cts +316 -0
- package/dist/components/element-component.d.ts +1 -1
- package/dist/components/gsplat-component.d.cts +108 -0
- package/dist/components/gsplat-component.d.ts +1 -1
- package/dist/components/layoutchild-component.d.cts +110 -0
- package/dist/components/layoutchild-component.d.ts +1 -1
- package/dist/components/layoutgroup-component.d.cts +136 -0
- package/dist/components/layoutgroup-component.d.ts +1 -1
- package/dist/components/light-component.d.cts +264 -0
- package/dist/components/light-component.d.ts +1 -1
- package/dist/components/listener-component.d.cts +20 -0
- package/dist/components/listener-component.d.ts +1 -1
- package/dist/components/particlesystem-component.d.cts +52 -0
- package/dist/components/particlesystem-component.d.ts +1 -1
- package/dist/components/render-component.d.cts +76 -0
- package/dist/components/render-component.d.ts +1 -1
- package/dist/components/rigidbody-component.d.cts +88 -0
- package/dist/components/rigidbody-component.d.ts +1 -1
- package/dist/components/screen-component.d.cts +70 -0
- package/dist/components/screen-component.d.ts +1 -1
- package/dist/components/script-component.d.cts +163 -0
- package/dist/components/script-component.d.ts +1 -1
- package/dist/components/script.d.cts +94 -0
- package/dist/components/script.d.ts +1 -1
- package/dist/components/scrollbar-component.d.cts +69 -0
- package/dist/components/scrollbar-component.d.ts +1 -1
- package/dist/components/scrollview-component.d.cts +178 -0
- package/dist/components/scrollview-component.d.ts +1 -1
- package/dist/components/sound-component.d.cts +108 -0
- package/dist/components/sound-component.d.ts +1 -1
- package/dist/components/sound-slot.d.cts +134 -0
- package/dist/components/sound-slot.d.ts +2 -2
- package/dist/custom-elements.json +80 -22
- package/dist/entity-base.d.cts +67 -0
- package/dist/entity-base.d.ts +2 -2
- package/dist/entity.d.cts +131 -0
- package/dist/entity.d.ts +1 -1
- package/dist/index.d.cts +79 -0
- package/dist/index.d.ts +33 -33
- package/dist/loading-bar.d.cts +35 -0
- package/dist/material.d.cts +1011 -0
- package/dist/model.d.cts +72 -0
- package/dist/model.d.ts +1 -1
- package/dist/module.d.cts +29 -0
- package/dist/module.d.ts +16 -10
- package/dist/node.d.cts +253 -0
- package/dist/node.d.ts +1 -1
- package/dist/parse.d.cts +147 -0
- package/dist/pwc.cjs +285 -92
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +286 -93
- 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 +286 -93
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.cts +117 -0
- package/dist/scene.d.ts +1 -1
- package/dist/sky.d.cts +121 -0
- package/dist/sky.d.ts +1 -1
- package/dist/vscode.html-custom-data.json +3 -3
- package/dist/web-types.json +42 -6
- package/package.json +16 -7
- package/src/app.ts +211 -49
- package/src/asset.ts +34 -2
- package/src/async-element.ts +4 -2
- package/src/components/button-component.ts +7 -7
- package/src/components/element-component.ts +7 -7
- package/src/components/gsplat-component.ts +3 -3
- package/src/components/particlesystem-component.ts +7 -8
- package/src/components/script-component.ts +2 -2
- package/src/components/sound-slot.ts +2 -2
- package/src/loading-bar.ts +2 -2
- package/src/material.ts +2 -2
- package/src/model.ts +2 -5
- package/src/module.ts +39 -20
- package/src/sky.ts +2 -3
package/src/app.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { CameraComponent, GraphNode, GSplatComponent, Entity } from 'playcanvas';
|
|
1
|
+
import type { CameraComponent, GraphicsDevice, GraphNode, GSplatComponent, Entity } from 'playcanvas';
|
|
2
2
|
import {
|
|
3
3
|
AppBase,
|
|
4
4
|
AppOptions,
|
|
5
5
|
createGraphicsDevice,
|
|
6
6
|
ElementInput,
|
|
7
|
-
|
|
7
|
+
FILLMODE_NONE,
|
|
8
8
|
Keyboard,
|
|
9
9
|
Mouse,
|
|
10
10
|
Picker,
|
|
@@ -76,16 +76,45 @@ import { parseBool, parseEnum, parseNumber } from './parse';
|
|
|
76
76
|
/** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
|
|
77
77
|
const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'] as const;
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
|
|
81
|
+
* box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
|
|
82
|
+
* canvas and loading bar anchored to it. `:where()` keeps every declaration at zero specificity,
|
|
83
|
+
* so any page rule - however plain - overrides these defaults.
|
|
84
|
+
*/
|
|
85
|
+
const ensureBaseStyles = () => {
|
|
86
|
+
const id = 'pc-app-styles';
|
|
87
|
+
if (document.getElementById(id)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const style = document.createElement('style');
|
|
91
|
+
style.id = id;
|
|
92
|
+
style.textContent = ':where(pc-app) { display: block; position: relative; width: 300px; height: 150px; }';
|
|
93
|
+
document.head.appendChild(style);
|
|
94
|
+
};
|
|
95
|
+
|
|
79
96
|
/**
|
|
80
97
|
* The AppElement interface provides properties and methods for manipulating
|
|
81
98
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
82
99
|
* The AppElement interface also inherits the properties and methods of the
|
|
83
100
|
* {@link HTMLElement} interface.
|
|
84
101
|
*
|
|
102
|
+
* The element is sized like a replaced element such as `<video>`: a block-level box that the
|
|
103
|
+
* page's CSS controls, 300x150 by default. The application's canvas always fills the element,
|
|
104
|
+
* and the drawing buffer resolution follows the element's size (capped by `max-pixel-ratio`),
|
|
105
|
+
* tracked live via a ResizeObserver — so the element can be embedded at any size, resized by
|
|
106
|
+
* its container, or made fullscreen with ordinary CSS such as `width: 100vw; height: 100dvh`.
|
|
107
|
+
*
|
|
85
108
|
* @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
|
|
86
109
|
* `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
|
|
87
110
|
* Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
|
|
88
111
|
* not bubble.
|
|
112
|
+
*
|
|
113
|
+
* @fires {ErrorEvent} error - Fired when the application cannot boot because no graphics device
|
|
114
|
+
* could be created (for example, a browser with WebGL disabled). `message` names the requested
|
|
115
|
+
* backends and `error` holds the underlying failure. The element never becomes ready
|
|
116
|
+
* and `app` stays `null` — listen for this event to show a fallback UI. Removing the element and
|
|
117
|
+
* re-inserting it retries the boot with its current attributes. Does not bubble.
|
|
89
118
|
*/
|
|
90
119
|
class AppElement extends AsyncElement {
|
|
91
120
|
/**
|
|
@@ -165,6 +194,13 @@ class AppElement extends AsyncElement {
|
|
|
165
194
|
|
|
166
195
|
private _loadProgress = 0;
|
|
167
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Tracks the element's box so the drawing buffer and picker follow it. Created per boot once
|
|
199
|
+
* the application exists, and disconnected on teardown. `null` where ResizeObserver is
|
|
200
|
+
* unavailable (jsdom), where the boot-time resolution set is the only sizing that happens.
|
|
201
|
+
*/
|
|
202
|
+
private _resizeObserver: ResizeObserver | null = null;
|
|
203
|
+
|
|
168
204
|
/**
|
|
169
205
|
* The PlayCanvas application instance. `null` until the element is ready, and again once it
|
|
170
206
|
* has been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
@@ -194,9 +230,6 @@ class AppElement extends AsyncElement {
|
|
|
194
230
|
constructor() {
|
|
195
231
|
super();
|
|
196
232
|
|
|
197
|
-
// Bind methods to maintain 'this' context
|
|
198
|
-
this._onWindowResize = this._onWindowResize.bind(this);
|
|
199
|
-
|
|
200
233
|
// Track pointer listeners being added to and removed from descendant entities.
|
|
201
234
|
// Registered once here rather than on every boot - the handlers no-op while there is no
|
|
202
235
|
// canvas, and a re-booted element must not stack a second set.
|
|
@@ -209,6 +242,10 @@ class AppElement extends AsyncElement {
|
|
|
209
242
|
async connectedCallback() {
|
|
210
243
|
const generation = ++this._bootGeneration;
|
|
211
244
|
|
|
245
|
+
// Installed before the loading bar is created: the bar anchors to this element, which
|
|
246
|
+
// these styles make a positioned block box
|
|
247
|
+
ensureBaseStyles();
|
|
248
|
+
|
|
212
249
|
// Created before the first await, so the bar is visible while modules and the graphics
|
|
213
250
|
// device are created, and exists before any disconnect could need to clean it up
|
|
214
251
|
if (this._loadingBar && !this._bar) {
|
|
@@ -227,8 +264,11 @@ class AppElement extends AsyncElement {
|
|
|
227
264
|
return;
|
|
228
265
|
}
|
|
229
266
|
|
|
230
|
-
// Create and append the canvas
|
|
267
|
+
// Create and append the canvas, filling the element's content box - the page sizes the
|
|
268
|
+
// element, and everything else follows. touch-action: none keeps touch drags driving the
|
|
269
|
+
// engine's input handlers instead of scrolling the page.
|
|
231
270
|
this._canvas = document.createElement('canvas');
|
|
271
|
+
this._canvas.style.cssText = 'display: block; width: 100%; height: 100%; touch-action: none;';
|
|
232
272
|
this.appendChild(this._canvas);
|
|
233
273
|
|
|
234
274
|
// Configure device types based on backend selection
|
|
@@ -241,14 +281,46 @@ class AppElement extends AsyncElement {
|
|
|
241
281
|
|
|
242
282
|
this._optionsLocked = true;
|
|
243
283
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
284
|
+
// createGraphicsDevice appends its final null-device fallback to the array in place, so
|
|
285
|
+
// the requested list is captured now for the failure message.
|
|
286
|
+
const requested = deviceTypes.join(', ');
|
|
287
|
+
|
|
288
|
+
let device: GraphicsDevice;
|
|
289
|
+
try {
|
|
290
|
+
device = await createGraphicsDevice(this._canvas, {
|
|
291
|
+
// @ts-ignore - alpha needs to be documented
|
|
292
|
+
alpha: this._alpha,
|
|
293
|
+
antialias: this._antialias,
|
|
294
|
+
depth: this._depthBuffer,
|
|
295
|
+
deviceTypes: deviceTypes,
|
|
296
|
+
stencil: this._stencilBuffer
|
|
297
|
+
});
|
|
298
|
+
} catch (error) {
|
|
299
|
+
// The element may have been removed while device creation was failing. The teardown
|
|
300
|
+
// has already cleaned up, and the failure belongs to a boot that no longer owns the
|
|
301
|
+
// element.
|
|
302
|
+
if (generation !== this._bootGeneration) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Return the element to its pre-boot state - no dead canvas, no loading bar stuck at
|
|
307
|
+
// zero - before announcing the failure. Readiness deliberately stays pending: nothing
|
|
308
|
+
// it would announce (the app, the entity hierarchy) exists, so a device-less element
|
|
309
|
+
// joins the documented never-ready cases and the failure surfaces through the error
|
|
310
|
+
// event instead.
|
|
311
|
+
if (this._canvas && this.contains(this._canvas)) {
|
|
312
|
+
this.removeChild(this._canvas);
|
|
313
|
+
}
|
|
314
|
+
this._canvas = null;
|
|
315
|
+
this._bar?.destroy();
|
|
316
|
+
this._bar = null;
|
|
317
|
+
|
|
318
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
319
|
+
const message = `pc-app failed to create a graphics device (${requested}) - ${reason}`;
|
|
320
|
+
console.error(message, error);
|
|
321
|
+
this.dispatchEvent(new ErrorEvent('error', { message, error }));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
252
324
|
|
|
253
325
|
// The element may have been removed while the device was created. disconnectedCallback
|
|
254
326
|
// has already cleaned up the canvas; the device was created inside the await, so it is
|
|
@@ -332,11 +404,22 @@ class AppElement extends AsyncElement {
|
|
|
332
404
|
this._app = app;
|
|
333
405
|
app.init(createOptions);
|
|
334
406
|
|
|
335
|
-
|
|
407
|
+
// FILLMODE_NONE leaves the canvas's CSS sizing alone (the engine's other fill modes
|
|
408
|
+
// stamp window-derived pixel sizes onto it); RESOLUTION_AUTO sizes the drawing buffer
|
|
409
|
+
// from the canvas's client size
|
|
410
|
+
app.setCanvasFillMode(FILLMODE_NONE);
|
|
336
411
|
app.setCanvasResolution(RESOLUTION_AUTO);
|
|
337
412
|
|
|
338
413
|
this._pickerCreate();
|
|
339
414
|
|
|
415
|
+
// Track the element's box rather than the window: containers resize without any window
|
|
416
|
+
// event (splitter drags, flex reflow, animations). Guarded because jsdom has no
|
|
417
|
+
// ResizeObserver - there, the resolution set above is the only sizing that happens.
|
|
418
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
419
|
+
this._resizeObserver = new ResizeObserver(() => this._syncCanvasSize());
|
|
420
|
+
this._resizeObserver.observe(this);
|
|
421
|
+
}
|
|
422
|
+
|
|
340
423
|
// Get all pc-asset elements that are direct children of the pc-app element
|
|
341
424
|
const assetElements = this.querySelectorAll<AssetElement>(':scope > pc-asset');
|
|
342
425
|
for (const assetElement of Array.from(assetElements)) {
|
|
@@ -426,9 +509,6 @@ class AppElement extends AsyncElement {
|
|
|
426
509
|
// first rAF tick
|
|
427
510
|
app.once('frameend', () => this._bar?.complete());
|
|
428
511
|
|
|
429
|
-
// Handle window resize to keep the canvas responsive
|
|
430
|
-
window.addEventListener('resize', this._onWindowResize);
|
|
431
|
-
|
|
432
512
|
this._onReady();
|
|
433
513
|
});
|
|
434
514
|
}
|
|
@@ -458,8 +538,9 @@ class AppElement extends AsyncElement {
|
|
|
458
538
|
this._hierarchyReady = false;
|
|
459
539
|
this._resetReady();
|
|
460
540
|
|
|
461
|
-
//
|
|
462
|
-
|
|
541
|
+
// Stop tracking the element's size
|
|
542
|
+
this._resizeObserver?.disconnect();
|
|
543
|
+
this._resizeObserver = null;
|
|
463
544
|
|
|
464
545
|
// Remove the canvas
|
|
465
546
|
if (this._canvas && this.contains(this._canvas)) {
|
|
@@ -468,10 +549,18 @@ class AppElement extends AsyncElement {
|
|
|
468
549
|
}
|
|
469
550
|
}
|
|
470
551
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
552
|
+
/**
|
|
553
|
+
* Syncs the drawing buffer and the picker to the canvas's current CSS size. The picker must
|
|
554
|
+
* track the buffer, or picks would land at stale coordinates after a resize. Skipped while
|
|
555
|
+
* an XR session presents - the session owns the buffer size.
|
|
556
|
+
*/
|
|
557
|
+
private _syncCanvasSize() {
|
|
558
|
+
if (!this.app || this.app.xr?.active) {
|
|
559
|
+
return;
|
|
474
560
|
}
|
|
561
|
+
this.app.updateCanvasSize();
|
|
562
|
+
const { width, height } = this.app.graphicsDevice;
|
|
563
|
+
this._picker?.resize(width, height);
|
|
475
564
|
}
|
|
476
565
|
|
|
477
566
|
private _pickerCreate() {
|
|
@@ -495,9 +584,9 @@ class AppElement extends AsyncElement {
|
|
|
495
584
|
// created from onpointer* attributes when their elements were first upgraded, or
|
|
496
585
|
// listeners carried over from before a re-boot)
|
|
497
586
|
pointerEventTypes.forEach((type) => {
|
|
498
|
-
const anyListeners = Array.from(
|
|
499
|
-
|
|
500
|
-
)
|
|
587
|
+
const anyListeners = Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')).some(
|
|
588
|
+
(entity) => entity._hasListeners(type)
|
|
589
|
+
);
|
|
501
590
|
if (anyListeners) {
|
|
502
591
|
this._onPointerListenerAdded(type);
|
|
503
592
|
}
|
|
@@ -603,22 +692,68 @@ class AppElement extends AsyncElement {
|
|
|
603
692
|
return null;
|
|
604
693
|
}
|
|
605
694
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
695
|
+
/**
|
|
696
|
+
* Converts a pointer event's client coordinates into drawing-buffer coordinates - the space
|
|
697
|
+
* the pick buffer and the camera viewports are laid out in. When the canvas has no CSS box
|
|
698
|
+
* to map through (jsdom; a hidden canvas receives no pointer events in a browser), the
|
|
699
|
+
* client coordinates are passed through unmapped and `mapped` is false, so callers know the
|
|
700
|
+
* coordinates correspond to no real geometry.
|
|
701
|
+
*
|
|
702
|
+
* @param event - The pointer event to convert.
|
|
703
|
+
* @param canvas - The canvas the event was dispatched on.
|
|
704
|
+
* @returns The buffer-space coordinates, and whether they were actually mapped.
|
|
705
|
+
*/
|
|
706
|
+
private _getPickerCoordinates(
|
|
707
|
+
event: PointerEvent,
|
|
708
|
+
canvas: HTMLCanvasElement
|
|
709
|
+
): { x: number; y: number; mapped: boolean } {
|
|
710
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
711
|
+
if (canvasRect.width === 0 || canvasRect.height === 0) {
|
|
712
|
+
return { x: event.clientX, y: event.clientY, mapped: false };
|
|
713
|
+
}
|
|
714
|
+
const scaleX = canvas.width / canvasRect.width;
|
|
715
|
+
const scaleY = canvas.height / canvasRect.height;
|
|
716
|
+
return {
|
|
717
|
+
x: (event.clientX - canvasRect.left) * scaleX,
|
|
718
|
+
y: (event.clientY - canvasRect.top) * scaleY,
|
|
719
|
+
mapped: true
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Whether a camera's viewport contains the point. A camera renders into its normalized
|
|
725
|
+
* `rect`, whose origin is the bottom-left of the canvas while buffer coordinates run from
|
|
726
|
+
* the top-left - so the vertical test flips, as the engine's ElementInput flips it for UI
|
|
727
|
+
* input. The right and bottom edges are exclusive: a viewport rasterizes the half-open
|
|
728
|
+
* pixel range [left, right) x [top, bottom), so a coordinate on a shared edge belongs to
|
|
729
|
+
* the viewport whose first pixel it is - never to the one it just left, whose pick buffer
|
|
730
|
+
* holds nothing there.
|
|
731
|
+
*
|
|
732
|
+
* @param camera - The camera to test.
|
|
733
|
+
* @param x - The x coordinate, in buffer space.
|
|
734
|
+
* @param y - The y coordinate, in buffer space.
|
|
735
|
+
* @param canvas - The canvas the coordinates are relative to.
|
|
736
|
+
* @returns Whether the camera's viewport contains the point.
|
|
737
|
+
*/
|
|
738
|
+
private _cameraContains(camera: CameraComponent, x: number, y: number, canvas: HTMLCanvasElement): boolean {
|
|
739
|
+
const rect = camera.rect;
|
|
740
|
+
const left = rect.x * canvas.width;
|
|
741
|
+
const bottom = (1 - rect.y) * canvas.height;
|
|
742
|
+
const top = bottom - rect.w * canvas.height;
|
|
743
|
+
return x >= left && x < left + rect.z * canvas.width && y >= top && y < bottom;
|
|
617
744
|
}
|
|
618
745
|
|
|
619
746
|
/**
|
|
620
747
|
* Picks the scene under the pointer and returns the graph node that was hit, or `null`.
|
|
621
748
|
*
|
|
749
|
+
* The camera is resolved the way the engine's ElementInput resolves it for UI input:
|
|
750
|
+
* enabled cameras are tried topmost-first (they render in ascending `priority` order),
|
|
751
|
+
* skipping cameras that render to a texture and cameras whose viewport `rect` does not
|
|
752
|
+
* contain the pointer. A camera that picks nothing ends the search if it clears the color
|
|
753
|
+
* buffer - its background visually owns the pixel - and otherwise cedes to the cameras
|
|
754
|
+
* beneath it, so an overlay camera only intercepts picks where it actually drew something.
|
|
755
|
+
* The pick buffer is prepared per camera, so each camera picks from its own layers.
|
|
756
|
+
*
|
|
622
757
|
* The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
|
|
623
758
|
* supported on WebGPU, where it returns an empty selection rather than failing - which
|
|
624
759
|
* silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
|
|
@@ -628,17 +763,44 @@ class AppElement extends AsyncElement {
|
|
|
628
763
|
* @returns The graph node under the pointer, or `null` if nothing was hit.
|
|
629
764
|
*/
|
|
630
765
|
private async _pickNode(event: PointerEvent): Promise<GraphNode | null> {
|
|
631
|
-
const
|
|
632
|
-
|
|
766
|
+
const app = this.app;
|
|
767
|
+
const picker = this._picker;
|
|
768
|
+
const canvas = this._canvas;
|
|
769
|
+
if (!app || !picker || !canvas) return null;
|
|
770
|
+
|
|
771
|
+
const { x, y, mapped } = this._getPickerCoordinates(event, canvas);
|
|
772
|
+
|
|
773
|
+
// Walked from the end: the array is sorted by ascending priority, so the last camera
|
|
774
|
+
// renders last and sits on top. Read through .at() because a pick handler may remove
|
|
775
|
+
// cameras while an earlier iteration's read back is in flight.
|
|
776
|
+
const cameras = app.systems.camera?.cameras ?? [];
|
|
777
|
+
for (let i = cameras.length - 1; i >= 0; i--) {
|
|
778
|
+
const camera = cameras.at(i);
|
|
779
|
+
|
|
780
|
+
// A camera rendering to a texture is not on the canvas.
|
|
781
|
+
if (!camera || camera.renderTarget) continue;
|
|
633
782
|
|
|
634
|
-
|
|
783
|
+
// Coordinates that could not be mapped cannot be tested for containment.
|
|
784
|
+
if (mapped && !this._cameraContains(camera, x, y, canvas)) continue;
|
|
635
785
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (selection.length === 0) return null;
|
|
786
|
+
picker.prepare(camera, app.scene);
|
|
787
|
+
const selection = await picker.getSelectionAsync(x, y);
|
|
639
788
|
|
|
640
|
-
|
|
641
|
-
|
|
789
|
+
// The element may have disconnected while the read back was in flight.
|
|
790
|
+
if (!this._picker || !this.app) return null;
|
|
791
|
+
|
|
792
|
+
if (selection.length > 0) {
|
|
793
|
+
const item = selection[0];
|
|
794
|
+
return item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Nothing hit. A camera that clears the color buffer paints its background over
|
|
798
|
+
// everything beneath it, so the miss is final; one that does not is an overlay
|
|
799
|
+
// that the cameras beneath show through, so they get their turn.
|
|
800
|
+
if (camera.clearColorBuffer) return null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return null;
|
|
642
804
|
}
|
|
643
805
|
|
|
644
806
|
private async _onPointerMove(event: PointerEvent) {
|
|
@@ -718,9 +880,9 @@ class AppElement extends AsyncElement {
|
|
|
718
880
|
}
|
|
719
881
|
|
|
720
882
|
private _onPointerListenerRemoved(type: string) {
|
|
721
|
-
const hasListeners = Array.from(
|
|
722
|
-
|
|
723
|
-
)
|
|
883
|
+
const hasListeners = Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')).some((entity) =>
|
|
884
|
+
entity._hasListeners(type)
|
|
885
|
+
);
|
|
724
886
|
|
|
725
887
|
if (!hasListeners && this._canvas) {
|
|
726
888
|
this._hasPointerListeners[type] = false;
|
|
@@ -862,7 +1024,7 @@ class AppElement extends AsyncElement {
|
|
|
862
1024
|
this._maxPixelRatio = value;
|
|
863
1025
|
if (this.app) {
|
|
864
1026
|
this.app.graphicsDevice.maxPixelRatio = value;
|
|
865
|
-
this.
|
|
1027
|
+
this._syncCanvasSize();
|
|
866
1028
|
}
|
|
867
1029
|
}
|
|
868
1030
|
|
package/src/asset.ts
CHANGED
|
@@ -153,6 +153,10 @@ const processBufferView = (
|
|
|
153
153
|
* immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
|
|
154
154
|
* elsewhere, or with an unsupported asset type, never become ready.
|
|
155
155
|
*
|
|
156
|
+
* A `lazy` asset loads on first use: the first time any element resolves it by `id` — a model,
|
|
157
|
+
* a material map, a sky, a script `asset:` reference — or when the `lazy` attribute is removed,
|
|
158
|
+
* whichever comes first. Until then it stays registered and unloaded.
|
|
159
|
+
*
|
|
156
160
|
* For `texture` and `textureatlas` assets, the texture options (`address-u`, `address-v`,
|
|
157
161
|
* `min-filter`, `mag-filter`, `anisotropy`, `mipmaps`, `srgb`, `flip-y`) apply when the texture is
|
|
158
162
|
* created and — like `lazy` — are observed: changing one updates a texture that has already
|
|
@@ -385,7 +389,9 @@ class AssetElement extends AsyncElement {
|
|
|
385
389
|
data = data ?? {};
|
|
386
390
|
|
|
387
391
|
// Resolve the referenced texture atlas to its (numeric) asset id. The atlas must be
|
|
388
|
-
// declared before the sprite so its asset already exists in the registry.
|
|
392
|
+
// declared before the sprite so its asset already exists in the registry. Resolved
|
|
393
|
+
// with get, not useAsset: creation-time wiring is not a use, and the engine's
|
|
394
|
+
// sprite handler loads the atlas when the sprite itself loads.
|
|
389
395
|
const atlas = this.getAttribute('atlas') ?? data.textureAtlasAsset;
|
|
390
396
|
if (typeof atlas === 'string') {
|
|
391
397
|
const atlasAsset = AssetElement.get(atlas);
|
|
@@ -564,13 +570,18 @@ class AssetElement extends AsyncElement {
|
|
|
564
570
|
}
|
|
565
571
|
|
|
566
572
|
/**
|
|
567
|
-
* Sets whether the asset should be loaded lazily.
|
|
573
|
+
* Sets whether the asset should be loaded lazily. A lazy asset is registered without being
|
|
574
|
+
* loaded; it loads on first use - the first time any element resolves it by `id` - or when
|
|
575
|
+
* this flag is cleared on a registered asset, whichever comes first.
|
|
568
576
|
* @param value - The lazy loading flag.
|
|
569
577
|
*/
|
|
570
578
|
set lazy(value: boolean) {
|
|
571
579
|
this._lazy = value;
|
|
572
580
|
if (this.asset) {
|
|
573
581
|
this.asset.preload = !value;
|
|
582
|
+
if (!value) {
|
|
583
|
+
this.asset.registry?.load(this.asset);
|
|
584
|
+
}
|
|
574
585
|
}
|
|
575
586
|
}
|
|
576
587
|
|
|
@@ -760,4 +771,25 @@ class AssetElement extends AsyncElement {
|
|
|
760
771
|
|
|
761
772
|
customElements.define('pc-asset', AssetElement);
|
|
762
773
|
|
|
774
|
+
/**
|
|
775
|
+
* Resolves an asset reference for use: {@link AssetElement.get}, plus starting the load of a
|
|
776
|
+
* registered asset that has not begun one - a `lazy` asset. Every element that consumes assets
|
|
777
|
+
* resolves its references here, which is what makes `lazy` mean load on first use without any
|
|
778
|
+
* consumer having to remember the load. The load is asynchronous - callers observe the asset's
|
|
779
|
+
* `load` event for the resource.
|
|
780
|
+
*
|
|
781
|
+
* @param id - The `id` of the `<pc-asset>` element.
|
|
782
|
+
* @returns The asset, or `undefined`.
|
|
783
|
+
* @internal
|
|
784
|
+
*/
|
|
785
|
+
export const useAsset = (id: string) => {
|
|
786
|
+
const asset = AssetElement.get(id);
|
|
787
|
+
// load() ignores an asset that is already loaded or loading, so repeated resolution
|
|
788
|
+
// costs nothing.
|
|
789
|
+
if (asset) {
|
|
790
|
+
asset.registry?.load(asset);
|
|
791
|
+
}
|
|
792
|
+
return asset;
|
|
793
|
+
};
|
|
794
|
+
|
|
763
795
|
export { AssetElement };
|
package/src/async-element.ts
CHANGED
|
@@ -96,7 +96,8 @@ type AsyncElementTagName = {
|
|
|
96
96
|
/**
|
|
97
97
|
* Waits for the first element matching the given tag name to be fully initialized. Note that the
|
|
98
98
|
* promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
|
|
99
|
-
* that is not a direct child of `<pc-scripts
|
|
99
|
+
* that is not a direct child of `<pc-scripts>`, or a `<pc-app>` that could not create a graphics
|
|
100
|
+
* device — listen for its `error` event instead). A component element outside a `<pc-entity>` is
|
|
100
101
|
* the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
|
|
101
102
|
* element logs a warning naming the parent it requires.
|
|
102
103
|
* @param target - The tag name of the element to wait for (e.g. `'pc-app'`).
|
|
@@ -120,7 +121,8 @@ function whenReady<T extends AsyncElement>(target: T): Promise<T>;
|
|
|
120
121
|
/**
|
|
121
122
|
* Waits for the first element matching the given CSS selector to be fully initialized. Note that
|
|
122
123
|
* the promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
|
|
123
|
-
* that is not a direct child of `<pc-scripts
|
|
124
|
+
* that is not a direct child of `<pc-scripts>`, or a `<pc-app>` that could not create a graphics
|
|
125
|
+
* device — listen for its `error` event instead). A component element outside a `<pc-entity>` is
|
|
124
126
|
* the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
|
|
125
127
|
* element logs a warning naming the parent it requires.
|
|
126
128
|
* @param target - A CSS selector matching the element to wait for (e.g. `'#my-app'`).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ButtonComponent } from 'playcanvas';
|
|
2
2
|
import { BUTTON_TRANSITION_MODE_SPRITE_CHANGE, BUTTON_TRANSITION_MODE_TINT, Color, Vec4 } from 'playcanvas';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { useAsset } from '../asset';
|
|
5
5
|
import { getEntity, parseBool, parseColor, parseEnum, parseNumber, parseVec4 } from '../parse';
|
|
6
6
|
|
|
7
7
|
import { ComponentElement } from './component';
|
|
@@ -74,17 +74,17 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
74
74
|
data.imageEntity = imageEntity;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const hoverSpriteAsset =
|
|
77
|
+
const hoverSpriteAsset = useAsset(this._hoverSpriteAsset);
|
|
78
78
|
if (hoverSpriteAsset) {
|
|
79
79
|
data.hoverSpriteAsset = hoverSpriteAsset.id;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const pressedSpriteAsset =
|
|
82
|
+
const pressedSpriteAsset = useAsset(this._pressedSpriteAsset);
|
|
83
83
|
if (pressedSpriteAsset) {
|
|
84
84
|
data.pressedSpriteAsset = pressedSpriteAsset.id;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const inactiveSpriteAsset =
|
|
87
|
+
const inactiveSpriteAsset = useAsset(this._inactiveSpriteAsset);
|
|
88
88
|
if (inactiveSpriteAsset) {
|
|
89
89
|
data.inactiveSpriteAsset = inactiveSpriteAsset.id;
|
|
90
90
|
}
|
|
@@ -265,7 +265,7 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
265
265
|
*/
|
|
266
266
|
set hoverSpriteAsset(value: string) {
|
|
267
267
|
this._hoverSpriteAsset = value;
|
|
268
|
-
const asset =
|
|
268
|
+
const asset = useAsset(value);
|
|
269
269
|
if (this.component && asset) {
|
|
270
270
|
this.component.hoverSpriteAsset = asset.id as any;
|
|
271
271
|
}
|
|
@@ -305,7 +305,7 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
305
305
|
*/
|
|
306
306
|
set pressedSpriteAsset(value: string) {
|
|
307
307
|
this._pressedSpriteAsset = value;
|
|
308
|
-
const asset =
|
|
308
|
+
const asset = useAsset(value);
|
|
309
309
|
if (this.component && asset) {
|
|
310
310
|
this.component.pressedSpriteAsset = asset.id as any;
|
|
311
311
|
}
|
|
@@ -345,7 +345,7 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
345
345
|
*/
|
|
346
346
|
set inactiveSpriteAsset(value: string) {
|
|
347
347
|
this._inactiveSpriteAsset = value;
|
|
348
|
-
const asset =
|
|
348
|
+
const asset = useAsset(value);
|
|
349
349
|
if (this.component && asset) {
|
|
350
350
|
this.component.inactiveSpriteAsset = asset.id as any;
|
|
351
351
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ElementComponent } from 'playcanvas';
|
|
2
2
|
import { Color, Vec2, Vec4 } from 'playcanvas';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { useAsset } from '../asset';
|
|
5
5
|
import { parseBool, parseColor, parseEnum, parseNumber, parseVec2, parseVec4 } from '../parse';
|
|
6
6
|
|
|
7
7
|
import { ComponentElement } from './component';
|
|
@@ -118,17 +118,17 @@ class ElementComponentElement extends ComponentElement {
|
|
|
118
118
|
|
|
119
119
|
// Asset references are resolved from `<pc-asset>` element ids to engine asset ids. They are
|
|
120
120
|
// only included when they resolve, so image/group elements (with no font) don't error.
|
|
121
|
-
const fontAsset =
|
|
121
|
+
const fontAsset = useAsset(this._fontAsset);
|
|
122
122
|
if (fontAsset) {
|
|
123
123
|
data.fontAsset = fontAsset.id;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const spriteAsset =
|
|
126
|
+
const spriteAsset = useAsset(this._spriteAsset);
|
|
127
127
|
if (spriteAsset) {
|
|
128
128
|
data.spriteAsset = spriteAsset.id;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
const textureAsset =
|
|
131
|
+
const textureAsset = useAsset(this._textureAsset);
|
|
132
132
|
if (textureAsset) {
|
|
133
133
|
data.textureAsset = textureAsset.id;
|
|
134
134
|
}
|
|
@@ -257,7 +257,7 @@ class ElementComponentElement extends ComponentElement {
|
|
|
257
257
|
*/
|
|
258
258
|
set fontAsset(value: string) {
|
|
259
259
|
this._fontAsset = value;
|
|
260
|
-
const asset =
|
|
260
|
+
const asset = useAsset(value);
|
|
261
261
|
if (this.component && asset) {
|
|
262
262
|
this.component.fontAsset = asset.id;
|
|
263
263
|
}
|
|
@@ -430,7 +430,7 @@ class ElementComponentElement extends ComponentElement {
|
|
|
430
430
|
*/
|
|
431
431
|
set spriteAsset(value: string) {
|
|
432
432
|
this._spriteAsset = value;
|
|
433
|
-
const asset =
|
|
433
|
+
const asset = useAsset(value);
|
|
434
434
|
if (this.component && asset) {
|
|
435
435
|
this.component.spriteAsset = asset.id;
|
|
436
436
|
}
|
|
@@ -488,7 +488,7 @@ class ElementComponentElement extends ComponentElement {
|
|
|
488
488
|
*/
|
|
489
489
|
set textureAsset(value: string) {
|
|
490
490
|
this._textureAsset = value;
|
|
491
|
-
const asset =
|
|
491
|
+
const asset = useAsset(value);
|
|
492
492
|
if (this.component && asset) {
|
|
493
493
|
this.component.textureAsset = asset.id;
|
|
494
494
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GSplatComponent } from 'playcanvas';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useAsset } from '../asset';
|
|
4
4
|
import { parseBool, parseNumber } from '../parse';
|
|
5
5
|
|
|
6
6
|
import { ComponentElement } from './component';
|
|
@@ -33,7 +33,7 @@ class GSplatComponentElement extends ComponentElement {
|
|
|
33
33
|
|
|
34
34
|
protected getInitialComponentData() {
|
|
35
35
|
return {
|
|
36
|
-
asset:
|
|
36
|
+
asset: useAsset(this._asset),
|
|
37
37
|
castShadows: this._castShadows,
|
|
38
38
|
lodBaseDistance: this._lodBaseDistance,
|
|
39
39
|
lodMultiplier: this._lodMultiplier,
|
|
@@ -56,7 +56,7 @@ class GSplatComponentElement extends ComponentElement {
|
|
|
56
56
|
*/
|
|
57
57
|
set asset(value: string) {
|
|
58
58
|
this._asset = value;
|
|
59
|
-
const asset =
|
|
59
|
+
const asset = useAsset(value);
|
|
60
60
|
if (this.component && asset) {
|
|
61
61
|
this.component.asset = asset;
|
|
62
62
|
}
|