pelican-engine 0.0.0-stage → 0.3.1

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.
Files changed (192) hide show
  1. package/AGENTS.md +147 -0
  2. package/LICENSE +22 -0
  3. package/README.md +129 -2
  4. package/distribution.json +197 -0
  5. package/docs/AI-WORKFLOW.md +517 -0
  6. package/docs/ARCHITECTURE.md +268 -0
  7. package/docs/CAPABILITIES.md +264 -0
  8. package/docs/DECISIONS.md +107 -0
  9. package/docs/M3-USAGE.md +192 -0
  10. package/docs/PELICAN-MIGRATION.md +38 -0
  11. package/docs/RENDER-BACKENDS.md +50 -0
  12. package/docs/ROADMAP.md +239 -0
  13. package/docs/THIRD-PARTY-NOTICES.md +151 -0
  14. package/docs/api/REFERENCE.md +1435 -0
  15. package/docs/api/index.json +1139 -0
  16. package/docs/npm/DISTRIBUTION.md +130 -0
  17. package/docs/npm/GAME-AUTHOR.md +147 -0
  18. package/engine.manifest.json +15 -0
  19. package/lib/assets/audio.js +67 -0
  20. package/lib/assets/errors.js +36 -0
  21. package/lib/assets/glb.js +277 -0
  22. package/lib/assets/index.js +53 -0
  23. package/lib/assets/jpeg.js +199 -0
  24. package/lib/assets/model.js +558 -0
  25. package/lib/assets/optimize.js +95 -0
  26. package/lib/assets/texture.js +187 -0
  27. package/lib/compiler/distribution.js +17 -0
  28. package/lib/compiler/extract.js +425 -0
  29. package/lib/compiler/graph.js +366 -0
  30. package/lib/compiler/index.js +574 -0
  31. package/lib/compiler/normalize.js +1194 -0
  32. package/lib/compiler/source-map.js +25 -0
  33. package/lib/compiler/types.js +2 -0
  34. package/lib/compiler/version.js +93 -0
  35. package/lib/experiments/simulation/index.js +2 -0
  36. package/lib/experiments/simulation/math.js +41 -0
  37. package/lib/experiments/simulation/probe.js +215 -0
  38. package/lib/experiments/simulation/scenes.js +192 -0
  39. package/lib/experiments/simulation/solver.js +346 -0
  40. package/lib/experiments/simulation/types.js +1 -0
  41. package/lib/experiments/world/adapters.js +30 -0
  42. package/lib/experiments/world/geometry.js +180 -0
  43. package/lib/experiments/world/index.js +2 -0
  44. package/lib/experiments/world/probe.js +288 -0
  45. package/lib/experiments/world/types.js +1 -0
  46. package/lib/experiments/world/world.js +599 -0
  47. package/lib/navigation/index.js +157 -0
  48. package/lib/physics/index.js +909 -0
  49. package/lib/protocol/index.js +92 -0
  50. package/lib/renderer/acceleration.js +609 -0
  51. package/lib/renderer/browser.js +2015 -0
  52. package/lib/renderer/bsdf.js +260 -0
  53. package/lib/renderer/checkpoint.js +88 -0
  54. package/lib/renderer/compute-executor.js +1034 -0
  55. package/lib/renderer/compute-texture-assets.js +49 -0
  56. package/lib/renderer/compute-view-contract.js +1 -0
  57. package/lib/renderer/denoiser.js +361 -0
  58. package/lib/renderer/dielectric.js +360 -0
  59. package/lib/renderer/embedded-image-pixels.js +368 -0
  60. package/lib/renderer/emissive-lights.js +67 -0
  61. package/lib/renderer/environment-importance.js +55 -0
  62. package/lib/renderer/environment.js +346 -0
  63. package/lib/renderer/gameplay.js +464 -0
  64. package/lib/renderer/geometry.js +103 -0
  65. package/lib/renderer/graph-expressions.js +485 -0
  66. package/lib/renderer/graph-tsl.js +141 -0
  67. package/lib/renderer/graph.js +214 -0
  68. package/lib/renderer/managed-texture-sampling.js +29 -0
  69. package/lib/renderer/material-graphs.js +297 -0
  70. package/lib/renderer/medium.js +348 -0
  71. package/lib/renderer/models.js +1053 -0
  72. package/lib/renderer/motion-shader.js +40 -0
  73. package/lib/renderer/path-shader.js +196 -0
  74. package/lib/renderer/pathtrace.js +324 -0
  75. package/lib/renderer/postprocess-contract.js +1 -0
  76. package/lib/renderer/postprocess.js +494 -0
  77. package/lib/renderer/render-buffers.js +132 -0
  78. package/lib/renderer/render-textures.js +155 -0
  79. package/lib/renderer/startup.js +50 -0
  80. package/lib/renderer/surface-capture.js +199 -0
  81. package/lib/renderer/surface-gpu.js +270 -0
  82. package/lib/renderer/surface-motion.js +84 -0
  83. package/lib/renderer/surface-ray-shader.js +91 -0
  84. package/lib/renderer/surface-shader.js +88 -0
  85. package/lib/renderer/surface-tracer.js +1720 -0
  86. package/lib/renderer/surface.js +1425 -0
  87. package/lib/renderer/temporal-filter.js +512 -0
  88. package/lib/renderer/texture-pixels.js +207 -0
  89. package/lib/renderer/traversal-nodes.js +54 -0
  90. package/lib/renderer/ui.js +446 -0
  91. package/lib/renderer/vertex-bounds.js +127 -0
  92. package/lib/renderer/vertex-contract.js +1 -0
  93. package/lib/renderer/vertex-graphs.js +197 -0
  94. package/lib/renderer/volume-shader.js +50 -0
  95. package/lib/runtime/compute-contract.js +1 -0
  96. package/lib/runtime/compute-service.js +674 -0
  97. package/lib/runtime/contracts.js +1 -0
  98. package/lib/runtime/error.js +37 -0
  99. package/lib/runtime/events.js +189 -0
  100. package/lib/runtime/game-services.js +772 -0
  101. package/lib/runtime/host.js +839 -0
  102. package/lib/runtime/material-parameters.js +145 -0
  103. package/lib/runtime/plugins.js +1 -0
  104. package/lib/runtime/postprocess-contract.js +1 -0
  105. package/lib/runtime/postprocess.js +189 -0
  106. package/lib/runtime/prefabs.js +13 -0
  107. package/lib/runtime/random.js +88 -0
  108. package/lib/runtime/render-buffer-contract.js +1 -0
  109. package/lib/runtime/render-buffers.js +243 -0
  110. package/lib/runtime/render-capabilities.js +17 -0
  111. package/lib/runtime/render-settings.js +249 -0
  112. package/lib/runtime/render-texture-contract.js +1 -0
  113. package/lib/runtime/render-textures.js +271 -0
  114. package/lib/runtime/resources.js +648 -0
  115. package/lib/runtime/scheduler.js +765 -0
  116. package/lib/runtime/storage.js +169 -0
  117. package/lib/runtime/world.js +594 -0
  118. package/lib/schema/index.js +24 -0
  119. package/lib/sdk/graph-values.js +16 -0
  120. package/lib/sdk/index.d.ts +800 -0
  121. package/lib/sdk/index.js +82 -0
  122. package/lib/tools/api-index.js +113 -0
  123. package/lib/tools/browser-session.js +89 -0
  124. package/lib/tools/cli.js +513 -0
  125. package/lib/tools/compress-dist.js +59 -0
  126. package/lib/tools/control-client.js +153 -0
  127. package/lib/tools/control-server.js +633 -0
  128. package/lib/tools/diagnostic-code.js +3 -0
  129. package/lib/tools/doctor.js +496 -0
  130. package/lib/tools/npm/browser-cli.js +6 -0
  131. package/lib/tools/npm/entry.js +40 -0
  132. package/lib/tools/npm/layout.js +195 -0
  133. package/lib/tools/setup.js +153 -0
  134. package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
  135. package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
  136. package/licenses/@gltf-transform/core/LICENSE.md +21 -0
  137. package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
  138. package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
  139. package/licenses/@img/colour/LICENSE.md +82 -0
  140. package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
  141. package/licenses/@oxc-project/types/LICENSE +22 -0
  142. package/licenses/@rolldown/pluginutils/LICENSE +21 -0
  143. package/licenses/@tweenjs/tween.js/LICENSE +23 -0
  144. package/licenses/@types/ndarray/LICENSE +21 -0
  145. package/licenses/@types/node/LICENSE +21 -0
  146. package/licenses/@types/stats.js/LICENSE +21 -0
  147. package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
  148. package/licenses/@types/three/LICENSE +21 -0
  149. package/licenses/@types/webxr/LICENSE +21 -0
  150. package/licenses/@webgpu/types/LICENSE +26 -0
  151. package/licenses/THIRD-PARTY-NOTICES.md +139 -0
  152. package/licenses/cwise-compiler/LICENSE +22 -0
  153. package/licenses/detect-libc/LICENSE +201 -0
  154. package/licenses/esbuild/LICENSE.md +21 -0
  155. package/licenses/fast-png/LICENSE +21 -0
  156. package/licenses/fdir/LICENSE +7 -0
  157. package/licenses/fflate/LICENSE +21 -0
  158. package/licenses/iobuffer/LICENSE +22 -0
  159. package/licenses/iota-array/LICENSE +22 -0
  160. package/licenses/is-buffer/LICENSE +21 -0
  161. package/licenses/ktx-parse/LICENSE +21 -0
  162. package/licenses/lightningcss/LICENSE +373 -0
  163. package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
  164. package/licenses/meshoptimizer/LICENSE.md +21 -0
  165. package/licenses/nanoid/LICENSE +20 -0
  166. package/licenses/ndarray/LICENSE +22 -0
  167. package/licenses/ndarray-lanczos/LICENSE +21 -0
  168. package/licenses/ndarray-ops/LICENSE +22 -0
  169. package/licenses/ndarray-pixels/LICENSE +21 -0
  170. package/licenses/ngraph.path/LICENSE +21 -0
  171. package/licenses/picocolors/LICENSE +15 -0
  172. package/licenses/picomatch/LICENSE +21 -0
  173. package/licenses/playwright/LICENSE +202 -0
  174. package/licenses/playwright/NOTICE +5 -0
  175. package/licenses/playwright-core/LICENSE +202 -0
  176. package/licenses/playwright-core/NOTICE +5 -0
  177. package/licenses/postcss/LICENSE +20 -0
  178. package/licenses/prettier/LICENSE +7 -0
  179. package/licenses/property-graph/LICENSE +21 -0
  180. package/licenses/rolldown/LICENSE +25 -0
  181. package/licenses/semver/LICENSE +15 -0
  182. package/licenses/sharp/LICENSE +191 -0
  183. package/licenses/source-map-js/LICENSE +28 -0
  184. package/licenses/three/LICENSE +21 -0
  185. package/licenses/tinyglobby/LICENSE +21 -0
  186. package/licenses/tsx/LICENSE +21 -0
  187. package/licenses/typescript/LICENSE.txt +55 -0
  188. package/licenses/undici-types/LICENSE +21 -0
  189. package/licenses/uniq/LICENSE +22 -0
  190. package/licenses/vite/LICENSE.md +2084 -0
  191. package/package.json +49 -4
  192. package/web/index.html +34 -0
@@ -0,0 +1,1053 @@
1
+ import * as THREE from "three/webgpu";
2
+ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
3
+ import { clone as cloneSkeleton } from "three/addons/utils/SkeletonUtils.js";
4
+ import { associateEmbeddedImage, releaseEmbeddedImage, } from "./embedded-image-pixels.js";
5
+ /** Tracks decoded dependencies, including resources outside the default scene. */
6
+ class ModelResources {
7
+ geometries = new Set();
8
+ materials = new Set();
9
+ textures = new Set();
10
+ skeletons = new Set();
11
+ bitmaps = new Set();
12
+ collect(value) {
13
+ if (Array.isArray(value)) {
14
+ for (const item of value)
15
+ this.collect(item);
16
+ }
17
+ else if (value instanceof THREE.Object3D) {
18
+ value.traverse((object) => {
19
+ const mesh = object;
20
+ this.collect(mesh.geometry);
21
+ this.collect(mesh.material);
22
+ if (object.isSkinnedMesh)
23
+ this.collect(object.skeleton);
24
+ });
25
+ }
26
+ else if (value instanceof THREE.BufferGeometry)
27
+ this.geometries.add(value);
28
+ else if (value instanceof THREE.Skeleton)
29
+ this.skeletons.add(value);
30
+ else if (value instanceof THREE.Material && !this.materials.has(value)) {
31
+ this.materials.add(value);
32
+ for (const field of Object.values(value))
33
+ if (field instanceof THREE.Texture)
34
+ this.collect(field);
35
+ }
36
+ else if (value instanceof THREE.Texture) {
37
+ this.textures.add(value);
38
+ const data = value.source?.data;
39
+ if (data && typeof data.close === "function")
40
+ this.bitmaps.add(data);
41
+ }
42
+ }
43
+ dispose() {
44
+ for (const skeleton of this.skeletons)
45
+ skeleton.dispose();
46
+ for (const geometry of this.geometries)
47
+ geometry.dispose();
48
+ for (const material of this.materials)
49
+ material.dispose();
50
+ for (const texture of this.textures) {
51
+ releaseEmbeddedImage(texture);
52
+ texture.dispose();
53
+ }
54
+ for (const bitmap of this.bitmaps)
55
+ bitmap.close();
56
+ this.skeletons.clear();
57
+ this.geometries.clear();
58
+ this.materials.clear();
59
+ this.textures.clear();
60
+ this.bitmaps.clear();
61
+ }
62
+ }
63
+ function disposeInstanceSkeletons(root) {
64
+ const skeletons = new Set();
65
+ root.traverse((object) => {
66
+ if (object.isSkinnedMesh)
67
+ skeletons.add(object.skeleton);
68
+ });
69
+ for (const skeleton of skeletons)
70
+ skeleton.dispose();
71
+ }
72
+ function assetIdentity(source) {
73
+ if (typeof source === "string") {
74
+ if (!source.trim())
75
+ throw new Error("TC_MODEL_INVALID_ASSET");
76
+ return { id: source };
77
+ }
78
+ if (!source || typeof source !== "object")
79
+ throw new Error("TC_MODEL_INVALID_ASSET");
80
+ const asset = source;
81
+ const id = asset.id;
82
+ if (typeof id !== "string" || !id.trim())
83
+ throw new Error("TC_MODEL_INVALID_ASSET");
84
+ const url = "url" in source ? source.url : asset.path;
85
+ if (url !== undefined && (typeof url !== "string" || !url.trim()))
86
+ throw new Error("TC_MODEL_INVALID_ASSET");
87
+ return { id, url };
88
+ }
89
+ class ModelLibraryImpl {
90
+ options;
91
+ urls = new Map();
92
+ templates = new Map();
93
+ pending = new Map();
94
+ loads = new Set();
95
+ instances = new Map();
96
+ definitions = new Map();
97
+ fetcher;
98
+ closed = false;
99
+ disposing;
100
+ animation = {
101
+ play: (entity, clip, options = {}) => this.play(entity, clip, options),
102
+ state: (entity) => {
103
+ const instance = this.instance(entity);
104
+ return {
105
+ clip: instance.clip,
106
+ time: instance.action?.time ?? 0,
107
+ playing: instance.action?.isRunning() ?? false,
108
+ };
109
+ },
110
+ };
111
+ constructor(sources, options) {
112
+ this.options = options;
113
+ this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
114
+ if (Array.isArray(sources)) {
115
+ for (const source of sources) {
116
+ const item = assetIdentity(source);
117
+ if (item.url) {
118
+ const previous = this.urls.get(item.id);
119
+ if (previous && previous !== item.url)
120
+ throw new Error("TC_MODEL_ASSET_CONFLICT");
121
+ this.urls.set(item.id, item.url);
122
+ }
123
+ }
124
+ }
125
+ else {
126
+ for (const [id, url] of Object.entries(sources)) {
127
+ if (!id.trim() || typeof url !== "string" || !url.trim())
128
+ throw new Error("TC_MODEL_INVALID_ASSET");
129
+ this.urls.set(id, url);
130
+ }
131
+ }
132
+ }
133
+ async prepare(assets = [...this.urls.keys()], options = {}) {
134
+ this.assertOpen();
135
+ if (options.signal?.aborted)
136
+ throw new Error("TC_MODEL_CANCELLED");
137
+ const identities = assets.map(assetIdentity);
138
+ const urls = new Map(this.urls);
139
+ for (const asset of identities) {
140
+ if (asset.url) {
141
+ const previous = urls.get(asset.id);
142
+ if (previous && previous !== asset.url)
143
+ throw new Error("TC_MODEL_ASSET_CONFLICT");
144
+ urls.set(asset.id, asset.url);
145
+ }
146
+ if (!urls.has(asset.id))
147
+ throw new Error(`TC_MODEL_ASSET_UNKNOWN: ${asset.id}`);
148
+ }
149
+ for (const [id, url] of urls)
150
+ this.urls.set(id, url);
151
+ await Promise.all(identities.map((asset) => this.loadTemplate(asset.id, options.signal)));
152
+ }
153
+ register(entityId, definition, parent) {
154
+ this.assertOpen();
155
+ const normalized = this.normalizeDefinition(entityId, definition);
156
+ const previous = this.definitions.get(entityId);
157
+ this.definitions.set(entityId, {
158
+ definition: normalized,
159
+ parent: parent ?? previous?.parent,
160
+ });
161
+ }
162
+ get(entityId) {
163
+ this.assertOpen();
164
+ return this.instances.get(entityId)?.handle;
165
+ }
166
+ attach(entityId, parent) {
167
+ this.assertOpen();
168
+ const known = this.knownDefinition(entityId);
169
+ let instance = this.get(entityId);
170
+ if (!instance)
171
+ instance = this.instantiate(entityId, known.definition);
172
+ parent.add(instance.node);
173
+ this.knownDefinition(entityId).parent = parent;
174
+ return instance;
175
+ }
176
+ reconcile(activeIds, parent) {
177
+ this.assertOpen();
178
+ const ids = new Set();
179
+ for (const id of activeIds) {
180
+ if (ids.has(id))
181
+ throw new Error(`TC_MODEL_ENTITY_DUPLICATE: ${id}`);
182
+ this.knownDefinition(id);
183
+ ids.add(id);
184
+ }
185
+ const staged = this.stageInstances(ids);
186
+ this.commitInstances(ids, staged, parent);
187
+ return [...ids].map((id) => this.instances.get(id).handle);
188
+ }
189
+ reconcileBindings(bindings) {
190
+ this.assertOpen();
191
+ const definitions = this.candidateDefinitions(bindings);
192
+ const ids = new Set(definitions.keys());
193
+ const staged = this.stageInstances(ids, definitions);
194
+ try {
195
+ for (const [id, state] of staged)
196
+ definitions.get(id).parent?.add(state.node);
197
+ }
198
+ catch (error) {
199
+ for (const state of staged.values())
200
+ this.releaseInstance(state);
201
+ throw error;
202
+ }
203
+ for (const [id, known] of definitions)
204
+ this.definitions.set(id, known);
205
+ this.commitInstances(ids, staged, undefined, true);
206
+ return [...ids].map((id) => this.instances.get(id).handle);
207
+ }
208
+ candidateDefinitions(bindings) {
209
+ const result = new Map();
210
+ for (const { entity, definition } of bindings) {
211
+ if (result.has(entity))
212
+ throw new Error(`TC_MODEL_ENTITY_DUPLICATE: ${entity}`);
213
+ result.set(entity, {
214
+ definition: this.normalizeDefinition(entity, definition, true),
215
+ parent: this.definitions.get(entity)?.parent,
216
+ });
217
+ }
218
+ return result;
219
+ }
220
+ instantiate(entityId, definition, parent) {
221
+ this.assertOpen();
222
+ if (this.instances.has(entityId))
223
+ throw new Error(`TC_MODEL_ENTITY_DUPLICATE: ${entityId}`);
224
+ const normalized = this.normalizeDefinition(entityId, definition);
225
+ const state = this.createInstance(entityId, normalized);
226
+ const rememberedParent = parent ?? this.definitions.get(entityId)?.parent;
227
+ this.definitions.set(entityId, {
228
+ definition: normalized,
229
+ parent: rememberedParent,
230
+ });
231
+ this.instances.set(entityId, state);
232
+ // Explicit instantiate only attaches when a parent is supplied; attach/reconcile can
233
+ // re-use a remembered parent after a host restore.
234
+ parent?.add(state.node);
235
+ return state.handle;
236
+ }
237
+ remove(entityId) {
238
+ const state = this.instances.get(entityId);
239
+ if (!state)
240
+ return;
241
+ this.releaseInstance(state);
242
+ this.instances.delete(entityId);
243
+ }
244
+ knownDefinition(entity) {
245
+ const known = this.definitions.get(entity);
246
+ if (!known)
247
+ throw new Error(`TC_MODEL_DEFINITION_MISSING: ${entity}; register the model definition before restoring or reconciling`);
248
+ return known;
249
+ }
250
+ normalizeDefinition(entityId, definition, allowReplacement = false) {
251
+ if (typeof entityId !== "string" || !entityId.trim())
252
+ throw new Error("TC_MODEL_INVALID_ENTITY");
253
+ const assetId = typeof definition.asset === "string"
254
+ ? definition.asset
255
+ : definition.asset.id;
256
+ const template = this.templates.get(assetId);
257
+ if (!template)
258
+ throw new Error(`TC_MODEL_NOT_PREPARED: ${assetId}`);
259
+ const scale = definition.scale ?? 1;
260
+ if (!Number.isFinite(scale) || scale <= 0)
261
+ throw new Error("TC_MODEL_INVALID_SCALE");
262
+ for (const [label, vector] of [
263
+ ["OFFSET", definition.offset],
264
+ ["ROTATION", definition.rotation],
265
+ ])
266
+ if (vector !== undefined &&
267
+ (!Array.isArray(vector) ||
268
+ vector.length !== 3 ||
269
+ ![0, 1, 2].every((index) => Number.isFinite(vector[index]))))
270
+ throw new Error(`TC_MODEL_INVALID_${label}`);
271
+ for (const field of ["castShadow", "receiveShadow"])
272
+ if (definition[field] !== undefined &&
273
+ typeof definition[field] !== "boolean")
274
+ throw new Error("TC_MODEL_INVALID_SHADOW");
275
+ const override = definition.material;
276
+ const materialId = typeof override === "string"
277
+ ? override
278
+ : override &&
279
+ typeof override === "object" &&
280
+ override.kind === "material"
281
+ ? override.id
282
+ : undefined;
283
+ if (override !== undefined &&
284
+ (typeof materialId !== "string" || !materialId.trim()))
285
+ throw new Error(`TC_MODEL_INVALID_MATERIAL: ${entityId}; use a non-empty material ID or MaterialDefinition`);
286
+ const slots = definition.materialSlots;
287
+ if (slots !== undefined && override !== undefined)
288
+ throw new Error(`TC_MODEL_MATERIAL_CONFLICT: ${entityId}; use material or materialSlots, not both`);
289
+ let materialSlots;
290
+ if (slots !== undefined) {
291
+ if (!slots || typeof slots !== "object" || Array.isArray(slots) || !Object.keys(slots).length)
292
+ throw new Error(`TC_MODEL_INVALID_MATERIAL_SLOTS: ${entityId}; provide a non-empty slot map`);
293
+ materialSlots = Object.fromEntries(Object.entries(slots).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, value]) => {
294
+ const id = typeof value === "string" ? value : value && typeof value === "object" && value.kind === "material" ? value.id : undefined;
295
+ if (!name.trim() || typeof id !== "string" || !id.trim())
296
+ throw new Error(`TC_MODEL_INVALID_MATERIAL_SLOTS: ${entityId}: ${name}; provide a named slot and material ID`);
297
+ return [name, id];
298
+ }));
299
+ }
300
+ const normalized = {
301
+ castShadow: definition.castShadow !== false,
302
+ receiveShadow: definition.receiveShadow !== false,
303
+ asset: assetId,
304
+ ...(materialId !== undefined ? { material: materialId } : {}),
305
+ ...(materialSlots !== undefined ? { materialSlots } : {}),
306
+ scale,
307
+ offset: [...(definition.offset ?? [0, 0, 0])],
308
+ rotation: [...(definition.rotation ?? [0, 0, 0])],
309
+ ...(definition.animation
310
+ ? {
311
+ animation: {
312
+ clip: definition.animation.clip,
313
+ speed: definition.animation.speed ?? 1,
314
+ loop: definition.animation.loop ?? true,
315
+ },
316
+ }
317
+ : {}),
318
+ };
319
+ if (normalized.animation) {
320
+ if (!template.clips.some((clip) => clip.name === normalized.animation.clip))
321
+ throw new Error(`TC_MODEL_CLIP_MISSING: ${normalized.animation.clip}; available: ${template.clips.map((clip) => clip.name).join(", ")}`);
322
+ if (!Number.isFinite(normalized.animation.speed) ||
323
+ typeof normalized.animation.loop !== "boolean")
324
+ throw new Error("TC_MODEL_INVALID_ANIMATION_OPTIONS");
325
+ }
326
+ const known = this.definitions.get(entityId);
327
+ if (known &&
328
+ !allowReplacement &&
329
+ JSON.stringify(known.definition) !== JSON.stringify(normalized))
330
+ throw new Error(`TC_MODEL_DEFINITION_CONFLICT: ${entityId}; use a new library when authored model definitions change`);
331
+ return normalized;
332
+ }
333
+ createInstance(entityId, definition, initializeAnimation = true) {
334
+ // Registration precedes game-material preparation. Resolve once here, before
335
+ // cloning any private skeletons, and keep the borrowed material unowned.
336
+ const material = definition.material === undefined
337
+ ? undefined
338
+ : this.options.material?.(definition.material);
339
+ if (definition.material !== undefined && material === undefined)
340
+ throw new Error(`TC_MODEL_MATERIAL_MISSING: ${entityId}: ${definition.material}; prepare the game material before creating model instances`);
341
+ const assetId = definition.asset;
342
+ const template = this.templates.get(assetId);
343
+ const slotMaterials = new Map();
344
+ for (const [slot, id] of Object.entries(definition.materialSlots ?? {})) {
345
+ const replacement = this.options.material?.(id);
346
+ if (replacement === undefined)
347
+ throw new Error(`TC_MODEL_MATERIAL_MISSING: ${entityId}: ${id}; prepare the game material before creating model instances`);
348
+ slotMaterials.set(slot, replacement);
349
+ }
350
+ const unmatched = new Set(slotMaterials.keys());
351
+ const available = new Set();
352
+ if (unmatched.size)
353
+ template.scene.traverse((object) => {
354
+ if (object instanceof THREE.Mesh)
355
+ for (const source of Array.isArray(object.material) ? object.material : [object.material]) {
356
+ if (source.name)
357
+ available.add(source.name);
358
+ unmatched.delete(source.name);
359
+ }
360
+ });
361
+ if (unmatched.size) {
362
+ const names = [...available].sort().slice(0, 20);
363
+ const missing = [...unmatched].sort().slice(0, 20);
364
+ const label = (name) => JSON.stringify(name.length > 120 ? name.slice(0, 120) + "..." : name);
365
+ throw new Error(`TC_MODEL_MATERIAL_SLOT_MISSING: ${entityId}: ${missing.map(label).join(", ")}${unmatched.size > missing.length ? ` and ${unmatched.size - missing.length} more` : ""}; available loaded material names: ${names.length ? names.map(label).join(", ") : "(none)"}${available.size > names.length ? ` and ${available.size - names.length} more` : ""}; use exact loaded names`);
366
+ }
367
+ const root = cloneSkeleton(template.scene);
368
+ root.traverse((object) => {
369
+ if (object instanceof THREE.Mesh) {
370
+ if (material !== undefined)
371
+ object.material = Array.isArray(object.material)
372
+ ? object.material.map(() => material)
373
+ : material;
374
+ else if (slotMaterials.size)
375
+ object.material = Array.isArray(object.material)
376
+ ? object.material.map((source) => slotMaterials.get(source.name) ?? source)
377
+ : slotMaterials.get(object.material.name) ?? object.material;
378
+ object.castShadow = definition.castShadow !== false;
379
+ object.receiveShadow = definition.receiveShadow !== false;
380
+ }
381
+ });
382
+ const node = new THREE.Group();
383
+ node.name = `pelican-model:${entityId}`;
384
+ node.scale.setScalar(definition.scale);
385
+ node.position.fromArray(definition.offset);
386
+ node.rotation.set(...definition.rotation);
387
+ node.add(root);
388
+ const state = {
389
+ entity: entityId,
390
+ asset: assetId,
391
+ node,
392
+ handle: undefined,
393
+ root,
394
+ mixer: new THREE.AnimationMixer(root),
395
+ clips: new Map(template.clips.map((clip) => [clip.name, clip])),
396
+ actions: new Map(),
397
+ action: null,
398
+ clip: null,
399
+ fades: new Map(),
400
+ };
401
+ state.handle = Object.freeze({
402
+ entity: entityId,
403
+ node,
404
+ dispose: () => {
405
+ if (this.instances.get(entityId) === state)
406
+ this.remove(entityId);
407
+ },
408
+ });
409
+ try {
410
+ if (initializeAnimation && definition.animation)
411
+ this.playState(state, definition.animation.clip, definition.animation);
412
+ return state;
413
+ }
414
+ catch (error) {
415
+ this.releaseInstance(state);
416
+ throw error;
417
+ }
418
+ }
419
+ releaseInstance(state) {
420
+ state.mixer.stopAllAction();
421
+ state.mixer.uncacheRoot(state.root);
422
+ state.node.removeFromParent();
423
+ disposeInstanceSkeletons(state.root);
424
+ }
425
+ stageInstances(ids, definitions = this.definitions, initializeAnimation = true) {
426
+ const staged = new Map();
427
+ try {
428
+ for (const id of ids) {
429
+ const known = definitions.get(id);
430
+ if (!known)
431
+ throw new Error(`TC_MODEL_DEFINITION_MISSING: ${id}`);
432
+ if (!this.instances.has(id) ||
433
+ JSON.stringify(this.definitions.get(id)?.definition) !== JSON.stringify(known.definition))
434
+ staged.set(id, this.createInstance(id, known.definition, initializeAnimation));
435
+ }
436
+ return staged;
437
+ }
438
+ catch (error) {
439
+ for (const state of staged.values())
440
+ this.releaseInstance(state);
441
+ throw error;
442
+ }
443
+ }
444
+ commitInstances(ids, staged, parent, stagedAttached = false) {
445
+ for (const id of this.instances.keys())
446
+ if (!ids.has(id))
447
+ this.remove(id);
448
+ for (const [id, state] of staged) {
449
+ this.remove(id);
450
+ this.instances.set(id, state);
451
+ }
452
+ for (const id of ids) {
453
+ const known = this.knownDefinition(id);
454
+ if (parent)
455
+ known.parent = parent;
456
+ if ((parent || (staged.has(id) && !stagedAttached)) && known.parent)
457
+ known.parent.add(this.instances.get(id).node);
458
+ }
459
+ }
460
+ tick(dt) {
461
+ this.assertOpen();
462
+ if (!Number.isFinite(dt) || dt < 0)
463
+ throw new Error("TC_MODEL_INVALID_DT");
464
+ for (const state of this.instances.values()) {
465
+ for (const [name, fade] of state.fades) {
466
+ fade.elapsed = Math.min(fade.duration, fade.elapsed + dt);
467
+ state.actions.get(name).setEffectiveWeight(this.fadeWeight(fade));
468
+ }
469
+ state.mixer.update(dt);
470
+ for (const [name, fade] of state.fades) {
471
+ if (fade.elapsed < fade.duration)
472
+ continue;
473
+ state.fades.delete(name);
474
+ if (fade.to === 0) {
475
+ state.actions.get(name).stop();
476
+ state.actions.delete(name);
477
+ }
478
+ }
479
+ }
480
+ }
481
+ snapshot() {
482
+ this.assertOpen();
483
+ return {
484
+ format: "pelican.models",
485
+ version: 2,
486
+ animations: [...this.instances.values()]
487
+ .sort((a, b) => a.entity.localeCompare(b.entity))
488
+ .map((state) => ({
489
+ entity: state.entity,
490
+ asset: state.asset,
491
+ currentClip: state.clip,
492
+ actions: [...state.actions.entries()].map(([clip, action]) => ({
493
+ clip,
494
+ time: action.time,
495
+ playing: action.isRunning(),
496
+ speed: action.timeScale,
497
+ loop: action.loop === THREE.LoopRepeat,
498
+ weight: action.getEffectiveWeight(),
499
+ paused: action.paused,
500
+ fade: state.fades.has(clip)
501
+ ? { ...state.fades.get(clip) }
502
+ : null,
503
+ })),
504
+ })),
505
+ };
506
+ }
507
+ validateSnapshot(value, bindings) {
508
+ this.assertOpen();
509
+ const definitions = bindings ? this.candidateDefinitions(bindings) : this.definitions;
510
+ const invalid = () => new Error("TC_MODEL_INVALID_SNAPSHOT");
511
+ if (!value || typeof value !== "object")
512
+ throw invalid();
513
+ const data = value;
514
+ if (data.format !== "pelican.models" ||
515
+ data.version !== 2 ||
516
+ !Array.isArray(data.animations))
517
+ throw invalid();
518
+ const ids = new Set();
519
+ for (const item of data.animations) {
520
+ if (!item ||
521
+ typeof item.entity !== "string" ||
522
+ !item.entity ||
523
+ ids.has(item.entity))
524
+ throw invalid();
525
+ ids.add(item.entity);
526
+ const known = definitions.get(item.entity);
527
+ if (!known)
528
+ throw new Error(`TC_MODEL_INVALID_SNAPSHOT: TC_MODEL_DEFINITION_MISSING: ${item.entity}; register the model definition before restoring`);
529
+ if (item.asset !== known.definition.asset)
530
+ throw invalid();
531
+ const template = this.templates.get(item.asset);
532
+ if (!template)
533
+ throw invalid();
534
+ const clips = new Map(template.clips.map((clip) => [clip.name, clip]));
535
+ if (item.currentClip !== null &&
536
+ (typeof item.currentClip !== "string" || !clips.has(item.currentClip)))
537
+ throw invalid();
538
+ if (!Array.isArray(item.actions))
539
+ throw invalid();
540
+ const actionNames = new Set();
541
+ for (const action of item.actions) {
542
+ if (!action ||
543
+ typeof action.clip !== "string" ||
544
+ actionNames.has(action.clip))
545
+ throw invalid();
546
+ actionNames.add(action.clip);
547
+ if (!clips.has(action.clip))
548
+ throw invalid();
549
+ if (!Number.isFinite(action.time) ||
550
+ action.time < 0 ||
551
+ action.time > clips.get(action.clip).duration ||
552
+ !Number.isFinite(action.speed) ||
553
+ typeof action.playing !== "boolean" ||
554
+ typeof action.loop !== "boolean" ||
555
+ typeof action.paused !== "boolean" ||
556
+ action.playing !== (!action.paused && action.speed !== 0) ||
557
+ !Number.isFinite(action.weight) ||
558
+ action.weight < 0 ||
559
+ action.weight > 1)
560
+ throw invalid();
561
+ const fade = action.fade;
562
+ if (fade !== null) {
563
+ if (!fade ||
564
+ typeof fade !== "object" ||
565
+ !Number.isFinite(fade.from) ||
566
+ fade.from < 0 ||
567
+ fade.from > 1 ||
568
+ (fade.to !== 0 && fade.to !== 1) ||
569
+ !Number.isFinite(fade.duration) ||
570
+ fade.duration <= 0 ||
571
+ !Number.isFinite(fade.elapsed) ||
572
+ fade.elapsed < 0 ||
573
+ fade.elapsed >= fade.duration ||
574
+ Math.abs(this.fadeWeight(fade) - action.weight) > 1e-7 ||
575
+ fade.to !== (action.clip === item.currentClip ? 1 : 0))
576
+ throw invalid();
577
+ }
578
+ if (action.clip !== item.currentClip && (!fade || fade.to !== 0))
579
+ throw invalid();
580
+ }
581
+ if (item.currentClip !== null && !actionNames.has(item.currentClip))
582
+ throw invalid();
583
+ if (item.currentClip === null && actionNames.size)
584
+ throw invalid();
585
+ }
586
+ return structuredClone(data);
587
+ }
588
+ restore(value, bindings) {
589
+ const snapshot = this.validateSnapshot(value, bindings);
590
+ const ids = new Set(snapshot.animations.map((item) => item.entity));
591
+ const definitions = bindings ? this.candidateDefinitions(bindings) : this.definitions;
592
+ const staged = this.stageInstances(ids, definitions, false);
593
+ let previous = new Map();
594
+ const poses = [];
595
+ const touched = [];
596
+ let committed = false;
597
+ let commitStarted = false;
598
+ try {
599
+ previous = new Map(this.snapshot().animations.map((item) => [item.entity, item]));
600
+ // Staged tracks can target materials shared with active instances.
601
+ const affected = snapshot.animations.map((item) => staged.get(item.entity) ?? this.instances.get(item.entity));
602
+ for (const state of affected) {
603
+ const tracks = new Map();
604
+ for (const clip of state.clips.values())
605
+ for (const track of clip.tracks)
606
+ tracks.set(track.name, track);
607
+ const values = [...tracks.values()].map((track) => {
608
+ const binding = new THREE.PropertyBinding(state.root, track.name);
609
+ const captured = new Array(track.getValueSize());
610
+ binding.getValue(captured, 0);
611
+ return { binding, values: captured };
612
+ });
613
+ poses.push(...values);
614
+ }
615
+ for (const item of snapshot.animations)
616
+ if (staged.has(item.entity))
617
+ this.restoreAnimation(staged.get(item.entity), item);
618
+ for (const [id, state] of staged)
619
+ definitions.get(id).parent?.add(state.node);
620
+ for (const item of snapshot.animations) {
621
+ if (staged.has(item.entity))
622
+ continue;
623
+ const state = this.instances.get(item.entity);
624
+ touched.push(state);
625
+ this.restoreAnimation(state, item);
626
+ }
627
+ if (bindings)
628
+ for (const [id, known] of definitions)
629
+ this.definitions.set(id, known);
630
+ commitStarted = true;
631
+ this.commitInstances(ids, staged, undefined, true);
632
+ committed = true;
633
+ }
634
+ catch (error) {
635
+ const failures = [];
636
+ for (const state of touched) {
637
+ try {
638
+ this.restoreAnimation(state, previous.get(state.entity));
639
+ }
640
+ catch (rollbackError) {
641
+ failures.push(rollbackError);
642
+ }
643
+ }
644
+ // Stopping staged mixers can write shared values; restore bindings afterwards.
645
+ for (const [id, candidate] of staged) {
646
+ if (this.instances.get(id) !== candidate) {
647
+ try {
648
+ this.releaseInstance(candidate);
649
+ }
650
+ catch (rollbackError) {
651
+ failures.push(rollbackError);
652
+ }
653
+ }
654
+ }
655
+ staged.clear();
656
+ for (const pose of poses) {
657
+ try {
658
+ pose.binding.setValue(pose.values, 0);
659
+ }
660
+ catch (rollbackError) {
661
+ failures.push(rollbackError);
662
+ }
663
+ }
664
+ if (commitStarted)
665
+ throw new AggregateError([error, ...failures], "TC_MODEL_RESTORE_COMMIT_FAILED: commit may be partial; model state is not guaranteed atomic");
666
+ if (failures.length)
667
+ throw new AggregateError([error, ...failures], "TC_MODEL_RESTORE_ROLLBACK_FAILED: original restore and rollback both failed");
668
+ throw error;
669
+ }
670
+ finally {
671
+ if (!committed)
672
+ for (const [id, candidate] of staged)
673
+ if (this.instances.get(id) !== candidate)
674
+ this.releaseInstance(candidate);
675
+ }
676
+ }
677
+ restoreAnimation(state, item) {
678
+ for (const action of state.actions.values())
679
+ action.stop();
680
+ state.actions.clear();
681
+ state.fades.clear();
682
+ state.action = null;
683
+ state.clip = null;
684
+ for (const saved of item.actions) {
685
+ const action = state.mixer.clipAction(state.clips.get(saved.clip));
686
+ action.reset();
687
+ action.setLoop(saved.loop ? THREE.LoopRepeat : THREE.LoopOnce, saved.loop ? Infinity : 1);
688
+ action.clampWhenFinished = !saved.loop;
689
+ action.timeScale = saved.speed;
690
+ action.enabled = true;
691
+ action.setEffectiveWeight(saved.weight);
692
+ action.play();
693
+ action.time = saved.time;
694
+ action.paused = saved.paused;
695
+ if (saved.fade)
696
+ state.fades.set(saved.clip, { ...saved.fade });
697
+ state.actions.set(saved.clip, action);
698
+ }
699
+ state.action =
700
+ item.currentClip === null ? null : state.actions.get(item.currentClip);
701
+ state.clip = item.currentClip;
702
+ state.mixer.update(0);
703
+ }
704
+ inspect() {
705
+ const geometries = new Set();
706
+ const materials = new Set();
707
+ const textures = new Set();
708
+ const skeletons = new Set();
709
+ for (const template of this.templates.values()) {
710
+ for (const value of template.resources.geometries)
711
+ geometries.add(value);
712
+ for (const value of template.resources.materials)
713
+ materials.add(value);
714
+ for (const value of template.resources.textures)
715
+ textures.add(value);
716
+ for (const value of template.resources.skeletons)
717
+ skeletons.add(value);
718
+ }
719
+ const templates = [...this.templates.values()].map((template) => {
720
+ return {
721
+ id: template.id,
722
+ animations: template.clips.map((clip) => clip.name),
723
+ meshes: template.meshes,
724
+ primitives: template.primitives,
725
+ };
726
+ });
727
+ return {
728
+ disposed: this.closed,
729
+ templates,
730
+ instances: this.instances.size,
731
+ definitions: this.definitions.size,
732
+ playing: [...this.instances.values()].filter((state) => state.action?.isRunning()).length,
733
+ owned: {
734
+ geometries: geometries.size,
735
+ materials: materials.size,
736
+ textures: textures.size,
737
+ skeletons: skeletons.size + this.instanceSkeletonCount(),
738
+ },
739
+ };
740
+ }
741
+ dispose() {
742
+ if (this.disposing)
743
+ return this.disposing;
744
+ this.closed = true;
745
+ const pending = [...this.loads];
746
+ for (const item of pending)
747
+ item.controller.abort();
748
+ for (const entity of [...this.instances.keys()])
749
+ this.remove(entity);
750
+ this.disposing = (async () => {
751
+ await Promise.allSettled(pending.map((item) => item.promise));
752
+ for (const template of this.templates.values())
753
+ template.resources.dispose();
754
+ this.templates.clear();
755
+ this.urls.clear();
756
+ this.definitions.clear();
757
+ this.pending.clear();
758
+ })();
759
+ return this.disposing;
760
+ }
761
+ instance(entity) {
762
+ this.assertOpen();
763
+ const value = this.instances.get(entity);
764
+ if (!value)
765
+ throw new Error(`TC_MODEL_ENTITY_MISSING: ${entity}`);
766
+ return value;
767
+ }
768
+ play(entity, clipName, options = {}) {
769
+ this.assertOpen();
770
+ this.playState(this.instance(entity), clipName, options);
771
+ }
772
+ playState(state, clipName, options) {
773
+ const clip = state.clips.get(clipName);
774
+ if (!clip)
775
+ throw new Error(`TC_MODEL_CLIP_MISSING: ${clipName}; available: ${[...state.clips.keys()].join(", ")}`);
776
+ const fade = options.fade ?? 0;
777
+ const speed = options.speed ?? 1;
778
+ const loop = options.loop ?? true;
779
+ if (!Number.isFinite(fade) ||
780
+ fade < 0 ||
781
+ !Number.isFinite(speed) ||
782
+ typeof loop !== "boolean")
783
+ throw new Error("TC_MODEL_INVALID_ANIMATION_OPTIONS");
784
+ const action = state.mixer.clipAction(clip);
785
+ if (fade > 0) {
786
+ for (const [name, old] of state.actions) {
787
+ if (old === action)
788
+ continue;
789
+ state.fades.set(name, {
790
+ from: old.getEffectiveWeight(),
791
+ to: 0,
792
+ duration: fade,
793
+ elapsed: 0,
794
+ });
795
+ }
796
+ }
797
+ else {
798
+ for (const old of state.actions.values())
799
+ old.stop();
800
+ state.actions.clear();
801
+ state.fades.clear();
802
+ }
803
+ action.reset();
804
+ action.setEffectiveTimeScale(speed);
805
+ action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, loop ? Infinity : 1);
806
+ action.clampWhenFinished = !loop;
807
+ action.setEffectiveWeight(fade > 0 ? 0 : 1).play();
808
+ if (fade > 0)
809
+ state.fades.set(clipName, { from: 0, to: 1, duration: fade, elapsed: 0 });
810
+ else
811
+ state.fades.delete(clipName);
812
+ state.actions.set(clipName, action);
813
+ state.action = action;
814
+ state.clip = clipName;
815
+ state.mixer.update(0);
816
+ }
817
+ fadeWeight(fade) {
818
+ return fade.from + (fade.to - fade.from) * (fade.elapsed / fade.duration);
819
+ }
820
+ async loadTemplate(id, signal) {
821
+ if (signal?.aborted)
822
+ throw new Error(`TC_MODEL_CANCELLED: ${id}`);
823
+ const ready = this.templates.get(id);
824
+ if (ready)
825
+ return ready;
826
+ let pending = this.pending.get(id);
827
+ if (!pending) {
828
+ const controller = new AbortController();
829
+ const entry = {
830
+ controller,
831
+ callers: 0,
832
+ settled: false,
833
+ promise: undefined,
834
+ };
835
+ entry.promise = Promise.resolve().then(async () => {
836
+ const resources = new ModelResources();
837
+ // Observers always fulfill, preventing orphan rejections while draining a failed parse.
838
+ const decoding = new Set();
839
+ const track = (promise) => {
840
+ const observed = promise.then((value) => {
841
+ resources.collect(value);
842
+ }, () => { });
843
+ decoding.add(observed);
844
+ void observed.then(() => decoding.delete(observed));
845
+ return promise;
846
+ };
847
+ try {
848
+ const url = this.resolveURL(this.urls.get(id));
849
+ if (controller.signal.aborted || this.closed)
850
+ throw new Error("cancelled");
851
+ const response = await this.fetcher(url, {
852
+ signal: controller.signal,
853
+ });
854
+ if (!response.ok)
855
+ throw new Error(`HTTP ${response.status}`);
856
+ const bytes = await response.arrayBuffer();
857
+ if (controller.signal.aborted || this.closed)
858
+ throw new Error("cancelled");
859
+ const manager = new THREE.LoadingManager(undefined, undefined, (url) => {
860
+ // GLTFLoader revokes successful image blob URLs, but not failed decodes.
861
+ if (url.startsWith("blob:"))
862
+ URL.revokeObjectURL(url);
863
+ });
864
+ const loader = new GLTFLoader(manager);
865
+ loader.register((parser) => {
866
+ const specularColorImages = new Set();
867
+ for (const material of parser.json.materials ?? []) {
868
+ const index = material.extensions?.KHR_materials_specular
869
+ ?.specularColorTexture?.index;
870
+ const source = index === undefined
871
+ ? undefined
872
+ : parser.json.textures?.[index]?.source;
873
+ if (source !== undefined)
874
+ specularColorImages.add(source);
875
+ }
876
+ const dependency = parser.getDependency.bind(parser);
877
+ parser.getDependency = (type, index) => track(dependency(type, index).then((value) => {
878
+ if (type === "texture" && !value)
879
+ throw new Error(`TC_MODEL_TEXTURE_FAILED: ${index}`);
880
+ return value;
881
+ }));
882
+ // Track each primitive separately: a failed sibling must not hide decoded geometry.
883
+ const geometries = parser.loadGeometries.bind(parser);
884
+ parser.loadGeometries = (primitives) => Promise.all(primitives.map((primitive) => track(geometries([primitive])))).then((groups) => groups.flat());
885
+ const image = parser.loadImageSource.bind(parser);
886
+ parser.loadImageSource = (index, loader) => track((async () => {
887
+ const texture = await track(image(index, loader));
888
+ const definition = parser.json.images[index];
889
+ const uri = definition.uri;
890
+ const mime = definition.mimeType ??
891
+ /^data:([^;,]+)/i.exec(uri ?? "")?.[1] ??
892
+ "";
893
+ // JPEG remains on the browser decoder path. Embedded PNG bytes are
894
+ // associated with Source, which GLTFLoader shares across clones.
895
+ if (specularColorImages.has(index) &&
896
+ mime.toLowerCase() === "image/png") {
897
+ let data;
898
+ if (definition.bufferView !== undefined) {
899
+ data = await parser.getDependency("bufferView", definition.bufferView);
900
+ }
901
+ else if (uri?.startsWith("data:")) {
902
+ data = await (await fetch(uri)).arrayBuffer();
903
+ }
904
+ else {
905
+ throw new Error(`TC_MODEL_IMAGE_SOURCE_UNSUPPORTED: ${index}`);
906
+ }
907
+ associateEmbeddedImage(texture, new Uint8Array(data), mime);
908
+ }
909
+ return texture;
910
+ })());
911
+ const finalMaterial = parser.assignFinalMaterial.bind(parser);
912
+ parser.assignFinalMaterial = (mesh) => {
913
+ finalMaterial(mesh);
914
+ resources.collect(mesh);
915
+ };
916
+ return {
917
+ name: "PELICAN_resource_ownership",
918
+ beforeRoot: () => {
919
+ // Subresource fetches bypass options.fetch and its abort signal in GLTFLoader.
920
+ for (const item of [
921
+ ...(parser.json.buffers ?? []),
922
+ ...(parser.json.images ?? []),
923
+ ])
924
+ if (item.uri && !item.uri.startsWith("data:"))
925
+ throw new Error(`TC_MODEL_EXTERNAL_URI_UNSUPPORTED: ${item.uri}; embed resources in the GLB`);
926
+ for (const name of parser.json.extensionsRequired ?? [])
927
+ if (!Object.hasOwn(parser.plugins, name) &&
928
+ !Object.hasOwn(parser.extensions, name))
929
+ throw new Error(`TC_MODEL_EXTENSION_UNSUPPORTED: ${name}`);
930
+ // GLTFLoader skips node-less pointer animation channels, including
931
+ // optional ones. Reject used channels rather than silently omit them.
932
+ for (const [animationIndex, animation] of (parser.json.animations ?? []).entries())
933
+ for (const [channelIndex, channel] of (animation.channels ?? []).entries())
934
+ if (channel.target?.path === "pointer" || Object.hasOwn(channel.target?.extensions ?? {}, "KHR_animation_pointer"))
935
+ throw new Error(`TC_MODEL_EXTENSION_UNSUPPORTED: KHR_animation_pointer animation ${animationIndex} channel ${channelIndex}; pointer animation is not supported`);
936
+ return null;
937
+ },
938
+ };
939
+ });
940
+ const gltf = await loader.parseAsync(bytes, new URL(".", url).href);
941
+ resources.collect(gltf.scenes);
942
+ const template = this.makeTemplate(id, gltf, resources);
943
+ if (controller.signal.aborted || this.closed)
944
+ throw new Error("cancelled");
945
+ this.templates.set(id, template);
946
+ return template;
947
+ }
948
+ catch (error) {
949
+ while (decoding.size)
950
+ await Promise.all([...decoding]);
951
+ resources.dispose();
952
+ if (controller.signal.aborted || this.closed)
953
+ throw new Error(`TC_MODEL_CANCELLED: ${id}`);
954
+ throw new Error(`TC_MODEL_LOAD_FAILED: ${id}: ${error instanceof Error ? error.message : "decode failed"}`);
955
+ }
956
+ finally {
957
+ entry.settled = true;
958
+ this.loads.delete(entry);
959
+ if (this.pending.get(id) === entry)
960
+ this.pending.delete(id);
961
+ }
962
+ });
963
+ pending = entry;
964
+ this.pending.set(id, entry);
965
+ this.loads.add(entry);
966
+ }
967
+ const shared = pending;
968
+ shared.callers++;
969
+ try {
970
+ return await new Promise((resolve, reject) => {
971
+ const cleanup = () => {
972
+ signal?.removeEventListener("abort", abort);
973
+ shared.controller.signal.removeEventListener("abort", abort);
974
+ };
975
+ const abort = () => {
976
+ cleanup();
977
+ reject(new Error(`TC_MODEL_CANCELLED: ${id}`));
978
+ };
979
+ signal?.addEventListener("abort", abort, { once: true });
980
+ shared.controller.signal.addEventListener("abort", abort, {
981
+ once: true,
982
+ });
983
+ shared.promise.then((value) => {
984
+ cleanup();
985
+ resolve(value);
986
+ }, (error) => {
987
+ cleanup();
988
+ reject(error);
989
+ });
990
+ if (signal?.aborted || shared.controller.signal.aborted)
991
+ abort();
992
+ });
993
+ }
994
+ finally {
995
+ shared.callers--;
996
+ if (shared.callers === 0 && !shared.settled) {
997
+ shared.controller.abort();
998
+ if (this.pending.get(id) === shared)
999
+ this.pending.delete(id);
1000
+ }
1001
+ }
1002
+ }
1003
+ makeTemplate(id, gltf, resources) {
1004
+ const names = new Set();
1005
+ for (const clip of gltf.animations) {
1006
+ if (!clip.name.trim() || names.has(clip.name))
1007
+ throw new Error(`TC_MODEL_CLIP_DUPLICATE: ${clip.name}; give every animation a unique name`);
1008
+ if (!Number.isFinite(clip.duration) || clip.duration <= 0)
1009
+ throw new Error(`TC_MODEL_INVALID_CLIP_DURATION: ${clip.name}; animation duration must be positive`);
1010
+ names.add(clip.name);
1011
+ }
1012
+ let meshes = 0, primitives = 0;
1013
+ gltf.scene.traverse((object) => {
1014
+ if (object.isMesh) {
1015
+ meshes++;
1016
+ const mesh = object;
1017
+ primitives += Array.isArray(mesh.material) ? mesh.material.length : 1;
1018
+ }
1019
+ });
1020
+ return {
1021
+ id,
1022
+ scene: gltf.scene,
1023
+ resources,
1024
+ clips: gltf.animations,
1025
+ meshes,
1026
+ primitives,
1027
+ };
1028
+ }
1029
+ instanceSkeletonCount() {
1030
+ const values = new Set();
1031
+ for (const state of this.instances.values())
1032
+ state.root.traverse((object) => {
1033
+ if (object.isSkinnedMesh)
1034
+ values.add(object.skeleton);
1035
+ });
1036
+ return values.size;
1037
+ }
1038
+ resolveURL(value) {
1039
+ try {
1040
+ return new URL(value, this.options.baseURL ?? import.meta.url).href;
1041
+ }
1042
+ catch {
1043
+ throw new Error("TC_MODEL_INVALID_URL");
1044
+ }
1045
+ }
1046
+ assertOpen() {
1047
+ if (this.closed)
1048
+ throw new Error("TC_MODEL_LIBRARY_DISPOSED");
1049
+ }
1050
+ }
1051
+ export function createModelLibrary(assets, options = {}) {
1052
+ return new ModelLibraryImpl(assets, options);
1053
+ }