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