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