@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/dist/pwc.cjs
CHANGED
|
@@ -110,12 +110,18 @@ async function whenReady(target) {
|
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
112
|
* The ModuleElement interface provides properties and methods for manipulating
|
|
113
|
-
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`}
|
|
114
|
-
* The ModuleElement interface also inherits the properties and methods of the
|
|
115
|
-
* {@link
|
|
113
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`}
|
|
114
|
+
* elements. The ModuleElement interface also inherits the properties and methods of the
|
|
115
|
+
* {@link AsyncElement} interface.
|
|
116
|
+
*
|
|
117
|
+
* The attributes are read once, when the module starts loading - on the element's first
|
|
118
|
+
* connection, or earlier if a containing `<pc-app>` boots first and collects it - so changing
|
|
119
|
+
* them later has no effect. The element becomes ready once the module has loaded. WebAssembly
|
|
120
|
+
* modules configure engine-global state that never unloads, so readiness is not re-armed by
|
|
121
|
+
* removing the element, and a re-inserted element does not load again.
|
|
116
122
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
123
|
+
* A `<pc-module>` without a `name` warns and never becomes ready; a containing `<pc-app>` still
|
|
124
|
+
* boots.
|
|
119
125
|
*
|
|
120
126
|
* @attribute {string} name - The name of the WebAssembly module to configure, e.g. `Basis` or
|
|
121
127
|
* `Ammo`.
|
|
@@ -124,19 +130,22 @@ async function whenReady(target) {
|
|
|
124
130
|
* @attribute {string} fallback - The URL of the module's asm.js fallback script, used when
|
|
125
131
|
* WebAssembly is unavailable.
|
|
126
132
|
*/
|
|
127
|
-
class ModuleElement extends
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
super();
|
|
132
|
-
this.loadPromise = this.loadModule();
|
|
133
|
+
class ModuleElement extends AsyncElement {
|
|
134
|
+
_loadPromise = null;
|
|
135
|
+
connectedCallback() {
|
|
136
|
+
this._getLoadPromise();
|
|
133
137
|
}
|
|
134
|
-
async
|
|
138
|
+
async _loadModule() {
|
|
135
139
|
const name = this.getAttribute('name');
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
if (!name) {
|
|
141
|
+
console.warn("pc-module requires a 'name' attribute - no module was configured");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const config = {
|
|
145
|
+
glueUrl: this.getAttribute('glue') ?? undefined,
|
|
146
|
+
wasmUrl: this.getAttribute('wasm') ?? undefined,
|
|
147
|
+
fallbackUrl: this.getAttribute('fallback') ?? undefined
|
|
148
|
+
};
|
|
140
149
|
if (name === 'Basis') {
|
|
141
150
|
playcanvas.basisInitialize(config);
|
|
142
151
|
}
|
|
@@ -146,16 +155,22 @@ class ModuleElement extends HTMLElement {
|
|
|
146
155
|
playcanvas.WasmModule.getInstance(name, () => resolve());
|
|
147
156
|
});
|
|
148
157
|
}
|
|
158
|
+
this._onReady();
|
|
149
159
|
}
|
|
150
160
|
/**
|
|
151
|
-
* Returns the promise that settles when the module has loaded
|
|
152
|
-
* `<pc-app>`
|
|
161
|
+
* Returns the promise that settles when the module has loaded, starting the load if it has
|
|
162
|
+
* not already started - a containing `<pc-app>` boots in document order, so it may collect
|
|
163
|
+
* this element before the element's own connectedCallback has run. A missing `name` resolves
|
|
164
|
+
* the promise without configuring anything, so a misconfigured module never blocks the app.
|
|
153
165
|
*
|
|
154
166
|
* @returns The load promise.
|
|
155
167
|
* @internal
|
|
156
168
|
*/
|
|
157
169
|
_getLoadPromise() {
|
|
158
|
-
|
|
170
|
+
if (!this._loadPromise) {
|
|
171
|
+
this._loadPromise = this._loadModule();
|
|
172
|
+
}
|
|
173
|
+
return this._loadPromise;
|
|
159
174
|
}
|
|
160
175
|
}
|
|
161
176
|
customElements.define('pc-module', ModuleElement);
|
|
@@ -185,9 +200,9 @@ class LoadingBar {
|
|
|
185
200
|
this._track.setAttribute('aria-label', 'Loading');
|
|
186
201
|
this._track.setAttribute('aria-valuemin', '0');
|
|
187
202
|
this._track.setAttribute('aria-valuemax', '100');
|
|
188
|
-
//
|
|
203
|
+
// Anchored to the pc-app element, which the library's base styles make a positioned box
|
|
189
204
|
this._track.style.cssText = [
|
|
190
|
-
'position:
|
|
205
|
+
'position: absolute',
|
|
191
206
|
'top: 0',
|
|
192
207
|
'left: 0',
|
|
193
208
|
'width: 100%',
|
|
@@ -699,16 +714,44 @@ const getEntity = (ref) => {
|
|
|
699
714
|
|
|
700
715
|
/** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
|
|
701
716
|
const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
|
|
717
|
+
/**
|
|
718
|
+
* Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
|
|
719
|
+
* box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
|
|
720
|
+
* canvas and loading bar anchored to it. `:where()` keeps every declaration at zero specificity,
|
|
721
|
+
* so any page rule - however plain - overrides these defaults.
|
|
722
|
+
*/
|
|
723
|
+
const ensureBaseStyles = () => {
|
|
724
|
+
const id = 'pc-app-styles';
|
|
725
|
+
if (document.getElementById(id)) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const style = document.createElement('style');
|
|
729
|
+
style.id = id;
|
|
730
|
+
style.textContent = ':where(pc-app) { display: block; position: relative; width: 300px; height: 150px; }';
|
|
731
|
+
document.head.appendChild(style);
|
|
732
|
+
};
|
|
702
733
|
/**
|
|
703
734
|
* The AppElement interface provides properties and methods for manipulating
|
|
704
735
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
705
736
|
* The AppElement interface also inherits the properties and methods of the
|
|
706
737
|
* {@link HTMLElement} interface.
|
|
707
738
|
*
|
|
739
|
+
* The element is sized like a replaced element such as `<video>`: a block-level box that the
|
|
740
|
+
* page's CSS controls, 300x150 by default. The application's canvas always fills the element,
|
|
741
|
+
* and the drawing buffer resolution follows the element's size (capped by `max-pixel-ratio`),
|
|
742
|
+
* tracked live via a ResizeObserver — so the element can be embedded at any size, resized by
|
|
743
|
+
* its container, or made fullscreen with ordinary CSS such as `width: 100vw; height: 100dvh`.
|
|
744
|
+
*
|
|
708
745
|
* @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
|
|
709
746
|
* `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
|
|
710
747
|
* Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
|
|
711
748
|
* not bubble.
|
|
749
|
+
*
|
|
750
|
+
* @fires {ErrorEvent} error - Fired when the application cannot boot because no graphics device
|
|
751
|
+
* could be created (for example, a browser with WebGL disabled). `message` names the requested
|
|
752
|
+
* backends and `error` holds the underlying failure. The element never becomes ready
|
|
753
|
+
* and `app` stays `null` — listen for this event to show a fallback UI. Removing the element and
|
|
754
|
+
* re-inserting it retries the boot with its current attributes. Does not bubble.
|
|
712
755
|
*/
|
|
713
756
|
class AppElement extends AsyncElement {
|
|
714
757
|
/**
|
|
@@ -768,6 +811,12 @@ class AppElement extends AsyncElement {
|
|
|
768
811
|
};
|
|
769
812
|
_app = null;
|
|
770
813
|
_loadProgress = 0;
|
|
814
|
+
/**
|
|
815
|
+
* Tracks the element's box so the drawing buffer and picker follow it. Created per boot once
|
|
816
|
+
* the application exists, and disconnected on teardown. `null` where ResizeObserver is
|
|
817
|
+
* unavailable (jsdom), where the boot-time resolution set is the only sizing that happens.
|
|
818
|
+
*/
|
|
819
|
+
_resizeObserver = null;
|
|
771
820
|
/**
|
|
772
821
|
* The PlayCanvas application instance. `null` until the element is ready, and again once it
|
|
773
822
|
* has been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
@@ -794,8 +843,6 @@ class AppElement extends AsyncElement {
|
|
|
794
843
|
*/
|
|
795
844
|
constructor() {
|
|
796
845
|
super();
|
|
797
|
-
// Bind methods to maintain 'this' context
|
|
798
|
-
this._onWindowResize = this._onWindowResize.bind(this);
|
|
799
846
|
// Track pointer listeners being added to and removed from descendant entities.
|
|
800
847
|
// Registered once here rather than on every boot - the handlers no-op while there is no
|
|
801
848
|
// canvas, and a re-booted element must not stack a second set.
|
|
@@ -806,6 +853,9 @@ class AppElement extends AsyncElement {
|
|
|
806
853
|
}
|
|
807
854
|
async connectedCallback() {
|
|
808
855
|
const generation = ++this._bootGeneration;
|
|
856
|
+
// Installed before the loading bar is created: the bar anchors to this element, which
|
|
857
|
+
// these styles make a positioned block box
|
|
858
|
+
ensureBaseStyles();
|
|
809
859
|
// Created before the first await, so the bar is visible while modules and the graphics
|
|
810
860
|
// device are created, and exists before any disconnect could need to clean it up
|
|
811
861
|
if (this._loadingBar && !this._bar) {
|
|
@@ -820,8 +870,11 @@ class AppElement extends AsyncElement {
|
|
|
820
870
|
if (generation !== this._bootGeneration) {
|
|
821
871
|
return;
|
|
822
872
|
}
|
|
823
|
-
// Create and append the canvas
|
|
873
|
+
// Create and append the canvas, filling the element's content box - the page sizes the
|
|
874
|
+
// element, and everything else follows. touch-action: none keeps touch drags driving the
|
|
875
|
+
// engine's input handlers instead of scrolling the page.
|
|
824
876
|
this._canvas = document.createElement('canvas');
|
|
877
|
+
this._canvas.style.cssText = 'display: block; width: 100%; height: 100%; touch-action: none;';
|
|
825
878
|
this.appendChild(this._canvas);
|
|
826
879
|
// Configure device types based on backend selection
|
|
827
880
|
const backendToDeviceTypes = {
|
|
@@ -831,14 +884,44 @@ class AppElement extends AsyncElement {
|
|
|
831
884
|
};
|
|
832
885
|
const deviceTypes = backendToDeviceTypes[this._backend] || [];
|
|
833
886
|
this._optionsLocked = true;
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
887
|
+
// createGraphicsDevice appends its final null-device fallback to the array in place, so
|
|
888
|
+
// the requested list is captured now for the failure message.
|
|
889
|
+
const requested = deviceTypes.join(', ');
|
|
890
|
+
let device;
|
|
891
|
+
try {
|
|
892
|
+
device = await playcanvas.createGraphicsDevice(this._canvas, {
|
|
893
|
+
// @ts-ignore - alpha needs to be documented
|
|
894
|
+
alpha: this._alpha,
|
|
895
|
+
antialias: this._antialias,
|
|
896
|
+
depth: this._depthBuffer,
|
|
897
|
+
deviceTypes: deviceTypes,
|
|
898
|
+
stencil: this._stencilBuffer
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
// The element may have been removed while device creation was failing. The teardown
|
|
903
|
+
// has already cleaned up, and the failure belongs to a boot that no longer owns the
|
|
904
|
+
// element.
|
|
905
|
+
if (generation !== this._bootGeneration) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
// Return the element to its pre-boot state - no dead canvas, no loading bar stuck at
|
|
909
|
+
// zero - before announcing the failure. Readiness deliberately stays pending: nothing
|
|
910
|
+
// it would announce (the app, the entity hierarchy) exists, so a device-less element
|
|
911
|
+
// joins the documented never-ready cases and the failure surfaces through the error
|
|
912
|
+
// event instead.
|
|
913
|
+
if (this._canvas && this.contains(this._canvas)) {
|
|
914
|
+
this.removeChild(this._canvas);
|
|
915
|
+
}
|
|
916
|
+
this._canvas = null;
|
|
917
|
+
this._bar?.destroy();
|
|
918
|
+
this._bar = null;
|
|
919
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
920
|
+
const message = `pc-app failed to create a graphics device (${requested}) - ${reason}`;
|
|
921
|
+
console.error(message, error);
|
|
922
|
+
this.dispatchEvent(new ErrorEvent('error', { message, error }));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
842
925
|
// The element may have been removed while the device was created. disconnectedCallback
|
|
843
926
|
// has already cleaned up the canvas; the device was created inside the await, so it is
|
|
844
927
|
// this boot's to release.
|
|
@@ -917,9 +1000,19 @@ class AppElement extends AsyncElement {
|
|
|
917
1000
|
const app = new playcanvas.AppBase(this._canvas);
|
|
918
1001
|
this._app = app;
|
|
919
1002
|
app.init(createOptions);
|
|
920
|
-
|
|
1003
|
+
// FILLMODE_NONE leaves the canvas's CSS sizing alone (the engine's other fill modes
|
|
1004
|
+
// stamp window-derived pixel sizes onto it); RESOLUTION_AUTO sizes the drawing buffer
|
|
1005
|
+
// from the canvas's client size
|
|
1006
|
+
app.setCanvasFillMode(playcanvas.FILLMODE_NONE);
|
|
921
1007
|
app.setCanvasResolution(playcanvas.RESOLUTION_AUTO);
|
|
922
1008
|
this._pickerCreate();
|
|
1009
|
+
// Track the element's box rather than the window: containers resize without any window
|
|
1010
|
+
// event (splitter drags, flex reflow, animations). Guarded because jsdom has no
|
|
1011
|
+
// ResizeObserver - there, the resolution set above is the only sizing that happens.
|
|
1012
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
1013
|
+
this._resizeObserver = new ResizeObserver(() => this._syncCanvasSize());
|
|
1014
|
+
this._resizeObserver.observe(this);
|
|
1015
|
+
}
|
|
923
1016
|
// Get all pc-asset elements that are direct children of the pc-app element
|
|
924
1017
|
const assetElements = this.querySelectorAll(':scope > pc-asset');
|
|
925
1018
|
for (const assetElement of Array.from(assetElements)) {
|
|
@@ -995,8 +1088,6 @@ class AppElement extends AsyncElement {
|
|
|
995
1088
|
// Dismiss the bar only once a frame has actually rendered; ready fires before the
|
|
996
1089
|
// first rAF tick
|
|
997
1090
|
app.once('frameend', () => this._bar?.complete());
|
|
998
|
-
// Handle window resize to keep the canvas responsive
|
|
999
|
-
window.addEventListener('resize', this._onWindowResize);
|
|
1000
1091
|
this._onReady();
|
|
1001
1092
|
});
|
|
1002
1093
|
}
|
|
@@ -1021,18 +1112,27 @@ class AppElement extends AsyncElement {
|
|
|
1021
1112
|
// no longer holds.
|
|
1022
1113
|
this._hierarchyReady = false;
|
|
1023
1114
|
this._resetReady();
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1115
|
+
// Stop tracking the element's size
|
|
1116
|
+
this._resizeObserver?.disconnect();
|
|
1117
|
+
this._resizeObserver = null;
|
|
1026
1118
|
// Remove the canvas
|
|
1027
1119
|
if (this._canvas && this.contains(this._canvas)) {
|
|
1028
1120
|
this.removeChild(this._canvas);
|
|
1029
1121
|
this._canvas = null;
|
|
1030
1122
|
}
|
|
1031
1123
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1124
|
+
/**
|
|
1125
|
+
* Syncs the drawing buffer and the picker to the canvas's current CSS size. The picker must
|
|
1126
|
+
* track the buffer, or picks would land at stale coordinates after a resize. Skipped while
|
|
1127
|
+
* an XR session presents - the session owns the buffer size.
|
|
1128
|
+
*/
|
|
1129
|
+
_syncCanvasSize() {
|
|
1130
|
+
if (!this.app || this.app.xr?.active) {
|
|
1131
|
+
return;
|
|
1035
1132
|
}
|
|
1133
|
+
this.app.updateCanvasSize();
|
|
1134
|
+
const { width, height } = this.app.graphicsDevice;
|
|
1135
|
+
this._picker?.resize(width, height);
|
|
1036
1136
|
}
|
|
1037
1137
|
_pickerCreate() {
|
|
1038
1138
|
const { width, height } = this.app.graphicsDevice;
|
|
@@ -1150,21 +1250,63 @@ class AppElement extends AsyncElement {
|
|
|
1150
1250
|
}
|
|
1151
1251
|
return null;
|
|
1152
1252
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1253
|
+
/**
|
|
1254
|
+
* Converts a pointer event's client coordinates into drawing-buffer coordinates - the space
|
|
1255
|
+
* the pick buffer and the camera viewports are laid out in. When the canvas has no CSS box
|
|
1256
|
+
* to map through (jsdom; a hidden canvas receives no pointer events in a browser), the
|
|
1257
|
+
* client coordinates are passed through unmapped and `mapped` is false, so callers know the
|
|
1258
|
+
* coordinates correspond to no real geometry.
|
|
1259
|
+
*
|
|
1260
|
+
* @param event - The pointer event to convert.
|
|
1261
|
+
* @param canvas - The canvas the event was dispatched on.
|
|
1262
|
+
* @returns The buffer-space coordinates, and whether they were actually mapped.
|
|
1263
|
+
*/
|
|
1264
|
+
_getPickerCoordinates(event, canvas) {
|
|
1265
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
1266
|
+
if (canvasRect.width === 0 || canvasRect.height === 0) {
|
|
1267
|
+
return { x: event.clientX, y: event.clientY, mapped: false };
|
|
1268
|
+
}
|
|
1269
|
+
const scaleX = canvas.width / canvasRect.width;
|
|
1270
|
+
const scaleY = canvas.height / canvasRect.height;
|
|
1271
|
+
return {
|
|
1272
|
+
x: (event.clientX - canvasRect.left) * scaleX,
|
|
1273
|
+
y: (event.clientY - canvasRect.top) * scaleY,
|
|
1274
|
+
mapped: true
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Whether a camera's viewport contains the point. A camera renders into its normalized
|
|
1279
|
+
* `rect`, whose origin is the bottom-left of the canvas while buffer coordinates run from
|
|
1280
|
+
* the top-left - so the vertical test flips, as the engine's ElementInput flips it for UI
|
|
1281
|
+
* input. The right and bottom edges are exclusive: a viewport rasterizes the half-open
|
|
1282
|
+
* pixel range [left, right) x [top, bottom), so a coordinate on a shared edge belongs to
|
|
1283
|
+
* the viewport whose first pixel it is - never to the one it just left, whose pick buffer
|
|
1284
|
+
* holds nothing there.
|
|
1285
|
+
*
|
|
1286
|
+
* @param camera - The camera to test.
|
|
1287
|
+
* @param x - The x coordinate, in buffer space.
|
|
1288
|
+
* @param y - The y coordinate, in buffer space.
|
|
1289
|
+
* @param canvas - The canvas the coordinates are relative to.
|
|
1290
|
+
* @returns Whether the camera's viewport contains the point.
|
|
1291
|
+
*/
|
|
1292
|
+
_cameraContains(camera, x, y, canvas) {
|
|
1293
|
+
const rect = camera.rect;
|
|
1294
|
+
const left = rect.x * canvas.width;
|
|
1295
|
+
const bottom = (1 - rect.y) * canvas.height;
|
|
1296
|
+
const top = bottom - rect.w * canvas.height;
|
|
1297
|
+
return x >= left && x < left + rect.z * canvas.width && y >= top && y < bottom;
|
|
1164
1298
|
}
|
|
1165
1299
|
/**
|
|
1166
1300
|
* Picks the scene under the pointer and returns the graph node that was hit, or `null`.
|
|
1167
1301
|
*
|
|
1302
|
+
* The camera is resolved the way the engine's ElementInput resolves it for UI input:
|
|
1303
|
+
* enabled cameras are tried topmost-first (they render in ascending `priority` order),
|
|
1304
|
+
* skipping cameras that render to a texture and cameras whose viewport `rect` does not
|
|
1305
|
+
* contain the pointer. A camera that picks nothing ends the search if it clears the color
|
|
1306
|
+
* buffer - its background visually owns the pixel - and otherwise cedes to the cameras
|
|
1307
|
+
* beneath it, so an overlay camera only intercepts picks where it actually drew something.
|
|
1308
|
+
* The pick buffer is prepared per camera, so each camera picks from its own layers.
|
|
1309
|
+
*
|
|
1168
1310
|
* The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
|
|
1169
1311
|
* supported on WebGPU, where it returns an empty selection rather than failing - which
|
|
1170
1312
|
* silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
|
|
@@ -1174,16 +1316,40 @@ class AppElement extends AsyncElement {
|
|
|
1174
1316
|
* @returns The graph node under the pointer, or `null` if nothing was hit.
|
|
1175
1317
|
*/
|
|
1176
1318
|
async _pickNode(event) {
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
this._picker.prepare(camera, this.app.scene);
|
|
1182
|
-
const selection = await this._picker.getSelectionAsync(x, y);
|
|
1183
|
-
if (selection.length === 0)
|
|
1319
|
+
const app = this.app;
|
|
1320
|
+
const picker = this._picker;
|
|
1321
|
+
const canvas = this._canvas;
|
|
1322
|
+
if (!app || !picker || !canvas)
|
|
1184
1323
|
return null;
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1324
|
+
const { x, y, mapped } = this._getPickerCoordinates(event, canvas);
|
|
1325
|
+
// Walked from the end: the array is sorted by ascending priority, so the last camera
|
|
1326
|
+
// renders last and sits on top. Read through .at() because a pick handler may remove
|
|
1327
|
+
// cameras while an earlier iteration's read back is in flight.
|
|
1328
|
+
const cameras = app.systems.camera?.cameras ?? [];
|
|
1329
|
+
for (let i = cameras.length - 1; i >= 0; i--) {
|
|
1330
|
+
const camera = cameras.at(i);
|
|
1331
|
+
// A camera rendering to a texture is not on the canvas.
|
|
1332
|
+
if (!camera || camera.renderTarget)
|
|
1333
|
+
continue;
|
|
1334
|
+
// Coordinates that could not be mapped cannot be tested for containment.
|
|
1335
|
+
if (mapped && !this._cameraContains(camera, x, y, canvas))
|
|
1336
|
+
continue;
|
|
1337
|
+
picker.prepare(camera, app.scene);
|
|
1338
|
+
const selection = await picker.getSelectionAsync(x, y);
|
|
1339
|
+
// The element may have disconnected while the read back was in flight.
|
|
1340
|
+
if (!this._picker || !this.app)
|
|
1341
|
+
return null;
|
|
1342
|
+
if (selection.length > 0) {
|
|
1343
|
+
const item = selection[0];
|
|
1344
|
+
return item instanceof playcanvas.MeshInstance ? item.node : item.entity;
|
|
1345
|
+
}
|
|
1346
|
+
// Nothing hit. A camera that clears the color buffer paints its background over
|
|
1347
|
+
// everything beneath it, so the miss is final; one that does not is an overlay
|
|
1348
|
+
// that the cameras beneath show through, so they get their turn.
|
|
1349
|
+
if (camera.clearColorBuffer)
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
return null;
|
|
1187
1353
|
}
|
|
1188
1354
|
async _onPointerMove(event) {
|
|
1189
1355
|
if (!this._picker || !this.app)
|
|
@@ -1370,7 +1536,7 @@ class AppElement extends AsyncElement {
|
|
|
1370
1536
|
this._maxPixelRatio = value;
|
|
1371
1537
|
if (this.app) {
|
|
1372
1538
|
this.app.graphicsDevice.maxPixelRatio = value;
|
|
1373
|
-
this.
|
|
1539
|
+
this._syncCanvasSize();
|
|
1374
1540
|
}
|
|
1375
1541
|
}
|
|
1376
1542
|
/**
|
|
@@ -2127,6 +2293,10 @@ const processBufferView = (gltfBuffer, buffers, continuation) => {
|
|
|
2127
2293
|
* immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
|
|
2128
2294
|
* elsewhere, or with an unsupported asset type, never become ready.
|
|
2129
2295
|
*
|
|
2296
|
+
* A `lazy` asset loads on first use: the first time any element resolves it by `id` — a model,
|
|
2297
|
+
* a material map, a sky, a script `asset:` reference — or when the `lazy` attribute is removed,
|
|
2298
|
+
* whichever comes first. Until then it stays registered and unloaded.
|
|
2299
|
+
*
|
|
2130
2300
|
* For `texture` and `textureatlas` assets, the texture options (`address-u`, `address-v`,
|
|
2131
2301
|
* `min-filter`, `mag-filter`, `anisotropy`, `mipmaps`, `srgb`, `flip-y`) apply when the texture is
|
|
2132
2302
|
* created and — like `lazy` — are observed: changing one updates a texture that has already
|
|
@@ -2326,7 +2496,9 @@ class AssetElement extends AsyncElement {
|
|
|
2326
2496
|
if (type === 'sprite') {
|
|
2327
2497
|
data = data ?? {};
|
|
2328
2498
|
// Resolve the referenced texture atlas to its (numeric) asset id. The atlas must be
|
|
2329
|
-
// declared before the sprite so its asset already exists in the registry.
|
|
2499
|
+
// declared before the sprite so its asset already exists in the registry. Resolved
|
|
2500
|
+
// with get, not useAsset: creation-time wiring is not a use, and the engine's
|
|
2501
|
+
// sprite handler loads the atlas when the sprite itself loads.
|
|
2330
2502
|
const atlas = this.getAttribute('atlas') ?? data.textureAtlasAsset;
|
|
2331
2503
|
if (typeof atlas === 'string') {
|
|
2332
2504
|
const atlasAsset = AssetElement.get(atlas);
|
|
@@ -2487,13 +2659,18 @@ class AssetElement extends AsyncElement {
|
|
|
2487
2659
|
return this._flipY;
|
|
2488
2660
|
}
|
|
2489
2661
|
/**
|
|
2490
|
-
* Sets whether the asset should be loaded lazily.
|
|
2662
|
+
* Sets whether the asset should be loaded lazily. A lazy asset is registered without being
|
|
2663
|
+
* loaded; it loads on first use - the first time any element resolves it by `id` - or when
|
|
2664
|
+
* this flag is cleared on a registered asset, whichever comes first.
|
|
2491
2665
|
* @param value - The lazy loading flag.
|
|
2492
2666
|
*/
|
|
2493
2667
|
set lazy(value) {
|
|
2494
2668
|
this._lazy = value;
|
|
2495
2669
|
if (this.asset) {
|
|
2496
2670
|
this.asset.preload = !value;
|
|
2671
|
+
if (!value) {
|
|
2672
|
+
this.asset.registry?.load(this.asset);
|
|
2673
|
+
}
|
|
2497
2674
|
}
|
|
2498
2675
|
}
|
|
2499
2676
|
/**
|
|
@@ -2677,6 +2854,26 @@ class AssetElement extends AsyncElement {
|
|
|
2677
2854
|
}
|
|
2678
2855
|
}
|
|
2679
2856
|
customElements.define('pc-asset', AssetElement);
|
|
2857
|
+
/**
|
|
2858
|
+
* Resolves an asset reference for use: {@link AssetElement.get}, plus starting the load of a
|
|
2859
|
+
* registered asset that has not begun one - a `lazy` asset. Every element that consumes assets
|
|
2860
|
+
* resolves its references here, which is what makes `lazy` mean load on first use without any
|
|
2861
|
+
* consumer having to remember the load. The load is asynchronous - callers observe the asset's
|
|
2862
|
+
* `load` event for the resource.
|
|
2863
|
+
*
|
|
2864
|
+
* @param id - The `id` of the `<pc-asset>` element.
|
|
2865
|
+
* @returns The asset, or `undefined`.
|
|
2866
|
+
* @internal
|
|
2867
|
+
*/
|
|
2868
|
+
const useAsset = (id) => {
|
|
2869
|
+
const asset = AssetElement.get(id);
|
|
2870
|
+
// load() ignores an asset that is already loaded or loading, so repeated resolution
|
|
2871
|
+
// costs nothing.
|
|
2872
|
+
if (asset) {
|
|
2873
|
+
asset.registry?.load(asset);
|
|
2874
|
+
}
|
|
2875
|
+
return asset;
|
|
2876
|
+
};
|
|
2680
2877
|
|
|
2681
2878
|
/**
|
|
2682
2879
|
* Represents a component in the PlayCanvas engine.
|
|
@@ -2960,15 +3157,15 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
2960
3157
|
if (imageEntity) {
|
|
2961
3158
|
data.imageEntity = imageEntity;
|
|
2962
3159
|
}
|
|
2963
|
-
const hoverSpriteAsset =
|
|
3160
|
+
const hoverSpriteAsset = useAsset(this._hoverSpriteAsset);
|
|
2964
3161
|
if (hoverSpriteAsset) {
|
|
2965
3162
|
data.hoverSpriteAsset = hoverSpriteAsset.id;
|
|
2966
3163
|
}
|
|
2967
|
-
const pressedSpriteAsset =
|
|
3164
|
+
const pressedSpriteAsset = useAsset(this._pressedSpriteAsset);
|
|
2968
3165
|
if (pressedSpriteAsset) {
|
|
2969
3166
|
data.pressedSpriteAsset = pressedSpriteAsset.id;
|
|
2970
3167
|
}
|
|
2971
|
-
const inactiveSpriteAsset =
|
|
3168
|
+
const inactiveSpriteAsset = useAsset(this._inactiveSpriteAsset);
|
|
2972
3169
|
if (inactiveSpriteAsset) {
|
|
2973
3170
|
data.inactiveSpriteAsset = inactiveSpriteAsset.id;
|
|
2974
3171
|
}
|
|
@@ -3130,7 +3327,7 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
3130
3327
|
*/
|
|
3131
3328
|
set hoverSpriteAsset(value) {
|
|
3132
3329
|
this._hoverSpriteAsset = value;
|
|
3133
|
-
const asset =
|
|
3330
|
+
const asset = useAsset(value);
|
|
3134
3331
|
if (this.component && asset) {
|
|
3135
3332
|
this.component.hoverSpriteAsset = asset.id;
|
|
3136
3333
|
}
|
|
@@ -3166,7 +3363,7 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
3166
3363
|
*/
|
|
3167
3364
|
set pressedSpriteAsset(value) {
|
|
3168
3365
|
this._pressedSpriteAsset = value;
|
|
3169
|
-
const asset =
|
|
3366
|
+
const asset = useAsset(value);
|
|
3170
3367
|
if (this.component && asset) {
|
|
3171
3368
|
this.component.pressedSpriteAsset = asset.id;
|
|
3172
3369
|
}
|
|
@@ -3202,7 +3399,7 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
3202
3399
|
*/
|
|
3203
3400
|
set inactiveSpriteAsset(value) {
|
|
3204
3401
|
this._inactiveSpriteAsset = value;
|
|
3205
|
-
const asset =
|
|
3402
|
+
const asset = useAsset(value);
|
|
3206
3403
|
if (this.component && asset) {
|
|
3207
3404
|
this.component.inactiveSpriteAsset = asset.id;
|
|
3208
3405
|
}
|
|
@@ -4060,15 +4257,15 @@ class ElementComponentElement extends ComponentElement {
|
|
|
4060
4257
|
};
|
|
4061
4258
|
// Asset references are resolved from `<pc-asset>` element ids to engine asset ids. They are
|
|
4062
4259
|
// only included when they resolve, so image/group elements (with no font) don't error.
|
|
4063
|
-
const fontAsset =
|
|
4260
|
+
const fontAsset = useAsset(this._fontAsset);
|
|
4064
4261
|
if (fontAsset) {
|
|
4065
4262
|
data.fontAsset = fontAsset.id;
|
|
4066
4263
|
}
|
|
4067
|
-
const spriteAsset =
|
|
4264
|
+
const spriteAsset = useAsset(this._spriteAsset);
|
|
4068
4265
|
if (spriteAsset) {
|
|
4069
4266
|
data.spriteAsset = spriteAsset.id;
|
|
4070
4267
|
}
|
|
4071
|
-
const textureAsset =
|
|
4268
|
+
const textureAsset = useAsset(this._textureAsset);
|
|
4072
4269
|
if (textureAsset) {
|
|
4073
4270
|
data.textureAsset = textureAsset.id;
|
|
4074
4271
|
}
|
|
@@ -4182,7 +4379,7 @@ class ElementComponentElement extends ComponentElement {
|
|
|
4182
4379
|
*/
|
|
4183
4380
|
set fontAsset(value) {
|
|
4184
4381
|
this._fontAsset = value;
|
|
4185
|
-
const asset =
|
|
4382
|
+
const asset = useAsset(value);
|
|
4186
4383
|
if (this.component && asset) {
|
|
4187
4384
|
this.component.fontAsset = asset.id;
|
|
4188
4385
|
}
|
|
@@ -4337,7 +4534,7 @@ class ElementComponentElement extends ComponentElement {
|
|
|
4337
4534
|
*/
|
|
4338
4535
|
set spriteAsset(value) {
|
|
4339
4536
|
this._spriteAsset = value;
|
|
4340
|
-
const asset =
|
|
4537
|
+
const asset = useAsset(value);
|
|
4341
4538
|
if (this.component && asset) {
|
|
4342
4539
|
this.component.spriteAsset = asset.id;
|
|
4343
4540
|
}
|
|
@@ -4389,7 +4586,7 @@ class ElementComponentElement extends ComponentElement {
|
|
|
4389
4586
|
*/
|
|
4390
4587
|
set textureAsset(value) {
|
|
4391
4588
|
this._textureAsset = value;
|
|
4392
|
-
const asset =
|
|
4589
|
+
const asset = useAsset(value);
|
|
4393
4590
|
if (this.component && asset) {
|
|
4394
4591
|
this.component.textureAsset = asset.id;
|
|
4395
4592
|
}
|
|
@@ -5624,13 +5821,14 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
5624
5821
|
super('particlesystem');
|
|
5625
5822
|
}
|
|
5626
5823
|
getInitialComponentData() {
|
|
5627
|
-
const asset =
|
|
5628
|
-
|
|
5824
|
+
const asset = useAsset(this._asset);
|
|
5825
|
+
// A lazy config has no resource yet - _loadAsset applies it once the load completes
|
|
5826
|
+
if (!asset || !asset.resource) {
|
|
5629
5827
|
return {};
|
|
5630
5828
|
}
|
|
5631
5829
|
if (asset.resource.colorMapAsset) {
|
|
5632
5830
|
const id = asset.resource.colorMapAsset;
|
|
5633
|
-
const colorMapAsset =
|
|
5831
|
+
const colorMapAsset = useAsset(id)?.id;
|
|
5634
5832
|
if (colorMapAsset) {
|
|
5635
5833
|
asset.resource.colorMapAsset = colorMapAsset;
|
|
5636
5834
|
}
|
|
@@ -5656,9 +5854,8 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
5656
5854
|
}
|
|
5657
5855
|
}
|
|
5658
5856
|
async _loadAsset() {
|
|
5659
|
-
|
|
5660
|
-
const
|
|
5661
|
-
const asset = AssetElement.get(this._asset);
|
|
5857
|
+
await this.closestApp?.ready();
|
|
5858
|
+
const asset = useAsset(this._asset);
|
|
5662
5859
|
if (!asset) {
|
|
5663
5860
|
return;
|
|
5664
5861
|
}
|
|
@@ -5669,7 +5866,6 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
5669
5866
|
asset.once('load', () => {
|
|
5670
5867
|
this.applyConfig(asset.resource);
|
|
5671
5868
|
});
|
|
5672
|
-
app.assets.load(asset);
|
|
5673
5869
|
}
|
|
5674
5870
|
}
|
|
5675
5871
|
/**
|
|
@@ -6085,7 +6281,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6085
6281
|
this._scheduleUpdate();
|
|
6086
6282
|
return;
|
|
6087
6283
|
}
|
|
6088
|
-
const asset =
|
|
6284
|
+
const asset = useAsset(id);
|
|
6089
6285
|
if (!asset)
|
|
6090
6286
|
return;
|
|
6091
6287
|
if (asset.loaded) {
|
|
@@ -9186,7 +9382,7 @@ const camelToKebab = (name) => {
|
|
|
9186
9382
|
* @returns The asset, or `raw`.
|
|
9187
9383
|
*/
|
|
9188
9384
|
const assetConversion = (rest, raw) => {
|
|
9189
|
-
const asset =
|
|
9385
|
+
const asset = useAsset(rest);
|
|
9190
9386
|
if (asset) {
|
|
9191
9387
|
return asset;
|
|
9192
9388
|
}
|
|
@@ -10006,7 +10202,7 @@ class SoundSlotElement extends AsyncElement {
|
|
|
10006
10202
|
set asset(value) {
|
|
10007
10203
|
this._asset = value;
|
|
10008
10204
|
if (this.soundSlot) {
|
|
10009
|
-
const id =
|
|
10205
|
+
const id = useAsset(value)?.id;
|
|
10010
10206
|
if (id) {
|
|
10011
10207
|
this.soundSlot.asset = id;
|
|
10012
10208
|
}
|
|
@@ -10213,7 +10409,7 @@ class GSplatComponentElement extends ComponentElement {
|
|
|
10213
10409
|
}
|
|
10214
10410
|
getInitialComponentData() {
|
|
10215
10411
|
return {
|
|
10216
|
-
asset:
|
|
10412
|
+
asset: useAsset(this._asset),
|
|
10217
10413
|
castShadows: this._castShadows,
|
|
10218
10414
|
lodBaseDistance: this._lodBaseDistance,
|
|
10219
10415
|
lodMultiplier: this._lodMultiplier,
|
|
@@ -10234,7 +10430,7 @@ class GSplatComponentElement extends ComponentElement {
|
|
|
10234
10430
|
*/
|
|
10235
10431
|
set asset(value) {
|
|
10236
10432
|
this._asset = value;
|
|
10237
|
-
const asset =
|
|
10433
|
+
const asset = useAsset(value);
|
|
10238
10434
|
if (this.component && asset) {
|
|
10239
10435
|
this.component.asset = asset;
|
|
10240
10436
|
}
|
|
@@ -10512,8 +10708,7 @@ class ModelElement extends AsyncElement {
|
|
|
10512
10708
|
if (generation !== this._loadGeneration) {
|
|
10513
10709
|
return;
|
|
10514
10710
|
}
|
|
10515
|
-
const
|
|
10516
|
-
const asset = AssetElement.get(this._asset);
|
|
10711
|
+
const asset = useAsset(this._asset);
|
|
10517
10712
|
if (!asset) {
|
|
10518
10713
|
// An empty id is a legitimate transient (the asset may be assigned later); a
|
|
10519
10714
|
// non-empty one that resolves to nothing is a dead end - say so rather than staying
|
|
@@ -10549,7 +10744,6 @@ class ModelElement extends AsyncElement {
|
|
|
10549
10744
|
}));
|
|
10550
10745
|
this._onReady();
|
|
10551
10746
|
});
|
|
10552
|
-
app.assets.load(asset);
|
|
10553
10747
|
}
|
|
10554
10748
|
}
|
|
10555
10749
|
_unloadModel() {
|
|
@@ -11514,7 +11708,7 @@ class SkyElement extends AsyncElement {
|
|
|
11514
11708
|
return;
|
|
11515
11709
|
}
|
|
11516
11710
|
this._appElement = appElement;
|
|
11517
|
-
const asset =
|
|
11711
|
+
const asset = useAsset(this._asset);
|
|
11518
11712
|
if (!asset) {
|
|
11519
11713
|
return;
|
|
11520
11714
|
}
|
|
@@ -11533,7 +11727,6 @@ class SkyElement extends AsyncElement {
|
|
|
11533
11727
|
}
|
|
11534
11728
|
this._generateSkybox(asset);
|
|
11535
11729
|
});
|
|
11536
|
-
app.assets.load(asset);
|
|
11537
11730
|
}
|
|
11538
11731
|
}
|
|
11539
11732
|
_unloadSkybox() {
|