@playcanvas/web-components 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.cts +5 -113
- package/dist/app.d.ts +5 -113
- package/dist/asset-binding.d.cts +1 -0
- package/dist/asset-binding.d.ts +1 -0
- package/dist/components/anim-clip.d.cts +8 -10
- package/dist/components/anim-clip.d.ts +8 -10
- package/dist/components/anim-component.d.cts +5 -4
- package/dist/components/anim-component.d.ts +5 -4
- package/dist/components/audio-listener-component.d.cts +5 -4
- package/dist/components/audio-listener-component.d.ts +5 -4
- package/dist/components/button-component.d.cts +5 -4
- package/dist/components/button-component.d.ts +5 -4
- package/dist/components/camera-component.d.cts +5 -4
- package/dist/components/camera-component.d.ts +5 -4
- package/dist/components/collision-component.d.cts +5 -4
- package/dist/components/collision-component.d.ts +5 -4
- package/dist/components/component.d.cts +2 -2
- package/dist/components/component.d.ts +2 -2
- package/dist/components/element-component.d.cts +5 -4
- package/dist/components/element-component.d.ts +5 -4
- package/dist/components/gsplat-component.d.cts +5 -4
- package/dist/components/gsplat-component.d.ts +5 -4
- package/dist/components/joint-component.d.cts +5 -4
- package/dist/components/joint-component.d.ts +5 -4
- package/dist/components/layout-child-component.d.cts +5 -4
- package/dist/components/layout-child-component.d.ts +5 -4
- package/dist/components/layout-group-component.d.cts +5 -4
- package/dist/components/layout-group-component.d.ts +5 -4
- package/dist/components/light-component.d.cts +5 -4
- package/dist/components/light-component.d.ts +5 -4
- package/dist/components/particle-system-component.d.cts +22 -5
- package/dist/components/particle-system-component.d.ts +22 -5
- package/dist/components/render-component.d.cts +5 -4
- package/dist/components/render-component.d.ts +5 -4
- package/dist/components/rigid-body-component.d.cts +5 -4
- package/dist/components/rigid-body-component.d.ts +5 -4
- package/dist/components/screen-component.d.cts +5 -4
- package/dist/components/screen-component.d.ts +5 -4
- package/dist/components/script-component.d.cts +5 -4
- package/dist/components/script-component.d.ts +5 -4
- package/dist/components/scroll-view-component.d.cts +5 -4
- package/dist/components/scroll-view-component.d.ts +5 -4
- package/dist/components/scrollbar-component.d.cts +5 -4
- package/dist/components/scrollbar-component.d.ts +5 -4
- package/dist/components/sound-component.d.cts +5 -4
- package/dist/components/sound-component.d.ts +5 -4
- package/dist/custom-elements.json +38 -38
- package/dist/entity-reference.d.cts +13 -0
- package/dist/entity-reference.d.ts +13 -0
- package/dist/material.d.cts +8 -6
- package/dist/material.d.ts +8 -6
- package/dist/model.d.cts +8 -9
- package/dist/model.d.ts +8 -9
- package/dist/parse.d.cts +0 -8
- package/dist/parse.d.ts +0 -8
- package/dist/pointer-controller.d.cts +1 -0
- package/dist/pointer-controller.d.ts +1 -0
- package/dist/pwc.cjs +1360 -1186
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +1360 -1186
- 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 +1361 -1187
- package/dist/pwc.mjs.map +1 -1
- package/dist/sky.d.cts +6 -7
- package/dist/sky.d.ts +6 -7
- package/dist/web-types.json +39 -39
- package/package.json +2 -2
- package/src/app.ts +16 -451
- package/src/asset-binding.ts +141 -0
- package/src/components/anim-clip.ts +22 -45
- package/src/components/anim-component.ts +10 -6
- package/src/components/audio-listener-component.ts +6 -5
- package/src/components/button-component.ts +8 -6
- package/src/components/camera-component.ts +6 -5
- package/src/components/collision-component.ts +6 -5
- package/src/components/component.ts +6 -4
- package/src/components/element-component.ts +6 -5
- package/src/components/gsplat-component.ts +6 -5
- package/src/components/joint-component.ts +8 -6
- package/src/components/layout-child-component.ts +6 -5
- package/src/components/layout-group-component.ts +6 -5
- package/src/components/light-component.ts +6 -5
- package/src/components/particle-system-component.ts +56 -30
- package/src/components/render-component.ts +6 -5
- package/src/components/rigid-body-component.ts +6 -5
- package/src/components/screen-component.ts +6 -5
- package/src/components/script-component.ts +8 -10
- package/src/components/scroll-view-component.ts +8 -6
- package/src/components/scrollbar-component.ts +8 -6
- package/src/components/sound-component.ts +6 -5
- package/src/entity-reference.ts +237 -0
- package/src/material.ts +25 -29
- package/src/model.ts +24 -48
- package/src/parse.ts +0 -232
- package/src/pointer-controller.ts +550 -0
- package/src/sky.ts +19 -30
package/dist/pwc.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, createGraphicsDevice, AppOptions, Keyboard, Mouse, ElementInput, AnimComponentSystem, AnimationComponentSystem, AudioListenerComponentSystem, ButtonComponentSystem, CameraComponentSystem, CollisionComponentSystem, ElementComponentSystem, GSplatComponentSystem, JointComponentSystem, LayoutChildComponentSystem, LayoutGroupComponentSystem, LightComponentSystem, ModelComponentSystem, ParticleSystemComponentSystem, RenderComponentSystem, RigidBodyComponentSystem, ScreenComponentSystem, ScriptComponentSystem, ScrollbarComponentSystem, ScrollViewComponentSystem, SoundComponentSystem, SpriteComponentSystem, ZoneComponentSystem, AnimClipHandler, AnimationHandler, AnimStateGraphHandler, AudioHandler, BinaryHandler, CssHandler, ContainerHandler, CubemapHandler, FolderHandler, FontHandler, GSplatHandler, HierarchyHandler, HtmlHandler, JsonHandler, MaterialHandler, ModelHandler, RenderHandler, ScriptHandler, SceneHandler, ShaderHandler, SpriteHandler, TemplateHandler, TextHandler, TextureAtlasHandler, TextureHandler, SoundManager, Lightmapper, BatchManager, XrManager, AppBase, FILLMODE_NONE, RESOLUTION_AUTO,
|
|
1
|
+
import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, Picker, MeshInstance, 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, 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, AnimTrack, ANIM_CONTROL_STATES, BUTTON_TRANSITION_MODE_TINT, BUTTON_TRANSITION_MODE_SPRITE_CHANGE, TONEMAP_NONE, PROJECTION_PERSPECTIVE, GAMMA_SRGB, GAMMA_NONE, XRTYPE_AR, 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.
|
|
@@ -584,14 +584,6 @@ const CSS_COLORS = {
|
|
|
584
584
|
* shared frozen constants (`Vec3.ZERO`, `Color.WHITE`) as defaults.
|
|
585
585
|
* - `parseBool` and `parseTags` take no attribute name, because every value is valid for them and
|
|
586
586
|
* so they never warn.
|
|
587
|
-
*
|
|
588
|
-
* `findEntityElement` and `getEntity` are the exceptions: they resolve a reference rather than
|
|
589
|
-
* parsing a literal, and return `null` instead of falling back to a default. A reference
|
|
590
|
-
* beginning with `#` is a document-wide selector (an element id, or any selector rooted in one);
|
|
591
|
-
* anything else is an entity name, resolved lexically through the entity hierarchy first and
|
|
592
|
-
* against the document after — never as a selector or an id. They also do not warn - what an
|
|
593
|
-
* unresolved reference means depends on the element holding it - so elements report through
|
|
594
|
-
* `resolveEntity`, which takes that meaning as parameters.
|
|
595
587
|
*/
|
|
596
588
|
/**
|
|
597
589
|
* Splits an attribute value into exactly `count` numeric components. Returns `null` when the
|
|
@@ -837,215 +829,10 @@ const parseVec4 = (value, defaultValue, attribute) => {
|
|
|
837
829
|
}
|
|
838
830
|
return new Vec4(components);
|
|
839
831
|
};
|
|
840
|
-
/**
|
|
841
|
-
* Runs querySelector, absorbing the SyntaxError an unparseable selector throws - references are
|
|
842
|
-
* arbitrary author text, so a lookup must fail to `null`, never throw.
|
|
843
|
-
*
|
|
844
|
-
* @param selector - The selector to query.
|
|
845
|
-
* @returns The matched element, or `null`.
|
|
846
|
-
*/
|
|
847
|
-
const query = (selector) => {
|
|
848
|
-
try {
|
|
849
|
-
return document.querySelector(selector);
|
|
850
|
-
}
|
|
851
|
-
catch {
|
|
852
|
-
return null;
|
|
853
|
-
}
|
|
854
|
-
};
|
|
855
|
-
/**
|
|
856
|
-
* Runs a lookup against one scope, checking the scope element itself before its subtree — a
|
|
857
|
-
* reference deep in a cloned prefab must be able to name the prefab's root. Absorbs the
|
|
858
|
-
* SyntaxError of an invalid selector like {@link query}: escaping quotes and backslashes does not
|
|
859
|
-
* make arbitrary text a valid CSS string (a reference containing a newline still throws), so a
|
|
860
|
-
* lookup must fail to `null`, never throw.
|
|
861
|
-
*
|
|
862
|
-
* @param scope - The element whose inclusive subtree to search.
|
|
863
|
-
* @param selector - The selector to query.
|
|
864
|
-
* @returns The matched element, or `null`.
|
|
865
|
-
*/
|
|
866
|
-
const queryScope = (scope, selector) => {
|
|
867
|
-
try {
|
|
868
|
-
return scope.matches(selector) ? scope : scope.querySelector(selector);
|
|
869
|
-
}
|
|
870
|
-
catch {
|
|
871
|
-
return null;
|
|
872
|
-
}
|
|
873
|
-
};
|
|
874
|
-
/**
|
|
875
|
-
* Reads the entity a resolved element is backing, through the `entity` accessor every
|
|
876
|
-
* entity-fronting element exposes. `null` for no element, and for an element backing nothing.
|
|
877
|
-
*
|
|
878
|
-
* @param element - The element to read, or `null`.
|
|
879
|
-
* @returns The backing entity, or `null`.
|
|
880
|
-
*/
|
|
881
|
-
const entityOf = (element) => {
|
|
882
|
-
return element?.entity ?? null;
|
|
883
|
-
};
|
|
884
|
-
/**
|
|
885
|
-
* The elements that front an entity: what a bare name can resolve to, and the scopes of the
|
|
886
|
-
* lexical name lookup.
|
|
887
|
-
*/
|
|
888
|
-
const ENTITY_KINDS = ['pc-entity', 'pc-model', 'pc-node'];
|
|
889
|
-
/**
|
|
890
|
-
* The entity-fronting elements as one selector, for the scope walk.
|
|
891
|
-
*/
|
|
892
|
-
const ENTITY_SCOPES = ENTITY_KINDS.join(', ');
|
|
893
|
-
/**
|
|
894
|
-
* Resolves a reference string to the element it names. The grammar is closed — every reference
|
|
895
|
-
* has exactly one interpretation:
|
|
896
|
-
*
|
|
897
|
-
* - A reference beginning with `#` is a document-wide CSS selector — an element id (`#body`), or
|
|
898
|
-
* any selector rooted in one (`#hud pc-entity`). It is authoritative: the name lookup never
|
|
899
|
-
* runs for it, so an unusually named entity cannot shadow it.
|
|
900
|
-
* - Any other reference is the name of an entity-fronting element (`<pc-entity>`, `<pc-model>` or
|
|
901
|
-
* `<pc-node>` — for a node, the glTF node name it binds), and nothing else. A bare reference is
|
|
902
|
-
* never interpreted as a selector or an element id, so adding or renaming elements can never
|
|
903
|
-
* change which form it takes.
|
|
904
|
-
*
|
|
905
|
-
* When `from` is supplied, a name resolves lexically first: the closest entity-fronting
|
|
906
|
-
* ancestor's inclusive subtree, then each outer entity-fronting ancestor, then the containing
|
|
907
|
-
* `<pc-app>`, then the document. This is what lets a `<template>` prefab reference its own
|
|
908
|
-
* entities by name — every clone resolves within itself before a document-wide lookup could reach
|
|
909
|
-
* an earlier clone — provided the prefab has a single entity-fronting root to be the enclosing
|
|
910
|
-
* scope.
|
|
911
|
-
*
|
|
912
|
-
* Separate from {@link getEntity} so a caller reporting a failure can tell the causes apart
|
|
913
|
-
* ({@link unresolvedCause} words them): nothing in the document matches the reference, or
|
|
914
|
-
* something matches but is not backing an entity (yet, or ever).
|
|
915
|
-
*
|
|
916
|
-
* @param ref - The reference string to resolve.
|
|
917
|
-
* @param from - The element resolving the reference, whose entity-fronting ancestors scope the
|
|
918
|
-
* name lookup. Omitted, the name lookup is document-wide only.
|
|
919
|
-
* @returns The matched element, or `null`.
|
|
920
|
-
* @internal
|
|
921
|
-
*/
|
|
922
|
-
const findEntityElement = (ref, from) => {
|
|
923
|
-
if (!ref) {
|
|
924
|
-
return null;
|
|
925
|
-
}
|
|
926
|
-
// A '#' reference is document-wide and bypasses the name lookup entirely - an entity named
|
|
927
|
-
// '#body' must never shadow the element whose id is 'body'.
|
|
928
|
-
if (ref.startsWith('#')) {
|
|
929
|
-
return query(ref);
|
|
930
|
-
}
|
|
931
|
-
// The name lands inside a quoted CSS string, so its quotes and backslashes are escaped -
|
|
932
|
-
// a name like `say "hi"` must resolve, not turn the lookup into a SyntaxError.
|
|
933
|
-
const escaped = ref.replace(/["\\]/g, '\\$&');
|
|
934
|
-
const nameSelector = ENTITY_KINDS.map(kind => `${kind}[name="${escaped}"]`).join(', ');
|
|
935
|
-
if (from) {
|
|
936
|
-
let scope = from.parentElement?.closest(ENTITY_SCOPES);
|
|
937
|
-
while (scope) {
|
|
938
|
-
const element = queryScope(scope, nameSelector);
|
|
939
|
-
if (element) {
|
|
940
|
-
return element;
|
|
941
|
-
}
|
|
942
|
-
scope = scope.parentElement?.closest(ENTITY_SCOPES);
|
|
943
|
-
}
|
|
944
|
-
const app = from.parentElement?.closest('pc-app');
|
|
945
|
-
if (app) {
|
|
946
|
-
const element = queryScope(app, nameSelector);
|
|
947
|
-
if (element) {
|
|
948
|
-
return element;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
return query(nameSelector);
|
|
953
|
-
};
|
|
954
|
-
/**
|
|
955
|
-
* Resolves a reference string to the {@link Entity} backing an entity-fronting element
|
|
956
|
-
* (`<pc-entity>`, `<pc-model>` or `<pc-node>`). The reference is a name — resolved lexically
|
|
957
|
-
* through the entity hierarchy first when `from` is supplied — or a document-wide `#` selector
|
|
958
|
-
* ({@link findEntityElement} details the grammar and order). Returns `null` if no matching
|
|
959
|
-
* element (or backing entity) is found.
|
|
960
|
-
*
|
|
961
|
-
* @param ref - The reference string to resolve.
|
|
962
|
-
* @param from - The element resolving the reference, whose entity-fronting ancestors scope the
|
|
963
|
-
* name lookup. Omitted, the name lookup is document-wide only.
|
|
964
|
-
* @returns The resolved entity, or `null`.
|
|
965
|
-
* @internal
|
|
966
|
-
*/
|
|
967
|
-
const getEntity = (ref, from) => {
|
|
968
|
-
return entityOf(findEntityElement(ref, from));
|
|
969
|
-
};
|
|
970
|
-
/**
|
|
971
|
-
* Describes why a non-empty reference did not resolve, for a warning. Three causes, because they
|
|
972
|
-
* have three different fixes: nothing matches (usually a typo), the matched element is not backing
|
|
973
|
-
* an entity yet (usually timing - a `pc-node` whose asset has not loaded - so resolving again
|
|
974
|
-
* later can work), or the matched element can never back one (the reference points at the wrong
|
|
975
|
-
* element, so only correcting it can). Capability is the `entity` accessor every entity-backing
|
|
976
|
-
* element inherits from EntityBaseElement.
|
|
977
|
-
*
|
|
978
|
-
* @param element - The element the reference matched, or `null` when nothing did.
|
|
979
|
-
* @returns The cause, phrased to follow `could not resolve ... -`.
|
|
980
|
-
* @internal
|
|
981
|
-
*/
|
|
982
|
-
const unresolvedCause = (element) => {
|
|
983
|
-
if (!element) {
|
|
984
|
-
return 'nothing in the document matches it';
|
|
985
|
-
}
|
|
986
|
-
const tag = `<${element.tagName.toLowerCase()}>`;
|
|
987
|
-
return 'entity' in element
|
|
988
|
-
? `${tag} matches it but is not backing an entity yet`
|
|
989
|
-
: `${tag} matches it but cannot back an entity`;
|
|
990
|
-
};
|
|
991
|
-
/**
|
|
992
|
-
* Builds the migration pointer for a bare reference that names nothing but matches the id of an
|
|
993
|
-
* entity-fronting element - it was almost certainly meant as an id, so point at the form that
|
|
994
|
-
* expresses it, escaped so the suggestion actually parses as a selector (an id like `a:b` must
|
|
995
|
-
* be written `#a\:b`). Empty when the reference is already a `#` form, matches no id, or the id
|
|
996
|
-
* belongs to an element that could never back an entity - suggesting it would only trade this
|
|
997
|
-
* warning for the wrong-target one.
|
|
998
|
-
*
|
|
999
|
-
* @param ref - The unresolved reference.
|
|
1000
|
-
* @param prefix - Text the suggested form must carry in the caller's syntax (e.g. `entity:`).
|
|
1001
|
-
* @returns The advice sentence, or an empty string.
|
|
1002
|
-
* @internal
|
|
1003
|
-
*/
|
|
1004
|
-
const idHint = (ref, prefix = '') => {
|
|
1005
|
-
const match = !ref.startsWith('#') && document.getElementById(ref);
|
|
1006
|
-
return match && 'entity' in match
|
|
1007
|
-
? `A bare reference is a name - write '${prefix}#${CSS.escape(ref)}' to reference the element with that id.`
|
|
1008
|
-
: '';
|
|
1009
|
-
};
|
|
1010
|
-
/**
|
|
1011
|
-
* Resolves a reference string to the {@link Entity} backing an entity-fronting element, scoped to
|
|
1012
|
-
* the resolving element ({@link findEntityElement} details the order) and warning when a
|
|
1013
|
-
* non-empty reference does not resolve - otherwise the reference fails silently, invisible
|
|
1014
|
-
* except through the behavior it should have driven. The message names which of the three causes
|
|
1015
|
-
* ({@link unresolvedCause}) it hit, and advises reassigning later only when that can work.
|
|
1016
|
-
*
|
|
1017
|
-
* An empty reference stays silent: it is the unset state of an optional attribute, and on some
|
|
1018
|
-
* elements (`pc-joint` `entity-b`, `pc-button` `image`) a documented value of its own.
|
|
1019
|
-
*
|
|
1020
|
-
* @param ref - The reference string to resolve.
|
|
1021
|
-
* @param from - The element resolving the reference; scopes the lookup and names the message.
|
|
1022
|
-
* @param attribute - The attribute being resolved, for the message.
|
|
1023
|
-
* @param consequence - What the unresolved reference means for the element, for the message.
|
|
1024
|
-
* @returns The resolved entity, or `null`.
|
|
1025
|
-
* @internal
|
|
1026
|
-
*/
|
|
1027
|
-
const resolveEntity = (ref, from, attribute, consequence) => {
|
|
1028
|
-
if (!ref) {
|
|
1029
|
-
return null;
|
|
1030
|
-
}
|
|
1031
|
-
const element = findEntityElement(ref, from);
|
|
1032
|
-
const entity = entityOf(element);
|
|
1033
|
-
if (!entity) {
|
|
1034
|
-
let advice = `Assign ${attribute} again once the entity exists.`;
|
|
1035
|
-
if (element && !('entity' in element)) {
|
|
1036
|
-
advice = `Point ${attribute} at a pc-entity, pc-model or pc-node instead.`;
|
|
1037
|
-
}
|
|
1038
|
-
else if (!element) {
|
|
1039
|
-
const hint = idHint(ref);
|
|
1040
|
-
if (hint) {
|
|
1041
|
-
advice = hint;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
console.warn(`${from.tagName.toLowerCase()} could not resolve ${attribute} '${ref}' - ${unresolvedCause(element)} - ${consequence}. ${advice}`);
|
|
1045
|
-
}
|
|
1046
|
-
return entity;
|
|
1047
|
-
};
|
|
1048
832
|
|
|
833
|
+
// Keep `export` on these declarations. TypeScript removes the declaration and its inline export
|
|
834
|
+
// when `stripInternal` is enabled. A separate `export { ... }` statement would remain in the
|
|
835
|
+
// generated .d.ts file and refer to a declaration that had been removed.
|
|
1049
836
|
/**
|
|
1050
837
|
* The event types whose listeners make an element a hover target. Hover resolution walks past
|
|
1051
838
|
* elements listening for none of them, so a silent element never swallows an ancestor's
|
|
@@ -1092,88 +879,29 @@ const commonAncestor = (a, b) => {
|
|
|
1092
879
|
return null;
|
|
1093
880
|
};
|
|
1094
881
|
/**
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*/
|
|
1100
|
-
const ensureBaseStyles = () => {
|
|
1101
|
-
const id = 'pc-app-styles';
|
|
1102
|
-
if (document.getElementById(id)) {
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
const style = document.createElement('style');
|
|
1106
|
-
style.id = id;
|
|
1107
|
-
style.textContent = ':where(pc-app) { display: block; position: relative; width: 300px; height: 150px; }';
|
|
1108
|
-
document.head.appendChild(style);
|
|
1109
|
-
};
|
|
1110
|
-
/**
|
|
1111
|
-
* The AppElement interface provides properties and methods for manipulating
|
|
1112
|
-
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
1113
|
-
* The AppElement interface also inherits the properties and methods of the
|
|
1114
|
-
* {@link HTMLElement} interface.
|
|
1115
|
-
*
|
|
1116
|
-
* The element is sized like a replaced element such as `<video>`: a block-level box that the
|
|
1117
|
-
* page's CSS controls, 300x150 by default. The application's canvas always fills the element,
|
|
1118
|
-
* and the drawing buffer resolution follows the element's size (capped by `max-pixel-ratio`),
|
|
1119
|
-
* tracked live via a ResizeObserver — so the element can be embedded at any size, resized by
|
|
1120
|
-
* its container, or made fullscreen with ordinary CSS such as `width: 100vw; height: 100dvh`.
|
|
1121
|
-
*
|
|
1122
|
-
* @elementSummary The `<pc-app>` element creates a PlayCanvas application and the canvas it renders
|
|
1123
|
-
* into, and is the root of every scene. It holds the `<pc-asset>`, `<pc-material>`, `<pc-wasm>` and
|
|
1124
|
-
* `<pc-scene>` elements, and the page's CSS sizes it, as it would a `<video>`.
|
|
882
|
+
* The pointer-input subsystem of a `<pc-app>` element: it owns the engine {@link Picker}, the
|
|
883
|
+
* canvas pointer handlers, and everything between them - mapping browser coordinates into the
|
|
884
|
+
* drawing buffer, selecting the camera, resolving picked nodes to listening elements, tracking
|
|
885
|
+
* hover, and dispatching the synthesized pointer and click events in canvas-event order.
|
|
1125
886
|
*
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
* not bubble.
|
|
887
|
+
* The host drives a small lifecycle: {@link connect} once the application and canvas exist,
|
|
888
|
+
* {@link resize} when the drawing buffer changes size, {@link syncListeners} when a descendant's
|
|
889
|
+
* pointer listeners change, and {@link disconnect} on teardown. Everything else is internal.
|
|
1130
890
|
*
|
|
1131
|
-
* @
|
|
1132
|
-
* could be created (for example, a browser with WebGL disabled). `message` names the requested
|
|
1133
|
-
* backends and `error` holds the underlying failure. The element never becomes ready
|
|
1134
|
-
* and `app` stays `null` — listen for this event to show a fallback UI. Removing the element and
|
|
1135
|
-
* re-inserting it retries the boot with its current attributes. Does not bubble.
|
|
891
|
+
* @internal
|
|
1136
892
|
*/
|
|
1137
|
-
class
|
|
893
|
+
class PointerController {
|
|
894
|
+
_host;
|
|
1138
895
|
/**
|
|
1139
|
-
*
|
|
896
|
+
* Incremented by every connect and disconnect. Async work captures the value when it starts
|
|
897
|
+
* and stops if it has moved on - so a pick or dispatch belonging to an earlier connection
|
|
898
|
+
* can neither keep reading through its destroyed picker nor deliver into a later
|
|
899
|
+
* connection. The field null checks alone cannot tell the two apart once a reconnect has
|
|
900
|
+
* repopulated them.
|
|
1140
901
|
*/
|
|
902
|
+
_generation = 0;
|
|
903
|
+
_app = null;
|
|
1141
904
|
_canvas = null;
|
|
1142
|
-
_alpha = true;
|
|
1143
|
-
_backend = 'webgpu';
|
|
1144
|
-
_antialias = true;
|
|
1145
|
-
_depthBuffer = true;
|
|
1146
|
-
_stencilBuffer = true;
|
|
1147
|
-
_maxPixelRatio = Infinity;
|
|
1148
|
-
_loadingBar = true;
|
|
1149
|
-
/**
|
|
1150
|
-
* Set once the graphics options above have been handed to `createGraphicsDevice`, after which
|
|
1151
|
-
* writing any of them changes nothing. Guards the warning in {@link _warnIfBooted}, and is
|
|
1152
|
-
* cleared on disconnect so a re-connected element boots from its current attributes.
|
|
1153
|
-
*/
|
|
1154
|
-
_optionsLocked = false;
|
|
1155
|
-
_bar = null;
|
|
1156
|
-
/**
|
|
1157
|
-
* Whether the application has created its initial entity hierarchy. Read by EntityElement to
|
|
1158
|
-
* decide whether a newly connected element must create its entity itself or leave it to the
|
|
1159
|
-
* boot sweep.
|
|
1160
|
-
* @internal
|
|
1161
|
-
*/
|
|
1162
|
-
_hierarchyReady = false;
|
|
1163
|
-
/**
|
|
1164
|
-
* Incremented on every connect and disconnect. Boot captures the value on entry and abandons
|
|
1165
|
-
* itself wherever it resumes from an await if the value has moved on — so a boot whose
|
|
1166
|
-
* element was removed cannot complete against a torn-down element, and a boot whose element
|
|
1167
|
-
* was removed and re-inserted (which starts a boot of its own) cannot race the newer one.
|
|
1168
|
-
*/
|
|
1169
|
-
_bootGeneration = 0;
|
|
1170
|
-
/**
|
|
1171
|
-
* The elements backing this application's entities, keyed by the entity itself. Registered
|
|
1172
|
-
* by entity-owning elements at creation (pc-entity, and pc-model for its host) and by
|
|
1173
|
-
* NodeElement at binding, and removed when an entity is destroyed or unbound, this joins
|
|
1174
|
-
* engine scene nodes back to their owning elements by identity - never by name.
|
|
1175
|
-
*/
|
|
1176
|
-
_entityElements = new Map();
|
|
1177
905
|
_picker = null;
|
|
1178
906
|
_hoveredEntity = null;
|
|
1179
907
|
// Identifies the newest in-flight hover pick, so out-of-order results can be discarded
|
|
@@ -1192,7 +920,7 @@ class AppElement extends AsyncElement {
|
|
|
1192
920
|
* which is also what keeps those two canvas listeners attached.
|
|
1193
921
|
*/
|
|
1194
922
|
_downPicks = new Map();
|
|
1195
|
-
/** Whether any element in the tree listens for click. Maintained by
|
|
923
|
+
/** Whether any element in the tree listens for click. Maintained by syncListeners. */
|
|
1196
924
|
_clickListened = false;
|
|
1197
925
|
/**
|
|
1198
926
|
* The previous click's target, time and count, for chaining successive clicks into the
|
|
@@ -1201,504 +929,194 @@ class AppElement extends AsyncElement {
|
|
|
1201
929
|
_lastClick = null;
|
|
1202
930
|
/**
|
|
1203
931
|
* Serializes dispatch of the discrete synthesized events (pointerdown, pointerup, click),
|
|
1204
|
-
* whose picks resolve in GPU order, not canvas-event order. Replaced on
|
|
1205
|
-
* that never resolves cannot stall the dispatches of a later boot.
|
|
932
|
+
* whose picks resolve in GPU order, not canvas-event order. Replaced on disconnect, so a
|
|
933
|
+
* pick that never resolves cannot stall the dispatches of a later boot.
|
|
1206
934
|
*/
|
|
1207
935
|
_dispatchChain = Promise.resolve();
|
|
1208
|
-
_app = null;
|
|
1209
|
-
_loadProgress = 0;
|
|
1210
936
|
/**
|
|
1211
|
-
*
|
|
1212
|
-
* the application exists, and disconnected on teardown. `null` where ResizeObserver is
|
|
1213
|
-
* unavailable (jsdom), where the boot-time resolution set is the only sizing that happens.
|
|
937
|
+
* @param host - The services the controller reads from its host element.
|
|
1214
938
|
*/
|
|
1215
|
-
|
|
939
|
+
constructor(host) {
|
|
940
|
+
this._host = host;
|
|
941
|
+
}
|
|
1216
942
|
/**
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
943
|
+
* Creates the picker and the canvas handlers for a booted application, and attaches whatever
|
|
944
|
+
* canvas listeners the tree's current element listeners already need (handlers created from
|
|
945
|
+
* inline attributes when their elements were first upgraded, or listeners carried over from
|
|
946
|
+
* before a re-boot).
|
|
947
|
+
*
|
|
948
|
+
* @param app - The application to pick against.
|
|
949
|
+
* @param canvas - The canvas the application renders into.
|
|
1221
950
|
*/
|
|
1222
|
-
|
|
1223
|
-
|
|
951
|
+
connect(app, canvas) {
|
|
952
|
+
this._generation++;
|
|
953
|
+
this._app = app;
|
|
954
|
+
this._canvas = canvas;
|
|
955
|
+
const { width, height } = app.graphicsDevice;
|
|
956
|
+
this._picker = new Picker(app, width, height);
|
|
957
|
+
// Create bound handlers but don't attach them yet. The move handler is async, so it is
|
|
958
|
+
// wrapped to discard the promise - a listener must not return one.
|
|
959
|
+
const listener = (handler) => {
|
|
960
|
+
return (event) => {
|
|
961
|
+
handler.call(this, event);
|
|
962
|
+
};
|
|
963
|
+
};
|
|
964
|
+
this._pointerHandlers.pointermove = listener(this._onPointerMove);
|
|
965
|
+
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
966
|
+
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
967
|
+
this._pointerHandlers.pointercancel = (event) => {
|
|
968
|
+
this._downPicks.delete(event.pointerId);
|
|
969
|
+
};
|
|
970
|
+
this.syncListeners();
|
|
1224
971
|
}
|
|
1225
972
|
/**
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
* initialize a loading UI; subsequent updates arrive via the `progress` event.
|
|
1230
|
-
* @returns The preload progress.
|
|
973
|
+
* Detaches the canvas listeners and drops every piece of pointer state, so nothing picked or
|
|
974
|
+
* queued before the teardown can affect a later boot. Safe to call on a controller that was
|
|
975
|
+
* never connected.
|
|
1231
976
|
*/
|
|
1232
|
-
|
|
1233
|
-
|
|
977
|
+
disconnect() {
|
|
978
|
+
this._generation++;
|
|
979
|
+
if (this._canvas) {
|
|
980
|
+
Object.entries(this._pointerHandlers).forEach(([type, handler]) => {
|
|
981
|
+
if (handler) {
|
|
982
|
+
this._canvas.removeEventListener(type, handler);
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
this._app = null;
|
|
987
|
+
this._canvas = null;
|
|
988
|
+
this._picker = null;
|
|
989
|
+
this._hoveredEntity = null;
|
|
990
|
+
this._pointerHandlers = {
|
|
991
|
+
pointermove: null,
|
|
992
|
+
pointerdown: null,
|
|
993
|
+
pointerup: null,
|
|
994
|
+
pointercancel: null
|
|
995
|
+
};
|
|
996
|
+
this._downPicks.clear();
|
|
997
|
+
this._clickListened = false;
|
|
998
|
+
this._lastClick = null;
|
|
999
|
+
// Replace the chain: a pick that never resolves must not stall a later boot's dispatches
|
|
1000
|
+
this._dispatchChain = Promise.resolve();
|
|
1234
1001
|
}
|
|
1235
1002
|
/**
|
|
1236
|
-
*
|
|
1003
|
+
* Resizes the picker to the drawing buffer. The picker must track the buffer, or picks would
|
|
1004
|
+
* land at stale coordinates after a resize.
|
|
1237
1005
|
*
|
|
1238
|
-
* @
|
|
1006
|
+
* @param width - The drawing buffer width.
|
|
1007
|
+
* @param height - The drawing buffer height.
|
|
1239
1008
|
*/
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
// Track listeners for the synthesized events being added to and removed from descendant
|
|
1243
|
-
// entities. Registered once here rather than on every boot - the sync no-ops while there
|
|
1244
|
-
// is no canvas, and a re-booted element must not stack a second set.
|
|
1245
|
-
SYNTHESIZED_EVENTS.forEach((type) => {
|
|
1246
|
-
this.addEventListener(`${type}:connect`, () => this._syncCanvasListeners());
|
|
1247
|
-
this.addEventListener(`${type}:disconnect`, () => this._syncCanvasListeners());
|
|
1248
|
-
});
|
|
1009
|
+
resize(width, height) {
|
|
1010
|
+
this._picker?.resize(width, height);
|
|
1249
1011
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
// where custom element definitions are never looked up - and appending the clone upgrades
|
|
1263
|
-
// its elements in tree order, this one before its descendants. The module query below would
|
|
1264
|
-
// otherwise find plain HTMLElements with no _getLoadPromise to call, and the boot would die
|
|
1265
|
-
// there, leaving the element permanently unready: no canvas, no entities, no application.
|
|
1266
|
-
//
|
|
1267
|
-
// Upgrading is the fix here rather than skipping whatever has not upgraded, because a
|
|
1268
|
-
// <pc-wasm> is the one child that nothing else ever builds on its own behalf - skipping
|
|
1269
|
-
// it would drop the wasm module the app asked for, silently and only for cloned apps.
|
|
1270
|
-
// Upgrading runs each descendant's connectedCallback synchronously, a few lines earlier
|
|
1271
|
-
// than the parser's path runs them but into the same state they see there: no application
|
|
1272
|
-
// yet and _hierarchyReady false, so they defer to the sweeps below. A descendant that
|
|
1273
|
-
// disconnects this element from there is caught by the generation check after the await,
|
|
1274
|
-
// as any other disconnect is. An already-upgraded subtree - every other insertion path -
|
|
1275
|
-
// is left completely untouched.
|
|
1276
|
-
customElements.upgrade(this);
|
|
1277
|
-
// Get all pc-wasm elements that are direct children of the pc-app element
|
|
1278
|
-
const wasmElements = this.querySelectorAll(':scope > pc-wasm');
|
|
1279
|
-
// Wait for all modules to load
|
|
1280
|
-
await Promise.all(Array.from(wasmElements).map((element) => element._getLoadPromise()));
|
|
1281
|
-
// The element may have been removed while the modules loaded. Nothing beyond the loading
|
|
1282
|
-
// bar exists yet, and disconnectedCallback has already destroyed that.
|
|
1283
|
-
if (generation !== this._bootGeneration) {
|
|
1012
|
+
/**
|
|
1013
|
+
* Attaches exactly the canvas listeners the tree's current element listeners need, and
|
|
1014
|
+
* detaches the rest. Called whenever a listener connects or disconnects anywhere under the
|
|
1015
|
+
* host element: several synthesized types can need the same canvas listener (enter, leave
|
|
1016
|
+
* and move all ride the move pick; click rides the down/up pair), so one type's removal
|
|
1017
|
+
* must not detach a listener another type still uses. Re-attaching an attached listener is
|
|
1018
|
+
* a no-op by EventTarget semantics, so no attach state is kept. Does nothing before
|
|
1019
|
+
* {@link connect} - connecting syncs once the handlers exist.
|
|
1020
|
+
*/
|
|
1021
|
+
syncListeners() {
|
|
1022
|
+
const canvas = this._canvas;
|
|
1023
|
+
if (!canvas)
|
|
1284
1024
|
return;
|
|
1025
|
+
const elements = this._host.pointerTargets();
|
|
1026
|
+
const needed = new Set();
|
|
1027
|
+
for (const type of SYNTHESIZED_EVENTS) {
|
|
1028
|
+
if (elements.some((element) => element._hasListeners(type))) {
|
|
1029
|
+
canvasEventsFor[type].forEach((canvasType) => needed.add(canvasType));
|
|
1030
|
+
}
|
|
1285
1031
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
this._canvas = document.createElement('canvas');
|
|
1290
|
-
this._canvas.style.cssText = 'display: block; width: 100%; height: 100%; touch-action: none;';
|
|
1291
|
-
this.appendChild(this._canvas);
|
|
1292
|
-
// Configure device types based on backend selection
|
|
1293
|
-
const backendToDeviceTypes = {
|
|
1294
|
-
webgpu: ['webgpu', 'webgl2'], // fallback to webgl2 if webgpu not available
|
|
1295
|
-
webgl2: ['webgl2'],
|
|
1296
|
-
null: ['null']
|
|
1297
|
-
};
|
|
1298
|
-
const deviceTypes = backendToDeviceTypes[this._backend] || [];
|
|
1299
|
-
this._optionsLocked = true;
|
|
1300
|
-
// createGraphicsDevice appends its final null-device fallback to the array in place, so
|
|
1301
|
-
// the requested list is captured now for the failure message.
|
|
1302
|
-
const requested = deviceTypes.join(', ');
|
|
1303
|
-
let device;
|
|
1304
|
-
try {
|
|
1305
|
-
device = await createGraphicsDevice(this._canvas, {
|
|
1306
|
-
// @ts-ignore - alpha needs to be documented
|
|
1307
|
-
alpha: this._alpha,
|
|
1308
|
-
antialias: this._antialias,
|
|
1309
|
-
depth: this._depthBuffer,
|
|
1310
|
-
deviceTypes: deviceTypes,
|
|
1311
|
-
stencil: this._stencilBuffer
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
catch (error) {
|
|
1315
|
-
// The element may have been removed while device creation was failing. The teardown
|
|
1316
|
-
// has already cleaned up, and the failure belongs to a boot that no longer owns the
|
|
1317
|
-
// element.
|
|
1318
|
-
if (generation !== this._bootGeneration) {
|
|
1032
|
+
this._clickListened = elements.some((element) => element._hasListeners('click'));
|
|
1033
|
+
Object.entries(this._pointerHandlers).forEach(([canvasType, handler]) => {
|
|
1034
|
+
if (!handler)
|
|
1319
1035
|
return;
|
|
1036
|
+
if (needed.has(canvasType)) {
|
|
1037
|
+
canvas.addEventListener(canvasType, handler);
|
|
1320
1038
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
// it would announce (the app, the entity hierarchy) exists, so a device-less element
|
|
1324
|
-
// joins the documented never-ready cases and the failure surfaces through the error
|
|
1325
|
-
// event instead.
|
|
1326
|
-
if (this._canvas && this.contains(this._canvas)) {
|
|
1327
|
-
this.removeChild(this._canvas);
|
|
1039
|
+
else {
|
|
1040
|
+
canvas.removeEventListener(canvasType, handler);
|
|
1328
1041
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Resolves the element that owns hover for a picked node: the nearest node up the parent
|
|
1046
|
+
* chain - starting with the node itself - whose element listens for any of the hover event
|
|
1047
|
+
* types. Skipping silent elements matches {@link _elementWithListener}, so a registered
|
|
1048
|
+
* element with no hover listeners (a `<pc-model>` host, a plain child entity) is transparent
|
|
1049
|
+
* to hover rather than swallowing a listening ancestor's enter/leave pair.
|
|
1050
|
+
*
|
|
1051
|
+
* @param node - The picked node, or `null`.
|
|
1052
|
+
* @returns The hover-owning element, or `null`.
|
|
1053
|
+
*/
|
|
1054
|
+
_hoverTarget(node) {
|
|
1055
|
+
while (node !== null) {
|
|
1056
|
+
const element = this._host.elementFromNode(node);
|
|
1057
|
+
if (element && hoverEventTypes.some((type) => element._hasListeners(type))) {
|
|
1058
|
+
return element;
|
|
1059
|
+
}
|
|
1060
|
+
node = node.parent;
|
|
1337
1061
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
useTouch: true
|
|
1356
|
-
});
|
|
1357
|
-
createOptions.componentSystems = [
|
|
1358
|
-
AnimComponentSystem,
|
|
1359
|
-
AnimationComponentSystem,
|
|
1360
|
-
AudioListenerComponentSystem,
|
|
1361
|
-
ButtonComponentSystem,
|
|
1362
|
-
CameraComponentSystem,
|
|
1363
|
-
CollisionComponentSystem,
|
|
1364
|
-
ElementComponentSystem,
|
|
1365
|
-
GSplatComponentSystem,
|
|
1366
|
-
JointComponentSystem,
|
|
1367
|
-
LayoutChildComponentSystem,
|
|
1368
|
-
LayoutGroupComponentSystem,
|
|
1369
|
-
LightComponentSystem,
|
|
1370
|
-
ModelComponentSystem,
|
|
1371
|
-
ParticleSystemComponentSystem,
|
|
1372
|
-
RenderComponentSystem,
|
|
1373
|
-
RigidBodyComponentSystem,
|
|
1374
|
-
ScreenComponentSystem,
|
|
1375
|
-
ScriptComponentSystem,
|
|
1376
|
-
ScrollbarComponentSystem,
|
|
1377
|
-
ScrollViewComponentSystem,
|
|
1378
|
-
SoundComponentSystem,
|
|
1379
|
-
SpriteComponentSystem,
|
|
1380
|
-
ZoneComponentSystem
|
|
1381
|
-
];
|
|
1382
|
-
createOptions.resourceHandlers = [
|
|
1383
|
-
AnimClipHandler,
|
|
1384
|
-
AnimationHandler,
|
|
1385
|
-
AnimStateGraphHandler,
|
|
1386
|
-
AudioHandler,
|
|
1387
|
-
BinaryHandler,
|
|
1388
|
-
CssHandler,
|
|
1389
|
-
ContainerHandler,
|
|
1390
|
-
CubemapHandler,
|
|
1391
|
-
FolderHandler,
|
|
1392
|
-
FontHandler,
|
|
1393
|
-
GSplatHandler,
|
|
1394
|
-
HierarchyHandler,
|
|
1395
|
-
HtmlHandler,
|
|
1396
|
-
JsonHandler,
|
|
1397
|
-
MaterialHandler,
|
|
1398
|
-
ModelHandler,
|
|
1399
|
-
RenderHandler,
|
|
1400
|
-
ScriptHandler,
|
|
1401
|
-
SceneHandler,
|
|
1402
|
-
ShaderHandler,
|
|
1403
|
-
SpriteHandler,
|
|
1404
|
-
TemplateHandler,
|
|
1405
|
-
TextHandler,
|
|
1406
|
-
TextureAtlasHandler,
|
|
1407
|
-
TextureHandler
|
|
1408
|
-
];
|
|
1409
|
-
createOptions.soundManager = new SoundManager();
|
|
1410
|
-
createOptions.lightmapper = Lightmapper;
|
|
1411
|
-
createOptions.batchManager = BatchManager;
|
|
1412
|
-
createOptions.xr = XrManager;
|
|
1413
|
-
const app = new AppBase(this._canvas);
|
|
1414
|
-
this._app = app;
|
|
1415
|
-
app.init(createOptions);
|
|
1416
|
-
// FILLMODE_NONE leaves the canvas's CSS sizing alone (the engine's other fill modes
|
|
1417
|
-
// stamp window-derived pixel sizes onto it); RESOLUTION_AUTO sizes the drawing buffer
|
|
1418
|
-
// from the canvas's client size
|
|
1419
|
-
app.setCanvasFillMode(FILLMODE_NONE);
|
|
1420
|
-
app.setCanvasResolution(RESOLUTION_AUTO);
|
|
1421
|
-
this._pickerCreate();
|
|
1422
|
-
// Track the element's box rather than the window: containers resize without any window
|
|
1423
|
-
// event (splitter drags, flex reflow, animations). Guarded because jsdom has no
|
|
1424
|
-
// ResizeObserver - there, the resolution set above is the only sizing that happens.
|
|
1425
|
-
if (typeof ResizeObserver !== 'undefined') {
|
|
1426
|
-
this._resizeObserver = new ResizeObserver(() => this._syncCanvasSize());
|
|
1427
|
-
this._resizeObserver.observe(this);
|
|
1428
|
-
}
|
|
1429
|
-
// Get all pc-asset elements that are direct children of the pc-app element
|
|
1430
|
-
const assetElements = this.querySelectorAll(':scope > pc-asset');
|
|
1431
|
-
for (const assetElement of Array.from(assetElements)) {
|
|
1432
|
-
assetElement._createAsset();
|
|
1433
|
-
const asset = assetElement.asset;
|
|
1434
|
-
if (asset) {
|
|
1435
|
-
app.assets.add(asset);
|
|
1436
|
-
// Adding a fileless asset (one built purely from data, such as a sprite)
|
|
1437
|
-
// completes it synchronously, dispatching the element's load event - whose
|
|
1438
|
-
// listeners may have removed this element. Stop before the next addition
|
|
1439
|
-
// reaches the destroyed registry, and before orphan entities are created.
|
|
1440
|
-
if (generation !== this._bootGeneration) {
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
// Get all pc-material elements that are direct children of the pc-app element
|
|
1446
|
-
const materialElements = this.querySelectorAll(':scope > pc-material');
|
|
1447
|
-
Array.from(materialElements).forEach((materialElement) => {
|
|
1448
|
-
materialElement._createMaterial();
|
|
1449
|
-
});
|
|
1450
|
-
// Create all entities. pc-model joins the sweep because it owns a host entity of its
|
|
1451
|
-
// own; its instantiated content arrives later, beneath that host.
|
|
1452
|
-
const ownerElements = this.querySelectorAll('pc-entity, pc-model');
|
|
1453
|
-
Array.from(ownerElements).forEach((ownerElement) => {
|
|
1454
|
-
ownerElement._createEntity(app);
|
|
1455
|
-
});
|
|
1456
|
-
// Build hierarchy
|
|
1457
|
-
ownerElements.forEach((ownerElement) => {
|
|
1458
|
-
ownerElement._buildHierarchy(app);
|
|
1459
|
-
});
|
|
1460
|
-
// Building the hierarchy dispatched each entity's ready event synchronously, and a
|
|
1461
|
-
// listener may have removed the element. The sweep itself degrades safely - destroying
|
|
1462
|
-
// the application nulls every element's entity, so the remaining builds no-op - but the
|
|
1463
|
-
// teardown's reset must not be overwritten here.
|
|
1464
|
-
if (generation !== this._bootGeneration) {
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
this._hierarchyReady = true;
|
|
1468
|
-
// Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
|
|
1469
|
-
// listener must be attached before preload() is called: an asset that is already loaded
|
|
1470
|
-
// ticks synchronously inside it.
|
|
1471
|
-
const total = app.assets.list({ preload: true }).length;
|
|
1472
|
-
let loaded = 0;
|
|
1473
|
-
const onPreloadProgress = () => {
|
|
1474
|
-
loaded += 1;
|
|
1475
|
-
this._loadProgress = loaded / total;
|
|
1476
|
-
this._bar?.progress(loaded, total);
|
|
1477
|
-
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded, total }));
|
|
1478
|
-
};
|
|
1479
|
-
app.on('preload:progress', onPreloadProgress);
|
|
1480
|
-
this._loadProgress = total === 0 ? 1 : 0;
|
|
1481
|
-
this._bar?.progress(0, total);
|
|
1482
|
-
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
|
|
1483
|
-
// The progress dispatch above ran listeners synchronously, and one may have removed the
|
|
1484
|
-
// element. The application is already destroyed - it must not be asked to preload.
|
|
1485
|
-
if (generation !== this._bootGeneration) {
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
// Load assets before starting the application
|
|
1489
|
-
app.preload(() => {
|
|
1490
|
-
// The element may have been removed while assets loaded. The application is already
|
|
1491
|
-
// destroyed, so it must not be started — and readiness must not be signaled for a
|
|
1492
|
-
// boot that no longer owns the element.
|
|
1493
|
-
if (generation !== this._bootGeneration) {
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
// Scope the counter to this preload pass, so a later app.preload() call by user code
|
|
1497
|
-
// cannot push `loaded` past `total`
|
|
1498
|
-
app.off('preload:progress', onPreloadProgress);
|
|
1499
|
-
this._loadProgress = 1;
|
|
1500
|
-
// Start the application
|
|
1501
|
-
app.start();
|
|
1502
|
-
// Dismiss the bar only once a frame has actually rendered; ready fires before the
|
|
1503
|
-
// first rAF tick
|
|
1504
|
-
app.once('frameend', () => this._bar?.complete());
|
|
1505
|
-
this._onReady();
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
disconnectedCallback() {
|
|
1509
|
-
// Invalidate any boot still in flight, so it abandons itself when it next resumes
|
|
1510
|
-
// instead of completing against a torn-down element.
|
|
1511
|
-
this._bootGeneration++;
|
|
1512
|
-
this._optionsLocked = false;
|
|
1513
|
-
this._pickerDestroy();
|
|
1514
|
-
// Clean up the application. Destroying it destroys every entity, whose destroy hooks
|
|
1515
|
-
// unregister them - clear() covers any entity the engine no longer reached.
|
|
1516
|
-
if (this._app) {
|
|
1517
|
-
this._app.destroy();
|
|
1518
|
-
this._app = null;
|
|
1519
|
-
}
|
|
1520
|
-
this._entityElements.clear();
|
|
1521
|
-
this._loadProgress = 0;
|
|
1522
|
-
this._bar?.destroy();
|
|
1523
|
-
this._bar = null;
|
|
1524
|
-
// Return the element to its pre-boot state, so re-inserting it boots afresh: descendants
|
|
1525
|
-
// must neither see a hierarchy that no longer exists nor resume against a readiness that
|
|
1526
|
-
// no longer holds.
|
|
1527
|
-
this._hierarchyReady = false;
|
|
1528
|
-
this._resetReady();
|
|
1529
|
-
// Stop tracking the element's size
|
|
1530
|
-
this._resizeObserver?.disconnect();
|
|
1531
|
-
this._resizeObserver = null;
|
|
1532
|
-
// Remove the canvas
|
|
1533
|
-
if (this._canvas && this.contains(this._canvas)) {
|
|
1534
|
-
this.removeChild(this._canvas);
|
|
1535
|
-
this._canvas = null;
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Like {@link _hoverTarget}, but for one event type: skips elements without a listener for
|
|
1066
|
+
* `type`, so a hit on an unlistened child still reaches a listening ancestor.
|
|
1067
|
+
*
|
|
1068
|
+
* @param node - The picked node, or `null`.
|
|
1069
|
+
* @param type - The pointer event type a listener is required for.
|
|
1070
|
+
* @returns The nearest listening element, or `null`.
|
|
1071
|
+
*/
|
|
1072
|
+
_elementWithListener(node, type) {
|
|
1073
|
+
while (node !== null) {
|
|
1074
|
+
const element = this._host.elementFromNode(node);
|
|
1075
|
+
if (element?._hasListeners(type)) {
|
|
1076
|
+
return element;
|
|
1077
|
+
}
|
|
1078
|
+
node = node.parent;
|
|
1536
1079
|
}
|
|
1080
|
+
return null;
|
|
1537
1081
|
}
|
|
1538
1082
|
/**
|
|
1539
|
-
*
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1083
|
+
* Converts a pointer event's client coordinates into drawing-buffer coordinates - the space
|
|
1084
|
+
* the pick buffer and the camera viewports are laid out in. When the canvas has no CSS box
|
|
1085
|
+
* to map through (jsdom; a hidden canvas receives no pointer events in a browser), the
|
|
1086
|
+
* client coordinates are passed through unmapped and `mapped` is false, so callers know the
|
|
1087
|
+
* coordinates correspond to no real geometry.
|
|
1088
|
+
*
|
|
1089
|
+
* @param event - The pointer event to convert.
|
|
1090
|
+
* @param canvas - The canvas the event was dispatched on.
|
|
1091
|
+
* @returns The buffer-space coordinates, and whether they were actually mapped.
|
|
1542
1092
|
*/
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
this.app.updateCanvasSize();
|
|
1548
|
-
const { width, height } = this.app.graphicsDevice;
|
|
1549
|
-
this._picker?.resize(width, height);
|
|
1550
|
-
}
|
|
1551
|
-
_pickerCreate() {
|
|
1552
|
-
const { width, height } = this.app.graphicsDevice;
|
|
1553
|
-
this._picker = new Picker(this.app, width, height);
|
|
1554
|
-
// Create bound handlers but don't attach them yet. The move handler is async, so it is
|
|
1555
|
-
// wrapped to discard the promise - a listener must not return one.
|
|
1556
|
-
const listener = (handler) => {
|
|
1557
|
-
return (event) => {
|
|
1558
|
-
handler.call(this, event);
|
|
1559
|
-
};
|
|
1560
|
-
};
|
|
1561
|
-
this._pointerHandlers.pointermove = listener(this._onPointerMove);
|
|
1562
|
-
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
1563
|
-
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
1564
|
-
this._pointerHandlers.pointercancel = (event) => {
|
|
1565
|
-
this._downPicks.delete(event.pointerId);
|
|
1566
|
-
};
|
|
1567
|
-
// Attach canvas listeners for element listeners registered before this boot (e.g.
|
|
1568
|
-
// handlers created from inline attributes when their elements were first upgraded, or
|
|
1569
|
-
// listeners carried over from before a re-boot)
|
|
1570
|
-
this._syncCanvasListeners();
|
|
1571
|
-
}
|
|
1572
|
-
_pickerDestroy() {
|
|
1573
|
-
if (this._canvas) {
|
|
1574
|
-
Object.entries(this._pointerHandlers).forEach(([type, handler]) => {
|
|
1575
|
-
if (handler) {
|
|
1576
|
-
this._canvas.removeEventListener(type, handler);
|
|
1577
|
-
}
|
|
1578
|
-
});
|
|
1093
|
+
_getPickerCoordinates(event, canvas) {
|
|
1094
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
1095
|
+
if (canvasRect.width === 0 || canvasRect.height === 0) {
|
|
1096
|
+
return { x: event.clientX, y: event.clientY, mapped: false };
|
|
1579
1097
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
pointercancel: null
|
|
1098
|
+
const scaleX = canvas.width / canvasRect.width;
|
|
1099
|
+
const scaleY = canvas.height / canvasRect.height;
|
|
1100
|
+
return {
|
|
1101
|
+
x: (event.clientX - canvasRect.left) * scaleX,
|
|
1102
|
+
y: (event.clientY - canvasRect.top) * scaleY,
|
|
1103
|
+
mapped: true
|
|
1587
1104
|
};
|
|
1588
|
-
this._downPicks.clear();
|
|
1589
|
-
this._clickListened = false;
|
|
1590
|
-
this._lastClick = null;
|
|
1591
|
-
// Replace the chain: a pick that never resolves must not stall a later boot's dispatches
|
|
1592
|
-
this._dispatchChain = Promise.resolve();
|
|
1593
1105
|
}
|
|
1594
1106
|
/**
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1107
|
+
* Whether a camera's viewport contains the point. A camera renders into its normalized
|
|
1108
|
+
* `rect`, whose origin is the bottom-left of the canvas while buffer coordinates run from
|
|
1109
|
+
* the top-left - so the vertical test flips, as the engine's ElementInput flips it for UI
|
|
1110
|
+
* input. The right and bottom edges are exclusive: a viewport rasterizes the half-open
|
|
1111
|
+
* pixel range [left, right) x [top, bottom), so a coordinate on a shared edge belongs to
|
|
1112
|
+
* the viewport whose first pixel it is - never to the one it just left, whose pick buffer
|
|
1113
|
+
* holds nothing there.
|
|
1597
1114
|
*
|
|
1598
|
-
* @param
|
|
1599
|
-
* @param
|
|
1600
|
-
* @
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
this._entityElements.set(entity, element);
|
|
1604
|
-
}
|
|
1605
|
-
/**
|
|
1606
|
-
* Removes the registration for a destroyed entity. Called by EntityElement.
|
|
1607
|
-
*
|
|
1608
|
-
* @param entity - The entity.
|
|
1609
|
-
* @internal
|
|
1610
|
-
*/
|
|
1611
|
-
_unregisterEntityElement(entity) {
|
|
1612
|
-
this._entityElements.delete(entity);
|
|
1613
|
-
}
|
|
1614
|
-
/**
|
|
1615
|
-
* Returns the `<pc-entity>`, `<pc-model>` or `<pc-node>` element whose backing entity is
|
|
1616
|
-
* `entity`, or `null` if the entity is not fronted by an element of this application - for
|
|
1617
|
-
* example, an unbound node inside a model's instantiated hierarchy, or an entity created
|
|
1618
|
-
* through the engine API.
|
|
1619
|
-
*
|
|
1620
|
-
* @param entity - The entity to look up.
|
|
1621
|
-
* @returns The element fronting the entity, or `null`.
|
|
1622
|
-
*/
|
|
1623
|
-
elementFromEntity(entity) {
|
|
1624
|
-
return this._entityElements.get(entity) ?? null;
|
|
1625
|
-
}
|
|
1626
|
-
/**
|
|
1627
|
-
* Resolves the element that owns hover for a picked node: the nearest node up the parent
|
|
1628
|
-
* chain - starting with the node itself - whose element listens for any of the hover event
|
|
1629
|
-
* types. Skipping silent elements matches {@link _elementWithListener}, so a registered
|
|
1630
|
-
* element with no hover listeners (a `<pc-model>` host, a plain child entity) is transparent
|
|
1631
|
-
* to hover rather than swallowing a listening ancestor's enter/leave pair.
|
|
1632
|
-
*
|
|
1633
|
-
* @param node - The picked node, or `null`.
|
|
1634
|
-
* @returns The hover-owning element, or `null`.
|
|
1635
|
-
*/
|
|
1636
|
-
_hoverTarget(node) {
|
|
1637
|
-
while (node !== null) {
|
|
1638
|
-
const element = this._entityElements.get(node);
|
|
1639
|
-
if (element && hoverEventTypes.some((type) => element._hasListeners(type))) {
|
|
1640
|
-
return element;
|
|
1641
|
-
}
|
|
1642
|
-
node = node.parent;
|
|
1643
|
-
}
|
|
1644
|
-
return null;
|
|
1645
|
-
}
|
|
1646
|
-
/**
|
|
1647
|
-
* Like {@link _elementFromNode}, but skips elements without a listener for `type`, so a hit
|
|
1648
|
-
* on an unlistened child still reaches a listening ancestor.
|
|
1649
|
-
*
|
|
1650
|
-
* @param node - The picked node, or `null`.
|
|
1651
|
-
* @param type - The pointer event type a listener is required for.
|
|
1652
|
-
* @returns The nearest listening element, or `null`.
|
|
1653
|
-
*/
|
|
1654
|
-
_elementWithListener(node, type) {
|
|
1655
|
-
while (node !== null) {
|
|
1656
|
-
const element = this._entityElements.get(node);
|
|
1657
|
-
if (element?._hasListeners(type)) {
|
|
1658
|
-
return element;
|
|
1659
|
-
}
|
|
1660
|
-
node = node.parent;
|
|
1661
|
-
}
|
|
1662
|
-
return null;
|
|
1663
|
-
}
|
|
1664
|
-
/**
|
|
1665
|
-
* Converts a pointer event's client coordinates into drawing-buffer coordinates - the space
|
|
1666
|
-
* the pick buffer and the camera viewports are laid out in. When the canvas has no CSS box
|
|
1667
|
-
* to map through (jsdom; a hidden canvas receives no pointer events in a browser), the
|
|
1668
|
-
* client coordinates are passed through unmapped and `mapped` is false, so callers know the
|
|
1669
|
-
* coordinates correspond to no real geometry.
|
|
1670
|
-
*
|
|
1671
|
-
* @param event - The pointer event to convert.
|
|
1672
|
-
* @param canvas - The canvas the event was dispatched on.
|
|
1673
|
-
* @returns The buffer-space coordinates, and whether they were actually mapped.
|
|
1674
|
-
*/
|
|
1675
|
-
_getPickerCoordinates(event, canvas) {
|
|
1676
|
-
const canvasRect = canvas.getBoundingClientRect();
|
|
1677
|
-
if (canvasRect.width === 0 || canvasRect.height === 0) {
|
|
1678
|
-
return { x: event.clientX, y: event.clientY, mapped: false };
|
|
1679
|
-
}
|
|
1680
|
-
const scaleX = canvas.width / canvasRect.width;
|
|
1681
|
-
const scaleY = canvas.height / canvasRect.height;
|
|
1682
|
-
return {
|
|
1683
|
-
x: (event.clientX - canvasRect.left) * scaleX,
|
|
1684
|
-
y: (event.clientY - canvasRect.top) * scaleY,
|
|
1685
|
-
mapped: true
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Whether a camera's viewport contains the point. A camera renders into its normalized
|
|
1690
|
-
* `rect`, whose origin is the bottom-left of the canvas while buffer coordinates run from
|
|
1691
|
-
* the top-left - so the vertical test flips, as the engine's ElementInput flips it for UI
|
|
1692
|
-
* input. The right and bottom edges are exclusive: a viewport rasterizes the half-open
|
|
1693
|
-
* pixel range [left, right) x [top, bottom), so a coordinate on a shared edge belongs to
|
|
1694
|
-
* the viewport whose first pixel it is - never to the one it just left, whose pick buffer
|
|
1695
|
-
* holds nothing there.
|
|
1696
|
-
*
|
|
1697
|
-
* @param camera - The camera to test.
|
|
1698
|
-
* @param x - The x coordinate, in buffer space.
|
|
1699
|
-
* @param y - The y coordinate, in buffer space.
|
|
1700
|
-
* @param canvas - The canvas the coordinates are relative to.
|
|
1701
|
-
* @returns Whether the camera's viewport contains the point.
|
|
1115
|
+
* @param camera - The camera to test.
|
|
1116
|
+
* @param x - The x coordinate, in buffer space.
|
|
1117
|
+
* @param y - The y coordinate, in buffer space.
|
|
1118
|
+
* @param canvas - The canvas the coordinates are relative to.
|
|
1119
|
+
* @returns Whether the camera's viewport contains the point.
|
|
1702
1120
|
*/
|
|
1703
1121
|
_cameraContains(camera, x, y, canvas) {
|
|
1704
1122
|
const rect = camera.rect;
|
|
@@ -1727,7 +1145,8 @@ class AppElement extends AsyncElement {
|
|
|
1727
1145
|
* @returns The graph node under the pointer, or `null` if nothing was hit.
|
|
1728
1146
|
*/
|
|
1729
1147
|
async _pickNode(event) {
|
|
1730
|
-
const
|
|
1148
|
+
const generation = this._generation;
|
|
1149
|
+
const app = this._app;
|
|
1731
1150
|
const picker = this._picker;
|
|
1732
1151
|
const canvas = this._canvas;
|
|
1733
1152
|
if (!app || !picker || !canvas)
|
|
@@ -1747,8 +1166,9 @@ class AppElement extends AsyncElement {
|
|
|
1747
1166
|
continue;
|
|
1748
1167
|
picker.prepare(camera, app.scene);
|
|
1749
1168
|
const selection = await picker.getSelectionAsync(x, y);
|
|
1750
|
-
// The
|
|
1751
|
-
|
|
1169
|
+
// The host may have disconnected - or disconnected and reconnected - while the read
|
|
1170
|
+
// back was in flight. Either way this pick's connection is gone.
|
|
1171
|
+
if (generation !== this._generation)
|
|
1752
1172
|
return null;
|
|
1753
1173
|
if (selection.length > 0) {
|
|
1754
1174
|
const item = selection[0];
|
|
@@ -1763,145 +1183,588 @@ class AppElement extends AsyncElement {
|
|
|
1763
1183
|
return null;
|
|
1764
1184
|
}
|
|
1765
1185
|
async _onPointerMove(event) {
|
|
1766
|
-
if (!this._picker || !this.
|
|
1186
|
+
if (!this._picker || !this._app)
|
|
1767
1187
|
return;
|
|
1768
1188
|
// Moves arrive faster than a pick resolves, so results can land out of order. Only the
|
|
1769
1189
|
// newest pick may update the hover state - an older one describes a pointer position the
|
|
1770
|
-
// user has already left
|
|
1190
|
+
// user has already left, and one from an earlier connection describes a scene that no
|
|
1191
|
+
// longer exists.
|
|
1192
|
+
const generation = this._generation;
|
|
1771
1193
|
const token = ++this._pickToken;
|
|
1772
1194
|
const node = await this._pickNode(event);
|
|
1773
|
-
if (token !== this._pickToken ||
|
|
1195
|
+
if (token !== this._pickToken || generation !== this._generation)
|
|
1196
|
+
return;
|
|
1197
|
+
// The hovered element is the nearest one up the node's parent chain with a hover
|
|
1198
|
+
// listener - the nearest-listener rule down/up use. Dispatch is still gated per event
|
|
1199
|
+
// type below: having any hover listener selects the target, each event needs its own.
|
|
1200
|
+
const newHoverEntity = this._hoverTarget(node);
|
|
1201
|
+
// Handle enter/leave events
|
|
1202
|
+
if (this._hoveredEntity !== newHoverEntity) {
|
|
1203
|
+
if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
|
|
1204
|
+
this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
|
|
1205
|
+
}
|
|
1206
|
+
if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
|
|
1207
|
+
newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// Update hover state
|
|
1211
|
+
this._hoveredEntity = newHoverEntity;
|
|
1212
|
+
// Handle pointermove event
|
|
1213
|
+
if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
|
|
1214
|
+
newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Appends a dispatch step to {@link _dispatchChain}. Must be called synchronously from the
|
|
1219
|
+
* canvas event handler - the order of appends is what carries canvas-event order. A step
|
|
1220
|
+
* that rejects is reported and released, so the steps queued behind it still dispatch.
|
|
1221
|
+
*
|
|
1222
|
+
* @param step - The dispatch work to run once every earlier step has finished.
|
|
1223
|
+
*/
|
|
1224
|
+
_chainDispatch(step) {
|
|
1225
|
+
this._dispatchChain = this._dispatchChain.then(step).catch((error) => {
|
|
1226
|
+
console.error(error);
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
_onPointerDown(event) {
|
|
1230
|
+
if (!this._picker || !this._app)
|
|
1231
|
+
return;
|
|
1232
|
+
const generation = this._generation;
|
|
1233
|
+
// Picks stay concurrent - only the dispatch of the results is serialized
|
|
1234
|
+
const pick = this._pickNode(event);
|
|
1235
|
+
// A click concludes on the matching pointerup, which needs to know what the press
|
|
1236
|
+
// picked. Primary button only - the only button a click can conclude from - and only
|
|
1237
|
+
// while click is listened for, since it is the click mapping that keeps the pointerup
|
|
1238
|
+
// and pointercancel listeners attached to clean the entry up again.
|
|
1239
|
+
if (this._clickListened && event.button === 0) {
|
|
1240
|
+
this._downPicks.set(event.pointerId, pick);
|
|
1241
|
+
}
|
|
1242
|
+
this._chainDispatch(async () => {
|
|
1243
|
+
const node = await pick;
|
|
1244
|
+
if (generation !== this._generation)
|
|
1245
|
+
return; // this press's connection is gone
|
|
1246
|
+
const entityElement = this._elementWithListener(node, 'pointerdown');
|
|
1247
|
+
if (entityElement) {
|
|
1248
|
+
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
_onPointerUp(event) {
|
|
1253
|
+
if (!this._picker || !this._app)
|
|
1254
|
+
return;
|
|
1255
|
+
const generation = this._generation;
|
|
1256
|
+
// The press pick this release may conclude as a click. Claimed synchronously, so the
|
|
1257
|
+
// entry is gone before any other event for this pointer can be handled.
|
|
1258
|
+
const downPick = this._downPicks.get(event.pointerId);
|
|
1259
|
+
this._downPicks.delete(event.pointerId);
|
|
1260
|
+
const pick = this._pickNode(event);
|
|
1261
|
+
this._chainDispatch(async () => {
|
|
1262
|
+
const node = await pick;
|
|
1263
|
+
if (generation !== this._generation)
|
|
1264
|
+
return; // this release's connection is gone
|
|
1265
|
+
const entityElement = this._elementWithListener(node, 'pointerup');
|
|
1266
|
+
if (entityElement) {
|
|
1267
|
+
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
// A click fires where the DOM fires it: at the nearest common inclusive ancestor of
|
|
1271
|
+
// what the press and the release picked, for the primary button only. Appended after
|
|
1272
|
+
// the release's own step, so it dispatches after the pointerup that concludes it.
|
|
1273
|
+
if (!downPick || event.button !== 0)
|
|
1274
|
+
return;
|
|
1275
|
+
this._chainDispatch(async () => {
|
|
1276
|
+
// A rejected pick was already reported by the press or release step that awaited it;
|
|
1277
|
+
// here it just means no click can conclude.
|
|
1278
|
+
const picked = await Promise.all([downPick, pick]).catch(() => null);
|
|
1279
|
+
if (!picked || generation !== this._generation)
|
|
1280
|
+
return;
|
|
1281
|
+
const [downNode, upNode] = picked;
|
|
1282
|
+
const clickElement = this._elementWithListener(commonAncestor(downNode, upNode), 'click');
|
|
1283
|
+
if (clickElement) {
|
|
1284
|
+
const click = new PointerEvent('click', event);
|
|
1285
|
+
// The init above copied pointerup's `detail`, which the Pointer Events spec fixes
|
|
1286
|
+
// at 0 - but click is exempt: its detail is the click count, chained here as the
|
|
1287
|
+
// platform chains it (same target, within the double-click window). Overridden
|
|
1288
|
+
// with defineProperty because an event instance used as an init dict cannot have
|
|
1289
|
+
// single fields replaced.
|
|
1290
|
+
const time = performance.now();
|
|
1291
|
+
const last = this._lastClick;
|
|
1292
|
+
const count = last && last.element === clickElement && time - last.time <= CLICK_CHAIN_MS ? last.count + 1 : 1;
|
|
1293
|
+
this._lastClick = { element: clickElement, time, count };
|
|
1294
|
+
Object.defineProperty(click, 'detail', { value: count });
|
|
1295
|
+
clickElement.dispatchEvent(click);
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
|
|
1303
|
+
* box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
|
|
1304
|
+
* canvas and loading bar anchored to it. `:where()` keeps every declaration at zero specificity,
|
|
1305
|
+
* so any page rule - however plain - overrides these defaults.
|
|
1306
|
+
*/
|
|
1307
|
+
const ensureBaseStyles = () => {
|
|
1308
|
+
const id = 'pc-app-styles';
|
|
1309
|
+
if (document.getElementById(id)) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const style = document.createElement('style');
|
|
1313
|
+
style.id = id;
|
|
1314
|
+
style.textContent = ':where(pc-app) { display: block; position: relative; width: 300px; height: 150px; }';
|
|
1315
|
+
document.head.appendChild(style);
|
|
1316
|
+
};
|
|
1317
|
+
/**
|
|
1318
|
+
* The AppElement interface provides properties and methods for manipulating
|
|
1319
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
|
|
1320
|
+
* The AppElement interface also inherits the properties and methods of the
|
|
1321
|
+
* {@link HTMLElement} interface.
|
|
1322
|
+
*
|
|
1323
|
+
* The element is sized like a replaced element such as `<video>`: a block-level box that the
|
|
1324
|
+
* page's CSS controls, 300x150 by default. The application's canvas always fills the element,
|
|
1325
|
+
* and the drawing buffer resolution follows the element's size (capped by `max-pixel-ratio`),
|
|
1326
|
+
* tracked live via a ResizeObserver — so the element can be embedded at any size, resized by
|
|
1327
|
+
* its container, or made fullscreen with ordinary CSS such as `width: 100vw; height: 100dvh`.
|
|
1328
|
+
*
|
|
1329
|
+
* @elementSummary The `<pc-app>` element creates a PlayCanvas application and the canvas it renders
|
|
1330
|
+
* into, and is the root of every scene. It holds the `<pc-asset>`, `<pc-material>`, `<pc-wasm>` and
|
|
1331
|
+
* `<pc-scene>` elements, and the page's CSS sizes it, as it would a `<video>`.
|
|
1332
|
+
*
|
|
1333
|
+
* @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
|
|
1334
|
+
* `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
|
|
1335
|
+
* Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
|
|
1336
|
+
* not bubble.
|
|
1337
|
+
*
|
|
1338
|
+
* @fires {ErrorEvent} error - Fired when the application cannot boot because no graphics device
|
|
1339
|
+
* could be created (for example, a browser with WebGL disabled). `message` names the requested
|
|
1340
|
+
* backends and `error` holds the underlying failure. The element never becomes ready
|
|
1341
|
+
* and `app` stays `null` — listen for this event to show a fallback UI. Removing the element and
|
|
1342
|
+
* re-inserting it retries the boot with its current attributes. Does not bubble.
|
|
1343
|
+
*/
|
|
1344
|
+
class AppElement extends AsyncElement {
|
|
1345
|
+
/**
|
|
1346
|
+
* The canvas element.
|
|
1347
|
+
*/
|
|
1348
|
+
_canvas = null;
|
|
1349
|
+
_alpha = true;
|
|
1350
|
+
_backend = 'webgpu';
|
|
1351
|
+
_antialias = true;
|
|
1352
|
+
_depthBuffer = true;
|
|
1353
|
+
_stencilBuffer = true;
|
|
1354
|
+
_maxPixelRatio = Infinity;
|
|
1355
|
+
_loadingBar = true;
|
|
1356
|
+
/**
|
|
1357
|
+
* Set once the graphics options above have been handed to `createGraphicsDevice`, after which
|
|
1358
|
+
* writing any of them changes nothing. Guards the warning in {@link _warnIfBooted}, and is
|
|
1359
|
+
* cleared on disconnect so a re-connected element boots from its current attributes.
|
|
1360
|
+
*/
|
|
1361
|
+
_optionsLocked = false;
|
|
1362
|
+
_bar = null;
|
|
1363
|
+
/**
|
|
1364
|
+
* Whether the application has created its initial entity hierarchy. Read by EntityElement to
|
|
1365
|
+
* decide whether a newly connected element must create its entity itself or leave it to the
|
|
1366
|
+
* boot sweep.
|
|
1367
|
+
* @internal
|
|
1368
|
+
*/
|
|
1369
|
+
_hierarchyReady = false;
|
|
1370
|
+
/**
|
|
1371
|
+
* Incremented on every connect and disconnect. Boot captures the value on entry and abandons
|
|
1372
|
+
* itself wherever it resumes from an await if the value has moved on — so a boot whose
|
|
1373
|
+
* element was removed cannot complete against a torn-down element, and a boot whose element
|
|
1374
|
+
* was removed and re-inserted (which starts a boot of its own) cannot race the newer one.
|
|
1375
|
+
*/
|
|
1376
|
+
_bootGeneration = 0;
|
|
1377
|
+
/**
|
|
1378
|
+
* The elements backing this application's entities, keyed by the entity itself. Registered
|
|
1379
|
+
* by entity-owning elements at creation (pc-entity, and pc-model for its host) and by
|
|
1380
|
+
* NodeElement at binding, and removed when an entity is destroyed or unbound, this joins
|
|
1381
|
+
* engine scene nodes back to their owning elements by identity - never by name.
|
|
1382
|
+
*/
|
|
1383
|
+
_entityElements = new Map();
|
|
1384
|
+
/**
|
|
1385
|
+
* The pointer-input subsystem: the picker, the canvas handlers, and the synthesized-event
|
|
1386
|
+
* dispatch. The element drives its lifecycle (connect on boot, resize with the drawing
|
|
1387
|
+
* buffer, listener syncs, disconnect on teardown) and hands it the two lookups it needs -
|
|
1388
|
+
* everything else about pointer input lives in the controller.
|
|
1389
|
+
*/
|
|
1390
|
+
_pointer = new PointerController({
|
|
1391
|
+
elementFromNode: (node) => this._entityElements.get(node) ?? null,
|
|
1392
|
+
pointerTargets: () => Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node'))
|
|
1393
|
+
});
|
|
1394
|
+
_app = null;
|
|
1395
|
+
_loadProgress = 0;
|
|
1396
|
+
/**
|
|
1397
|
+
* Tracks the element's box so the drawing buffer and picker follow it. Created per boot once
|
|
1398
|
+
* the application exists, and disconnected on teardown. `null` where ResizeObserver is
|
|
1399
|
+
* unavailable (jsdom), where the boot-time resolution set is the only sizing that happens.
|
|
1400
|
+
*/
|
|
1401
|
+
_resizeObserver = null;
|
|
1402
|
+
/**
|
|
1403
|
+
* The PlayCanvas application instance. `null` until the element is ready, and again once it
|
|
1404
|
+
* has been removed from the document — await {@link whenReady} or the element's `ready()`
|
|
1405
|
+
* promise before accessing it.
|
|
1406
|
+
* @returns The application instance, or `null`.
|
|
1407
|
+
*/
|
|
1408
|
+
get app() {
|
|
1409
|
+
return this._app;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* The asset preload progress of the application, as a fraction from 0 to 1. It is 0 until
|
|
1413
|
+
* preloading begins (and again once the element has been removed from the document), and 1
|
|
1414
|
+
* once preloading has finished — including when there was nothing to preload. Read this to
|
|
1415
|
+
* initialize a loading UI; subsequent updates arrive via the `progress` event.
|
|
1416
|
+
* @returns The preload progress.
|
|
1417
|
+
*/
|
|
1418
|
+
get loadProgress() {
|
|
1419
|
+
return this._loadProgress;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Creates a new AppElement instance.
|
|
1423
|
+
*
|
|
1424
|
+
* @ignore
|
|
1425
|
+
*/
|
|
1426
|
+
constructor() {
|
|
1427
|
+
super();
|
|
1428
|
+
// Track listeners for the synthesized events being added to and removed from descendant
|
|
1429
|
+
// entities. Registered once here rather than on every boot - the sync no-ops while there
|
|
1430
|
+
// is no canvas, and a re-booted element must not stack a second set.
|
|
1431
|
+
SYNTHESIZED_EVENTS.forEach((type) => {
|
|
1432
|
+
this.addEventListener(`${type}:connect`, () => this._pointer.syncListeners());
|
|
1433
|
+
this.addEventListener(`${type}:disconnect`, () => this._pointer.syncListeners());
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
async connectedCallback() {
|
|
1437
|
+
const generation = ++this._bootGeneration;
|
|
1438
|
+
// Installed before the loading bar is created: the bar anchors to this element, which
|
|
1439
|
+
// these styles make a positioned block box
|
|
1440
|
+
ensureBaseStyles();
|
|
1441
|
+
// Created before the first await, so the bar is visible while modules and the graphics
|
|
1442
|
+
// device are created, and exists before any disconnect could need to clean it up
|
|
1443
|
+
if (this._loadingBar && !this._bar) {
|
|
1444
|
+
this._bar = new LoadingBar(this);
|
|
1445
|
+
}
|
|
1446
|
+
// Upgrade the subtree before reading anything out of it. A subtree cloned from a
|
|
1447
|
+
// <template> arrives entirely unupgraded - template content lives in an inert document,
|
|
1448
|
+
// where custom element definitions are never looked up - and appending the clone upgrades
|
|
1449
|
+
// its elements in tree order, this one before its descendants. The module query below would
|
|
1450
|
+
// otherwise find plain HTMLElements with no _getLoadPromise to call, and the boot would die
|
|
1451
|
+
// there, leaving the element permanently unready: no canvas, no entities, no application.
|
|
1452
|
+
//
|
|
1453
|
+
// Upgrading is the fix here rather than skipping whatever has not upgraded, because a
|
|
1454
|
+
// <pc-wasm> is the one child that nothing else ever builds on its own behalf - skipping
|
|
1455
|
+
// it would drop the wasm module the app asked for, silently and only for cloned apps.
|
|
1456
|
+
// Upgrading runs each descendant's connectedCallback synchronously, a few lines earlier
|
|
1457
|
+
// than the parser's path runs them but into the same state they see there: no application
|
|
1458
|
+
// yet and _hierarchyReady false, so they defer to the sweeps below. A descendant that
|
|
1459
|
+
// disconnects this element from there is caught by the generation check after the await,
|
|
1460
|
+
// as any other disconnect is. An already-upgraded subtree - every other insertion path -
|
|
1461
|
+
// is left completely untouched.
|
|
1462
|
+
customElements.upgrade(this);
|
|
1463
|
+
// Get all pc-wasm elements that are direct children of the pc-app element
|
|
1464
|
+
const wasmElements = this.querySelectorAll(':scope > pc-wasm');
|
|
1465
|
+
// Wait for all modules to load
|
|
1466
|
+
await Promise.all(Array.from(wasmElements).map((element) => element._getLoadPromise()));
|
|
1467
|
+
// The element may have been removed while the modules loaded. Nothing beyond the loading
|
|
1468
|
+
// bar exists yet, and disconnectedCallback has already destroyed that.
|
|
1469
|
+
if (generation !== this._bootGeneration) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
// Create and append the canvas, filling the element's content box - the page sizes the
|
|
1473
|
+
// element, and everything else follows. touch-action: none keeps touch drags driving the
|
|
1474
|
+
// engine's input handlers instead of scrolling the page.
|
|
1475
|
+
this._canvas = document.createElement('canvas');
|
|
1476
|
+
this._canvas.style.cssText = 'display: block; width: 100%; height: 100%; touch-action: none;';
|
|
1477
|
+
this.appendChild(this._canvas);
|
|
1478
|
+
// Configure device types based on backend selection
|
|
1479
|
+
const backendToDeviceTypes = {
|
|
1480
|
+
webgpu: ['webgpu', 'webgl2'], // fallback to webgl2 if webgpu not available
|
|
1481
|
+
webgl2: ['webgl2'],
|
|
1482
|
+
null: ['null']
|
|
1483
|
+
};
|
|
1484
|
+
const deviceTypes = backendToDeviceTypes[this._backend] || [];
|
|
1485
|
+
this._optionsLocked = true;
|
|
1486
|
+
// createGraphicsDevice appends its final null-device fallback to the array in place, so
|
|
1487
|
+
// the requested list is captured now for the failure message.
|
|
1488
|
+
const requested = deviceTypes.join(', ');
|
|
1489
|
+
let device;
|
|
1490
|
+
try {
|
|
1491
|
+
device = await createGraphicsDevice(this._canvas, {
|
|
1492
|
+
// @ts-ignore - alpha needs to be documented
|
|
1493
|
+
alpha: this._alpha,
|
|
1494
|
+
antialias: this._antialias,
|
|
1495
|
+
depth: this._depthBuffer,
|
|
1496
|
+
deviceTypes: deviceTypes,
|
|
1497
|
+
stencil: this._stencilBuffer
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
catch (error) {
|
|
1501
|
+
// The element may have been removed while device creation was failing. The teardown
|
|
1502
|
+
// has already cleaned up, and the failure belongs to a boot that no longer owns the
|
|
1503
|
+
// element.
|
|
1504
|
+
if (generation !== this._bootGeneration) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
// Return the element to its pre-boot state - no dead canvas, no loading bar stuck at
|
|
1508
|
+
// zero - before announcing the failure. Readiness deliberately stays pending: nothing
|
|
1509
|
+
// it would announce (the app, the entity hierarchy) exists, so a device-less element
|
|
1510
|
+
// joins the documented never-ready cases and the failure surfaces through the error
|
|
1511
|
+
// event instead.
|
|
1512
|
+
if (this._canvas && this.contains(this._canvas)) {
|
|
1513
|
+
this.removeChild(this._canvas);
|
|
1514
|
+
}
|
|
1515
|
+
this._canvas = null;
|
|
1516
|
+
this._bar?.destroy();
|
|
1517
|
+
this._bar = null;
|
|
1518
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1519
|
+
const message = `pc-app failed to create a graphics device (${requested}) - ${reason}`;
|
|
1520
|
+
console.error(message, error);
|
|
1521
|
+
this.dispatchEvent(new ErrorEvent('error', { message, error }));
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
// The element may have been removed while the device was created. disconnectedCallback
|
|
1525
|
+
// has already cleaned up the canvas; the device was created inside the await, so it is
|
|
1526
|
+
// this boot's to release.
|
|
1527
|
+
if (generation !== this._bootGeneration) {
|
|
1528
|
+
device.destroy();
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
// Assigned rather than resolved to a number here: the engine caps against the live
|
|
1532
|
+
// window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
|
|
1533
|
+
// display when a window moves between monitors of differing density.
|
|
1534
|
+
device.maxPixelRatio = this._maxPixelRatio;
|
|
1535
|
+
const createOptions = new AppOptions();
|
|
1536
|
+
createOptions.graphicsDevice = device;
|
|
1537
|
+
createOptions.keyboard = new Keyboard(window);
|
|
1538
|
+
createOptions.mouse = new Mouse(this._canvas);
|
|
1539
|
+
createOptions.elementInput = new ElementInput(this._canvas, {
|
|
1540
|
+
useMouse: true,
|
|
1541
|
+
useTouch: true
|
|
1542
|
+
});
|
|
1543
|
+
createOptions.componentSystems = [
|
|
1544
|
+
AnimComponentSystem,
|
|
1545
|
+
AnimationComponentSystem,
|
|
1546
|
+
AudioListenerComponentSystem,
|
|
1547
|
+
ButtonComponentSystem,
|
|
1548
|
+
CameraComponentSystem,
|
|
1549
|
+
CollisionComponentSystem,
|
|
1550
|
+
ElementComponentSystem,
|
|
1551
|
+
GSplatComponentSystem,
|
|
1552
|
+
JointComponentSystem,
|
|
1553
|
+
LayoutChildComponentSystem,
|
|
1554
|
+
LayoutGroupComponentSystem,
|
|
1555
|
+
LightComponentSystem,
|
|
1556
|
+
ModelComponentSystem,
|
|
1557
|
+
ParticleSystemComponentSystem,
|
|
1558
|
+
RenderComponentSystem,
|
|
1559
|
+
RigidBodyComponentSystem,
|
|
1560
|
+
ScreenComponentSystem,
|
|
1561
|
+
ScriptComponentSystem,
|
|
1562
|
+
ScrollbarComponentSystem,
|
|
1563
|
+
ScrollViewComponentSystem,
|
|
1564
|
+
SoundComponentSystem,
|
|
1565
|
+
SpriteComponentSystem,
|
|
1566
|
+
ZoneComponentSystem
|
|
1567
|
+
];
|
|
1568
|
+
createOptions.resourceHandlers = [
|
|
1569
|
+
AnimClipHandler,
|
|
1570
|
+
AnimationHandler,
|
|
1571
|
+
AnimStateGraphHandler,
|
|
1572
|
+
AudioHandler,
|
|
1573
|
+
BinaryHandler,
|
|
1574
|
+
CssHandler,
|
|
1575
|
+
ContainerHandler,
|
|
1576
|
+
CubemapHandler,
|
|
1577
|
+
FolderHandler,
|
|
1578
|
+
FontHandler,
|
|
1579
|
+
GSplatHandler,
|
|
1580
|
+
HierarchyHandler,
|
|
1581
|
+
HtmlHandler,
|
|
1582
|
+
JsonHandler,
|
|
1583
|
+
MaterialHandler,
|
|
1584
|
+
ModelHandler,
|
|
1585
|
+
RenderHandler,
|
|
1586
|
+
ScriptHandler,
|
|
1587
|
+
SceneHandler,
|
|
1588
|
+
ShaderHandler,
|
|
1589
|
+
SpriteHandler,
|
|
1590
|
+
TemplateHandler,
|
|
1591
|
+
TextHandler,
|
|
1592
|
+
TextureAtlasHandler,
|
|
1593
|
+
TextureHandler
|
|
1594
|
+
];
|
|
1595
|
+
createOptions.soundManager = new SoundManager();
|
|
1596
|
+
createOptions.lightmapper = Lightmapper;
|
|
1597
|
+
createOptions.batchManager = BatchManager;
|
|
1598
|
+
createOptions.xr = XrManager;
|
|
1599
|
+
const app = new AppBase(this._canvas);
|
|
1600
|
+
this._app = app;
|
|
1601
|
+
app.init(createOptions);
|
|
1602
|
+
// FILLMODE_NONE leaves the canvas's CSS sizing alone (the engine's other fill modes
|
|
1603
|
+
// stamp window-derived pixel sizes onto it); RESOLUTION_AUTO sizes the drawing buffer
|
|
1604
|
+
// from the canvas's client size
|
|
1605
|
+
app.setCanvasFillMode(FILLMODE_NONE);
|
|
1606
|
+
app.setCanvasResolution(RESOLUTION_AUTO);
|
|
1607
|
+
this._pointer.connect(app, this._canvas);
|
|
1608
|
+
// Track the element's box rather than the window: containers resize without any window
|
|
1609
|
+
// event (splitter drags, flex reflow, animations). Guarded because jsdom has no
|
|
1610
|
+
// ResizeObserver - there, the resolution set above is the only sizing that happens.
|
|
1611
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
1612
|
+
this._resizeObserver = new ResizeObserver(() => this._syncCanvasSize());
|
|
1613
|
+
this._resizeObserver.observe(this);
|
|
1614
|
+
}
|
|
1615
|
+
// Get all pc-asset elements that are direct children of the pc-app element
|
|
1616
|
+
const assetElements = this.querySelectorAll(':scope > pc-asset');
|
|
1617
|
+
for (const assetElement of Array.from(assetElements)) {
|
|
1618
|
+
assetElement._createAsset();
|
|
1619
|
+
const asset = assetElement.asset;
|
|
1620
|
+
if (asset) {
|
|
1621
|
+
app.assets.add(asset);
|
|
1622
|
+
// Adding a fileless asset (one built purely from data, such as a sprite)
|
|
1623
|
+
// completes it synchronously, dispatching the element's load event - whose
|
|
1624
|
+
// listeners may have removed this element. Stop before the next addition
|
|
1625
|
+
// reaches the destroyed registry, and before orphan entities are created.
|
|
1626
|
+
if (generation !== this._bootGeneration) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
// Get all pc-material elements that are direct children of the pc-app element
|
|
1632
|
+
const materialElements = this.querySelectorAll(':scope > pc-material');
|
|
1633
|
+
Array.from(materialElements).forEach((materialElement) => {
|
|
1634
|
+
materialElement._createMaterial();
|
|
1635
|
+
});
|
|
1636
|
+
// Create all entities. pc-model joins the sweep because it owns a host entity of its
|
|
1637
|
+
// own; its instantiated content arrives later, beneath that host.
|
|
1638
|
+
const ownerElements = this.querySelectorAll('pc-entity, pc-model');
|
|
1639
|
+
Array.from(ownerElements).forEach((ownerElement) => {
|
|
1640
|
+
ownerElement._createEntity(app);
|
|
1641
|
+
});
|
|
1642
|
+
// Build hierarchy
|
|
1643
|
+
ownerElements.forEach((ownerElement) => {
|
|
1644
|
+
ownerElement._buildHierarchy(app);
|
|
1645
|
+
});
|
|
1646
|
+
// Building the hierarchy dispatched each entity's ready event synchronously, and a
|
|
1647
|
+
// listener may have removed the element. The sweep itself degrades safely - destroying
|
|
1648
|
+
// the application nulls every element's entity, so the remaining builds no-op - but the
|
|
1649
|
+
// teardown's reset must not be overwritten here.
|
|
1650
|
+
if (generation !== this._bootGeneration) {
|
|
1774
1651
|
return;
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
//
|
|
1778
|
-
|
|
1779
|
-
//
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1652
|
+
}
|
|
1653
|
+
this._hierarchyReady = true;
|
|
1654
|
+
// Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
|
|
1655
|
+
// listener must be attached before preload() is called: an asset that is already loaded
|
|
1656
|
+
// ticks synchronously inside it.
|
|
1657
|
+
const total = app.assets.list({ preload: true }).length;
|
|
1658
|
+
let loaded = 0;
|
|
1659
|
+
const onPreloadProgress = () => {
|
|
1660
|
+
loaded += 1;
|
|
1661
|
+
this._loadProgress = loaded / total;
|
|
1662
|
+
this._bar?.progress(loaded, total);
|
|
1663
|
+
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded, total }));
|
|
1664
|
+
};
|
|
1665
|
+
app.on('preload:progress', onPreloadProgress);
|
|
1666
|
+
this._loadProgress = total === 0 ? 1 : 0;
|
|
1667
|
+
this._bar?.progress(0, total);
|
|
1668
|
+
this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
|
|
1669
|
+
// The progress dispatch above ran listeners synchronously, and one may have removed the
|
|
1670
|
+
// element. The application is already destroyed - it must not be asked to preload.
|
|
1671
|
+
if (generation !== this._bootGeneration) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
// Load assets before starting the application
|
|
1675
|
+
app.preload(() => {
|
|
1676
|
+
// The element may have been removed while assets loaded. The application is already
|
|
1677
|
+
// destroyed, so it must not be started — and readiness must not be signaled for a
|
|
1678
|
+
// boot that no longer owns the element.
|
|
1679
|
+
if (generation !== this._bootGeneration) {
|
|
1680
|
+
return;
|
|
1786
1681
|
}
|
|
1682
|
+
// Scope the counter to this preload pass, so a later app.preload() call by user code
|
|
1683
|
+
// cannot push `loaded` past `total`
|
|
1684
|
+
app.off('preload:progress', onPreloadProgress);
|
|
1685
|
+
this._loadProgress = 1;
|
|
1686
|
+
// Start the application
|
|
1687
|
+
app.start();
|
|
1688
|
+
// Dismiss the bar only once a frame has actually rendered; ready fires before the
|
|
1689
|
+
// first rAF tick
|
|
1690
|
+
app.once('frameend', () => this._bar?.complete());
|
|
1691
|
+
this._onReady();
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
disconnectedCallback() {
|
|
1695
|
+
// Invalidate any boot still in flight, so it abandons itself when it next resumes
|
|
1696
|
+
// instead of completing against a torn-down element.
|
|
1697
|
+
this._bootGeneration++;
|
|
1698
|
+
this._optionsLocked = false;
|
|
1699
|
+
this._pointer.disconnect();
|
|
1700
|
+
// Clean up the application. Destroying it destroys every entity, whose destroy hooks
|
|
1701
|
+
// unregister them - clear() covers any entity the engine no longer reached.
|
|
1702
|
+
if (this._app) {
|
|
1703
|
+
this._app.destroy();
|
|
1704
|
+
this._app = null;
|
|
1787
1705
|
}
|
|
1788
|
-
|
|
1789
|
-
this.
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1706
|
+
this._entityElements.clear();
|
|
1707
|
+
this._loadProgress = 0;
|
|
1708
|
+
this._bar?.destroy();
|
|
1709
|
+
this._bar = null;
|
|
1710
|
+
// Return the element to its pre-boot state, so re-inserting it boots afresh: descendants
|
|
1711
|
+
// must neither see a hierarchy that no longer exists nor resume against a readiness that
|
|
1712
|
+
// no longer holds.
|
|
1713
|
+
this._hierarchyReady = false;
|
|
1714
|
+
this._resetReady();
|
|
1715
|
+
// Stop tracking the element's size
|
|
1716
|
+
this._resizeObserver?.disconnect();
|
|
1717
|
+
this._resizeObserver = null;
|
|
1718
|
+
// Remove the canvas
|
|
1719
|
+
if (this._canvas && this.contains(this._canvas)) {
|
|
1720
|
+
this.removeChild(this._canvas);
|
|
1721
|
+
this._canvas = null;
|
|
1793
1722
|
}
|
|
1794
1723
|
}
|
|
1795
1724
|
/**
|
|
1796
|
-
*
|
|
1797
|
-
*
|
|
1798
|
-
*
|
|
1799
|
-
*
|
|
1800
|
-
* @param step - The dispatch work to run once every earlier step has finished.
|
|
1725
|
+
* Syncs the drawing buffer and the picker to the canvas's current CSS size. The picker must
|
|
1726
|
+
* track the buffer, or picks would land at stale coordinates after a resize. Skipped while
|
|
1727
|
+
* an XR session presents - the session owns the buffer size.
|
|
1801
1728
|
*/
|
|
1802
|
-
|
|
1803
|
-
this.
|
|
1804
|
-
console.error(error);
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
_onPointerDown(event) {
|
|
1808
|
-
if (!this._picker || !this.app)
|
|
1729
|
+
_syncCanvasSize() {
|
|
1730
|
+
if (!this.app || this.app.xr?.active) {
|
|
1809
1731
|
return;
|
|
1810
|
-
// Picks stay concurrent - only the dispatch of the results is serialized
|
|
1811
|
-
const pick = this._pickNode(event);
|
|
1812
|
-
// A click concludes on the matching pointerup, which needs to know what the press
|
|
1813
|
-
// picked. Primary button only - the only button a click can conclude from - and only
|
|
1814
|
-
// while click is listened for, since it is the click mapping that keeps the pointerup
|
|
1815
|
-
// and pointercancel listeners attached to clean the entry up again.
|
|
1816
|
-
if (this._clickListened && event.button === 0) {
|
|
1817
|
-
this._downPicks.set(event.pointerId, pick);
|
|
1818
1732
|
}
|
|
1819
|
-
this.
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
return; // the element disconnected while the pick was in flight
|
|
1823
|
-
const entityElement = this._elementWithListener(node, 'pointerdown');
|
|
1824
|
-
if (entityElement) {
|
|
1825
|
-
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
1826
|
-
}
|
|
1827
|
-
});
|
|
1733
|
+
this.app.updateCanvasSize();
|
|
1734
|
+
const { width, height } = this.app.graphicsDevice;
|
|
1735
|
+
this._pointer.resize(width, height);
|
|
1828
1736
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
if (!this._picker)
|
|
1840
|
-
return; // the element disconnected while the pick was in flight
|
|
1841
|
-
const entityElement = this._elementWithListener(node, 'pointerup');
|
|
1842
|
-
if (entityElement) {
|
|
1843
|
-
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
1844
|
-
}
|
|
1845
|
-
});
|
|
1846
|
-
// A click fires where the DOM fires it: at the nearest common inclusive ancestor of
|
|
1847
|
-
// what the press and the release picked, for the primary button only. Appended after
|
|
1848
|
-
// the release's own step, so it dispatches after the pointerup that concludes it.
|
|
1849
|
-
if (!downPick || event.button !== 0)
|
|
1850
|
-
return;
|
|
1851
|
-
this._chainDispatch(async () => {
|
|
1852
|
-
// A rejected pick was already reported by the press or release step that awaited it;
|
|
1853
|
-
// here it just means no click can conclude.
|
|
1854
|
-
const picked = await Promise.all([downPick, pick]).catch(() => null);
|
|
1855
|
-
if (!picked || !this._picker)
|
|
1856
|
-
return;
|
|
1857
|
-
const [downNode, upNode] = picked;
|
|
1858
|
-
const clickElement = this._elementWithListener(commonAncestor(downNode, upNode), 'click');
|
|
1859
|
-
if (clickElement) {
|
|
1860
|
-
const click = new PointerEvent('click', event);
|
|
1861
|
-
// The init above copied pointerup's `detail`, which the Pointer Events spec fixes
|
|
1862
|
-
// at 0 - but click is exempt: its detail is the click count, chained here as the
|
|
1863
|
-
// platform chains it (same target, within the double-click window). Overridden
|
|
1864
|
-
// with defineProperty because an event instance used as an init dict cannot have
|
|
1865
|
-
// single fields replaced.
|
|
1866
|
-
const time = performance.now();
|
|
1867
|
-
const last = this._lastClick;
|
|
1868
|
-
const count = last && last.element === clickElement && time - last.time <= CLICK_CHAIN_MS ? last.count + 1 : 1;
|
|
1869
|
-
this._lastClick = { element: clickElement, time, count };
|
|
1870
|
-
Object.defineProperty(click, 'detail', { value: count });
|
|
1871
|
-
clickElement.dispatchEvent(click);
|
|
1872
|
-
}
|
|
1873
|
-
});
|
|
1737
|
+
/**
|
|
1738
|
+
* Registers the element that fronts an entity. Called by EntityElement when it creates its
|
|
1739
|
+
* entity, and by NodeElement when it binds one.
|
|
1740
|
+
*
|
|
1741
|
+
* @param entity - The entity.
|
|
1742
|
+
* @param element - The element that fronts it.
|
|
1743
|
+
* @internal
|
|
1744
|
+
*/
|
|
1745
|
+
_registerEntityElement(entity, element) {
|
|
1746
|
+
this._entityElements.set(entity, element);
|
|
1874
1747
|
}
|
|
1875
1748
|
/**
|
|
1876
|
-
*
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
1879
|
-
*
|
|
1880
|
-
* must not detach a listener another type still uses. Re-attaching an attached listener is
|
|
1881
|
-
* a no-op by EventTarget semantics, so no attach state is kept.
|
|
1749
|
+
* Removes the registration for a destroyed entity. Called by EntityElement.
|
|
1750
|
+
*
|
|
1751
|
+
* @param entity - The entity.
|
|
1752
|
+
* @internal
|
|
1882
1753
|
*/
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
return;
|
|
1898
|
-
if (needed.has(canvasType)) {
|
|
1899
|
-
canvas.addEventListener(canvasType, handler);
|
|
1900
|
-
}
|
|
1901
|
-
else {
|
|
1902
|
-
canvas.removeEventListener(canvasType, handler);
|
|
1903
|
-
}
|
|
1904
|
-
});
|
|
1754
|
+
_unregisterEntityElement(entity) {
|
|
1755
|
+
this._entityElements.delete(entity);
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Returns the `<pc-entity>`, `<pc-model>` or `<pc-node>` element whose backing entity is
|
|
1759
|
+
* `entity`, or `null` if the entity is not fronted by an element of this application - for
|
|
1760
|
+
* example, an unbound node inside a model's instantiated hierarchy, or an entity created
|
|
1761
|
+
* through the engine API.
|
|
1762
|
+
*
|
|
1763
|
+
* @param entity - The entity to look up.
|
|
1764
|
+
* @returns The element fronting the entity, or `null`.
|
|
1765
|
+
*/
|
|
1766
|
+
elementFromEntity(entity) {
|
|
1767
|
+
return this._entityElements.get(entity) ?? null;
|
|
1905
1768
|
}
|
|
1906
1769
|
/**
|
|
1907
1770
|
* Warns that a graphics option was written too late to have any effect. These options are read
|
|
@@ -3277,31 +3140,133 @@ class AssetElement extends AsyncElement {
|
|
|
3277
3140
|
else {
|
|
3278
3141
|
this.srgb = null;
|
|
3279
3142
|
}
|
|
3280
|
-
break;
|
|
3143
|
+
break;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
customElements.define('pc-asset', AssetElement);
|
|
3148
|
+
/**
|
|
3149
|
+
* Resolves an asset reference for use: {@link AssetElement.get}, plus starting the load of a
|
|
3150
|
+
* registered asset that has not begun one - a `lazy` asset. Every element that consumes assets
|
|
3151
|
+
* resolves its references here, which is what makes `lazy` mean load on first use without any
|
|
3152
|
+
* consumer having to remember the load. The load is asynchronous - callers observe the asset's
|
|
3153
|
+
* `load` event for the resource.
|
|
3154
|
+
*
|
|
3155
|
+
* @param id - The `id` of the `<pc-asset>` element.
|
|
3156
|
+
* @returns The asset, or `undefined`.
|
|
3157
|
+
* @internal
|
|
3158
|
+
*/
|
|
3159
|
+
const useAsset = (id) => {
|
|
3160
|
+
const asset = AssetElement.get(id);
|
|
3161
|
+
// load() ignores an asset that is already loaded or loading, so repeated resolution
|
|
3162
|
+
// costs nothing.
|
|
3163
|
+
if (asset) {
|
|
3164
|
+
asset.registry?.load(asset);
|
|
3165
|
+
}
|
|
3166
|
+
return asset;
|
|
3167
|
+
};
|
|
3168
|
+
|
|
3169
|
+
/**
|
|
3170
|
+
* Watches the asset currently selected by an element.
|
|
3171
|
+
*
|
|
3172
|
+
* An element can change its asset while the old one is still loading, or disconnect before the
|
|
3173
|
+
* load finishes. This class makes sure callbacks from those older loads do nothing. Calling
|
|
3174
|
+
* {@link bind} stops watching the previous asset and starts watching the new one. Calling
|
|
3175
|
+
* {@link cancel} stops watching altogether.
|
|
3176
|
+
*
|
|
3177
|
+
* Asset lookup goes through {@link useAsset}, so selecting a lazy asset starts its load. The
|
|
3178
|
+
* caller still decides what to do with the result, such as creating scene content, reporting an
|
|
3179
|
+
* error, or marking an element ready.
|
|
3180
|
+
*
|
|
3181
|
+
* Reuse one `AssetBinding` for each asset-valued property throughout the element's lifetime. It
|
|
3182
|
+
* is safe for a callback to call `bind` again: the new asset remains active after the callback
|
|
3183
|
+
* returns.
|
|
3184
|
+
*
|
|
3185
|
+
* @internal
|
|
3186
|
+
*/
|
|
3187
|
+
class AssetBinding {
|
|
3188
|
+
/**
|
|
3189
|
+
* Each bind or cancel gets a new number. Event handlers remember the number they were created
|
|
3190
|
+
* with and return if it is no longer current. Old listeners are normally removed as well, but
|
|
3191
|
+
* this check also protects against an event that was already in progress when removal
|
|
3192
|
+
* happened.
|
|
3193
|
+
*/
|
|
3194
|
+
_generation = 0;
|
|
3195
|
+
_loadHandle = null;
|
|
3196
|
+
_errorHandle = null;
|
|
3197
|
+
_detach() {
|
|
3198
|
+
this._loadHandle?.off();
|
|
3199
|
+
this._loadHandle = null;
|
|
3200
|
+
this._errorHandle?.off();
|
|
3201
|
+
this._errorHandle = null;
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Stops watching the current asset and prevents its callbacks from running. The binding can
|
|
3205
|
+
* be used again by calling {@link bind}.
|
|
3206
|
+
*/
|
|
3207
|
+
cancel() {
|
|
3208
|
+
this._generation++;
|
|
3209
|
+
this._detach();
|
|
3210
|
+
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Starts watching the asset registered under `id` and stops watching the previous one. A
|
|
3213
|
+
* missing `id` still clears the previous binding. Looking up a lazy asset starts its load.
|
|
3214
|
+
*
|
|
3215
|
+
* If the asset has already loaded successfully, `load` runs before this method returns. An
|
|
3216
|
+
* earlier failure calls `error` immediately when that callback is provided; without one, the
|
|
3217
|
+
* binding waits for a later successful reload. Assets still loading are watched for the same
|
|
3218
|
+
* two outcomes. Once an event is handled, both listeners are removed.
|
|
3219
|
+
*
|
|
3220
|
+
* @param id - The `id` of the `<pc-asset>` element to bind to.
|
|
3221
|
+
* @param callbacks - Functions to call when loading succeeds or fails.
|
|
3222
|
+
* @returns The selected asset, or `undefined` if no asset has this `id`. The caller decides
|
|
3223
|
+
* how to handle a missing asset.
|
|
3224
|
+
*/
|
|
3225
|
+
bind(id, callbacks) {
|
|
3226
|
+
const generation = ++this._generation;
|
|
3227
|
+
this._detach();
|
|
3228
|
+
const asset = useAsset(id);
|
|
3229
|
+
if (!asset) {
|
|
3230
|
+
return undefined;
|
|
3231
|
+
}
|
|
3232
|
+
const { error } = callbacks;
|
|
3233
|
+
if (asset.loaded) {
|
|
3234
|
+
// PlayCanvas sets `loaded` after both success and failure, so a resource must also be
|
|
3235
|
+
// present before this counts as success. Use `!= null` deliberately: `undefined` and
|
|
3236
|
+
// `null` both mean there is no resource, while a valid resource can still be falsy
|
|
3237
|
+
// (for example, an empty text file produces '').
|
|
3238
|
+
if (asset.resource != null) {
|
|
3239
|
+
callbacks.load(asset);
|
|
3240
|
+
return asset;
|
|
3241
|
+
}
|
|
3242
|
+
if (error) {
|
|
3243
|
+
error(`asset '${id}' failed to load`);
|
|
3244
|
+
return asset;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
// Old listeners are normally removed by bind or cancel. The number check is a second
|
|
3248
|
+
// safeguard for a late event. Check it before _detach so an old callback cannot remove
|
|
3249
|
+
// the listeners for the current asset. Whichever current event runs first removes both
|
|
3250
|
+
// listeners.
|
|
3251
|
+
this._loadHandle = asset.once('load', () => {
|
|
3252
|
+
if (generation !== this._generation) {
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
this._detach();
|
|
3256
|
+
callbacks.load(asset);
|
|
3257
|
+
});
|
|
3258
|
+
if (error) {
|
|
3259
|
+
this._errorHandle = asset.once('error', (err) => {
|
|
3260
|
+
if (generation !== this._generation) {
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
this._detach();
|
|
3264
|
+
error(err);
|
|
3265
|
+
});
|
|
3281
3266
|
}
|
|
3267
|
+
return asset;
|
|
3282
3268
|
}
|
|
3283
3269
|
}
|
|
3284
|
-
customElements.define('pc-asset', AssetElement);
|
|
3285
|
-
/**
|
|
3286
|
-
* Resolves an asset reference for use: {@link AssetElement.get}, plus starting the load of a
|
|
3287
|
-
* registered asset that has not begun one - a `lazy` asset. Every element that consumes assets
|
|
3288
|
-
* resolves its references here, which is what makes `lazy` mean load on first use without any
|
|
3289
|
-
* consumer having to remember the load. The load is asynchronous - callers observe the asset's
|
|
3290
|
-
* `load` event for the resource.
|
|
3291
|
-
*
|
|
3292
|
-
* @param id - The `id` of the `<pc-asset>` element.
|
|
3293
|
-
* @returns The asset, or `undefined`.
|
|
3294
|
-
* @internal
|
|
3295
|
-
*/
|
|
3296
|
-
const useAsset = (id) => {
|
|
3297
|
-
const asset = AssetElement.get(id);
|
|
3298
|
-
// load() ignores an asset that is already loaded or loading, so repeated resolution
|
|
3299
|
-
// costs nothing.
|
|
3300
|
-
if (asset) {
|
|
3301
|
-
asset.registry?.load(asset);
|
|
3302
|
-
}
|
|
3303
|
-
return asset;
|
|
3304
|
-
};
|
|
3305
3270
|
|
|
3306
3271
|
/**
|
|
3307
3272
|
* Formats one line of the printable hierarchy: the node's name, an `[index]` marker when the
|
|
@@ -3403,18 +3368,18 @@ class ModelElement extends EntityOwnerElement {
|
|
|
3403
3368
|
_contentEntity = null;
|
|
3404
3369
|
/**
|
|
3405
3370
|
* Incremented on every new load, on disconnect, and when the host entity dies, and captured
|
|
3406
|
-
* by a load when it starts. A load that resumes from an await
|
|
3407
|
-
*
|
|
3408
|
-
*
|
|
3371
|
+
* by a load when it starts. A load that resumes from an await abandons itself if the value
|
|
3372
|
+
* has moved on, so a superseded load can neither instantiate a second content root nor
|
|
3373
|
+
* parent one under a host a newer cycle has already replaced. The asset subscription itself
|
|
3374
|
+
* is guarded by the binding below.
|
|
3409
3375
|
*/
|
|
3410
3376
|
_loadGeneration = 0;
|
|
3411
3377
|
/**
|
|
3412
|
-
* The
|
|
3413
|
-
*
|
|
3414
|
-
*
|
|
3378
|
+
* The subscription to the current container asset while it is loading. Whatever supersedes
|
|
3379
|
+
* the load — a newer load, a disconnect, the host dying — cancels it, so the asset settling
|
|
3380
|
+
* later cannot deliver to a load that no longer owns the element.
|
|
3415
3381
|
*/
|
|
3416
|
-
|
|
3417
|
-
_errorHandle = null;
|
|
3382
|
+
_binding = new AssetBinding();
|
|
3418
3383
|
/**
|
|
3419
3384
|
* The root entity of the instantiated model content, parented beneath the host entity.
|
|
3420
3385
|
* `null` until the container asset has loaded and been instantiated, after a failed load,
|
|
@@ -3505,7 +3470,7 @@ class ModelElement extends EntityOwnerElement {
|
|
|
3505
3470
|
// resets the element. The generation guard comes first so a load suspended on an await
|
|
3506
3471
|
// cannot resume against the torn-down element.
|
|
3507
3472
|
this._loadGeneration++;
|
|
3508
|
-
this.
|
|
3473
|
+
this._binding.cancel();
|
|
3509
3474
|
this._entity?.destroy();
|
|
3510
3475
|
}
|
|
3511
3476
|
/**
|
|
@@ -3524,16 +3489,10 @@ class ModelElement extends EntityOwnerElement {
|
|
|
3524
3489
|
*/
|
|
3525
3490
|
_onEntityDestroy(entity) {
|
|
3526
3491
|
this._loadGeneration++;
|
|
3527
|
-
this.
|
|
3492
|
+
this._binding.cancel();
|
|
3528
3493
|
this._contentEntity = null;
|
|
3529
3494
|
super._onEntityDestroy(entity);
|
|
3530
3495
|
}
|
|
3531
|
-
_detachLoadHandlers() {
|
|
3532
|
-
this._loadHandle?.off();
|
|
3533
|
-
this._loadHandle = null;
|
|
3534
|
-
this._errorHandle?.off();
|
|
3535
|
-
this._errorHandle = null;
|
|
3536
|
-
}
|
|
3537
3496
|
/**
|
|
3538
3497
|
* Resolves readiness and dispatches the `load` event. Called once the instantiated content
|
|
3539
3498
|
* has been parented beneath the host — the host itself is already in the scene graph by
|
|
@@ -3560,7 +3519,7 @@ class ModelElement extends EntityOwnerElement {
|
|
|
3560
3519
|
this._destroyContent();
|
|
3561
3520
|
// Supersede any load already in flight - only the newest load may instantiate
|
|
3562
3521
|
const generation = ++this._loadGeneration;
|
|
3563
|
-
this.
|
|
3522
|
+
this._binding.cancel();
|
|
3564
3523
|
// Re-arm readiness so a waiter obtained after an asset change resolves against the new
|
|
3565
3524
|
// content. A no-op on first connection, where readiness is still pending.
|
|
3566
3525
|
this._resetReady();
|
|
@@ -3586,39 +3545,23 @@ class ModelElement extends EntityOwnerElement {
|
|
|
3586
3545
|
this._onReady();
|
|
3587
3546
|
return;
|
|
3588
3547
|
}
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
return;
|
|
3595
|
-
}
|
|
3596
|
-
if (asset.loaded) {
|
|
3597
|
-
this._instantiate(asset.resource);
|
|
3598
|
-
}
|
|
3599
|
-
else {
|
|
3600
|
-
// The generation is re-checked even though a superseded handler is detached: the
|
|
3601
|
-
// detach relies on how the engine's event emitter treats removal, while the check
|
|
3602
|
-
// holds on its own. Whichever of load/error fires first detaches the other.
|
|
3603
|
-
this._loadHandle = asset.once('load', () => {
|
|
3604
|
-
this._detachLoadHandlers();
|
|
3605
|
-
if (generation !== this._loadGeneration) {
|
|
3606
|
-
return;
|
|
3607
|
-
}
|
|
3608
|
-
this._instantiate(asset.resource);
|
|
3609
|
-
});
|
|
3610
|
-
this._errorHandle = asset.once('error', (err) => {
|
|
3611
|
-
this._detachLoadHandlers();
|
|
3612
|
-
if (generation !== this._loadGeneration) {
|
|
3613
|
-
return;
|
|
3614
|
-
}
|
|
3548
|
+
// Every path that moves _loadGeneration also rebinds or cancels the binding, so a
|
|
3549
|
+
// delivery below is always current - no generation re-check needed in the callbacks.
|
|
3550
|
+
const asset = this._binding.bind(this._asset, {
|
|
3551
|
+
load: ({ resource }) => this._instantiate(resource),
|
|
3552
|
+
error: (err) => {
|
|
3615
3553
|
// A failed load settles readiness with a null contentEntity, mirroring pc-asset:
|
|
3616
3554
|
// readiness means the load settled, not that it succeeded.
|
|
3617
3555
|
this.dispatchEvent(new ErrorEvent('error', {
|
|
3618
3556
|
message: err instanceof Error ? err.message : String(err)
|
|
3619
3557
|
}));
|
|
3620
3558
|
this._onReady();
|
|
3621
|
-
}
|
|
3559
|
+
}
|
|
3560
|
+
});
|
|
3561
|
+
if (!asset) {
|
|
3562
|
+
// A non-empty id that resolves to nothing is a dead end - say so rather than staying
|
|
3563
|
+
// silently pending.
|
|
3564
|
+
console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
|
|
3622
3565
|
}
|
|
3623
3566
|
}
|
|
3624
3567
|
/**
|
|
@@ -3753,6 +3696,8 @@ class ComponentElement extends AsyncElement {
|
|
|
3753
3696
|
console.warn(`${this.tagName.toLowerCase()}${label} - '${entity.name}' already has a '${this._componentName}' component - component not added`);
|
|
3754
3697
|
return;
|
|
3755
3698
|
}
|
|
3699
|
+
// The name passed by the subclass selects the engine system that creates its T - a
|
|
3700
|
+
// pairing the type system cannot express, so it is asserted this once
|
|
3756
3701
|
this._component = entity.addComponent(this._componentName, this.getInitialComponentData());
|
|
3757
3702
|
}
|
|
3758
3703
|
async _addComponent() {
|
|
@@ -4015,6 +3960,9 @@ class AnimComponentElement extends ComponentElement {
|
|
|
4015
3960
|
*/
|
|
4016
3961
|
_applyRootBone() {
|
|
4017
3962
|
const component = this.component;
|
|
3963
|
+
if (!component) {
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
4018
3966
|
// A non-null root this element did not assign came through the engine API. A fresh
|
|
4019
3967
|
// component starts at null, which is always reclaimable.
|
|
4020
3968
|
if (component.rootBone !== null && component.rootBone !== this._managedRootBone) {
|
|
@@ -4087,7 +4035,7 @@ class AnimComponentElement extends ComponentElement {
|
|
|
4087
4035
|
* declared `clip` selection can apply before any asset has loaded.
|
|
4088
4036
|
*/
|
|
4089
4037
|
_assignClip(clip) {
|
|
4090
|
-
this.component
|
|
4038
|
+
this.component?.assignAnimation(clip.name, clip._track ?? AnimTrack.EMPTY, undefined, clip.speed, clip.loop);
|
|
4091
4039
|
}
|
|
4092
4040
|
/**
|
|
4093
4041
|
* Validates a clip child and, when valid, assigns its state and starts its track resolution.
|
|
@@ -4395,8 +4343,9 @@ class AnimComponentElement extends ComponentElement {
|
|
|
4395
4343
|
component.playing = true;
|
|
4396
4344
|
}
|
|
4397
4345
|
/**
|
|
4398
|
-
* Gets the underlying PlayCanvas anim component.
|
|
4399
|
-
*
|
|
4346
|
+
* Gets the underlying PlayCanvas anim component. `null` until the element is
|
|
4347
|
+
* ready — see {@link ComponentElement.component}.
|
|
4348
|
+
* @returns The anim component, or `null`.
|
|
4400
4349
|
*/
|
|
4401
4350
|
get component() {
|
|
4402
4351
|
return super.component;
|
|
@@ -4545,6 +4494,11 @@ class AnimClipElement extends AsyncElement {
|
|
|
4545
4494
|
*/
|
|
4546
4495
|
_animElement = null;
|
|
4547
4496
|
_asset = '';
|
|
4497
|
+
/**
|
|
4498
|
+
* Watches the current source asset while it loads. Starting a new resolution or
|
|
4499
|
+
* disconnecting cancels it, so a superseded source can never hand its track to the parent.
|
|
4500
|
+
*/
|
|
4501
|
+
_binding = new AssetBinding();
|
|
4548
4502
|
/**
|
|
4549
4503
|
* Incremented on every connect and disconnect, and captured by connectedCallback on entry —
|
|
4550
4504
|
* a resume from an await abandons itself if the value has moved on, so a stale callback can
|
|
@@ -4552,19 +4506,13 @@ class AnimClipElement extends AsyncElement {
|
|
|
4552
4506
|
* own callback.
|
|
4553
4507
|
*/
|
|
4554
4508
|
_connectionGeneration = 0;
|
|
4555
|
-
_errorHandle = null;
|
|
4556
4509
|
/**
|
|
4557
4510
|
* Incremented on every track resolution and on disconnect, and captured by a resolution when
|
|
4558
|
-
* it starts. A resolution that resumes from an await
|
|
4559
|
-
*
|
|
4511
|
+
* it starts. A resolution that resumes from an await abandons itself if the value has moved
|
|
4512
|
+
* on, so a superseded resolution cannot hand a stale track to the parent. The asset
|
|
4513
|
+
* subscription itself is guarded by the binding above.
|
|
4560
4514
|
*/
|
|
4561
4515
|
_loadGeneration = 0;
|
|
4562
|
-
/**
|
|
4563
|
-
* The pending asset subscriptions of the current resolution, if it is waiting for its asset.
|
|
4564
|
-
* Held so that whatever supersedes the resolution can detach the handlers from the asset,
|
|
4565
|
-
* rather than leave them registered until the asset settles (or forever, if it never does).
|
|
4566
|
-
*/
|
|
4567
|
-
_loadHandle = null;
|
|
4568
4516
|
_loop = true;
|
|
4569
4517
|
_name = '';
|
|
4570
4518
|
_speed = 1;
|
|
@@ -4604,7 +4552,7 @@ class AnimClipElement extends AsyncElement {
|
|
|
4604
4552
|
// Invalidate any connectedCallback or track resolution still suspended on an await
|
|
4605
4553
|
this._connectionGeneration++;
|
|
4606
4554
|
this._loadGeneration++;
|
|
4607
|
-
this.
|
|
4555
|
+
this._binding.cancel();
|
|
4608
4556
|
// Uses the cached parent rather than a fresh lookup, since parentElement is already null
|
|
4609
4557
|
// by now. The component itself is null if the whole <pc-app> is being torn down —
|
|
4610
4558
|
// parents disconnect first and have already removed the component.
|
|
@@ -4622,12 +4570,6 @@ class AnimClipElement extends AsyncElement {
|
|
|
4622
4570
|
}
|
|
4623
4571
|
return animElement;
|
|
4624
4572
|
}
|
|
4625
|
-
_detachLoadHandlers() {
|
|
4626
|
-
this._loadHandle?.off();
|
|
4627
|
-
this._loadHandle = null;
|
|
4628
|
-
this._errorHandle?.off();
|
|
4629
|
-
this._errorHandle = null;
|
|
4630
|
-
}
|
|
4631
4573
|
/**
|
|
4632
4574
|
* Reports a name-validation failure from the owning `<pc-anim>`, once per name value.
|
|
4633
4575
|
*
|
|
@@ -4653,34 +4595,19 @@ class AnimClipElement extends AsyncElement {
|
|
|
4653
4595
|
async _resolveTrack(animElement) {
|
|
4654
4596
|
this._animElement = animElement;
|
|
4655
4597
|
const generation = ++this._loadGeneration;
|
|
4656
|
-
this.
|
|
4598
|
+
this._binding.cancel();
|
|
4657
4599
|
if (this._asset) {
|
|
4658
|
-
|
|
4600
|
+
// Every path that moves _loadGeneration also rebinds or cancels the binding, so a
|
|
4601
|
+
// delivery below is always current.
|
|
4602
|
+
const asset = this._binding.bind(this._asset, {
|
|
4603
|
+
load: (loaded) => this._extractTrack(loaded, `asset '${this._asset}'`),
|
|
4604
|
+
error: () => {
|
|
4605
|
+
this._warnSource(`pc-anim-clip '${this._name}' - asset '${this._asset}' failed to load - clip not assigned`);
|
|
4606
|
+
}
|
|
4607
|
+
});
|
|
4659
4608
|
if (!asset) {
|
|
4660
4609
|
this._warnSource(`pc-anim-clip '${this._name}' could not find asset '${this._asset}' - clip not assigned`);
|
|
4661
|
-
return;
|
|
4662
4610
|
}
|
|
4663
|
-
if (asset.loaded) {
|
|
4664
|
-
this._extractTrack(asset, `asset '${this._asset}'`);
|
|
4665
|
-
return;
|
|
4666
|
-
}
|
|
4667
|
-
// Whichever of load/error fires first detaches the other. The generation is
|
|
4668
|
-
// re-checked even though a superseded handler is detached: the detach relies on how
|
|
4669
|
-
// the engine's event emitter treats removal, while the check holds on its own.
|
|
4670
|
-
this._loadHandle = asset.once('load', () => {
|
|
4671
|
-
this._detachLoadHandlers();
|
|
4672
|
-
if (generation !== this._loadGeneration) {
|
|
4673
|
-
return;
|
|
4674
|
-
}
|
|
4675
|
-
this._extractTrack(asset, `asset '${this._asset}'`);
|
|
4676
|
-
});
|
|
4677
|
-
this._errorHandle = asset.once('error', () => {
|
|
4678
|
-
this._detachLoadHandlers();
|
|
4679
|
-
if (generation !== this._loadGeneration) {
|
|
4680
|
-
return;
|
|
4681
|
-
}
|
|
4682
|
-
this._warnSource(`pc-anim-clip '${this._name}' - asset '${this._asset}' failed to load - clip not assigned`);
|
|
4683
|
-
});
|
|
4684
4611
|
return;
|
|
4685
4612
|
}
|
|
4686
4613
|
const model = animElement.parentElement;
|
|
@@ -4787,99 +4714,321 @@ class AnimClipElement extends AsyncElement {
|
|
|
4787
4714
|
this._loop = value;
|
|
4788
4715
|
this._animElement?._onClipParamsChanged(this);
|
|
4789
4716
|
}
|
|
4790
|
-
/**
|
|
4791
|
-
* Gets whether the clip loops.
|
|
4792
|
-
* @returns Whether the clip loops.
|
|
4793
|
-
*/
|
|
4794
|
-
get loop() {
|
|
4795
|
-
return this._loop;
|
|
4717
|
+
/**
|
|
4718
|
+
* Gets whether the clip loops.
|
|
4719
|
+
* @returns Whether the clip loops.
|
|
4720
|
+
*/
|
|
4721
|
+
get loop() {
|
|
4722
|
+
return this._loop;
|
|
4723
|
+
}
|
|
4724
|
+
/**
|
|
4725
|
+
* Sets the name of the clip: the name it is played by, and the track looked up in the
|
|
4726
|
+
* clip's source. Names must be unique within a `<pc-anim>` and must not contain `.`.
|
|
4727
|
+
* @param value - The clip name.
|
|
4728
|
+
*/
|
|
4729
|
+
set name(value) {
|
|
4730
|
+
this._name = value;
|
|
4731
|
+
this._warnedInvalid = false;
|
|
4732
|
+
if (this._animElement) {
|
|
4733
|
+
this._resetReady();
|
|
4734
|
+
this._animElement._refreshClips();
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
/**
|
|
4738
|
+
* Gets the name of the clip.
|
|
4739
|
+
* @returns The clip name.
|
|
4740
|
+
*/
|
|
4741
|
+
get name() {
|
|
4742
|
+
return this._name;
|
|
4743
|
+
}
|
|
4744
|
+
/**
|
|
4745
|
+
* Sets the playback speed of the clip, where negative values play it backwards. Applies
|
|
4746
|
+
* immediately, preserving the playhead. Defaults to 1.
|
|
4747
|
+
* @param value - The playback speed.
|
|
4748
|
+
*/
|
|
4749
|
+
set speed(value) {
|
|
4750
|
+
this._speed = value;
|
|
4751
|
+
this._animElement?._onClipParamsChanged(this);
|
|
4752
|
+
}
|
|
4753
|
+
/**
|
|
4754
|
+
* Gets the playback speed of the clip.
|
|
4755
|
+
* @returns The playback speed.
|
|
4756
|
+
*/
|
|
4757
|
+
get speed() {
|
|
4758
|
+
return this._speed;
|
|
4759
|
+
}
|
|
4760
|
+
static get observedAttributes() {
|
|
4761
|
+
return ['asset', 'loop', 'name', 'speed'];
|
|
4762
|
+
}
|
|
4763
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
4764
|
+
switch (name) {
|
|
4765
|
+
case 'asset':
|
|
4766
|
+
this.asset = newValue ?? '';
|
|
4767
|
+
break;
|
|
4768
|
+
case 'loop':
|
|
4769
|
+
this.loop = parseBool(newValue, true);
|
|
4770
|
+
break;
|
|
4771
|
+
case 'name':
|
|
4772
|
+
this.name = newValue ?? '';
|
|
4773
|
+
break;
|
|
4774
|
+
case 'speed':
|
|
4775
|
+
this.speed = parseNumber(newValue, 1, name);
|
|
4776
|
+
break;
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
customElements.define('pc-anim-clip', AnimClipElement);
|
|
4781
|
+
|
|
4782
|
+
/**
|
|
4783
|
+
* The AudioListenerComponentElement interface provides properties and methods for manipulating
|
|
4784
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-audio-listener/ | `<pc-audio-listener>`} elements.
|
|
4785
|
+
* The AudioListenerComponentElement interface also inherits the properties and methods of the
|
|
4786
|
+
* {@link HTMLElement} interface.
|
|
4787
|
+
*
|
|
4788
|
+
* Engine component: {@link AudioListenerComponent} (`audiolistener`).
|
|
4789
|
+
*
|
|
4790
|
+
* @elementSummary The `<pc-audio-listener>` element makes its entity the point from which
|
|
4791
|
+
* positional sounds are heard, typically the entity holding the `<pc-camera>`. Must be a child of a
|
|
4792
|
+
* `<pc-entity>`, `<pc-model>` or `<pc-node>`.
|
|
4793
|
+
*
|
|
4794
|
+
* @category Components
|
|
4795
|
+
*/
|
|
4796
|
+
class AudioListenerComponentElement extends ComponentElement {
|
|
4797
|
+
/** @ignore */
|
|
4798
|
+
constructor() {
|
|
4799
|
+
super('audiolistener');
|
|
4800
|
+
}
|
|
4801
|
+
/**
|
|
4802
|
+
* Gets the underlying PlayCanvas audio listener component. `null` until the element is
|
|
4803
|
+
* ready — see {@link ComponentElement.component}.
|
|
4804
|
+
* @returns The audio listener component, or `null`.
|
|
4805
|
+
*/
|
|
4806
|
+
get component() {
|
|
4807
|
+
return super.component;
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
customElements.define('pc-audio-listener', AudioListenerComponentElement);
|
|
4811
|
+
|
|
4812
|
+
/**
|
|
4813
|
+
* Resolves entity reference strings against the document. A reference beginning with `#` is a
|
|
4814
|
+
* document-wide selector (an element id, or any selector rooted in one); anything else is an
|
|
4815
|
+
* entity name, resolved lexically through the entity hierarchy first and against the document
|
|
4816
|
+
* after — never as a selector or an id.
|
|
4817
|
+
*
|
|
4818
|
+
* Unlike the attribute parsers in `parse.ts`, these helpers touch the DOM, and `findEntityElement`
|
|
4819
|
+
* and `getEntity` return `null` instead of falling back to a default. They also do not warn -
|
|
4820
|
+
* what an unresolved reference means depends on the element holding it - so elements report
|
|
4821
|
+
* through `resolveEntity`, which takes that meaning as parameters, and `pc-script` builds its own
|
|
4822
|
+
* message from the lower-level pieces.
|
|
4823
|
+
*/
|
|
4824
|
+
/**
|
|
4825
|
+
* Runs querySelector, absorbing the SyntaxError an unparseable selector throws - references are
|
|
4826
|
+
* arbitrary author text, so a lookup must fail to `null`, never throw.
|
|
4827
|
+
*
|
|
4828
|
+
* @param selector - The selector to query.
|
|
4829
|
+
* @returns The matched element, or `null`.
|
|
4830
|
+
*/
|
|
4831
|
+
const query = (selector) => {
|
|
4832
|
+
try {
|
|
4833
|
+
return document.querySelector(selector);
|
|
4834
|
+
}
|
|
4835
|
+
catch {
|
|
4836
|
+
return null;
|
|
4837
|
+
}
|
|
4838
|
+
};
|
|
4839
|
+
/**
|
|
4840
|
+
* Runs a lookup against one scope, checking the scope element itself before its subtree — a
|
|
4841
|
+
* reference deep in a cloned prefab must be able to name the prefab's root. Absorbs the
|
|
4842
|
+
* SyntaxError of an invalid selector like {@link query}: escaping quotes and backslashes does not
|
|
4843
|
+
* make arbitrary text a valid CSS string (a reference containing a newline still throws), so a
|
|
4844
|
+
* lookup must fail to `null`, never throw.
|
|
4845
|
+
*
|
|
4846
|
+
* @param scope - The element whose inclusive subtree to search.
|
|
4847
|
+
* @param selector - The selector to query.
|
|
4848
|
+
* @returns The matched element, or `null`.
|
|
4849
|
+
*/
|
|
4850
|
+
const queryScope = (scope, selector) => {
|
|
4851
|
+
try {
|
|
4852
|
+
return scope.matches(selector) ? scope : scope.querySelector(selector);
|
|
4853
|
+
}
|
|
4854
|
+
catch {
|
|
4855
|
+
return null;
|
|
4856
|
+
}
|
|
4857
|
+
};
|
|
4858
|
+
/**
|
|
4859
|
+
* Reads the entity a resolved element is backing, through the `entity` accessor every
|
|
4860
|
+
* entity-fronting element exposes. `null` for no element, and for an element backing nothing.
|
|
4861
|
+
*
|
|
4862
|
+
* @param element - The element to read, or `null`.
|
|
4863
|
+
* @returns The backing entity, or `null`.
|
|
4864
|
+
*/
|
|
4865
|
+
const entityOf = (element) => {
|
|
4866
|
+
return element?.entity ?? null;
|
|
4867
|
+
};
|
|
4868
|
+
/**
|
|
4869
|
+
* The elements that front an entity: what a bare name can resolve to, and the scopes of the
|
|
4870
|
+
* lexical name lookup.
|
|
4871
|
+
*/
|
|
4872
|
+
const ENTITY_KINDS = ['pc-entity', 'pc-model', 'pc-node'];
|
|
4873
|
+
/**
|
|
4874
|
+
* The entity-fronting elements as one selector, for the scope walk.
|
|
4875
|
+
*/
|
|
4876
|
+
const ENTITY_SCOPES = ENTITY_KINDS.join(', ');
|
|
4877
|
+
/**
|
|
4878
|
+
* Resolves a reference string to the element it names. The grammar is closed — every reference
|
|
4879
|
+
* has exactly one interpretation:
|
|
4880
|
+
*
|
|
4881
|
+
* - A reference beginning with `#` is a document-wide CSS selector — an element id (`#body`), or
|
|
4882
|
+
* any selector rooted in one (`#hud pc-entity`). It is authoritative: the name lookup never
|
|
4883
|
+
* runs for it, so an unusually named entity cannot shadow it.
|
|
4884
|
+
* - Any other reference is the name of an entity-fronting element (`<pc-entity>`, `<pc-model>` or
|
|
4885
|
+
* `<pc-node>` — for a node, the glTF node name it binds), and nothing else. A bare reference is
|
|
4886
|
+
* never interpreted as a selector or an element id, so adding or renaming elements can never
|
|
4887
|
+
* change which form it takes.
|
|
4888
|
+
*
|
|
4889
|
+
* When `from` is supplied, a name resolves lexically first: the closest entity-fronting
|
|
4890
|
+
* ancestor's inclusive subtree, then each outer entity-fronting ancestor, then the containing
|
|
4891
|
+
* `<pc-app>`, then the document. This is what lets a `<template>` prefab reference its own
|
|
4892
|
+
* entities by name — every clone resolves within itself before a document-wide lookup could reach
|
|
4893
|
+
* an earlier clone — provided the prefab has a single entity-fronting root to be the enclosing
|
|
4894
|
+
* scope.
|
|
4895
|
+
*
|
|
4896
|
+
* Separate from {@link getEntity} so a caller reporting a failure can tell the causes apart
|
|
4897
|
+
* ({@link unresolvedCause} words them): nothing in the document matches the reference, or
|
|
4898
|
+
* something matches but is not backing an entity (yet, or ever).
|
|
4899
|
+
*
|
|
4900
|
+
* @param ref - The reference string to resolve.
|
|
4901
|
+
* @param from - The element resolving the reference, whose entity-fronting ancestors scope the
|
|
4902
|
+
* name lookup. Omitted, the name lookup is document-wide only.
|
|
4903
|
+
* @returns The matched element, or `null`.
|
|
4904
|
+
* @internal
|
|
4905
|
+
*/
|
|
4906
|
+
const findEntityElement = (ref, from) => {
|
|
4907
|
+
if (!ref) {
|
|
4908
|
+
return null;
|
|
4909
|
+
}
|
|
4910
|
+
// A '#' reference is document-wide and bypasses the name lookup entirely - an entity named
|
|
4911
|
+
// '#body' must never shadow the element whose id is 'body'.
|
|
4912
|
+
if (ref.startsWith('#')) {
|
|
4913
|
+
return query(ref);
|
|
4796
4914
|
}
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4915
|
+
// The name lands inside a quoted CSS string, so its quotes and backslashes are escaped -
|
|
4916
|
+
// a name like `say "hi"` must resolve, not turn the lookup into a SyntaxError.
|
|
4917
|
+
const escaped = ref.replace(/["\\]/g, '\\$&');
|
|
4918
|
+
const nameSelector = ENTITY_KINDS.map(kind => `${kind}[name="${escaped}"]`).join(', ');
|
|
4919
|
+
if (from) {
|
|
4920
|
+
let scope = from.parentElement?.closest(ENTITY_SCOPES);
|
|
4921
|
+
while (scope) {
|
|
4922
|
+
const element = queryScope(scope, nameSelector);
|
|
4923
|
+
if (element) {
|
|
4924
|
+
return element;
|
|
4925
|
+
}
|
|
4926
|
+
scope = scope.parentElement?.closest(ENTITY_SCOPES);
|
|
4808
4927
|
}
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
return this._name;
|
|
4816
|
-
}
|
|
4817
|
-
/**
|
|
4818
|
-
* Sets the playback speed of the clip, where negative values play it backwards. Applies
|
|
4819
|
-
* immediately, preserving the playhead. Defaults to 1.
|
|
4820
|
-
* @param value - The playback speed.
|
|
4821
|
-
*/
|
|
4822
|
-
set speed(value) {
|
|
4823
|
-
this._speed = value;
|
|
4824
|
-
this._animElement?._onClipParamsChanged(this);
|
|
4825
|
-
}
|
|
4826
|
-
/**
|
|
4827
|
-
* Gets the playback speed of the clip.
|
|
4828
|
-
* @returns The playback speed.
|
|
4829
|
-
*/
|
|
4830
|
-
get speed() {
|
|
4831
|
-
return this._speed;
|
|
4832
|
-
}
|
|
4833
|
-
static get observedAttributes() {
|
|
4834
|
-
return ['asset', 'loop', 'name', 'speed'];
|
|
4835
|
-
}
|
|
4836
|
-
attributeChangedCallback(name, _oldValue, newValue) {
|
|
4837
|
-
switch (name) {
|
|
4838
|
-
case 'asset':
|
|
4839
|
-
this.asset = newValue ?? '';
|
|
4840
|
-
break;
|
|
4841
|
-
case 'loop':
|
|
4842
|
-
this.loop = parseBool(newValue, true);
|
|
4843
|
-
break;
|
|
4844
|
-
case 'name':
|
|
4845
|
-
this.name = newValue ?? '';
|
|
4846
|
-
break;
|
|
4847
|
-
case 'speed':
|
|
4848
|
-
this.speed = parseNumber(newValue, 1, name);
|
|
4849
|
-
break;
|
|
4928
|
+
const app = from.parentElement?.closest('pc-app');
|
|
4929
|
+
if (app) {
|
|
4930
|
+
const element = queryScope(app, nameSelector);
|
|
4931
|
+
if (element) {
|
|
4932
|
+
return element;
|
|
4933
|
+
}
|
|
4850
4934
|
}
|
|
4851
4935
|
}
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4936
|
+
return query(nameSelector);
|
|
4937
|
+
};
|
|
4855
4938
|
/**
|
|
4856
|
-
*
|
|
4857
|
-
*
|
|
4858
|
-
*
|
|
4859
|
-
* {@link
|
|
4939
|
+
* Resolves a reference string to the {@link Entity} backing an entity-fronting element
|
|
4940
|
+
* (`<pc-entity>`, `<pc-model>` or `<pc-node>`). The reference is a name — resolved lexically
|
|
4941
|
+
* through the entity hierarchy first when `from` is supplied — or a document-wide `#` selector
|
|
4942
|
+
* ({@link findEntityElement} details the grammar and order). Returns `null` if no matching
|
|
4943
|
+
* element (or backing entity) is found.
|
|
4860
4944
|
*
|
|
4861
|
-
*
|
|
4945
|
+
* @param ref - The reference string to resolve.
|
|
4946
|
+
* @param from - The element resolving the reference, whose entity-fronting ancestors scope the
|
|
4947
|
+
* name lookup. Omitted, the name lookup is document-wide only.
|
|
4948
|
+
* @returns The resolved entity, or `null`.
|
|
4949
|
+
* @internal
|
|
4950
|
+
*/
|
|
4951
|
+
const getEntity = (ref, from) => {
|
|
4952
|
+
return entityOf(findEntityElement(ref, from));
|
|
4953
|
+
};
|
|
4954
|
+
/**
|
|
4955
|
+
* Describes why a non-empty reference did not resolve, for a warning. Three causes, because they
|
|
4956
|
+
* have three different fixes: nothing matches (usually a typo), the matched element is not backing
|
|
4957
|
+
* an entity yet (usually timing - a `pc-node` whose asset has not loaded - so resolving again
|
|
4958
|
+
* later can work), or the matched element can never back one (the reference points at the wrong
|
|
4959
|
+
* element, so only correcting it can). Capability is the `entity` accessor every entity-backing
|
|
4960
|
+
* element inherits from EntityBaseElement.
|
|
4862
4961
|
*
|
|
4863
|
-
* @
|
|
4864
|
-
*
|
|
4865
|
-
*
|
|
4962
|
+
* @param element - The element the reference matched, or `null` when nothing did.
|
|
4963
|
+
* @returns The cause, phrased to follow `could not resolve ... -`.
|
|
4964
|
+
* @internal
|
|
4965
|
+
*/
|
|
4966
|
+
const unresolvedCause = (element) => {
|
|
4967
|
+
if (!element) {
|
|
4968
|
+
return 'nothing in the document matches it';
|
|
4969
|
+
}
|
|
4970
|
+
const tag = `<${element.tagName.toLowerCase()}>`;
|
|
4971
|
+
return 'entity' in element
|
|
4972
|
+
? `${tag} matches it but is not backing an entity yet`
|
|
4973
|
+
: `${tag} matches it but cannot back an entity`;
|
|
4974
|
+
};
|
|
4975
|
+
/**
|
|
4976
|
+
* Builds the migration pointer for a bare reference that names nothing but matches the id of an
|
|
4977
|
+
* entity-fronting element - it was almost certainly meant as an id, so point at the form that
|
|
4978
|
+
* expresses it, escaped so the suggestion actually parses as a selector (an id like `a:b` must
|
|
4979
|
+
* be written `#a\:b`). Empty when the reference is already a `#` form, matches no id, or the id
|
|
4980
|
+
* belongs to an element that could never back an entity - suggesting it would only trade this
|
|
4981
|
+
* warning for the wrong-target one.
|
|
4866
4982
|
*
|
|
4867
|
-
* @
|
|
4983
|
+
* @param ref - The unresolved reference.
|
|
4984
|
+
* @param prefix - Text the suggested form must carry in the caller's syntax (e.g. `entity:`).
|
|
4985
|
+
* @returns The advice sentence, or an empty string.
|
|
4986
|
+
* @internal
|
|
4868
4987
|
*/
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4988
|
+
const idHint = (ref, prefix = '') => {
|
|
4989
|
+
const match = !ref.startsWith('#') && document.getElementById(ref);
|
|
4990
|
+
return match && 'entity' in match
|
|
4991
|
+
? `A bare reference is a name - write '${prefix}#${CSS.escape(ref)}' to reference the element with that id.`
|
|
4992
|
+
: '';
|
|
4993
|
+
};
|
|
4994
|
+
/**
|
|
4995
|
+
* Resolves a reference string to the {@link Entity} backing an entity-fronting element, scoped to
|
|
4996
|
+
* the resolving element ({@link findEntityElement} details the order) and warning when a
|
|
4997
|
+
* non-empty reference does not resolve - otherwise the reference fails silently, invisible
|
|
4998
|
+
* except through the behavior it should have driven. The message names which of the three causes
|
|
4999
|
+
* ({@link unresolvedCause}) it hit, and advises reassigning later only when that can work.
|
|
5000
|
+
*
|
|
5001
|
+
* An empty reference stays silent: it is the unset state of an optional attribute, and on some
|
|
5002
|
+
* elements (`pc-joint` `entity-b`, `pc-button` `image`) a documented value of its own.
|
|
5003
|
+
*
|
|
5004
|
+
* @param ref - The reference string to resolve.
|
|
5005
|
+
* @param from - The element resolving the reference; scopes the lookup and names the message.
|
|
5006
|
+
* @param attribute - The attribute being resolved, for the message.
|
|
5007
|
+
* @param consequence - What the unresolved reference means for the element, for the message.
|
|
5008
|
+
* @returns The resolved entity, or `null`.
|
|
5009
|
+
* @internal
|
|
5010
|
+
*/
|
|
5011
|
+
const resolveEntity = (ref, from, attribute, consequence) => {
|
|
5012
|
+
if (!ref) {
|
|
5013
|
+
return null;
|
|
4873
5014
|
}
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
5015
|
+
const element = findEntityElement(ref, from);
|
|
5016
|
+
const entity = entityOf(element);
|
|
5017
|
+
if (!entity) {
|
|
5018
|
+
let advice = `Assign ${attribute} again once the entity exists.`;
|
|
5019
|
+
if (element && !('entity' in element)) {
|
|
5020
|
+
advice = `Point ${attribute} at a pc-entity, pc-model or pc-node instead.`;
|
|
5021
|
+
}
|
|
5022
|
+
else if (!element) {
|
|
5023
|
+
const hint = idHint(ref);
|
|
5024
|
+
if (hint) {
|
|
5025
|
+
advice = hint;
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
console.warn(`${from.tagName.toLowerCase()} could not resolve ${attribute} '${ref}' - ${unresolvedCause(element)} - ${consequence}. ${advice}`);
|
|
4880
5029
|
}
|
|
4881
|
-
|
|
4882
|
-
|
|
5030
|
+
return entity;
|
|
5031
|
+
};
|
|
4883
5032
|
|
|
4884
5033
|
const transitionModes = new Map([
|
|
4885
5034
|
['tint', BUTTON_TRANSITION_MODE_TINT],
|
|
@@ -4955,8 +5104,9 @@ class ButtonComponentElement extends ComponentElement {
|
|
|
4955
5104
|
return data;
|
|
4956
5105
|
}
|
|
4957
5106
|
/**
|
|
4958
|
-
* Gets the underlying PlayCanvas button component.
|
|
4959
|
-
*
|
|
5107
|
+
* Gets the underlying PlayCanvas button component. `null` until the element is
|
|
5108
|
+
* ready — see {@link ComponentElement.component}.
|
|
5109
|
+
* @returns The button component, or `null`.
|
|
4960
5110
|
*/
|
|
4961
5111
|
get component() {
|
|
4962
5112
|
return super.component;
|
|
@@ -5414,8 +5564,9 @@ class CameraComponentElement extends ComponentElement {
|
|
|
5414
5564
|
}
|
|
5415
5565
|
}
|
|
5416
5566
|
/**
|
|
5417
|
-
* Gets the underlying PlayCanvas camera component.
|
|
5418
|
-
*
|
|
5567
|
+
* Gets the underlying PlayCanvas camera component. `null` until the element is
|
|
5568
|
+
* ready — see {@link ComponentElement.component}.
|
|
5569
|
+
* @returns The camera component, or `null`.
|
|
5419
5570
|
*/
|
|
5420
5571
|
get component() {
|
|
5421
5572
|
return super.component;
|
|
@@ -5904,8 +6055,9 @@ class CollisionComponentElement extends ComponentElement {
|
|
|
5904
6055
|
component.renderAsset = asset;
|
|
5905
6056
|
}
|
|
5906
6057
|
/**
|
|
5907
|
-
* Gets the underlying PlayCanvas collision component.
|
|
5908
|
-
*
|
|
6058
|
+
* Gets the underlying PlayCanvas collision component. `null` until the element is
|
|
6059
|
+
* ready — see {@link ComponentElement.component}.
|
|
6060
|
+
* @returns The collision component, or `null`.
|
|
5909
6061
|
*/
|
|
5910
6062
|
get component() {
|
|
5911
6063
|
return super.component;
|
|
@@ -6143,8 +6295,9 @@ class ElementComponentElement extends ComponentElement {
|
|
|
6143
6295
|
return data;
|
|
6144
6296
|
}
|
|
6145
6297
|
/**
|
|
6146
|
-
* Gets the underlying PlayCanvas element component.
|
|
6147
|
-
*
|
|
6298
|
+
* Gets the underlying PlayCanvas element component. `null` until the element is
|
|
6299
|
+
* ready — see {@link ComponentElement.component}.
|
|
6300
|
+
* @returns The element component, or `null`.
|
|
6148
6301
|
*/
|
|
6149
6302
|
get component() {
|
|
6150
6303
|
return super.component;
|
|
@@ -6921,8 +7074,9 @@ class JointComponentElement extends ComponentElement {
|
|
|
6921
7074
|
component.on('break', this._onBreak, this);
|
|
6922
7075
|
}
|
|
6923
7076
|
/**
|
|
6924
|
-
* Gets the underlying PlayCanvas joint component.
|
|
6925
|
-
*
|
|
7077
|
+
* Gets the underlying PlayCanvas joint component. `null` until the element is
|
|
7078
|
+
* ready — see {@link ComponentElement.component}.
|
|
7079
|
+
* @returns The joint component, or `null`.
|
|
6926
7080
|
*/
|
|
6927
7081
|
get component() {
|
|
6928
7082
|
return super.component;
|
|
@@ -7648,8 +7802,9 @@ class LayoutChildComponentElement extends ComponentElement {
|
|
|
7648
7802
|
};
|
|
7649
7803
|
}
|
|
7650
7804
|
/**
|
|
7651
|
-
* Gets the underlying PlayCanvas layout child component.
|
|
7652
|
-
*
|
|
7805
|
+
* Gets the underlying PlayCanvas layout child component. `null` until the element is
|
|
7806
|
+
* ready — see {@link ComponentElement.component}.
|
|
7807
|
+
* @returns The layout child component, or `null`.
|
|
7653
7808
|
*/
|
|
7654
7809
|
get component() {
|
|
7655
7810
|
return super.component;
|
|
@@ -7868,8 +8023,9 @@ class LayoutGroupComponentElement extends ComponentElement {
|
|
|
7868
8023
|
};
|
|
7869
8024
|
}
|
|
7870
8025
|
/**
|
|
7871
|
-
* Gets the underlying PlayCanvas layout group component.
|
|
7872
|
-
*
|
|
8026
|
+
* Gets the underlying PlayCanvas layout group component. `null` until the element is
|
|
8027
|
+
* ready — see {@link ComponentElement.component}.
|
|
8028
|
+
* @returns The layout group component, or `null`.
|
|
7873
8029
|
*/
|
|
7874
8030
|
get component() {
|
|
7875
8031
|
return super.component;
|
|
@@ -8158,8 +8314,9 @@ class LightComponentElement extends ComponentElement {
|
|
|
8158
8314
|
};
|
|
8159
8315
|
}
|
|
8160
8316
|
/**
|
|
8161
|
-
* Gets the underlying PlayCanvas light component.
|
|
8162
|
-
*
|
|
8317
|
+
* Gets the underlying PlayCanvas light component. `null` until the element is
|
|
8318
|
+
* ready — see {@link ComponentElement.component}.
|
|
8319
|
+
* @returns The light component, or `null`.
|
|
8163
8320
|
*/
|
|
8164
8321
|
get component() {
|
|
8165
8322
|
return super.component;
|
|
@@ -8675,36 +8832,68 @@ customElements.define('pc-light', LightComponentElement);
|
|
|
8675
8832
|
*/
|
|
8676
8833
|
class ParticleSystemComponentElement extends ComponentElement {
|
|
8677
8834
|
_asset = '';
|
|
8835
|
+
/**
|
|
8836
|
+
* The subscription to the current config asset while its load is in flight. Rebinding
|
|
8837
|
+
* supersedes it and disconnect cancels it, so a superseded config — an earlier asset that
|
|
8838
|
+
* finishes loading after its replacement, or a callback left behind by a previous
|
|
8839
|
+
* connection — can never configure the component.
|
|
8840
|
+
*/
|
|
8841
|
+
_binding = new AssetBinding();
|
|
8678
8842
|
/** @ignore */
|
|
8679
8843
|
constructor() {
|
|
8680
8844
|
super('particlesystem');
|
|
8681
8845
|
}
|
|
8682
8846
|
getInitialComponentData() {
|
|
8683
8847
|
const asset = useAsset(this._asset);
|
|
8684
|
-
// A lazy config has no resource yet -
|
|
8848
|
+
// A lazy config has no resource yet - the config binding applies it once the load
|
|
8849
|
+
// completes
|
|
8685
8850
|
if (!asset || !asset.resource) {
|
|
8686
8851
|
return {};
|
|
8687
8852
|
}
|
|
8688
|
-
|
|
8689
|
-
const id = asset.resource.colorMapAsset;
|
|
8690
|
-
const colorMapAsset = useAsset(id)?.id;
|
|
8691
|
-
if (colorMapAsset) {
|
|
8692
|
-
asset.resource.colorMapAsset = colorMapAsset;
|
|
8693
|
-
}
|
|
8694
|
-
}
|
|
8853
|
+
this._resolveColorMap(asset.resource);
|
|
8695
8854
|
return asset.resource;
|
|
8696
8855
|
}
|
|
8856
|
+
initComponent() {
|
|
8857
|
+
// A loaded config already arrived through getInitialComponentData - the binding is only
|
|
8858
|
+
// needed for a load still in flight. Resolution here also starts a lazy config's load.
|
|
8859
|
+
const asset = useAsset(this._asset);
|
|
8860
|
+
if (asset && !asset.loaded) {
|
|
8861
|
+
this._bindConfig();
|
|
8862
|
+
}
|
|
8863
|
+
}
|
|
8864
|
+
disconnectedCallback() {
|
|
8865
|
+
// The binding dies with the connection, so a config that finishes loading later cannot
|
|
8866
|
+
// configure the component a reconnection creates - that connection binds afresh.
|
|
8867
|
+
this._binding.cancel();
|
|
8868
|
+
super.disconnectedCallback();
|
|
8869
|
+
}
|
|
8697
8870
|
/**
|
|
8698
|
-
* Gets the underlying PlayCanvas particle system component.
|
|
8699
|
-
*
|
|
8871
|
+
* Gets the underlying PlayCanvas particle system component. `null` until the element is
|
|
8872
|
+
* ready — see {@link ComponentElement.component}.
|
|
8873
|
+
* @returns The particle system component, or `null`.
|
|
8700
8874
|
*/
|
|
8701
8875
|
get component() {
|
|
8702
8876
|
return super.component;
|
|
8703
8877
|
}
|
|
8878
|
+
/**
|
|
8879
|
+
* Rewrites the config's `colorMapAsset` from the `pc-asset` id it is authored with to the
|
|
8880
|
+
* engine asset id the component resolves, starting the texture's load if it is lazy. The
|
|
8881
|
+
* rewrite is in place, so a config applied again — a host cycle, a reconnection — is already
|
|
8882
|
+
* resolved and passes through unchanged.
|
|
8883
|
+
*/
|
|
8884
|
+
_resolveColorMap(resource) {
|
|
8885
|
+
if (resource.colorMapAsset) {
|
|
8886
|
+
const colorMapAsset = useAsset(resource.colorMapAsset)?.id;
|
|
8887
|
+
if (colorMapAsset) {
|
|
8888
|
+
resource.colorMapAsset = colorMapAsset;
|
|
8889
|
+
}
|
|
8890
|
+
}
|
|
8891
|
+
}
|
|
8704
8892
|
applyConfig(resource) {
|
|
8705
8893
|
if (!this.component) {
|
|
8706
8894
|
return;
|
|
8707
8895
|
}
|
|
8896
|
+
this._resolveColorMap(resource);
|
|
8708
8897
|
// Set all the config properties on the component
|
|
8709
8898
|
for (const key in resource) {
|
|
8710
8899
|
if (Object.hasOwn(resource, key)) {
|
|
@@ -8712,20 +8901,10 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
8712
8901
|
}
|
|
8713
8902
|
}
|
|
8714
8903
|
}
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
return;
|
|
8720
|
-
}
|
|
8721
|
-
if (asset.loaded) {
|
|
8722
|
-
this.applyConfig(asset.resource);
|
|
8723
|
-
}
|
|
8724
|
-
else {
|
|
8725
|
-
asset.once('load', () => {
|
|
8726
|
-
this.applyConfig(asset.resource);
|
|
8727
|
-
});
|
|
8728
|
-
}
|
|
8904
|
+
_bindConfig() {
|
|
8905
|
+
this._binding.bind(this._asset, {
|
|
8906
|
+
load: (asset) => this.applyConfig(asset.resource)
|
|
8907
|
+
});
|
|
8729
8908
|
}
|
|
8730
8909
|
/**
|
|
8731
8910
|
* Sets the id of the `pc-asset` to use for the model.
|
|
@@ -8734,7 +8913,7 @@ class ParticleSystemComponentElement extends ComponentElement {
|
|
|
8734
8913
|
set asset(value) {
|
|
8735
8914
|
this._asset = value;
|
|
8736
8915
|
if (this.isConnected) {
|
|
8737
|
-
this.
|
|
8916
|
+
this._bindConfig();
|
|
8738
8917
|
}
|
|
8739
8918
|
}
|
|
8740
8919
|
/**
|
|
@@ -8951,11 +9130,12 @@ class MaterialElement extends HTMLElement {
|
|
|
8951
9130
|
_useSkybox = true;
|
|
8952
9131
|
_useTonemap = true;
|
|
8953
9132
|
/**
|
|
8954
|
-
*
|
|
8955
|
-
*
|
|
8956
|
-
* element
|
|
9133
|
+
* One asset binding per texture slot, created on first use and kept for the element's
|
|
9134
|
+
* lifetime. A slot's binding is superseded when the slot is reassigned and cancelled when the
|
|
9135
|
+
* element disconnects, so a late-arriving asset can never write a texture the element no
|
|
9136
|
+
* longer wants.
|
|
8957
9137
|
*/
|
|
8958
|
-
|
|
9138
|
+
_mapBindings = new Map();
|
|
8959
9139
|
_updateScheduled = false;
|
|
8960
9140
|
_glossConflictWarned = false;
|
|
8961
9141
|
/**
|
|
@@ -9082,10 +9262,9 @@ class MaterialElement extends HTMLElement {
|
|
|
9082
9262
|
material.update();
|
|
9083
9263
|
}
|
|
9084
9264
|
disconnectedCallback() {
|
|
9085
|
-
for (const
|
|
9086
|
-
|
|
9265
|
+
for (const binding of this._mapBindings.values()) {
|
|
9266
|
+
binding.cancel();
|
|
9087
9267
|
}
|
|
9088
|
-
this._mapHandles.clear();
|
|
9089
9268
|
if (this.material) {
|
|
9090
9269
|
this.material.destroy();
|
|
9091
9270
|
this.material = null;
|
|
@@ -9131,16 +9310,23 @@ class MaterialElement extends HTMLElement {
|
|
|
9131
9310
|
'the roughness-* attributes invert gloss, so the two families contradict each other. Use one or the other.');
|
|
9132
9311
|
}
|
|
9133
9312
|
/**
|
|
9134
|
-
* Points a texture slot at the resource of a `pc-asset`, waiting for the asset to load when
|
|
9135
|
-
* has not already. An empty id clears the slot
|
|
9313
|
+
* Points a texture slot at the resource of a `pc-asset`, waiting for the asset to load when
|
|
9314
|
+
* it has not already. An empty id clears the slot; a slot keeps its current texture while
|
|
9315
|
+
* the new asset loads, and also across a failed load - a later reload can still deliver.
|
|
9136
9316
|
*
|
|
9137
9317
|
* @param id - The id of the `pc-asset`, or an empty string to clear the slot.
|
|
9138
9318
|
* @param slot - The material property to write.
|
|
9139
9319
|
*/
|
|
9140
9320
|
_setMap(id, slot) {
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9321
|
+
let binding = this._mapBindings.get(slot);
|
|
9322
|
+
if (!binding) {
|
|
9323
|
+
binding = new AssetBinding();
|
|
9324
|
+
this._mapBindings.set(slot, binding);
|
|
9325
|
+
}
|
|
9326
|
+
// Drop any load still pending for this slot - its texture is no longer the one we want.
|
|
9327
|
+
// Cancelled here rather than left to the bind below, which the material-less and
|
|
9328
|
+
// clear-slot returns never reach.
|
|
9329
|
+
binding.cancel();
|
|
9144
9330
|
if (!this.material)
|
|
9145
9331
|
return;
|
|
9146
9332
|
if (!id) {
|
|
@@ -9148,17 +9334,9 @@ class MaterialElement extends HTMLElement {
|
|
|
9148
9334
|
this._scheduleUpdate();
|
|
9149
9335
|
return;
|
|
9150
9336
|
}
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
if (asset.loaded) {
|
|
9155
|
-
this._applyMap(slot, asset.resource);
|
|
9156
|
-
return;
|
|
9157
|
-
}
|
|
9158
|
-
this._mapHandles.set(slot, asset.once('load', () => {
|
|
9159
|
-
this._mapHandles.delete(slot);
|
|
9160
|
-
this._applyMap(slot, asset.resource);
|
|
9161
|
-
}));
|
|
9337
|
+
binding.bind(id, {
|
|
9338
|
+
load: (asset) => this._applyMap(slot, asset.resource)
|
|
9339
|
+
});
|
|
9162
9340
|
}
|
|
9163
9341
|
/**
|
|
9164
9342
|
* @param slot - The material property to write.
|
|
@@ -11054,8 +11232,9 @@ class RenderComponentElement extends ComponentElement {
|
|
|
11054
11232
|
};
|
|
11055
11233
|
}
|
|
11056
11234
|
/**
|
|
11057
|
-
* Gets the underlying PlayCanvas render component.
|
|
11058
|
-
*
|
|
11235
|
+
* Gets the underlying PlayCanvas render component. `null` until the element is
|
|
11236
|
+
* ready — see {@link ComponentElement.component}.
|
|
11237
|
+
* @returns The render component, or `null`.
|
|
11059
11238
|
*/
|
|
11060
11239
|
get component() {
|
|
11061
11240
|
return super.component;
|
|
@@ -11226,8 +11405,9 @@ class RigidBodyComponentElement extends ComponentElement {
|
|
|
11226
11405
|
};
|
|
11227
11406
|
}
|
|
11228
11407
|
/**
|
|
11229
|
-
* Gets the underlying PlayCanvas rigidbody component.
|
|
11230
|
-
*
|
|
11408
|
+
* Gets the underlying PlayCanvas rigidbody component. `null` until the element is
|
|
11409
|
+
* ready — see {@link ComponentElement.component}.
|
|
11410
|
+
* @returns The rigidbody component, or `null`.
|
|
11231
11411
|
*/
|
|
11232
11412
|
get component() {
|
|
11233
11413
|
return super.component;
|
|
@@ -11406,8 +11586,9 @@ class ScreenComponentElement extends ComponentElement {
|
|
|
11406
11586
|
};
|
|
11407
11587
|
}
|
|
11408
11588
|
/**
|
|
11409
|
-
* Gets the underlying PlayCanvas screen component.
|
|
11410
|
-
*
|
|
11589
|
+
* Gets the underlying PlayCanvas screen component. `null` until the element is
|
|
11590
|
+
* ready — see {@link ComponentElement.component}.
|
|
11591
|
+
* @returns The screen component, or `null`.
|
|
11411
11592
|
*/
|
|
11412
11593
|
get component() {
|
|
11413
11594
|
return super.component;
|
|
@@ -11564,8 +11745,9 @@ class ScrollbarComponentElement extends ComponentElement {
|
|
|
11564
11745
|
return data;
|
|
11565
11746
|
}
|
|
11566
11747
|
/**
|
|
11567
|
-
* Gets the underlying PlayCanvas scrollbar component.
|
|
11568
|
-
*
|
|
11748
|
+
* Gets the underlying PlayCanvas scrollbar component. `null` until the element is
|
|
11749
|
+
* ready — see {@link ComponentElement.component}.
|
|
11750
|
+
* @returns The scrollbar component, or `null`.
|
|
11569
11751
|
*/
|
|
11570
11752
|
get component() {
|
|
11571
11753
|
return super.component;
|
|
@@ -11741,8 +11923,9 @@ class ScrollViewComponentElement extends ComponentElement {
|
|
|
11741
11923
|
return data;
|
|
11742
11924
|
}
|
|
11743
11925
|
/**
|
|
11744
|
-
* Gets the underlying PlayCanvas scroll view component.
|
|
11745
|
-
*
|
|
11926
|
+
* Gets the underlying PlayCanvas scroll view component. `null` until the element is
|
|
11927
|
+
* ready — see {@link ComponentElement.component}.
|
|
11928
|
+
* @returns The scroll view component, or `null`.
|
|
11746
11929
|
*/
|
|
11747
11930
|
get component() {
|
|
11748
11931
|
return super.component;
|
|
@@ -12894,8 +13077,9 @@ class ScriptComponentElement extends ComponentElement {
|
|
|
12894
13077
|
super.disconnectedCallback?.();
|
|
12895
13078
|
}
|
|
12896
13079
|
/**
|
|
12897
|
-
* Gets the underlying PlayCanvas script component.
|
|
12898
|
-
*
|
|
13080
|
+
* Gets the underlying PlayCanvas script component. `null` until the element is
|
|
13081
|
+
* ready — see {@link ComponentElement.component}.
|
|
13082
|
+
* @returns The script component, or `null`.
|
|
12899
13083
|
*/
|
|
12900
13084
|
get component() {
|
|
12901
13085
|
return super.component;
|
|
@@ -12941,8 +13125,9 @@ class SoundComponentElement extends ComponentElement {
|
|
|
12941
13125
|
};
|
|
12942
13126
|
}
|
|
12943
13127
|
/**
|
|
12944
|
-
* Gets the underlying PlayCanvas sound component.
|
|
12945
|
-
*
|
|
13128
|
+
* Gets the underlying PlayCanvas sound component. `null` until the element is
|
|
13129
|
+
* ready — see {@link ComponentElement.component}.
|
|
13130
|
+
* @returns The sound component, or `null`.
|
|
12946
13131
|
*/
|
|
12947
13132
|
get component() {
|
|
12948
13133
|
return super.component;
|
|
@@ -13423,8 +13608,9 @@ class GSplatComponentElement extends ComponentElement {
|
|
|
13423
13608
|
};
|
|
13424
13609
|
}
|
|
13425
13610
|
/**
|
|
13426
|
-
* Gets the underlying PlayCanvas gsplat component.
|
|
13427
|
-
*
|
|
13611
|
+
* Gets the underlying PlayCanvas gsplat component. `null` until the element is
|
|
13612
|
+
* ready — see {@link ComponentElement.component}.
|
|
13613
|
+
* @returns The gsplat component, or `null`.
|
|
13428
13614
|
*/
|
|
13429
13615
|
get component() {
|
|
13430
13616
|
return super.component;
|
|
@@ -14717,31 +14903,27 @@ class SkyElement extends AsyncElement {
|
|
|
14717
14903
|
_appElement = null;
|
|
14718
14904
|
/**
|
|
14719
14905
|
* Incremented on every new load and on disconnect, and captured by a load when it starts. A
|
|
14720
|
-
* load that resumes from an await
|
|
14721
|
-
*
|
|
14906
|
+
* load that resumes from an await abandons itself if the value has moved on, so a superseded
|
|
14907
|
+
* load cannot generate a skybox for a scene it no longer configures. The asset subscription
|
|
14908
|
+
* itself is guarded by the binding below.
|
|
14722
14909
|
*/
|
|
14723
14910
|
_loadGeneration = 0;
|
|
14724
14911
|
/**
|
|
14725
|
-
*
|
|
14726
|
-
*
|
|
14727
|
-
* than leave it registered until the asset loads (or forever, if it never does).
|
|
14912
|
+
* Watches the current texture asset while it loads. Starting a new load or disconnecting
|
|
14913
|
+
* cancels it, so a superseded texture can never generate the skybox.
|
|
14728
14914
|
*/
|
|
14729
|
-
|
|
14915
|
+
_binding = new AssetBinding();
|
|
14730
14916
|
connectedCallback() {
|
|
14731
14917
|
this._loadSkybox();
|
|
14732
14918
|
this._onReady();
|
|
14733
14919
|
}
|
|
14734
14920
|
disconnectedCallback() {
|
|
14735
14921
|
this._loadGeneration++;
|
|
14736
|
-
this.
|
|
14922
|
+
this._binding.cancel();
|
|
14737
14923
|
this._unloadSkybox();
|
|
14738
14924
|
this._appElement = null;
|
|
14739
14925
|
this._resetReady();
|
|
14740
14926
|
}
|
|
14741
|
-
_detachLoadHandler() {
|
|
14742
|
-
this._loadHandle?.off();
|
|
14743
|
-
this._loadHandle = null;
|
|
14744
|
-
}
|
|
14745
14927
|
_generateSkybox(asset) {
|
|
14746
14928
|
if (!this._scene)
|
|
14747
14929
|
return;
|
|
@@ -14773,7 +14955,7 @@ class SkyElement extends AsyncElement {
|
|
|
14773
14955
|
async _loadSkybox() {
|
|
14774
14956
|
// Supersede any load already in flight - only the newest load may generate the skybox
|
|
14775
14957
|
const generation = ++this._loadGeneration;
|
|
14776
|
-
this.
|
|
14958
|
+
this._binding.cancel();
|
|
14777
14959
|
const appElement = await this.closestApp?.ready();
|
|
14778
14960
|
// The element may have been removed, or another load started, while we waited
|
|
14779
14961
|
if (generation !== this._loadGeneration) {
|
|
@@ -14784,26 +14966,18 @@ class SkyElement extends AsyncElement {
|
|
|
14784
14966
|
return;
|
|
14785
14967
|
}
|
|
14786
14968
|
this._appElement = appElement;
|
|
14787
|
-
|
|
14788
|
-
|
|
14969
|
+
// The scene is only adopted once the reference resolves: an unresolved id must leave the
|
|
14970
|
+
// scene untouched, or this element's teardown would destroy a skybox it never created.
|
|
14971
|
+
// The bind below repeats the resolution, which useAsset documents as free - it cannot
|
|
14972
|
+
// happen after the bind, because a loaded asset delivers before bind returns and
|
|
14973
|
+
// _generateSkybox needs the scene by then.
|
|
14974
|
+
if (!useAsset(this._asset)) {
|
|
14789
14975
|
return;
|
|
14790
14976
|
}
|
|
14791
14977
|
this._scene = app.scene;
|
|
14792
|
-
|
|
14793
|
-
this._generateSkybox(asset)
|
|
14794
|
-
}
|
|
14795
|
-
else {
|
|
14796
|
-
// The generation is re-checked even though a superseded handler is detached: the
|
|
14797
|
-
// detach relies on how the engine's event emitter treats removal, while the check
|
|
14798
|
-
// holds on its own.
|
|
14799
|
-
this._loadHandle = asset.once('load', () => {
|
|
14800
|
-
this._loadHandle = null;
|
|
14801
|
-
if (generation !== this._loadGeneration) {
|
|
14802
|
-
return;
|
|
14803
|
-
}
|
|
14804
|
-
this._generateSkybox(asset);
|
|
14805
|
-
});
|
|
14806
|
-
}
|
|
14978
|
+
this._binding.bind(this._asset, {
|
|
14979
|
+
load: (asset) => this._generateSkybox(asset)
|
|
14980
|
+
});
|
|
14807
14981
|
}
|
|
14808
14982
|
_unloadSkybox() {
|
|
14809
14983
|
const scene = this._scene;
|