@playcanvas/web-components 0.11.1 → 0.13.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/README.md +1 -1
- package/dist/app.d.cts +246 -0
- package/dist/app.d.ts +38 -13
- package/dist/asset.d.cts +218 -0
- package/dist/asset.d.ts +145 -6
- package/dist/async-element.d.cts +103 -0
- package/dist/async-element.d.ts +11 -8
- package/dist/colors.d.cts +1 -0
- package/dist/components/button-component.d.cts +186 -0
- package/dist/components/button-component.d.ts +1 -1
- package/dist/components/camera-component.d.cts +253 -0
- package/dist/components/camera-component.d.ts +1 -1
- package/dist/components/collision-component.d.cts +73 -0
- package/dist/components/collision-component.d.ts +17 -1
- package/dist/components/component.d.cts +82 -0
- package/dist/components/component.d.ts +20 -1
- package/dist/components/element-component.d.cts +316 -0
- package/dist/components/element-component.d.ts +1 -1
- package/dist/components/gsplat-component.d.cts +108 -0
- package/dist/components/gsplat-component.d.ts +1 -1
- package/dist/components/layoutchild-component.d.cts +110 -0
- package/dist/components/layoutchild-component.d.ts +1 -1
- package/dist/components/layoutgroup-component.d.cts +136 -0
- package/dist/components/layoutgroup-component.d.ts +1 -1
- package/dist/components/light-component.d.cts +264 -0
- package/dist/components/light-component.d.ts +1 -1
- package/dist/components/listener-component.d.cts +20 -0
- package/dist/components/listener-component.d.ts +1 -1
- package/dist/components/particlesystem-component.d.cts +52 -0
- package/dist/components/particlesystem-component.d.ts +1 -1
- package/dist/components/render-component.d.cts +76 -0
- package/dist/components/render-component.d.ts +1 -1
- package/dist/components/rigidbody-component.d.cts +88 -0
- package/dist/components/rigidbody-component.d.ts +1 -1
- package/dist/components/screen-component.d.cts +70 -0
- package/dist/components/screen-component.d.ts +1 -1
- package/dist/components/script-component.d.cts +163 -0
- package/dist/components/script-component.d.ts +1 -1
- package/dist/components/script.d.cts +94 -0
- package/dist/components/script.d.ts +1 -1
- package/dist/components/scrollbar-component.d.cts +69 -0
- package/dist/components/scrollbar-component.d.ts +1 -1
- package/dist/components/scrollview-component.d.cts +178 -0
- package/dist/components/scrollview-component.d.ts +1 -1
- package/dist/components/sound-component.d.cts +108 -0
- package/dist/components/sound-component.d.ts +1 -1
- package/dist/components/sound-slot.d.cts +134 -0
- package/dist/components/sound-slot.d.ts +2 -2
- package/dist/custom-elements.json +1127 -179
- package/dist/entity-base.d.cts +67 -0
- package/dist/entity-base.d.ts +67 -0
- package/dist/entity.d.cts +131 -0
- package/dist/entity.d.ts +3 -38
- package/dist/index.d.cts +79 -0
- package/dist/index.d.ts +35 -32
- package/dist/loading-bar.d.cts +35 -0
- package/dist/material.d.cts +1011 -0
- package/dist/material.d.ts +2 -1
- package/dist/model.d.cts +72 -0
- package/dist/model.d.ts +27 -5
- package/dist/module.d.cts +29 -0
- package/dist/module.d.ts +16 -10
- package/dist/node.d.cts +253 -0
- package/dist/node.d.ts +253 -0
- package/dist/parse.d.cts +147 -0
- package/dist/pwc.cjs +1496 -200
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +1497 -201
- 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 +1496 -202
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.cts +117 -0
- package/dist/scene.d.ts +1 -1
- package/dist/sky.d.cts +121 -0
- package/dist/sky.d.ts +1 -1
- package/dist/vscode.html-custom-data.json +127 -5
- package/dist/web-types.json +399 -59
- package/package.json +16 -7
- package/src/app.ts +137 -45
- package/src/asset.ts +439 -9
- package/src/async-element.ts +11 -8
- package/src/components/collision-component.ts +35 -0
- package/src/components/component.ts +93 -3
- package/src/entity-base.ts +136 -0
- package/src/entity.ts +23 -117
- package/src/index.ts +5 -0
- package/src/loading-bar.ts +2 -2
- package/src/material.ts +2 -2
- package/src/model.ts +79 -11
- package/src/module.ts +39 -20
- package/src/node.ts +715 -0
- package/src/sky.ts +0 -1
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,
|
|
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_NONE, RESOLUTION_AUTO, Picker, MeshInstance, Entity, Asset, SPRITE_RENDERMODE_SIMPLE, FILTER_LINEAR_MIPMAP_LINEAR, FILTER_LINEAR, ADDRESS_REPEAT, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, ADDRESS_CLAMP_TO_EDGE, ADDRESS_MIRRORED_REPEAT, FILTER_NEAREST, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_LINEAR_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, 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.
|
|
@@ -27,12 +27,13 @@ class AsyncElement extends HTMLElement {
|
|
|
27
27
|
return this.parentElement?.closest('pc-app') ?? null;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* The nearest ancestor
|
|
31
|
-
* ancestor. The search starts at the parent, so an element
|
|
32
|
-
*
|
|
30
|
+
* The nearest ancestor element that fronts an entity — `<pc-entity>` or `<pc-node>` — or
|
|
31
|
+
* `null` if this element has no such ancestor. The search starts at the parent, so an element
|
|
32
|
+
* never resolves to itself.
|
|
33
|
+
* @returns The closest entity-fronting element, or `null`.
|
|
33
34
|
*/
|
|
34
35
|
get closestEntity() {
|
|
35
|
-
return this.parentElement?.closest('pc-entity') ?? null;
|
|
36
|
+
return this.parentElement?.closest('pc-entity, pc-node') ?? null;
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
39
|
* Called when the element is fully initialized and ready. Subclasses should call this when
|
|
@@ -107,12 +108,18 @@ async function whenReady(target) {
|
|
|
107
108
|
|
|
108
109
|
/**
|
|
109
110
|
* The ModuleElement interface provides properties and methods for manipulating
|
|
110
|
-
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`}
|
|
111
|
-
* The ModuleElement interface also inherits the properties and methods of the
|
|
112
|
-
* {@link
|
|
111
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`}
|
|
112
|
+
* elements. The ModuleElement interface also inherits the properties and methods of the
|
|
113
|
+
* {@link AsyncElement} interface.
|
|
113
114
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
115
|
+
* The attributes are read once, when the module starts loading - on the element's first
|
|
116
|
+
* connection, or earlier if a containing `<pc-app>` boots first and collects it - so changing
|
|
117
|
+
* them later has no effect. The element becomes ready once the module has loaded. WebAssembly
|
|
118
|
+
* modules configure engine-global state that never unloads, so readiness is not re-armed by
|
|
119
|
+
* removing the element, and a re-inserted element does not load again.
|
|
120
|
+
*
|
|
121
|
+
* A `<pc-module>` without a `name` warns and never becomes ready; a containing `<pc-app>` still
|
|
122
|
+
* boots.
|
|
116
123
|
*
|
|
117
124
|
* @attribute {string} name - The name of the WebAssembly module to configure, e.g. `Basis` or
|
|
118
125
|
* `Ammo`.
|
|
@@ -121,19 +128,22 @@ async function whenReady(target) {
|
|
|
121
128
|
* @attribute {string} fallback - The URL of the module's asm.js fallback script, used when
|
|
122
129
|
* WebAssembly is unavailable.
|
|
123
130
|
*/
|
|
124
|
-
class ModuleElement extends
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
super();
|
|
129
|
-
this.loadPromise = this.loadModule();
|
|
131
|
+
class ModuleElement extends AsyncElement {
|
|
132
|
+
_loadPromise = null;
|
|
133
|
+
connectedCallback() {
|
|
134
|
+
this._getLoadPromise();
|
|
130
135
|
}
|
|
131
|
-
async
|
|
136
|
+
async _loadModule() {
|
|
132
137
|
const name = this.getAttribute('name');
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
if (!name) {
|
|
139
|
+
console.warn("pc-module requires a 'name' attribute - no module was configured");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const config = {
|
|
143
|
+
glueUrl: this.getAttribute('glue') ?? undefined,
|
|
144
|
+
wasmUrl: this.getAttribute('wasm') ?? undefined,
|
|
145
|
+
fallbackUrl: this.getAttribute('fallback') ?? undefined
|
|
146
|
+
};
|
|
137
147
|
if (name === 'Basis') {
|
|
138
148
|
basisInitialize(config);
|
|
139
149
|
}
|
|
@@ -143,16 +153,22 @@ class ModuleElement extends HTMLElement {
|
|
|
143
153
|
WasmModule.getInstance(name, () => resolve());
|
|
144
154
|
});
|
|
145
155
|
}
|
|
156
|
+
this._onReady();
|
|
146
157
|
}
|
|
147
158
|
/**
|
|
148
|
-
* Returns the promise that settles when the module has loaded
|
|
149
|
-
* `<pc-app>`
|
|
159
|
+
* Returns the promise that settles when the module has loaded, starting the load if it has
|
|
160
|
+
* not already started - a containing `<pc-app>` boots in document order, so it may collect
|
|
161
|
+
* this element before the element's own connectedCallback has run. A missing `name` resolves
|
|
162
|
+
* the promise without configuring anything, so a misconfigured module never blocks the app.
|
|
150
163
|
*
|
|
151
164
|
* @returns The load promise.
|
|
152
165
|
* @internal
|
|
153
166
|
*/
|
|
154
167
|
_getLoadPromise() {
|
|
155
|
-
|
|
168
|
+
if (!this._loadPromise) {
|
|
169
|
+
this._loadPromise = this._loadModule();
|
|
170
|
+
}
|
|
171
|
+
return this._loadPromise;
|
|
156
172
|
}
|
|
157
173
|
}
|
|
158
174
|
customElements.define('pc-module', ModuleElement);
|
|
@@ -182,9 +198,9 @@ class LoadingBar {
|
|
|
182
198
|
this._track.setAttribute('aria-label', 'Loading');
|
|
183
199
|
this._track.setAttribute('aria-valuemin', '0');
|
|
184
200
|
this._track.setAttribute('aria-valuemax', '100');
|
|
185
|
-
//
|
|
201
|
+
// Anchored to the pc-app element, which the library's base styles make a positioned box
|
|
186
202
|
this._track.style.cssText = [
|
|
187
|
-
'position:
|
|
203
|
+
'position: absolute',
|
|
188
204
|
'top: 0',
|
|
189
205
|
'left: 0',
|
|
190
206
|
'width: 100%',
|
|
@@ -696,16 +712,44 @@ const getEntity = (ref) => {
|
|
|
696
712
|
|
|
697
713
|
/** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
|
|
698
714
|
const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
|
|
715
|
+
/**
|
|
716
|
+
* Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
|
|
717
|
+
* box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
|
|
718
|
+
* canvas and loading bar anchored to it. `:where()` keeps every declaration at zero specificity,
|
|
719
|
+
* so any page rule - however plain - overrides these defaults.
|
|
720
|
+
*/
|
|
721
|
+
const ensureBaseStyles = () => {
|
|
722
|
+
const id = 'pc-app-styles';
|
|
723
|
+
if (document.getElementById(id)) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const style = document.createElement('style');
|
|
727
|
+
style.id = id;
|
|
728
|
+
style.textContent = ':where(pc-app) { display: block; position: relative; width: 300px; height: 150px; }';
|
|
729
|
+
document.head.appendChild(style);
|
|
730
|
+
};
|
|
699
731
|
/**
|
|
700
732
|
* The AppElement interface provides properties and methods for manipulating
|
|
701
733
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
702
734
|
* The AppElement interface also inherits the properties and methods of the
|
|
703
735
|
* {@link HTMLElement} interface.
|
|
704
736
|
*
|
|
737
|
+
* The element is sized like a replaced element such as `<video>`: a block-level box that the
|
|
738
|
+
* page's CSS controls, 300x150 by default. The application's canvas always fills the element,
|
|
739
|
+
* and the drawing buffer resolution follows the element's size (capped by `max-pixel-ratio`),
|
|
740
|
+
* tracked live via a ResizeObserver — so the element can be embedded at any size, resized by
|
|
741
|
+
* its container, or made fullscreen with ordinary CSS such as `width: 100vw; height: 100dvh`.
|
|
742
|
+
*
|
|
705
743
|
* @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
|
|
706
744
|
* `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
|
|
707
745
|
* Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
|
|
708
746
|
* not bubble.
|
|
747
|
+
*
|
|
748
|
+
* @fires {ErrorEvent} error - Fired when the application cannot boot because no graphics device
|
|
749
|
+
* could be created (for example, a browser with WebGL disabled). `message` names the requested
|
|
750
|
+
* backends and `error` holds the underlying failure. The element never becomes ready
|
|
751
|
+
* and `app` stays `null` — listen for this event to show a fallback UI. Removing the element and
|
|
752
|
+
* re-inserting it retries the boot with its current attributes. Does not bubble.
|
|
709
753
|
*/
|
|
710
754
|
class AppElement extends AsyncElement {
|
|
711
755
|
/**
|
|
@@ -742,8 +786,9 @@ class AppElement extends AsyncElement {
|
|
|
742
786
|
_bootGeneration = 0;
|
|
743
787
|
/**
|
|
744
788
|
* The elements backing this application's entities, keyed by the entity itself. Registered
|
|
745
|
-
* by EntityElement at creation and removed when an entity is
|
|
746
|
-
* scene nodes back to their owning elements by
|
|
789
|
+
* by EntityElement at creation (and NodeElement at binding) and removed when an entity is
|
|
790
|
+
* destroyed or unbound, this joins engine scene nodes back to their owning elements by
|
|
791
|
+
* identity - never by name.
|
|
747
792
|
*/
|
|
748
793
|
_entityElements = new Map();
|
|
749
794
|
_picker = null;
|
|
@@ -764,6 +809,12 @@ class AppElement extends AsyncElement {
|
|
|
764
809
|
};
|
|
765
810
|
_app = null;
|
|
766
811
|
_loadProgress = 0;
|
|
812
|
+
/**
|
|
813
|
+
* Tracks the element's box so the drawing buffer and picker follow it. Created per boot once
|
|
814
|
+
* the application exists, and disconnected on teardown. `null` where ResizeObserver is
|
|
815
|
+
* unavailable (jsdom), where the boot-time resolution set is the only sizing that happens.
|
|
816
|
+
*/
|
|
817
|
+
_resizeObserver = null;
|
|
767
818
|
/**
|
|
768
819
|
* The PlayCanvas application instance. `null` until the element is ready, and again once it
|
|
769
820
|
* has been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
@@ -790,8 +841,6 @@ class AppElement extends AsyncElement {
|
|
|
790
841
|
*/
|
|
791
842
|
constructor() {
|
|
792
843
|
super();
|
|
793
|
-
// Bind methods to maintain 'this' context
|
|
794
|
-
this._onWindowResize = this._onWindowResize.bind(this);
|
|
795
844
|
// Track pointer listeners being added to and removed from descendant entities.
|
|
796
845
|
// Registered once here rather than on every boot - the handlers no-op while there is no
|
|
797
846
|
// canvas, and a re-booted element must not stack a second set.
|
|
@@ -802,6 +851,9 @@ class AppElement extends AsyncElement {
|
|
|
802
851
|
}
|
|
803
852
|
async connectedCallback() {
|
|
804
853
|
const generation = ++this._bootGeneration;
|
|
854
|
+
// Installed before the loading bar is created: the bar anchors to this element, which
|
|
855
|
+
// these styles make a positioned block box
|
|
856
|
+
ensureBaseStyles();
|
|
805
857
|
// Created before the first await, so the bar is visible while modules and the graphics
|
|
806
858
|
// device are created, and exists before any disconnect could need to clean it up
|
|
807
859
|
if (this._loadingBar && !this._bar) {
|
|
@@ -816,8 +868,11 @@ class AppElement extends AsyncElement {
|
|
|
816
868
|
if (generation !== this._bootGeneration) {
|
|
817
869
|
return;
|
|
818
870
|
}
|
|
819
|
-
// Create and append the canvas
|
|
871
|
+
// Create and append the canvas, filling the element's content box - the page sizes the
|
|
872
|
+
// element, and everything else follows. touch-action: none keeps touch drags driving the
|
|
873
|
+
// engine's input handlers instead of scrolling the page.
|
|
820
874
|
this._canvas = document.createElement('canvas');
|
|
875
|
+
this._canvas.style.cssText = 'display: block; width: 100%; height: 100%; touch-action: none;';
|
|
821
876
|
this.appendChild(this._canvas);
|
|
822
877
|
// Configure device types based on backend selection
|
|
823
878
|
const backendToDeviceTypes = {
|
|
@@ -827,14 +882,44 @@ class AppElement extends AsyncElement {
|
|
|
827
882
|
};
|
|
828
883
|
const deviceTypes = backendToDeviceTypes[this._backend] || [];
|
|
829
884
|
this._optionsLocked = true;
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
885
|
+
// createGraphicsDevice appends its final null-device fallback to the array in place, so
|
|
886
|
+
// the requested list is captured now for the failure message.
|
|
887
|
+
const requested = deviceTypes.join(', ');
|
|
888
|
+
let device;
|
|
889
|
+
try {
|
|
890
|
+
device = await createGraphicsDevice(this._canvas, {
|
|
891
|
+
// @ts-ignore - alpha needs to be documented
|
|
892
|
+
alpha: this._alpha,
|
|
893
|
+
antialias: this._antialias,
|
|
894
|
+
depth: this._depthBuffer,
|
|
895
|
+
deviceTypes: deviceTypes,
|
|
896
|
+
stencil: this._stencilBuffer
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
// The element may have been removed while device creation was failing. The teardown
|
|
901
|
+
// has already cleaned up, and the failure belongs to a boot that no longer owns the
|
|
902
|
+
// element.
|
|
903
|
+
if (generation !== this._bootGeneration) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
// Return the element to its pre-boot state - no dead canvas, no loading bar stuck at
|
|
907
|
+
// zero - before announcing the failure. Readiness deliberately stays pending: nothing
|
|
908
|
+
// it would announce (the app, the entity hierarchy) exists, so a device-less element
|
|
909
|
+
// joins the documented never-ready cases and the failure surfaces through the error
|
|
910
|
+
// event instead.
|
|
911
|
+
if (this._canvas && this.contains(this._canvas)) {
|
|
912
|
+
this.removeChild(this._canvas);
|
|
913
|
+
}
|
|
914
|
+
this._canvas = null;
|
|
915
|
+
this._bar?.destroy();
|
|
916
|
+
this._bar = null;
|
|
917
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
918
|
+
const message = `pc-app failed to create a graphics device (${requested}) - ${reason}`;
|
|
919
|
+
console.error(message, error);
|
|
920
|
+
this.dispatchEvent(new ErrorEvent('error', { message, error }));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
838
923
|
// The element may have been removed while the device was created. disconnectedCallback
|
|
839
924
|
// has already cleaned up the canvas; the device was created inside the await, so it is
|
|
840
925
|
// this boot's to release.
|
|
@@ -913,9 +998,19 @@ class AppElement extends AsyncElement {
|
|
|
913
998
|
const app = new AppBase(this._canvas);
|
|
914
999
|
this._app = app;
|
|
915
1000
|
app.init(createOptions);
|
|
916
|
-
|
|
1001
|
+
// FILLMODE_NONE leaves the canvas's CSS sizing alone (the engine's other fill modes
|
|
1002
|
+
// stamp window-derived pixel sizes onto it); RESOLUTION_AUTO sizes the drawing buffer
|
|
1003
|
+
// from the canvas's client size
|
|
1004
|
+
app.setCanvasFillMode(FILLMODE_NONE);
|
|
917
1005
|
app.setCanvasResolution(RESOLUTION_AUTO);
|
|
918
1006
|
this._pickerCreate();
|
|
1007
|
+
// Track the element's box rather than the window: containers resize without any window
|
|
1008
|
+
// event (splitter drags, flex reflow, animations). Guarded because jsdom has no
|
|
1009
|
+
// ResizeObserver - there, the resolution set above is the only sizing that happens.
|
|
1010
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
1011
|
+
this._resizeObserver = new ResizeObserver(() => this._syncCanvasSize());
|
|
1012
|
+
this._resizeObserver.observe(this);
|
|
1013
|
+
}
|
|
919
1014
|
// Get all pc-asset elements that are direct children of the pc-app element
|
|
920
1015
|
const assetElements = this.querySelectorAll(':scope > pc-asset');
|
|
921
1016
|
for (const assetElement of Array.from(assetElements)) {
|
|
@@ -991,8 +1086,6 @@ class AppElement extends AsyncElement {
|
|
|
991
1086
|
// Dismiss the bar only once a frame has actually rendered; ready fires before the
|
|
992
1087
|
// first rAF tick
|
|
993
1088
|
app.once('frameend', () => this._bar?.complete());
|
|
994
|
-
// Handle window resize to keep the canvas responsive
|
|
995
|
-
window.addEventListener('resize', this._onWindowResize);
|
|
996
1089
|
this._onReady();
|
|
997
1090
|
});
|
|
998
1091
|
}
|
|
@@ -1017,18 +1110,27 @@ class AppElement extends AsyncElement {
|
|
|
1017
1110
|
// no longer holds.
|
|
1018
1111
|
this._hierarchyReady = false;
|
|
1019
1112
|
this._resetReady();
|
|
1020
|
-
//
|
|
1021
|
-
|
|
1113
|
+
// Stop tracking the element's size
|
|
1114
|
+
this._resizeObserver?.disconnect();
|
|
1115
|
+
this._resizeObserver = null;
|
|
1022
1116
|
// Remove the canvas
|
|
1023
1117
|
if (this._canvas && this.contains(this._canvas)) {
|
|
1024
1118
|
this.removeChild(this._canvas);
|
|
1025
1119
|
this._canvas = null;
|
|
1026
1120
|
}
|
|
1027
1121
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1122
|
+
/**
|
|
1123
|
+
* Syncs the drawing buffer and the picker to the canvas's current CSS size. The picker must
|
|
1124
|
+
* track the buffer, or picks would land at stale coordinates after a resize. Skipped while
|
|
1125
|
+
* an XR session presents - the session owns the buffer size.
|
|
1126
|
+
*/
|
|
1127
|
+
_syncCanvasSize() {
|
|
1128
|
+
if (!this.app || this.app.xr?.active) {
|
|
1129
|
+
return;
|
|
1031
1130
|
}
|
|
1131
|
+
this.app.updateCanvasSize();
|
|
1132
|
+
const { width, height } = this.app.graphicsDevice;
|
|
1133
|
+
this._picker?.resize(width, height);
|
|
1032
1134
|
}
|
|
1033
1135
|
_pickerCreate() {
|
|
1034
1136
|
const { width, height } = this.app.graphicsDevice;
|
|
@@ -1048,7 +1150,7 @@ class AppElement extends AsyncElement {
|
|
|
1048
1150
|
// created from onpointer* attributes when their elements were first upgraded, or
|
|
1049
1151
|
// listeners carried over from before a re-boot)
|
|
1050
1152
|
pointerEventTypes.forEach((type) => {
|
|
1051
|
-
const anyListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
|
|
1153
|
+
const anyListeners = Array.from(this.querySelectorAll('pc-entity, pc-node')).some((entity) => entity._hasListeners(type));
|
|
1052
1154
|
if (anyListeners) {
|
|
1053
1155
|
this._onPointerListenerAdded(type);
|
|
1054
1156
|
}
|
|
@@ -1078,11 +1180,11 @@ class AppElement extends AsyncElement {
|
|
|
1078
1180
|
};
|
|
1079
1181
|
}
|
|
1080
1182
|
/**
|
|
1081
|
-
* Registers the element that
|
|
1082
|
-
* entity.
|
|
1183
|
+
* Registers the element that fronts an entity. Called by EntityElement when it creates its
|
|
1184
|
+
* entity, and by NodeElement when it binds one.
|
|
1083
1185
|
*
|
|
1084
1186
|
* @param entity - The entity.
|
|
1085
|
-
* @param element - The element that
|
|
1187
|
+
* @param element - The element that fronts it.
|
|
1086
1188
|
* @internal
|
|
1087
1189
|
*/
|
|
1088
1190
|
_registerEntityElement(entity, element) {
|
|
@@ -1098,21 +1200,22 @@ class AppElement extends AsyncElement {
|
|
|
1098
1200
|
this._entityElements.delete(entity);
|
|
1099
1201
|
}
|
|
1100
1202
|
/**
|
|
1101
|
-
* Returns the `<pc-entity>` element whose backing entity is `entity`, or
|
|
1102
|
-
* entity
|
|
1103
|
-
* model's instantiated hierarchy, or an entity created through the
|
|
1203
|
+
* Returns the `<pc-entity>` or `<pc-node>` element whose backing entity is `entity`, or
|
|
1204
|
+
* `null` if the entity is not fronted by an element of this application - for example, an
|
|
1205
|
+
* unbound node inside a model's instantiated hierarchy, or an entity created through the
|
|
1206
|
+
* engine API.
|
|
1104
1207
|
*
|
|
1105
1208
|
* @param entity - The entity to look up.
|
|
1106
|
-
* @returns The element
|
|
1209
|
+
* @returns The element fronting the entity, or `null`.
|
|
1107
1210
|
*/
|
|
1108
1211
|
elementFromEntity(entity) {
|
|
1109
1212
|
return this._entityElements.get(entity) ?? null;
|
|
1110
1213
|
}
|
|
1111
1214
|
/**
|
|
1112
1215
|
* Resolves the element that owns a picked node: the nearest node up the parent chain -
|
|
1113
|
-
* starting with the node itself - that
|
|
1114
|
-
* A hit inside a model's instantiated hierarchy therefore resolves to the
|
|
1115
|
-
* the model.
|
|
1216
|
+
* starting with the node itself - that is fronted by a `<pc-entity>` or `<pc-node>` of this
|
|
1217
|
+
* application. A hit inside a model's instantiated hierarchy therefore resolves to the
|
|
1218
|
+
* nearest bound `<pc-node>`, or failing that the element hosting the model.
|
|
1116
1219
|
*
|
|
1117
1220
|
* @param node - The picked node, or `null`.
|
|
1118
1221
|
* @returns The owning element, or `null`.
|
|
@@ -1244,7 +1347,7 @@ class AppElement extends AsyncElement {
|
|
|
1244
1347
|
}
|
|
1245
1348
|
}
|
|
1246
1349
|
_onPointerListenerRemoved(type) {
|
|
1247
|
-
const hasListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
|
|
1350
|
+
const hasListeners = Array.from(this.querySelectorAll('pc-entity, pc-node')).some((entity) => entity._hasListeners(type));
|
|
1248
1351
|
if (!hasListeners && this._canvas) {
|
|
1249
1352
|
this._hasPointerListeners[type] = false;
|
|
1250
1353
|
const handler = type === 'pointerenter' || type === 'pointerleave'
|
|
@@ -1365,7 +1468,7 @@ class AppElement extends AsyncElement {
|
|
|
1365
1468
|
this._maxPixelRatio = value;
|
|
1366
1469
|
if (this.app) {
|
|
1367
1470
|
this.app.graphicsDevice.maxPixelRatio = value;
|
|
1368
|
-
this.
|
|
1471
|
+
this._syncCanvasSize();
|
|
1369
1472
|
}
|
|
1370
1473
|
}
|
|
1371
1474
|
/**
|
|
@@ -1422,6 +1525,125 @@ class AppElement extends AsyncElement {
|
|
|
1422
1525
|
}
|
|
1423
1526
|
customElements.define('pc-app', AppElement);
|
|
1424
1527
|
|
|
1528
|
+
/**
|
|
1529
|
+
* The attribute names of the inline `onpointer*` event handlers, shared by every element that
|
|
1530
|
+
* fronts an engine entity. Spread into `observedAttributes` by subclasses.
|
|
1531
|
+
* @ignore
|
|
1532
|
+
*/
|
|
1533
|
+
const POINTER_ATTRIBUTES = [
|
|
1534
|
+
'onpointerenter',
|
|
1535
|
+
'onpointerleave',
|
|
1536
|
+
'onpointerdown',
|
|
1537
|
+
'onpointerup',
|
|
1538
|
+
'onpointermove'
|
|
1539
|
+
];
|
|
1540
|
+
/**
|
|
1541
|
+
* The base class for elements that front an engine {@link Entity}: `<pc-entity>`, which creates
|
|
1542
|
+
* one, and `<pc-node>`, which binds to one inside a model's instantiated hierarchy. It carries
|
|
1543
|
+
* what both need — the `entity` contract, registration with the owning application (which joins
|
|
1544
|
+
* picked scene nodes back to elements by identity, never by name), and the pointer listener
|
|
1545
|
+
* bookkeeping that lets the application lazily attach its canvas handlers.
|
|
1546
|
+
*/
|
|
1547
|
+
class EntityBaseElement extends AsyncElement {
|
|
1548
|
+
_entity = null;
|
|
1549
|
+
/**
|
|
1550
|
+
* The application element this entity is registered with, cached at registration time so the
|
|
1551
|
+
* entity can be unregistered even once this element has left the DOM.
|
|
1552
|
+
*/
|
|
1553
|
+
_appElement = null;
|
|
1554
|
+
/**
|
|
1555
|
+
* The pointer event listeners for the entity.
|
|
1556
|
+
*/
|
|
1557
|
+
_listeners = {};
|
|
1558
|
+
/**
|
|
1559
|
+
* The event types for which an inline `onpointer*` attribute is currently present.
|
|
1560
|
+
*/
|
|
1561
|
+
_inlineHandlerTypes = new Set();
|
|
1562
|
+
/**
|
|
1563
|
+
* The PlayCanvas entity instance. `null` until the element is ready, and again once the
|
|
1564
|
+
* entity is gone — await {@link whenReady} or the element's `ready()` promise before
|
|
1565
|
+
* accessing it.
|
|
1566
|
+
* @returns The entity instance, or `null`.
|
|
1567
|
+
*/
|
|
1568
|
+
get entity() {
|
|
1569
|
+
return this._entity;
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Registers `entity` as this element's backing entity with the owning application, which
|
|
1573
|
+
* joins engine nodes back to elements by identity (never by name).
|
|
1574
|
+
*
|
|
1575
|
+
* @param entity - The entity to register.
|
|
1576
|
+
*/
|
|
1577
|
+
_registerEntity(entity) {
|
|
1578
|
+
this._appElement = this.closestApp;
|
|
1579
|
+
this._appElement?._registerEntityElement(entity, this);
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Removes the registration for `entity`.
|
|
1583
|
+
*
|
|
1584
|
+
* @param entity - The entity to unregister.
|
|
1585
|
+
*/
|
|
1586
|
+
_unregisterEntity(entity) {
|
|
1587
|
+
this._appElement?._unregisterEntityElement(entity);
|
|
1588
|
+
this._appElement = null;
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
|
|
1592
|
+
* runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
|
|
1593
|
+
* the previous handler and removing it removes the handler, exactly like `onclick` on any
|
|
1594
|
+
* HTML element. But because they bypass {@link addEventListener}, the connect/disconnect
|
|
1595
|
+
* bookkeeping that lets the application lazily attach its canvas pointer handlers must be
|
|
1596
|
+
* kept in sync here.
|
|
1597
|
+
*
|
|
1598
|
+
* @param name - The attribute name (e.g. 'onpointerdown').
|
|
1599
|
+
* @param value - The attribute value, or `null` when the attribute has been removed.
|
|
1600
|
+
*/
|
|
1601
|
+
_updateInlineHandler(name, value) {
|
|
1602
|
+
const type = name.substring(2);
|
|
1603
|
+
const had = this._inlineHandlerTypes.has(type);
|
|
1604
|
+
const has = value !== null;
|
|
1605
|
+
if (has && !had) {
|
|
1606
|
+
this._inlineHandlerTypes.add(type);
|
|
1607
|
+
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
1608
|
+
}
|
|
1609
|
+
else if (!has && had) {
|
|
1610
|
+
this._inlineHandlerTypes.delete(type);
|
|
1611
|
+
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
addEventListener(type, listener, options) {
|
|
1615
|
+
if (!this._listeners[type]) {
|
|
1616
|
+
this._listeners[type] = [];
|
|
1617
|
+
}
|
|
1618
|
+
this._listeners[type].push(listener);
|
|
1619
|
+
super.addEventListener(type, listener, options);
|
|
1620
|
+
if (type.startsWith('pointer')) {
|
|
1621
|
+
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
removeEventListener(type, listener, options) {
|
|
1625
|
+
if (this._listeners[type]) {
|
|
1626
|
+
this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
|
|
1627
|
+
}
|
|
1628
|
+
super.removeEventListener(type, listener, options);
|
|
1629
|
+
if (type.startsWith('pointer')) {
|
|
1630
|
+
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Whether the element has a listener for an event type, registered either with
|
|
1635
|
+
* {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
|
|
1636
|
+
* containing `<pc-app>` element to gate pointer event synthesis.
|
|
1637
|
+
*
|
|
1638
|
+
* @param type - The event type.
|
|
1639
|
+
* @returns Whether a listener is registered.
|
|
1640
|
+
* @internal
|
|
1641
|
+
*/
|
|
1642
|
+
_hasListeners(type) {
|
|
1643
|
+
return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1425
1647
|
/**
|
|
1426
1648
|
* The EntityElement interface provides properties and methods for manipulating
|
|
1427
1649
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | `<pc-entity>`} elements.
|
|
@@ -1446,7 +1668,7 @@ customElements.define('pc-app', AppElement);
|
|
|
1446
1668
|
* @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
|
|
1447
1669
|
* @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
|
|
1448
1670
|
*/
|
|
1449
|
-
class EntityElement extends
|
|
1671
|
+
class EntityElement extends EntityBaseElement {
|
|
1450
1672
|
/**
|
|
1451
1673
|
* Whether the entity is enabled.
|
|
1452
1674
|
*/
|
|
@@ -1471,33 +1693,10 @@ class EntityElement extends AsyncElement {
|
|
|
1471
1693
|
* The tags of the entity.
|
|
1472
1694
|
*/
|
|
1473
1695
|
_tags = [];
|
|
1474
|
-
/**
|
|
1475
|
-
* The pointer event listeners for the entity.
|
|
1476
|
-
*/
|
|
1477
|
-
_listeners = {};
|
|
1478
|
-
/**
|
|
1479
|
-
* The event types for which an inline `onpointer*` attribute is currently present.
|
|
1480
|
-
*/
|
|
1481
|
-
_inlineHandlerTypes = new Set();
|
|
1482
1696
|
/**
|
|
1483
1697
|
* Whether the hierarchy has been built for this entity.
|
|
1484
1698
|
*/
|
|
1485
1699
|
_built = false;
|
|
1486
|
-
_entity = null;
|
|
1487
|
-
/**
|
|
1488
|
-
* The application element this entity is registered with, cached at creation time so the
|
|
1489
|
-
* entity can be unregistered even once this element has left the DOM.
|
|
1490
|
-
*/
|
|
1491
|
-
_appElement = null;
|
|
1492
|
-
/**
|
|
1493
|
-
* The PlayCanvas entity instance. `null` until the element is ready, and again once it has
|
|
1494
|
-
* been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
1495
|
-
* promise before accessing it.
|
|
1496
|
-
* @returns The entity instance, or `null`.
|
|
1497
|
-
*/
|
|
1498
|
-
get entity() {
|
|
1499
|
-
return this._entity;
|
|
1500
|
-
}
|
|
1501
1700
|
/**
|
|
1502
1701
|
* Creates the backing entity. Called by the containing `<pc-app>` element during its boot
|
|
1503
1702
|
* sweep, and on connection for elements inserted while the application is already running.
|
|
@@ -1525,13 +1724,11 @@ class EntityElement extends AsyncElement {
|
|
|
1525
1724
|
if (this._tags.length > 0) {
|
|
1526
1725
|
entity.tags.add(this._tags);
|
|
1527
1726
|
}
|
|
1528
|
-
// Register with the owning application
|
|
1529
|
-
//
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1532
|
-
|
|
1533
|
-
this._appElement = this.closestApp;
|
|
1534
|
-
this._appElement?._registerEntityElement(entity, this);
|
|
1727
|
+
// Register with the owning application and hook the entity's destruction. The engine
|
|
1728
|
+
// fires 'destroy' for every entity in a destroyed subtree, so the element learns of its
|
|
1729
|
+
// entity's death no matter who causes it: this element, an ancestor, the whole
|
|
1730
|
+
// application, or a user script calling entity.destroy().
|
|
1731
|
+
this._registerEntity(entity);
|
|
1535
1732
|
entity.once('destroy', this._onEntityDestroy, this);
|
|
1536
1733
|
}
|
|
1537
1734
|
/**
|
|
@@ -1544,17 +1741,16 @@ class EntityElement extends AsyncElement {
|
|
|
1544
1741
|
* @param entity - The entity that was destroyed.
|
|
1545
1742
|
*/
|
|
1546
1743
|
_onEntityDestroy(entity) {
|
|
1547
|
-
this.
|
|
1548
|
-
this._appElement = null;
|
|
1744
|
+
this._unregisterEntity(entity);
|
|
1549
1745
|
this._entity = null;
|
|
1550
1746
|
this._built = false;
|
|
1551
1747
|
this._resetReady();
|
|
1552
1748
|
}
|
|
1553
1749
|
/**
|
|
1554
|
-
* Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>`
|
|
1555
|
-
* there is one, and under the application root otherwise. Called by the
|
|
1556
|
-
* element once a sweep has created every entity, so a parent's
|
|
1557
|
-
* document order.
|
|
1750
|
+
* Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` or
|
|
1751
|
+
* `<pc-node>` when there is one, and under the application root otherwise. Called by the
|
|
1752
|
+
* containing `<pc-app>` element once a sweep has created every entity, so a parent's
|
|
1753
|
+
* existence never depends on document order.
|
|
1558
1754
|
*
|
|
1559
1755
|
* @param app - The application whose root adopts parentless entities.
|
|
1560
1756
|
* @internal
|
|
@@ -1562,8 +1758,14 @@ class EntityElement extends AsyncElement {
|
|
|
1562
1758
|
_buildHierarchy(app) {
|
|
1563
1759
|
if (!this.entity || this._built)
|
|
1564
1760
|
return;
|
|
1565
|
-
this._built = true;
|
|
1566
1761
|
const closestEntity = this.closestEntity;
|
|
1762
|
+
// A host element without an entity is an unresolved `<pc-node>`: building now would
|
|
1763
|
+
// mis-anchor this entity to the application root while the host is still resolving.
|
|
1764
|
+
// Stay unbuilt - the host drives this subtree itself once it binds.
|
|
1765
|
+
if (closestEntity && !closestEntity.entity) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
this._built = true;
|
|
1567
1769
|
if (closestEntity?.entity) {
|
|
1568
1770
|
closestEntity.entity.addChild(this.entity);
|
|
1569
1771
|
}
|
|
@@ -1709,44 +1911,8 @@ class EntityElement extends AsyncElement {
|
|
|
1709
1911
|
get tags() {
|
|
1710
1912
|
return this._tags;
|
|
1711
1913
|
}
|
|
1712
|
-
/**
|
|
1713
|
-
* Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
|
|
1714
|
-
* runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
|
|
1715
|
-
* the previous handler and removing it removes the handler, exactly like `onclick` on any
|
|
1716
|
-
* HTML element. But because they bypass {@link addEventListener}, the connect/disconnect
|
|
1717
|
-
* bookkeeping that lets the application lazily attach its canvas pointer handlers must be
|
|
1718
|
-
* kept in sync here.
|
|
1719
|
-
*
|
|
1720
|
-
* @param name - The attribute name (e.g. 'onpointerdown').
|
|
1721
|
-
* @param value - The attribute value, or `null` when the attribute has been removed.
|
|
1722
|
-
*/
|
|
1723
|
-
_updateInlineHandler(name, value) {
|
|
1724
|
-
const type = name.substring(2);
|
|
1725
|
-
const had = this._inlineHandlerTypes.has(type);
|
|
1726
|
-
const has = value !== null;
|
|
1727
|
-
if (has && !had) {
|
|
1728
|
-
this._inlineHandlerTypes.add(type);
|
|
1729
|
-
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
1730
|
-
}
|
|
1731
|
-
else if (!has && had) {
|
|
1732
|
-
this._inlineHandlerTypes.delete(type);
|
|
1733
|
-
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
1914
|
static get observedAttributes() {
|
|
1737
|
-
return [
|
|
1738
|
-
'enabled',
|
|
1739
|
-
'name',
|
|
1740
|
-
'position',
|
|
1741
|
-
'rotation',
|
|
1742
|
-
'scale',
|
|
1743
|
-
'tags',
|
|
1744
|
-
'onpointerenter',
|
|
1745
|
-
'onpointerleave',
|
|
1746
|
-
'onpointerdown',
|
|
1747
|
-
'onpointerup',
|
|
1748
|
-
'onpointermove'
|
|
1749
|
-
];
|
|
1915
|
+
return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
|
|
1750
1916
|
}
|
|
1751
1917
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
1752
1918
|
switch (name) {
|
|
@@ -1777,37 +1943,6 @@ class EntityElement extends AsyncElement {
|
|
|
1777
1943
|
break;
|
|
1778
1944
|
}
|
|
1779
1945
|
}
|
|
1780
|
-
addEventListener(type, listener, options) {
|
|
1781
|
-
if (!this._listeners[type]) {
|
|
1782
|
-
this._listeners[type] = [];
|
|
1783
|
-
}
|
|
1784
|
-
this._listeners[type].push(listener);
|
|
1785
|
-
super.addEventListener(type, listener, options);
|
|
1786
|
-
if (type.startsWith('pointer')) {
|
|
1787
|
-
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
removeEventListener(type, listener, options) {
|
|
1791
|
-
if (this._listeners[type]) {
|
|
1792
|
-
this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
|
|
1793
|
-
}
|
|
1794
|
-
super.removeEventListener(type, listener, options);
|
|
1795
|
-
if (type.startsWith('pointer')) {
|
|
1796
|
-
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
/**
|
|
1800
|
-
* Whether the element has a listener for an event type, registered either with
|
|
1801
|
-
* {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
|
|
1802
|
-
* containing `<pc-app>` element to gate pointer event synthesis.
|
|
1803
|
-
*
|
|
1804
|
-
* @param type - The event type.
|
|
1805
|
-
* @returns Whether a listener is registered.
|
|
1806
|
-
* @internal
|
|
1807
|
-
*/
|
|
1808
|
-
_hasListeners(type) {
|
|
1809
|
-
return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
|
|
1810
|
-
}
|
|
1811
1946
|
}
|
|
1812
1947
|
customElements.define('pc-entity', EntityElement);
|
|
1813
1948
|
|
|
@@ -1992,6 +2127,48 @@ const renderModes = new Map([
|
|
|
1992
2127
|
['sliced', SPRITE_RENDERMODE_SLICED],
|
|
1993
2128
|
['tiled', SPRITE_RENDERMODE_TILED]
|
|
1994
2129
|
]);
|
|
2130
|
+
const addressModes = new Map([
|
|
2131
|
+
['repeat', ADDRESS_REPEAT],
|
|
2132
|
+
['clamp', ADDRESS_CLAMP_TO_EDGE],
|
|
2133
|
+
['mirror', ADDRESS_MIRRORED_REPEAT]
|
|
2134
|
+
]);
|
|
2135
|
+
const minFilterModes = new Map([
|
|
2136
|
+
['nearest', FILTER_NEAREST],
|
|
2137
|
+
['linear', FILTER_LINEAR],
|
|
2138
|
+
['nearest-mip-nearest', FILTER_NEAREST_MIPMAP_NEAREST],
|
|
2139
|
+
['linear-mip-nearest', FILTER_LINEAR_MIPMAP_NEAREST],
|
|
2140
|
+
['nearest-mip-linear', FILTER_NEAREST_MIPMAP_LINEAR],
|
|
2141
|
+
['linear-mip-linear', FILTER_LINEAR_MIPMAP_LINEAR]
|
|
2142
|
+
]);
|
|
2143
|
+
const magFilterModes = new Map([
|
|
2144
|
+
['nearest', FILTER_NEAREST],
|
|
2145
|
+
['linear', FILTER_LINEAR]
|
|
2146
|
+
]);
|
|
2147
|
+
// The engine's texture JSON spells the filter names with underscores ('linear_mip_linear'); the
|
|
2148
|
+
// attribute values are kebab-case like every other enum attribute in this library. The address
|
|
2149
|
+
// mode names contain no dashes, so for them the rename is the identity.
|
|
2150
|
+
const toTextureJson = (name) => name.replace(/-/g, '_');
|
|
2151
|
+
// Engine Texture constructor defaults, restored on a loaded texture when a texture option
|
|
2152
|
+
// attribute is removed.
|
|
2153
|
+
const textureOptionDefaults = {
|
|
2154
|
+
addressU: ADDRESS_REPEAT,
|
|
2155
|
+
addressV: ADDRESS_REPEAT,
|
|
2156
|
+
anisotropy: 1,
|
|
2157
|
+
flipY: false,
|
|
2158
|
+
magFilter: FILTER_LINEAR,
|
|
2159
|
+
minFilter: FILTER_LINEAR_MIPMAP_LINEAR,
|
|
2160
|
+
mipmaps: true,
|
|
2161
|
+
srgb: false
|
|
2162
|
+
};
|
|
2163
|
+
// Attributes that only apply to certain asset types, used to warn when one is set on an asset of
|
|
2164
|
+
// any other type (where it would otherwise be silently ignored).
|
|
2165
|
+
const typeScopedAttributes = [
|
|
2166
|
+
[
|
|
2167
|
+
['address-u', 'address-v', 'anisotropy', 'flip-y', 'mag-filter', 'min-filter', 'mipmaps', 'srgb'],
|
|
2168
|
+
['texture', 'textureatlas']
|
|
2169
|
+
],
|
|
2170
|
+
[['atlas', 'frame-keys', 'pixels-per-unit', 'render-mode'], ['sprite']]
|
|
2171
|
+
];
|
|
1995
2172
|
const extToType = new Map([
|
|
1996
2173
|
['bin', 'binary'],
|
|
1997
2174
|
['css', 'css'],
|
|
@@ -2048,8 +2225,16 @@ const processBufferView = (gltfBuffer, buffers, continuation) => {
|
|
|
2048
2225
|
* immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
|
|
2049
2226
|
* elsewhere, or with an unsupported asset type, never become ready.
|
|
2050
2227
|
*
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
2228
|
+
* For `texture` and `textureatlas` assets, the texture options (`address-u`, `address-v`,
|
|
2229
|
+
* `min-filter`, `mag-filter`, `anisotropy`, `mipmaps`, `srgb`, `flip-y`) apply when the texture is
|
|
2230
|
+
* created and — like `lazy` — are observed: changing one updates a texture that has already
|
|
2231
|
+
* loaded, and removing one restores the engine default. Changing `srgb` or `mipmaps` on a loaded
|
|
2232
|
+
* texture recreates the underlying GPU resource, so prefer declaring those up front. Each option
|
|
2233
|
+
* overrides the matching key in the `data` JSON; options left unset write nothing, leaving the
|
|
2234
|
+
* engine's per-format defaults in force.
|
|
2235
|
+
*
|
|
2236
|
+
* Apart from `lazy` and the texture options, these attributes are read once when the asset is
|
|
2237
|
+
* created, so changing them later has no effect.
|
|
2053
2238
|
*
|
|
2054
2239
|
* @attribute {string} id - The identifier used to reference the asset from other elements.
|
|
2055
2240
|
* @attribute {string} src - The URL of the asset to load.
|
|
@@ -2071,7 +2256,15 @@ const processBufferView = (gltfBuffer, buffers, continuation) => {
|
|
|
2071
2256
|
* not that it succeeded.
|
|
2072
2257
|
*/
|
|
2073
2258
|
class AssetElement extends AsyncElement {
|
|
2259
|
+
_addressU = null;
|
|
2260
|
+
_addressV = null;
|
|
2261
|
+
_anisotropy = null;
|
|
2262
|
+
_flipY = null;
|
|
2074
2263
|
_lazy = false;
|
|
2264
|
+
_magFilter = null;
|
|
2265
|
+
_minFilter = null;
|
|
2266
|
+
_mipmaps = null;
|
|
2267
|
+
_srgb = null;
|
|
2075
2268
|
/**
|
|
2076
2269
|
* The asset that is loaded. Available once the element is ready — await
|
|
2077
2270
|
* {@link whenReady} or the element's `ready()` promise before accessing it.
|
|
@@ -2142,6 +2335,15 @@ class AssetElement extends AsyncElement {
|
|
|
2142
2335
|
console.warn(`Unsupported asset type: ${src}`);
|
|
2143
2336
|
return;
|
|
2144
2337
|
}
|
|
2338
|
+
// Attributes scoped to other asset types have no effect here - say so rather than
|
|
2339
|
+
// failing silently.
|
|
2340
|
+
const inapplicable = typeScopedAttributes
|
|
2341
|
+
.filter(([, types]) => !types.includes(type))
|
|
2342
|
+
.flatMap(([attributes]) => attributes)
|
|
2343
|
+
.filter((attribute) => this.hasAttribute(attribute));
|
|
2344
|
+
if (inapplicable.length > 0) {
|
|
2345
|
+
console.warn(`pc-asset '${id || src}' has attributes that do not apply to asset type '${type}' and are ignored: ${inapplicable.join(', ')}`);
|
|
2346
|
+
}
|
|
2145
2347
|
// Optional inline asset data, used by data-driven assets such as texture atlases (frame
|
|
2146
2348
|
// definitions) and sprites (atlas reference, frame keys, etc.).
|
|
2147
2349
|
const data = this._buildData(type);
|
|
@@ -2169,9 +2371,11 @@ class AssetElement extends AsyncElement {
|
|
|
2169
2371
|
this.asset.on('error', this._onAssetError, this);
|
|
2170
2372
|
}
|
|
2171
2373
|
/**
|
|
2172
|
-
* Builds the `data` object for the asset from an optional inline `data` attribute (JSON)
|
|
2173
|
-
*
|
|
2174
|
-
* `render-mode`).
|
|
2374
|
+
* Builds the `data` object for the asset from an optional inline `data` attribute (JSON), the
|
|
2375
|
+
* texture option attributes (for `texture` and `textureatlas` assets), and the sprite
|
|
2376
|
+
* convenience attributes (`atlas`, `frame-keys`, `pixels-per-unit`, `render-mode`). An
|
|
2377
|
+
* attribute overrides the matching `data` JSON key. Returns `undefined` when there is no data
|
|
2378
|
+
* to apply.
|
|
2175
2379
|
* @param type - The resolved asset type.
|
|
2176
2380
|
* @returns The asset data, or `undefined`.
|
|
2177
2381
|
*/
|
|
@@ -2186,6 +2390,37 @@ class AssetElement extends AsyncElement {
|
|
|
2186
2390
|
console.warn(`Invalid 'data' JSON on pc-asset: ${dataAttr}`);
|
|
2187
2391
|
}
|
|
2188
2392
|
}
|
|
2393
|
+
if (type === 'texture' || type === 'textureatlas') {
|
|
2394
|
+
data = data ?? {};
|
|
2395
|
+
// Only options the user actually set are written: the engine reads these keys with
|
|
2396
|
+
// hasOwnProperty semantics, and an absent key leaves its per-format default (an HDR's
|
|
2397
|
+
// 'rgbe' type, a KTX2's transcoded format) in force.
|
|
2398
|
+
if (this._addressU !== null) {
|
|
2399
|
+
data.addressu = this._addressU;
|
|
2400
|
+
}
|
|
2401
|
+
if (this._addressV !== null) {
|
|
2402
|
+
data.addressv = this._addressV;
|
|
2403
|
+
}
|
|
2404
|
+
if (this._anisotropy !== null) {
|
|
2405
|
+
data.anisotropy = this._anisotropy;
|
|
2406
|
+
}
|
|
2407
|
+
if (this._flipY !== null) {
|
|
2408
|
+
// 'flipY' is the one camelCase key in the engine's texture JSON
|
|
2409
|
+
data.flipY = this._flipY;
|
|
2410
|
+
}
|
|
2411
|
+
if (this._magFilter !== null) {
|
|
2412
|
+
data.magfilter = toTextureJson(this._magFilter);
|
|
2413
|
+
}
|
|
2414
|
+
if (this._minFilter !== null) {
|
|
2415
|
+
data.minfilter = toTextureJson(this._minFilter);
|
|
2416
|
+
}
|
|
2417
|
+
if (this._mipmaps !== null) {
|
|
2418
|
+
data.mipmaps = this._mipmaps;
|
|
2419
|
+
}
|
|
2420
|
+
if (this._srgb !== null) {
|
|
2421
|
+
data.srgb = this._srgb;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2189
2424
|
if (type === 'sprite') {
|
|
2190
2425
|
data = data ?? {};
|
|
2191
2426
|
// Resolve the referenced texture atlas to its (numeric) asset id. The atlas must be
|
|
@@ -2219,6 +2454,56 @@ class AssetElement extends AsyncElement {
|
|
|
2219
2454
|
}
|
|
2220
2455
|
return data;
|
|
2221
2456
|
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Returns the engine texture behind this asset, when there is one: the resource itself for a
|
|
2459
|
+
* `texture` asset, the atlas's texture for a `textureatlas` asset, `null` otherwise
|
|
2460
|
+
* (including before the asset has loaded).
|
|
2461
|
+
* @returns The texture, or `null`.
|
|
2462
|
+
*/
|
|
2463
|
+
_texture() {
|
|
2464
|
+
const asset = this.asset;
|
|
2465
|
+
if (!asset?.resource)
|
|
2466
|
+
return null;
|
|
2467
|
+
if (asset.type === 'texture')
|
|
2468
|
+
return asset.resource;
|
|
2469
|
+
if (asset.type === 'textureatlas')
|
|
2470
|
+
return asset.resource.texture ?? null;
|
|
2471
|
+
return null;
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Writes one texture option through to the created asset, if any. The engine-JSON key is
|
|
2475
|
+
* written into `asset.data`, mutated in place - replacing the whole object would make the
|
|
2476
|
+
* registry re-patch every key, and a re-patched `srgb` or `mipmaps` recreates the texture
|
|
2477
|
+
* even when unchanged. The in-place key is what a not-yet-started load reads at texture
|
|
2478
|
+
* construction, and what any later reload reads. When the texture already exists, the
|
|
2479
|
+
* corresponding property is assigned directly; `null` (attribute removed) deletes the key
|
|
2480
|
+
* and restores the engine default. Assets of any other type are left untouched.
|
|
2481
|
+
*
|
|
2482
|
+
* @param key - The engine texture JSON key in `asset.data`.
|
|
2483
|
+
* @param property - The Texture property to assign.
|
|
2484
|
+
* @param dataValue - The engine-JSON value for `asset.data`, or `null` to delete the key.
|
|
2485
|
+
* @param textureValue - The value for the Texture property, or `null` for the engine default.
|
|
2486
|
+
*/
|
|
2487
|
+
_applyTextureOption(key, property, dataValue, textureValue) {
|
|
2488
|
+
const asset = this.asset;
|
|
2489
|
+
if (!asset || (asset.type !== 'texture' && asset.type !== 'textureatlas'))
|
|
2490
|
+
return;
|
|
2491
|
+
const data = asset.data;
|
|
2492
|
+
if (dataValue === null) {
|
|
2493
|
+
delete data[key];
|
|
2494
|
+
}
|
|
2495
|
+
else {
|
|
2496
|
+
data[key] = dataValue;
|
|
2497
|
+
}
|
|
2498
|
+
const texture = this._texture();
|
|
2499
|
+
if (texture) {
|
|
2500
|
+
// Every option here is a number- or boolean-valued Texture property; the
|
|
2501
|
+
// value/property pairing is fixed by the callers, which TypeScript cannot see
|
|
2502
|
+
// through the union.
|
|
2503
|
+
texture[property] =
|
|
2504
|
+
textureValue ?? textureOptionDefaults[property];
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2222
2507
|
_destroyAsset() {
|
|
2223
2508
|
if (this.asset) {
|
|
2224
2509
|
// A caller that keeps the Asset alive must not dispatch on a removed element
|
|
@@ -2231,22 +2516,164 @@ class AssetElement extends AsyncElement {
|
|
|
2231
2516
|
}
|
|
2232
2517
|
}
|
|
2233
2518
|
/**
|
|
2234
|
-
* Sets
|
|
2235
|
-
*
|
|
2519
|
+
* Sets the texture's horizontal (U) address mode: how texture coordinates outside the 0 to 1
|
|
2520
|
+
* range sample the texture. Applies to `texture` and `textureatlas` assets, both when the
|
|
2521
|
+
* texture is created and after it has loaded.
|
|
2522
|
+
* @param value - The address mode, or `null` to use the engine default of 'repeat'.
|
|
2236
2523
|
*/
|
|
2237
|
-
set
|
|
2238
|
-
this.
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
}
|
|
2524
|
+
set addressU(value) {
|
|
2525
|
+
this._addressU = value;
|
|
2526
|
+
const constant = value === null ? null : (addressModes.get(value) ?? ADDRESS_REPEAT);
|
|
2527
|
+
this._applyTextureOption('addressu', 'addressU', value, constant);
|
|
2242
2528
|
}
|
|
2243
2529
|
/**
|
|
2244
|
-
* Gets
|
|
2245
|
-
* @returns The
|
|
2530
|
+
* Gets the texture's horizontal (U) address mode.
|
|
2531
|
+
* @returns The address mode, or `null` when unset.
|
|
2246
2532
|
*/
|
|
2247
|
-
get
|
|
2533
|
+
get addressU() {
|
|
2534
|
+
return this._addressU;
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Sets the texture's vertical (V) address mode: how texture coordinates outside the 0 to 1
|
|
2538
|
+
* range sample the texture. Applies to `texture` and `textureatlas` assets, both when the
|
|
2539
|
+
* texture is created and after it has loaded.
|
|
2540
|
+
* @param value - The address mode, or `null` to use the engine default of 'repeat'.
|
|
2541
|
+
*/
|
|
2542
|
+
set addressV(value) {
|
|
2543
|
+
this._addressV = value;
|
|
2544
|
+
const constant = value === null ? null : (addressModes.get(value) ?? ADDRESS_REPEAT);
|
|
2545
|
+
this._applyTextureOption('addressv', 'addressV', value, constant);
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Gets the texture's vertical (V) address mode.
|
|
2549
|
+
* @returns The address mode, or `null` when unset.
|
|
2550
|
+
*/
|
|
2551
|
+
get addressV() {
|
|
2552
|
+
return this._addressV;
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Sets the texture's maximum anisotropic filtering level, which improves quality at oblique
|
|
2556
|
+
* viewing angles. Applies to `texture` and `textureatlas` assets, both when the texture is
|
|
2557
|
+
* created and after it has loaded.
|
|
2558
|
+
* @param value - The anisotropy level, or `null` to use the engine default of 1.
|
|
2559
|
+
*/
|
|
2560
|
+
set anisotropy(value) {
|
|
2561
|
+
this._anisotropy = value;
|
|
2562
|
+
this._applyTextureOption('anisotropy', 'anisotropy', value, value);
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Gets the texture's maximum anisotropic filtering level.
|
|
2566
|
+
* @returns The anisotropy level, or `null` when unset.
|
|
2567
|
+
*/
|
|
2568
|
+
get anisotropy() {
|
|
2569
|
+
return this._anisotropy;
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Sets whether the texture's image data is flipped vertically at upload. Applies to `texture`
|
|
2573
|
+
* and `textureatlas` assets, both when the texture is created and after it has loaded.
|
|
2574
|
+
* @param value - The flip flag, or `null` to use the engine default of `false`.
|
|
2575
|
+
*/
|
|
2576
|
+
set flipY(value) {
|
|
2577
|
+
this._flipY = value;
|
|
2578
|
+
this._applyTextureOption('flipY', 'flipY', value, value);
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Gets whether the texture's image data is flipped vertically at upload.
|
|
2582
|
+
* @returns The flip flag, or `null` when unset.
|
|
2583
|
+
*/
|
|
2584
|
+
get flipY() {
|
|
2585
|
+
return this._flipY;
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Sets whether the asset should be loaded lazily.
|
|
2589
|
+
* @param value - The lazy loading flag.
|
|
2590
|
+
*/
|
|
2591
|
+
set lazy(value) {
|
|
2592
|
+
this._lazy = value;
|
|
2593
|
+
if (this.asset) {
|
|
2594
|
+
this.asset.preload = !value;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Gets whether the asset should be loaded lazily.
|
|
2599
|
+
* @returns The lazy loading flag.
|
|
2600
|
+
*/
|
|
2601
|
+
get lazy() {
|
|
2248
2602
|
return this._lazy;
|
|
2249
2603
|
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Sets the texture's magnification filter, used when the texture is displayed larger than its
|
|
2606
|
+
* source size. Applies to `texture` and `textureatlas` assets, both when the texture is
|
|
2607
|
+
* created and after it has loaded.
|
|
2608
|
+
* @param value - The filter, or `null` to use the engine default of 'linear'.
|
|
2609
|
+
*/
|
|
2610
|
+
set magFilter(value) {
|
|
2611
|
+
this._magFilter = value;
|
|
2612
|
+
const json = value === null ? null : toTextureJson(value);
|
|
2613
|
+
const constant = value === null ? null : (magFilterModes.get(value) ?? FILTER_LINEAR);
|
|
2614
|
+
this._applyTextureOption('magfilter', 'magFilter', json, constant);
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Gets the texture's magnification filter.
|
|
2618
|
+
* @returns The filter, or `null` when unset.
|
|
2619
|
+
*/
|
|
2620
|
+
get magFilter() {
|
|
2621
|
+
return this._magFilter;
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Sets the texture's minification filter, used when the texture is displayed smaller than its
|
|
2625
|
+
* source size. The mip variants blend within (and, for the second `linear`, between) mipmap
|
|
2626
|
+
* levels. Applies to `texture` and `textureatlas` assets, both when the texture is created
|
|
2627
|
+
* and after it has loaded.
|
|
2628
|
+
* @param value - The filter, or `null` to use the engine default of 'linear-mip-linear'.
|
|
2629
|
+
*/
|
|
2630
|
+
set minFilter(value) {
|
|
2631
|
+
this._minFilter = value;
|
|
2632
|
+
const json = value === null ? null : toTextureJson(value);
|
|
2633
|
+
const constant = value === null ? null : (minFilterModes.get(value) ?? FILTER_LINEAR_MIPMAP_LINEAR);
|
|
2634
|
+
this._applyTextureOption('minfilter', 'minFilter', json, constant);
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* Gets the texture's minification filter.
|
|
2638
|
+
* @returns The filter, or `null` when unset.
|
|
2639
|
+
*/
|
|
2640
|
+
get minFilter() {
|
|
2641
|
+
return this._minFilter;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Sets whether the texture generates and uses mipmaps. Changing this on a loaded texture
|
|
2645
|
+
* recreates the underlying GPU resource, so prefer declaring it up front. Applies to
|
|
2646
|
+
* `texture` and `textureatlas` assets.
|
|
2647
|
+
* @param value - The mipmaps flag, or `null` to use the engine default of `true`.
|
|
2648
|
+
*/
|
|
2649
|
+
set mipmaps(value) {
|
|
2650
|
+
this._mipmaps = value;
|
|
2651
|
+
this._applyTextureOption('mipmaps', 'mipmaps', value, value);
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Gets whether the texture generates and uses mipmaps.
|
|
2655
|
+
* @returns The mipmaps flag, or `null` when unset.
|
|
2656
|
+
*/
|
|
2657
|
+
get mipmaps() {
|
|
2658
|
+
return this._mipmaps;
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Sets whether the texture holds sRGB (gamma-encoded) color data, enabling hardware gamma
|
|
2662
|
+
* decode. Free when set before the texture loads; changing it on a loaded texture recreates
|
|
2663
|
+
* the underlying GPU resource. Applies to `texture` and `textureatlas` assets.
|
|
2664
|
+
* @param value - The sRGB flag, or `null` to use the engine default of `false`.
|
|
2665
|
+
*/
|
|
2666
|
+
set srgb(value) {
|
|
2667
|
+
this._srgb = value;
|
|
2668
|
+
this._applyTextureOption('srgb', 'srgb', value, value);
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Gets whether the texture holds sRGB (gamma-encoded) color data.
|
|
2672
|
+
* @returns The sRGB flag, or `null` when unset.
|
|
2673
|
+
*/
|
|
2674
|
+
get srgb() {
|
|
2675
|
+
return this._srgb;
|
|
2676
|
+
}
|
|
2250
2677
|
/**
|
|
2251
2678
|
* Returns the {@link Asset} created by the `<pc-asset>` element with the given `id`, or
|
|
2252
2679
|
* `undefined` if there is no such element or its asset has not been created yet.
|
|
@@ -2259,11 +2686,91 @@ class AssetElement extends AsyncElement {
|
|
|
2259
2686
|
return assetElement?.asset;
|
|
2260
2687
|
}
|
|
2261
2688
|
static get observedAttributes() {
|
|
2262
|
-
return [
|
|
2689
|
+
return [
|
|
2690
|
+
'address-u',
|
|
2691
|
+
'address-v',
|
|
2692
|
+
'anisotropy',
|
|
2693
|
+
'flip-y',
|
|
2694
|
+
'lazy',
|
|
2695
|
+
'mag-filter',
|
|
2696
|
+
'min-filter',
|
|
2697
|
+
'mipmaps',
|
|
2698
|
+
'srgb'
|
|
2699
|
+
];
|
|
2263
2700
|
}
|
|
2264
2701
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
2265
|
-
|
|
2266
|
-
|
|
2702
|
+
// Each texture option keeps its parse* call as the branch's first assignment (the CEM
|
|
2703
|
+
// manifest derives the attribute's type and default from it - a ternary would degrade
|
|
2704
|
+
// both to plain string) and treats a removed attribute (null) as a reset to unset,
|
|
2705
|
+
// which restores the engine default on a loaded texture.
|
|
2706
|
+
switch (name) {
|
|
2707
|
+
case 'address-u':
|
|
2708
|
+
if (newValue !== null) {
|
|
2709
|
+
this.addressU = parseEnum(newValue, addressModes, 'repeat', name);
|
|
2710
|
+
}
|
|
2711
|
+
else {
|
|
2712
|
+
this.addressU = null;
|
|
2713
|
+
}
|
|
2714
|
+
break;
|
|
2715
|
+
case 'address-v':
|
|
2716
|
+
if (newValue !== null) {
|
|
2717
|
+
this.addressV = parseEnum(newValue, addressModes, 'repeat', name);
|
|
2718
|
+
}
|
|
2719
|
+
else {
|
|
2720
|
+
this.addressV = null;
|
|
2721
|
+
}
|
|
2722
|
+
break;
|
|
2723
|
+
case 'anisotropy':
|
|
2724
|
+
if (newValue !== null) {
|
|
2725
|
+
this.anisotropy = parseNumber(newValue, 1, name);
|
|
2726
|
+
}
|
|
2727
|
+
else {
|
|
2728
|
+
this.anisotropy = null;
|
|
2729
|
+
}
|
|
2730
|
+
break;
|
|
2731
|
+
case 'flip-y':
|
|
2732
|
+
if (newValue !== null) {
|
|
2733
|
+
this.flipY = parseBool(newValue, false);
|
|
2734
|
+
}
|
|
2735
|
+
else {
|
|
2736
|
+
this.flipY = null;
|
|
2737
|
+
}
|
|
2738
|
+
break;
|
|
2739
|
+
case 'lazy':
|
|
2740
|
+
this.lazy = parseBool(newValue, false);
|
|
2741
|
+
break;
|
|
2742
|
+
case 'mag-filter':
|
|
2743
|
+
if (newValue !== null) {
|
|
2744
|
+
this.magFilter = parseEnum(newValue, magFilterModes, 'linear', name);
|
|
2745
|
+
}
|
|
2746
|
+
else {
|
|
2747
|
+
this.magFilter = null;
|
|
2748
|
+
}
|
|
2749
|
+
break;
|
|
2750
|
+
case 'min-filter':
|
|
2751
|
+
if (newValue !== null) {
|
|
2752
|
+
this.minFilter = parseEnum(newValue, minFilterModes, 'linear-mip-linear', name);
|
|
2753
|
+
}
|
|
2754
|
+
else {
|
|
2755
|
+
this.minFilter = null;
|
|
2756
|
+
}
|
|
2757
|
+
break;
|
|
2758
|
+
case 'mipmaps':
|
|
2759
|
+
if (newValue !== null) {
|
|
2760
|
+
this.mipmaps = parseBool(newValue, true);
|
|
2761
|
+
}
|
|
2762
|
+
else {
|
|
2763
|
+
this.mipmaps = null;
|
|
2764
|
+
}
|
|
2765
|
+
break;
|
|
2766
|
+
case 'srgb':
|
|
2767
|
+
if (newValue !== null) {
|
|
2768
|
+
this.srgb = parseBool(newValue, false);
|
|
2769
|
+
}
|
|
2770
|
+
else {
|
|
2771
|
+
this.srgb = null;
|
|
2772
|
+
}
|
|
2773
|
+
break;
|
|
2267
2774
|
}
|
|
2268
2775
|
}
|
|
2269
2776
|
}
|
|
@@ -2279,6 +2786,16 @@ class ComponentElement extends AsyncElement {
|
|
|
2279
2786
|
_enabled = true;
|
|
2280
2787
|
_component = null;
|
|
2281
2788
|
_appElement = null;
|
|
2789
|
+
/**
|
|
2790
|
+
* The element hosting this component, held so the host's readiness cycles can be observed
|
|
2791
|
+
* even after `closestEntity` would no longer resolve (during teardown).
|
|
2792
|
+
*/
|
|
2793
|
+
_hostElement = null;
|
|
2794
|
+
/**
|
|
2795
|
+
* The listener re-applying this component when the host's readiness cycles. Held for
|
|
2796
|
+
* removal on disconnect.
|
|
2797
|
+
*/
|
|
2798
|
+
_hostReadyListener = null;
|
|
2282
2799
|
/**
|
|
2283
2800
|
* Incremented on every connect and disconnect. connectedCallback captures the value on entry
|
|
2284
2801
|
* and abandons itself wherever it resumes from an await if the value has moved on — so a
|
|
@@ -2306,6 +2823,37 @@ class ComponentElement extends AsyncElement {
|
|
|
2306
2823
|
getInitialComponentData() {
|
|
2307
2824
|
return {};
|
|
2308
2825
|
}
|
|
2826
|
+
/**
|
|
2827
|
+
* Creates the component on the host's current entity, removing it first from a previous
|
|
2828
|
+
* entity that is still alive (a retargeted `<pc-node>` moves its decorations with it). When
|
|
2829
|
+
* the entity already has a component of this type — a glTF node arriving with its authored
|
|
2830
|
+
* `render` component, say — warns and leaves `component` null. The element-level warning is
|
|
2831
|
+
* load-bearing: the engine's own duplicate-addComponent warning is Debug-stripped from
|
|
2832
|
+
* production builds, which would otherwise leave a silent null.
|
|
2833
|
+
*/
|
|
2834
|
+
_applyComponent() {
|
|
2835
|
+
const entity = this._hostElement?.entity ?? null;
|
|
2836
|
+
if (this._component && this._component.entity === entity) {
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
// A retarget leaves the previous component on a still-live entity - remove it so the
|
|
2840
|
+
// decoration follows the element, or vanishes with a dissolved binding. A destroyed
|
|
2841
|
+
// entity took its components with it.
|
|
2842
|
+
const previous = this._component;
|
|
2843
|
+
if (previous?.entity && previous.entity.c[this._componentName] === previous) {
|
|
2844
|
+
previous.entity.removeComponent(this._componentName);
|
|
2845
|
+
}
|
|
2846
|
+
this._component = null;
|
|
2847
|
+
if (!entity) {
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
if (entity.c[this._componentName]) {
|
|
2851
|
+
const label = this.id ? ` '${this.id}'` : '';
|
|
2852
|
+
console.warn(`${this.tagName.toLowerCase()}${label} - '${entity.name}' already has a '${this._componentName}' component - component not added`);
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
this._component = entity.addComponent(this._componentName, this.getInitialComponentData());
|
|
2856
|
+
}
|
|
2309
2857
|
async _addComponent() {
|
|
2310
2858
|
const generation = this._connectionGeneration;
|
|
2311
2859
|
const entityElement = this.closestEntity;
|
|
@@ -2322,9 +2870,40 @@ class ComponentElement extends AsyncElement {
|
|
|
2322
2870
|
if (generation !== this._connectionGeneration) {
|
|
2323
2871
|
return;
|
|
2324
2872
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2873
|
+
this._hostElement = entityElement;
|
|
2874
|
+
this._applyComponent();
|
|
2875
|
+
// Re-apply when the host's readiness cycles without this element disconnecting: a
|
|
2876
|
+
// `<pc-node>` rebinding after its model reloads or retargets, or a re-created entity.
|
|
2877
|
+
// The 'ready' event bubbles, so events from descendants pass through this host - only
|
|
2878
|
+
// the host's own cycles count. Readiness is cycled here too, so decorations one level
|
|
2879
|
+
// down re-apply the same way.
|
|
2880
|
+
this._hostReadyListener = (event) => {
|
|
2881
|
+
if (event.target !== this._hostElement) {
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
if (generation !== this._connectionGeneration) {
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
this._hostCycled();
|
|
2888
|
+
};
|
|
2889
|
+
entityElement.addEventListener('ready', this._hostReadyListener);
|
|
2890
|
+
}
|
|
2891
|
+
/**
|
|
2892
|
+
* Re-evaluates this component against the host's current entity: applied to a new entity,
|
|
2893
|
+
* moved from a still-live old one, or removed when the host no longer fronts an entity at
|
|
2894
|
+
* all. Readiness follows - it cycles with a re-application and stays unresolved while the
|
|
2895
|
+
* host is unbound. Called by the host-ready listener, and directly by a `<pc-node>`
|
|
2896
|
+
* dissolving its binding: the one transition that fires no ready event to ride.
|
|
2897
|
+
*
|
|
2898
|
+
* @internal
|
|
2899
|
+
*/
|
|
2900
|
+
_hostCycled() {
|
|
2901
|
+
this._resetReady();
|
|
2902
|
+
this._applyComponent();
|
|
2903
|
+
if (this._hostElement?.entity) {
|
|
2904
|
+
this.initComponent();
|
|
2905
|
+
this._onReady();
|
|
2906
|
+
}
|
|
2328
2907
|
}
|
|
2329
2908
|
/**
|
|
2330
2909
|
* Configures the newly added component. Overridden by subclasses whose setup goes beyond
|
|
@@ -2353,6 +2932,11 @@ class ComponentElement extends AsyncElement {
|
|
|
2353
2932
|
disconnectedCallback() {
|
|
2354
2933
|
// Invalidate any connectedCallback still suspended on an await
|
|
2355
2934
|
this._connectionGeneration++;
|
|
2935
|
+
if (this._hostElement && this._hostReadyListener) {
|
|
2936
|
+
this._hostElement.removeEventListener('ready', this._hostReadyListener);
|
|
2937
|
+
}
|
|
2938
|
+
this._hostElement = null;
|
|
2939
|
+
this._hostReadyListener = null;
|
|
2356
2940
|
// Remove the component when the element is disconnected. Skip this when the owning
|
|
2357
2941
|
// application has already been destroyed — removing a <pc-app> disconnects it before
|
|
2358
2942
|
// its children, taking the component systems with it.
|
|
@@ -3312,6 +3896,12 @@ customElements.define('pc-camera', CameraComponentElement);
|
|
|
3312
3896
|
* The CollisionComponentElement interface also inherits the properties and methods of the
|
|
3313
3897
|
* {@link HTMLElement} interface.
|
|
3314
3898
|
*
|
|
3899
|
+
* For `type="mesh"`, the collision geometry defaults to the host entity's own render component
|
|
3900
|
+
* (its render asset) — a collider matching the visible mesh, which is what a mesh collider on a
|
|
3901
|
+
* glTF node means. The default resolves each time the component applies, so a `pc-node` that
|
|
3902
|
+
* retargets or rebinds picks up the new node's geometry. An entity with no asset-backed render
|
|
3903
|
+
* component warns, and the collider has no shape.
|
|
3904
|
+
*
|
|
3315
3905
|
* @category Components
|
|
3316
3906
|
*/
|
|
3317
3907
|
class CollisionComponentElement extends ComponentElement {
|
|
@@ -3339,6 +3929,29 @@ class CollisionComponentElement extends ComponentElement {
|
|
|
3339
3929
|
type: this._type
|
|
3340
3930
|
};
|
|
3341
3931
|
}
|
|
3932
|
+
initComponent() {
|
|
3933
|
+
this._applyMeshGeometryDefault();
|
|
3934
|
+
}
|
|
3935
|
+
/**
|
|
3936
|
+
* Defaults a mesh collider's geometry to the host entity's own render component. The
|
|
3937
|
+
* engine's mesh collider only works with explicitly supplied geometry, and the element has
|
|
3938
|
+
* no attribute to supply it - so the host's visible geometry, the meaning a mesh collider
|
|
3939
|
+
* on a glTF node carries, fills the gap. Runs on every application (so a rebound `pc-node`
|
|
3940
|
+
* recomputes it) and on a runtime switch to `type="mesh"`; an explicitly assigned
|
|
3941
|
+
* `renderAsset` is never overwritten.
|
|
3942
|
+
*/
|
|
3943
|
+
_applyMeshGeometryDefault() {
|
|
3944
|
+
const component = this.component;
|
|
3945
|
+
if (!component || this._type !== 'mesh' || component.renderAsset !== null) {
|
|
3946
|
+
return;
|
|
3947
|
+
}
|
|
3948
|
+
const asset = component.entity.render?.asset ?? null;
|
|
3949
|
+
if (asset === null) {
|
|
3950
|
+
console.warn(`pc-collision type="mesh" on '${component.entity.name}' found no asset-backed render component to take geometry from - collider has no shape`);
|
|
3951
|
+
return;
|
|
3952
|
+
}
|
|
3953
|
+
component.renderAsset = asset;
|
|
3954
|
+
}
|
|
3342
3955
|
/**
|
|
3343
3956
|
* Gets the underlying PlayCanvas collision component.
|
|
3344
3957
|
* @returns The collision component.
|
|
@@ -3413,6 +4026,7 @@ class CollisionComponentElement extends ComponentElement {
|
|
|
3413
4026
|
this._type = value;
|
|
3414
4027
|
if (this.component) {
|
|
3415
4028
|
this.component.type = value;
|
|
4029
|
+
this._applyMeshGeometryDefault();
|
|
3416
4030
|
}
|
|
3417
4031
|
}
|
|
3418
4032
|
get type() {
|
|
@@ -5583,13 +6197,13 @@ class MaterialElement extends HTMLElement {
|
|
|
5583
6197
|
}
|
|
5584
6198
|
/**
|
|
5585
6199
|
* @param slot - The material property to write.
|
|
5586
|
-
* @param texture - The loaded texture
|
|
6200
|
+
* @param texture - The loaded texture, applied with its sampler state untouched - anisotropy
|
|
6201
|
+
* and friends belong to the `pc-asset`'s texture options.
|
|
5587
6202
|
*/
|
|
5588
6203
|
_applyMap(slot, texture) {
|
|
5589
6204
|
if (!this.material)
|
|
5590
6205
|
return;
|
|
5591
6206
|
this.material[slot] = texture;
|
|
5592
|
-
texture.anisotropy = 4;
|
|
5593
6207
|
this._scheduleUpdate();
|
|
5594
6208
|
}
|
|
5595
6209
|
/**
|
|
@@ -9869,6 +10483,21 @@ customElements.define('pc-gsplat', GSplatComponentElement);
|
|
|
9869
10483
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | `<pc-model>`} elements.
|
|
9870
10484
|
* The ModelElement interface also inherits the properties and methods of the
|
|
9871
10485
|
* {@link HTMLElement} interface.
|
|
10486
|
+
*
|
|
10487
|
+
* The element becomes ready once its container asset has loaded and the instantiated hierarchy has
|
|
10488
|
+
* been added to the scene — `entity` is non-null by then. A failed load also settles readiness,
|
|
10489
|
+
* with `entity` remaining `null`: readiness means the load settled, not that it succeeded — listen
|
|
10490
|
+
* for `error`, or check `entity`, to tell the outcomes apart. Changing `asset` re-arms readiness
|
|
10491
|
+
* and instantiates anew, so a `ready()` obtained after the change resolves against the new
|
|
10492
|
+
* hierarchy. A `pc-model` outside a `pc-app`, or referencing an unknown asset id, warns and never
|
|
10493
|
+
* becomes ready.
|
|
10494
|
+
*
|
|
10495
|
+
* @fires {Event} load - Fired each time a container asset finishes instantiating, including
|
|
10496
|
+
* re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
|
|
10497
|
+
* capture-phase listener on an ancestor.
|
|
10498
|
+
* @fires {ErrorEvent} error - Fired when the container asset fails to load, with the engine's
|
|
10499
|
+
* error in `message`. Does not bubble. The element still becomes ready — readiness means the load
|
|
10500
|
+
* settled, not that it succeeded.
|
|
9872
10501
|
*/
|
|
9873
10502
|
class ModelElement extends AsyncElement {
|
|
9874
10503
|
_asset = '';
|
|
@@ -9881,11 +10510,12 @@ class ModelElement extends AsyncElement {
|
|
|
9881
10510
|
*/
|
|
9882
10511
|
_loadGeneration = 0;
|
|
9883
10512
|
/**
|
|
9884
|
-
* The pending asset
|
|
9885
|
-
*
|
|
9886
|
-
*
|
|
10513
|
+
* The pending asset subscriptions of the current load, if it is waiting for its asset. Held
|
|
10514
|
+
* so that whatever supersedes the load can detach the handlers from the asset, rather than
|
|
10515
|
+
* leave them registered until the asset settles (or forever, if it never does).
|
|
9887
10516
|
*/
|
|
9888
10517
|
_loadHandle = null;
|
|
10518
|
+
_errorHandle = null;
|
|
9889
10519
|
/**
|
|
9890
10520
|
* The root entity of the instantiated model. `null` until the container asset has loaded
|
|
9891
10521
|
* and been instantiated, and again once the element has been removed from the document.
|
|
@@ -9895,18 +10525,36 @@ class ModelElement extends AsyncElement {
|
|
|
9895
10525
|
return this._entity;
|
|
9896
10526
|
}
|
|
9897
10527
|
connectedCallback() {
|
|
10528
|
+
// A model outside an application is inert and never becomes ready, so awaiting it hangs.
|
|
10529
|
+
// Warn rather than fail silently, naming the parent it requires, as every other misplaced
|
|
10530
|
+
// element does.
|
|
10531
|
+
if (!this.closestApp) {
|
|
10532
|
+
const label = this._asset ? ` '${this._asset}'` : '';
|
|
10533
|
+
console.warn(`pc-model${label} must be a descendant of pc-app - model not created`);
|
|
10534
|
+
return;
|
|
10535
|
+
}
|
|
9898
10536
|
this._loadModel();
|
|
9899
|
-
this._onReady();
|
|
9900
10537
|
}
|
|
9901
10538
|
disconnectedCallback() {
|
|
9902
10539
|
this._loadGeneration++;
|
|
9903
|
-
this.
|
|
10540
|
+
this._detachLoadHandlers();
|
|
9904
10541
|
this._unloadModel();
|
|
9905
10542
|
this._resetReady();
|
|
9906
10543
|
}
|
|
9907
|
-
|
|
10544
|
+
_detachLoadHandlers() {
|
|
9908
10545
|
this._loadHandle?.off();
|
|
9909
10546
|
this._loadHandle = null;
|
|
10547
|
+
this._errorHandle?.off();
|
|
10548
|
+
this._errorHandle = null;
|
|
10549
|
+
}
|
|
10550
|
+
/**
|
|
10551
|
+
* Resolves readiness and dispatches the `load` event. Called once the instantiated hierarchy
|
|
10552
|
+
* has been parented — readiness means "in the scene graph", matching `pc-entity`, so a ready
|
|
10553
|
+
* model's entity always has world transforms.
|
|
10554
|
+
*/
|
|
10555
|
+
_announceLoad() {
|
|
10556
|
+
this._onReady();
|
|
10557
|
+
this.dispatchEvent(new Event('load'));
|
|
9910
10558
|
}
|
|
9911
10559
|
_instantiate(container) {
|
|
9912
10560
|
const generation = this._loadGeneration;
|
|
@@ -9928,6 +10576,7 @@ class ModelElement extends AsyncElement {
|
|
|
9928
10576
|
return;
|
|
9929
10577
|
}
|
|
9930
10578
|
parentEntityElement.entity.addChild(entity);
|
|
10579
|
+
this._announceLoad();
|
|
9931
10580
|
});
|
|
9932
10581
|
}
|
|
9933
10582
|
else {
|
|
@@ -9938,6 +10587,7 @@ class ModelElement extends AsyncElement {
|
|
|
9938
10587
|
return;
|
|
9939
10588
|
}
|
|
9940
10589
|
appElement.app.root.addChild(entity);
|
|
10590
|
+
this._announceLoad();
|
|
9941
10591
|
});
|
|
9942
10592
|
}
|
|
9943
10593
|
}
|
|
@@ -9946,15 +10596,29 @@ class ModelElement extends AsyncElement {
|
|
|
9946
10596
|
this._unloadModel();
|
|
9947
10597
|
// Supersede any load already in flight - only the newest load may instantiate
|
|
9948
10598
|
const generation = ++this._loadGeneration;
|
|
9949
|
-
this.
|
|
9950
|
-
|
|
10599
|
+
this._detachLoadHandlers();
|
|
10600
|
+
// Re-arm readiness so a waiter obtained after an asset change resolves against the new
|
|
10601
|
+
// hierarchy. A no-op on first connection, where readiness is still pending.
|
|
10602
|
+
this._resetReady();
|
|
10603
|
+
const appElement = this.closestApp;
|
|
10604
|
+
if (!appElement) {
|
|
10605
|
+
// Outside pc-app; connectedCallback already warned. Reached through the asset setter.
|
|
10606
|
+
return;
|
|
10607
|
+
}
|
|
10608
|
+
await appElement.ready();
|
|
9951
10609
|
// The element may have been removed, or another load started, while we waited
|
|
9952
10610
|
if (generation !== this._loadGeneration) {
|
|
9953
10611
|
return;
|
|
9954
10612
|
}
|
|
9955
|
-
const app = appElement
|
|
10613
|
+
const app = appElement.app;
|
|
9956
10614
|
const asset = AssetElement.get(this._asset);
|
|
9957
10615
|
if (!asset) {
|
|
10616
|
+
// An empty id is a legitimate transient (the asset may be assigned later); a
|
|
10617
|
+
// non-empty one that resolves to nothing is a dead end - say so rather than staying
|
|
10618
|
+
// silently pending.
|
|
10619
|
+
if (this._asset) {
|
|
10620
|
+
console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
|
|
10621
|
+
}
|
|
9958
10622
|
return;
|
|
9959
10623
|
}
|
|
9960
10624
|
if (asset.loaded) {
|
|
@@ -9963,14 +10627,26 @@ class ModelElement extends AsyncElement {
|
|
|
9963
10627
|
else {
|
|
9964
10628
|
// The generation is re-checked even though a superseded handler is detached: the
|
|
9965
10629
|
// detach relies on how the engine's event emitter treats removal, while the check
|
|
9966
|
-
// holds on its own.
|
|
10630
|
+
// holds on its own. Whichever of load/error fires first detaches the other.
|
|
9967
10631
|
this._loadHandle = asset.once('load', () => {
|
|
9968
|
-
this.
|
|
10632
|
+
this._detachLoadHandlers();
|
|
9969
10633
|
if (generation !== this._loadGeneration) {
|
|
9970
10634
|
return;
|
|
9971
10635
|
}
|
|
9972
10636
|
this._instantiate(asset.resource);
|
|
9973
10637
|
});
|
|
10638
|
+
this._errorHandle = asset.once('error', (err) => {
|
|
10639
|
+
this._detachLoadHandlers();
|
|
10640
|
+
if (generation !== this._loadGeneration) {
|
|
10641
|
+
return;
|
|
10642
|
+
}
|
|
10643
|
+
// A failed load settles readiness with a null entity, mirroring pc-asset:
|
|
10644
|
+
// readiness means the load settled, not that it succeeded.
|
|
10645
|
+
this.dispatchEvent(new ErrorEvent('error', {
|
|
10646
|
+
message: err instanceof Error ? err.message : String(err)
|
|
10647
|
+
}));
|
|
10648
|
+
this._onReady();
|
|
10649
|
+
});
|
|
9974
10650
|
app.assets.load(asset);
|
|
9975
10651
|
}
|
|
9976
10652
|
}
|
|
@@ -10008,6 +10684,625 @@ class ModelElement extends AsyncElement {
|
|
|
10008
10684
|
}
|
|
10009
10685
|
customElements.define('pc-model', ModelElement);
|
|
10010
10686
|
|
|
10687
|
+
/**
|
|
10688
|
+
* Computes the Levenshtein distance between two strings, for near-miss suggestions in the
|
|
10689
|
+
* resolution warnings.
|
|
10690
|
+
*
|
|
10691
|
+
* @param a - The first string.
|
|
10692
|
+
* @param b - The second string.
|
|
10693
|
+
* @returns The edit distance.
|
|
10694
|
+
*/
|
|
10695
|
+
const levenshtein = (a, b) => {
|
|
10696
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
10697
|
+
for (let i = 1; i <= a.length; i++) {
|
|
10698
|
+
let previous = row[0];
|
|
10699
|
+
row[0] = i;
|
|
10700
|
+
for (let j = 1; j <= b.length; j++) {
|
|
10701
|
+
const current = row[j];
|
|
10702
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
10703
|
+
previous = current;
|
|
10704
|
+
}
|
|
10705
|
+
}
|
|
10706
|
+
return row[b.length];
|
|
10707
|
+
};
|
|
10708
|
+
/**
|
|
10709
|
+
* The NodeElement interface provides properties and methods for manipulating
|
|
10710
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-node/ | `<pc-node>`}
|
|
10711
|
+
* elements. The NodeElement interface also inherits the properties and methods of the
|
|
10712
|
+
* {@link HTMLElement} interface.
|
|
10713
|
+
*
|
|
10714
|
+
* A `pc-node` is an override element: where `pc-entity` creates an entity, `pc-node` binds to a
|
|
10715
|
+
* node a `pc-model` loaded and declares overrides against the authored asset — components to
|
|
10716
|
+
* add, properties to change, content to attach. Attributes present apply as overrides; attributes
|
|
10717
|
+
* absent leave authored values untouched, and removing an attribute (or assigning `null` to the
|
|
10718
|
+
* matching property) restores the authored value.
|
|
10719
|
+
*
|
|
10720
|
+
* `name` selects among the host model's nodes (first match in depth-first order), nesting a
|
|
10721
|
+
* `pc-node` inside another scopes the search to that subtree, and `index` picks among identically
|
|
10722
|
+
* named matches. When `name` matches more than one node and no `index` is given, the element
|
|
10723
|
+
* warns and binds nothing.
|
|
10724
|
+
*
|
|
10725
|
+
* The element becomes ready once bound, and never while unresolved — a missing or ambiguous
|
|
10726
|
+
* name warns and records the failure in `state`, readiness stays unresolved, and descendants
|
|
10727
|
+
* wait with it.
|
|
10728
|
+
*
|
|
10729
|
+
* The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
|
|
10730
|
+
* intersects the bound node's geometry, exactly as for `<pc-entity>`.
|
|
10731
|
+
*
|
|
10732
|
+
* @attribute {string} name - The name of the node to bind, resolved within the nearest ancestor
|
|
10733
|
+
* `pc-model` (or `pc-node`) once it has instantiated.
|
|
10734
|
+
* @attribute {number} index - Which match to bind when `name` matches more than one node,
|
|
10735
|
+
* 0-based in depth-first order. Optional for a unique match; required for an ambiguous one.
|
|
10736
|
+
* @attribute {boolean} enabled - Overrides the node's enabled state.
|
|
10737
|
+
* @attribute {string} position - Overrides the node's local position, as an "x y z" triple.
|
|
10738
|
+
* @attribute {string} rotation - Overrides the node's local rotation (Euler angles), as an
|
|
10739
|
+
* "x y z" triple.
|
|
10740
|
+
* @attribute {string} scale - Overrides the node's local scale, as an "x y z" triple.
|
|
10741
|
+
* @attribute {string} tags - Overrides the node's tags, separated by spaces or commas.
|
|
10742
|
+
* @attribute {string} onpointerenter - Script to run when the pointer moves onto the node.
|
|
10743
|
+
* @attribute {string} onpointerleave - Script to run when the pointer moves off the node.
|
|
10744
|
+
* @attribute {string} onpointermove - Script to run when the pointer moves over the node.
|
|
10745
|
+
* @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
|
|
10746
|
+
* node.
|
|
10747
|
+
* @attribute {string} onpointerup - Script to run when a pointer button is released over the
|
|
10748
|
+
* node.
|
|
10749
|
+
* @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the node.
|
|
10750
|
+
* @fires {PointerEvent} pointerleave - Fired when the pointer moves off the node.
|
|
10751
|
+
* @fires {PointerEvent} pointermove - Fired when the pointer moves over the node.
|
|
10752
|
+
* @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the node.
|
|
10753
|
+
* @fires {PointerEvent} pointerup - Fired when a pointer button is released over the node.
|
|
10754
|
+
*/
|
|
10755
|
+
class NodeElement extends EntityBaseElement {
|
|
10756
|
+
_name = '';
|
|
10757
|
+
_index = null;
|
|
10758
|
+
_state = 'pending';
|
|
10759
|
+
_path = null;
|
|
10760
|
+
/**
|
|
10761
|
+
* The element whose entity roots this element's search: the nearest ancestor `pc-node`, or
|
|
10762
|
+
* failing that the nearest ancestor `pc-model`. Resolved on connection.
|
|
10763
|
+
*/
|
|
10764
|
+
_host = null;
|
|
10765
|
+
/**
|
|
10766
|
+
* The listener following the host's binding cycles. Both host kinds announce each cycle
|
|
10767
|
+
* with a `ready` event — `pc-model` on every instantiation, `pc-node` on every bind.
|
|
10768
|
+
*/
|
|
10769
|
+
_hostListener = null;
|
|
10770
|
+
/**
|
|
10771
|
+
* The subscription to the bound entity's destruction, detached on unbind so a retargeted
|
|
10772
|
+
* element cannot be reset by the eventual death of a node it no longer fronts.
|
|
10773
|
+
*/
|
|
10774
|
+
_destroyHandle = null;
|
|
10775
|
+
/** The authored values displaced by this element's overrides, captured per property. */
|
|
10776
|
+
_authored = {};
|
|
10777
|
+
// Override values. `null` means "no override": the authored value stays in force.
|
|
10778
|
+
_enabled = null;
|
|
10779
|
+
_position = null;
|
|
10780
|
+
_rotation = null;
|
|
10781
|
+
_scale = null;
|
|
10782
|
+
_tags = null;
|
|
10783
|
+
/**
|
|
10784
|
+
* The binding state: `pending` until the host instantiates and `name` resolves, `bound`
|
|
10785
|
+
* once decorated, `missing`/`ambiguous`/`duplicate` when resolution failed (each also
|
|
10786
|
+
* warns). Useful for asserting a document's bindings programmatically.
|
|
10787
|
+
* @returns The binding state.
|
|
10788
|
+
*/
|
|
10789
|
+
get state() {
|
|
10790
|
+
return this._state;
|
|
10791
|
+
}
|
|
10792
|
+
/**
|
|
10793
|
+
* The path of the bound node below the search root, `/`-separated, or `null` while not
|
|
10794
|
+
* bound.
|
|
10795
|
+
* @returns The bound node's path, or `null`.
|
|
10796
|
+
*/
|
|
10797
|
+
get path() {
|
|
10798
|
+
return this._path;
|
|
10799
|
+
}
|
|
10800
|
+
connectedCallback() {
|
|
10801
|
+
const host = (this.parentElement?.closest('pc-model, pc-node') ?? null);
|
|
10802
|
+
if (!host) {
|
|
10803
|
+
const label = this._name ? ` '${this._name}'` : '';
|
|
10804
|
+
console.warn(`pc-node${label} must be a descendant of pc-model - node not bound`);
|
|
10805
|
+
return;
|
|
10806
|
+
}
|
|
10807
|
+
this._host = host;
|
|
10808
|
+
// Follow the host's binding cycles. `ready` bubbles, so cycles of elements nested under
|
|
10809
|
+
// the host pass through it - only the host's own count.
|
|
10810
|
+
this._hostListener = (event) => {
|
|
10811
|
+
if (event.target !== this._host) {
|
|
10812
|
+
return;
|
|
10813
|
+
}
|
|
10814
|
+
this._rebind();
|
|
10815
|
+
};
|
|
10816
|
+
host.addEventListener('ready', this._hostListener);
|
|
10817
|
+
// The host may already be instantiated (an element inserted after load binds immediately)
|
|
10818
|
+
this._rebind();
|
|
10819
|
+
}
|
|
10820
|
+
disconnectedCallback() {
|
|
10821
|
+
if (this._host && this._hostListener) {
|
|
10822
|
+
this._host.removeEventListener('ready', this._hostListener);
|
|
10823
|
+
}
|
|
10824
|
+
this._host = null;
|
|
10825
|
+
this._hostListener = null;
|
|
10826
|
+
// Removal reverts: the model owns the node, so the entity is left as authored. Children
|
|
10827
|
+
// clean up through their own disconnect behavior.
|
|
10828
|
+
this._unbind();
|
|
10829
|
+
this._state = 'pending';
|
|
10830
|
+
}
|
|
10831
|
+
/**
|
|
10832
|
+
* Re-resolves the binding against the host's current hierarchy: on connection, on a `name`
|
|
10833
|
+
* or `index` change, and on every host cycle (a model [re]instantiating, an enclosing
|
|
10834
|
+
* `pc-node` [re]binding). When re-resolution yields the entity already bound, the binding
|
|
10835
|
+
* is retained untouched — a redundant edit must not flicker overrides through a revert.
|
|
10836
|
+
*/
|
|
10837
|
+
_rebind() {
|
|
10838
|
+
const hostEntity = this._host?.entity ?? null;
|
|
10839
|
+
if (!hostEntity || !this._name) {
|
|
10840
|
+
// Host not instantiated (or nothing to look up yet): return to pending. An assigned
|
|
10841
|
+
// name arriving later, or the host's next cycle, resolves it.
|
|
10842
|
+
this._unbind();
|
|
10843
|
+
this._state = 'pending';
|
|
10844
|
+
return;
|
|
10845
|
+
}
|
|
10846
|
+
const target = this._resolve(hostEntity);
|
|
10847
|
+
if (target && target === this._entity) {
|
|
10848
|
+
this._path = this._pathOf(target, hostEntity);
|
|
10849
|
+
return;
|
|
10850
|
+
}
|
|
10851
|
+
this._unbind();
|
|
10852
|
+
if (!target) {
|
|
10853
|
+
// _resolve warned and set the failure state
|
|
10854
|
+
return;
|
|
10855
|
+
}
|
|
10856
|
+
this._bind(target, hostEntity);
|
|
10857
|
+
}
|
|
10858
|
+
/**
|
|
10859
|
+
* Resolves `name` (and `index`) to an entity under `hostEntity`, warning and recording the
|
|
10860
|
+
* failure state when it cannot.
|
|
10861
|
+
*
|
|
10862
|
+
* @param hostEntity - The root of the search.
|
|
10863
|
+
* @returns The resolved entity, or `null`.
|
|
10864
|
+
*/
|
|
10865
|
+
_resolve(hostEntity) {
|
|
10866
|
+
const matches = hostEntity.find((node) => node.name === this._name);
|
|
10867
|
+
if (matches.length === 0) {
|
|
10868
|
+
const closest = this._closestName(hostEntity);
|
|
10869
|
+
const hint = closest ? ` - closest match: '${closest}'` : '';
|
|
10870
|
+
console.warn(`pc-node '${this._name}' not found in ${this._describeHost()}${hint}`);
|
|
10871
|
+
this._state = 'missing';
|
|
10872
|
+
return null;
|
|
10873
|
+
}
|
|
10874
|
+
let target;
|
|
10875
|
+
if (this._index !== null) {
|
|
10876
|
+
if (this._index >= matches.length) {
|
|
10877
|
+
console.warn(`pc-node '${this._name}' index ${this._index} is out of range - ${matches.length} match(es) in ${this._describeHost()}`);
|
|
10878
|
+
this._state = 'missing';
|
|
10879
|
+
return null;
|
|
10880
|
+
}
|
|
10881
|
+
target = matches[this._index];
|
|
10882
|
+
}
|
|
10883
|
+
else if (matches.length > 1) {
|
|
10884
|
+
// Ambiguity binds nothing: a fallback guess performs side effects on the wrong
|
|
10885
|
+
// scene node, and would go wrong silently when a re-export introduces a duplicate
|
|
10886
|
+
// name. The candidates tell the author exactly what to write.
|
|
10887
|
+
const candidates = matches.map((m, i) => `[${i}] ${this._pathOf(m, hostEntity)}`).join(', ');
|
|
10888
|
+
console.warn(`pc-node '${this._name}' is ambiguous in ${this._describeHost()} - specify index: ${candidates}`);
|
|
10889
|
+
this._state = 'ambiguous';
|
|
10890
|
+
return null;
|
|
10891
|
+
}
|
|
10892
|
+
else {
|
|
10893
|
+
target = matches[0];
|
|
10894
|
+
}
|
|
10895
|
+
const owner = this.closestApp?.elementFromEntity(target);
|
|
10896
|
+
if (owner && owner !== this) {
|
|
10897
|
+
console.warn(`pc-node '${this._name}' resolves to a node already bound by another element - element ignored`);
|
|
10898
|
+
this._state = 'duplicate';
|
|
10899
|
+
return null;
|
|
10900
|
+
}
|
|
10901
|
+
return target;
|
|
10902
|
+
}
|
|
10903
|
+
/**
|
|
10904
|
+
* Binds `target`: registers it (making it a pick target), hooks its destruction, applies
|
|
10905
|
+
* this element's overrides, announces readiness and builds the deferred child subtree.
|
|
10906
|
+
*
|
|
10907
|
+
* @param target - The entity to bind.
|
|
10908
|
+
* @param hostEntity - The search root, for the path.
|
|
10909
|
+
*/
|
|
10910
|
+
_bind(target, hostEntity) {
|
|
10911
|
+
this._entity = target;
|
|
10912
|
+
this._registerEntity(target);
|
|
10913
|
+
this._destroyHandle = target.once('destroy', this._onEntityDestroy, this);
|
|
10914
|
+
this._state = 'bound';
|
|
10915
|
+
this._path = this._pathOf(target, hostEntity);
|
|
10916
|
+
this._applyOverrides();
|
|
10917
|
+
this._onReady();
|
|
10918
|
+
this._buildChildren();
|
|
10919
|
+
}
|
|
10920
|
+
/**
|
|
10921
|
+
* Dissolves the current binding, restoring every authored value this element's overrides
|
|
10922
|
+
* displaced and removing the decorations this binding hosts: attachment entities are
|
|
10923
|
+
* destroyed (re-created against the next binding) and component decorations are removed
|
|
10924
|
+
* from the abandoned node. Both sweeps are scoped by `closestEntity`, so a still-bound
|
|
10925
|
+
* nested `pc-node` keeps its own decorations. Safe to call in any state.
|
|
10926
|
+
*/
|
|
10927
|
+
_unbind() {
|
|
10928
|
+
const entity = this._entity;
|
|
10929
|
+
if (!entity) {
|
|
10930
|
+
return;
|
|
10931
|
+
}
|
|
10932
|
+
this._revertOverrides();
|
|
10933
|
+
// Attachment points anchor to the bound node, so they cannot outlive the binding. Each
|
|
10934
|
+
// destroyed entity resets its element, which the next _buildChildren re-creates.
|
|
10935
|
+
this.querySelectorAll('pc-entity').forEach((child) => {
|
|
10936
|
+
if (child.closestEntity === this) {
|
|
10937
|
+
child.entity?.destroy();
|
|
10938
|
+
}
|
|
10939
|
+
});
|
|
10940
|
+
this._destroyHandle?.off();
|
|
10941
|
+
this._destroyHandle = null;
|
|
10942
|
+
this._unregisterEntity(entity);
|
|
10943
|
+
this._entity = null;
|
|
10944
|
+
this._path = null;
|
|
10945
|
+
this._authored = {};
|
|
10946
|
+
// Component decorations come off through the same hook the host-ready cycle uses. A
|
|
10947
|
+
// dissolve that never rebinds fires no ready event, so the sweep is explicit - after
|
|
10948
|
+
// `_entity` is cleared, so the hook sees a host without an entity.
|
|
10949
|
+
this.querySelectorAll('*').forEach((child) => {
|
|
10950
|
+
if (child instanceof ComponentElement && child.closestEntity === this) {
|
|
10951
|
+
child._hostCycled();
|
|
10952
|
+
}
|
|
10953
|
+
});
|
|
10954
|
+
this._resetReady();
|
|
10955
|
+
}
|
|
10956
|
+
/**
|
|
10957
|
+
* Handles the destruction of the bound entity - its model unloading, reloading, or a script
|
|
10958
|
+
* destroying it. There is nothing to revert on a destroyed entity; the element returns to
|
|
10959
|
+
* pending and the host's next cycle re-resolves it.
|
|
10960
|
+
*/
|
|
10961
|
+
_onEntityDestroy(entity) {
|
|
10962
|
+
this._destroyHandle = null;
|
|
10963
|
+
this._unregisterEntity(entity);
|
|
10964
|
+
this._entity = null;
|
|
10965
|
+
this._path = null;
|
|
10966
|
+
this._authored = {};
|
|
10967
|
+
this._state = 'pending';
|
|
10968
|
+
this._resetReady();
|
|
10969
|
+
}
|
|
10970
|
+
/**
|
|
10971
|
+
* Creates and parents the entities of child `pc-entity` elements - the attachment points.
|
|
10972
|
+
* Mirrors the runtime-insertion path in EntityElement.connectedCallback: children were
|
|
10973
|
+
* deferred while this host was unresolved (or reset when a previous binding dissolved), and
|
|
10974
|
+
* build here once it binds.
|
|
10975
|
+
*/
|
|
10976
|
+
_buildChildren() {
|
|
10977
|
+
const app = this.closestApp?.app;
|
|
10978
|
+
if (!app) {
|
|
10979
|
+
return;
|
|
10980
|
+
}
|
|
10981
|
+
const childEntities = this.querySelectorAll('pc-entity');
|
|
10982
|
+
childEntities.forEach((child) => {
|
|
10983
|
+
child._createEntity(app);
|
|
10984
|
+
});
|
|
10985
|
+
childEntities.forEach((child) => {
|
|
10986
|
+
child._buildHierarchy(app);
|
|
10987
|
+
});
|
|
10988
|
+
}
|
|
10989
|
+
/**
|
|
10990
|
+
* Applies every override that is explicitly set, capturing the authored value it displaces.
|
|
10991
|
+
*/
|
|
10992
|
+
_applyOverrides() {
|
|
10993
|
+
if (this._enabled !== null) {
|
|
10994
|
+
this.enabled = this._enabled;
|
|
10995
|
+
}
|
|
10996
|
+
if (this._position !== null) {
|
|
10997
|
+
this.position = this._position;
|
|
10998
|
+
}
|
|
10999
|
+
if (this._rotation !== null) {
|
|
11000
|
+
this.rotation = this._rotation;
|
|
11001
|
+
}
|
|
11002
|
+
if (this._scale !== null) {
|
|
11003
|
+
this.scale = this._scale;
|
|
11004
|
+
}
|
|
11005
|
+
if (this._tags !== null) {
|
|
11006
|
+
this.tags = this._tags;
|
|
11007
|
+
}
|
|
11008
|
+
}
|
|
11009
|
+
/**
|
|
11010
|
+
* Restores every authored value this element's overrides displaced. The override values
|
|
11011
|
+
* themselves are kept - they re-apply on the next binding.
|
|
11012
|
+
*/
|
|
11013
|
+
_revertOverrides() {
|
|
11014
|
+
const entity = this._entity;
|
|
11015
|
+
const authored = this._authored;
|
|
11016
|
+
if (authored.enabled !== undefined) {
|
|
11017
|
+
entity.enabled = authored.enabled;
|
|
11018
|
+
}
|
|
11019
|
+
if (authored.position) {
|
|
11020
|
+
entity.setLocalPosition(authored.position);
|
|
11021
|
+
}
|
|
11022
|
+
if (authored.rotation) {
|
|
11023
|
+
entity.setLocalRotation(authored.rotation);
|
|
11024
|
+
}
|
|
11025
|
+
if (authored.scale) {
|
|
11026
|
+
entity.setLocalScale(authored.scale);
|
|
11027
|
+
}
|
|
11028
|
+
if (authored.tags) {
|
|
11029
|
+
entity.tags.clear();
|
|
11030
|
+
entity.tags.add(authored.tags);
|
|
11031
|
+
}
|
|
11032
|
+
this._authored = {};
|
|
11033
|
+
}
|
|
11034
|
+
/**
|
|
11035
|
+
* Renders the path of `node` below `root`, for the `path` property and the resolution
|
|
11036
|
+
* warnings.
|
|
11037
|
+
*
|
|
11038
|
+
* @param node - The node to describe.
|
|
11039
|
+
* @param root - The search root.
|
|
11040
|
+
* @returns The `/`-separated path.
|
|
11041
|
+
*/
|
|
11042
|
+
_pathOf(node, root) {
|
|
11043
|
+
const parts = [];
|
|
11044
|
+
for (let current = node; current && current !== root; current = current.parent) {
|
|
11045
|
+
parts.unshift(current.name);
|
|
11046
|
+
}
|
|
11047
|
+
return parts.join('/') || node.name;
|
|
11048
|
+
}
|
|
11049
|
+
/**
|
|
11050
|
+
* Describes the search root for warnings: the model's asset id, or the enclosing node's
|
|
11051
|
+
* name.
|
|
11052
|
+
* @returns The description.
|
|
11053
|
+
*/
|
|
11054
|
+
_describeHost() {
|
|
11055
|
+
if (this._host instanceof ModelElement) {
|
|
11056
|
+
return `model '${this._host.asset}'`;
|
|
11057
|
+
}
|
|
11058
|
+
return `pc-node '${this._host?.name ?? ''}' subtree`;
|
|
11059
|
+
}
|
|
11060
|
+
/**
|
|
11061
|
+
* Finds the node name nearest to the missing `name`, for the miss warning. The names are
|
|
11062
|
+
* already in hand from resolution, so the suggestion is nearly free.
|
|
11063
|
+
*
|
|
11064
|
+
* @param hostEntity - The root of the search.
|
|
11065
|
+
* @returns The closest name within an edit distance of 2, or `null`.
|
|
11066
|
+
*/
|
|
11067
|
+
_closestName(hostEntity) {
|
|
11068
|
+
let best = null;
|
|
11069
|
+
let bestDistance = 3;
|
|
11070
|
+
hostEntity.find((node) => {
|
|
11071
|
+
const distance = levenshtein(this._name, node.name);
|
|
11072
|
+
if (distance < bestDistance) {
|
|
11073
|
+
bestDistance = distance;
|
|
11074
|
+
best = node.name;
|
|
11075
|
+
}
|
|
11076
|
+
return false;
|
|
11077
|
+
});
|
|
11078
|
+
return best;
|
|
11079
|
+
}
|
|
11080
|
+
/**
|
|
11081
|
+
* Sets the name of the node to bind. A change retargets: the current binding's overrides
|
|
11082
|
+
* revert and the new name resolves afresh. `name` on a `pc-node` is never a rename of the
|
|
11083
|
+
* authored node - it is only ever a reference.
|
|
11084
|
+
* @param value - The node name.
|
|
11085
|
+
*/
|
|
11086
|
+
set name(value) {
|
|
11087
|
+
this._name = value;
|
|
11088
|
+
if (this.isConnected && this._host) {
|
|
11089
|
+
this._rebind();
|
|
11090
|
+
}
|
|
11091
|
+
}
|
|
11092
|
+
/**
|
|
11093
|
+
* Gets the name of the node to bind.
|
|
11094
|
+
* @returns The node name.
|
|
11095
|
+
*/
|
|
11096
|
+
get name() {
|
|
11097
|
+
return this._name;
|
|
11098
|
+
}
|
|
11099
|
+
/**
|
|
11100
|
+
* Sets which match to bind when `name` matches more than one node, 0-based in depth-first
|
|
11101
|
+
* order. A change retargets, like `name`. `null` means unset - required when the name is
|
|
11102
|
+
* ambiguous, optional otherwise.
|
|
11103
|
+
* @param value - The match index, or `null`.
|
|
11104
|
+
*/
|
|
11105
|
+
set index(value) {
|
|
11106
|
+
this._index = value;
|
|
11107
|
+
if (this.isConnected && this._host) {
|
|
11108
|
+
this._rebind();
|
|
11109
|
+
}
|
|
11110
|
+
}
|
|
11111
|
+
/**
|
|
11112
|
+
* Gets which match to bind.
|
|
11113
|
+
* @returns The match index, or `null` when unset.
|
|
11114
|
+
*/
|
|
11115
|
+
get index() {
|
|
11116
|
+
return this._index;
|
|
11117
|
+
}
|
|
11118
|
+
/**
|
|
11119
|
+
* Sets the enabled override. `null` clears it, restoring the authored state.
|
|
11120
|
+
* @param value - The enabled state, or `null`.
|
|
11121
|
+
*/
|
|
11122
|
+
set enabled(value) {
|
|
11123
|
+
this._enabled = value;
|
|
11124
|
+
const entity = this._state === 'bound' ? this._entity : null;
|
|
11125
|
+
if (!entity) {
|
|
11126
|
+
return;
|
|
11127
|
+
}
|
|
11128
|
+
if (value !== null) {
|
|
11129
|
+
this._authored.enabled ??= entity.enabled;
|
|
11130
|
+
entity.enabled = value;
|
|
11131
|
+
}
|
|
11132
|
+
else if (this._authored.enabled !== undefined) {
|
|
11133
|
+
entity.enabled = this._authored.enabled;
|
|
11134
|
+
delete this._authored.enabled;
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11137
|
+
/**
|
|
11138
|
+
* Gets the enabled override.
|
|
11139
|
+
* @returns The enabled state, or `null` while no override is set.
|
|
11140
|
+
*/
|
|
11141
|
+
get enabled() {
|
|
11142
|
+
return this._enabled;
|
|
11143
|
+
}
|
|
11144
|
+
/**
|
|
11145
|
+
* Sets the local position override. `null` clears it, restoring the authored position.
|
|
11146
|
+
* @param value - The position, or `null`.
|
|
11147
|
+
*/
|
|
11148
|
+
set position(value) {
|
|
11149
|
+
this._position = value;
|
|
11150
|
+
const entity = this._state === 'bound' ? this._entity : null;
|
|
11151
|
+
if (!entity) {
|
|
11152
|
+
return;
|
|
11153
|
+
}
|
|
11154
|
+
if (value !== null) {
|
|
11155
|
+
this._authored.position ??= entity.getLocalPosition().clone();
|
|
11156
|
+
entity.setLocalPosition(value);
|
|
11157
|
+
}
|
|
11158
|
+
else if (this._authored.position) {
|
|
11159
|
+
entity.setLocalPosition(this._authored.position);
|
|
11160
|
+
delete this._authored.position;
|
|
11161
|
+
}
|
|
11162
|
+
}
|
|
11163
|
+
/**
|
|
11164
|
+
* Gets the local position override.
|
|
11165
|
+
* @returns The position, or `null` while no override is set.
|
|
11166
|
+
*/
|
|
11167
|
+
get position() {
|
|
11168
|
+
return this._position;
|
|
11169
|
+
}
|
|
11170
|
+
/**
|
|
11171
|
+
* Sets the local rotation override, as Euler angles in degrees. `null` clears it, restoring
|
|
11172
|
+
* the authored rotation.
|
|
11173
|
+
* @param value - The rotation, or `null`.
|
|
11174
|
+
*/
|
|
11175
|
+
set rotation(value) {
|
|
11176
|
+
this._rotation = value;
|
|
11177
|
+
const entity = this._state === 'bound' ? this._entity : null;
|
|
11178
|
+
if (!entity) {
|
|
11179
|
+
return;
|
|
11180
|
+
}
|
|
11181
|
+
if (value !== null) {
|
|
11182
|
+
// The authored rotation is cached as a quaternion: it restores exactly, where a
|
|
11183
|
+
// round trip through Euler angles need not.
|
|
11184
|
+
this._authored.rotation ??= entity.getLocalRotation().clone();
|
|
11185
|
+
entity.setLocalEulerAngles(value);
|
|
11186
|
+
}
|
|
11187
|
+
else if (this._authored.rotation) {
|
|
11188
|
+
entity.setLocalRotation(this._authored.rotation);
|
|
11189
|
+
delete this._authored.rotation;
|
|
11190
|
+
}
|
|
11191
|
+
}
|
|
11192
|
+
/**
|
|
11193
|
+
* Gets the local rotation override.
|
|
11194
|
+
* @returns The rotation, or `null` while no override is set.
|
|
11195
|
+
*/
|
|
11196
|
+
get rotation() {
|
|
11197
|
+
return this._rotation;
|
|
11198
|
+
}
|
|
11199
|
+
/**
|
|
11200
|
+
* Sets the local scale override. `null` clears it, restoring the authored scale.
|
|
11201
|
+
* @param value - The scale, or `null`.
|
|
11202
|
+
*/
|
|
11203
|
+
set scale(value) {
|
|
11204
|
+
this._scale = value;
|
|
11205
|
+
const entity = this._state === 'bound' ? this._entity : null;
|
|
11206
|
+
if (!entity) {
|
|
11207
|
+
return;
|
|
11208
|
+
}
|
|
11209
|
+
if (value !== null) {
|
|
11210
|
+
this._authored.scale ??= entity.getLocalScale().clone();
|
|
11211
|
+
entity.setLocalScale(value);
|
|
11212
|
+
}
|
|
11213
|
+
else if (this._authored.scale) {
|
|
11214
|
+
entity.setLocalScale(this._authored.scale);
|
|
11215
|
+
delete this._authored.scale;
|
|
11216
|
+
}
|
|
11217
|
+
}
|
|
11218
|
+
/**
|
|
11219
|
+
* Gets the local scale override.
|
|
11220
|
+
* @returns The scale, or `null` while no override is set.
|
|
11221
|
+
*/
|
|
11222
|
+
get scale() {
|
|
11223
|
+
return this._scale;
|
|
11224
|
+
}
|
|
11225
|
+
/**
|
|
11226
|
+
* Sets the tags override. `null` clears it, restoring the authored tags.
|
|
11227
|
+
* @param value - The tags, or `null`.
|
|
11228
|
+
*/
|
|
11229
|
+
set tags(value) {
|
|
11230
|
+
this._tags = value;
|
|
11231
|
+
const entity = this._state === 'bound' ? this._entity : null;
|
|
11232
|
+
if (!entity) {
|
|
11233
|
+
return;
|
|
11234
|
+
}
|
|
11235
|
+
if (value !== null) {
|
|
11236
|
+
this._authored.tags ??= entity.tags.list().slice();
|
|
11237
|
+
entity.tags.clear();
|
|
11238
|
+
entity.tags.add(value);
|
|
11239
|
+
}
|
|
11240
|
+
else if (this._authored.tags) {
|
|
11241
|
+
entity.tags.clear();
|
|
11242
|
+
entity.tags.add(this._authored.tags);
|
|
11243
|
+
delete this._authored.tags;
|
|
11244
|
+
}
|
|
11245
|
+
}
|
|
11246
|
+
/**
|
|
11247
|
+
* Gets the tags override.
|
|
11248
|
+
* @returns The tags, or `null` while no override is set.
|
|
11249
|
+
*/
|
|
11250
|
+
get tags() {
|
|
11251
|
+
return this._tags;
|
|
11252
|
+
}
|
|
11253
|
+
static get observedAttributes() {
|
|
11254
|
+
return ['enabled', 'index', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
|
|
11255
|
+
}
|
|
11256
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
11257
|
+
switch (name) {
|
|
11258
|
+
case 'enabled':
|
|
11259
|
+
this.enabled = newValue === null ? null : parseBool(newValue, true);
|
|
11260
|
+
break;
|
|
11261
|
+
case 'index':
|
|
11262
|
+
if (newValue === null) {
|
|
11263
|
+
this.index = null;
|
|
11264
|
+
}
|
|
11265
|
+
else {
|
|
11266
|
+
// Number('') is 0, which would make index="" silently mean the first match
|
|
11267
|
+
const index = newValue.trim() === '' ? NaN : Number(newValue);
|
|
11268
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
11269
|
+
// Invalid values are treated as absent: under ambiguity that means
|
|
11270
|
+
// unbound, the fail-safe direction.
|
|
11271
|
+
console.warn(`pc-node index '${newValue}' is not a non-negative integer - treated as absent`);
|
|
11272
|
+
this.index = null;
|
|
11273
|
+
}
|
|
11274
|
+
else {
|
|
11275
|
+
this.index = index;
|
|
11276
|
+
}
|
|
11277
|
+
}
|
|
11278
|
+
break;
|
|
11279
|
+
case 'name':
|
|
11280
|
+
this.name = newValue ?? '';
|
|
11281
|
+
break;
|
|
11282
|
+
case 'position':
|
|
11283
|
+
this.position = newValue === null ? null : parseVec3(newValue, Vec3.ZERO, name);
|
|
11284
|
+
break;
|
|
11285
|
+
case 'rotation':
|
|
11286
|
+
this.rotation = newValue === null ? null : parseVec3(newValue, Vec3.ZERO, name);
|
|
11287
|
+
break;
|
|
11288
|
+
case 'scale':
|
|
11289
|
+
this.scale = newValue === null ? null : parseVec3(newValue, Vec3.ONE, name);
|
|
11290
|
+
break;
|
|
11291
|
+
case 'tags':
|
|
11292
|
+
this.tags = newValue === null ? null : parseTags(newValue);
|
|
11293
|
+
break;
|
|
11294
|
+
case 'onpointerenter':
|
|
11295
|
+
case 'onpointerleave':
|
|
11296
|
+
case 'onpointerdown':
|
|
11297
|
+
case 'onpointerup':
|
|
11298
|
+
case 'onpointermove':
|
|
11299
|
+
this._updateInlineHandler(name, newValue);
|
|
11300
|
+
break;
|
|
11301
|
+
}
|
|
11302
|
+
}
|
|
11303
|
+
}
|
|
11304
|
+
customElements.define('pc-node', NodeElement);
|
|
11305
|
+
|
|
10011
11306
|
/**
|
|
10012
11307
|
* The SceneElement interface provides properties and methods for manipulating
|
|
10013
11308
|
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scene/ | `<pc-scene>`} elements.
|
|
@@ -10280,7 +11575,6 @@ class SkyElement extends AsyncElement {
|
|
|
10280
11575
|
return;
|
|
10281
11576
|
const source = asset.resource;
|
|
10282
11577
|
const skybox = EnvLighting.generateSkyboxCubemap(source);
|
|
10283
|
-
skybox.anisotropy = 4;
|
|
10284
11578
|
// This element owns what it generated (see _unloadSkybox) - replacing a skybox from an
|
|
10285
11579
|
// earlier load must release it, not orphan it on the GPU
|
|
10286
11580
|
this._scene.skybox?.destroy();
|
|
@@ -10529,5 +11823,5 @@ class SkyElement extends AsyncElement {
|
|
|
10529
11823
|
}
|
|
10530
11824
|
customElements.define('pc-sky', SkyElement);
|
|
10531
11825
|
|
|
10532
|
-
export { AppElement, AssetElement, AsyncElement, ButtonComponentElement, CameraComponentElement, CollisionComponentElement, ComponentElement, ElementComponentElement, EntityElement, GSplatComponentElement, LayoutChildComponentElement, LayoutGroupComponentElement, LightComponentElement, ListenerComponentElement, MaterialElement, ModelElement, ModuleElement, ParticleSystemComponentElement, RenderComponentElement, RigidBodyComponentElement, SceneElement, ScreenComponentElement, ScriptComponentElement, ScriptElement, ScrollViewComponentElement, ScrollbarComponentElement, SkyElement, SoundComponentElement, SoundSlotElement, whenReady };
|
|
11826
|
+
export { AppElement, AssetElement, AsyncElement, ButtonComponentElement, CameraComponentElement, CollisionComponentElement, ComponentElement, ElementComponentElement, EntityBaseElement, EntityElement, GSplatComponentElement, LayoutChildComponentElement, LayoutGroupComponentElement, LightComponentElement, ListenerComponentElement, MaterialElement, ModelElement, ModuleElement, NodeElement, ParticleSystemComponentElement, RenderComponentElement, RigidBodyComponentElement, SceneElement, ScreenComponentElement, ScriptComponentElement, ScriptElement, ScrollViewComponentElement, ScrollbarComponentElement, SkyElement, SoundComponentElement, SoundSlotElement, whenReady };
|
|
10533
11827
|
//# sourceMappingURL=pwc.mjs.map
|