@playcanvas/web-components 0.15.0 → 0.16.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/components/anim-clip.d.cts +127 -0
- package/dist/components/anim-clip.d.ts +127 -0
- package/dist/components/anim-component.d.cts +207 -0
- package/dist/components/anim-component.d.ts +207 -0
- package/dist/custom-elements.json +532 -0
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/pwc.cjs +1527 -638
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +1527 -638
- 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 +1527 -640
- package/dist/pwc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +59 -0
- package/dist/web-types.json +156 -1
- package/package.json +1 -1
- package/src/components/anim-clip.ts +395 -0
- package/src/components/anim-component.ts +649 -0
- package/src/index.ts +6 -0
- package/src/model.ts +0 -7
package/dist/pwc.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, createGraphicsDevice, AppOptions, Keyboard, Mouse, ElementInput, AnimComponentSystem, AnimationComponentSystem, AudioListenerComponentSystem, ButtonComponentSystem, CameraComponentSystem, CollisionComponentSystem, ElementComponentSystem, GSplatComponentSystem, JointComponentSystem, LayoutChildComponentSystem, LayoutGroupComponentSystem, LightComponentSystem, ModelComponentSystem, ParticleSystemComponentSystem, RenderComponentSystem, RigidBodyComponentSystem, ScreenComponentSystem, ScriptComponentSystem, ScrollbarComponentSystem, ScrollViewComponentSystem, SoundComponentSystem, SpriteComponentSystem, ZoneComponentSystem, AnimClipHandler, AnimationHandler, AnimStateGraphHandler, AudioHandler, BinaryHandler, CssHandler, ContainerHandler, CubemapHandler, FolderHandler, FontHandler, GSplatHandler, HierarchyHandler, HtmlHandler, JsonHandler, MaterialHandler, ModelHandler, RenderHandler, ScriptHandler, SceneHandler, ShaderHandler, SpriteHandler, TemplateHandler, TextHandler, TextureAtlasHandler, TextureHandler, SoundManager, Lightmapper, BatchManager, XrManager, AppBase, FILLMODE_NONE, RESOLUTION_AUTO, Picker, MeshInstance, Entity, Asset, SPRITE_RENDERMODE_SIMPLE, FILTER_LINEAR_MIPMAP_LINEAR, FILTER_LINEAR, ADDRESS_REPEAT, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, ADDRESS_CLAMP_TO_EDGE, ADDRESS_MIRRORED_REPEAT, FILTER_NEAREST, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_LINEAR_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, BUTTON_TRANSITION_MODE_TINT, BUTTON_TRANSITION_MODE_SPRITE_CHANGE, TONEMAP_NONE, PROJECTION_PERSPECTIVE, GAMMA_SRGB, GAMMA_NONE, XRTYPE_VR, TONEMAP_LINEAR, TONEMAP_FILMIC, TONEMAP_HEJL, TONEMAP_ACES, TONEMAP_ACES2, TONEMAP_NEUTRAL, PROJECTION_ORTHOGRAPHIC, ORIENTATION_HORIZONTAL, FITTING_NONE, FITTING_STRETCH, FITTING_SHRINK, FITTING_BOTH, ORIENTATION_VERTICAL, SHADOW_PCF3_32F, SHADOW_PCF1_16F, SHADOW_PCF1_32F, SHADOW_PCF3_16F, SHADOW_PCF5_16F, SHADOW_PCF5_32F, SHADOW_VSM_16F, SHADOW_VSM_32F, SHADOW_PCSS_32F, StandardMaterial, BLEND_NONE, CULLFACE_BACK, FRESNEL_SCHLICK, SPECOCC_AO, BLEND_NORMAL, BLEND_ADDITIVE, BLEND_ADDITIVEALPHA, BLEND_PREMULTIPLIED, BLEND_MULTIPLICATIVE, BLEND_MULTIPLICATIVE2X, BLEND_SCREEN, BLEND_MIN, BLEND_MAX, BLEND_SUBTRACTIVE, CULLFACE_NONE, CULLFACE_FRONT, CULLFACE_FRONTANDBACK, FRESNEL_NONE, SPECOCC_NONE, SPECOCC_GLOSSDEPENDENT, SCALEMODE_NONE, SCALEMODE_BLEND, SCROLL_MODE_BOUNCE, SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED, SCROLLBAR_VISIBILITY_SHOW_ALWAYS, SCROLL_MODE_CLAMP, SCROLL_MODE_INFINITE, EnvLighting, LAYERID_SKYBOX } from 'playcanvas';
|
|
1
|
+
import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, createGraphicsDevice, AppOptions, Keyboard, Mouse, ElementInput, AnimComponentSystem, AnimationComponentSystem, AudioListenerComponentSystem, ButtonComponentSystem, CameraComponentSystem, CollisionComponentSystem, ElementComponentSystem, GSplatComponentSystem, JointComponentSystem, LayoutChildComponentSystem, LayoutGroupComponentSystem, LightComponentSystem, ModelComponentSystem, ParticleSystemComponentSystem, RenderComponentSystem, RigidBodyComponentSystem, ScreenComponentSystem, ScriptComponentSystem, ScrollbarComponentSystem, ScrollViewComponentSystem, SoundComponentSystem, SpriteComponentSystem, ZoneComponentSystem, AnimClipHandler, AnimationHandler, AnimStateGraphHandler, AudioHandler, BinaryHandler, CssHandler, ContainerHandler, CubemapHandler, FolderHandler, FontHandler, GSplatHandler, HierarchyHandler, HtmlHandler, JsonHandler, MaterialHandler, ModelHandler, RenderHandler, ScriptHandler, SceneHandler, ShaderHandler, SpriteHandler, TemplateHandler, TextHandler, TextureAtlasHandler, TextureHandler, SoundManager, Lightmapper, BatchManager, XrManager, AppBase, FILLMODE_NONE, RESOLUTION_AUTO, Picker, MeshInstance, Entity, Asset, SPRITE_RENDERMODE_SIMPLE, FILTER_LINEAR_MIPMAP_LINEAR, FILTER_LINEAR, ADDRESS_REPEAT, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, ADDRESS_CLAMP_TO_EDGE, ADDRESS_MIRRORED_REPEAT, FILTER_NEAREST, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_LINEAR_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, AnimTrack, ANIM_CONTROL_STATES, BUTTON_TRANSITION_MODE_TINT, BUTTON_TRANSITION_MODE_SPRITE_CHANGE, TONEMAP_NONE, PROJECTION_PERSPECTIVE, GAMMA_SRGB, GAMMA_NONE, XRTYPE_VR, TONEMAP_LINEAR, TONEMAP_FILMIC, TONEMAP_HEJL, TONEMAP_ACES, TONEMAP_ACES2, TONEMAP_NEUTRAL, PROJECTION_ORTHOGRAPHIC, ORIENTATION_HORIZONTAL, FITTING_NONE, FITTING_STRETCH, FITTING_SHRINK, FITTING_BOTH, ORIENTATION_VERTICAL, SHADOW_PCF3_32F, SHADOW_PCF1_16F, SHADOW_PCF1_32F, SHADOW_PCF3_16F, SHADOW_PCF5_16F, SHADOW_PCF5_32F, SHADOW_VSM_16F, SHADOW_VSM_32F, SHADOW_PCSS_32F, StandardMaterial, BLEND_NONE, CULLFACE_BACK, FRESNEL_SCHLICK, SPECOCC_AO, BLEND_NORMAL, BLEND_ADDITIVE, BLEND_ADDITIVEALPHA, BLEND_PREMULTIPLIED, BLEND_MULTIPLICATIVE, BLEND_MULTIPLICATIVE2X, BLEND_SCREEN, BLEND_MIN, BLEND_MAX, BLEND_SUBTRACTIVE, CULLFACE_NONE, CULLFACE_FRONT, CULLFACE_FRONTANDBACK, FRESNEL_NONE, SPECOCC_NONE, SPECOCC_GLOSSDEPENDENT, SCALEMODE_NONE, SCALEMODE_BLEND, SCROLL_MODE_BOUNCE, SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED, SCROLLBAR_VISIBILITY_SHOW_ALWAYS, SCROLL_MODE_CLAMP, SCROLL_MODE_INFINITE, EnvLighting, LAYERID_SKYBOX } from 'playcanvas';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Base class for all PlayCanvas Web Components that initialize asynchronously.
|
|
@@ -2924,214 +2924,1391 @@ const useAsset = (id) => {
|
|
|
2924
2924
|
return asset;
|
|
2925
2925
|
};
|
|
2926
2926
|
|
|
2927
|
+
/**
|
|
2928
|
+
* Formats one line of the printable hierarchy: the node's name, an `[index]` marker when the
|
|
2929
|
+
* name is shared by several nodes in the model, the attached component types, and the material
|
|
2930
|
+
* names of a render component.
|
|
2931
|
+
*
|
|
2932
|
+
* @param node - The node to format.
|
|
2933
|
+
* @param counts - The number of nodes bearing each name.
|
|
2934
|
+
* @returns The formatted line.
|
|
2935
|
+
*/
|
|
2936
|
+
const formatNode = (node, counts) => {
|
|
2937
|
+
const index = (counts.get(node.name) ?? 0) > 1 ? ` [${node.index}]` : '';
|
|
2938
|
+
const components = node.components.length > 0 ? ` (${node.components.join(', ')})` : '';
|
|
2939
|
+
// Braces rather than brackets: `[N]` already means a match index on this line
|
|
2940
|
+
const materials = node.materials.length > 0 ? ` {${node.materials.map((slot) => slot.name ?? 'null').join(', ')}}` : '';
|
|
2941
|
+
return `${node.name}${index}${components}${materials}`;
|
|
2942
|
+
};
|
|
2943
|
+
/**
|
|
2944
|
+
* Formats the printable form of a hierarchy subtree.
|
|
2945
|
+
*
|
|
2946
|
+
* @param root - The subtree root.
|
|
2947
|
+
* @param counts - The number of nodes bearing each name.
|
|
2948
|
+
* @returns The tree, one line per node.
|
|
2949
|
+
*/
|
|
2950
|
+
const formatHierarchy = (root, counts) => {
|
|
2951
|
+
const lines = [formatNode(root, counts)];
|
|
2952
|
+
const walk = (node, prefix) => {
|
|
2953
|
+
node.children.forEach((child, i) => {
|
|
2954
|
+
const last = i === node.children.length - 1;
|
|
2955
|
+
lines.push(`${prefix}${last ? '└─ ' : '├─ '}${formatNode(child, counts)}`);
|
|
2956
|
+
walk(child, `${prefix}${last ? ' ' : '│ '}`);
|
|
2957
|
+
});
|
|
2958
|
+
};
|
|
2959
|
+
walk(root, '');
|
|
2960
|
+
return lines.join('\n');
|
|
2961
|
+
};
|
|
2962
|
+
/**
|
|
2963
|
+
* The ModelElement interface provides properties and methods for manipulating
|
|
2964
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | `<pc-model>`} elements.
|
|
2965
|
+
* The ModelElement interface also inherits the properties and methods of the
|
|
2966
|
+
* {@link HTMLElement} interface.
|
|
2967
|
+
*
|
|
2968
|
+
* The element becomes ready once its container asset has loaded and the instantiated hierarchy has
|
|
2969
|
+
* been added to the scene — `entity` is non-null by then. A failed load also settles readiness,
|
|
2970
|
+
* with `entity` remaining `null`: readiness means the load settled, not that it succeeded — listen
|
|
2971
|
+
* for `error`, or check `entity`, to tell the outcomes apart. Changing `asset` re-arms readiness
|
|
2972
|
+
* and instantiates anew, so a `ready()` obtained after the change resolves against the new
|
|
2973
|
+
* hierarchy. A `pc-model` outside a `pc-app`, or referencing an unknown asset id, warns and never
|
|
2974
|
+
* becomes ready.
|
|
2975
|
+
*
|
|
2976
|
+
* @fires {Event} load - Fired each time a container asset finishes instantiating, including
|
|
2977
|
+
* re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
|
|
2978
|
+
* capture-phase listener on an ancestor.
|
|
2979
|
+
* @fires {ErrorEvent} error - Fired when the container asset fails to load, with the engine's
|
|
2980
|
+
* error in `message`. Does not bubble. The element still becomes ready — readiness means the load
|
|
2981
|
+
* settled, not that it succeeded.
|
|
2982
|
+
*/
|
|
2983
|
+
class ModelElement extends AsyncElement {
|
|
2984
|
+
_asset = '';
|
|
2985
|
+
_entity = null;
|
|
2986
|
+
/**
|
|
2987
|
+
* Incremented on every new load and on disconnect, and captured by a load when it starts. A
|
|
2988
|
+
* load that resumes from an await or a load callback abandons itself if the value has moved
|
|
2989
|
+
* on, so a superseded load can neither instantiate a second entity nor parent one that has
|
|
2990
|
+
* since been destroyed.
|
|
2991
|
+
*/
|
|
2992
|
+
_loadGeneration = 0;
|
|
2993
|
+
/**
|
|
2994
|
+
* The pending asset subscriptions of the current load, if it is waiting for its asset. Held
|
|
2995
|
+
* so that whatever supersedes the load can detach the handlers from the asset, rather than
|
|
2996
|
+
* leave them registered until the asset settles (or forever, if it never does).
|
|
2997
|
+
*/
|
|
2998
|
+
_loadHandle = null;
|
|
2999
|
+
_errorHandle = null;
|
|
3000
|
+
/**
|
|
3001
|
+
* The root entity of the instantiated model. `null` until the container asset has loaded
|
|
3002
|
+
* and been instantiated, and again once the element has been removed from the document.
|
|
3003
|
+
* @returns The model's root entity, or `null`.
|
|
3004
|
+
*/
|
|
3005
|
+
get entity() {
|
|
3006
|
+
return this._entity;
|
|
3007
|
+
}
|
|
3008
|
+
/**
|
|
3009
|
+
* Returns a snapshot of the instantiated node tree, or `null` while there is none (the
|
|
3010
|
+
* container asset has not loaded, or the element has left the document). One call grounds a
|
|
3011
|
+
* session — a browser console, a test, an agent — in the vocabulary `pc-node` binding
|
|
3012
|
+
* resolves against: the instantiated names ({@link HierarchyNode.name}), paths, match
|
|
3013
|
+
* indices, attached component types and the material assignments of render components
|
|
3014
|
+
* ({@link HierarchyNode.materials}). `String(...)` of the result, or of any node in it,
|
|
3015
|
+
* is the printable form.
|
|
3016
|
+
*
|
|
3017
|
+
* The snapshot is plain data, computed afresh each call: it does not follow later changes
|
|
3018
|
+
* to the hierarchy, and mutating it changes nothing.
|
|
3019
|
+
*
|
|
3020
|
+
* @returns The root of the instantiated node tree, or `null`.
|
|
3021
|
+
*/
|
|
3022
|
+
hierarchy() {
|
|
3023
|
+
const root = this._entity;
|
|
3024
|
+
if (!root) {
|
|
3025
|
+
return null;
|
|
3026
|
+
}
|
|
3027
|
+
// Ordinals are assigned in the traversal resolution searches — pre-order depth-first
|
|
3028
|
+
// from the model root, the root itself included — so each node's index is exactly what
|
|
3029
|
+
// a pc-node's index attribute selects. Once the walk completes, the map holds the total
|
|
3030
|
+
// count per name, which is what the printable form reads to annotate only shared names.
|
|
3031
|
+
const ordinals = new Map();
|
|
3032
|
+
const describe = (entity, pathBelowRoot) => {
|
|
3033
|
+
const index = ordinals.get(entity.name) ?? 0;
|
|
3034
|
+
ordinals.set(entity.name, index + 1);
|
|
3035
|
+
const node = {
|
|
3036
|
+
name: entity.name,
|
|
3037
|
+
// The root has no path below itself; its own name stands in, as it does for
|
|
3038
|
+
// the path a pc-node bound to the root reports.
|
|
3039
|
+
path: pathBelowRoot || entity.name,
|
|
3040
|
+
index,
|
|
3041
|
+
// A plain GraphNode grafted into the hierarchy has no component storage
|
|
3042
|
+
components: Object.keys(entity.c ?? {}).sort(),
|
|
3043
|
+
materials: (entity.render?.meshInstances ?? []).map((meshInstance, slot) => ({
|
|
3044
|
+
index: slot,
|
|
3045
|
+
name: meshInstance.material?.name ?? null
|
|
3046
|
+
})),
|
|
3047
|
+
children: entity.children.map((child) => describe(child, pathBelowRoot ? `${pathBelowRoot}/${child.name}` : child.name))
|
|
3048
|
+
};
|
|
3049
|
+
// Non-enumerable, keeping the snapshot plain data under JSON.stringify, spreads and
|
|
3050
|
+
// key enumeration. Deferred to call time, by which the ordinal map holds its totals.
|
|
3051
|
+
Object.defineProperty(node, 'toString', {
|
|
3052
|
+
enumerable: false,
|
|
3053
|
+
value: () => formatHierarchy(node, ordinals)
|
|
3054
|
+
});
|
|
3055
|
+
return node;
|
|
3056
|
+
};
|
|
3057
|
+
return describe(root, '');
|
|
3058
|
+
}
|
|
3059
|
+
connectedCallback() {
|
|
3060
|
+
// A model outside an application is inert and never becomes ready, so awaiting it hangs.
|
|
3061
|
+
// Warn rather than fail silently, naming the parent it requires, as every other misplaced
|
|
3062
|
+
// element does.
|
|
3063
|
+
if (!this.closestApp) {
|
|
3064
|
+
const label = this._asset ? ` '${this._asset}'` : '';
|
|
3065
|
+
console.warn(`pc-model${label} must be a descendant of pc-app - model not created`);
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
this._loadModel();
|
|
3069
|
+
}
|
|
3070
|
+
disconnectedCallback() {
|
|
3071
|
+
this._loadGeneration++;
|
|
3072
|
+
this._detachLoadHandlers();
|
|
3073
|
+
this._unloadModel();
|
|
3074
|
+
this._resetReady();
|
|
3075
|
+
}
|
|
3076
|
+
_detachLoadHandlers() {
|
|
3077
|
+
this._loadHandle?.off();
|
|
3078
|
+
this._loadHandle = null;
|
|
3079
|
+
this._errorHandle?.off();
|
|
3080
|
+
this._errorHandle = null;
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* Resolves readiness and dispatches the `load` event. Called once the instantiated hierarchy
|
|
3084
|
+
* has been parented — readiness means "in the scene graph", matching `pc-entity`, so a ready
|
|
3085
|
+
* model's entity always has world transforms.
|
|
3086
|
+
*/
|
|
3087
|
+
_announceLoad() {
|
|
3088
|
+
this._onReady();
|
|
3089
|
+
this.dispatchEvent(new Event('load'));
|
|
3090
|
+
}
|
|
3091
|
+
_instantiate(container) {
|
|
3092
|
+
const generation = this._loadGeneration;
|
|
3093
|
+
const entity = container.instantiateRenderEntity();
|
|
3094
|
+
this._entity = entity;
|
|
3095
|
+
// The parent's readiness re-arms when it is torn down, so these can resume in a later
|
|
3096
|
+
// connection cycle. The entity is captured above and the generation re-checked, so a
|
|
3097
|
+
// stale resume cannot parent an entity a newer cycle has already destroyed.
|
|
3098
|
+
const parentEntityElement = this.closestEntity;
|
|
3099
|
+
if (parentEntityElement) {
|
|
3100
|
+
parentEntityElement.ready().then(() => {
|
|
3101
|
+
if (generation !== this._loadGeneration) {
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
parentEntityElement.entity.addChild(entity);
|
|
3105
|
+
this._announceLoad();
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
else {
|
|
3109
|
+
const appElement = this.closestApp;
|
|
3110
|
+
if (appElement) {
|
|
3111
|
+
appElement.ready().then(() => {
|
|
3112
|
+
if (generation !== this._loadGeneration) {
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
appElement.app.root.addChild(entity);
|
|
3116
|
+
this._announceLoad();
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
async _loadModel() {
|
|
3122
|
+
this._unloadModel();
|
|
3123
|
+
// Supersede any load already in flight - only the newest load may instantiate
|
|
3124
|
+
const generation = ++this._loadGeneration;
|
|
3125
|
+
this._detachLoadHandlers();
|
|
3126
|
+
// Re-arm readiness so a waiter obtained after an asset change resolves against the new
|
|
3127
|
+
// hierarchy. A no-op on first connection, where readiness is still pending.
|
|
3128
|
+
this._resetReady();
|
|
3129
|
+
const appElement = this.closestApp;
|
|
3130
|
+
if (!appElement) {
|
|
3131
|
+
// Outside pc-app; connectedCallback already warned. Reached through the asset setter.
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
await appElement.ready();
|
|
3135
|
+
// The element may have been removed, or another load started, while we waited
|
|
3136
|
+
if (generation !== this._loadGeneration) {
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
const asset = useAsset(this._asset);
|
|
3140
|
+
if (!asset) {
|
|
3141
|
+
// An empty id is a legitimate transient (the asset may be assigned later); a
|
|
3142
|
+
// non-empty one that resolves to nothing is a dead end - say so rather than staying
|
|
3143
|
+
// silently pending.
|
|
3144
|
+
if (this._asset) {
|
|
3145
|
+
console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
|
|
3146
|
+
}
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
if (asset.loaded) {
|
|
3150
|
+
this._instantiate(asset.resource);
|
|
3151
|
+
}
|
|
3152
|
+
else {
|
|
3153
|
+
// The generation is re-checked even though a superseded handler is detached: the
|
|
3154
|
+
// detach relies on how the engine's event emitter treats removal, while the check
|
|
3155
|
+
// holds on its own. Whichever of load/error fires first detaches the other.
|
|
3156
|
+
this._loadHandle = asset.once('load', () => {
|
|
3157
|
+
this._detachLoadHandlers();
|
|
3158
|
+
if (generation !== this._loadGeneration) {
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
this._instantiate(asset.resource);
|
|
3162
|
+
});
|
|
3163
|
+
this._errorHandle = asset.once('error', (err) => {
|
|
3164
|
+
this._detachLoadHandlers();
|
|
3165
|
+
if (generation !== this._loadGeneration) {
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
// A failed load settles readiness with a null entity, mirroring pc-asset:
|
|
3169
|
+
// readiness means the load settled, not that it succeeded.
|
|
3170
|
+
this.dispatchEvent(new ErrorEvent('error', {
|
|
3171
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3172
|
+
}));
|
|
3173
|
+
this._onReady();
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
_unloadModel() {
|
|
3178
|
+
this._entity?.destroy();
|
|
3179
|
+
this._entity = null;
|
|
3180
|
+
}
|
|
3181
|
+
/**
|
|
3182
|
+
* Sets the id of the `pc-asset` to use for the model.
|
|
3183
|
+
* @param value - The asset ID.
|
|
3184
|
+
*/
|
|
3185
|
+
set asset(value) {
|
|
3186
|
+
this._asset = value;
|
|
3187
|
+
if (this.isConnected) {
|
|
3188
|
+
this._loadModel();
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* Gets the id of the `pc-asset` to use for the model.
|
|
3193
|
+
* @returns The asset ID.
|
|
3194
|
+
*/
|
|
3195
|
+
get asset() {
|
|
3196
|
+
return this._asset;
|
|
3197
|
+
}
|
|
3198
|
+
static get observedAttributes() {
|
|
3199
|
+
return ['asset'];
|
|
3200
|
+
}
|
|
3201
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
3202
|
+
switch (name) {
|
|
3203
|
+
case 'asset':
|
|
3204
|
+
this.asset = newValue ?? '';
|
|
3205
|
+
break;
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
customElements.define('pc-model', ModelElement);
|
|
3210
|
+
|
|
2927
3211
|
/**
|
|
2928
3212
|
* Represents a component in the PlayCanvas engine.
|
|
2929
3213
|
*
|
|
2930
3214
|
* @category Components
|
|
2931
3215
|
*/
|
|
2932
|
-
class ComponentElement extends AsyncElement {
|
|
2933
|
-
_componentName;
|
|
2934
|
-
_enabled = true;
|
|
2935
|
-
_component = null;
|
|
2936
|
-
_appElement = null;
|
|
3216
|
+
class ComponentElement extends AsyncElement {
|
|
3217
|
+
_componentName;
|
|
3218
|
+
_enabled = true;
|
|
3219
|
+
_component = null;
|
|
3220
|
+
_appElement = null;
|
|
3221
|
+
/**
|
|
3222
|
+
* The element hosting this component, held so the host's readiness cycles can be observed
|
|
3223
|
+
* even after `closestEntity` would no longer resolve (during teardown).
|
|
3224
|
+
*/
|
|
3225
|
+
_hostElement = null;
|
|
3226
|
+
/**
|
|
3227
|
+
* The listener re-applying this component when the host's readiness cycles. Held for
|
|
3228
|
+
* removal on disconnect.
|
|
3229
|
+
*/
|
|
3230
|
+
_hostReadyListener = null;
|
|
3231
|
+
/**
|
|
3232
|
+
* Incremented on every connect and disconnect. connectedCallback captures the value on entry
|
|
3233
|
+
* and abandons itself wherever it resumes from an await if the value has moved on — so a
|
|
3234
|
+
* callback whose element was removed cannot act on a torn-down tree, and one whose element
|
|
3235
|
+
* was removed and re-inserted (which runs a callback of its own) cannot add the component a
|
|
3236
|
+
* second time.
|
|
3237
|
+
*/
|
|
3238
|
+
_connectionGeneration = 0;
|
|
3239
|
+
/**
|
|
3240
|
+
* Creates a new ComponentElement instance.
|
|
3241
|
+
*
|
|
3242
|
+
* @param componentName - The name of the component.
|
|
3243
|
+
* @ignore
|
|
3244
|
+
*/
|
|
3245
|
+
constructor(componentName) {
|
|
3246
|
+
super();
|
|
3247
|
+
this._componentName = componentName;
|
|
3248
|
+
}
|
|
3249
|
+
/**
|
|
3250
|
+
* Returns the data the component is created with. Overridden by subclasses to supply the
|
|
3251
|
+
* initial values of their cached properties.
|
|
3252
|
+
*
|
|
3253
|
+
* @returns The initial component data.
|
|
3254
|
+
*/
|
|
3255
|
+
getInitialComponentData() {
|
|
3256
|
+
return {};
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3259
|
+
* Creates the component on the host's current entity, removing it first from a previous
|
|
3260
|
+
* entity that is still alive (a retargeted `<pc-node>` moves its decorations with it). When
|
|
3261
|
+
* the entity already has a component of this type — a glTF node arriving with its authored
|
|
3262
|
+
* `render` component, say — warns and leaves `component` null. The element-level warning is
|
|
3263
|
+
* load-bearing: the engine's own duplicate-addComponent warning is Debug-stripped from
|
|
3264
|
+
* production builds, which would otherwise leave a silent null.
|
|
3265
|
+
*/
|
|
3266
|
+
_applyComponent() {
|
|
3267
|
+
const entity = this._hostElement?.entity ?? null;
|
|
3268
|
+
if (this._component && this._component.entity === entity) {
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
// A retarget leaves the previous component on a still-live entity - remove it so the
|
|
3272
|
+
// decoration follows the element, or vanishes with a dissolved binding. A destroyed
|
|
3273
|
+
// entity took its components with it.
|
|
3274
|
+
const previous = this._component;
|
|
3275
|
+
if (previous?.entity && previous.entity.c[this._componentName] === previous) {
|
|
3276
|
+
previous.entity.removeComponent(this._componentName);
|
|
3277
|
+
}
|
|
3278
|
+
this._component = null;
|
|
3279
|
+
if (!entity) {
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
if (entity.c[this._componentName]) {
|
|
3283
|
+
const label = this.id ? ` '${this.id}'` : '';
|
|
3284
|
+
console.warn(`${this.tagName.toLowerCase()}${label} - '${entity.name}' already has a '${this._componentName}' component - component not added`);
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
this._component = entity.addComponent(this._componentName, this.getInitialComponentData());
|
|
3288
|
+
}
|
|
3289
|
+
async _addComponent() {
|
|
3290
|
+
const generation = this._connectionGeneration;
|
|
3291
|
+
const entityElement = this.closestEntity;
|
|
3292
|
+
if (!entityElement) {
|
|
3293
|
+
// A component can only exist on an entity, so an element placed outside one is inert.
|
|
3294
|
+
// It still becomes ready (with a null `component`), so warn rather than fail silently
|
|
3295
|
+
const label = this.id ? ` '${this.id}'` : '';
|
|
3296
|
+
console.warn(`${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity - component not added`);
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
3299
|
+
await entityElement.ready();
|
|
3300
|
+
// The element may have been removed, or removed and re-inserted, while the entity became
|
|
3301
|
+
// ready — the component belongs to the connection that owns the current generation.
|
|
3302
|
+
if (generation !== this._connectionGeneration) {
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
this._hostElement = entityElement;
|
|
3306
|
+
this._applyComponent();
|
|
3307
|
+
// Re-apply when the host's readiness cycles without this element disconnecting: a
|
|
3308
|
+
// `<pc-node>` rebinding after its model reloads or retargets, or a re-created entity.
|
|
3309
|
+
// The 'ready' event bubbles, so events from descendants pass through this host - only
|
|
3310
|
+
// the host's own cycles count. Readiness is cycled here too, so decorations one level
|
|
3311
|
+
// down re-apply the same way.
|
|
3312
|
+
this._hostReadyListener = (event) => {
|
|
3313
|
+
if (event.target !== this._hostElement) {
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
if (generation !== this._connectionGeneration) {
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
this._hostCycled();
|
|
3320
|
+
};
|
|
3321
|
+
entityElement.addEventListener('ready', this._hostReadyListener);
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* Re-evaluates this component against the host's current entity: applied to a new entity,
|
|
3325
|
+
* moved from a still-live old one, or removed when the host no longer fronts an entity at
|
|
3326
|
+
* all. Readiness follows - it cycles with a re-application and stays unresolved while the
|
|
3327
|
+
* host is unbound. Called by the host-ready listener, and directly by a `<pc-node>`
|
|
3328
|
+
* dissolving its binding: the one transition that fires no ready event to ride.
|
|
3329
|
+
*
|
|
3330
|
+
* @internal
|
|
3331
|
+
*/
|
|
3332
|
+
_hostCycled() {
|
|
3333
|
+
this._resetReady();
|
|
3334
|
+
this._applyComponent();
|
|
3335
|
+
if (this._hostElement?.entity) {
|
|
3336
|
+
this.initComponent();
|
|
3337
|
+
this._onReady();
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
/**
|
|
3341
|
+
* Configures the newly added component. Overridden by subclasses whose setup goes beyond
|
|
3342
|
+
* the initial data — child-element handling, asset resolution and the like.
|
|
3343
|
+
*/
|
|
3344
|
+
initComponent() {
|
|
3345
|
+
// optional hook
|
|
3346
|
+
}
|
|
3347
|
+
async connectedCallback() {
|
|
3348
|
+
const generation = ++this._connectionGeneration;
|
|
3349
|
+
this._appElement = this.closestApp ?? null;
|
|
3350
|
+
await this._appElement?.ready();
|
|
3351
|
+
// The element may have been removed, or removed and re-inserted, while the application
|
|
3352
|
+
// became ready. A re-insertion runs a connectedCallback of its own, so a stale resume
|
|
3353
|
+
// must not add the component alongside it.
|
|
3354
|
+
if (generation !== this._connectionGeneration) {
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
await this._addComponent();
|
|
3358
|
+
if (generation !== this._connectionGeneration) {
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
this.initComponent();
|
|
3362
|
+
this._onReady();
|
|
3363
|
+
}
|
|
3364
|
+
disconnectedCallback() {
|
|
3365
|
+
// Invalidate any connectedCallback still suspended on an await
|
|
3366
|
+
this._connectionGeneration++;
|
|
3367
|
+
if (this._hostElement && this._hostReadyListener) {
|
|
3368
|
+
this._hostElement.removeEventListener('ready', this._hostReadyListener);
|
|
3369
|
+
}
|
|
3370
|
+
this._hostElement = null;
|
|
3371
|
+
this._hostReadyListener = null;
|
|
3372
|
+
// Remove the component when the element is disconnected. Skip this when the owning
|
|
3373
|
+
// application has already been destroyed — removing a <pc-app> disconnects it before
|
|
3374
|
+
// its children, taking the component systems with it.
|
|
3375
|
+
if (this._appElement?.app && this._component?.entity) {
|
|
3376
|
+
this._component.entity.removeComponent(this._componentName);
|
|
3377
|
+
}
|
|
3378
|
+
this._component = null;
|
|
3379
|
+
this._appElement = null;
|
|
3380
|
+
this._resetReady();
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* The PlayCanvas component instance. `null` until the element is ready, and also for an
|
|
3384
|
+
* element that is not a descendant of a `<pc-entity>` — await {@link whenReady} or the
|
|
3385
|
+
* element's `ready()` promise before accessing it.
|
|
3386
|
+
* @returns The component instance, or `null`.
|
|
3387
|
+
*/
|
|
3388
|
+
get component() {
|
|
3389
|
+
return this._component;
|
|
3390
|
+
}
|
|
3391
|
+
/**
|
|
3392
|
+
* Sets the enabled state of the component.
|
|
3393
|
+
* @param value - The enabled state of the component.
|
|
3394
|
+
*/
|
|
3395
|
+
set enabled(value) {
|
|
3396
|
+
this._enabled = value;
|
|
3397
|
+
if (this.component) {
|
|
3398
|
+
this.component.enabled = value;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* Gets the enabled state of the component.
|
|
3403
|
+
* @returns The enabled state of the component.
|
|
3404
|
+
*/
|
|
3405
|
+
get enabled() {
|
|
3406
|
+
return this._enabled;
|
|
3407
|
+
}
|
|
3408
|
+
static get observedAttributes() {
|
|
3409
|
+
return ['enabled'];
|
|
3410
|
+
}
|
|
3411
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
3412
|
+
switch (name) {
|
|
3413
|
+
case 'enabled':
|
|
3414
|
+
this.enabled = parseBool(newValue, true);
|
|
3415
|
+
break;
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
/**
|
|
3421
|
+
* The AnimComponentElement interface provides properties and methods for manipulating
|
|
3422
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim/ | `<pc-anim>`} elements.
|
|
3423
|
+
* The AnimComponentElement interface also inherits the properties and methods of the
|
|
3424
|
+
* {@link HTMLElement} interface.
|
|
3425
|
+
*
|
|
3426
|
+
* The element drives animation clips over the host entity's hierarchy. Clips come from
|
|
3427
|
+
* `<pc-anim-clip>` children — or, when the element is a direct child of a `<pc-model>` and
|
|
3428
|
+
* declares no clips, every animation of that model's container asset is assigned, named by track
|
|
3429
|
+
* name, in container order. The first clip plays automatically (opt out with `activate="false"`);
|
|
3430
|
+
* switch clips declaratively through the `clip` attribute, or imperatively through {@link play}
|
|
3431
|
+
* and {@link transition}. Tracks bind to scene nodes by name, so any hierarchy whose node names
|
|
3432
|
+
* match a clip's curves can be animated — a model's skeleton is simply the common case.
|
|
3433
|
+
*
|
|
3434
|
+
* The engine reports no clip completion: a non-looping clip holds its last pose silently. Poll
|
|
3435
|
+
* the underlying {@link AnimComponent} (via {@link component}) for playback state beyond what
|
|
3436
|
+
* this element exposes.
|
|
3437
|
+
*
|
|
3438
|
+
* @category Components
|
|
3439
|
+
*/
|
|
3440
|
+
class AnimComponentElement extends ComponentElement {
|
|
3441
|
+
/**
|
|
3442
|
+
* Whether playback starts automatically once a clip is assigned.
|
|
3443
|
+
*/
|
|
3444
|
+
_activate = true;
|
|
3445
|
+
/**
|
|
3446
|
+
* The clip elements whose states are currently assigned, by clip name. The single writer of
|
|
3447
|
+
* a state: a later clip child re-using an adopted name is rejected as a duplicate.
|
|
3448
|
+
*/
|
|
3449
|
+
_assignedClips = new Map();
|
|
3450
|
+
/**
|
|
3451
|
+
* Whether the current clip set was auto-assigned from the enclosing model rather than
|
|
3452
|
+
* declared by clip children.
|
|
3453
|
+
*/
|
|
3454
|
+
_autoAssigned = false;
|
|
3455
|
+
/**
|
|
3456
|
+
* The name of the active clip.
|
|
3457
|
+
*/
|
|
3458
|
+
_clip = '';
|
|
3459
|
+
/**
|
|
3460
|
+
* The element the model-readiness listener is attached to, held so disconnection can detach
|
|
3461
|
+
* it after `closestEntity` no longer resolves.
|
|
3462
|
+
*/
|
|
3463
|
+
_modelListenerTarget = null;
|
|
3464
|
+
/**
|
|
3465
|
+
* Incremented whenever the clip source changes, and captured by an auto-assign pass on
|
|
3466
|
+
* entry — a pass resuming from an await abandons itself if the value has moved on, so a
|
|
3467
|
+
* superseded pass cannot assign clips alongside declared children or a newer pass.
|
|
3468
|
+
*/
|
|
3469
|
+
_sourceGeneration = 0;
|
|
3470
|
+
/**
|
|
3471
|
+
* The playback speed multiplier applied across all clips.
|
|
3472
|
+
*/
|
|
3473
|
+
_speed = 1;
|
|
3474
|
+
/**
|
|
3475
|
+
* The cross-fade duration of declarative clip switches, in seconds.
|
|
3476
|
+
*/
|
|
3477
|
+
_transitionTime = 0;
|
|
3478
|
+
/**
|
|
3479
|
+
* The unknown clip name already warned about, so a repeated selection of the same missing
|
|
3480
|
+
* name complains once.
|
|
3481
|
+
*/
|
|
3482
|
+
_warnedClip = null;
|
|
3483
|
+
/**
|
|
3484
|
+
* Rebinds when a model under the host announces readiness. The engine resolves each curve
|
|
3485
|
+
* once, at the first tick after assignment, and never retries — and its mesh-instance
|
|
3486
|
+
* broadcast fires before an instantiated hierarchy is parented, so a model that loads after
|
|
3487
|
+
* the clips were assigned would otherwise stay silently unbound. A re-instantiation of the
|
|
3488
|
+
* implicit clip source (the parent `<pc-model>`) means a new container, so the clip set
|
|
3489
|
+
* refreshes instead — unless every clip declares its own asset, where a rebind suffices.
|
|
3490
|
+
*/
|
|
3491
|
+
_onModelReady = (event) => {
|
|
3492
|
+
if (!(event.target instanceof ModelElement) || !this.component) {
|
|
3493
|
+
return;
|
|
3494
|
+
}
|
|
3495
|
+
if (event.target === this.parentElement) {
|
|
3496
|
+
const implicit = this._autoAssigned ||
|
|
3497
|
+
[...this._assignedClips.values()].some(clip => !clip.asset);
|
|
3498
|
+
if (implicit) {
|
|
3499
|
+
this._refreshClips();
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
this.component.rebind();
|
|
3504
|
+
};
|
|
3505
|
+
/** @ignore */
|
|
3506
|
+
constructor() {
|
|
3507
|
+
super('anim');
|
|
3508
|
+
}
|
|
3509
|
+
getInitialComponentData() {
|
|
3510
|
+
// The engine assigns creation data in key order and `activate` gates playback, so it
|
|
3511
|
+
// must precede any future key that builds layers (e.g. a state graph)
|
|
3512
|
+
return {
|
|
3513
|
+
activate: this._activate,
|
|
3514
|
+
speed: this._speed
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
initComponent() {
|
|
3518
|
+
if (!this.component) {
|
|
3519
|
+
return;
|
|
3520
|
+
}
|
|
3521
|
+
// A host readiness cycle can re-run this. An identical re-add is deduped by the DOM;
|
|
3522
|
+
// the explicit swap handles the listener target changing across connections.
|
|
3523
|
+
const host = this.closestEntity;
|
|
3524
|
+
if (host && host !== this._modelListenerTarget) {
|
|
3525
|
+
this._modelListenerTarget?.removeEventListener('ready', this._onModelReady);
|
|
3526
|
+
host.addEventListener('ready', this._onModelReady);
|
|
3527
|
+
this._modelListenerTarget = host;
|
|
3528
|
+
}
|
|
3529
|
+
this._applyClips();
|
|
3530
|
+
}
|
|
3531
|
+
disconnectedCallback() {
|
|
3532
|
+
this._modelListenerTarget?.removeEventListener('ready', this._onModelReady);
|
|
3533
|
+
this._modelListenerTarget = null;
|
|
3534
|
+
// Invalidate any auto-assign still awaiting its model, and drop the adoption
|
|
3535
|
+
// bookkeeping so a reconnection starts clean
|
|
3536
|
+
this._sourceGeneration++;
|
|
3537
|
+
this._assignedClips.clear();
|
|
3538
|
+
this._autoAssigned = false;
|
|
3539
|
+
super.disconnectedCallback();
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* The clip children in DOM order. Read afresh each pass — the DOM is the single source of
|
|
3543
|
+
* truth for the declared clip set.
|
|
3544
|
+
*/
|
|
3545
|
+
_clipElements() {
|
|
3546
|
+
return Array.from(this.querySelectorAll(':scope > pc-anim-clip'));
|
|
3547
|
+
}
|
|
3548
|
+
/**
|
|
3549
|
+
* Assigns a clip's state. Until the clip's real track resolves, the engine's own placeholder
|
|
3550
|
+
* track stands in — it keeps the layer playable, so `activate` can start playback and the
|
|
3551
|
+
* declared `clip` selection can apply before any asset has loaded.
|
|
3552
|
+
*/
|
|
3553
|
+
_assignClip(clip) {
|
|
3554
|
+
this.component.assignAnimation(clip.name, clip._track ?? AnimTrack.EMPTY, undefined, clip.speed, clip.loop);
|
|
3555
|
+
}
|
|
3556
|
+
/**
|
|
3557
|
+
* Validates a clip child and, when valid, assigns its state and starts its track resolution.
|
|
3558
|
+
*
|
|
3559
|
+
* @param clip - The clip element.
|
|
3560
|
+
* @returns Whether the clip was adopted.
|
|
3561
|
+
*/
|
|
3562
|
+
_adoptClip(clip) {
|
|
3563
|
+
const name = clip.name;
|
|
3564
|
+
if (!name) {
|
|
3565
|
+
clip._markInvalid('pc-anim-clip must have a name - clip not assigned');
|
|
3566
|
+
return false;
|
|
3567
|
+
}
|
|
3568
|
+
if (name.indexOf('.') !== -1) {
|
|
3569
|
+
clip._markInvalid(`pc-anim-clip '${name}' - '.' in a clip name is reserved for blend tree paths - clip not assigned`);
|
|
3570
|
+
return false;
|
|
3571
|
+
}
|
|
3572
|
+
if (this._assignedClips.has(name)) {
|
|
3573
|
+
clip._markInvalid(`pc-anim-clip '${name}' - an earlier clip already uses this name - clip not assigned`);
|
|
3574
|
+
return false;
|
|
3575
|
+
}
|
|
3576
|
+
this._assignedClips.set(name, clip);
|
|
3577
|
+
this._assignClip(clip);
|
|
3578
|
+
clip._resolveTrack(this);
|
|
3579
|
+
return true;
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* Assigns the current clip set: the declared clip children when there are any, otherwise the
|
|
3583
|
+
* enclosing model's clips. Runs against a fresh component after a host cycle, so the
|
|
3584
|
+
* adoption bookkeeping rebuilds from scratch.
|
|
3585
|
+
*/
|
|
3586
|
+
_applyClips(restore) {
|
|
3587
|
+
if (!this.component) {
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
this._sourceGeneration++;
|
|
3591
|
+
this._assignedClips.clear();
|
|
3592
|
+
this._autoAssigned = false;
|
|
3593
|
+
const clips = this._clipElements();
|
|
3594
|
+
if (clips.length === 0) {
|
|
3595
|
+
this._kickAutoAssign(restore);
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
for (const clip of clips) {
|
|
3599
|
+
this._adoptClip(clip);
|
|
3600
|
+
}
|
|
3601
|
+
this._applySelection(restore);
|
|
3602
|
+
}
|
|
3603
|
+
/**
|
|
3604
|
+
* Assigns every clip of the enclosing model's container, named by track name, in container
|
|
3605
|
+
* order. Names the engine cannot host — dotted (reserved for blend tree paths) or already
|
|
3606
|
+
* taken — are skipped with a warning naming each.
|
|
3607
|
+
*/
|
|
3608
|
+
async _kickAutoAssign(restore) {
|
|
3609
|
+
const generation = this._sourceGeneration;
|
|
3610
|
+
const model = this.parentElement;
|
|
3611
|
+
if (!(model instanceof ModelElement)) {
|
|
3612
|
+
// Not inside a model: an empty component, driven through the JS API
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
await model.ready();
|
|
3616
|
+
// The source may have changed while the model loaded - a declared clip child appearing
|
|
3617
|
+
// flips the element over to declared mode, and wins
|
|
3618
|
+
const component = this.component;
|
|
3619
|
+
if (generation !== this._sourceGeneration || !component || this._clipElements().length > 0) {
|
|
3620
|
+
return;
|
|
3621
|
+
}
|
|
3622
|
+
const container = AssetElement.get(model.asset)?.resource;
|
|
3623
|
+
if (!container) {
|
|
3624
|
+
// The load failed; the model already reported it
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
const label = this.id ? ` '${this.id}'` : '';
|
|
3628
|
+
if (container.animations.length === 0) {
|
|
3629
|
+
console.warn(`pc-anim${label} - model '${model.asset}' has no animations`);
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
const seen = new Set();
|
|
3633
|
+
for (const animationAsset of container.animations) {
|
|
3634
|
+
const track = animationAsset.resource;
|
|
3635
|
+
if (!(track instanceof AnimTrack)) {
|
|
3636
|
+
continue;
|
|
3637
|
+
}
|
|
3638
|
+
if (track.name.indexOf('.') !== -1) {
|
|
3639
|
+
console.warn(`pc-anim${label} - track '${track.name}' - '.' in a clip name is reserved for blend tree paths - track skipped`);
|
|
3640
|
+
continue;
|
|
3641
|
+
}
|
|
3642
|
+
if (seen.has(track.name)) {
|
|
3643
|
+
console.warn(`pc-anim${label} - duplicate track name '${track.name}' - track skipped`);
|
|
3644
|
+
continue;
|
|
3645
|
+
}
|
|
3646
|
+
seen.add(track.name);
|
|
3647
|
+
component.assignAnimation(track.name, track);
|
|
3648
|
+
}
|
|
3649
|
+
this._autoAssigned = seen.size > 0;
|
|
3650
|
+
this._applySelection(restore);
|
|
3651
|
+
}
|
|
3652
|
+
/**
|
|
3653
|
+
* Applies the active-clip selection: the declared `clip` when it names an assigned state,
|
|
3654
|
+
* else a captured pre-rebuild state when it survived, else the engine's default (the first
|
|
3655
|
+
* assigned clip). A restore also reinstates the playhead and both playing flags exactly as
|
|
3656
|
+
* captured — the reassignment that preceded it set both to the `activate` outcome, which is
|
|
3657
|
+
* not necessarily the state the rebuild interrupted.
|
|
3658
|
+
*/
|
|
3659
|
+
_applySelection(restore) {
|
|
3660
|
+
const component = this.component;
|
|
3661
|
+
const layer = component ? component.baseLayer : null;
|
|
3662
|
+
if (!component || !layer) {
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
if (this._clip && !layer.states.includes(this._clip)) {
|
|
3666
|
+
this._warnUnknownClip(this._clip);
|
|
3667
|
+
}
|
|
3668
|
+
let target = null;
|
|
3669
|
+
if (this._clip && layer.states.includes(this._clip)) {
|
|
3670
|
+
target = this._clip;
|
|
3671
|
+
}
|
|
3672
|
+
else if (restore && layer.states.includes(restore.state)) {
|
|
3673
|
+
target = restore.state;
|
|
3674
|
+
}
|
|
3675
|
+
if (target && layer.activeState !== target) {
|
|
3676
|
+
layer.play(target);
|
|
3677
|
+
}
|
|
3678
|
+
if (restore) {
|
|
3679
|
+
if (target === restore.state) {
|
|
3680
|
+
layer.activeStateCurrentTime = restore.time;
|
|
3681
|
+
}
|
|
3682
|
+
layer.playing = restore.layerPlaying;
|
|
3683
|
+
component.playing = restore.playing;
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
_warnUnknownClip(name) {
|
|
3687
|
+
if (this._warnedClip === name) {
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
this._warnedClip = name;
|
|
3691
|
+
const label = this.id ? ` '${this.id}'` : '';
|
|
3692
|
+
console.warn(`pc-anim${label} has no clip named '${name}' - selection unchanged`);
|
|
3693
|
+
}
|
|
3694
|
+
/**
|
|
3695
|
+
* Rebuilds the clip set from the DOM, restoring the active clip and playhead when they
|
|
3696
|
+
* survive the rebuild. The engine cannot remove a state from a loaded graph (unassigning
|
|
3697
|
+
* only empties the state's tracks), so removals, renames and source changes drop the whole
|
|
3698
|
+
* graph and reassign.
|
|
3699
|
+
*
|
|
3700
|
+
* @internal
|
|
3701
|
+
*/
|
|
3702
|
+
_refreshClips() {
|
|
3703
|
+
const component = this.component;
|
|
3704
|
+
if (!component) {
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
const layer = component.baseLayer;
|
|
3708
|
+
const restore = layer ? {
|
|
3709
|
+
state: layer.activeState,
|
|
3710
|
+
time: layer.activeStateCurrentTime,
|
|
3711
|
+
playing: component.playing,
|
|
3712
|
+
layerPlaying: layer.playing
|
|
3713
|
+
} : undefined;
|
|
3714
|
+
component.removeStateGraph();
|
|
3715
|
+
this._applyClips(restore);
|
|
3716
|
+
}
|
|
3717
|
+
/**
|
|
3718
|
+
* Adopts a clip child announced by its connectedCallback. The initComponent sweep adopts
|
|
3719
|
+
* children already present, so this is a no-op for those; it serves clips appended later,
|
|
3720
|
+
* and flips an auto-assigned element over to its declared children — declared clips win.
|
|
3721
|
+
*
|
|
3722
|
+
* @param clip - The clip element.
|
|
3723
|
+
* @internal
|
|
3724
|
+
*/
|
|
3725
|
+
_registerClip(clip) {
|
|
3726
|
+
if (!this.component) {
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
if (this._autoAssigned) {
|
|
3730
|
+
this._refreshClips();
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
if (this._assignedClips.get(clip.name) === clip) {
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
// A clip child appearing supersedes an auto-assign still awaiting its model
|
|
3737
|
+
this._sourceGeneration++;
|
|
3738
|
+
if (this._adoptClip(clip)) {
|
|
3739
|
+
this._applySelection();
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
/**
|
|
3743
|
+
* Releases a disconnected clip child. Rebuilds the set — a state cannot be removed from a
|
|
3744
|
+
* live graph — and the removal of the last child inside a `<pc-model>` flips the element
|
|
3745
|
+
* back to auto-assigning the model's clips.
|
|
3746
|
+
*
|
|
3747
|
+
* @param clip - The clip element.
|
|
3748
|
+
* @internal
|
|
3749
|
+
*/
|
|
3750
|
+
_unregisterClip(clip) {
|
|
3751
|
+
if (!this.component) {
|
|
3752
|
+
// The whole subtree is coming down (parents disconnect first) - nothing to rebuild
|
|
3753
|
+
return;
|
|
3754
|
+
}
|
|
3755
|
+
if (this._assignedClips.get(clip.name) !== clip) {
|
|
3756
|
+
// The clip never held a state (invalid or duplicate name)
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3759
|
+
this._refreshClips();
|
|
3760
|
+
}
|
|
3761
|
+
/**
|
|
3762
|
+
* Swaps a clip's resolved track in for the placeholder (or for its previous track after an
|
|
3763
|
+
* asset change). A swap of the active clip restarts it: the engine preserves the playhead
|
|
3764
|
+
* through a track replacement, which would land mid-way into unrelated animation.
|
|
3765
|
+
*
|
|
3766
|
+
* @param clip - The clip element.
|
|
3767
|
+
* @returns Whether the clip still owns its state — the resolution may have been superseded
|
|
3768
|
+
* by a rebuild that dropped it.
|
|
3769
|
+
* @internal
|
|
3770
|
+
*/
|
|
3771
|
+
_onClipResolved(clip) {
|
|
3772
|
+
const component = this.component;
|
|
3773
|
+
if (!component || this._assignedClips.get(clip.name) !== clip) {
|
|
3774
|
+
return false;
|
|
3775
|
+
}
|
|
3776
|
+
this._assignClip(clip);
|
|
3777
|
+
const layer = component.baseLayer;
|
|
3778
|
+
if (layer && layer.activeState === clip.name) {
|
|
3779
|
+
layer.play(clip.name);
|
|
3780
|
+
}
|
|
3781
|
+
return true;
|
|
3782
|
+
}
|
|
3783
|
+
/**
|
|
3784
|
+
* Applies a clip's changed speed or loop. The engine bakes both into the playback state it
|
|
3785
|
+
* creates on state entry, so a live change re-enters the state and restores the playhead.
|
|
3786
|
+
*
|
|
3787
|
+
* @param clip - The clip element.
|
|
3788
|
+
* @internal
|
|
3789
|
+
*/
|
|
3790
|
+
_onClipParamsChanged(clip) {
|
|
3791
|
+
const component = this.component;
|
|
3792
|
+
if (!component || this._assignedClips.get(clip.name) !== clip) {
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
this._assignClip(clip);
|
|
3796
|
+
const layer = component.baseLayer;
|
|
3797
|
+
if (layer && layer.activeState === clip.name) {
|
|
3798
|
+
const time = layer.activeStateCurrentTime;
|
|
3799
|
+
layer.play(clip.name);
|
|
3800
|
+
layer.activeStateCurrentTime = time;
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
/**
|
|
3804
|
+
* Resumes playback, optionally switching to a named clip first (a hard cut). A name that
|
|
3805
|
+
* matches no clip leaves the selection unchanged.
|
|
3806
|
+
*
|
|
3807
|
+
* @param name - The name of the clip to play. Resumes the current clip when omitted.
|
|
3808
|
+
*/
|
|
3809
|
+
play(name) {
|
|
3810
|
+
const component = this.component;
|
|
3811
|
+
const layer = component ? component.baseLayer : null;
|
|
3812
|
+
if (!component || !layer) {
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
// layer.play sets the layer controller's playing flag; the component's is the system
|
|
3816
|
+
// gate. Setting both is what makes this a resume regardless of how playback stopped.
|
|
3817
|
+
if (name !== undefined) {
|
|
3818
|
+
if (!layer.states.includes(name)) {
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3821
|
+
layer.play(name);
|
|
3822
|
+
}
|
|
3823
|
+
else {
|
|
3824
|
+
layer.play();
|
|
3825
|
+
}
|
|
3826
|
+
component.playing = true;
|
|
3827
|
+
}
|
|
3828
|
+
/**
|
|
3829
|
+
* Pauses playback, preserving the playhead — {@link play} resumes from where it stopped.
|
|
3830
|
+
*/
|
|
3831
|
+
pause() {
|
|
3832
|
+
if (!this.component) {
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
// Only the component flag - the single gate the system tick reads - is cleared. The
|
|
3836
|
+
// layer controller's flag is left as-is so a pause is exactly reversible, whether
|
|
3837
|
+
// resumed through play() (which sets both) or through the component API directly.
|
|
3838
|
+
this.component.playing = false;
|
|
3839
|
+
}
|
|
3840
|
+
/**
|
|
3841
|
+
* Cross-fades to a named clip and ensures playback is running. A name that matches no clip
|
|
3842
|
+
* leaves the selection unchanged.
|
|
3843
|
+
*
|
|
3844
|
+
* @param name - The name of the clip to fade to.
|
|
3845
|
+
* @param time - The fade duration in seconds. Defaults to the `transition-time` attribute.
|
|
3846
|
+
*/
|
|
3847
|
+
transition(name, time) {
|
|
3848
|
+
const component = this.component;
|
|
3849
|
+
const layer = component ? component.baseLayer : null;
|
|
3850
|
+
if (!component || !layer || !layer.states.includes(name)) {
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
layer.transition(name, Math.max(0, time ?? this._transitionTime));
|
|
3854
|
+
layer.playing = true;
|
|
3855
|
+
component.playing = true;
|
|
3856
|
+
}
|
|
3857
|
+
/**
|
|
3858
|
+
* Gets the underlying PlayCanvas anim component.
|
|
3859
|
+
* @returns The anim component.
|
|
3860
|
+
*/
|
|
3861
|
+
get component() {
|
|
3862
|
+
return super.component;
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Gets the names of the assigned clips.
|
|
3866
|
+
* @returns The clip names, in assignment order.
|
|
3867
|
+
*/
|
|
3868
|
+
get clips() {
|
|
3869
|
+
const layer = this.component ? this.component.baseLayer : null;
|
|
3870
|
+
return layer ? layer.states.filter(state => !ANIM_CONTROL_STATES.includes(state)) : [];
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Sets whether playback starts automatically once a clip is assigned. Defaults to `true`.
|
|
3874
|
+
* Applies when clips are assigned — it does not stop a clip that is already playing.
|
|
3875
|
+
* @param value - Whether playback starts automatically.
|
|
3876
|
+
*/
|
|
3877
|
+
set activate(value) {
|
|
3878
|
+
this._activate = value;
|
|
3879
|
+
if (this.component) {
|
|
3880
|
+
this.component.activate = value;
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
/**
|
|
3884
|
+
* Gets whether playback starts automatically once a clip is assigned.
|
|
3885
|
+
* @returns Whether playback starts automatically.
|
|
3886
|
+
*/
|
|
3887
|
+
get activate() {
|
|
3888
|
+
return this._activate;
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Sets the name of the active clip. Changing it switches playback, cross-fading over
|
|
3892
|
+
* `transition-time` seconds (a hard cut at 0). An empty value leaves the current clip
|
|
3893
|
+
* playing; a name that matches no clip warns and leaves the selection unchanged.
|
|
3894
|
+
* @param value - The name of the active clip.
|
|
3895
|
+
*/
|
|
3896
|
+
set clip(value) {
|
|
3897
|
+
this._clip = value;
|
|
3898
|
+
const component = this.component;
|
|
3899
|
+
const layer = component ? component.baseLayer : null;
|
|
3900
|
+
if (!component || !layer || !value) {
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
if (!layer.states.includes(value)) {
|
|
3904
|
+
this._warnUnknownClip(value);
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
if (layer.activeState === value) {
|
|
3908
|
+
return;
|
|
3909
|
+
}
|
|
3910
|
+
if (this._transitionTime > 0) {
|
|
3911
|
+
this.transition(value);
|
|
3912
|
+
}
|
|
3913
|
+
else {
|
|
3914
|
+
this.play(value);
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
/**
|
|
3918
|
+
* Gets the name of the active clip.
|
|
3919
|
+
* @returns The name of the active clip.
|
|
3920
|
+
*/
|
|
3921
|
+
get clip() {
|
|
3922
|
+
return this._clip;
|
|
3923
|
+
}
|
|
3924
|
+
/**
|
|
3925
|
+
* Sets the playback speed multiplier applied across all clips, where 0 freezes playback.
|
|
3926
|
+
* Defaults to 1.
|
|
3927
|
+
* @param value - The playback speed multiplier.
|
|
3928
|
+
*/
|
|
3929
|
+
set speed(value) {
|
|
3930
|
+
this._speed = value;
|
|
3931
|
+
if (this.component) {
|
|
3932
|
+
this.component.speed = value;
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
/**
|
|
3936
|
+
* Gets the playback speed multiplier applied across all clips.
|
|
3937
|
+
* @returns The playback speed multiplier.
|
|
3938
|
+
*/
|
|
3939
|
+
get speed() {
|
|
3940
|
+
return this._speed;
|
|
3941
|
+
}
|
|
3942
|
+
/**
|
|
3943
|
+
* Sets the cross-fade duration of clip switches made through the `clip` attribute, in
|
|
3944
|
+
* seconds. Defaults to 0 (a hard cut).
|
|
3945
|
+
* @param value - The cross-fade duration in seconds.
|
|
3946
|
+
*/
|
|
3947
|
+
set transitionTime(value) {
|
|
3948
|
+
this._transitionTime = value;
|
|
3949
|
+
}
|
|
3950
|
+
/**
|
|
3951
|
+
* Gets the cross-fade duration of clip switches made through the `clip` attribute.
|
|
3952
|
+
* @returns The cross-fade duration in seconds.
|
|
3953
|
+
*/
|
|
3954
|
+
get transitionTime() {
|
|
3955
|
+
return this._transitionTime;
|
|
3956
|
+
}
|
|
3957
|
+
static get observedAttributes() {
|
|
3958
|
+
return [...super.observedAttributes, 'activate', 'clip', 'speed', 'transition-time'];
|
|
3959
|
+
}
|
|
3960
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
3961
|
+
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
3962
|
+
switch (name) {
|
|
3963
|
+
case 'activate':
|
|
3964
|
+
this.activate = parseBool(newValue, true);
|
|
3965
|
+
break;
|
|
3966
|
+
case 'clip':
|
|
3967
|
+
this.clip = newValue ?? '';
|
|
3968
|
+
break;
|
|
3969
|
+
case 'speed':
|
|
3970
|
+
this.speed = parseNumber(newValue, 1, name);
|
|
3971
|
+
break;
|
|
3972
|
+
case 'transition-time':
|
|
3973
|
+
this.transitionTime = parseNumber(newValue, 0, name);
|
|
3974
|
+
break;
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
customElements.define('pc-anim', AnimComponentElement);
|
|
3979
|
+
|
|
3980
|
+
/**
|
|
3981
|
+
* The AnimClipElement interface provides properties and methods for manipulating
|
|
3982
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim-clip/ | `<pc-anim-clip>`}
|
|
3983
|
+
* elements. The AnimClipElement interface also inherits the properties and methods of the
|
|
3984
|
+
* {@link HTMLElement} interface.
|
|
3985
|
+
*
|
|
3986
|
+
* A clip declares one named animation on its parent `<pc-anim>`. `name` is both the clip's name
|
|
3987
|
+
* and the track looked up in the clip's source: an explicit `asset` (a `container`, an
|
|
3988
|
+
* `animation` `.glb`, or an `animclip` JSON), or, without one, the container of the `<pc-model>`
|
|
3989
|
+
* enclosing the parent `<pc-anim>`. A source holding a single track supplies it whatever it is
|
|
3990
|
+
* named; in a multi-track source the track named `name` is chosen, falling back to the first
|
|
3991
|
+
* with a warning. The element becomes ready once its resolved track is assigned.
|
|
3992
|
+
*
|
|
3993
|
+
* @category Components
|
|
3994
|
+
*/
|
|
3995
|
+
class AnimClipElement extends AsyncElement {
|
|
2937
3996
|
/**
|
|
2938
|
-
* The
|
|
2939
|
-
*
|
|
3997
|
+
* The `<pc-anim>` this clip was adopted by, captured when the parent adopts the clip and on
|
|
3998
|
+
* connection.
|
|
3999
|
+
*
|
|
4000
|
+
* `disconnectedCallback` cannot rediscover it: by the time the element is disconnected its
|
|
4001
|
+
* `parentElement` is already `null`, so a lookup would both fail to find the component and
|
|
4002
|
+
* emit a misleading "must be a direct child" warning for what is an ordinary removal.
|
|
2940
4003
|
*/
|
|
2941
|
-
|
|
4004
|
+
_animElement = null;
|
|
4005
|
+
_asset = '';
|
|
2942
4006
|
/**
|
|
2943
|
-
*
|
|
2944
|
-
*
|
|
4007
|
+
* Incremented on every connect and disconnect, and captured by connectedCallback on entry —
|
|
4008
|
+
* a resume from an await abandons itself if the value has moved on, so a stale callback can
|
|
4009
|
+
* neither act on a torn-down tree nor register its clip alongside a re-inserted element's
|
|
4010
|
+
* own callback.
|
|
2945
4011
|
*/
|
|
2946
|
-
|
|
4012
|
+
_connectionGeneration = 0;
|
|
4013
|
+
_errorHandle = null;
|
|
2947
4014
|
/**
|
|
2948
|
-
* Incremented on every
|
|
2949
|
-
*
|
|
2950
|
-
*
|
|
2951
|
-
* was removed and re-inserted (which runs a callback of its own) cannot add the component a
|
|
2952
|
-
* second time.
|
|
4015
|
+
* Incremented on every track resolution and on disconnect, and captured by a resolution when
|
|
4016
|
+
* it starts. A resolution that resumes from an await or an asset callback abandons itself if
|
|
4017
|
+
* the value has moved on, so a superseded resolution cannot hand a stale track to the parent.
|
|
2953
4018
|
*/
|
|
2954
|
-
|
|
4019
|
+
_loadGeneration = 0;
|
|
2955
4020
|
/**
|
|
2956
|
-
*
|
|
4021
|
+
* The pending asset subscriptions of the current resolution, if it is waiting for its asset.
|
|
4022
|
+
* Held so that whatever supersedes the resolution can detach the handlers from the asset,
|
|
4023
|
+
* rather than leave them registered until the asset settles (or forever, if it never does).
|
|
4024
|
+
*/
|
|
4025
|
+
_loadHandle = null;
|
|
4026
|
+
_loop = true;
|
|
4027
|
+
_name = '';
|
|
4028
|
+
_speed = 1;
|
|
4029
|
+
/**
|
|
4030
|
+
* The source complaint already made — the asset id it was made for, or `''` for the
|
|
4031
|
+
* no-asset-no-model case — so re-resolutions (host cycles, model reloads) do not repeat it.
|
|
4032
|
+
*/
|
|
4033
|
+
_warnedSource = null;
|
|
4034
|
+
/**
|
|
4035
|
+
* Whether the owning `<pc-anim>` already rejected this clip's name — its sweeps re-run on
|
|
4036
|
+
* host cycles and must not repeat the complaint.
|
|
4037
|
+
*/
|
|
4038
|
+
_warnedInvalid = false;
|
|
4039
|
+
/**
|
|
4040
|
+
* The clip's resolved track. `null` until resolution completes, during which the owning
|
|
4041
|
+
* `<pc-anim>` assigns the engine's placeholder track in its stead.
|
|
2957
4042
|
*
|
|
2958
|
-
* @
|
|
2959
|
-
* @ignore
|
|
4043
|
+
* @internal
|
|
2960
4044
|
*/
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
4045
|
+
_track = null;
|
|
4046
|
+
async connectedCallback() {
|
|
4047
|
+
const generation = ++this._connectionGeneration;
|
|
4048
|
+
const animElement = this.animElement;
|
|
4049
|
+
await animElement?.ready();
|
|
4050
|
+
// The element may have been removed (perhaps re-inserted, which runs a callback of its
|
|
4051
|
+
// own), or its parent torn down, while we were waiting. A <pc-app> disconnects before
|
|
4052
|
+
// its children, so by the time we resume the component can already be gone - see the
|
|
4053
|
+
// matching guard in disconnectedCallback below.
|
|
4054
|
+
const component = animElement ? animElement.component : null;
|
|
4055
|
+
if (generation !== this._connectionGeneration || !animElement || !component) {
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
this._animElement = animElement;
|
|
4059
|
+
animElement._registerClip(this);
|
|
4060
|
+
}
|
|
4061
|
+
disconnectedCallback() {
|
|
4062
|
+
// Invalidate any connectedCallback or track resolution still suspended on an await
|
|
4063
|
+
this._connectionGeneration++;
|
|
4064
|
+
this._loadGeneration++;
|
|
4065
|
+
this._detachLoadHandlers();
|
|
4066
|
+
// Uses the cached parent rather than a fresh lookup, since parentElement is already null
|
|
4067
|
+
// by now. The component itself is null if the whole <pc-app> is being torn down —
|
|
4068
|
+
// parents disconnect first and have already removed the component.
|
|
4069
|
+
this._animElement?._unregisterClip(this);
|
|
4070
|
+
this._animElement = null;
|
|
4071
|
+
this._track = null;
|
|
4072
|
+
this._resetReady();
|
|
4073
|
+
}
|
|
4074
|
+
get animElement() {
|
|
4075
|
+
const animElement = this.parentElement;
|
|
4076
|
+
if (!(animElement instanceof AnimComponentElement)) {
|
|
4077
|
+
const label = this._name ? ` '${this._name}'` : '';
|
|
4078
|
+
console.warn(`pc-anim-clip${label} must be a direct child of a pc-anim element`);
|
|
4079
|
+
return null;
|
|
4080
|
+
}
|
|
4081
|
+
return animElement;
|
|
4082
|
+
}
|
|
4083
|
+
_detachLoadHandlers() {
|
|
4084
|
+
this._loadHandle?.off();
|
|
4085
|
+
this._loadHandle = null;
|
|
4086
|
+
this._errorHandle?.off();
|
|
4087
|
+
this._errorHandle = null;
|
|
2964
4088
|
}
|
|
2965
4089
|
/**
|
|
2966
|
-
*
|
|
2967
|
-
* initial values of their cached properties.
|
|
4090
|
+
* Reports a name-validation failure from the owning `<pc-anim>`, once per name value.
|
|
2968
4091
|
*
|
|
2969
|
-
* @
|
|
4092
|
+
* @param message - The complaint.
|
|
4093
|
+
* @internal
|
|
2970
4094
|
*/
|
|
2971
|
-
|
|
2972
|
-
|
|
4095
|
+
_markInvalid(message) {
|
|
4096
|
+
if (this._warnedInvalid) {
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
4099
|
+
this._warnedInvalid = true;
|
|
4100
|
+
console.warn(message);
|
|
2973
4101
|
}
|
|
2974
4102
|
/**
|
|
2975
|
-
*
|
|
2976
|
-
*
|
|
2977
|
-
*
|
|
2978
|
-
*
|
|
2979
|
-
*
|
|
2980
|
-
*
|
|
4103
|
+
* Resolves the clip's track from its source and hands it to the owning `<pc-anim>`. Called
|
|
4104
|
+
* by the parent whenever the clip is (re)adopted, and again when the source changes; a newer
|
|
4105
|
+
* resolution supersedes one still in flight. The element becomes ready once the resolved
|
|
4106
|
+
* track is assigned.
|
|
4107
|
+
*
|
|
4108
|
+
* @param animElement - The owning `<pc-anim>`.
|
|
4109
|
+
* @internal
|
|
2981
4110
|
*/
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
4111
|
+
async _resolveTrack(animElement) {
|
|
4112
|
+
this._animElement = animElement;
|
|
4113
|
+
const generation = ++this._loadGeneration;
|
|
4114
|
+
this._detachLoadHandlers();
|
|
4115
|
+
if (this._asset) {
|
|
4116
|
+
const asset = useAsset(this._asset);
|
|
4117
|
+
if (!asset) {
|
|
4118
|
+
this._warnSource(`pc-anim-clip '${this._name}' could not find asset '${this._asset}' - clip not assigned`);
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
if (asset.loaded) {
|
|
4122
|
+
this._extractTrack(asset, `asset '${this._asset}'`);
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4125
|
+
// Whichever of load/error fires first detaches the other. The generation is
|
|
4126
|
+
// re-checked even though a superseded handler is detached: the detach relies on how
|
|
4127
|
+
// the engine's event emitter treats removal, while the check holds on its own.
|
|
4128
|
+
this._loadHandle = asset.once('load', () => {
|
|
4129
|
+
this._detachLoadHandlers();
|
|
4130
|
+
if (generation !== this._loadGeneration) {
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
this._extractTrack(asset, `asset '${this._asset}'`);
|
|
4134
|
+
});
|
|
4135
|
+
this._errorHandle = asset.once('error', () => {
|
|
4136
|
+
this._detachLoadHandlers();
|
|
4137
|
+
if (generation !== this._loadGeneration) {
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
this._warnSource(`pc-anim-clip '${this._name}' - asset '${this._asset}' failed to load - clip not assigned`);
|
|
4141
|
+
});
|
|
2985
4142
|
return;
|
|
2986
4143
|
}
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
if (previous?.entity && previous.entity.c[this._componentName] === previous) {
|
|
2992
|
-
previous.entity.removeComponent(this._componentName);
|
|
4144
|
+
const model = animElement.parentElement;
|
|
4145
|
+
if (!(model instanceof ModelElement)) {
|
|
4146
|
+
this._warnSource(`pc-anim-clip '${this._name}' has no asset and no enclosing pc-model - clip not assigned`);
|
|
4147
|
+
return;
|
|
2993
4148
|
}
|
|
2994
|
-
|
|
2995
|
-
if (
|
|
4149
|
+
await model.ready();
|
|
4150
|
+
if (generation !== this._loadGeneration) {
|
|
2996
4151
|
return;
|
|
2997
4152
|
}
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
4153
|
+
const asset = AssetElement.get(model.asset);
|
|
4154
|
+
if (!asset?.resource) {
|
|
4155
|
+
// The model's load failed; it already reported the error
|
|
3001
4156
|
return;
|
|
3002
4157
|
}
|
|
3003
|
-
this.
|
|
4158
|
+
this._extractTrack(asset, `model '${model.asset}'`);
|
|
3004
4159
|
}
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
const label = this.id ? ` '${this.id}'` : '';
|
|
3012
|
-
console.warn(`${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity - component not added`);
|
|
4160
|
+
/**
|
|
4161
|
+
* Complains about the clip's source, once per source value — resolutions re-run on host
|
|
4162
|
+
* cycles and model reloads, and must not repeat the complaint.
|
|
4163
|
+
*/
|
|
4164
|
+
_warnSource(message) {
|
|
4165
|
+
if (this._warnedSource === this._asset) {
|
|
3013
4166
|
return;
|
|
3014
4167
|
}
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
4168
|
+
this._warnedSource = this._asset;
|
|
4169
|
+
console.warn(message);
|
|
4170
|
+
}
|
|
4171
|
+
/**
|
|
4172
|
+
* Picks the clip's track out of a loaded source asset: the track named `name`, or a lone
|
|
4173
|
+
* track whatever it is named, or the first of several with a warning.
|
|
4174
|
+
*
|
|
4175
|
+
* @param asset - The loaded source asset.
|
|
4176
|
+
* @param source - How warnings name the source.
|
|
4177
|
+
*/
|
|
4178
|
+
_extractTrack(asset, source) {
|
|
4179
|
+
const label = `pc-anim-clip '${this._name}'`;
|
|
4180
|
+
// Widened: the engine registers an 'animclip' handler but omits the type from the
|
|
4181
|
+
// Asset.type union
|
|
4182
|
+
const type = asset.type;
|
|
4183
|
+
let candidates;
|
|
4184
|
+
switch (type) {
|
|
4185
|
+
case 'container':
|
|
4186
|
+
candidates = asset.resource.animations.map((animationAsset) => animationAsset.resource);
|
|
4187
|
+
break;
|
|
4188
|
+
case 'animation':
|
|
4189
|
+
candidates = asset.resources;
|
|
4190
|
+
break;
|
|
4191
|
+
case 'animclip':
|
|
4192
|
+
candidates = [asset.resource];
|
|
4193
|
+
break;
|
|
4194
|
+
default:
|
|
4195
|
+
this._warnSource(`${label} - ${source} has type '${asset.type}', expected 'container', 'animation' or 'animclip' - clip not assigned`);
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
// A JSON 'animation' asset parses to the engine's legacy Animation class, which the anim
|
|
4199
|
+
// system rejects - only real AnimTracks qualify
|
|
4200
|
+
const tracks = candidates.filter((candidate) => candidate instanceof AnimTrack);
|
|
4201
|
+
if (tracks.length === 0) {
|
|
4202
|
+
this._warnSource(`${label} - ${source} contains no usable animation track - clip not assigned`);
|
|
3019
4203
|
return;
|
|
3020
4204
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
// the host's own cycles count. Readiness is cycled here too, so decorations one level
|
|
3027
|
-
// down re-apply the same way.
|
|
3028
|
-
this._hostReadyListener = (event) => {
|
|
3029
|
-
if (event.target !== this._hostElement) {
|
|
3030
|
-
return;
|
|
3031
|
-
}
|
|
3032
|
-
if (generation !== this._connectionGeneration) {
|
|
3033
|
-
return;
|
|
4205
|
+
let track = tracks.find((candidate) => candidate.name === this._name);
|
|
4206
|
+
if (!track) {
|
|
4207
|
+
track = tracks[0];
|
|
4208
|
+
if (tracks.length > 1) {
|
|
4209
|
+
console.warn(`${label} - no track named '${this._name}' in ${source} - using '${track.name}' (available: ${tracks.map((candidate) => candidate.name).join(', ')})`);
|
|
3034
4210
|
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
4211
|
+
}
|
|
4212
|
+
this._track = track;
|
|
4213
|
+
if (this._animElement?._onClipResolved(this)) {
|
|
4214
|
+
this._onReady();
|
|
4215
|
+
}
|
|
3038
4216
|
}
|
|
3039
4217
|
/**
|
|
3040
|
-
*
|
|
3041
|
-
*
|
|
3042
|
-
*
|
|
3043
|
-
*
|
|
3044
|
-
* dissolving its binding: the one transition that fires no ready event to ride.
|
|
3045
|
-
*
|
|
3046
|
-
* @internal
|
|
4218
|
+
* Sets the id of the `pc-asset` supplying the clip's track: a `container`, an `animation`
|
|
4219
|
+
* `.glb`, or an `animclip` JSON. When empty, the track comes from the container of the
|
|
4220
|
+
* `<pc-model>` enclosing the parent `<pc-anim>`.
|
|
4221
|
+
* @param value - The asset id.
|
|
3047
4222
|
*/
|
|
3048
|
-
|
|
3049
|
-
this.
|
|
3050
|
-
this.
|
|
3051
|
-
if (this.
|
|
3052
|
-
this.
|
|
3053
|
-
this.
|
|
4223
|
+
set asset(value) {
|
|
4224
|
+
this._asset = value;
|
|
4225
|
+
this._warnedSource = null;
|
|
4226
|
+
if (this._animElement) {
|
|
4227
|
+
this._resetReady();
|
|
4228
|
+
this._track = null;
|
|
4229
|
+
this._resolveTrack(this._animElement);
|
|
3054
4230
|
}
|
|
3055
4231
|
}
|
|
3056
4232
|
/**
|
|
3057
|
-
*
|
|
3058
|
-
*
|
|
4233
|
+
* Gets the id of the `pc-asset` supplying the clip's track.
|
|
4234
|
+
* @returns The asset id.
|
|
4235
|
+
*/
|
|
4236
|
+
get asset() {
|
|
4237
|
+
return this._asset;
|
|
4238
|
+
}
|
|
4239
|
+
/**
|
|
4240
|
+
* Sets whether the clip loops. A non-looping clip holds its last pose when it ends — the
|
|
4241
|
+
* engine reports no completion. Defaults to `true`.
|
|
4242
|
+
* @param value - Whether the clip loops.
|
|
3059
4243
|
*/
|
|
3060
|
-
|
|
3061
|
-
|
|
4244
|
+
set loop(value) {
|
|
4245
|
+
this._loop = value;
|
|
4246
|
+
this._animElement?._onClipParamsChanged(this);
|
|
3062
4247
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
// must not add the component alongside it.
|
|
3070
|
-
if (generation !== this._connectionGeneration) {
|
|
3071
|
-
return;
|
|
3072
|
-
}
|
|
3073
|
-
await this._addComponent();
|
|
3074
|
-
if (generation !== this._connectionGeneration) {
|
|
3075
|
-
return;
|
|
3076
|
-
}
|
|
3077
|
-
this.initComponent();
|
|
3078
|
-
this._onReady();
|
|
4248
|
+
/**
|
|
4249
|
+
* Gets whether the clip loops.
|
|
4250
|
+
* @returns Whether the clip loops.
|
|
4251
|
+
*/
|
|
4252
|
+
get loop() {
|
|
4253
|
+
return this._loop;
|
|
3079
4254
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
this.
|
|
3087
|
-
this.
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
if (this._appElement?.app && this._component?.entity) {
|
|
3092
|
-
this._component.entity.removeComponent(this._componentName);
|
|
4255
|
+
/**
|
|
4256
|
+
* Sets the name of the clip: the name it is played by, and the track looked up in the
|
|
4257
|
+
* clip's source. Names must be unique within a `<pc-anim>` and must not contain `.`.
|
|
4258
|
+
* @param value - The clip name.
|
|
4259
|
+
*/
|
|
4260
|
+
set name(value) {
|
|
4261
|
+
this._name = value;
|
|
4262
|
+
this._warnedInvalid = false;
|
|
4263
|
+
if (this._animElement) {
|
|
4264
|
+
this._resetReady();
|
|
4265
|
+
this._animElement._refreshClips();
|
|
3093
4266
|
}
|
|
3094
|
-
this._component = null;
|
|
3095
|
-
this._appElement = null;
|
|
3096
|
-
this._resetReady();
|
|
3097
4267
|
}
|
|
3098
4268
|
/**
|
|
3099
|
-
*
|
|
3100
|
-
*
|
|
3101
|
-
* element's `ready()` promise before accessing it.
|
|
3102
|
-
* @returns The component instance, or `null`.
|
|
4269
|
+
* Gets the name of the clip.
|
|
4270
|
+
* @returns The clip name.
|
|
3103
4271
|
*/
|
|
3104
|
-
get
|
|
3105
|
-
return this.
|
|
4272
|
+
get name() {
|
|
4273
|
+
return this._name;
|
|
3106
4274
|
}
|
|
3107
4275
|
/**
|
|
3108
|
-
* Sets the
|
|
3109
|
-
*
|
|
4276
|
+
* Sets the playback speed of the clip, where negative values play it backwards. Applies
|
|
4277
|
+
* immediately, preserving the playhead. Defaults to 1.
|
|
4278
|
+
* @param value - The playback speed.
|
|
3110
4279
|
*/
|
|
3111
|
-
set
|
|
3112
|
-
this.
|
|
3113
|
-
|
|
3114
|
-
this.component.enabled = value;
|
|
3115
|
-
}
|
|
4280
|
+
set speed(value) {
|
|
4281
|
+
this._speed = value;
|
|
4282
|
+
this._animElement?._onClipParamsChanged(this);
|
|
3116
4283
|
}
|
|
3117
4284
|
/**
|
|
3118
|
-
* Gets the
|
|
3119
|
-
* @returns The
|
|
4285
|
+
* Gets the playback speed of the clip.
|
|
4286
|
+
* @returns The playback speed.
|
|
3120
4287
|
*/
|
|
3121
|
-
get
|
|
3122
|
-
return this.
|
|
4288
|
+
get speed() {
|
|
4289
|
+
return this._speed;
|
|
3123
4290
|
}
|
|
3124
4291
|
static get observedAttributes() {
|
|
3125
|
-
return ['
|
|
4292
|
+
return ['asset', 'loop', 'name', 'speed'];
|
|
3126
4293
|
}
|
|
3127
4294
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
3128
4295
|
switch (name) {
|
|
3129
|
-
case '
|
|
3130
|
-
this.
|
|
4296
|
+
case 'asset':
|
|
4297
|
+
this.asset = newValue ?? '';
|
|
4298
|
+
break;
|
|
4299
|
+
case 'loop':
|
|
4300
|
+
this.loop = parseBool(newValue, true);
|
|
4301
|
+
break;
|
|
4302
|
+
case 'name':
|
|
4303
|
+
this.name = newValue ?? '';
|
|
4304
|
+
break;
|
|
4305
|
+
case 'speed':
|
|
4306
|
+
this.speed = parseNumber(newValue, 1, name);
|
|
3131
4307
|
break;
|
|
3132
4308
|
}
|
|
3133
4309
|
}
|
|
3134
4310
|
}
|
|
4311
|
+
customElements.define('pc-anim-clip', AnimClipElement);
|
|
3135
4312
|
|
|
3136
4313
|
/**
|
|
3137
4314
|
* The ListenerComponentElement interface provides properties and methods for manipulating
|
|
@@ -11245,574 +12422,284 @@ class SoundSlotElement extends AsyncElement {
|
|
|
11245
12422
|
* Gets the overlap flag of the sound slot.
|
|
11246
12423
|
* @returns The overlap flag.
|
|
11247
12424
|
*/
|
|
11248
|
-
get overlap() {
|
|
11249
|
-
return this._overlap;
|
|
11250
|
-
}
|
|
11251
|
-
/**
|
|
11252
|
-
* Sets the pitch of the sound slot.
|
|
11253
|
-
* @param value - The pitch.
|
|
11254
|
-
*/
|
|
11255
|
-
set pitch(value) {
|
|
11256
|
-
this._pitch = value;
|
|
11257
|
-
if (this.soundSlot) {
|
|
11258
|
-
this.soundSlot.pitch = value;
|
|
11259
|
-
}
|
|
11260
|
-
}
|
|
11261
|
-
/**
|
|
11262
|
-
* Gets the pitch of the sound slot.
|
|
11263
|
-
* @returns The pitch.
|
|
11264
|
-
*/
|
|
11265
|
-
get pitch() {
|
|
11266
|
-
return this._pitch;
|
|
11267
|
-
}
|
|
11268
|
-
/**
|
|
11269
|
-
* Sets the start time of the sound slot.
|
|
11270
|
-
* @param value - The start time.
|
|
11271
|
-
*/
|
|
11272
|
-
set startTime(value) {
|
|
11273
|
-
this._startTime = value;
|
|
11274
|
-
if (this.soundSlot) {
|
|
11275
|
-
this.soundSlot.startTime = value;
|
|
11276
|
-
}
|
|
11277
|
-
}
|
|
11278
|
-
/**
|
|
11279
|
-
* Gets the start time of the sound slot.
|
|
11280
|
-
* @returns The start time.
|
|
11281
|
-
*/
|
|
11282
|
-
get startTime() {
|
|
11283
|
-
return this._startTime;
|
|
11284
|
-
}
|
|
11285
|
-
/**
|
|
11286
|
-
* Sets the volume of the sound slot.
|
|
11287
|
-
* @param value - The volume.
|
|
11288
|
-
*/
|
|
11289
|
-
set volume(value) {
|
|
11290
|
-
this._volume = value;
|
|
11291
|
-
if (this.soundSlot) {
|
|
11292
|
-
this.soundSlot.volume = value;
|
|
11293
|
-
}
|
|
11294
|
-
}
|
|
11295
|
-
/**
|
|
11296
|
-
* Gets the volume of the sound slot.
|
|
11297
|
-
* @returns The volume.
|
|
11298
|
-
*/
|
|
11299
|
-
get volume() {
|
|
11300
|
-
return this._volume;
|
|
11301
|
-
}
|
|
11302
|
-
static get observedAttributes() {
|
|
11303
|
-
return ['asset', 'auto-play', 'duration', 'loop', 'name', 'overlap', 'pitch', 'start-time', 'volume'];
|
|
11304
|
-
}
|
|
11305
|
-
attributeChangedCallback(name, _oldValue, newValue) {
|
|
11306
|
-
switch (name) {
|
|
11307
|
-
case 'asset':
|
|
11308
|
-
this.asset = newValue ?? '';
|
|
11309
|
-
break;
|
|
11310
|
-
case 'auto-play':
|
|
11311
|
-
this.autoPlay = parseBool(newValue, false);
|
|
11312
|
-
break;
|
|
11313
|
-
case 'duration':
|
|
11314
|
-
this.duration = parseNumber(newValue, null, name);
|
|
11315
|
-
break;
|
|
11316
|
-
case 'loop':
|
|
11317
|
-
this.loop = parseBool(newValue, false);
|
|
11318
|
-
break;
|
|
11319
|
-
case 'name':
|
|
11320
|
-
this.name = newValue ?? '';
|
|
11321
|
-
break;
|
|
11322
|
-
case 'overlap':
|
|
11323
|
-
this.overlap = parseBool(newValue, false);
|
|
11324
|
-
break;
|
|
11325
|
-
case 'pitch':
|
|
11326
|
-
this.pitch = parseNumber(newValue, 1, name);
|
|
11327
|
-
break;
|
|
11328
|
-
case 'start-time':
|
|
11329
|
-
this.startTime = parseNumber(newValue, 0, name);
|
|
11330
|
-
break;
|
|
11331
|
-
case 'volume':
|
|
11332
|
-
this.volume = parseNumber(newValue, 1, name);
|
|
11333
|
-
break;
|
|
11334
|
-
}
|
|
11335
|
-
}
|
|
11336
|
-
}
|
|
11337
|
-
customElements.define('pc-sound', SoundSlotElement);
|
|
11338
|
-
|
|
11339
|
-
/**
|
|
11340
|
-
* The GSplatComponentElement interface provides properties and methods for manipulating
|
|
11341
|
-
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-gsplat/ | `<pc-gsplat>`} elements.
|
|
11342
|
-
* The GSplatComponentElement interface also inherits the properties and methods of the
|
|
11343
|
-
* {@link HTMLElement} interface.
|
|
11344
|
-
*
|
|
11345
|
-
* @category Components
|
|
11346
|
-
*/
|
|
11347
|
-
class GSplatComponentElement extends ComponentElement {
|
|
11348
|
-
_asset = '';
|
|
11349
|
-
_castShadows = false;
|
|
11350
|
-
_lodBaseDistance = 5;
|
|
11351
|
-
_lodMultiplier = 3;
|
|
11352
|
-
_lodRangeMin = 0;
|
|
11353
|
-
_lodRangeMax = 99;
|
|
11354
|
-
/** @ignore */
|
|
11355
|
-
constructor() {
|
|
11356
|
-
super('gsplat');
|
|
11357
|
-
}
|
|
11358
|
-
getInitialComponentData() {
|
|
11359
|
-
return {
|
|
11360
|
-
asset: useAsset(this._asset),
|
|
11361
|
-
castShadows: this._castShadows,
|
|
11362
|
-
lodBaseDistance: this._lodBaseDistance,
|
|
11363
|
-
lodMultiplier: this._lodMultiplier,
|
|
11364
|
-
lodRangeMin: this._lodRangeMin,
|
|
11365
|
-
lodRangeMax: this._lodRangeMax
|
|
11366
|
-
};
|
|
11367
|
-
}
|
|
11368
|
-
/**
|
|
11369
|
-
* Gets the underlying PlayCanvas gsplat component.
|
|
11370
|
-
* @returns The gsplat component.
|
|
11371
|
-
*/
|
|
11372
|
-
get component() {
|
|
11373
|
-
return super.component;
|
|
11374
|
-
}
|
|
11375
|
-
/**
|
|
11376
|
-
* Sets id of the `pc-asset` to use for the splat.
|
|
11377
|
-
* @param value - The asset ID.
|
|
11378
|
-
*/
|
|
11379
|
-
set asset(value) {
|
|
11380
|
-
this._asset = value;
|
|
11381
|
-
const asset = useAsset(value);
|
|
11382
|
-
if (this.component && asset) {
|
|
11383
|
-
this.component.asset = asset;
|
|
11384
|
-
}
|
|
11385
|
-
}
|
|
11386
|
-
/**
|
|
11387
|
-
* Gets the id of the `pc-asset` to use for the splat.
|
|
11388
|
-
* @returns The asset ID.
|
|
11389
|
-
*/
|
|
11390
|
-
get asset() {
|
|
11391
|
-
return this._asset;
|
|
11392
|
-
}
|
|
11393
|
-
/**
|
|
11394
|
-
* Sets whether the splat casts shadows.
|
|
11395
|
-
* @param value - Whether the splat casts shadows.
|
|
11396
|
-
*/
|
|
11397
|
-
set castShadows(value) {
|
|
11398
|
-
this._castShadows = value;
|
|
11399
|
-
if (this.component) {
|
|
11400
|
-
this.component.castShadows = value;
|
|
11401
|
-
}
|
|
11402
|
-
}
|
|
11403
|
-
/**
|
|
11404
|
-
* Gets whether the splat casts shadows.
|
|
11405
|
-
* @returns Whether the splat casts shadows.
|
|
11406
|
-
*/
|
|
11407
|
-
get castShadows() {
|
|
11408
|
-
return this._castShadows;
|
|
11409
|
-
}
|
|
11410
|
-
/**
|
|
11411
|
-
* Sets the base distance for the first LOD transition (LOD 0 to LOD 1). Splats closer than
|
|
11412
|
-
* this distance use the highest quality LOD. Each subsequent LOD level transitions at a
|
|
11413
|
-
* progressively larger distance, controlled by {@link lodMultiplier}. Clamped to a minimum of
|
|
11414
|
-
* 0.1. Defaults to 5. Only affects assets that contain LOD levels (e.g. `.lod-meta.json`).
|
|
11415
|
-
* @param value - The LOD base distance.
|
|
11416
|
-
*/
|
|
11417
|
-
set lodBaseDistance(value) {
|
|
11418
|
-
this._lodBaseDistance = value;
|
|
11419
|
-
if (this.component) {
|
|
11420
|
-
this.component.lodBaseDistance = value;
|
|
11421
|
-
}
|
|
11422
|
-
}
|
|
11423
|
-
/**
|
|
11424
|
-
* Gets the base distance for the first LOD transition.
|
|
11425
|
-
* @returns The LOD base distance.
|
|
11426
|
-
*/
|
|
11427
|
-
get lodBaseDistance() {
|
|
11428
|
-
return this._lodBaseDistance;
|
|
12425
|
+
get overlap() {
|
|
12426
|
+
return this._overlap;
|
|
11429
12427
|
}
|
|
11430
12428
|
/**
|
|
11431
|
-
* Sets the
|
|
11432
|
-
*
|
|
11433
|
-
* values keep higher quality at distance; higher values switch to coarser LODs sooner. Clamped
|
|
11434
|
-
* to a minimum of 1.2. Defaults to 3. Only affects assets that contain LOD levels (e.g.
|
|
11435
|
-
* `.lod-meta.json`).
|
|
11436
|
-
* @param value - The LOD multiplier.
|
|
12429
|
+
* Sets the pitch of the sound slot.
|
|
12430
|
+
* @param value - The pitch.
|
|
11437
12431
|
*/
|
|
11438
|
-
set
|
|
11439
|
-
this.
|
|
11440
|
-
if (this.
|
|
11441
|
-
this.
|
|
12432
|
+
set pitch(value) {
|
|
12433
|
+
this._pitch = value;
|
|
12434
|
+
if (this.soundSlot) {
|
|
12435
|
+
this.soundSlot.pitch = value;
|
|
11442
12436
|
}
|
|
11443
12437
|
}
|
|
11444
12438
|
/**
|
|
11445
|
-
* Gets the
|
|
11446
|
-
* @returns The
|
|
12439
|
+
* Gets the pitch of the sound slot.
|
|
12440
|
+
* @returns The pitch.
|
|
11447
12441
|
*/
|
|
11448
|
-
get
|
|
11449
|
-
return this.
|
|
12442
|
+
get pitch() {
|
|
12443
|
+
return this._pitch;
|
|
11450
12444
|
}
|
|
11451
12445
|
/**
|
|
11452
|
-
* Sets the
|
|
11453
|
-
*
|
|
11454
|
-
* quality (largest) LOD files. Defaults to 0. Only affects assets that contain LOD levels (e.g.
|
|
11455
|
-
* `.lod-meta.json`).
|
|
11456
|
-
* @param value - The minimum LOD index.
|
|
12446
|
+
* Sets the start time of the sound slot.
|
|
12447
|
+
* @param value - The start time.
|
|
11457
12448
|
*/
|
|
11458
|
-
set
|
|
11459
|
-
this.
|
|
11460
|
-
if (this.
|
|
11461
|
-
this.
|
|
12449
|
+
set startTime(value) {
|
|
12450
|
+
this._startTime = value;
|
|
12451
|
+
if (this.soundSlot) {
|
|
12452
|
+
this.soundSlot.startTime = value;
|
|
11462
12453
|
}
|
|
11463
12454
|
}
|
|
11464
12455
|
/**
|
|
11465
|
-
* Gets the
|
|
11466
|
-
* @returns The
|
|
12456
|
+
* Gets the start time of the sound slot.
|
|
12457
|
+
* @returns The start time.
|
|
11467
12458
|
*/
|
|
11468
|
-
get
|
|
11469
|
-
return this.
|
|
12459
|
+
get startTime() {
|
|
12460
|
+
return this._startTime;
|
|
11470
12461
|
}
|
|
11471
12462
|
/**
|
|
11472
|
-
* Sets the
|
|
11473
|
-
*
|
|
11474
|
-
* cap". Defaults to 99. Only affects assets that contain LOD levels (e.g. `.lod-meta.json`).
|
|
11475
|
-
* @param value - The maximum LOD index.
|
|
12463
|
+
* Sets the volume of the sound slot.
|
|
12464
|
+
* @param value - The volume.
|
|
11476
12465
|
*/
|
|
11477
|
-
set
|
|
11478
|
-
this.
|
|
11479
|
-
if (this.
|
|
11480
|
-
this.
|
|
12466
|
+
set volume(value) {
|
|
12467
|
+
this._volume = value;
|
|
12468
|
+
if (this.soundSlot) {
|
|
12469
|
+
this.soundSlot.volume = value;
|
|
11481
12470
|
}
|
|
11482
12471
|
}
|
|
11483
12472
|
/**
|
|
11484
|
-
* Gets the
|
|
11485
|
-
* @returns The
|
|
12473
|
+
* Gets the volume of the sound slot.
|
|
12474
|
+
* @returns The volume.
|
|
11486
12475
|
*/
|
|
11487
|
-
get
|
|
11488
|
-
return this.
|
|
12476
|
+
get volume() {
|
|
12477
|
+
return this._volume;
|
|
11489
12478
|
}
|
|
11490
12479
|
static get observedAttributes() {
|
|
11491
|
-
return [
|
|
11492
|
-
...super.observedAttributes,
|
|
11493
|
-
'asset',
|
|
11494
|
-
'cast-shadows',
|
|
11495
|
-
'lod-base-distance',
|
|
11496
|
-
'lod-multiplier',
|
|
11497
|
-
'lod-range-min',
|
|
11498
|
-
'lod-range-max'
|
|
11499
|
-
];
|
|
12480
|
+
return ['asset', 'auto-play', 'duration', 'loop', 'name', 'overlap', 'pitch', 'start-time', 'volume'];
|
|
11500
12481
|
}
|
|
11501
12482
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
11502
|
-
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
11503
12483
|
switch (name) {
|
|
11504
12484
|
case 'asset':
|
|
11505
12485
|
this.asset = newValue ?? '';
|
|
11506
12486
|
break;
|
|
11507
|
-
case '
|
|
11508
|
-
this.
|
|
12487
|
+
case 'auto-play':
|
|
12488
|
+
this.autoPlay = parseBool(newValue, false);
|
|
11509
12489
|
break;
|
|
11510
|
-
case '
|
|
11511
|
-
this.
|
|
12490
|
+
case 'duration':
|
|
12491
|
+
this.duration = parseNumber(newValue, null, name);
|
|
11512
12492
|
break;
|
|
11513
|
-
case '
|
|
11514
|
-
this.
|
|
12493
|
+
case 'loop':
|
|
12494
|
+
this.loop = parseBool(newValue, false);
|
|
11515
12495
|
break;
|
|
11516
|
-
case '
|
|
11517
|
-
this.
|
|
12496
|
+
case 'name':
|
|
12497
|
+
this.name = newValue ?? '';
|
|
11518
12498
|
break;
|
|
11519
|
-
case '
|
|
11520
|
-
this.
|
|
12499
|
+
case 'overlap':
|
|
12500
|
+
this.overlap = parseBool(newValue, false);
|
|
12501
|
+
break;
|
|
12502
|
+
case 'pitch':
|
|
12503
|
+
this.pitch = parseNumber(newValue, 1, name);
|
|
12504
|
+
break;
|
|
12505
|
+
case 'start-time':
|
|
12506
|
+
this.startTime = parseNumber(newValue, 0, name);
|
|
12507
|
+
break;
|
|
12508
|
+
case 'volume':
|
|
12509
|
+
this.volume = parseNumber(newValue, 1, name);
|
|
11521
12510
|
break;
|
|
11522
12511
|
}
|
|
11523
12512
|
}
|
|
11524
12513
|
}
|
|
11525
|
-
customElements.define('pc-
|
|
12514
|
+
customElements.define('pc-sound', SoundSlotElement);
|
|
11526
12515
|
|
|
11527
12516
|
/**
|
|
11528
|
-
*
|
|
11529
|
-
*
|
|
11530
|
-
*
|
|
11531
|
-
*
|
|
11532
|
-
* @param node - The node to format.
|
|
11533
|
-
* @param counts - The number of nodes bearing each name.
|
|
11534
|
-
* @returns The formatted line.
|
|
11535
|
-
*/
|
|
11536
|
-
const formatNode = (node, counts) => {
|
|
11537
|
-
const index = (counts.get(node.name) ?? 0) > 1 ? ` [${node.index}]` : '';
|
|
11538
|
-
const components = node.components.length > 0 ? ` (${node.components.join(', ')})` : '';
|
|
11539
|
-
// Braces rather than brackets: `[N]` already means a match index on this line
|
|
11540
|
-
const materials = node.materials.length > 0 ? ` {${node.materials.map((slot) => slot.name ?? 'null').join(', ')}}` : '';
|
|
11541
|
-
return `${node.name}${index}${components}${materials}`;
|
|
11542
|
-
};
|
|
11543
|
-
/**
|
|
11544
|
-
* Formats the printable form of a hierarchy subtree.
|
|
11545
|
-
*
|
|
11546
|
-
* @param root - The subtree root.
|
|
11547
|
-
* @param counts - The number of nodes bearing each name.
|
|
11548
|
-
* @returns The tree, one line per node.
|
|
11549
|
-
*/
|
|
11550
|
-
const formatHierarchy = (root, counts) => {
|
|
11551
|
-
const lines = [formatNode(root, counts)];
|
|
11552
|
-
const walk = (node, prefix) => {
|
|
11553
|
-
node.children.forEach((child, i) => {
|
|
11554
|
-
const last = i === node.children.length - 1;
|
|
11555
|
-
lines.push(`${prefix}${last ? '└─ ' : '├─ '}${formatNode(child, counts)}`);
|
|
11556
|
-
walk(child, `${prefix}${last ? ' ' : '│ '}`);
|
|
11557
|
-
});
|
|
11558
|
-
};
|
|
11559
|
-
walk(root, '');
|
|
11560
|
-
return lines.join('\n');
|
|
11561
|
-
};
|
|
11562
|
-
/**
|
|
11563
|
-
* The ModelElement interface provides properties and methods for manipulating
|
|
11564
|
-
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | `<pc-model>`} elements.
|
|
11565
|
-
* The ModelElement interface also inherits the properties and methods of the
|
|
12517
|
+
* The GSplatComponentElement interface provides properties and methods for manipulating
|
|
12518
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-gsplat/ | `<pc-gsplat>`} elements.
|
|
12519
|
+
* The GSplatComponentElement interface also inherits the properties and methods of the
|
|
11566
12520
|
* {@link HTMLElement} interface.
|
|
11567
12521
|
*
|
|
11568
|
-
*
|
|
11569
|
-
* been added to the scene — `entity` is non-null by then. A failed load also settles readiness,
|
|
11570
|
-
* with `entity` remaining `null`: readiness means the load settled, not that it succeeded — listen
|
|
11571
|
-
* for `error`, or check `entity`, to tell the outcomes apart. Changing `asset` re-arms readiness
|
|
11572
|
-
* and instantiates anew, so a `ready()` obtained after the change resolves against the new
|
|
11573
|
-
* hierarchy. A `pc-model` outside a `pc-app`, or referencing an unknown asset id, warns and never
|
|
11574
|
-
* becomes ready.
|
|
11575
|
-
*
|
|
11576
|
-
* @fires {Event} load - Fired each time a container asset finishes instantiating, including
|
|
11577
|
-
* re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
|
|
11578
|
-
* capture-phase listener on an ancestor.
|
|
11579
|
-
* @fires {ErrorEvent} error - Fired when the container asset fails to load, with the engine's
|
|
11580
|
-
* error in `message`. Does not bubble. The element still becomes ready — readiness means the load
|
|
11581
|
-
* settled, not that it succeeded.
|
|
12522
|
+
* @category Components
|
|
11582
12523
|
*/
|
|
11583
|
-
class
|
|
12524
|
+
class GSplatComponentElement extends ComponentElement {
|
|
11584
12525
|
_asset = '';
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
/**
|
|
11594
|
-
* The pending asset subscriptions of the current load, if it is waiting for its asset. Held
|
|
11595
|
-
* so that whatever supersedes the load can detach the handlers from the asset, rather than
|
|
11596
|
-
* leave them registered until the asset settles (or forever, if it never does).
|
|
11597
|
-
*/
|
|
11598
|
-
_loadHandle = null;
|
|
11599
|
-
_errorHandle = null;
|
|
11600
|
-
/**
|
|
11601
|
-
* The root entity of the instantiated model. `null` until the container asset has loaded
|
|
11602
|
-
* and been instantiated, and again once the element has been removed from the document.
|
|
11603
|
-
* @returns The model's root entity, or `null`.
|
|
11604
|
-
*/
|
|
11605
|
-
get entity() {
|
|
11606
|
-
return this._entity;
|
|
12526
|
+
_castShadows = false;
|
|
12527
|
+
_lodBaseDistance = 5;
|
|
12528
|
+
_lodMultiplier = 3;
|
|
12529
|
+
_lodRangeMin = 0;
|
|
12530
|
+
_lodRangeMax = 99;
|
|
12531
|
+
/** @ignore */
|
|
12532
|
+
constructor() {
|
|
12533
|
+
super('gsplat');
|
|
11607
12534
|
}
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
*
|
|
11617
|
-
* The snapshot is plain data, computed afresh each call: it does not follow later changes
|
|
11618
|
-
* to the hierarchy, and mutating it changes nothing.
|
|
11619
|
-
*
|
|
11620
|
-
* @returns The root of the instantiated node tree, or `null`.
|
|
11621
|
-
*/
|
|
11622
|
-
hierarchy() {
|
|
11623
|
-
const root = this._entity;
|
|
11624
|
-
if (!root) {
|
|
11625
|
-
return null;
|
|
11626
|
-
}
|
|
11627
|
-
// Ordinals are assigned in the traversal resolution searches — pre-order depth-first
|
|
11628
|
-
// from the model root, the root itself included — so each node's index is exactly what
|
|
11629
|
-
// a pc-node's index attribute selects. Once the walk completes, the map holds the total
|
|
11630
|
-
// count per name, which is what the printable form reads to annotate only shared names.
|
|
11631
|
-
const ordinals = new Map();
|
|
11632
|
-
const describe = (entity, pathBelowRoot) => {
|
|
11633
|
-
const index = ordinals.get(entity.name) ?? 0;
|
|
11634
|
-
ordinals.set(entity.name, index + 1);
|
|
11635
|
-
const node = {
|
|
11636
|
-
name: entity.name,
|
|
11637
|
-
// The root has no path below itself; its own name stands in, as it does for
|
|
11638
|
-
// the path a pc-node bound to the root reports.
|
|
11639
|
-
path: pathBelowRoot || entity.name,
|
|
11640
|
-
index,
|
|
11641
|
-
// A plain GraphNode grafted into the hierarchy has no component storage
|
|
11642
|
-
components: Object.keys(entity.c ?? {}).sort(),
|
|
11643
|
-
materials: (entity.render?.meshInstances ?? []).map((meshInstance, slot) => ({
|
|
11644
|
-
index: slot,
|
|
11645
|
-
name: meshInstance.material?.name ?? null
|
|
11646
|
-
})),
|
|
11647
|
-
children: entity.children.map((child) => describe(child, pathBelowRoot ? `${pathBelowRoot}/${child.name}` : child.name))
|
|
11648
|
-
};
|
|
11649
|
-
// Non-enumerable, keeping the snapshot plain data under JSON.stringify, spreads and
|
|
11650
|
-
// key enumeration. Deferred to call time, by which the ordinal map holds its totals.
|
|
11651
|
-
Object.defineProperty(node, 'toString', {
|
|
11652
|
-
enumerable: false,
|
|
11653
|
-
value: () => formatHierarchy(node, ordinals)
|
|
11654
|
-
});
|
|
11655
|
-
return node;
|
|
12535
|
+
getInitialComponentData() {
|
|
12536
|
+
return {
|
|
12537
|
+
asset: useAsset(this._asset),
|
|
12538
|
+
castShadows: this._castShadows,
|
|
12539
|
+
lodBaseDistance: this._lodBaseDistance,
|
|
12540
|
+
lodMultiplier: this._lodMultiplier,
|
|
12541
|
+
lodRangeMin: this._lodRangeMin,
|
|
12542
|
+
lodRangeMax: this._lodRangeMax
|
|
11656
12543
|
};
|
|
11657
|
-
return describe(root, '');
|
|
11658
12544
|
}
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
12545
|
+
/**
|
|
12546
|
+
* Gets the underlying PlayCanvas gsplat component.
|
|
12547
|
+
* @returns The gsplat component.
|
|
12548
|
+
*/
|
|
12549
|
+
get component() {
|
|
12550
|
+
return super.component;
|
|
12551
|
+
}
|
|
12552
|
+
/**
|
|
12553
|
+
* Sets id of the `pc-asset` to use for the splat.
|
|
12554
|
+
* @param value - The asset ID.
|
|
12555
|
+
*/
|
|
12556
|
+
set asset(value) {
|
|
12557
|
+
this._asset = value;
|
|
12558
|
+
const asset = useAsset(value);
|
|
12559
|
+
if (this.component && asset) {
|
|
12560
|
+
this.component.asset = asset;
|
|
11667
12561
|
}
|
|
11668
|
-
this._loadModel();
|
|
11669
12562
|
}
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
12563
|
+
/**
|
|
12564
|
+
* Gets the id of the `pc-asset` to use for the splat.
|
|
12565
|
+
* @returns The asset ID.
|
|
12566
|
+
*/
|
|
12567
|
+
get asset() {
|
|
12568
|
+
return this._asset;
|
|
11675
12569
|
}
|
|
11676
|
-
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
12570
|
+
/**
|
|
12571
|
+
* Sets whether the splat casts shadows.
|
|
12572
|
+
* @param value - Whether the splat casts shadows.
|
|
12573
|
+
*/
|
|
12574
|
+
set castShadows(value) {
|
|
12575
|
+
this._castShadows = value;
|
|
12576
|
+
if (this.component) {
|
|
12577
|
+
this.component.castShadows = value;
|
|
12578
|
+
}
|
|
11681
12579
|
}
|
|
11682
12580
|
/**
|
|
11683
|
-
*
|
|
11684
|
-
*
|
|
11685
|
-
* model's entity always has world transforms.
|
|
12581
|
+
* Gets whether the splat casts shadows.
|
|
12582
|
+
* @returns Whether the splat casts shadows.
|
|
11686
12583
|
*/
|
|
11687
|
-
|
|
11688
|
-
this.
|
|
11689
|
-
this.dispatchEvent(new Event('load'));
|
|
12584
|
+
get castShadows() {
|
|
12585
|
+
return this._castShadows;
|
|
11690
12586
|
}
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
// connection cycle. The entity is captured above and the generation re-checked, so a
|
|
11703
|
-
// stale resume cannot parent an entity a newer cycle has already destroyed.
|
|
11704
|
-
const parentEntityElement = this.closestEntity;
|
|
11705
|
-
if (parentEntityElement) {
|
|
11706
|
-
parentEntityElement.ready().then(() => {
|
|
11707
|
-
if (generation !== this._loadGeneration) {
|
|
11708
|
-
return;
|
|
11709
|
-
}
|
|
11710
|
-
parentEntityElement.entity.addChild(entity);
|
|
11711
|
-
this._announceLoad();
|
|
11712
|
-
});
|
|
11713
|
-
}
|
|
11714
|
-
else {
|
|
11715
|
-
const appElement = this.closestApp;
|
|
11716
|
-
if (appElement) {
|
|
11717
|
-
appElement.ready().then(() => {
|
|
11718
|
-
if (generation !== this._loadGeneration) {
|
|
11719
|
-
return;
|
|
11720
|
-
}
|
|
11721
|
-
appElement.app.root.addChild(entity);
|
|
11722
|
-
this._announceLoad();
|
|
11723
|
-
});
|
|
11724
|
-
}
|
|
12587
|
+
/**
|
|
12588
|
+
* Sets the base distance for the first LOD transition (LOD 0 to LOD 1). Splats closer than
|
|
12589
|
+
* this distance use the highest quality LOD. Each subsequent LOD level transitions at a
|
|
12590
|
+
* progressively larger distance, controlled by {@link lodMultiplier}. Clamped to a minimum of
|
|
12591
|
+
* 0.1. Defaults to 5. Only affects assets that contain LOD levels (e.g. `.lod-meta.json`).
|
|
12592
|
+
* @param value - The LOD base distance.
|
|
12593
|
+
*/
|
|
12594
|
+
set lodBaseDistance(value) {
|
|
12595
|
+
this._lodBaseDistance = value;
|
|
12596
|
+
if (this.component) {
|
|
12597
|
+
this.component.lodBaseDistance = value;
|
|
11725
12598
|
}
|
|
11726
12599
|
}
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
if (!asset) {
|
|
11747
|
-
// An empty id is a legitimate transient (the asset may be assigned later); a
|
|
11748
|
-
// non-empty one that resolves to nothing is a dead end - say so rather than staying
|
|
11749
|
-
// silently pending.
|
|
11750
|
-
if (this._asset) {
|
|
11751
|
-
console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
|
|
11752
|
-
}
|
|
11753
|
-
return;
|
|
11754
|
-
}
|
|
11755
|
-
if (asset.loaded) {
|
|
11756
|
-
this._instantiate(asset.resource);
|
|
12600
|
+
/**
|
|
12601
|
+
* Gets the base distance for the first LOD transition.
|
|
12602
|
+
* @returns The LOD base distance.
|
|
12603
|
+
*/
|
|
12604
|
+
get lodBaseDistance() {
|
|
12605
|
+
return this._lodBaseDistance;
|
|
12606
|
+
}
|
|
12607
|
+
/**
|
|
12608
|
+
* Sets the multiplier between successive LOD distance thresholds. Each LOD level transitions
|
|
12609
|
+
* at this factor times the previous level's distance, creating a geometric progression. Lower
|
|
12610
|
+
* values keep higher quality at distance; higher values switch to coarser LODs sooner. Clamped
|
|
12611
|
+
* to a minimum of 1.2. Defaults to 3. Only affects assets that contain LOD levels (e.g.
|
|
12612
|
+
* `.lod-meta.json`).
|
|
12613
|
+
* @param value - The LOD multiplier.
|
|
12614
|
+
*/
|
|
12615
|
+
set lodMultiplier(value) {
|
|
12616
|
+
this._lodMultiplier = value;
|
|
12617
|
+
if (this.component) {
|
|
12618
|
+
this.component.lodMultiplier = value;
|
|
11757
12619
|
}
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
message: err instanceof Error ? err.message : String(err)
|
|
11778
|
-
}));
|
|
11779
|
-
this._onReady();
|
|
11780
|
-
});
|
|
12620
|
+
}
|
|
12621
|
+
/**
|
|
12622
|
+
* Gets the multiplier between successive LOD distance thresholds.
|
|
12623
|
+
* @returns The LOD multiplier.
|
|
12624
|
+
*/
|
|
12625
|
+
get lodMultiplier() {
|
|
12626
|
+
return this._lodMultiplier;
|
|
12627
|
+
}
|
|
12628
|
+
/**
|
|
12629
|
+
* Sets the minimum allowed LOD index (inclusive). The LOD selected by distance is clamped so it
|
|
12630
|
+
* never goes finer (lower index) than this value. Raising it avoids downloading the highest
|
|
12631
|
+
* quality (largest) LOD files. Defaults to 0. Only affects assets that contain LOD levels (e.g.
|
|
12632
|
+
* `.lod-meta.json`).
|
|
12633
|
+
* @param value - The minimum LOD index.
|
|
12634
|
+
*/
|
|
12635
|
+
set lodRangeMin(value) {
|
|
12636
|
+
this._lodRangeMin = value;
|
|
12637
|
+
if (this.component) {
|
|
12638
|
+
this.component.lodRangeMin = value;
|
|
11781
12639
|
}
|
|
11782
12640
|
}
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
12641
|
+
/**
|
|
12642
|
+
* Gets the minimum allowed LOD index.
|
|
12643
|
+
* @returns The minimum LOD index.
|
|
12644
|
+
*/
|
|
12645
|
+
get lodRangeMin() {
|
|
12646
|
+
return this._lodRangeMin;
|
|
11786
12647
|
}
|
|
11787
12648
|
/**
|
|
11788
|
-
* Sets the
|
|
11789
|
-
*
|
|
12649
|
+
* Sets the maximum allowed LOD index (inclusive). The LOD selected by distance is clamped so it
|
|
12650
|
+
* never goes coarser (higher index) than this value. The default of 99 effectively means "no
|
|
12651
|
+
* cap". Defaults to 99. Only affects assets that contain LOD levels (e.g. `.lod-meta.json`).
|
|
12652
|
+
* @param value - The maximum LOD index.
|
|
11790
12653
|
*/
|
|
11791
|
-
set
|
|
11792
|
-
this.
|
|
11793
|
-
if (this.
|
|
11794
|
-
this.
|
|
12654
|
+
set lodRangeMax(value) {
|
|
12655
|
+
this._lodRangeMax = value;
|
|
12656
|
+
if (this.component) {
|
|
12657
|
+
this.component.lodRangeMax = value;
|
|
11795
12658
|
}
|
|
11796
12659
|
}
|
|
11797
12660
|
/**
|
|
11798
|
-
* Gets the
|
|
11799
|
-
* @returns The
|
|
12661
|
+
* Gets the maximum allowed LOD index.
|
|
12662
|
+
* @returns The maximum LOD index.
|
|
11800
12663
|
*/
|
|
11801
|
-
get
|
|
11802
|
-
return this.
|
|
12664
|
+
get lodRangeMax() {
|
|
12665
|
+
return this._lodRangeMax;
|
|
11803
12666
|
}
|
|
11804
12667
|
static get observedAttributes() {
|
|
11805
|
-
return [
|
|
12668
|
+
return [
|
|
12669
|
+
...super.observedAttributes,
|
|
12670
|
+
'asset',
|
|
12671
|
+
'cast-shadows',
|
|
12672
|
+
'lod-base-distance',
|
|
12673
|
+
'lod-multiplier',
|
|
12674
|
+
'lod-range-min',
|
|
12675
|
+
'lod-range-max'
|
|
12676
|
+
];
|
|
11806
12677
|
}
|
|
11807
12678
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
12679
|
+
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
11808
12680
|
switch (name) {
|
|
11809
12681
|
case 'asset':
|
|
11810
12682
|
this.asset = newValue ?? '';
|
|
11811
12683
|
break;
|
|
12684
|
+
case 'cast-shadows':
|
|
12685
|
+
this.castShadows = parseBool(newValue, false);
|
|
12686
|
+
break;
|
|
12687
|
+
case 'lod-base-distance':
|
|
12688
|
+
this.lodBaseDistance = parseNumber(newValue, 5, name);
|
|
12689
|
+
break;
|
|
12690
|
+
case 'lod-multiplier':
|
|
12691
|
+
this.lodMultiplier = parseNumber(newValue, 3, name);
|
|
12692
|
+
break;
|
|
12693
|
+
case 'lod-range-min':
|
|
12694
|
+
this.lodRangeMin = parseNumber(newValue, 0, name);
|
|
12695
|
+
break;
|
|
12696
|
+
case 'lod-range-max':
|
|
12697
|
+
this.lodRangeMax = parseNumber(newValue, 99, name);
|
|
12698
|
+
break;
|
|
11812
12699
|
}
|
|
11813
12700
|
}
|
|
11814
12701
|
}
|
|
11815
|
-
customElements.define('pc-
|
|
12702
|
+
customElements.define('pc-gsplat', GSplatComponentElement);
|
|
11816
12703
|
|
|
11817
12704
|
/**
|
|
11818
12705
|
* Parses one mapping into its valid rules, warning for each entry that is not one: an unknown
|
|
@@ -13177,5 +14064,5 @@ class SkyElement extends AsyncElement {
|
|
|
13177
14064
|
}
|
|
13178
14065
|
customElements.define('pc-sky', SkyElement);
|
|
13179
14066
|
|
|
13180
|
-
export { AppElement, AssetElement, AsyncElement, ButtonComponentElement, CameraComponentElement, CollisionComponentElement, ComponentElement, ElementComponentElement, EntityBaseElement, EntityElement, GSplatComponentElement, JointComponentElement, LayoutChildComponentElement, LayoutGroupComponentElement, LightComponentElement, ListenerComponentElement, MaterialElement, ModelElement, ModuleElement, NodeElement, ParticleSystemComponentElement, RenderComponentElement, RigidBodyComponentElement, SceneElement, ScreenComponentElement, ScriptComponentElement, ScriptElement, ScrollViewComponentElement, ScrollbarComponentElement, SkyElement, SoundComponentElement, SoundSlotElement, whenReady };
|
|
14067
|
+
export { AnimClipElement, AnimComponentElement, AppElement, AssetElement, AsyncElement, ButtonComponentElement, CameraComponentElement, CollisionComponentElement, ComponentElement, ElementComponentElement, EntityBaseElement, EntityElement, GSplatComponentElement, JointComponentElement, LayoutChildComponentElement, LayoutGroupComponentElement, LightComponentElement, ListenerComponentElement, MaterialElement, ModelElement, ModuleElement, NodeElement, ParticleSystemComponentElement, RenderComponentElement, RigidBodyComponentElement, SceneElement, ScreenComponentElement, ScriptComponentElement, ScriptElement, ScrollViewComponentElement, ScrollbarComponentElement, SkyElement, SoundComponentElement, SoundSlotElement, whenReady };
|
|
13181
14068
|
//# sourceMappingURL=pwc.mjs.map
|