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