@playcanvas/web-components 0.10.0 → 0.10.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/dist/app.d.ts +52 -7
- package/dist/asset.d.ts +9 -0
- package/dist/async-element.d.ts +12 -2
- package/dist/components/component.d.ts +5 -4
- package/dist/custom-elements.json +583 -110
- package/dist/entity.d.ts +5 -4
- package/dist/loading-bar.d.ts +35 -0
- package/dist/material.d.ts +8 -2
- package/dist/pwc.cjs +385 -105
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +385 -105
- 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 +385 -105
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.ts +11 -3
- package/dist/vscode.html-custom-data.json +8 -3
- package/dist/web-types.json +322 -78
- package/package.json +1 -1
- package/src/app.ts +172 -73
- package/src/asset.ts +25 -0
- package/src/async-element.ts +14 -4
- package/src/components/component.ts +6 -5
- package/src/entity.ts +34 -18
- package/src/loading-bar.ts +122 -0
- package/src/material.ts +10 -4
- package/src/scene.ts +49 -19
package/dist/pwc.js
CHANGED
|
@@ -20,11 +20,21 @@
|
|
|
20
20
|
this._readyResolve = resolve;
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The nearest ancestor `<pc-app>` element, or `null` if this element has no `<pc-app>`
|
|
25
|
+
* ancestor. The search starts at the parent, so an element never resolves to itself.
|
|
26
|
+
* @returns The closest app element, or `null`.
|
|
27
|
+
*/
|
|
23
28
|
get closestApp() {
|
|
24
|
-
return this.parentElement?.closest('pc-app');
|
|
29
|
+
return this.parentElement?.closest('pc-app') ?? null;
|
|
25
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The nearest ancestor `<pc-entity>` element, or `null` if this element has no `<pc-entity>`
|
|
33
|
+
* ancestor. The search starts at the parent, so an element never resolves to itself.
|
|
34
|
+
* @returns The closest entity element, or `null`.
|
|
35
|
+
*/
|
|
26
36
|
get closestEntity() {
|
|
27
|
-
return this.parentElement?.closest('pc-entity');
|
|
37
|
+
return this.parentElement?.closest('pc-entity') ?? null;
|
|
28
38
|
}
|
|
29
39
|
/**
|
|
30
40
|
* Called when the element is fully initialized and ready. Subclasses should call this when
|
|
@@ -118,6 +128,116 @@
|
|
|
118
128
|
}
|
|
119
129
|
customElements.define('pc-module', ModuleElement);
|
|
120
130
|
|
|
131
|
+
/** Covers the 0.2s opacity transition; jsdom never fires transitionend, so removal is timed. */
|
|
132
|
+
const REMOVAL_DELAY_MS = 250;
|
|
133
|
+
/**
|
|
134
|
+
* The slim progress bar `<pc-app>` shows while it boots and preloads. An implementation detail of
|
|
135
|
+
* AppElement rather than a custom element, so its shape can change without a breaking change.
|
|
136
|
+
*
|
|
137
|
+
* All styling is inline, so the library injects no stylesheet. The colors and height resolve CSS
|
|
138
|
+
* custom properties — `--pc-loading-bar-color`, `--pc-loading-bar-background` and
|
|
139
|
+
* `--pc-loading-bar-height` — so a page can theme the bar from `pc-app` or `:root`.
|
|
140
|
+
*/
|
|
141
|
+
class LoadingBar {
|
|
142
|
+
_track;
|
|
143
|
+
_fill;
|
|
144
|
+
_sweep = null;
|
|
145
|
+
_removal = null;
|
|
146
|
+
/**
|
|
147
|
+
* Creates the bar and appends it to `parent`, starting in the indeterminate state.
|
|
148
|
+
* @param parent - The element to append the bar to.
|
|
149
|
+
*/
|
|
150
|
+
constructor(parent) {
|
|
151
|
+
this._track = document.createElement('div');
|
|
152
|
+
this._track.setAttribute('role', 'progressbar');
|
|
153
|
+
this._track.setAttribute('aria-label', 'Loading');
|
|
154
|
+
this._track.setAttribute('aria-valuemin', '0');
|
|
155
|
+
this._track.setAttribute('aria-valuemax', '100');
|
|
156
|
+
// Fixed positioning matches the canvas, which always fills the window (FILLMODE_FILL_WINDOW)
|
|
157
|
+
this._track.style.cssText = [
|
|
158
|
+
'position: fixed',
|
|
159
|
+
'top: 0',
|
|
160
|
+
'left: 0',
|
|
161
|
+
'width: 100%',
|
|
162
|
+
'height: var(--pc-loading-bar-height, 3px)',
|
|
163
|
+
'background: var(--pc-loading-bar-background, rgba(0, 0, 0, 0.1))',
|
|
164
|
+
'z-index: 10000',
|
|
165
|
+
'pointer-events: none',
|
|
166
|
+
'opacity: 1',
|
|
167
|
+
'transition: opacity 0.2s ease'
|
|
168
|
+
].join('; ');
|
|
169
|
+
this._fill = document.createElement('div');
|
|
170
|
+
this._fill.style.cssText = [
|
|
171
|
+
'width: 100%',
|
|
172
|
+
'height: 100%',
|
|
173
|
+
'transform-origin: left center',
|
|
174
|
+
'transform: scaleX(0)',
|
|
175
|
+
'background: var(--pc-loading-bar-color, #f60)',
|
|
176
|
+
'transition: transform 0.2s ease'
|
|
177
|
+
].join('; ');
|
|
178
|
+
this._track.appendChild(this._fill);
|
|
179
|
+
parent.appendChild(this._track);
|
|
180
|
+
// Indeterminate sweep until the first progress() call reports a real total. No
|
|
181
|
+
// aria-valuenow is set, which is what marks a progressbar indeterminate. jsdom has no Web
|
|
182
|
+
// Animations API, so the guard degrades to a static bar there rather than crashing boot.
|
|
183
|
+
if (typeof this._fill.animate === 'function') {
|
|
184
|
+
this._sweep = this._fill.animate([
|
|
185
|
+
{ transform: 'scaleX(0.25) translateX(-100%)' },
|
|
186
|
+
{ transform: 'scaleX(0.25) translateX(500%)' }
|
|
187
|
+
], {
|
|
188
|
+
duration: 1000,
|
|
189
|
+
iterations: Infinity,
|
|
190
|
+
easing: 'ease-in-out'
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Reflects preload progress, switching the bar from indeterminate to determinate on the first
|
|
196
|
+
* call.
|
|
197
|
+
* @param loaded - The number of assets that have finished loading.
|
|
198
|
+
* @param total - The number of assets being preloaded.
|
|
199
|
+
*/
|
|
200
|
+
progress(loaded, total) {
|
|
201
|
+
if (this._sweep) {
|
|
202
|
+
this._sweep.cancel();
|
|
203
|
+
this._sweep = null;
|
|
204
|
+
}
|
|
205
|
+
const fraction = total === 0 ? 1 : loaded / total;
|
|
206
|
+
this._track.setAttribute('aria-valuenow', String(Math.round(fraction * 100)));
|
|
207
|
+
this._fill.style.transform = `scaleX(${fraction})`;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Fills the bar, fades it out and removes it. Idempotent.
|
|
211
|
+
*/
|
|
212
|
+
complete() {
|
|
213
|
+
if (this._removal !== null) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (this._sweep) {
|
|
217
|
+
this._sweep.cancel();
|
|
218
|
+
this._sweep = null;
|
|
219
|
+
}
|
|
220
|
+
this._track.setAttribute('aria-valuenow', '100');
|
|
221
|
+
this._fill.style.transform = 'scaleX(1)';
|
|
222
|
+
this._track.style.opacity = '0';
|
|
223
|
+
this._removal = setTimeout(() => this._track.remove(), REMOVAL_DELAY_MS);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Removes the bar immediately, cancelling any pending fade. Idempotent.
|
|
227
|
+
*/
|
|
228
|
+
destroy() {
|
|
229
|
+
if (this._sweep) {
|
|
230
|
+
this._sweep.cancel();
|
|
231
|
+
this._sweep = null;
|
|
232
|
+
}
|
|
233
|
+
if (this._removal !== null) {
|
|
234
|
+
clearTimeout(this._removal);
|
|
235
|
+
this._removal = null;
|
|
236
|
+
}
|
|
237
|
+
this._track.remove();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
121
241
|
const CSS_COLORS = {
|
|
122
242
|
aliceblue: '#f0f8ff',
|
|
123
243
|
antiquewhite: '#faebd7',
|
|
@@ -545,6 +665,11 @@
|
|
|
545
665
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
546
666
|
* The AppElement interface also inherits the properties and methods of the
|
|
547
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.
|
|
548
673
|
*/
|
|
549
674
|
class AppElement extends AsyncElement {
|
|
550
675
|
/**
|
|
@@ -557,6 +682,8 @@
|
|
|
557
682
|
_depth = true;
|
|
558
683
|
_stencil = true;
|
|
559
684
|
_highResolution = true;
|
|
685
|
+
_loadingBar = true;
|
|
686
|
+
_bar = null;
|
|
560
687
|
_hierarchyReady = false;
|
|
561
688
|
_picker = null;
|
|
562
689
|
_hasPointerListeners = {
|
|
@@ -567,20 +694,34 @@
|
|
|
567
694
|
pointermove: false
|
|
568
695
|
};
|
|
569
696
|
_hoveredEntity = null;
|
|
697
|
+
// Identifies the newest in-flight hover pick, so out-of-order results can be discarded
|
|
698
|
+
_pickToken = 0;
|
|
570
699
|
_pointerHandlers = {
|
|
571
700
|
pointermove: null,
|
|
572
701
|
pointerdown: null,
|
|
573
702
|
pointerup: null
|
|
574
703
|
};
|
|
575
704
|
_app = null;
|
|
705
|
+
_loadProgress = 0;
|
|
576
706
|
/**
|
|
577
|
-
* The PlayCanvas application instance.
|
|
578
|
-
* {@link whenReady} or the element's `ready()`
|
|
579
|
-
*
|
|
707
|
+
* The PlayCanvas application instance. `null` until the element is ready, and again once it
|
|
708
|
+
* has been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
709
|
+
* promise before accessing it.
|
|
710
|
+
* @returns The application instance, or `null`.
|
|
580
711
|
*/
|
|
581
712
|
get app() {
|
|
582
713
|
return this._app;
|
|
583
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* The asset preload progress of the application, as a fraction from 0 to 1. It is 0 until
|
|
717
|
+
* preloading begins (and again once the element has been removed from the document), and 1
|
|
718
|
+
* once preloading has finished — including when there was nothing to preload. Read this to
|
|
719
|
+
* initialize a loading UI; subsequent updates arrive via the `progress` event.
|
|
720
|
+
* @returns The preload progress.
|
|
721
|
+
*/
|
|
722
|
+
get loadProgress() {
|
|
723
|
+
return this._loadProgress;
|
|
724
|
+
}
|
|
584
725
|
/**
|
|
585
726
|
* Creates a new AppElement instance.
|
|
586
727
|
*
|
|
@@ -592,6 +733,11 @@
|
|
|
592
733
|
this._onWindowResize = this._onWindowResize.bind(this);
|
|
593
734
|
}
|
|
594
735
|
async connectedCallback() {
|
|
736
|
+
// Created before the first await, so the bar is visible while modules and the graphics
|
|
737
|
+
// device are created, and exists before any disconnect could need to clean it up
|
|
738
|
+
if (this._loadingBar && !this._bar) {
|
|
739
|
+
this._bar = new LoadingBar(this);
|
|
740
|
+
}
|
|
595
741
|
// Get all pc-module elements that are direct children of the pc-app element
|
|
596
742
|
const moduleElements = this.querySelectorAll(':scope > pc-module');
|
|
597
743
|
// Wait for all modules to load
|
|
@@ -679,10 +825,11 @@
|
|
|
679
825
|
createOptions.lightmapper = playcanvas.Lightmapper;
|
|
680
826
|
createOptions.batchManager = playcanvas.BatchManager;
|
|
681
827
|
createOptions.xr = playcanvas.XrManager;
|
|
682
|
-
|
|
683
|
-
this.app
|
|
684
|
-
|
|
685
|
-
|
|
828
|
+
const app = new playcanvas.AppBase(this._canvas);
|
|
829
|
+
this._app = app;
|
|
830
|
+
app.init(createOptions);
|
|
831
|
+
app.setCanvasFillMode(playcanvas.FILLMODE_FILL_WINDOW);
|
|
832
|
+
app.setCanvasResolution(playcanvas.RESOLUTION_AUTO);
|
|
686
833
|
this._pickerCreate();
|
|
687
834
|
// Get all pc-asset elements that are direct children of the pc-app element
|
|
688
835
|
const assetElements = this.querySelectorAll(':scope > pc-asset');
|
|
@@ -690,7 +837,7 @@
|
|
|
690
837
|
assetElement.createAsset();
|
|
691
838
|
const asset = assetElement.asset;
|
|
692
839
|
if (asset) {
|
|
693
|
-
|
|
840
|
+
app.assets.add(asset);
|
|
694
841
|
}
|
|
695
842
|
});
|
|
696
843
|
// Get all pc-material elements that are direct children of the pc-app element
|
|
@@ -701,17 +848,39 @@
|
|
|
701
848
|
// Create all entities
|
|
702
849
|
const entityElements = this.querySelectorAll('pc-entity');
|
|
703
850
|
Array.from(entityElements).forEach((entityElement) => {
|
|
704
|
-
entityElement.createEntity(
|
|
851
|
+
entityElement.createEntity(app);
|
|
705
852
|
});
|
|
706
853
|
// Build hierarchy
|
|
707
854
|
entityElements.forEach((entityElement) => {
|
|
708
|
-
entityElement.buildHierarchy(
|
|
855
|
+
entityElement.buildHierarchy(app);
|
|
709
856
|
});
|
|
710
857
|
this._hierarchyReady = true;
|
|
858
|
+
// Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
|
|
859
|
+
// listener must be attached before preload() is called: an asset that is already loaded
|
|
860
|
+
// ticks synchronously inside it.
|
|
861
|
+
const total = app.assets.list({ preload: true }).length;
|
|
862
|
+
let loaded = 0;
|
|
863
|
+
const onPreloadProgress = () => {
|
|
864
|
+
loaded += 1;
|
|
865
|
+
this._loadProgress = loaded / total;
|
|
866
|
+
this._bar?.progress(loaded, total);
|
|
867
|
+
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded, total }));
|
|
868
|
+
};
|
|
869
|
+
app.on('preload:progress', onPreloadProgress);
|
|
870
|
+
this._loadProgress = total === 0 ? 1 : 0;
|
|
871
|
+
this._bar?.progress(0, total);
|
|
872
|
+
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
|
|
711
873
|
// Load assets before starting the application
|
|
712
|
-
|
|
874
|
+
app.preload(() => {
|
|
875
|
+
// Scope the counter to this preload pass, so a later app.preload() call by user code
|
|
876
|
+
// cannot push `loaded` past `total`
|
|
877
|
+
app.off('preload:progress', onPreloadProgress);
|
|
878
|
+
this._loadProgress = 1;
|
|
713
879
|
// Start the application
|
|
714
|
-
|
|
880
|
+
app.start();
|
|
881
|
+
// Dismiss the bar only once a frame has actually rendered; ready fires before the
|
|
882
|
+
// first rAF tick
|
|
883
|
+
app.once('frameend', () => this._bar?.complete());
|
|
715
884
|
// Handle window resize to keep the canvas responsive
|
|
716
885
|
window.addEventListener('resize', this._onWindowResize);
|
|
717
886
|
this._onReady();
|
|
@@ -720,10 +889,13 @@
|
|
|
720
889
|
disconnectedCallback() {
|
|
721
890
|
this._pickerDestroy();
|
|
722
891
|
// Clean up the application
|
|
723
|
-
if (this.
|
|
724
|
-
this.
|
|
892
|
+
if (this._app) {
|
|
893
|
+
this._app.destroy();
|
|
725
894
|
this._app = null;
|
|
726
895
|
}
|
|
896
|
+
this._loadProgress = 0;
|
|
897
|
+
this._bar?.destroy();
|
|
898
|
+
this._bar = null;
|
|
727
899
|
// Remove event listeners
|
|
728
900
|
window.removeEventListener('resize', this._onWindowResize);
|
|
729
901
|
// Remove the canvas
|
|
@@ -740,10 +912,17 @@
|
|
|
740
912
|
_pickerCreate() {
|
|
741
913
|
const { width, height } = this.app.graphicsDevice;
|
|
742
914
|
this._picker = new playcanvas.Picker(this.app, width, height);
|
|
743
|
-
// Create bound handlers but don't attach them yet
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
915
|
+
// Create bound handlers but don't attach them yet. The handlers pick asynchronously, so
|
|
916
|
+
// each is wrapped to discard the promise - a listener must not return one, and nothing
|
|
917
|
+
// awaits the result.
|
|
918
|
+
const listener = (handler) => {
|
|
919
|
+
return (event) => {
|
|
920
|
+
handler.call(this, event);
|
|
921
|
+
};
|
|
922
|
+
};
|
|
923
|
+
this._pointerHandlers.pointermove = listener(this._onPointerMove);
|
|
924
|
+
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
925
|
+
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
747
926
|
// Listen for pointer listeners being added/removed
|
|
748
927
|
['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
|
|
749
928
|
this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
|
|
@@ -791,29 +970,49 @@
|
|
|
791
970
|
const y = (event.clientY - canvasRect.top) * scaleY;
|
|
792
971
|
return { x, y };
|
|
793
972
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
973
|
+
/**
|
|
974
|
+
* Picks the scene under the pointer and returns the graph node that was hit, or `null`.
|
|
975
|
+
*
|
|
976
|
+
* The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
|
|
977
|
+
* supported on WebGPU, where it returns an empty selection rather than failing - which
|
|
978
|
+
* silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
|
|
979
|
+
* async variant works on both backends and does not block the main thread on a GPU read.
|
|
980
|
+
*
|
|
981
|
+
* @param event - The pointer event to pick under.
|
|
982
|
+
* @returns The graph node under the pointer, or `null` if nothing was hit.
|
|
983
|
+
*/
|
|
984
|
+
async _pickNode(event) {
|
|
797
985
|
const camera = this.app.root.findComponent('camera');
|
|
798
986
|
if (!camera)
|
|
799
|
-
return;
|
|
800
|
-
// Use the helper to convert event coordinates into canvas/picker coordinates.
|
|
987
|
+
return null;
|
|
801
988
|
const { x, y } = this._getPickerCoordinates(event);
|
|
802
989
|
this._picker.prepare(camera, this.app.scene);
|
|
803
|
-
const selection = this._picker.
|
|
990
|
+
const selection = await this._picker.getSelectionAsync(x, y);
|
|
991
|
+
if (selection.length === 0)
|
|
992
|
+
return null;
|
|
993
|
+
const item = selection[0];
|
|
994
|
+
return item instanceof playcanvas.MeshInstance ? item.node : item.entity;
|
|
995
|
+
}
|
|
996
|
+
async _onPointerMove(event) {
|
|
997
|
+
if (!this._picker || !this.app)
|
|
998
|
+
return;
|
|
999
|
+
// Moves arrive faster than a pick resolves, so results can land out of order. Only the
|
|
1000
|
+
// newest pick may update the hover state - an older one describes a pointer position the
|
|
1001
|
+
// user has already left.
|
|
1002
|
+
const token = ++this._pickToken;
|
|
1003
|
+
const node = await this._pickNode(event);
|
|
1004
|
+
if (token !== this._pickToken || !this._picker)
|
|
1005
|
+
return;
|
|
804
1006
|
// Get the currently hovered entity by walking up the hierarchy
|
|
805
1007
|
let newHoverEntity = null;
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
newHoverEntity = entityElement;
|
|
813
|
-
break;
|
|
814
|
-
}
|
|
815
|
-
currentNode = currentNode.parent;
|
|
1008
|
+
let currentNode = node;
|
|
1009
|
+
while (currentNode !== null) {
|
|
1010
|
+
const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
|
|
1011
|
+
if (entityElement) {
|
|
1012
|
+
newHoverEntity = entityElement;
|
|
1013
|
+
break;
|
|
816
1014
|
}
|
|
1015
|
+
currentNode = currentNode.parent;
|
|
817
1016
|
}
|
|
818
1017
|
// Handle enter/leave events
|
|
819
1018
|
if (this._hoveredEntity !== newHoverEntity) {
|
|
@@ -831,46 +1030,30 @@
|
|
|
831
1030
|
newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
|
|
832
1031
|
}
|
|
833
1032
|
}
|
|
834
|
-
_onPointerDown(event) {
|
|
1033
|
+
async _onPointerDown(event) {
|
|
835
1034
|
if (!this._picker || !this.app)
|
|
836
1035
|
return;
|
|
837
|
-
|
|
838
|
-
if (!
|
|
839
|
-
return;
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const item = selection[0];
|
|
846
|
-
let currentNode = item instanceof playcanvas.MeshInstance ? item.node : item.entity;
|
|
847
|
-
while (currentNode !== null) {
|
|
848
|
-
const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
|
|
849
|
-
if (entityElement && entityElement.hasListeners('pointerdown')) {
|
|
850
|
-
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
851
|
-
break;
|
|
852
|
-
}
|
|
853
|
-
currentNode = currentNode.parent;
|
|
1036
|
+
let currentNode = await this._pickNode(event);
|
|
1037
|
+
if (!this._picker)
|
|
1038
|
+
return; // the element disconnected while the pick was in flight
|
|
1039
|
+
while (currentNode !== null) {
|
|
1040
|
+
const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
|
|
1041
|
+
if (entityElement && entityElement.hasListeners('pointerdown')) {
|
|
1042
|
+
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
1043
|
+
break;
|
|
854
1044
|
}
|
|
1045
|
+
currentNode = currentNode.parent;
|
|
855
1046
|
}
|
|
856
1047
|
}
|
|
857
|
-
_onPointerUp(event) {
|
|
1048
|
+
async _onPointerUp(event) {
|
|
858
1049
|
if (!this._picker || !this.app)
|
|
859
1050
|
return;
|
|
860
|
-
const
|
|
861
|
-
if (!
|
|
1051
|
+
const node = await this._pickNode(event);
|
|
1052
|
+
if (!node || !this._picker)
|
|
862
1053
|
return;
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const selection = this._picker.getSelection(x, y);
|
|
867
|
-
if (selection.length > 0) {
|
|
868
|
-
const item = selection[0];
|
|
869
|
-
const node = item instanceof playcanvas.MeshInstance ? item.node : item.entity;
|
|
870
|
-
const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`);
|
|
871
|
-
if (entityElement && entityElement.hasListeners('pointerup')) {
|
|
872
|
-
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
873
|
-
}
|
|
1054
|
+
const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`);
|
|
1055
|
+
if (entityElement && entityElement.hasListeners('pointerup')) {
|
|
1056
|
+
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
874
1057
|
}
|
|
875
1058
|
}
|
|
876
1059
|
_onPointerListenerAdded(type) {
|
|
@@ -981,6 +1164,29 @@
|
|
|
981
1164
|
get highResolution() {
|
|
982
1165
|
return this._highResolution;
|
|
983
1166
|
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Sets whether the application shows its built-in loading bar while it boots and preloads its
|
|
1169
|
+
* assets. Enabled by default; setting `false` removes the bar immediately, while setting
|
|
1170
|
+
* `true` has no effect until the element is next connected. The bar can be themed with the
|
|
1171
|
+
* CSS custom properties `--pc-loading-bar-color`, `--pc-loading-bar-background` and
|
|
1172
|
+
* `--pc-loading-bar-height`.
|
|
1173
|
+
* @param value - The loading bar flag.
|
|
1174
|
+
*/
|
|
1175
|
+
set loadingBar(value) {
|
|
1176
|
+
this._loadingBar = value;
|
|
1177
|
+
if (!value && this._bar) {
|
|
1178
|
+
this._bar.destroy();
|
|
1179
|
+
this._bar = null;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Gets whether the application shows its built-in loading bar while it boots and preloads
|
|
1184
|
+
* its assets.
|
|
1185
|
+
* @returns The loading bar flag.
|
|
1186
|
+
*/
|
|
1187
|
+
get loadingBar() {
|
|
1188
|
+
return this._loadingBar;
|
|
1189
|
+
}
|
|
984
1190
|
/**
|
|
985
1191
|
* Sets the stencil flag.
|
|
986
1192
|
* @param value - The stencil flag.
|
|
@@ -996,7 +1202,7 @@
|
|
|
996
1202
|
return this._stencil;
|
|
997
1203
|
}
|
|
998
1204
|
static get observedAttributes() {
|
|
999
|
-
return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution'];
|
|
1205
|
+
return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution', 'loading-bar'];
|
|
1000
1206
|
}
|
|
1001
1207
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
1002
1208
|
switch (name) {
|
|
@@ -1015,6 +1221,9 @@
|
|
|
1015
1221
|
case 'high-resolution':
|
|
1016
1222
|
this.highResolution = parseBool(newValue, true);
|
|
1017
1223
|
break;
|
|
1224
|
+
case 'loading-bar':
|
|
1225
|
+
this.loadingBar = parseBool(newValue, true);
|
|
1226
|
+
break;
|
|
1018
1227
|
case 'stencil':
|
|
1019
1228
|
this.stencil = parseBool(newValue, true);
|
|
1020
1229
|
break;
|
|
@@ -1086,9 +1295,10 @@
|
|
|
1086
1295
|
_built = false;
|
|
1087
1296
|
_entity = null;
|
|
1088
1297
|
/**
|
|
1089
|
-
* The PlayCanvas entity instance.
|
|
1090
|
-
* {@link whenReady} or the element's `ready()`
|
|
1091
|
-
*
|
|
1298
|
+
* The PlayCanvas entity instance. `null` until the element is ready, and again once it has
|
|
1299
|
+
* been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
1300
|
+
* promise before accessing it.
|
|
1301
|
+
* @returns The entity instance, or `null`.
|
|
1092
1302
|
*/
|
|
1093
1303
|
get entity() {
|
|
1094
1304
|
return this._entity;
|
|
@@ -1100,16 +1310,18 @@
|
|
|
1100
1310
|
if (this._entity) {
|
|
1101
1311
|
return;
|
|
1102
1312
|
}
|
|
1103
|
-
//
|
|
1104
|
-
|
|
1313
|
+
// Seed from the cached fields rather than re-reading the attributes. Every observed
|
|
1314
|
+
// attribute is routed through its property setter by attributeChangedCallback, so the field
|
|
1315
|
+
// already holds the parsed attribute value - and it also holds anything assigned through the
|
|
1316
|
+
// property API before the app booted, which reading the attribute back would discard.
|
|
1317
|
+
const entity = new playcanvas.Entity(this._name, app);
|
|
1105
1318
|
this._entity = entity;
|
|
1106
|
-
entity.enabled =
|
|
1107
|
-
entity.setLocalPosition(
|
|
1108
|
-
entity.setLocalEulerAngles(
|
|
1109
|
-
entity.setLocalScale(
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
entity.tags.add(tags);
|
|
1319
|
+
entity.enabled = this._enabled;
|
|
1320
|
+
entity.setLocalPosition(this._position);
|
|
1321
|
+
entity.setLocalEulerAngles(this._rotation);
|
|
1322
|
+
entity.setLocalScale(this._scale);
|
|
1323
|
+
if (this._tags.length > 0) {
|
|
1324
|
+
entity.tags.add(this._tags);
|
|
1113
1325
|
}
|
|
1114
1326
|
}
|
|
1115
1327
|
buildHierarchy(app) {
|
|
@@ -1128,8 +1340,15 @@
|
|
|
1128
1340
|
connectedCallback() {
|
|
1129
1341
|
// Wait for app to be ready
|
|
1130
1342
|
const closestApp = this.closestApp;
|
|
1131
|
-
if (!closestApp)
|
|
1343
|
+
if (!closestApp) {
|
|
1344
|
+
// An entity outside an application is inert and never becomes ready, so awaiting it
|
|
1345
|
+
// hangs. Warn rather than fail silently, naming the parent it requires, as every other
|
|
1346
|
+
// misplaced element does.
|
|
1347
|
+
const name = this.getAttribute('name');
|
|
1348
|
+
const label = name ? ` '${name}'` : '';
|
|
1349
|
+
console.warn(`pc-entity${label} must be a descendant of pc-app - entity not created`);
|
|
1132
1350
|
return;
|
|
1351
|
+
}
|
|
1133
1352
|
// If app is already running, create entity immediately
|
|
1134
1353
|
if (closestApp.hierarchyReady) {
|
|
1135
1354
|
const app = closestApp.app;
|
|
@@ -1147,10 +1366,15 @@
|
|
|
1147
1366
|
}
|
|
1148
1367
|
disconnectedCallback() {
|
|
1149
1368
|
if (this.entity) {
|
|
1150
|
-
// Notify all children that their entities are about to become invalid
|
|
1369
|
+
// Notify all children that their entities are about to become invalid. Both fields have
|
|
1370
|
+
// to be reset here, not just _entity: a descendant's own disconnectedCallback runs after
|
|
1371
|
+
// this one and skips its reset behind the `if (this.entity)` guard, because we have
|
|
1372
|
+
// already nulled the entity it tests. Leaving _built set would make buildHierarchy bail
|
|
1373
|
+
// on re-insertion, so the descendant would get a fresh entity that is never parented.
|
|
1151
1374
|
const children = this.querySelectorAll('pc-entity');
|
|
1152
1375
|
children.forEach((child) => {
|
|
1153
1376
|
child._entity = null;
|
|
1377
|
+
child._built = false;
|
|
1154
1378
|
});
|
|
1155
1379
|
// Destroy the entity
|
|
1156
1380
|
this.entity.destroy();
|
|
@@ -1605,6 +1829,13 @@
|
|
|
1605
1829
|
* @attribute {number} pixels-per-unit - For a `sprite` asset, the number of pixels per world unit.
|
|
1606
1830
|
* @attribute {'simple' | 'sliced' | 'tiled'} render-mode - For a `sprite` asset, how the sprite is
|
|
1607
1831
|
* rendered when resized.
|
|
1832
|
+
*
|
|
1833
|
+
* @fires {Event} load - Fired each time the asset finishes loading, including a `lazy` asset
|
|
1834
|
+
* loaded later and any subsequent reloads. Does not bubble — listen on this element, or use a
|
|
1835
|
+
* capture-phase listener on an ancestor to observe every asset.
|
|
1836
|
+
* @fires {ErrorEvent} error - Fired when the asset fails to load, with the engine's error in
|
|
1837
|
+
* `message`. Does not bubble. The element still becomes ready — readiness means the load settled,
|
|
1838
|
+
* not that it succeeded.
|
|
1608
1839
|
*/
|
|
1609
1840
|
class AssetElement extends AsyncElement {
|
|
1610
1841
|
_lazy = false;
|
|
@@ -1648,6 +1879,14 @@
|
|
|
1648
1879
|
disconnectedCallback() {
|
|
1649
1880
|
this.destroyAsset();
|
|
1650
1881
|
}
|
|
1882
|
+
_onAssetLoad() {
|
|
1883
|
+
this.dispatchEvent(new Event('load'));
|
|
1884
|
+
}
|
|
1885
|
+
_onAssetError(err) {
|
|
1886
|
+
this.dispatchEvent(new ErrorEvent('error', {
|
|
1887
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1888
|
+
}));
|
|
1889
|
+
}
|
|
1651
1890
|
createAsset() {
|
|
1652
1891
|
const id = this.getAttribute('id') || '';
|
|
1653
1892
|
const src = this.getAttribute('src') || '';
|
|
@@ -1682,6 +1921,10 @@
|
|
|
1682
1921
|
this.asset = new playcanvas.Asset(id, type, src ? { url: src } : null, data);
|
|
1683
1922
|
}
|
|
1684
1923
|
this.asset.preload = !this._lazy;
|
|
1924
|
+
// Forward the engine asset's load outcome as DOM events on this element, like <img>.
|
|
1925
|
+
// Attached before the asset joins the registry, which is what starts a preloaded load.
|
|
1926
|
+
this.asset.on('load', this._onAssetLoad, this);
|
|
1927
|
+
this.asset.on('error', this._onAssetError, this);
|
|
1685
1928
|
}
|
|
1686
1929
|
/**
|
|
1687
1930
|
* Builds the `data` object for the asset from an optional inline `data` attribute (JSON) and,
|
|
@@ -1736,6 +1979,9 @@
|
|
|
1736
1979
|
}
|
|
1737
1980
|
destroyAsset() {
|
|
1738
1981
|
if (this.asset) {
|
|
1982
|
+
// A caller that keeps the Asset alive must not dispatch on a removed element
|
|
1983
|
+
this.asset.off('load', this._onAssetLoad, this);
|
|
1984
|
+
this.asset.off('error', this._onAssetError, this);
|
|
1739
1985
|
// Deregister first so unload() can still notify the registry
|
|
1740
1986
|
this.asset.registry?.remove(this.asset);
|
|
1741
1987
|
this.asset.unload();
|
|
@@ -1831,9 +2077,10 @@
|
|
|
1831
2077
|
this._appElement = null;
|
|
1832
2078
|
}
|
|
1833
2079
|
/**
|
|
1834
|
-
* The PlayCanvas component instance.
|
|
1835
|
-
* {@link whenReady} or the
|
|
1836
|
-
*
|
|
2080
|
+
* The PlayCanvas component instance. `null` until the element is ready, and also for an
|
|
2081
|
+
* element that is not a descendant of a `<pc-entity>` — await {@link whenReady} or the
|
|
2082
|
+
* element's `ready()` promise before accessing it.
|
|
2083
|
+
* @returns The component instance, or `null`.
|
|
1837
2084
|
*/
|
|
1838
2085
|
get component() {
|
|
1839
2086
|
return this._component;
|
|
@@ -4726,8 +4973,14 @@
|
|
|
4726
4973
|
* created on insertion.
|
|
4727
4974
|
*
|
|
4728
4975
|
* The element is metal/rough by default: unlike a bare `StandardMaterial` it enables the metalness
|
|
4729
|
-
* workflow, which is what the `metalness-*` attributes assume and what glTF means by PBR.
|
|
4730
|
-
* `
|
|
4976
|
+
* workflow, which is what the `metalness-*` attributes assume and what glTF means by PBR. It also
|
|
4977
|
+
* defaults `metalness` to 0 rather than the engine's 1, because those two defaults have to be
|
|
4978
|
+
* chosen together - the engine's 1 is unreachable under its own `useMetalness` of false, and with
|
|
4979
|
+
* the workflow on it would make every material fully metallic, so `<pc-material diffuse="crimson">`
|
|
4980
|
+
* would render as dark tinted reflections of an environment that may not exist rather than as a
|
|
4981
|
+
* crimson surface. `metalness="1"` remains one attribute away.
|
|
4982
|
+
*
|
|
4983
|
+
* The `roughness` and `roughness-map` attributes are aliases for `gloss` and `gloss-map` that
|
|
4731
4984
|
* additionally invert the gloss channel; do not mix the two families on one element.
|
|
4732
4985
|
*
|
|
4733
4986
|
* The two aliases are documented here rather than on an accessor, because they resolve to the
|
|
@@ -4787,7 +5040,7 @@
|
|
|
4787
5040
|
_heightMapRotation = 0;
|
|
4788
5041
|
_heightMapTiling = new playcanvas.Vec2(1, 1);
|
|
4789
5042
|
_heightMapUv = 0;
|
|
4790
|
-
_metalness =
|
|
5043
|
+
_metalness = 0;
|
|
4791
5044
|
_metalnessMap = '';
|
|
4792
5045
|
_metalnessMapChannel = 'g';
|
|
4793
5046
|
_metalnessMapOffset = new playcanvas.Vec2(0, 0);
|
|
@@ -6730,7 +6983,7 @@
|
|
|
6730
6983
|
this.heightMapUv = parseNumber(newValue, 0, name);
|
|
6731
6984
|
break;
|
|
6732
6985
|
case 'metalness':
|
|
6733
|
-
this.metalness = parseNumber(newValue,
|
|
6986
|
+
this.metalness = parseNumber(newValue, 0, name);
|
|
6734
6987
|
break;
|
|
6735
6988
|
case 'metalness-map':
|
|
6736
6989
|
this.metalnessMap = newValue ?? '';
|
|
@@ -9328,30 +9581,58 @@
|
|
|
9328
9581
|
_gravity = new playcanvas.Vec3(0, -9.81, 0);
|
|
9329
9582
|
_scene = null;
|
|
9330
9583
|
/**
|
|
9331
|
-
* The PlayCanvas scene instance.
|
|
9584
|
+
* The PlayCanvas scene instance. `null` until the element is ready — await
|
|
9332
9585
|
* {@link whenReady} or the element's `ready()` promise before accessing it.
|
|
9333
|
-
* @returns The scene instance
|
|
9586
|
+
* @returns The scene instance, or `null`.
|
|
9334
9587
|
*/
|
|
9335
9588
|
get scene() {
|
|
9336
9589
|
return this._scene;
|
|
9337
9590
|
}
|
|
9338
9591
|
async connectedCallback() {
|
|
9339
|
-
|
|
9340
|
-
|
|
9592
|
+
const appElement = this.closestApp;
|
|
9593
|
+
if (!appElement) {
|
|
9594
|
+
console.warn('pc-scene must be a descendant of pc-app - scene settings not applied');
|
|
9595
|
+
return;
|
|
9596
|
+
}
|
|
9597
|
+
await appElement.ready();
|
|
9598
|
+
// The element may have been removed or re-parented while waiting for the app. Matches the
|
|
9599
|
+
// guard in AssetElement and MaterialElement, but compares closestApp rather than
|
|
9600
|
+
// parentElement because pc-scene resolves its app by ancestor rather than direct child.
|
|
9601
|
+
// Without this, a scene re-parented mid-await would take its Scene from the app it started
|
|
9602
|
+
// under while _applyGravity resolved the app it ended up under, splitting the two.
|
|
9603
|
+
if (!this.isConnected || this.closestApp !== appElement) {
|
|
9604
|
+
return;
|
|
9605
|
+
}
|
|
9606
|
+
// The application is gone if the tree was torn down while we awaited readiness. There is
|
|
9607
|
+
// nothing to configure and nothing the author can act on, so this stays silent.
|
|
9608
|
+
const app = appElement.app;
|
|
9609
|
+
if (!app) {
|
|
9610
|
+
return;
|
|
9611
|
+
}
|
|
9612
|
+
this._scene = app.scene;
|
|
9341
9613
|
this.updateSceneSettings();
|
|
9342
9614
|
this._onReady();
|
|
9343
9615
|
}
|
|
9344
9616
|
updateSceneSettings() {
|
|
9345
|
-
if (this.
|
|
9346
|
-
this.
|
|
9347
|
-
this.
|
|
9348
|
-
this.
|
|
9349
|
-
this.
|
|
9350
|
-
this.
|
|
9351
|
-
|
|
9352
|
-
appElement.app.systems.rigidbody.gravity.copy(this._gravity);
|
|
9617
|
+
if (this._scene) {
|
|
9618
|
+
this._scene.fog.type = this._fog;
|
|
9619
|
+
this._scene.fog.color = this._fogColor;
|
|
9620
|
+
this._scene.fog.density = this._fogDensity;
|
|
9621
|
+
this._scene.fog.start = this._fogStart;
|
|
9622
|
+
this._scene.fog.end = this._fogEnd;
|
|
9623
|
+
this._applyGravity(this._gravity);
|
|
9353
9624
|
}
|
|
9354
9625
|
}
|
|
9626
|
+
/**
|
|
9627
|
+
* Applies gravity to the rigid body system. Resolved through `closestApp` rather than
|
|
9628
|
+
* `parentElement` so that a `<pc-scene>` nested inside a wrapper element behaves the same as
|
|
9629
|
+
* a direct child, matching how `connectedCallback` resolves the application.
|
|
9630
|
+
*
|
|
9631
|
+
* @param value - The gravity to apply.
|
|
9632
|
+
*/
|
|
9633
|
+
_applyGravity(value) {
|
|
9634
|
+
this.closestApp?.app?.systems.rigidbody?.gravity.copy(value);
|
|
9635
|
+
}
|
|
9355
9636
|
/**
|
|
9356
9637
|
* Sets the fog type of the scene. Can be `none`, `linear`, `exp` or `exp2`. Defaults to
|
|
9357
9638
|
* `none`.
|
|
@@ -9444,9 +9725,8 @@
|
|
|
9444
9725
|
*/
|
|
9445
9726
|
set gravity(value) {
|
|
9446
9727
|
this._gravity = value;
|
|
9447
|
-
if (this.
|
|
9448
|
-
|
|
9449
|
-
appElement.app.systems.rigidbody.gravity.copy(value);
|
|
9728
|
+
if (this._scene) {
|
|
9729
|
+
this._applyGravity(value);
|
|
9450
9730
|
}
|
|
9451
9731
|
}
|
|
9452
9732
|
/**
|