@playcanvas/web-components 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +151 -35
- package/dist/asset.d.ts +9 -0
- package/dist/async-element.d.ts +12 -2
- package/dist/components/camera-component.d.ts +13 -13
- package/dist/components/component.d.ts +5 -4
- package/dist/components/screen-component.d.ts +25 -4
- package/dist/components/scrollview-component.d.ts +6 -4
- package/dist/custom-elements.json +812 -196
- package/dist/entity.d.ts +18 -4
- package/dist/loading-bar.d.ts +35 -0
- package/dist/material.d.ts +8 -2
- package/dist/parse.d.ts +4 -2
- package/dist/pwc.cjs +661 -229
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +661 -229
- package/dist/pwc.js.map +1 -1
- package/dist/pwc.min.js +1 -1
- package/dist/pwc.min.js.map +1 -1
- package/dist/pwc.min.mjs +1 -1
- package/dist/pwc.min.mjs.map +1 -1
- package/dist/pwc.mjs +662 -230
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.ts +11 -3
- package/dist/sky.d.ts +12 -11
- package/dist/vscode.html-custom-data.json +47 -22
- package/dist/web-types.json +384 -121
- package/package.json +1 -1
- package/src/app.ts +333 -124
- package/src/asset.ts +25 -0
- package/src/async-element.ts +14 -4
- package/src/components/camera-component.ts +31 -26
- package/src/components/component.ts +6 -5
- package/src/components/screen-component.ts +42 -12
- package/src/components/scrollview-component.ts +6 -4
- package/src/entity.ts +61 -27
- package/src/loading-bar.ts +122 -0
- package/src/material.ts +10 -4
- package/src/parse.ts +5 -3
- package/src/scene.ts +49 -19
- package/src/sky.ts +26 -25
package/dist/pwc.cjs
CHANGED
|
@@ -18,11 +18,21 @@ class AsyncElement extends HTMLElement {
|
|
|
18
18
|
this._readyResolve = resolve;
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* The nearest ancestor `<pc-app>` element, or `null` if this element has no `<pc-app>`
|
|
23
|
+
* ancestor. The search starts at the parent, so an element never resolves to itself.
|
|
24
|
+
* @returns The closest app element, or `null`.
|
|
25
|
+
*/
|
|
21
26
|
get closestApp() {
|
|
22
|
-
return this.parentElement?.closest('pc-app');
|
|
27
|
+
return this.parentElement?.closest('pc-app') ?? null;
|
|
23
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The nearest ancestor `<pc-entity>` element, or `null` if this element has no `<pc-entity>`
|
|
31
|
+
* ancestor. The search starts at the parent, so an element never resolves to itself.
|
|
32
|
+
* @returns The closest entity element, or `null`.
|
|
33
|
+
*/
|
|
24
34
|
get closestEntity() {
|
|
25
|
-
return this.parentElement?.closest('pc-entity');
|
|
35
|
+
return this.parentElement?.closest('pc-entity') ?? null;
|
|
26
36
|
}
|
|
27
37
|
/**
|
|
28
38
|
* Called when the element is fully initialized and ready. Subclasses should call this when
|
|
@@ -116,6 +126,116 @@ class ModuleElement extends HTMLElement {
|
|
|
116
126
|
}
|
|
117
127
|
customElements.define('pc-module', ModuleElement);
|
|
118
128
|
|
|
129
|
+
/** Covers the 0.2s opacity transition; jsdom never fires transitionend, so removal is timed. */
|
|
130
|
+
const REMOVAL_DELAY_MS = 250;
|
|
131
|
+
/**
|
|
132
|
+
* The slim progress bar `<pc-app>` shows while it boots and preloads. An implementation detail of
|
|
133
|
+
* AppElement rather than a custom element, so its shape can change without a breaking change.
|
|
134
|
+
*
|
|
135
|
+
* All styling is inline, so the library injects no stylesheet. The colors and height resolve CSS
|
|
136
|
+
* custom properties — `--pc-loading-bar-color`, `--pc-loading-bar-background` and
|
|
137
|
+
* `--pc-loading-bar-height` — so a page can theme the bar from `pc-app` or `:root`.
|
|
138
|
+
*/
|
|
139
|
+
class LoadingBar {
|
|
140
|
+
_track;
|
|
141
|
+
_fill;
|
|
142
|
+
_sweep = null;
|
|
143
|
+
_removal = null;
|
|
144
|
+
/**
|
|
145
|
+
* Creates the bar and appends it to `parent`, starting in the indeterminate state.
|
|
146
|
+
* @param parent - The element to append the bar to.
|
|
147
|
+
*/
|
|
148
|
+
constructor(parent) {
|
|
149
|
+
this._track = document.createElement('div');
|
|
150
|
+
this._track.setAttribute('role', 'progressbar');
|
|
151
|
+
this._track.setAttribute('aria-label', 'Loading');
|
|
152
|
+
this._track.setAttribute('aria-valuemin', '0');
|
|
153
|
+
this._track.setAttribute('aria-valuemax', '100');
|
|
154
|
+
// Fixed positioning matches the canvas, which always fills the window (FILLMODE_FILL_WINDOW)
|
|
155
|
+
this._track.style.cssText = [
|
|
156
|
+
'position: fixed',
|
|
157
|
+
'top: 0',
|
|
158
|
+
'left: 0',
|
|
159
|
+
'width: 100%',
|
|
160
|
+
'height: var(--pc-loading-bar-height, 3px)',
|
|
161
|
+
'background: var(--pc-loading-bar-background, rgba(0, 0, 0, 0.1))',
|
|
162
|
+
'z-index: 10000',
|
|
163
|
+
'pointer-events: none',
|
|
164
|
+
'opacity: 1',
|
|
165
|
+
'transition: opacity 0.2s ease'
|
|
166
|
+
].join('; ');
|
|
167
|
+
this._fill = document.createElement('div');
|
|
168
|
+
this._fill.style.cssText = [
|
|
169
|
+
'width: 100%',
|
|
170
|
+
'height: 100%',
|
|
171
|
+
'transform-origin: left center',
|
|
172
|
+
'transform: scaleX(0)',
|
|
173
|
+
'background: var(--pc-loading-bar-color, #f60)',
|
|
174
|
+
'transition: transform 0.2s ease'
|
|
175
|
+
].join('; ');
|
|
176
|
+
this._track.appendChild(this._fill);
|
|
177
|
+
parent.appendChild(this._track);
|
|
178
|
+
// Indeterminate sweep until the first progress() call reports a real total. No
|
|
179
|
+
// aria-valuenow is set, which is what marks a progressbar indeterminate. jsdom has no Web
|
|
180
|
+
// Animations API, so the guard degrades to a static bar there rather than crashing boot.
|
|
181
|
+
if (typeof this._fill.animate === 'function') {
|
|
182
|
+
this._sweep = this._fill.animate([
|
|
183
|
+
{ transform: 'scaleX(0.25) translateX(-100%)' },
|
|
184
|
+
{ transform: 'scaleX(0.25) translateX(500%)' }
|
|
185
|
+
], {
|
|
186
|
+
duration: 1000,
|
|
187
|
+
iterations: Infinity,
|
|
188
|
+
easing: 'ease-in-out'
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Reflects preload progress, switching the bar from indeterminate to determinate on the first
|
|
194
|
+
* call.
|
|
195
|
+
* @param loaded - The number of assets that have finished loading.
|
|
196
|
+
* @param total - The number of assets being preloaded.
|
|
197
|
+
*/
|
|
198
|
+
progress(loaded, total) {
|
|
199
|
+
if (this._sweep) {
|
|
200
|
+
this._sweep.cancel();
|
|
201
|
+
this._sweep = null;
|
|
202
|
+
}
|
|
203
|
+
const fraction = total === 0 ? 1 : loaded / total;
|
|
204
|
+
this._track.setAttribute('aria-valuenow', String(Math.round(fraction * 100)));
|
|
205
|
+
this._fill.style.transform = `scaleX(${fraction})`;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Fills the bar, fades it out and removes it. Idempotent.
|
|
209
|
+
*/
|
|
210
|
+
complete() {
|
|
211
|
+
if (this._removal !== null) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (this._sweep) {
|
|
215
|
+
this._sweep.cancel();
|
|
216
|
+
this._sweep = null;
|
|
217
|
+
}
|
|
218
|
+
this._track.setAttribute('aria-valuenow', '100');
|
|
219
|
+
this._fill.style.transform = 'scaleX(1)';
|
|
220
|
+
this._track.style.opacity = '0';
|
|
221
|
+
this._removal = setTimeout(() => this._track.remove(), REMOVAL_DELAY_MS);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Removes the bar immediately, cancelling any pending fade. Idempotent.
|
|
225
|
+
*/
|
|
226
|
+
destroy() {
|
|
227
|
+
if (this._sweep) {
|
|
228
|
+
this._sweep.cancel();
|
|
229
|
+
this._sweep = null;
|
|
230
|
+
}
|
|
231
|
+
if (this._removal !== null) {
|
|
232
|
+
clearTimeout(this._removal);
|
|
233
|
+
this._removal = null;
|
|
234
|
+
}
|
|
235
|
+
this._track.remove();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
119
239
|
const CSS_COLORS = {
|
|
120
240
|
aliceblue: '#f0f8ff',
|
|
121
241
|
antiquewhite: '#faebd7',
|
|
@@ -369,7 +489,9 @@ const parseColor = (value, defaultValue, attribute) => {
|
|
|
369
489
|
* the value is invalid — the latter also logs a warning listing the valid names.
|
|
370
490
|
*
|
|
371
491
|
* @param value - The attribute value to parse (`null` when the attribute is absent).
|
|
372
|
-
* @param valid - The valid names: an array, or a map whose keys are the valid names.
|
|
492
|
+
* @param valid - The valid names: an array, or a map whose keys are the valid names. Only the keys
|
|
493
|
+
* are read, so the map's value type is unconstrained - engine enums are mostly numeric constants,
|
|
494
|
+
* but some (e.g. `SCALEMODE_BLEND`) are strings.
|
|
373
495
|
* @param defaultValue - The value to use when the attribute is absent or invalid.
|
|
374
496
|
* @param attribute - The attribute name, used in the warning message.
|
|
375
497
|
* @returns The resolved enum name.
|
|
@@ -543,6 +665,11 @@ const getEntity = (ref) => {
|
|
|
543
665
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
544
666
|
* The AppElement interface also inherits the properties and methods of the
|
|
545
667
|
* {@link HTMLElement} interface.
|
|
668
|
+
*
|
|
669
|
+
* @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
|
|
670
|
+
* `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
|
|
671
|
+
* Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
|
|
672
|
+
* not bubble.
|
|
546
673
|
*/
|
|
547
674
|
class AppElement extends AsyncElement {
|
|
548
675
|
/**
|
|
@@ -552,10 +679,24 @@ class AppElement extends AsyncElement {
|
|
|
552
679
|
_alpha = true;
|
|
553
680
|
_backend = 'webgpu';
|
|
554
681
|
_antialias = true;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
682
|
+
_depthBuffer = true;
|
|
683
|
+
_stencilBuffer = true;
|
|
684
|
+
_maxPixelRatio = Infinity;
|
|
685
|
+
_loadingBar = true;
|
|
686
|
+
/**
|
|
687
|
+
* Set once the graphics options above have been handed to `createGraphicsDevice`, after which
|
|
688
|
+
* writing any of them changes nothing. Guards the warning in {@link _warnIfBooted}, and is
|
|
689
|
+
* cleared on disconnect so a re-connected element boots from its current attributes.
|
|
690
|
+
*/
|
|
691
|
+
_optionsLocked = false;
|
|
692
|
+
_bar = null;
|
|
558
693
|
_hierarchyReady = false;
|
|
694
|
+
/**
|
|
695
|
+
* The elements backing this application's entities, keyed by the entity itself. Registered
|
|
696
|
+
* by EntityElement at creation and removed when an entity is destroyed, this joins engine
|
|
697
|
+
* scene nodes back to their owning elements by identity - never by name.
|
|
698
|
+
*/
|
|
699
|
+
_entityElements = new Map();
|
|
559
700
|
_picker = null;
|
|
560
701
|
_hasPointerListeners = {
|
|
561
702
|
pointerenter: false,
|
|
@@ -565,20 +706,34 @@ class AppElement extends AsyncElement {
|
|
|
565
706
|
pointermove: false
|
|
566
707
|
};
|
|
567
708
|
_hoveredEntity = null;
|
|
709
|
+
// Identifies the newest in-flight hover pick, so out-of-order results can be discarded
|
|
710
|
+
_pickToken = 0;
|
|
568
711
|
_pointerHandlers = {
|
|
569
712
|
pointermove: null,
|
|
570
713
|
pointerdown: null,
|
|
571
714
|
pointerup: null
|
|
572
715
|
};
|
|
573
716
|
_app = null;
|
|
717
|
+
_loadProgress = 0;
|
|
574
718
|
/**
|
|
575
|
-
* The PlayCanvas application instance.
|
|
576
|
-
* {@link whenReady} or the element's `ready()`
|
|
577
|
-
*
|
|
719
|
+
* The PlayCanvas application instance. `null` until the element is ready, and again once it
|
|
720
|
+
* has been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
721
|
+
* promise before accessing it.
|
|
722
|
+
* @returns The application instance, or `null`.
|
|
578
723
|
*/
|
|
579
724
|
get app() {
|
|
580
725
|
return this._app;
|
|
581
726
|
}
|
|
727
|
+
/**
|
|
728
|
+
* The asset preload progress of the application, as a fraction from 0 to 1. It is 0 until
|
|
729
|
+
* preloading begins (and again once the element has been removed from the document), and 1
|
|
730
|
+
* once preloading has finished — including when there was nothing to preload. Read this to
|
|
731
|
+
* initialize a loading UI; subsequent updates arrive via the `progress` event.
|
|
732
|
+
* @returns The preload progress.
|
|
733
|
+
*/
|
|
734
|
+
get loadProgress() {
|
|
735
|
+
return this._loadProgress;
|
|
736
|
+
}
|
|
582
737
|
/**
|
|
583
738
|
* Creates a new AppElement instance.
|
|
584
739
|
*
|
|
@@ -590,6 +745,11 @@ class AppElement extends AsyncElement {
|
|
|
590
745
|
this._onWindowResize = this._onWindowResize.bind(this);
|
|
591
746
|
}
|
|
592
747
|
async connectedCallback() {
|
|
748
|
+
// Created before the first await, so the bar is visible while modules and the graphics
|
|
749
|
+
// device are created, and exists before any disconnect could need to clean it up
|
|
750
|
+
if (this._loadingBar && !this._bar) {
|
|
751
|
+
this._bar = new LoadingBar(this);
|
|
752
|
+
}
|
|
593
753
|
// Get all pc-module elements that are direct children of the pc-app element
|
|
594
754
|
const moduleElements = this.querySelectorAll(':scope > pc-module');
|
|
595
755
|
// Wait for all modules to load
|
|
@@ -604,15 +764,19 @@ class AppElement extends AsyncElement {
|
|
|
604
764
|
null: ['null']
|
|
605
765
|
};
|
|
606
766
|
const deviceTypes = backendToDeviceTypes[this._backend] || [];
|
|
767
|
+
this._optionsLocked = true;
|
|
607
768
|
const device = await playcanvas.createGraphicsDevice(this._canvas, {
|
|
608
769
|
// @ts-ignore - alpha needs to be documented
|
|
609
770
|
alpha: this._alpha,
|
|
610
771
|
antialias: this._antialias,
|
|
611
|
-
depth: this.
|
|
772
|
+
depth: this._depthBuffer,
|
|
612
773
|
deviceTypes: deviceTypes,
|
|
613
|
-
stencil: this.
|
|
774
|
+
stencil: this._stencilBuffer
|
|
614
775
|
});
|
|
615
|
-
|
|
776
|
+
// Assigned rather than resolved to a number here: the engine caps against the live
|
|
777
|
+
// window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
|
|
778
|
+
// display when a window moves between monitors of differing density.
|
|
779
|
+
device.maxPixelRatio = this._maxPixelRatio;
|
|
616
780
|
const createOptions = new playcanvas.AppOptions();
|
|
617
781
|
createOptions.graphicsDevice = device;
|
|
618
782
|
createOptions.keyboard = new playcanvas.Keyboard(window);
|
|
@@ -677,10 +841,11 @@ class AppElement extends AsyncElement {
|
|
|
677
841
|
createOptions.lightmapper = playcanvas.Lightmapper;
|
|
678
842
|
createOptions.batchManager = playcanvas.BatchManager;
|
|
679
843
|
createOptions.xr = playcanvas.XrManager;
|
|
680
|
-
|
|
681
|
-
this.app
|
|
682
|
-
|
|
683
|
-
|
|
844
|
+
const app = new playcanvas.AppBase(this._canvas);
|
|
845
|
+
this._app = app;
|
|
846
|
+
app.init(createOptions);
|
|
847
|
+
app.setCanvasFillMode(playcanvas.FILLMODE_FILL_WINDOW);
|
|
848
|
+
app.setCanvasResolution(playcanvas.RESOLUTION_AUTO);
|
|
684
849
|
this._pickerCreate();
|
|
685
850
|
// Get all pc-asset elements that are direct children of the pc-app element
|
|
686
851
|
const assetElements = this.querySelectorAll(':scope > pc-asset');
|
|
@@ -688,7 +853,7 @@ class AppElement extends AsyncElement {
|
|
|
688
853
|
assetElement.createAsset();
|
|
689
854
|
const asset = assetElement.asset;
|
|
690
855
|
if (asset) {
|
|
691
|
-
|
|
856
|
+
app.assets.add(asset);
|
|
692
857
|
}
|
|
693
858
|
});
|
|
694
859
|
// Get all pc-material elements that are direct children of the pc-app element
|
|
@@ -699,29 +864,57 @@ class AppElement extends AsyncElement {
|
|
|
699
864
|
// Create all entities
|
|
700
865
|
const entityElements = this.querySelectorAll('pc-entity');
|
|
701
866
|
Array.from(entityElements).forEach((entityElement) => {
|
|
702
|
-
entityElement.createEntity(
|
|
867
|
+
entityElement.createEntity(app);
|
|
703
868
|
});
|
|
704
869
|
// Build hierarchy
|
|
705
870
|
entityElements.forEach((entityElement) => {
|
|
706
|
-
entityElement.buildHierarchy(
|
|
871
|
+
entityElement.buildHierarchy(app);
|
|
707
872
|
});
|
|
708
873
|
this._hierarchyReady = true;
|
|
874
|
+
// Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
|
|
875
|
+
// listener must be attached before preload() is called: an asset that is already loaded
|
|
876
|
+
// ticks synchronously inside it.
|
|
877
|
+
const total = app.assets.list({ preload: true }).length;
|
|
878
|
+
let loaded = 0;
|
|
879
|
+
const onPreloadProgress = () => {
|
|
880
|
+
loaded += 1;
|
|
881
|
+
this._loadProgress = loaded / total;
|
|
882
|
+
this._bar?.progress(loaded, total);
|
|
883
|
+
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded, total }));
|
|
884
|
+
};
|
|
885
|
+
app.on('preload:progress', onPreloadProgress);
|
|
886
|
+
this._loadProgress = total === 0 ? 1 : 0;
|
|
887
|
+
this._bar?.progress(0, total);
|
|
888
|
+
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
|
|
709
889
|
// Load assets before starting the application
|
|
710
|
-
|
|
890
|
+
app.preload(() => {
|
|
891
|
+
// Scope the counter to this preload pass, so a later app.preload() call by user code
|
|
892
|
+
// cannot push `loaded` past `total`
|
|
893
|
+
app.off('preload:progress', onPreloadProgress);
|
|
894
|
+
this._loadProgress = 1;
|
|
711
895
|
// Start the application
|
|
712
|
-
|
|
896
|
+
app.start();
|
|
897
|
+
// Dismiss the bar only once a frame has actually rendered; ready fires before the
|
|
898
|
+
// first rAF tick
|
|
899
|
+
app.once('frameend', () => this._bar?.complete());
|
|
713
900
|
// Handle window resize to keep the canvas responsive
|
|
714
901
|
window.addEventListener('resize', this._onWindowResize);
|
|
715
902
|
this._onReady();
|
|
716
903
|
});
|
|
717
904
|
}
|
|
718
905
|
disconnectedCallback() {
|
|
906
|
+
this._optionsLocked = false;
|
|
719
907
|
this._pickerDestroy();
|
|
720
|
-
// Clean up the application
|
|
721
|
-
|
|
722
|
-
|
|
908
|
+
// Clean up the application. Destroying it destroys every entity, whose destroy hooks
|
|
909
|
+
// unregister them - clear() covers any entity the engine no longer reached.
|
|
910
|
+
if (this._app) {
|
|
911
|
+
this._app.destroy();
|
|
723
912
|
this._app = null;
|
|
724
913
|
}
|
|
914
|
+
this._entityElements.clear();
|
|
915
|
+
this._loadProgress = 0;
|
|
916
|
+
this._bar?.destroy();
|
|
917
|
+
this._bar = null;
|
|
725
918
|
// Remove event listeners
|
|
726
919
|
window.removeEventListener('resize', this._onWindowResize);
|
|
727
920
|
// Remove the canvas
|
|
@@ -738,10 +931,17 @@ class AppElement extends AsyncElement {
|
|
|
738
931
|
_pickerCreate() {
|
|
739
932
|
const { width, height } = this.app.graphicsDevice;
|
|
740
933
|
this._picker = new playcanvas.Picker(this.app, width, height);
|
|
741
|
-
// Create bound handlers but don't attach them yet
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
934
|
+
// Create bound handlers but don't attach them yet. The handlers pick asynchronously, so
|
|
935
|
+
// each is wrapped to discard the promise - a listener must not return one, and nothing
|
|
936
|
+
// awaits the result.
|
|
937
|
+
const listener = (handler) => {
|
|
938
|
+
return (event) => {
|
|
939
|
+
handler.call(this, event);
|
|
940
|
+
};
|
|
941
|
+
};
|
|
942
|
+
this._pointerHandlers.pointermove = listener(this._onPointerMove);
|
|
943
|
+
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
944
|
+
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
745
945
|
// Listen for pointer listeners being added/removed
|
|
746
946
|
['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
|
|
747
947
|
this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
|
|
@@ -777,6 +977,74 @@ class AppElement extends AsyncElement {
|
|
|
777
977
|
pointermove: false
|
|
778
978
|
};
|
|
779
979
|
}
|
|
980
|
+
/**
|
|
981
|
+
* Registers the element that created an entity. Called by EntityElement when it creates its
|
|
982
|
+
* entity.
|
|
983
|
+
*
|
|
984
|
+
* @param entity - The entity.
|
|
985
|
+
* @param element - The element that created it.
|
|
986
|
+
* @ignore
|
|
987
|
+
*/
|
|
988
|
+
_registerEntityElement(entity, element) {
|
|
989
|
+
this._entityElements.set(entity, element);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Removes the registration for a destroyed entity. Called by EntityElement.
|
|
993
|
+
*
|
|
994
|
+
* @param entity - The entity.
|
|
995
|
+
* @ignore
|
|
996
|
+
*/
|
|
997
|
+
_unregisterEntityElement(entity) {
|
|
998
|
+
this._entityElements.delete(entity);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Returns the `<pc-entity>` element whose backing entity is `entity`, or `null` if the
|
|
1002
|
+
* entity was not created by an element of this application - for example, a node inside a
|
|
1003
|
+
* model's instantiated hierarchy, or an entity created through the engine API.
|
|
1004
|
+
*
|
|
1005
|
+
* @param entity - The entity to look up.
|
|
1006
|
+
* @returns The element backing the entity, or `null`.
|
|
1007
|
+
*/
|
|
1008
|
+
elementFromEntity(entity) {
|
|
1009
|
+
return this._entityElements.get(entity) ?? null;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Resolves the element that owns a picked node: the nearest node up the parent chain -
|
|
1013
|
+
* starting with the node itself - that was created by a `<pc-entity>` of this application.
|
|
1014
|
+
* A hit inside a model's instantiated hierarchy therefore resolves to the element hosting
|
|
1015
|
+
* the model.
|
|
1016
|
+
*
|
|
1017
|
+
* @param node - The picked node, or `null`.
|
|
1018
|
+
* @returns The owning element, or `null`.
|
|
1019
|
+
*/
|
|
1020
|
+
_elementFromNode(node) {
|
|
1021
|
+
while (node !== null) {
|
|
1022
|
+
const element = this._entityElements.get(node);
|
|
1023
|
+
if (element) {
|
|
1024
|
+
return element;
|
|
1025
|
+
}
|
|
1026
|
+
node = node.parent;
|
|
1027
|
+
}
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Like {@link _elementFromNode}, but skips elements without a listener for `type`, so a hit
|
|
1032
|
+
* on an unlistened child still reaches a listening ancestor.
|
|
1033
|
+
*
|
|
1034
|
+
* @param node - The picked node, or `null`.
|
|
1035
|
+
* @param type - The pointer event type a listener is required for.
|
|
1036
|
+
* @returns The nearest listening element, or `null`.
|
|
1037
|
+
*/
|
|
1038
|
+
_elementWithListener(node, type) {
|
|
1039
|
+
while (node !== null) {
|
|
1040
|
+
const element = this._entityElements.get(node);
|
|
1041
|
+
if (element?.hasListeners(type)) {
|
|
1042
|
+
return element;
|
|
1043
|
+
}
|
|
1044
|
+
node = node.parent;
|
|
1045
|
+
}
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
780
1048
|
// New helper to convert CSS coordinates to canvas (picker) coordinates
|
|
781
1049
|
_getPickerCoordinates(event) {
|
|
782
1050
|
// Get the canvas' bounding rectangle in CSS pixels.
|
|
@@ -789,30 +1057,42 @@ class AppElement extends AsyncElement {
|
|
|
789
1057
|
const y = (event.clientY - canvasRect.top) * scaleY;
|
|
790
1058
|
return { x, y };
|
|
791
1059
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1060
|
+
/**
|
|
1061
|
+
* Picks the scene under the pointer and returns the graph node that was hit, or `null`.
|
|
1062
|
+
*
|
|
1063
|
+
* The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
|
|
1064
|
+
* supported on WebGPU, where it returns an empty selection rather than failing - which
|
|
1065
|
+
* silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
|
|
1066
|
+
* async variant works on both backends and does not block the main thread on a GPU read.
|
|
1067
|
+
*
|
|
1068
|
+
* @param event - The pointer event to pick under.
|
|
1069
|
+
* @returns The graph node under the pointer, or `null` if nothing was hit.
|
|
1070
|
+
*/
|
|
1071
|
+
async _pickNode(event) {
|
|
795
1072
|
const camera = this.app.root.findComponent('camera');
|
|
796
1073
|
if (!camera)
|
|
797
|
-
return;
|
|
798
|
-
// Use the helper to convert event coordinates into canvas/picker coordinates.
|
|
1074
|
+
return null;
|
|
799
1075
|
const { x, y } = this._getPickerCoordinates(event);
|
|
800
1076
|
this._picker.prepare(camera, this.app.scene);
|
|
801
|
-
const selection = this._picker.
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1077
|
+
const selection = await this._picker.getSelectionAsync(x, y);
|
|
1078
|
+
if (selection.length === 0)
|
|
1079
|
+
return null;
|
|
1080
|
+
const item = selection[0];
|
|
1081
|
+
return item instanceof playcanvas.MeshInstance ? item.node : item.entity;
|
|
1082
|
+
}
|
|
1083
|
+
async _onPointerMove(event) {
|
|
1084
|
+
if (!this._picker || !this.app)
|
|
1085
|
+
return;
|
|
1086
|
+
// Moves arrive faster than a pick resolves, so results can land out of order. Only the
|
|
1087
|
+
// newest pick may update the hover state - an older one describes a pointer position the
|
|
1088
|
+
// user has already left.
|
|
1089
|
+
const token = ++this._pickToken;
|
|
1090
|
+
const node = await this._pickNode(event);
|
|
1091
|
+
if (token !== this._pickToken || !this._picker)
|
|
1092
|
+
return;
|
|
1093
|
+
// The hovered element is the nearest one up the node's parent chain, listening or not -
|
|
1094
|
+
// dispatch is gated per event type below
|
|
1095
|
+
const newHoverEntity = this._elementFromNode(node);
|
|
816
1096
|
// Handle enter/leave events
|
|
817
1097
|
if (this._hoveredEntity !== newHoverEntity) {
|
|
818
1098
|
if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) {
|
|
@@ -829,46 +1109,26 @@ class AppElement extends AsyncElement {
|
|
|
829
1109
|
newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
|
|
830
1110
|
}
|
|
831
1111
|
}
|
|
832
|
-
_onPointerDown(event) {
|
|
1112
|
+
async _onPointerDown(event) {
|
|
833
1113
|
if (!this._picker || !this.app)
|
|
834
1114
|
return;
|
|
835
|
-
const
|
|
836
|
-
if (!
|
|
837
|
-
return;
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
const selection = this._picker.getSelection(x, y);
|
|
842
|
-
if (selection.length > 0) {
|
|
843
|
-
const item = selection[0];
|
|
844
|
-
let currentNode = item instanceof playcanvas.MeshInstance ? item.node : item.entity;
|
|
845
|
-
while (currentNode !== null) {
|
|
846
|
-
const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
|
|
847
|
-
if (entityElement && entityElement.hasListeners('pointerdown')) {
|
|
848
|
-
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
849
|
-
break;
|
|
850
|
-
}
|
|
851
|
-
currentNode = currentNode.parent;
|
|
852
|
-
}
|
|
1115
|
+
const node = await this._pickNode(event);
|
|
1116
|
+
if (!this._picker)
|
|
1117
|
+
return; // the element disconnected while the pick was in flight
|
|
1118
|
+
const entityElement = this._elementWithListener(node, 'pointerdown');
|
|
1119
|
+
if (entityElement) {
|
|
1120
|
+
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
853
1121
|
}
|
|
854
1122
|
}
|
|
855
|
-
_onPointerUp(event) {
|
|
1123
|
+
async _onPointerUp(event) {
|
|
856
1124
|
if (!this._picker || !this.app)
|
|
857
1125
|
return;
|
|
858
|
-
const
|
|
859
|
-
if (!
|
|
860
|
-
return;
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const selection = this._picker.getSelection(x, y);
|
|
865
|
-
if (selection.length > 0) {
|
|
866
|
-
const item = selection[0];
|
|
867
|
-
const node = item instanceof playcanvas.MeshInstance ? item.node : item.entity;
|
|
868
|
-
const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`);
|
|
869
|
-
if (entityElement && entityElement.hasListeners('pointerup')) {
|
|
870
|
-
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
871
|
-
}
|
|
1126
|
+
const node = await this._pickNode(event);
|
|
1127
|
+
if (!this._picker)
|
|
1128
|
+
return; // the element disconnected while the pick was in flight
|
|
1129
|
+
const entityElement = this._elementWithListener(node, 'pointerup');
|
|
1130
|
+
if (entityElement) {
|
|
1131
|
+
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
872
1132
|
}
|
|
873
1133
|
}
|
|
874
1134
|
_onPointerListenerAdded(type) {
|
|
@@ -897,28 +1157,43 @@ class AppElement extends AsyncElement {
|
|
|
897
1157
|
}
|
|
898
1158
|
}
|
|
899
1159
|
/**
|
|
900
|
-
*
|
|
1160
|
+
* Warns that a graphics option was written too late to have any effect. These options are read
|
|
1161
|
+
* once, when the element connects and creates its graphics device, so a later write updates
|
|
1162
|
+
* only the element's own property - silently, without this.
|
|
1163
|
+
*
|
|
1164
|
+
* @param name - The name of the option, as its attribute.
|
|
1165
|
+
*/
|
|
1166
|
+
_warnIfBooted(name) {
|
|
1167
|
+
if (this._optionsLocked) {
|
|
1168
|
+
console.warn(`Attribute '${name}' on <pc-app> is only read when the application boots, so this change has no effect. Set it before the element is connected, or remove and re-insert the element to reboot with the new value.`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Sets whether the frame buffer has an alpha channel, which is what lets the page show through
|
|
1173
|
+
* wherever the scene has not drawn. Read only when the application boots.
|
|
901
1174
|
* @param value - The alpha flag.
|
|
902
1175
|
*/
|
|
903
1176
|
set alpha(value) {
|
|
1177
|
+
this._warnIfBooted('alpha');
|
|
904
1178
|
this._alpha = value;
|
|
905
1179
|
}
|
|
906
1180
|
/**
|
|
907
|
-
* Gets the alpha
|
|
1181
|
+
* Gets whether the frame buffer has an alpha channel.
|
|
908
1182
|
* @returns The alpha flag.
|
|
909
1183
|
*/
|
|
910
1184
|
get alpha() {
|
|
911
1185
|
return this._alpha;
|
|
912
1186
|
}
|
|
913
1187
|
/**
|
|
914
|
-
* Sets the
|
|
1188
|
+
* Sets whether the frame buffer is anti-aliased. Read only when the application boots.
|
|
915
1189
|
* @param value - The antialias flag.
|
|
916
1190
|
*/
|
|
917
1191
|
set antialias(value) {
|
|
1192
|
+
this._warnIfBooted('antialias');
|
|
918
1193
|
this._antialias = value;
|
|
919
1194
|
}
|
|
920
1195
|
/**
|
|
921
|
-
* Gets the
|
|
1196
|
+
* Gets whether the frame buffer is anti-aliased.
|
|
922
1197
|
* @returns The antialias flag.
|
|
923
1198
|
*/
|
|
924
1199
|
get antialias() {
|
|
@@ -926,10 +1201,11 @@ class AppElement extends AsyncElement {
|
|
|
926
1201
|
}
|
|
927
1202
|
/**
|
|
928
1203
|
* Sets the graphics backend. Defaults to 'webgpu', which falls back to 'webgl2' if WebGPU
|
|
929
|
-
* is not supported by the browser.
|
|
1204
|
+
* is not supported by the browser. Read only when the application boots.
|
|
930
1205
|
* @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
|
|
931
1206
|
*/
|
|
932
1207
|
set backend(value) {
|
|
1208
|
+
this._warnIfBooted('backend');
|
|
933
1209
|
this._backend = value;
|
|
934
1210
|
}
|
|
935
1211
|
/**
|
|
@@ -940,18 +1216,20 @@ class AppElement extends AsyncElement {
|
|
|
940
1216
|
return this._backend;
|
|
941
1217
|
}
|
|
942
1218
|
/**
|
|
943
|
-
* Sets the depth
|
|
944
|
-
*
|
|
1219
|
+
* Sets whether the frame buffer has a depth buffer, which the renderer needs to resolve which
|
|
1220
|
+
* surface is nearest the camera. Read only when the application boots.
|
|
1221
|
+
* @param value - The depth buffer flag.
|
|
945
1222
|
*/
|
|
946
|
-
set
|
|
947
|
-
this.
|
|
1223
|
+
set depthBuffer(value) {
|
|
1224
|
+
this._warnIfBooted('depth-buffer');
|
|
1225
|
+
this._depthBuffer = value;
|
|
948
1226
|
}
|
|
949
1227
|
/**
|
|
950
|
-
* Gets the depth
|
|
951
|
-
* @returns The depth flag.
|
|
1228
|
+
* Gets whether the frame buffer has a depth buffer.
|
|
1229
|
+
* @returns The depth buffer flag.
|
|
952
1230
|
*/
|
|
953
|
-
get
|
|
954
|
-
return this.
|
|
1231
|
+
get depthBuffer() {
|
|
1232
|
+
return this._depthBuffer;
|
|
955
1233
|
}
|
|
956
1234
|
/**
|
|
957
1235
|
* Gets the hierarchy ready flag.
|
|
@@ -962,39 +1240,68 @@ class AppElement extends AsyncElement {
|
|
|
962
1240
|
return this._hierarchyReady;
|
|
963
1241
|
}
|
|
964
1242
|
/**
|
|
965
|
-
* Sets the
|
|
966
|
-
*
|
|
967
|
-
*
|
|
1243
|
+
* Sets whether the application shows its built-in loading bar while it boots and preloads its
|
|
1244
|
+
* assets. Enabled by default; setting `false` removes the bar immediately, while setting
|
|
1245
|
+
* `true` has no effect until the element is next connected. The bar can be themed with the
|
|
1246
|
+
* CSS custom properties `--pc-loading-bar-color`, `--pc-loading-bar-background` and
|
|
1247
|
+
* `--pc-loading-bar-height`.
|
|
1248
|
+
* @param value - The loading bar flag.
|
|
968
1249
|
*/
|
|
969
|
-
set
|
|
970
|
-
this.
|
|
1250
|
+
set loadingBar(value) {
|
|
1251
|
+
this._loadingBar = value;
|
|
1252
|
+
if (!value && this._bar) {
|
|
1253
|
+
this._bar.destroy();
|
|
1254
|
+
this._bar = null;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Gets whether the application shows its built-in loading bar while it boots and preloads
|
|
1259
|
+
* its assets.
|
|
1260
|
+
* @returns The loading bar flag.
|
|
1261
|
+
*/
|
|
1262
|
+
get loadingBar() {
|
|
1263
|
+
return this._loadingBar;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Sets the cap on the pixel ratio the application renders at. The canvas is sized by the
|
|
1267
|
+
* smaller of this value and the display's own device pixel ratio, so the default of `Infinity`
|
|
1268
|
+
* renders at full physical resolution, `1` renders at CSS resolution, and an intermediate
|
|
1269
|
+
* value such as `2` keeps a dense display sharp without paying for every one of its pixels.
|
|
1270
|
+
* Must be greater than 0. Unlike the other graphics options, this applies immediately.
|
|
1271
|
+
* @param value - The maximum pixel ratio.
|
|
1272
|
+
*/
|
|
1273
|
+
set maxPixelRatio(value) {
|
|
1274
|
+
this._maxPixelRatio = value;
|
|
971
1275
|
if (this.app) {
|
|
972
|
-
this.app.graphicsDevice.maxPixelRatio = value
|
|
1276
|
+
this.app.graphicsDevice.maxPixelRatio = value;
|
|
1277
|
+
this.app.resizeCanvas();
|
|
973
1278
|
}
|
|
974
1279
|
}
|
|
975
1280
|
/**
|
|
976
|
-
* Gets the
|
|
977
|
-
* @returns The
|
|
1281
|
+
* Gets the cap on the pixel ratio the application renders at.
|
|
1282
|
+
* @returns The maximum pixel ratio.
|
|
978
1283
|
*/
|
|
979
|
-
get
|
|
980
|
-
return this.
|
|
1284
|
+
get maxPixelRatio() {
|
|
1285
|
+
return this._maxPixelRatio;
|
|
981
1286
|
}
|
|
982
1287
|
/**
|
|
983
|
-
* Sets the stencil
|
|
984
|
-
*
|
|
1288
|
+
* Sets whether the frame buffer has a stencil buffer, which stencil-based effects and UI
|
|
1289
|
+
* masking need. Read only when the application boots.
|
|
1290
|
+
* @param value - The stencil buffer flag.
|
|
985
1291
|
*/
|
|
986
|
-
set
|
|
987
|
-
this.
|
|
1292
|
+
set stencilBuffer(value) {
|
|
1293
|
+
this._warnIfBooted('stencil-buffer');
|
|
1294
|
+
this._stencilBuffer = value;
|
|
988
1295
|
}
|
|
989
1296
|
/**
|
|
990
|
-
* Gets the stencil
|
|
991
|
-
* @returns The stencil flag.
|
|
1297
|
+
* Gets whether the frame buffer has a stencil buffer.
|
|
1298
|
+
* @returns The stencil buffer flag.
|
|
992
1299
|
*/
|
|
993
|
-
get
|
|
994
|
-
return this.
|
|
1300
|
+
get stencilBuffer() {
|
|
1301
|
+
return this._stencilBuffer;
|
|
995
1302
|
}
|
|
996
1303
|
static get observedAttributes() {
|
|
997
|
-
return ['alpha', 'antialias', 'backend', 'depth', '
|
|
1304
|
+
return ['alpha', 'antialias', 'backend', 'depth-buffer', 'loading-bar', 'max-pixel-ratio', 'stencil-buffer'];
|
|
998
1305
|
}
|
|
999
1306
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
1000
1307
|
switch (name) {
|
|
@@ -1007,14 +1314,17 @@ class AppElement extends AsyncElement {
|
|
|
1007
1314
|
case 'backend':
|
|
1008
1315
|
this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgpu', name);
|
|
1009
1316
|
break;
|
|
1010
|
-
case 'depth':
|
|
1011
|
-
this.
|
|
1317
|
+
case 'depth-buffer':
|
|
1318
|
+
this.depthBuffer = parseBool(newValue, true);
|
|
1012
1319
|
break;
|
|
1013
|
-
case '
|
|
1014
|
-
this.
|
|
1320
|
+
case 'loading-bar':
|
|
1321
|
+
this.loadingBar = parseBool(newValue, true);
|
|
1015
1322
|
break;
|
|
1016
|
-
case '
|
|
1017
|
-
this.
|
|
1323
|
+
case 'max-pixel-ratio':
|
|
1324
|
+
this.maxPixelRatio = parseNumber(newValue, Infinity, name);
|
|
1325
|
+
break;
|
|
1326
|
+
case 'stencil-buffer':
|
|
1327
|
+
this.stencilBuffer = parseBool(newValue, true);
|
|
1018
1328
|
break;
|
|
1019
1329
|
}
|
|
1020
1330
|
}
|
|
@@ -1084,9 +1394,15 @@ class EntityElement extends AsyncElement {
|
|
|
1084
1394
|
_built = false;
|
|
1085
1395
|
_entity = null;
|
|
1086
1396
|
/**
|
|
1087
|
-
* The
|
|
1088
|
-
*
|
|
1089
|
-
|
|
1397
|
+
* The application element this entity is registered with, cached at creation time so the
|
|
1398
|
+
* entity can be unregistered even once this element has left the DOM.
|
|
1399
|
+
*/
|
|
1400
|
+
_appElement = null;
|
|
1401
|
+
/**
|
|
1402
|
+
* The PlayCanvas entity instance. `null` until the element is ready, and again once it has
|
|
1403
|
+
* been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
1404
|
+
* promise before accessing it.
|
|
1405
|
+
* @returns The entity instance, or `null`.
|
|
1090
1406
|
*/
|
|
1091
1407
|
get entity() {
|
|
1092
1408
|
return this._entity;
|
|
@@ -1098,17 +1414,40 @@ class EntityElement extends AsyncElement {
|
|
|
1098
1414
|
if (this._entity) {
|
|
1099
1415
|
return;
|
|
1100
1416
|
}
|
|
1101
|
-
//
|
|
1102
|
-
|
|
1417
|
+
// Seed from the cached fields rather than re-reading the attributes. Every observed
|
|
1418
|
+
// attribute is routed through its property setter by attributeChangedCallback, so the field
|
|
1419
|
+
// already holds the parsed attribute value - and it also holds anything assigned through the
|
|
1420
|
+
// property API before the app booted, which reading the attribute back would discard.
|
|
1421
|
+
const entity = new playcanvas.Entity(this._name, app);
|
|
1103
1422
|
this._entity = entity;
|
|
1104
|
-
entity.enabled =
|
|
1105
|
-
entity.setLocalPosition(
|
|
1106
|
-
entity.setLocalEulerAngles(
|
|
1107
|
-
entity.setLocalScale(
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1423
|
+
entity.enabled = this._enabled;
|
|
1424
|
+
entity.setLocalPosition(this._position);
|
|
1425
|
+
entity.setLocalEulerAngles(this._rotation);
|
|
1426
|
+
entity.setLocalScale(this._scale);
|
|
1427
|
+
if (this._tags.length > 0) {
|
|
1428
|
+
entity.tags.add(this._tags);
|
|
1429
|
+
}
|
|
1430
|
+
// Register with the owning application, which joins engine nodes back to elements by
|
|
1431
|
+
// identity (never by name), and hook the entity's destruction. The engine fires 'destroy'
|
|
1432
|
+
// for every entity in a destroyed subtree, so the element learns of its entity's death no
|
|
1433
|
+
// matter who causes it: this element, an ancestor, the whole application, or a user
|
|
1434
|
+
// script calling entity.destroy().
|
|
1435
|
+
this._appElement = this.closestApp;
|
|
1436
|
+
this._appElement?._registerEntityElement(entity, this);
|
|
1437
|
+
entity.once('destroy', this._onEntityDestroy, this);
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Handles the destruction of the backing entity. Resets the element so a later re-insertion
|
|
1441
|
+
* starts clean: `_built` must be cleared alongside `_entity`, or buildHierarchy would bail
|
|
1442
|
+
* and a re-created entity would never be parented.
|
|
1443
|
+
*
|
|
1444
|
+
* @param entity - The entity that was destroyed.
|
|
1445
|
+
*/
|
|
1446
|
+
_onEntityDestroy(entity) {
|
|
1447
|
+
this._appElement?._unregisterEntityElement(entity);
|
|
1448
|
+
this._appElement = null;
|
|
1449
|
+
this._entity = null;
|
|
1450
|
+
this._built = false;
|
|
1112
1451
|
}
|
|
1113
1452
|
buildHierarchy(app) {
|
|
1114
1453
|
if (!this.entity || this._built)
|
|
@@ -1126,8 +1465,15 @@ class EntityElement extends AsyncElement {
|
|
|
1126
1465
|
connectedCallback() {
|
|
1127
1466
|
// Wait for app to be ready
|
|
1128
1467
|
const closestApp = this.closestApp;
|
|
1129
|
-
if (!closestApp)
|
|
1468
|
+
if (!closestApp) {
|
|
1469
|
+
// An entity outside an application is inert and never becomes ready, so awaiting it
|
|
1470
|
+
// hangs. Warn rather than fail silently, naming the parent it requires, as every other
|
|
1471
|
+
// misplaced element does.
|
|
1472
|
+
const name = this.getAttribute('name');
|
|
1473
|
+
const label = name ? ` '${name}'` : '';
|
|
1474
|
+
console.warn(`pc-entity${label} must be a descendant of pc-app - entity not created`);
|
|
1130
1475
|
return;
|
|
1476
|
+
}
|
|
1131
1477
|
// If app is already running, create entity immediately
|
|
1132
1478
|
if (closestApp.hierarchyReady) {
|
|
1133
1479
|
const app = closestApp.app;
|
|
@@ -1144,17 +1490,11 @@ class EntityElement extends AsyncElement {
|
|
|
1144
1490
|
}
|
|
1145
1491
|
}
|
|
1146
1492
|
disconnectedCallback() {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
});
|
|
1153
|
-
// Destroy the entity
|
|
1154
|
-
this.entity.destroy();
|
|
1155
|
-
this._entity = null;
|
|
1156
|
-
this._built = false;
|
|
1157
|
-
}
|
|
1493
|
+
// Destroying the entity destroys its whole subtree, and the engine fires 'destroy' for
|
|
1494
|
+
// every entity in it - so _onEntityDestroy resets this element AND every descendant
|
|
1495
|
+
// element before the descendants' own disconnectedCallbacks run. Their entities are null
|
|
1496
|
+
// by then, making this call a no-op for them.
|
|
1497
|
+
this._entity?.destroy();
|
|
1158
1498
|
}
|
|
1159
1499
|
/**
|
|
1160
1500
|
* Sets the enabled state of the entity.
|
|
@@ -1603,6 +1943,13 @@ const processBufferView = (gltfBuffer, buffers, continuation) => {
|
|
|
1603
1943
|
* @attribute {number} pixels-per-unit - For a `sprite` asset, the number of pixels per world unit.
|
|
1604
1944
|
* @attribute {'simple' | 'sliced' | 'tiled'} render-mode - For a `sprite` asset, how the sprite is
|
|
1605
1945
|
* rendered when resized.
|
|
1946
|
+
*
|
|
1947
|
+
* @fires {Event} load - Fired each time the asset finishes loading, including a `lazy` asset
|
|
1948
|
+
* loaded later and any subsequent reloads. Does not bubble — listen on this element, or use a
|
|
1949
|
+
* capture-phase listener on an ancestor to observe every asset.
|
|
1950
|
+
* @fires {ErrorEvent} error - Fired when the asset fails to load, with the engine's error in
|
|
1951
|
+
* `message`. Does not bubble. The element still becomes ready — readiness means the load settled,
|
|
1952
|
+
* not that it succeeded.
|
|
1606
1953
|
*/
|
|
1607
1954
|
class AssetElement extends AsyncElement {
|
|
1608
1955
|
_lazy = false;
|
|
@@ -1646,6 +1993,14 @@ class AssetElement extends AsyncElement {
|
|
|
1646
1993
|
disconnectedCallback() {
|
|
1647
1994
|
this.destroyAsset();
|
|
1648
1995
|
}
|
|
1996
|
+
_onAssetLoad() {
|
|
1997
|
+
this.dispatchEvent(new Event('load'));
|
|
1998
|
+
}
|
|
1999
|
+
_onAssetError(err) {
|
|
2000
|
+
this.dispatchEvent(new ErrorEvent('error', {
|
|
2001
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2002
|
+
}));
|
|
2003
|
+
}
|
|
1649
2004
|
createAsset() {
|
|
1650
2005
|
const id = this.getAttribute('id') || '';
|
|
1651
2006
|
const src = this.getAttribute('src') || '';
|
|
@@ -1680,6 +2035,10 @@ class AssetElement extends AsyncElement {
|
|
|
1680
2035
|
this.asset = new playcanvas.Asset(id, type, src ? { url: src } : null, data);
|
|
1681
2036
|
}
|
|
1682
2037
|
this.asset.preload = !this._lazy;
|
|
2038
|
+
// Forward the engine asset's load outcome as DOM events on this element, like <img>.
|
|
2039
|
+
// Attached before the asset joins the registry, which is what starts a preloaded load.
|
|
2040
|
+
this.asset.on('load', this._onAssetLoad, this);
|
|
2041
|
+
this.asset.on('error', this._onAssetError, this);
|
|
1683
2042
|
}
|
|
1684
2043
|
/**
|
|
1685
2044
|
* Builds the `data` object for the asset from an optional inline `data` attribute (JSON) and,
|
|
@@ -1734,6 +2093,9 @@ class AssetElement extends AsyncElement {
|
|
|
1734
2093
|
}
|
|
1735
2094
|
destroyAsset() {
|
|
1736
2095
|
if (this.asset) {
|
|
2096
|
+
// A caller that keeps the Asset alive must not dispatch on a removed element
|
|
2097
|
+
this.asset.off('load', this._onAssetLoad, this);
|
|
2098
|
+
this.asset.off('error', this._onAssetError, this);
|
|
1737
2099
|
// Deregister first so unload() can still notify the registry
|
|
1738
2100
|
this.asset.registry?.remove(this.asset);
|
|
1739
2101
|
this.asset.unload();
|
|
@@ -1829,9 +2191,10 @@ class ComponentElement extends AsyncElement {
|
|
|
1829
2191
|
this._appElement = null;
|
|
1830
2192
|
}
|
|
1831
2193
|
/**
|
|
1832
|
-
* The PlayCanvas component instance.
|
|
1833
|
-
* {@link whenReady} or the
|
|
1834
|
-
*
|
|
2194
|
+
* The PlayCanvas component instance. `null` until the element is ready, and also for an
|
|
2195
|
+
* element that is not a descendant of a `<pc-entity>` — await {@link whenReady} or the
|
|
2196
|
+
* element's `ready()` promise before accessing it.
|
|
2197
|
+
* @returns The component instance, or `null`.
|
|
1835
2198
|
*/
|
|
1836
2199
|
get component() {
|
|
1837
2200
|
return this._component;
|
|
@@ -2278,6 +2641,10 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
2278
2641
|
}
|
|
2279
2642
|
customElements.define('pc-button', ButtonComponentElement);
|
|
2280
2643
|
|
|
2644
|
+
const projections = new Map([
|
|
2645
|
+
['perspective', playcanvas.PROJECTION_PERSPECTIVE],
|
|
2646
|
+
['orthographic', playcanvas.PROJECTION_ORTHOGRAPHIC]
|
|
2647
|
+
]);
|
|
2281
2648
|
const tonemaps = new Map([
|
|
2282
2649
|
['none', playcanvas.TONEMAP_NONE],
|
|
2283
2650
|
['linear', playcanvas.TONEMAP_LINEAR],
|
|
@@ -2308,7 +2675,7 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2308
2675
|
_gamma = 'srgb';
|
|
2309
2676
|
_horizontalFov = false;
|
|
2310
2677
|
_nearClip = 0.1;
|
|
2311
|
-
|
|
2678
|
+
_projection = 'perspective';
|
|
2312
2679
|
_orthoHeight = 10;
|
|
2313
2680
|
_priority = 0;
|
|
2314
2681
|
_rect = new playcanvas.Vec4(0, 0, 1, 1);
|
|
@@ -2332,12 +2699,12 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2332
2699
|
gammaCorrection: this._gamma === 'srgb' ? playcanvas.GAMMA_SRGB : playcanvas.GAMMA_NONE,
|
|
2333
2700
|
horizontalFov: this._horizontalFov,
|
|
2334
2701
|
nearClip: this._nearClip,
|
|
2335
|
-
projection: this.
|
|
2702
|
+
projection: projections.get(this._projection) ?? playcanvas.PROJECTION_PERSPECTIVE,
|
|
2336
2703
|
orthoHeight: this._orthoHeight,
|
|
2337
2704
|
priority: this._priority,
|
|
2338
2705
|
rect: this._rect,
|
|
2339
2706
|
scissorRect: this._scissorRect,
|
|
2340
|
-
toneMapping: tonemaps.get(this._tonemap)
|
|
2707
|
+
toneMapping: tonemaps.get(this._tonemap) ?? playcanvas.TONEMAP_NONE
|
|
2341
2708
|
};
|
|
2342
2709
|
}
|
|
2343
2710
|
get xrAvailable() {
|
|
@@ -2579,23 +2946,6 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2579
2946
|
get nearClip() {
|
|
2580
2947
|
return this._nearClip;
|
|
2581
2948
|
}
|
|
2582
|
-
/**
|
|
2583
|
-
* Sets the orthographic projection of the camera.
|
|
2584
|
-
* @param value - The orthographic projection.
|
|
2585
|
-
*/
|
|
2586
|
-
set orthographic(value) {
|
|
2587
|
-
this._orthographic = value;
|
|
2588
|
-
if (this.component) {
|
|
2589
|
-
this.component.projection = value ? playcanvas.PROJECTION_ORTHOGRAPHIC : playcanvas.PROJECTION_PERSPECTIVE;
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
/**
|
|
2593
|
-
* Gets the orthographic projection of the camera.
|
|
2594
|
-
* @returns The orthographic projection.
|
|
2595
|
-
*/
|
|
2596
|
-
get orthographic() {
|
|
2597
|
-
return this._orthographic;
|
|
2598
|
-
}
|
|
2599
2949
|
/**
|
|
2600
2950
|
* Sets the orthographic height of the camera.
|
|
2601
2951
|
* @param value - The orthographic height.
|
|
@@ -2630,6 +2980,23 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2630
2980
|
get priority() {
|
|
2631
2981
|
return this._priority;
|
|
2632
2982
|
}
|
|
2983
|
+
/**
|
|
2984
|
+
* Sets the projection of the camera. Use `orthoHeight` to size an orthographic projection.
|
|
2985
|
+
* @param value - The projection ('perspective' or 'orthographic').
|
|
2986
|
+
*/
|
|
2987
|
+
set projection(value) {
|
|
2988
|
+
this._projection = value;
|
|
2989
|
+
if (this.component) {
|
|
2990
|
+
this.component.projection = projections.get(value) ?? playcanvas.PROJECTION_PERSPECTIVE;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Gets the projection of the camera.
|
|
2995
|
+
* @returns The projection.
|
|
2996
|
+
*/
|
|
2997
|
+
get projection() {
|
|
2998
|
+
return this._projection;
|
|
2999
|
+
}
|
|
2633
3000
|
/**
|
|
2634
3001
|
* Sets the rect of the camera.
|
|
2635
3002
|
* @param value - The rect.
|
|
@@ -2696,9 +3063,9 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2696
3063
|
'gamma',
|
|
2697
3064
|
'horizontal-fov',
|
|
2698
3065
|
'near-clip',
|
|
2699
|
-
'orthographic',
|
|
2700
3066
|
'ortho-height',
|
|
2701
3067
|
'priority',
|
|
3068
|
+
'projection',
|
|
2702
3069
|
'rect',
|
|
2703
3070
|
'scissor-rect',
|
|
2704
3071
|
'tonemap'
|
|
@@ -2743,15 +3110,15 @@ class CameraComponentElement extends ComponentElement {
|
|
|
2743
3110
|
case 'near-clip':
|
|
2744
3111
|
this.nearClip = parseNumber(newValue, 0.1, name);
|
|
2745
3112
|
break;
|
|
2746
|
-
case 'orthographic':
|
|
2747
|
-
this.orthographic = parseBool(newValue, false);
|
|
2748
|
-
break;
|
|
2749
3113
|
case 'ortho-height':
|
|
2750
3114
|
this.orthoHeight = parseNumber(newValue, 10, name);
|
|
2751
3115
|
break;
|
|
2752
3116
|
case 'priority':
|
|
2753
3117
|
this.priority = parseNumber(newValue, 0, name);
|
|
2754
3118
|
break;
|
|
3119
|
+
case 'projection':
|
|
3120
|
+
this.projection = parseEnum(newValue, projections, 'perspective', name);
|
|
3121
|
+
break;
|
|
2755
3122
|
case 'rect':
|
|
2756
3123
|
this.rect = parseVec4(newValue, new playcanvas.Vec4(0, 0, 1, 1), name);
|
|
2757
3124
|
break;
|
|
@@ -4724,8 +5091,14 @@ const roughnessAliases = ['roughness', 'roughness-map'];
|
|
|
4724
5091
|
* created on insertion.
|
|
4725
5092
|
*
|
|
4726
5093
|
* The element is metal/rough by default: unlike a bare `StandardMaterial` it enables the metalness
|
|
4727
|
-
* workflow, which is what the `metalness-*` attributes assume and what glTF means by PBR.
|
|
4728
|
-
* `
|
|
5094
|
+
* workflow, which is what the `metalness-*` attributes assume and what glTF means by PBR. It also
|
|
5095
|
+
* defaults `metalness` to 0 rather than the engine's 1, because those two defaults have to be
|
|
5096
|
+
* chosen together - the engine's 1 is unreachable under its own `useMetalness` of false, and with
|
|
5097
|
+
* the workflow on it would make every material fully metallic, so `<pc-material diffuse="crimson">`
|
|
5098
|
+
* would render as dark tinted reflections of an environment that may not exist rather than as a
|
|
5099
|
+
* crimson surface. `metalness="1"` remains one attribute away.
|
|
5100
|
+
*
|
|
5101
|
+
* The `roughness` and `roughness-map` attributes are aliases for `gloss` and `gloss-map` that
|
|
4729
5102
|
* additionally invert the gloss channel; do not mix the two families on one element.
|
|
4730
5103
|
*
|
|
4731
5104
|
* The two aliases are documented here rather than on an accessor, because they resolve to the
|
|
@@ -4785,7 +5158,7 @@ class MaterialElement extends HTMLElement {
|
|
|
4785
5158
|
_heightMapRotation = 0;
|
|
4786
5159
|
_heightMapTiling = new playcanvas.Vec2(1, 1);
|
|
4787
5160
|
_heightMapUv = 0;
|
|
4788
|
-
_metalness =
|
|
5161
|
+
_metalness = 0;
|
|
4789
5162
|
_metalnessMap = '';
|
|
4790
5163
|
_metalnessMapChannel = 'g';
|
|
4791
5164
|
_metalnessMapOffset = new playcanvas.Vec2(0, 0);
|
|
@@ -6728,7 +7101,7 @@ class MaterialElement extends HTMLElement {
|
|
|
6728
7101
|
this.heightMapUv = parseNumber(newValue, 0, name);
|
|
6729
7102
|
break;
|
|
6730
7103
|
case 'metalness':
|
|
6731
|
-
this.metalness = parseNumber(newValue,
|
|
7104
|
+
this.metalness = parseNumber(newValue, 0, name);
|
|
6732
7105
|
break;
|
|
6733
7106
|
case 'metalness-map':
|
|
6734
7107
|
this.metalnessMap = newValue ?? '';
|
|
@@ -7163,6 +7536,14 @@ class RigidBodyComponentElement extends ComponentElement {
|
|
|
7163
7536
|
}
|
|
7164
7537
|
customElements.define('pc-rigidbody', RigidBodyComponentElement);
|
|
7165
7538
|
|
|
7539
|
+
// The engine's SCALEMODE_* constants are the strings 'none' and 'blend', so this map happens to be
|
|
7540
|
+
// an identity. It is still the right shape: it supplies parseEnum's valid-name list, it is what the
|
|
7541
|
+
// manifest generator reads the enum values from, and it keeps the attribute vocabulary independent
|
|
7542
|
+
// of constants the engine is free to change.
|
|
7543
|
+
const scaleModes = new Map([
|
|
7544
|
+
['none', playcanvas.SCALEMODE_NONE],
|
|
7545
|
+
['blend', playcanvas.SCALEMODE_BLEND]
|
|
7546
|
+
]);
|
|
7166
7547
|
/**
|
|
7167
7548
|
* The ScreenComponentElement interface provides properties and methods for manipulating
|
|
7168
7549
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-screen/ | `<pc-screen>`} elements.
|
|
@@ -7176,7 +7557,7 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
7176
7557
|
_resolution = new playcanvas.Vec2(640, 320);
|
|
7177
7558
|
_referenceResolution = new playcanvas.Vec2(640, 320);
|
|
7178
7559
|
_priority = 0;
|
|
7179
|
-
|
|
7560
|
+
_scaleMode = 'none';
|
|
7180
7561
|
_scaleBlend = 0.5;
|
|
7181
7562
|
/** @ignore */
|
|
7182
7563
|
constructor() {
|
|
@@ -7188,7 +7569,7 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
7188
7569
|
referenceResolution: this._referenceResolution,
|
|
7189
7570
|
resolution: this._resolution,
|
|
7190
7571
|
scaleBlend: this._scaleBlend,
|
|
7191
|
-
scaleMode: this.
|
|
7572
|
+
scaleMode: scaleModes.get(this._scaleMode) ?? playcanvas.SCALEMODE_NONE,
|
|
7192
7573
|
screenSpace: this._screenSpace
|
|
7193
7574
|
};
|
|
7194
7575
|
}
|
|
@@ -7226,23 +7607,44 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
7226
7607
|
get resolution() {
|
|
7227
7608
|
return this._resolution;
|
|
7228
7609
|
}
|
|
7610
|
+
/**
|
|
7611
|
+
* Sets how the screen's `resolution` and `referenceResolution` are weighted against each other
|
|
7612
|
+
* when `scaleMode` is `blend`, from 0 (follow the resolution) to 1 (follow the reference
|
|
7613
|
+
* resolution). Ignored while `scaleMode` is `none`.
|
|
7614
|
+
* @param value - The scale blend factor.
|
|
7615
|
+
*/
|
|
7229
7616
|
set scaleBlend(value) {
|
|
7230
7617
|
this._scaleBlend = value;
|
|
7231
7618
|
if (this.component) {
|
|
7232
7619
|
this.component.scaleBlend = this._scaleBlend;
|
|
7233
7620
|
}
|
|
7234
7621
|
}
|
|
7622
|
+
/**
|
|
7623
|
+
* Gets how the screen's resolutions are weighted against each other.
|
|
7624
|
+
* @returns The scale blend factor.
|
|
7625
|
+
*/
|
|
7235
7626
|
get scaleBlend() {
|
|
7236
7627
|
return this._scaleBlend;
|
|
7237
7628
|
}
|
|
7238
|
-
|
|
7239
|
-
|
|
7629
|
+
/**
|
|
7630
|
+
* Sets how the screen scales its contents. `none` renders at `resolution` and ignores
|
|
7631
|
+
* `referenceResolution`; `blend` scales between the two, weighted by `scaleBlend`, which is what
|
|
7632
|
+
* keeps a UI laid out at one resolution usable at another. Requires `screenSpace` - the engine
|
|
7633
|
+
* forces `none` on a world-space screen, which does not support scaling.
|
|
7634
|
+
* @param value - The scale mode ('none' or 'blend').
|
|
7635
|
+
*/
|
|
7636
|
+
set scaleMode(value) {
|
|
7637
|
+
this._scaleMode = value;
|
|
7240
7638
|
if (this.component) {
|
|
7241
|
-
this.component.scaleMode =
|
|
7639
|
+
this.component.scaleMode = scaleModes.get(value) ?? playcanvas.SCALEMODE_NONE;
|
|
7242
7640
|
}
|
|
7243
7641
|
}
|
|
7244
|
-
|
|
7245
|
-
|
|
7642
|
+
/**
|
|
7643
|
+
* Gets how the screen scales its contents.
|
|
7644
|
+
* @returns The scale mode.
|
|
7645
|
+
*/
|
|
7646
|
+
get scaleMode() {
|
|
7647
|
+
return this._scaleMode;
|
|
7246
7648
|
}
|
|
7247
7649
|
set screenSpace(value) {
|
|
7248
7650
|
this._screenSpace = value;
|
|
@@ -7256,12 +7658,12 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
7256
7658
|
static get observedAttributes() {
|
|
7257
7659
|
return [
|
|
7258
7660
|
...super.observedAttributes,
|
|
7259
|
-
'blend',
|
|
7260
7661
|
'screen-space',
|
|
7261
7662
|
'resolution',
|
|
7262
7663
|
'reference-resolution',
|
|
7263
7664
|
'priority',
|
|
7264
|
-
'scale-blend'
|
|
7665
|
+
'scale-blend',
|
|
7666
|
+
'scale-mode'
|
|
7265
7667
|
];
|
|
7266
7668
|
}
|
|
7267
7669
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
@@ -7279,8 +7681,8 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
7279
7681
|
case 'scale-blend':
|
|
7280
7682
|
this.scaleBlend = parseNumber(newValue, 0.5, name);
|
|
7281
7683
|
break;
|
|
7282
|
-
case '
|
|
7283
|
-
this.
|
|
7684
|
+
case 'scale-mode':
|
|
7685
|
+
this.scaleMode = parseEnum(newValue, scaleModes, 'none', name);
|
|
7284
7686
|
break;
|
|
7285
7687
|
case 'screen-space':
|
|
7286
7688
|
this.screenSpace = parseBool(newValue, false);
|
|
@@ -7503,7 +7905,8 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
7503
7905
|
return super.component;
|
|
7504
7906
|
}
|
|
7505
7907
|
/**
|
|
7506
|
-
* Sets whether horizontal
|
|
7908
|
+
* Sets whether scrolling along the horizontal axis is enabled. This is a toggle, unlike the
|
|
7909
|
+
* `orientation` of a `<pc-scrollbar>`, for which `horizontal` is one of the accepted values.
|
|
7507
7910
|
* @param value - Whether horizontal scrolling is enabled.
|
|
7508
7911
|
*/
|
|
7509
7912
|
set horizontal(value) {
|
|
@@ -7513,14 +7916,15 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
7513
7916
|
}
|
|
7514
7917
|
}
|
|
7515
7918
|
/**
|
|
7516
|
-
* Gets whether horizontal
|
|
7919
|
+
* Gets whether scrolling along the horizontal axis is enabled.
|
|
7517
7920
|
* @returns Whether horizontal scrolling is enabled.
|
|
7518
7921
|
*/
|
|
7519
7922
|
get horizontal() {
|
|
7520
7923
|
return this._horizontal;
|
|
7521
7924
|
}
|
|
7522
7925
|
/**
|
|
7523
|
-
* Sets whether vertical
|
|
7926
|
+
* Sets whether scrolling along the vertical axis is enabled. This is a toggle, unlike the
|
|
7927
|
+
* `orientation` of a `<pc-scrollbar>`, for which `vertical` is one of the accepted values.
|
|
7524
7928
|
* @param value - Whether vertical scrolling is enabled.
|
|
7525
7929
|
*/
|
|
7526
7930
|
set vertical(value) {
|
|
@@ -7530,7 +7934,7 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
7530
7934
|
}
|
|
7531
7935
|
}
|
|
7532
7936
|
/**
|
|
7533
|
-
* Gets whether vertical
|
|
7937
|
+
* Gets whether scrolling along the vertical axis is enabled.
|
|
7534
7938
|
* @returns Whether vertical scrolling is enabled.
|
|
7535
7939
|
*/
|
|
7536
7940
|
get vertical() {
|
|
@@ -9326,30 +9730,58 @@ class SceneElement extends AsyncElement {
|
|
|
9326
9730
|
_gravity = new playcanvas.Vec3(0, -9.81, 0);
|
|
9327
9731
|
_scene = null;
|
|
9328
9732
|
/**
|
|
9329
|
-
* The PlayCanvas scene instance.
|
|
9733
|
+
* The PlayCanvas scene instance. `null` until the element is ready — await
|
|
9330
9734
|
* {@link whenReady} or the element's `ready()` promise before accessing it.
|
|
9331
|
-
* @returns The scene instance
|
|
9735
|
+
* @returns The scene instance, or `null`.
|
|
9332
9736
|
*/
|
|
9333
9737
|
get scene() {
|
|
9334
9738
|
return this._scene;
|
|
9335
9739
|
}
|
|
9336
9740
|
async connectedCallback() {
|
|
9337
|
-
|
|
9338
|
-
|
|
9741
|
+
const appElement = this.closestApp;
|
|
9742
|
+
if (!appElement) {
|
|
9743
|
+
console.warn('pc-scene must be a descendant of pc-app - scene settings not applied');
|
|
9744
|
+
return;
|
|
9745
|
+
}
|
|
9746
|
+
await appElement.ready();
|
|
9747
|
+
// The element may have been removed or re-parented while waiting for the app. Matches the
|
|
9748
|
+
// guard in AssetElement and MaterialElement, but compares closestApp rather than
|
|
9749
|
+
// parentElement because pc-scene resolves its app by ancestor rather than direct child.
|
|
9750
|
+
// Without this, a scene re-parented mid-await would take its Scene from the app it started
|
|
9751
|
+
// under while _applyGravity resolved the app it ended up under, splitting the two.
|
|
9752
|
+
if (!this.isConnected || this.closestApp !== appElement) {
|
|
9753
|
+
return;
|
|
9754
|
+
}
|
|
9755
|
+
// The application is gone if the tree was torn down while we awaited readiness. There is
|
|
9756
|
+
// nothing to configure and nothing the author can act on, so this stays silent.
|
|
9757
|
+
const app = appElement.app;
|
|
9758
|
+
if (!app) {
|
|
9759
|
+
return;
|
|
9760
|
+
}
|
|
9761
|
+
this._scene = app.scene;
|
|
9339
9762
|
this.updateSceneSettings();
|
|
9340
9763
|
this._onReady();
|
|
9341
9764
|
}
|
|
9342
9765
|
updateSceneSettings() {
|
|
9343
|
-
if (this.
|
|
9344
|
-
this.
|
|
9345
|
-
this.
|
|
9346
|
-
this.
|
|
9347
|
-
this.
|
|
9348
|
-
this.
|
|
9349
|
-
|
|
9350
|
-
appElement.app.systems.rigidbody.gravity.copy(this._gravity);
|
|
9766
|
+
if (this._scene) {
|
|
9767
|
+
this._scene.fog.type = this._fog;
|
|
9768
|
+
this._scene.fog.color = this._fogColor;
|
|
9769
|
+
this._scene.fog.density = this._fogDensity;
|
|
9770
|
+
this._scene.fog.start = this._fogStart;
|
|
9771
|
+
this._scene.fog.end = this._fogEnd;
|
|
9772
|
+
this._applyGravity(this._gravity);
|
|
9351
9773
|
}
|
|
9352
9774
|
}
|
|
9775
|
+
/**
|
|
9776
|
+
* Applies gravity to the rigid body system. Resolved through `closestApp` rather than
|
|
9777
|
+
* `parentElement` so that a `<pc-scene>` nested inside a wrapper element behaves the same as
|
|
9778
|
+
* a direct child, matching how `connectedCallback` resolves the application.
|
|
9779
|
+
*
|
|
9780
|
+
* @param value - The gravity to apply.
|
|
9781
|
+
*/
|
|
9782
|
+
_applyGravity(value) {
|
|
9783
|
+
this.closestApp?.app?.systems.rigidbody?.gravity.copy(value);
|
|
9784
|
+
}
|
|
9353
9785
|
/**
|
|
9354
9786
|
* Sets the fog type of the scene. Can be `none`, `linear`, `exp` or `exp2`. Defaults to
|
|
9355
9787
|
* `none`.
|
|
@@ -9442,9 +9874,8 @@ class SceneElement extends AsyncElement {
|
|
|
9442
9874
|
*/
|
|
9443
9875
|
set gravity(value) {
|
|
9444
9876
|
this._gravity = value;
|
|
9445
|
-
if (this.
|
|
9446
|
-
|
|
9447
|
-
appElement.app.systems.rigidbody.gravity.copy(value);
|
|
9877
|
+
if (this._scene) {
|
|
9878
|
+
this._applyGravity(value);
|
|
9448
9879
|
}
|
|
9449
9880
|
}
|
|
9450
9881
|
/**
|
|
@@ -9493,7 +9924,7 @@ class SkyElement extends AsyncElement {
|
|
|
9493
9924
|
_center = new playcanvas.Vec3(0, 0.01, 0);
|
|
9494
9925
|
_intensity = 1;
|
|
9495
9926
|
_rotation = new playcanvas.Vec3();
|
|
9496
|
-
|
|
9927
|
+
_mipLevel = 0;
|
|
9497
9928
|
_lighting = false;
|
|
9498
9929
|
_scale = new playcanvas.Vec3(100, 100, 100);
|
|
9499
9930
|
_type = 'infinite';
|
|
@@ -9527,7 +9958,7 @@ class SkyElement extends AsyncElement {
|
|
|
9527
9958
|
this._scene.sky.node.setLocalScale(this._scale);
|
|
9528
9959
|
this._scene.sky.center = this._center;
|
|
9529
9960
|
this._scene.skyboxIntensity = this._intensity;
|
|
9530
|
-
this._scene.skyboxMip = this.
|
|
9961
|
+
this._scene.skyboxMip = this._mipLevel;
|
|
9531
9962
|
}
|
|
9532
9963
|
async _loadSkybox() {
|
|
9533
9964
|
const appElement = await this.closestApp?.ready();
|
|
@@ -9619,23 +10050,6 @@ class SkyElement extends AsyncElement {
|
|
|
9619
10050
|
get intensity() {
|
|
9620
10051
|
return this._intensity;
|
|
9621
10052
|
}
|
|
9622
|
-
/**
|
|
9623
|
-
* Sets the mip level of the skybox.
|
|
9624
|
-
* @param value - The mip level.
|
|
9625
|
-
*/
|
|
9626
|
-
set level(value) {
|
|
9627
|
-
this._level = value;
|
|
9628
|
-
if (this._scene) {
|
|
9629
|
-
this._scene.skyboxMip = this._level;
|
|
9630
|
-
}
|
|
9631
|
-
}
|
|
9632
|
-
/**
|
|
9633
|
-
* Gets the mip level of the skybox.
|
|
9634
|
-
* @returns The mip level.
|
|
9635
|
-
*/
|
|
9636
|
-
get level() {
|
|
9637
|
-
return this._level;
|
|
9638
|
-
}
|
|
9639
10053
|
/**
|
|
9640
10054
|
* Sets whether the skybox is used as a light source.
|
|
9641
10055
|
* @param value - Whether to use lighting.
|
|
@@ -9650,6 +10064,24 @@ class SkyElement extends AsyncElement {
|
|
|
9650
10064
|
get lighting() {
|
|
9651
10065
|
return this._lighting;
|
|
9652
10066
|
}
|
|
10067
|
+
/**
|
|
10068
|
+
* Sets the mip level of the skybox, where 0 is the sharpest. Raising it selects a blurrier mip,
|
|
10069
|
+
* which is how a skybox is softened without blurring the texture itself.
|
|
10070
|
+
* @param value - The mip level.
|
|
10071
|
+
*/
|
|
10072
|
+
set mipLevel(value) {
|
|
10073
|
+
this._mipLevel = value;
|
|
10074
|
+
if (this._scene) {
|
|
10075
|
+
this._scene.skyboxMip = this._mipLevel;
|
|
10076
|
+
}
|
|
10077
|
+
}
|
|
10078
|
+
/**
|
|
10079
|
+
* Gets the mip level of the skybox.
|
|
10080
|
+
* @returns The mip level.
|
|
10081
|
+
*/
|
|
10082
|
+
get mipLevel() {
|
|
10083
|
+
return this._mipLevel;
|
|
10084
|
+
}
|
|
9653
10085
|
/**
|
|
9654
10086
|
* Sets the Euler rotation of the skybox.
|
|
9655
10087
|
* @param value - The rotation.
|
|
@@ -9706,7 +10138,7 @@ class SkyElement extends AsyncElement {
|
|
|
9706
10138
|
return this._type;
|
|
9707
10139
|
}
|
|
9708
10140
|
static get observedAttributes() {
|
|
9709
|
-
return ['asset', 'center', 'intensity', '
|
|
10141
|
+
return ['asset', 'center', 'intensity', 'lighting', 'mip-level', 'rotation', 'scale', 'type'];
|
|
9710
10142
|
}
|
|
9711
10143
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
9712
10144
|
switch (name) {
|
|
@@ -9719,12 +10151,12 @@ class SkyElement extends AsyncElement {
|
|
|
9719
10151
|
case 'intensity':
|
|
9720
10152
|
this.intensity = parseNumber(newValue, 1, name);
|
|
9721
10153
|
break;
|
|
9722
|
-
case 'level':
|
|
9723
|
-
this.level = parseNumber(newValue, 0, name);
|
|
9724
|
-
break;
|
|
9725
10154
|
case 'lighting':
|
|
9726
10155
|
this.lighting = parseBool(newValue, false);
|
|
9727
10156
|
break;
|
|
10157
|
+
case 'mip-level':
|
|
10158
|
+
this.mipLevel = parseNumber(newValue, 0, name);
|
|
10159
|
+
break;
|
|
9728
10160
|
case 'rotation':
|
|
9729
10161
|
this.rotation = parseVec3(newValue, playcanvas.Vec3.ZERO, name);
|
|
9730
10162
|
break;
|