@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.
@@ -906,6 +906,65 @@
906
906
  ],
907
907
  "references": []
908
908
  },
909
+ {
910
+ "name": "pc-anim-clip",
911
+ "description": "The AnimClipElement interface provides properties and methods for manipulating\n[`<pc-anim-clip>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim-clip/)\nelements. The AnimClipElement interface also inherits the properties and methods of the\nHTMLElement interface.\n\nA clip declares one named animation on its parent `<pc-anim>`. `name` is both the clip's name\nand the track looked up in the clip's source: an explicit `asset` (a `container`, an\n`animation` `.glb`, or an `animclip` JSON), or, without one, the container of the `<pc-model>`\nenclosing the parent `<pc-anim>`. A source holding a single track supplies it whatever it is\nnamed; in a multi-track source the track named `name` is chosen, falling back to the first\nwith a warning. The element becomes ready once its resolved track is assigned.\n\n---\n\n\n### **Events:**\n - **ready** - Fired when the element is fully initialized — once per readiness cycle, so an element that is torn down and re-initialized (for example by removing and re-inserting it) fires it again. Bubbles and is composed.\n\n### **Methods:**\n - **ready(): __** - Returns a promise that resolves with this element when it's ready. This is the low-level\nprimitive underlying whenReady, which is the recommended way to wait for elements.\n\nReadiness tracks the element's current lifecycle: once a ready element is torn down (for\nexample by removing it from the document), this returns a fresh promise that resolves when\nthe element is next ready. A promise obtained earlier stays resolved — call this again\nafter re-inserting an element rather than reusing a promise from before its removal.",
912
+ "attributes": [
913
+ {
914
+ "name": "asset",
915
+ "description": "The id of the `pc-asset` supplying the clip's track.",
916
+ "values": []
917
+ },
918
+ {
919
+ "name": "loop",
920
+ "description": "Whether the clip loops.",
921
+ "values": []
922
+ },
923
+ {
924
+ "name": "name",
925
+ "description": "The name of the clip.",
926
+ "values": []
927
+ },
928
+ {
929
+ "name": "speed",
930
+ "description": "The playback speed of the clip.",
931
+ "values": []
932
+ }
933
+ ],
934
+ "references": []
935
+ },
936
+ {
937
+ "name": "pc-anim",
938
+ "description": "The AnimComponentElement interface provides properties and methods for manipulating\n[`<pc-anim>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim/) elements.\nThe AnimComponentElement interface also inherits the properties and methods of the\nHTMLElement interface.\n\nThe element drives animation clips over the host entity's hierarchy. Clips come from\n`<pc-anim-clip>` children — or, when the element is a direct child of a `<pc-model>` and\ndeclares no clips, every animation of that model's container asset is assigned, named by track\nname, in container order. The first clip plays automatically (opt out with `activate=\"false\"`);\nswitch clips declaratively through the `clip` attribute, or imperatively through play\nand transition. Tracks bind to scene nodes by name, so any hierarchy whose node names\nmatch a clip's curves can be animated — a model's skeleton is simply the common case.\n\nThe engine reports no clip completion: a non-looping clip holds its last pose silently. Poll\nthe underlying AnimComponent (via component) for playback state beyond what\nthis element exposes.\n\n---\n\n\n### **Events:**\n - **ready** - Fired when the element is fully initialized — once per readiness cycle, so an element that is torn down and re-initialized (for example by removing and re-inserting it) fires it again. Bubbles and is composed.\n\n### **Methods:**\n - **pause()** - Pauses playback, preserving the playhead — play resumes from where it stopped.\n- **play(name: _string_)** - Resumes playback, optionally switching to a named clip first (a hard cut). A name that\nmatches no clip leaves the selection unchanged.\n- **ready(): __** - Returns a promise that resolves with this element when it's ready. This is the low-level\nprimitive underlying whenReady, which is the recommended way to wait for elements.\n\nReadiness tracks the element's current lifecycle: once a ready element is torn down (for\nexample by removing it from the document), this returns a fresh promise that resolves when\nthe element is next ready. A promise obtained earlier stays resolved — call this again\nafter re-inserting an element rather than reusing a promise from before its removal.\n- **transition(name: _string_, time: _number_)** - Cross-fades to a named clip and ensures playback is running. A name that matches no clip\nleaves the selection unchanged.",
939
+ "attributes": [
940
+ {
941
+ "name": "activate",
942
+ "description": "Whether playback starts automatically once a clip is assigned.",
943
+ "values": []
944
+ },
945
+ {
946
+ "name": "clip",
947
+ "description": "The name of the active clip.",
948
+ "values": []
949
+ },
950
+ {
951
+ "name": "enabled",
952
+ "description": "The enabled state of the component.",
953
+ "values": []
954
+ },
955
+ {
956
+ "name": "speed",
957
+ "description": "The playback speed multiplier applied across all clips.",
958
+ "values": []
959
+ },
960
+ {
961
+ "name": "transition-time",
962
+ "description": "The cross-fade duration of clip switches made through the `clip` attribute.",
963
+ "values": []
964
+ }
965
+ ],
966
+ "references": []
967
+ },
909
968
  {
910
969
  "name": "pc-button",
911
970
  "description": "The ButtonComponentElement interface provides properties and methods for manipulating\n[`<pc-button>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-button/) elements.\nThe ButtonComponentElement interface also inherits the properties and methods of the\nHTMLElement interface.\n\n---\n\n\n### **Events:**\n - **ready** - Fired when the element is fully initialized — once per readiness cycle, so an element that is torn down and re-initialized (for example by removing and re-inserting it) fires it again. Bubbles and is composed.\n\n### **Methods:**\n - **ready(): __** - Returns a promise that resolves with this element when it's ready. This is the low-level\nprimitive underlying whenReady, which is the recommended way to wait for elements.\n\nReadiness tracks the element's current lifecycle: once a ready element is torn down (for\nexample by removing it from the document), this returns a fresh promise that resolves when\nthe element is next ready. A promise obtained earlier stays resolved — call this again\nafter re-inserting an element rather than reusing a promise from before its removal.",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
3
3
  "name": "@playcanvas/web-components",
4
- "version": "0.15.0",
4
+ "version": "0.16.0",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
@@ -1825,6 +1825,161 @@
1825
1825
  ]
1826
1826
  }
1827
1827
  },
1828
+ {
1829
+ "name": "pc-anim-clip",
1830
+ "description": "The AnimClipElement interface provides properties and methods for manipulating\n[`<pc-anim-clip>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim-clip/)\nelements. The AnimClipElement interface also inherits the properties and methods of the\nHTMLElement interface.\n\nA clip declares one named animation on its parent `<pc-anim>`. `name` is both the clip's name\nand the track looked up in the clip's source: an explicit `asset` (a `container`, an\n`animation` `.glb`, or an `animclip` JSON), or, without one, the container of the `<pc-model>`\nenclosing the parent `<pc-anim>`. A source holding a single track supplies it whatever it is\nnamed; in a multi-track source the track named `name` is chosen, falling back to the first\nwith a warning. The element becomes ready once its resolved track is assigned.\n\n---\n\n\n### **Events:**\n - **ready** - Fired when the element is fully initialized — once per readiness cycle, so an element that is torn down and re-initialized (for example by removing and re-inserting it) fires it again. Bubbles and is composed.\n\n### **Methods:**\n - **ready(): __** - Returns a promise that resolves with this element when it's ready. This is the low-level\nprimitive underlying whenReady, which is the recommended way to wait for elements.\n\nReadiness tracks the element's current lifecycle: once a ready element is torn down (for\nexample by removing it from the document), this returns a fresh promise that resolves when\nthe element is next ready. A promise obtained earlier stays resolved — call this again\nafter re-inserting an element rather than reusing a promise from before its removal.",
1831
+ "doc-url": "",
1832
+ "attributes": [
1833
+ {
1834
+ "name": "asset",
1835
+ "description": "The id of the `pc-asset` supplying the clip's track.",
1836
+ "value": { "type": "string" }
1837
+ },
1838
+ {
1839
+ "name": "loop",
1840
+ "description": "Whether the clip loops.",
1841
+ "value": { "type": "boolean", "default": "true" }
1842
+ },
1843
+ {
1844
+ "name": "name",
1845
+ "description": "The name of the clip.",
1846
+ "value": { "type": "string" }
1847
+ },
1848
+ {
1849
+ "name": "speed",
1850
+ "description": "The playback speed of the clip.",
1851
+ "value": { "type": "number", "default": "1" }
1852
+ }
1853
+ ],
1854
+ "events": [
1855
+ {
1856
+ "name": "ready",
1857
+ "type": "CustomEvent",
1858
+ "description": "Fired when the element is fully initialized — once per readiness cycle, so an element that is torn down and re-initialized (for example by removing and re-inserting it) fires it again. Bubbles and is composed."
1859
+ }
1860
+ ],
1861
+ "js": {
1862
+ "properties": [
1863
+ {
1864
+ "name": "asset",
1865
+ "description": "Gets the id of the `pc-asset` supplying the clip's track."
1866
+ },
1867
+ {
1868
+ "name": "closestApp",
1869
+ "description": "The nearest ancestor `<pc-app>` element, or `null` if this element has no `<pc-app>`\nancestor. The search starts at the parent, so an element never resolves to itself.",
1870
+ "type": "AppElement | null"
1871
+ },
1872
+ {
1873
+ "name": "closestEntity",
1874
+ "description": "The nearest ancestor element that fronts an entity — `<pc-entity>` or `<pc-node>` — or\n`null` if this element has no such ancestor. The search starts at the parent, so an element\nnever resolves to itself.",
1875
+ "type": "EntityBaseElement | null"
1876
+ },
1877
+ { "name": "loop", "description": "Gets whether the clip loops." },
1878
+ { "name": "name", "description": "Gets the name of the clip." },
1879
+ {
1880
+ "name": "speed",
1881
+ "description": "Gets the playback speed of the clip."
1882
+ }
1883
+ ],
1884
+ "events": [
1885
+ {
1886
+ "name": "ready",
1887
+ "type": "CustomEvent",
1888
+ "description": "Fired when the element is fully initialized — once per readiness cycle, so an element that is torn down and re-initialized (for example by removing and re-inserting it) fires it again. Bubbles and is composed."
1889
+ }
1890
+ ]
1891
+ }
1892
+ },
1893
+ {
1894
+ "name": "pc-anim",
1895
+ "description": "The AnimComponentElement interface provides properties and methods for manipulating\n[`<pc-anim>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim/) elements.\nThe AnimComponentElement interface also inherits the properties and methods of the\nHTMLElement interface.\n\nThe element drives animation clips over the host entity's hierarchy. Clips come from\n`<pc-anim-clip>` children — or, when the element is a direct child of a `<pc-model>` and\ndeclares no clips, every animation of that model's container asset is assigned, named by track\nname, in container order. The first clip plays automatically (opt out with `activate=\"false\"`);\nswitch clips declaratively through the `clip` attribute, or imperatively through play\nand transition. Tracks bind to scene nodes by name, so any hierarchy whose node names\nmatch a clip's curves can be animated — a model's skeleton is simply the common case.\n\nThe engine reports no clip completion: a non-looping clip holds its last pose silently. Poll\nthe underlying AnimComponent (via component) for playback state beyond what\nthis element exposes.\n\n---\n\n\n### **Events:**\n - **ready** - Fired when the element is fully initialized — once per readiness cycle, so an element that is torn down and re-initialized (for example by removing and re-inserting it) fires it again. Bubbles and is composed.\n\n### **Methods:**\n - **pause()** - Pauses playback, preserving the playhead — play resumes from where it stopped.\n- **play(name: _string_)** - Resumes playback, optionally switching to a named clip first (a hard cut). A name that\nmatches no clip leaves the selection unchanged.\n- **ready(): __** - Returns a promise that resolves with this element when it's ready. This is the low-level\nprimitive underlying whenReady, which is the recommended way to wait for elements.\n\nReadiness tracks the element's current lifecycle: once a ready element is torn down (for\nexample by removing it from the document), this returns a fresh promise that resolves when\nthe element is next ready. A promise obtained earlier stays resolved — call this again\nafter re-inserting an element rather than reusing a promise from before its removal.\n- **transition(name: _string_, time: _number_)** - Cross-fades to a named clip and ensures playback is running. A name that matches no clip\nleaves the selection unchanged.",
1896
+ "doc-url": "",
1897
+ "attributes": [
1898
+ {
1899
+ "name": "activate",
1900
+ "description": "Whether playback starts automatically once a clip is assigned.",
1901
+ "value": { "type": "boolean", "default": "true" }
1902
+ },
1903
+ {
1904
+ "name": "clip",
1905
+ "description": "The name of the active clip.",
1906
+ "value": { "type": "string" }
1907
+ },
1908
+ {
1909
+ "name": "enabled",
1910
+ "description": "The enabled state of the component.",
1911
+ "value": { "type": "boolean", "default": "true" }
1912
+ },
1913
+ {
1914
+ "name": "speed",
1915
+ "description": "The playback speed multiplier applied across all clips.",
1916
+ "value": { "type": "number", "default": "1" }
1917
+ },
1918
+ {
1919
+ "name": "transition-time",
1920
+ "description": "The cross-fade duration of clip switches made through the `clip` attribute.",
1921
+ "value": { "type": "number", "default": "0" }
1922
+ }
1923
+ ],
1924
+ "events": [
1925
+ {
1926
+ "name": "ready",
1927
+ "type": "CustomEvent",
1928
+ "description": "Fired when the element is fully initialized — once per readiness cycle, so an element that is torn down and re-initialized (for example by removing and re-inserting it) fires it again. Bubbles and is composed."
1929
+ }
1930
+ ],
1931
+ "js": {
1932
+ "properties": [
1933
+ {
1934
+ "name": "activate",
1935
+ "description": "Gets whether playback starts automatically once a clip is assigned."
1936
+ },
1937
+ {
1938
+ "name": "clip",
1939
+ "description": "Gets the name of the active clip."
1940
+ },
1941
+ {
1942
+ "name": "clips",
1943
+ "description": "Gets the names of the assigned clips.",
1944
+ "type": "string[]"
1945
+ },
1946
+ {
1947
+ "name": "closestApp",
1948
+ "description": "The nearest ancestor `<pc-app>` element, or `null` if this element has no `<pc-app>`\nancestor. The search starts at the parent, so an element never resolves to itself.",
1949
+ "type": "AppElement | null"
1950
+ },
1951
+ {
1952
+ "name": "closestEntity",
1953
+ "description": "The nearest ancestor element that fronts an entity — `<pc-entity>` or `<pc-node>` — or\n`null` if this element has no such ancestor. The search starts at the parent, so an element\nnever resolves to itself.",
1954
+ "type": "EntityBaseElement | null"
1955
+ },
1956
+ {
1957
+ "name": "component",
1958
+ "description": "Gets the underlying PlayCanvas anim component.",
1959
+ "type": "Component | null"
1960
+ },
1961
+ {
1962
+ "name": "enabled",
1963
+ "description": "Gets the enabled state of the component."
1964
+ },
1965
+ {
1966
+ "name": "speed",
1967
+ "description": "Gets the playback speed multiplier applied across all clips."
1968
+ },
1969
+ {
1970
+ "name": "transitionTime",
1971
+ "description": "Gets the cross-fade duration of clip switches made through the `clip` attribute."
1972
+ }
1973
+ ],
1974
+ "events": [
1975
+ {
1976
+ "name": "ready",
1977
+ "type": "CustomEvent",
1978
+ "description": "Fired when the element is fully initialized — once per readiness cycle, so an element that is torn down and re-initialized (for example by removing and re-inserting it) fires it again. Bubbles and is composed."
1979
+ }
1980
+ ]
1981
+ }
1982
+ },
1828
1983
  {
1829
1984
  "name": "pc-button",
1830
1985
  "description": "The ButtonComponentElement interface provides properties and methods for manipulating\n[`<pc-button>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-button/) elements.\nThe ButtonComponentElement interface also inherits the properties and methods of the\nHTMLElement interface.\n\n---\n\n\n### **Events:**\n - **ready** - Fired when the element is fully initialized — once per readiness cycle, so an element that is torn down and re-initialized (for example by removing and re-inserting it) fires it again. Bubbles and is composed.\n\n### **Methods:**\n - **ready(): __** - Returns a promise that resolves with this element when it's ready. This is the low-level\nprimitive underlying whenReady, which is the recommended way to wait for elements.\n\nReadiness tracks the element's current lifecycle: once a ready element is torn down (for\nexample by removing it from the document), this returns a fresh promise that resolves when\nthe element is next ready. A promise obtained earlier stays resolved — call this again\nafter re-inserting an element rather than reusing a promise from before its removal.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcanvas/web-components",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "author": "PlayCanvas <support@playcanvas.com>",
5
5
  "homepage": "https://playcanvas.com",
6
6
  "description": "Web Components for the PlayCanvas Engine",
@@ -0,0 +1,395 @@
1
+ import type { Asset, EventHandle } from 'playcanvas';
2
+ import { AnimTrack } from 'playcanvas';
3
+
4
+ import { AssetElement, useAsset } from '../asset';
5
+ import { AsyncElement } from '../async-element';
6
+ import { ModelElement } from '../model';
7
+ import { parseBool, parseNumber } from '../parse';
8
+
9
+ import type { ContainerWithAnimations } from './anim-component';
10
+ import { AnimComponentElement } from './anim-component';
11
+
12
+ /**
13
+ * The AnimClipElement interface provides properties and methods for manipulating
14
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim-clip/ | `<pc-anim-clip>`}
15
+ * elements. The AnimClipElement interface also inherits the properties and methods of the
16
+ * {@link HTMLElement} interface.
17
+ *
18
+ * A clip declares one named animation on its parent `<pc-anim>`. `name` is both the clip's name
19
+ * and the track looked up in the clip's source: an explicit `asset` (a `container`, an
20
+ * `animation` `.glb`, or an `animclip` JSON), or, without one, the container of the `<pc-model>`
21
+ * enclosing the parent `<pc-anim>`. A source holding a single track supplies it whatever it is
22
+ * named; in a multi-track source the track named `name` is chosen, falling back to the first
23
+ * with a warning. The element becomes ready once its resolved track is assigned.
24
+ *
25
+ * @category Components
26
+ */
27
+ class AnimClipElement extends AsyncElement {
28
+ /**
29
+ * The `<pc-anim>` this clip was adopted by, captured when the parent adopts the clip and on
30
+ * connection.
31
+ *
32
+ * `disconnectedCallback` cannot rediscover it: by the time the element is disconnected its
33
+ * `parentElement` is already `null`, so a lookup would both fail to find the component and
34
+ * emit a misleading "must be a direct child" warning for what is an ordinary removal.
35
+ */
36
+ private _animElement: AnimComponentElement | null = null;
37
+
38
+ private _asset = '';
39
+
40
+ /**
41
+ * Incremented on every connect and disconnect, and captured by connectedCallback on entry —
42
+ * a resume from an await abandons itself if the value has moved on, so a stale callback can
43
+ * neither act on a torn-down tree nor register its clip alongside a re-inserted element's
44
+ * own callback.
45
+ */
46
+ private _connectionGeneration = 0;
47
+
48
+ private _errorHandle: EventHandle | null = null;
49
+
50
+ /**
51
+ * Incremented on every track resolution and on disconnect, and captured by a resolution when
52
+ * it starts. A resolution that resumes from an await or an asset callback abandons itself if
53
+ * the value has moved on, so a superseded resolution cannot hand a stale track to the parent.
54
+ */
55
+ private _loadGeneration = 0;
56
+
57
+ /**
58
+ * The pending asset subscriptions of the current resolution, if it is waiting for its asset.
59
+ * Held so that whatever supersedes the resolution can detach the handlers from the asset,
60
+ * rather than leave them registered until the asset settles (or forever, if it never does).
61
+ */
62
+ private _loadHandle: EventHandle | null = null;
63
+
64
+ private _loop = true;
65
+
66
+ private _name = '';
67
+
68
+ private _speed = 1;
69
+
70
+ /**
71
+ * The source complaint already made — the asset id it was made for, or `''` for the
72
+ * no-asset-no-model case — so re-resolutions (host cycles, model reloads) do not repeat it.
73
+ */
74
+ private _warnedSource: string | null = null;
75
+
76
+ /**
77
+ * Whether the owning `<pc-anim>` already rejected this clip's name — its sweeps re-run on
78
+ * host cycles and must not repeat the complaint.
79
+ */
80
+ private _warnedInvalid = false;
81
+
82
+ /**
83
+ * The clip's resolved track. `null` until resolution completes, during which the owning
84
+ * `<pc-anim>` assigns the engine's placeholder track in its stead.
85
+ *
86
+ * @internal
87
+ */
88
+ _track: AnimTrack | null = null;
89
+
90
+ async connectedCallback() {
91
+ const generation = ++this._connectionGeneration;
92
+
93
+ const animElement = this.animElement;
94
+ await animElement?.ready();
95
+
96
+ // The element may have been removed (perhaps re-inserted, which runs a callback of its
97
+ // own), or its parent torn down, while we were waiting. A <pc-app> disconnects before
98
+ // its children, so by the time we resume the component can already be gone - see the
99
+ // matching guard in disconnectedCallback below.
100
+ const component = animElement ? animElement.component : null;
101
+ if (generation !== this._connectionGeneration || !animElement || !component) {
102
+ return;
103
+ }
104
+
105
+ this._animElement = animElement;
106
+ animElement._registerClip(this);
107
+ }
108
+
109
+ disconnectedCallback() {
110
+ // Invalidate any connectedCallback or track resolution still suspended on an await
111
+ this._connectionGeneration++;
112
+ this._loadGeneration++;
113
+ this._detachLoadHandlers();
114
+
115
+ // Uses the cached parent rather than a fresh lookup, since parentElement is already null
116
+ // by now. The component itself is null if the whole <pc-app> is being torn down —
117
+ // parents disconnect first and have already removed the component.
118
+ this._animElement?._unregisterClip(this);
119
+ this._animElement = null;
120
+ this._track = null;
121
+ this._resetReady();
122
+ }
123
+
124
+ protected get animElement(): AnimComponentElement | null {
125
+ const animElement = this.parentElement as AnimComponentElement;
126
+
127
+ if (!(animElement instanceof AnimComponentElement)) {
128
+ const label = this._name ? ` '${this._name}'` : '';
129
+ console.warn(`pc-anim-clip${label} must be a direct child of a pc-anim element`);
130
+ return null;
131
+ }
132
+
133
+ return animElement;
134
+ }
135
+
136
+ private _detachLoadHandlers() {
137
+ this._loadHandle?.off();
138
+ this._loadHandle = null;
139
+ this._errorHandle?.off();
140
+ this._errorHandle = null;
141
+ }
142
+
143
+ /**
144
+ * Reports a name-validation failure from the owning `<pc-anim>`, once per name value.
145
+ *
146
+ * @param message - The complaint.
147
+ * @internal
148
+ */
149
+ _markInvalid(message: string) {
150
+ if (this._warnedInvalid) {
151
+ return;
152
+ }
153
+ this._warnedInvalid = true;
154
+ console.warn(message);
155
+ }
156
+
157
+ /**
158
+ * Resolves the clip's track from its source and hands it to the owning `<pc-anim>`. Called
159
+ * by the parent whenever the clip is (re)adopted, and again when the source changes; a newer
160
+ * resolution supersedes one still in flight. The element becomes ready once the resolved
161
+ * track is assigned.
162
+ *
163
+ * @param animElement - The owning `<pc-anim>`.
164
+ * @internal
165
+ */
166
+ async _resolveTrack(animElement: AnimComponentElement) {
167
+ this._animElement = animElement;
168
+
169
+ const generation = ++this._loadGeneration;
170
+ this._detachLoadHandlers();
171
+
172
+ if (this._asset) {
173
+ const asset = useAsset(this._asset);
174
+ if (!asset) {
175
+ this._warnSource(`pc-anim-clip '${this._name}' could not find asset '${this._asset}' - clip not assigned`);
176
+ return;
177
+ }
178
+ if (asset.loaded) {
179
+ this._extractTrack(asset, `asset '${this._asset}'`);
180
+ return;
181
+ }
182
+ // Whichever of load/error fires first detaches the other. The generation is
183
+ // re-checked even though a superseded handler is detached: the detach relies on how
184
+ // the engine's event emitter treats removal, while the check holds on its own.
185
+ this._loadHandle = asset.once('load', () => {
186
+ this._detachLoadHandlers();
187
+ if (generation !== this._loadGeneration) {
188
+ return;
189
+ }
190
+ this._extractTrack(asset, `asset '${this._asset}'`);
191
+ });
192
+ this._errorHandle = asset.once('error', () => {
193
+ this._detachLoadHandlers();
194
+ if (generation !== this._loadGeneration) {
195
+ return;
196
+ }
197
+ this._warnSource(`pc-anim-clip '${this._name}' - asset '${this._asset}' failed to load - clip not assigned`);
198
+ });
199
+ return;
200
+ }
201
+
202
+ const model = animElement.parentElement;
203
+ if (!(model instanceof ModelElement)) {
204
+ this._warnSource(`pc-anim-clip '${this._name}' has no asset and no enclosing pc-model - clip not assigned`);
205
+ return;
206
+ }
207
+
208
+ await model.ready();
209
+ if (generation !== this._loadGeneration) {
210
+ return;
211
+ }
212
+
213
+ const asset = AssetElement.get(model.asset);
214
+ if (!asset?.resource) {
215
+ // The model's load failed; it already reported the error
216
+ return;
217
+ }
218
+ this._extractTrack(asset, `model '${model.asset}'`);
219
+ }
220
+
221
+ /**
222
+ * Complains about the clip's source, once per source value — resolutions re-run on host
223
+ * cycles and model reloads, and must not repeat the complaint.
224
+ */
225
+ private _warnSource(message: string) {
226
+ if (this._warnedSource === this._asset) {
227
+ return;
228
+ }
229
+ this._warnedSource = this._asset;
230
+ console.warn(message);
231
+ }
232
+
233
+ /**
234
+ * Picks the clip's track out of a loaded source asset: the track named `name`, or a lone
235
+ * track whatever it is named, or the first of several with a warning.
236
+ *
237
+ * @param asset - The loaded source asset.
238
+ * @param source - How warnings name the source.
239
+ */
240
+ private _extractTrack(asset: Asset, source: string) {
241
+ const label = `pc-anim-clip '${this._name}'`;
242
+
243
+ // Widened: the engine registers an 'animclip' handler but omits the type from the
244
+ // Asset.type union
245
+ const type: string = asset.type;
246
+
247
+ let candidates: unknown[];
248
+ switch (type) {
249
+ case 'container':
250
+ candidates = (asset.resource as ContainerWithAnimations).animations.map(
251
+ (animationAsset) => animationAsset.resource
252
+ );
253
+ break;
254
+ case 'animation':
255
+ candidates = asset.resources;
256
+ break;
257
+ case 'animclip':
258
+ candidates = [asset.resource];
259
+ break;
260
+ default:
261
+ this._warnSource(`${label} - ${source} has type '${asset.type}', expected 'container', 'animation' or 'animclip' - clip not assigned`);
262
+ return;
263
+ }
264
+
265
+ // A JSON 'animation' asset parses to the engine's legacy Animation class, which the anim
266
+ // system rejects - only real AnimTracks qualify
267
+ const tracks = candidates.filter((candidate): candidate is AnimTrack => candidate instanceof AnimTrack);
268
+ if (tracks.length === 0) {
269
+ this._warnSource(`${label} - ${source} contains no usable animation track - clip not assigned`);
270
+ return;
271
+ }
272
+
273
+ let track = tracks.find((candidate) => candidate.name === this._name);
274
+ if (!track) {
275
+ track = tracks[0];
276
+ if (tracks.length > 1) {
277
+ console.warn(
278
+ `${label} - no track named '${this._name}' in ${source} - using '${track.name}' (available: ${tracks.map((candidate) => candidate.name).join(', ')})`
279
+ );
280
+ }
281
+ }
282
+
283
+ this._track = track;
284
+ if (this._animElement?._onClipResolved(this)) {
285
+ this._onReady();
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Sets the id of the `pc-asset` supplying the clip's track: a `container`, an `animation`
291
+ * `.glb`, or an `animclip` JSON. When empty, the track comes from the container of the
292
+ * `<pc-model>` enclosing the parent `<pc-anim>`.
293
+ * @param value - The asset id.
294
+ */
295
+ set asset(value: string) {
296
+ this._asset = value;
297
+ this._warnedSource = null;
298
+ if (this._animElement) {
299
+ this._resetReady();
300
+ this._track = null;
301
+ this._resolveTrack(this._animElement);
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Gets the id of the `pc-asset` supplying the clip's track.
307
+ * @returns The asset id.
308
+ */
309
+ get asset() {
310
+ return this._asset;
311
+ }
312
+
313
+ /**
314
+ * Sets whether the clip loops. A non-looping clip holds its last pose when it ends — the
315
+ * engine reports no completion. Defaults to `true`.
316
+ * @param value - Whether the clip loops.
317
+ */
318
+ set loop(value: boolean) {
319
+ this._loop = value;
320
+ this._animElement?._onClipParamsChanged(this);
321
+ }
322
+
323
+ /**
324
+ * Gets whether the clip loops.
325
+ * @returns Whether the clip loops.
326
+ */
327
+ get loop() {
328
+ return this._loop;
329
+ }
330
+
331
+ /**
332
+ * Sets the name of the clip: the name it is played by, and the track looked up in the
333
+ * clip's source. Names must be unique within a `<pc-anim>` and must not contain `.`.
334
+ * @param value - The clip name.
335
+ */
336
+ set name(value: string) {
337
+ this._name = value;
338
+ this._warnedInvalid = false;
339
+ if (this._animElement) {
340
+ this._resetReady();
341
+ this._animElement._refreshClips();
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Gets the name of the clip.
347
+ * @returns The clip name.
348
+ */
349
+ get name() {
350
+ return this._name;
351
+ }
352
+
353
+ /**
354
+ * Sets the playback speed of the clip, where negative values play it backwards. Applies
355
+ * immediately, preserving the playhead. Defaults to 1.
356
+ * @param value - The playback speed.
357
+ */
358
+ set speed(value: number) {
359
+ this._speed = value;
360
+ this._animElement?._onClipParamsChanged(this);
361
+ }
362
+
363
+ /**
364
+ * Gets the playback speed of the clip.
365
+ * @returns The playback speed.
366
+ */
367
+ get speed() {
368
+ return this._speed;
369
+ }
370
+
371
+ static get observedAttributes() {
372
+ return ['asset', 'loop', 'name', 'speed'];
373
+ }
374
+
375
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
376
+ switch (name) {
377
+ case 'asset':
378
+ this.asset = newValue ?? '';
379
+ break;
380
+ case 'loop':
381
+ this.loop = parseBool(newValue, true);
382
+ break;
383
+ case 'name':
384
+ this.name = newValue ?? '';
385
+ break;
386
+ case 'speed':
387
+ this.speed = parseNumber(newValue, 1, name);
388
+ break;
389
+ }
390
+ }
391
+ }
392
+
393
+ customElements.define('pc-anim-clip', AnimClipElement);
394
+
395
+ export { AnimClipElement };