@needle-tools/gltf-build-pipeline 2.13.0 → 2.13.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 (103) hide show
  1. package/CHANGELOG.md +520 -517
  2. package/README.md +38 -38
  3. package/dist/cache/cache.d.ts +55 -55
  4. package/dist/cache/cache.js +326 -326
  5. package/dist/cache/index.d.ts +1 -1
  6. package/dist/cache/index.js +1 -1
  7. package/dist/cli/index.d.ts +2 -2
  8. package/dist/cli/index.js +198 -198
  9. package/dist/config/index.d.ts +53 -53
  10. package/dist/config/index.js +152 -152
  11. package/dist/constants.d.ts +7 -7
  12. package/dist/constants.js +7 -7
  13. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -24
  14. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -54
  15. package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -1
  16. package/dist/extensions/NEEDLE_compression_texture/index.js +1 -1
  17. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +27 -27
  18. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +88 -88
  19. package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -1
  20. package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -1
  21. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +27 -27
  22. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +980 -980
  23. package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -3
  24. package/dist/extensions/NEEDLE_opaque/index.js +30 -30
  25. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +48 -48
  26. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +69 -69
  27. package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -1
  28. package/dist/extensions/NEEDLE_progressive/index.js +1 -1
  29. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +23 -23
  30. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.js +41 -41
  31. package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +25 -25
  32. package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +38 -38
  33. package/dist/extensions/index.d.ts +7 -7
  34. package/dist/extensions/index.js +17 -17
  35. package/dist/extensions/utils.d.ts +6 -6
  36. package/dist/extensions/utils.js +72 -72
  37. package/dist/functions/weld.3.d.ts +76 -76
  38. package/dist/functions/weld.3.js +414 -414
  39. package/dist/index.d.ts +8 -8
  40. package/dist/index.js +8 -8
  41. package/dist/scripts/index.d.ts +2 -2
  42. package/dist/scripts/index.js +2 -2
  43. package/dist/scripts/pack-gltf.d.ts +15 -15
  44. package/dist/scripts/pack-gltf.js +135 -133
  45. package/dist/scripts/stats.d.ts +2 -2
  46. package/dist/scripts/stats.js +19 -19
  47. package/dist/transforms/index.d.ts +9 -9
  48. package/dist/transforms/index.js +9 -9
  49. package/dist/transforms/needle_animation_transform.d.ts +2 -2
  50. package/dist/transforms/needle_animation_transform.js +5 -5
  51. package/dist/transforms/needle_asset.d.ts +3 -3
  52. package/dist/transforms/needle_asset.js +50 -50
  53. package/dist/transforms/needle_common.d.ts +17 -17
  54. package/dist/transforms/needle_common.js +1 -1
  55. package/dist/transforms/needle_mesh_transform.d.ts +15 -15
  56. package/dist/transforms/needle_mesh_transform.js +223 -223
  57. package/dist/transforms/needle_progressive.d.ts +45 -45
  58. package/dist/transforms/needle_progressive.js +1043 -1043
  59. package/dist/transforms/needle_texture_transform.d.ts +13 -13
  60. package/dist/transforms/needle_texture_transform.js +257 -257
  61. package/dist/transforms/needle_toktx.d.ts +26 -26
  62. package/dist/transforms/needle_toktx.js +243 -243
  63. package/dist/transforms/needle_webp.d.ts +15 -15
  64. package/dist/transforms/needle_webp.js +53 -53
  65. package/dist/transforms/toktx.d.ts +52 -52
  66. package/dist/transforms/toktx.js +221 -221
  67. package/dist/transforms/toktx_types.d.ts +78 -78
  68. package/dist/transforms/toktx_types.js +62 -62
  69. package/dist/transforms/util.d.ts +21 -21
  70. package/dist/transforms/util.js +90 -90
  71. package/dist/utils/compression_utils.d.ts +2 -2
  72. package/dist/utils/compression_utils.js +6 -6
  73. package/dist/utils/fileutils.d.ts +19 -19
  74. package/dist/utils/fileutils.js +177 -177
  75. package/dist/utils/guid.d.ts +1 -1
  76. package/dist/utils/guid.js +5 -5
  77. package/dist/utils/index.d.ts +10 -10
  78. package/dist/utils/index.js +10 -10
  79. package/dist/utils/merge.d.ts +12 -12
  80. package/dist/utils/merge.js +31 -31
  81. package/dist/utils/mesh.d.ts +13 -13
  82. package/dist/utils/mesh.js +124 -124
  83. package/dist/utils/nodeio.d.ts +6 -6
  84. package/dist/utils/nodeio.js +86 -86
  85. package/dist/utils/stats.d.ts +70 -70
  86. package/dist/utils/stats.js +258 -258
  87. package/dist/utils/texture.d.ts +5 -5
  88. package/dist/utils/texture.js +14 -14
  89. package/dist/utils/validate.d.ts +5 -5
  90. package/dist/utils/validate.js +17 -17
  91. package/dist/utils/version.d.ts +1 -1
  92. package/dist/utils/version.gen.d.ts +1 -1
  93. package/dist/utils/version.gen.js +1 -1
  94. package/dist/utils/version.js +4 -4
  95. package/package.json +79 -79
  96. package/scripts/clear-caches.mjs +3 -3
  97. package/scripts/defaults.mjs +11 -11
  98. package/scripts/fileutils.mjs +5 -5
  99. package/scripts/index.mjs +41 -41
  100. package/scripts/limit-caches.mjs +3 -3
  101. package/scripts/make-progressive.mjs +16 -16
  102. package/scripts/pack-gltf.mjs +106 -106
  103. package/tsconfig.json +19 -19
@@ -1,980 +1,980 @@
1
- import { Extension, ExtensionProperty, PropertyType, Node, TextureChannel } from '@gltf-transform/core';
2
- import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
3
- import { isLightmap } from '../../utils/texture.js';
4
- const ALL_PROPERTY_TYPES = [];
5
- for (const key in PropertyType) {
6
- ALL_PROPERTY_TYPES.push(PropertyType[key]);
7
- }
8
- export let currentGenerator = "";
9
- // an opaque extension can be used to pass data through the gltf transform process
10
- // it should also be able to re-write/fix json pointers after the transformation which might have shuffled them around
11
- export function createOpaqueExtension(name, types, opts) {
12
- if (!Array.isArray(types))
13
- types = [types];
14
- const EXTENSION_NAME = name;
15
- const ExtensionPropertyTypes = ALL_PROPERTY_TYPES;
16
- const debugLog = opts?.debug ?? false;
17
- class NEEDLE_opaque extends Extension {
18
- extensionName = EXTENSION_NAME;
19
- static EXTENSION_NAME = EXTENSION_NAME;
20
- prewriteTypes = [PropertyType.MATERIAL];
21
- // private roots: Array<OpaqueProperty> = [];
22
- jsonPointerHandler;
23
- read(context) {
24
- if (opts?.breakpoint === true)
25
- debugger;
26
- currentGenerator = context.jsonDoc.json.asset?.generator;
27
- if (debugLog)
28
- console.log("READ " + EXTENSION_NAME);
29
- this.jsonPointerHandler = new JsonPointerHandler(this.document, EXTENSION_NAME);
30
- this.jsonPointerHandler.debug = debugLog;
31
- this.jsonPointerHandler.breakPoint = opts?.breakpoint;
32
- for (const type of types) {
33
- switch (type) {
34
- case PropertyType.ACCESSOR:
35
- this.onRead(context, "accessors", "accessorIndexMap");
36
- break;
37
- case PropertyType.ANIMATION:
38
- this.onRead(context, "animations", "animationIndexMap");
39
- break;
40
- case PropertyType.ANIMATION_CHANNEL:
41
- if (EXTENSION_NAME === "KHR_animation_pointer") {
42
- context.jsonDoc.json.animations?.forEach((animDef, animIndex) => {
43
- const anim = context.animations[animIndex];
44
- const channels = anim.listChannels();
45
- animDef.channels?.forEach((channel, channelIndex) => {
46
- const jsonTarget = channel.target;
47
- const khr_animation = jsonTarget.extensions && jsonTarget.extensions[EXTENSION_NAME];
48
- if (!khr_animation)
49
- return;
50
- const pointer = khr_animation.pointer;
51
- if (pointer) {
52
- const sections = pointer.split("/");
53
- const nodeIndex = parseInt(sections[2]);
54
- const targetName = sections[3];
55
- if (targetName === "scale" || targetName === "translation" || targetName === "rotation") {
56
- channels[channelIndex].setTargetNode(context.nodes[nodeIndex]);
57
- channels[channelIndex].setTargetPath(targetName);
58
- }
59
- }
60
- const existing = anim.getExtension(EXTENSION_NAME);
61
- if (debugLog)
62
- console.log(">>>", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
63
- if (existing) {
64
- assignChannels(existing);
65
- return;
66
- }
67
- const prop = this.createExtension();
68
- // there seems no way (yet?) to add extensions to channel.targets (?)
69
- // https://github.com/donmccurdy/glTF-Transform/discussions/613
70
- // so we add it to an object by keys to the animation itself and read it back in WRITE
71
- const khr_animation_data = {};
72
- khr_animation_data[channelIndex] = khr_animation;
73
- anim.setExtension(EXTENSION_NAME, prop);
74
- assignChannels(prop);
75
- function assignChannels(prop) {
76
- prop.add(ctx => {
77
- const animations = ctx.jsonDoc.json.animations;
78
- if (!animations)
79
- return;
80
- const animDef = animations[animIndex];
81
- const channel = animDef?.channels[channelIndex];
82
- if (channel) {
83
- if (debugLog)
84
- console.log("<<<", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
85
- channel.target = jsonTarget;
86
- }
87
- else if (debugLog)
88
- console.log("WARN: ", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, "not found");
89
- });
90
- }
91
- ;
92
- });
93
- });
94
- }
95
- break;
96
- case PropertyType.ANIMATION_SAMPLER:
97
- break;
98
- case PropertyType.BUFFER:
99
- this.onRead(context, "buffers", "bufferIndexMap");
100
- break;
101
- case PropertyType.CAMERA:
102
- this.onRead(context, "cameras", "cameraIndexMap");
103
- break;
104
- case PropertyType.MATERIAL:
105
- this.onRead(context, "materials", "materialIndexMap");
106
- break;
107
- case PropertyType.TEXTURE:
108
- context.jsonDoc.json.textures?.forEach((textureDefinition, index) => {
109
- if (!textureDefinition.extensions)
110
- return;
111
- const ext = textureDefinition.extensions[EXTENSION_NAME];
112
- if (!ext)
113
- return;
114
- // TODO: textures and textureDefinition are not in the same order?
115
- // see https://github.com/donmccurdy/glTF-Transform/blob/0e99c426b8ef41536228fe860ab8329be5d1d50f/packages/core/src/io/reader.ts#L132
116
- // textures are in image order
117
- // TODO: we need to use the image index to find the texture
118
- let texture = getTexture(context, textureDefinition);
119
- if (!texture) {
120
- console.log("WARN: could potentially not find gltf transform texture for " + index);
121
- texture = context.textures[index];
122
- }
123
- const prop = this.createExtension();
124
- this.jsonPointerHandler.read(context, texture, ext, prop);
125
- texture.setExtension(EXTENSION_NAME, prop);
126
- const handler = new TextureHandler(this.document, textureDefinition, EXTENSION_NAME, ext, debugLog);
127
- prop.add(handler);
128
- });
129
- break;
130
- case PropertyType.NODE:
131
- this.onRead(context, "nodes", "nodeIndexMap");
132
- break;
133
- case PropertyType.MESH:
134
- this.onRead(context, "meshes", "meshIndexMap");
135
- break;
136
- case PropertyType.PRIMITIVE:
137
- context.jsonDoc.json.meshes?.forEach((meshDefinition, meshIndex) => {
138
- const mesh = context.meshes[meshIndex];
139
- const primitives = mesh.listPrimitives();
140
- meshDefinition.primitives?.forEach((primitiveDefinition, primitiveIndex) => {
141
- if (!primitiveDefinition.extensions)
142
- return;
143
- const ext = primitiveDefinition.extensions[EXTENSION_NAME];
144
- if (!ext)
145
- return;
146
- const primitive = primitives[primitiveIndex];
147
- const prop = this.createExtension();
148
- this.jsonPointerHandler.read(context, primitive, ext, prop);
149
- primitive.setExtension(EXTENSION_NAME, prop);
150
- // this is invoked on write
151
- prop.add(ctx => {
152
- const newMeshIndex = ctx.meshIndexMap.get(mesh);
153
- const newMesh = ctx.jsonDoc.json.meshes[newMeshIndex];
154
- const newPrimitive = newMesh.primitives[primitiveIndex];
155
- newPrimitive.extensions = newPrimitive.extensions || {};
156
- newPrimitive.extensions[EXTENSION_NAME] = ext;
157
- });
158
- });
159
- });
160
- break;
161
- case PropertyType.ROOT:
162
- const sourceArray = context.jsonDoc.json.extensions;
163
- if (!sourceArray)
164
- continue;
165
- const ext = sourceArray[EXTENSION_NAME];
166
- if (ext) {
167
- const root = this.document.getRoot();
168
- const prop = this.createExtension();
169
- this.jsonPointerHandler.read(context, root, ext, prop);
170
- root.setExtension(EXTENSION_NAME, prop);
171
- this.lightmapWorkaround(context, prop, this.document.getLogger());
172
- prop.add(ctx => {
173
- ctx.jsonDoc.json.extensions = ctx.jsonDoc.json.extensions || {};
174
- ctx.jsonDoc.json.extensions[EXTENSION_NAME] = ext;
175
- });
176
- }
177
- break;
178
- }
179
- }
180
- return this;
181
- }
182
- // this is temporarely here to prevent compression on textures named Lightmap
183
- // until we have better control over the compression process
184
- lightmapWorkaround(context, prop, logger) {
185
- const textureDefinitions = context.jsonDoc.json.textures;
186
- if (!textureDefinitions)
187
- return;
188
- const textures = this.document.getRoot().listTextures();
189
- for (let i = 0; i < textures.length; i++) {
190
- const tex = textures[i];
191
- const texDef = textureDefinitions[i];
192
- const name = texDef?.name;
193
- if (isLightmap(tex) && tex.getMimeType() !== "image/exr") {
194
- prop.setReference("lightmapTexture", tex, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
195
- logger.info(`Found lightmap texture ${name}: ${tex.getMimeType()} in ${EXTENSION_NAME}`);
196
- }
197
- }
198
- }
199
- createExtension(name = '') {
200
- return new OpaqueProperty(this.document.getGraph(), name);
201
- ;
202
- }
203
- onRead(context, gltfField, map) {
204
- if (gltfField === undefined)
205
- return;
206
- try {
207
- const sourceArray = context.jsonDoc.json[gltfField];
208
- const target = context[gltfField];
209
- sourceArray?.forEach((def, index) => {
210
- if (!def.extensions || !def.extensions[EXTENSION_NAME])
211
- return;
212
- const ext = def.extensions[EXTENSION_NAME];
213
- if (debugLog)
214
- console.log("READ", index, ext);
215
- const prop = this.createExtension();
216
- // attach the property
217
- target[index].setExtension(EXTENSION_NAME, prop);
218
- // capture all the json pointers in the extension
219
- this.jsonPointerHandler.read(context, target[index], ext, prop);
220
- // make sure we write our extension back to the gltf
221
- prop.add(new OpaqueWriter(map, gltfField, ext));
222
- });
223
- }
224
- catch (error) {
225
- console.error("ERR: " + error);
226
- }
227
- }
228
- prewrite(context, _propertyType) {
229
- // Attempt resolve textures here so that the json.textures is already populated for cases where a texture isn't used by any material
230
- // this is e.g. the case for textures that are just referenced by a component but not used by any texture
231
- // https://linear.app/needle/issue/NE-4928
232
- this.jsonPointerHandler.resolveAndWrite(context, "prewrite");
233
- return this;
234
- }
235
- write(context) {
236
- if (opts?.breakpoint === true)
237
- debugger;
238
- if (opts?.write === false) {
239
- return this;
240
- }
241
- if (debugLog)
242
- console.log("WRITE " + EXTENSION_NAME);
243
- this.jsonPointerHandler.resolveAndWrite(context, "write");
244
- const root = this.document.getRoot();
245
- // for (const rootExt of this.roots) {
246
- // rootExt.onWrite(context, root);
247
- // }
248
- for (const type of types) {
249
- switch (type) {
250
- case PropertyType.ACCESSOR:
251
- this.onWrite(context, () => root.listAccessors());
252
- break;
253
- case PropertyType.ANIMATION:
254
- this.onWrite(context, () => root.listAnimations());
255
- break;
256
- case PropertyType.ANIMATION_CHANNEL:
257
- break;
258
- case PropertyType.ANIMATION_SAMPLER:
259
- break;
260
- case PropertyType.BUFFER:
261
- this.onWrite(context, () => root.listBuffers());
262
- break;
263
- case PropertyType.CAMERA:
264
- this.onWrite(context, () => root.listCameras());
265
- break;
266
- case PropertyType.MATERIAL:
267
- this.onWrite(context, () => root.listMaterials());
268
- break;
269
- case PropertyType.TEXTURE:
270
- this.onWrite(context, () => root.listTextures());
271
- break;
272
- case PropertyType.NODE:
273
- this.onWrite(context, () => root.listNodes());
274
- break;
275
- case PropertyType.MESH:
276
- this.onWrite(context, () => root.listMeshes());
277
- break;
278
- case PropertyType.PRIMITIVE:
279
- const allPrimitives = root.listMeshes().flatMap(mesh => mesh.listPrimitives());
280
- for (const primitive of allPrimitives) {
281
- const ext = primitive.getExtension(EXTENSION_NAME);
282
- if (ext instanceof OpaqueProperty) {
283
- ext.onWrite(context, primitive);
284
- }
285
- }
286
- break;
287
- case PropertyType.ROOT:
288
- const prop = root.getExtension(EXTENSION_NAME);
289
- if (prop instanceof OpaqueProperty) {
290
- prop.onWrite(context, root);
291
- }
292
- else if (debugLog)
293
- console.warn("WARN: no extension found on root", EXTENSION_NAME);
294
- break;
295
- }
296
- }
297
- return this;
298
- }
299
- onWrite(context, get) {
300
- const arr = get();
301
- if (!arr)
302
- return;
303
- for (const entry of arr) {
304
- const ext = entry.getExtension(EXTENSION_NAME);
305
- if (ext instanceof OpaqueProperty) {
306
- ext.onWrite(context, entry);
307
- }
308
- }
309
- ;
310
- }
311
- }
312
- // function setExtension(ext: ExtensibleProperty, name, extensionProperty) {
313
- // // for opaque extension we need to bypass the type system
314
- // // because we generate the OpaqueProperty class at runtime and thus the method name for _validateParent is different when compiled
315
- // // there's likely a better way to do this... as usual
316
- // const pext = ext as any as { setRefMap: (mapName: string, key: string, value: any) => any }
317
- // return pext.setRefMap('extensions', name, extensionProperty);
318
- // }
319
- class OpaqueProperty extends ExtensionProperty {
320
- static EXTENSION_NAME = EXTENSION_NAME;
321
- extensionName = typeof EXTENSION_NAME;
322
- parentTypes = ExtensionPropertyTypes;
323
- propertyType = "NEEDLE_opaque_property";
324
- init() {
325
- }
326
- setReference(name, value, attributes = { modifyChild: true }) {
327
- this.setRef(name, value, attributes);
328
- }
329
- writers = [];
330
- add(writer) {
331
- if (typeof writer === "function") {
332
- writer = new GenericWriter(writer);
333
- }
334
- this.writers.push(writer);
335
- }
336
- onWrite(context, prop) {
337
- for (const writer of this.writers) {
338
- writer.write?.call(writer, context, prop);
339
- }
340
- }
341
- getExtensionDefinition() {
342
- const ext = {};
343
- for (const writer of this.writers) {
344
- if (writer.assignTo)
345
- writer.assignTo(ext);
346
- }
347
- return ext;
348
- }
349
- }
350
- // called after the transformation
351
- // writes the extension json back to the correct object
352
- class OpaqueWriter {
353
- map;
354
- gltfField;
355
- ext;
356
- assignTo(obj) {
357
- Object.assign(obj, this.ext);
358
- }
359
- constructor(map, gltfField, ext) {
360
- this.map = map;
361
- this.gltfField = gltfField;
362
- this.ext = ext;
363
- }
364
- write(context, prop) {
365
- if (prop && this.gltfField !== undefined) {
366
- const map = context[this.map];
367
- const index = map.get(prop);
368
- const result = tryGetInArray(context.jsonDoc.json[this.gltfField], index);
369
- writeExtension(result, EXTENSION_NAME, this.ext);
370
- if (debugLog) {
371
- // console.log("WRITE", EXTENSION_NAME, "to", this.gltfField, "at index", index);
372
- // console.log("WRITE RESULT", result);
373
- }
374
- }
375
- }
376
- }
377
- return NEEDLE_opaque;
378
- }
379
- /** use to register a generic callback */
380
- class GenericWriter {
381
- fn;
382
- constructor(fn) {
383
- this.fn = fn;
384
- }
385
- write(ctx) {
386
- this.fn(ctx);
387
- }
388
- }
389
- class TextureHandler {
390
- document;
391
- textureDefinition;
392
- index;
393
- key;
394
- ext;
395
- debug = false;
396
- assignTo(obj) {
397
- Object.assign(obj, this.ext);
398
- }
399
- constructor(doc, def, key, ext, debug) {
400
- this.document = doc;
401
- this.textureDefinition = def;
402
- this.key = key;
403
- this.ext = ext;
404
- this.debug = debug;
405
- this.index = this.tryGetSourceIndex(this.textureDefinition);
406
- if (debug)
407
- console.log(def);
408
- }
409
- write(ctx) {
410
- let foundTexture = false;
411
- const textureDefs = ctx.jsonDoc.json.textures || [];
412
- const imageIndex = this.index !== undefined ? this.index : this.tryGetSourceIndex(this.textureDefinition);
413
- if (imageIndex === undefined) {
414
- console.warn("WARN: no source index found for texture", this.key);
415
- }
416
- textureDefs?.forEach((textureDef, i) => {
417
- if (foundTexture)
418
- return;
419
- const textureImageIndex = this.tryGetSourceIndex(textureDef);
420
- if (imageIndex === textureImageIndex) {
421
- if (!textureDef.extensions || !textureDef.extensions[this.key]) {
422
- // assign texture extension again
423
- textureDef.extensions = textureDef.extensions || {};
424
- foundTexture = true;
425
- if (this.debug)
426
- console.log("Re-Assign extension:", this.key, "at", i);
427
- const newExt = { ...this.ext };
428
- if (newExt.source !== undefined)
429
- newExt.source = imageIndex;
430
- textureDef.extensions[this.key] = newExt;
431
- }
432
- }
433
- });
434
- if (foundTexture === false) {
435
- if (this.key !== "EXT_texture_exr" && this.debug)
436
- console.warn("WARN: failed to re-assign extension:", this.key, this.index, this.tryGetSourceIndex(this.textureDefinition), textureDefs.length);
437
- }
438
- // this.document
439
- // .getRoot()
440
- // .listTextures()
441
- // .forEach((texture, index) => {
442
- // if (foundTexture) return;
443
- // if (!texture.getExtension(this.key)) return;
444
- // const imageIndex = ctx.imageIndexMap.get(texture);
445
- // });
446
- }
447
- tryGetSourceIndex(def) {
448
- let index = def.source;
449
- if (index !== undefined)
450
- return index;
451
- // if a glb was already compressed the source index might be missing so we also need to search in extensions
452
- if (def.extensions) {
453
- for (const ext of Object.keys(def.extensions)) {
454
- const extDef = def.extensions[ext];
455
- if (extDef.source !== undefined) {
456
- index = extDef.source;
457
- break;
458
- }
459
- }
460
- }
461
- return index;
462
- }
463
- }
464
- // declare type MaterialTextureCallback = (ctx: WriterContext, map: TemporaryMaterial, index: number) => void;
465
- // class TemporaryMaterial implements IExtensionWriter {
466
- // private readonly document: Document;
467
- // private readonly material: Material;
468
- // private readonly tex: Texture;
469
- // private readonly callbacks: MaterialTextureCallback[] = [];
470
- // constructor(doc: Document, tex: Texture, name: string) {
471
- // console.log("Create material", name);
472
- // this.document = doc;
473
- // this.tex = tex;
474
- // this.material = this.document
475
- // .createMaterial(name)
476
- // .setBaseColorTexture(tex);
477
- // }
478
- // add(cb: MaterialTextureCallback): TemporaryMaterial {
479
- // this.callbacks.push(cb);
480
- // return this;
481
- // }
482
- // write(context: WriterContext): void {
483
- // const matIndex = context.materialIndexMap.get(this.material);
484
- // if (matIndex !== undefined && matIndex !== -1) {
485
- // console.log("Remove material", this.material.getName(), matIndex);
486
- // context.jsonDoc.json.materials?.splice(matIndex, 1);
487
- // if (this.callbacks.length > 0) {
488
- // const texInfo = this.material.getBaseColorTextureInfo();
489
- // if (texInfo) {
490
- // const def = context.textureInfoDefMap.get(texInfo);
491
- // if (def) {
492
- // const index = def.index;
493
- // for (const cb of this.callbacks) {
494
- // cb(context, this, index);
495
- // }
496
- // }
497
- // }
498
- // }
499
- // }
500
- // }
501
- // }
502
- // json pointer -------------------------------------------------------------
503
- // abstract class PointerProperty extends ExtensionProperty {
504
- // abstract setPointer(texture: Texture, name: string);
505
- // }
506
- // const { R, G, B, A } = TextureChannel;
507
- // function createPointerProperty(EXTENSION_NAME: string, doc: Document): PointerProperty {
508
- // class TexturePointerPointerProperty extends ExtensionProperty<any> {
509
- // static EXTENSION_NAME = EXTENSION_NAME;
510
- // readonly extensionName: string = EXTENSION_NAME;
511
- // parentTypes: PropertyType[] = ALL_PROPERTY_TYPES;
512
- // propertyType = "NEEDLE_pointer_property";
513
- // init() { }
514
- // public setPointer(texture: Texture | null, name: string): this {
515
- // console.log("SET REF", name);
516
- // return this.setRef(name, texture, { channels: R | G | B | A });
517
- // }
518
- // }
519
- // return new TexturePointerPointerProperty(doc.getGraph());
520
- // }
521
- class JsonPointerHandler {
522
- extension_name;
523
- document;
524
- map = new Map();
525
- debug = false;
526
- breakPoint = false;
527
- VRM_0_MaterialIndexRegex = new RegExp(/materialProperties\/(?<material_index>\d+)\//g);
528
- constructor(document, extension_name) {
529
- this.document = document;
530
- this.extension_name = extension_name;
531
- }
532
- read(context, extProp, obj, prop, key, fullPath) {
533
- const value = key !== undefined ? obj[key] : obj;
534
- // the pointer path contains the full path from the root object to the json pointer
535
- if (key !== undefined) {
536
- fullPath ??= "";
537
- fullPath += "/" + key;
538
- if (this.breakPoint === "pointer:read") {
539
- debugger;
540
- }
541
- }
542
- // if(this.debug && key === "colliders") debugger;
543
- switch (typeof value) {
544
- case 'object':
545
- if (value !== null && value !== undefined) {
546
- if (Array.isArray(value)) {
547
- for (let i = 0; i < value.length; i++) {
548
- this.read(context, extProp, value, prop, i.toString(), fullPath);
549
- }
550
- }
551
- else {
552
- for (const key of Object.keys(value)) {
553
- this.read(context, extProp, value, prop, key, fullPath);
554
- }
555
- }
556
- }
557
- break;
558
- case "string":
559
- if (value && key) {
560
- let prefix = "/textures/";
561
- let isTexturePointer = value.startsWith(prefix);
562
- if (!isTexturePointer) {
563
- // backwards compatibility for old format where it didnt start with /textures/ but with textures/
564
- prefix = "textures/";
565
- isTexturePointer = value.startsWith(prefix);
566
- }
567
- if (isTexturePointer) {
568
- if (this.debug)
569
- console.log("Read pointer", value);
570
- let pointer = this.map.get(value);
571
- if (!pointer) {
572
- const length = prefix.length;
573
- const index = parseInt(value.substring(length));
574
- const path = value.substring(0, length);
575
- const definition = context.jsonDoc.json.textures[index];
576
- const imageIndex = findSource(definition);
577
- if (this.debug)
578
- console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
579
- const object = tryGetInArray(context.textures, imageIndex);
580
- if (object) {
581
- const res = new TextureIndexResolver(this.document, object, definition);
582
- res.debug = this.debug;
583
- res.init(context); // < init here otherwise the fields defined in the TexturePointer will be undefined (this caused e.g. finding the previous texture filter to fail)
584
- pointer = new JsonPointer(path, res, value);
585
- pointer.debug = this.debug;
586
- this.map.set(value, pointer);
587
- // Assume all texture slots are being used. The referenceName is the json pointer right now - usually this is e.g. `baseColorTexture` (the name ends up in the slots array)
588
- const referenceName = value;
589
- // const slots = listTextureSlots(object);
590
- prop.setReference(referenceName, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
591
- // make sure there's a reference to the buffer to avoid dedup or the buffer being removed because it MIGHT not be used by any node
592
- // TODO: how do we get the buffer that the texture/image is currently using - here we just take the first buffer but this might not be correct
593
- const buffer = this.document.getRoot().listBuffers()[0];
594
- if (buffer)
595
- prop.setReference("buffer", buffer);
596
- }
597
- else {
598
- console.warn("Texture not found", fullPath, value);
599
- }
600
- }
601
- if (pointer) {
602
- if (this.debug)
603
- console.log("> Register pointer", this.extension_name + "/" + fullPath + " : " + value);
604
- pointer?.register(obj, key, fullPath);
605
- }
606
- else
607
- console.warn("WARN: failed registering pointer", fullPath, value);
608
- }
609
- if (value.startsWith("/materials/")) {
610
- if (this.debug)
611
- console.log("Read pointer", value);
612
- const index = parseInt(value.substring("/materials/".length));
613
- const object = context.materials[index];
614
- if (object) {
615
- // set a reference to the material to avoid dedup or the material being removed because it MIGHT not be used by any node
616
- // BUT a component is referencing the material so we don't want it ever to be removed
617
- // see https://github.com/donmccurdy/glTF-Transform/issues/980#issuecomment-1585748202
618
- if (prop) {
619
- prop.setReference(value, object);
620
- }
621
- let pointer = this.map.get(value);
622
- if (!pointer) {
623
- const definition = context.jsonDoc.json.materials[index];
624
- const res = new MaterialIndexResolver(this.document, object, definition);
625
- res.debug = this.debug;
626
- res.init(context); // < init here otherwise the fields defined in the TexturePointer will be undefined (this caused e.g. finding the previous texture filter to fail)
627
- pointer = new JsonPointer("/materials/", res, value);
628
- pointer.debug = this.debug;
629
- this.map.set(value, pointer);
630
- prop.setReference(value, object);
631
- }
632
- pointer?.register(obj, key, fullPath);
633
- }
634
- }
635
- if (value.startsWith("/animations/")) {
636
- if (this.debug)
637
- console.log("Read pointer", value);
638
- const index = parseInt(value.substring("/animations/".length));
639
- const object = context.animations[index];
640
- if (object) {
641
- let pointer = this.map.get(value);
642
- if (!pointer) {
643
- // TODO: resolve animations + we need to set the reference properly so we can use prune. See https://linear.app/needle/issue/NE-4982
644
- // const definition = context.jsonDoc.json.animations![index];
645
- // const res = new AnimationIndexResolver(this.document, object, definition);
646
- // res.debug = this.debug;
647
- // res.init(context);
648
- // pointer = new JsonPointer("/animations/", res, value);
649
- // pointer.debug = this.debug;
650
- // this.map.set(value, pointer);
651
- prop.setReference(value, object);
652
- }
653
- pointer?.register(obj, key, fullPath);
654
- }
655
- }
656
- if (value.startsWith("/meshes/")) {
657
- if (this.debug)
658
- console.log("Read pointer", value);
659
- const index = parseInt(value.substring("/meshes/".length));
660
- const object = context.meshes[index];
661
- if (object) {
662
- let pointer = this.map.get(value);
663
- if (!pointer) {
664
- const definition = context.jsonDoc.json.meshes[index];
665
- const res = new MeshIndexResolver(this.document, object, definition, prop);
666
- res.debug = this.debug;
667
- res.init(context);
668
- pointer = new JsonPointer("/meshes/", res, value);
669
- pointer.debug = this.debug;
670
- this.map.set(value, pointer);
671
- prop.setReference(value, object);
672
- }
673
- pointer?.register(obj, key, fullPath);
674
- }
675
- }
676
- }
677
- break;
678
- case 'number':
679
- if (key === "node" || key === "mesh"
680
- // VRM 1.0 spring bone center https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_springBone-1.0/schema/VRMC_springBone.spring.schema.json
681
- || fullPath.match(/springs\/\d+\/center/g)) {
682
- const object = context.nodes[value];
683
- if (object) {
684
- const res = new NodeIndexResolver(this.document, object, context.jsonDoc.json.nodes[value]);
685
- res.debug = this.debug;
686
- res.init(context);
687
- const dictKey = `/${key}/${value}`;
688
- let pointer = this.map.get(dictKey);
689
- if (!pointer) {
690
- pointer = new JsonPointer(null, res, value);
691
- }
692
- pointer.debug = this.debug;
693
- pointer.register(obj, key, fullPath);
694
- this.map.set(dictKey, pointer);
695
- prop.setReference(dictKey, object);
696
- }
697
- }
698
- else if (
699
- // key texture is matching for example for VRM meta/texture (AvatarSample_A)
700
- key === "texture"
701
- // VRM 0.0 material properties
702
- || fullPath.match(/materialProperties\/(?<material_index>\d+)\/textureProperties/g)
703
- // VRM 1.0 meta
704
- || fullPath.match(/meta\/thumbnailImage/g)
705
- // NEEDLE_materials_mtlx where the root extension has { textures: [0, 1, 2, ...] }
706
- || fullPath.match(/^\/textures\/\d{1,}$/)) {
707
- const index = value;
708
- // e.g. in meta.texture for VRM avatars it seems like we can have negative values
709
- if (index < 0) {
710
- console.warn(`WARN: texture index is negative ${index} at ${fullPath}`);
711
- return;
712
- }
713
- const definition = context.jsonDoc.json.textures[index];
714
- if (!definition) {
715
- console.warn(`WARN: texture definition with index ${index} not found or does not exist. Set in \"${fullPath}\"`);
716
- return;
717
- }
718
- const imageIndex = findSource(definition);
719
- if (this.debug)
720
- console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
721
- const object = tryGetInArray(context.textures, imageIndex);
722
- if (object) {
723
- if (this.debug)
724
- console.log("Found texture index at " + fullPath + ": " + value);
725
- const res = new TextureIndexResolver(this.document, object, definition);
726
- res.debug = this.debug;
727
- res.init(context);
728
- const dictKey = `/textures/${value}`;
729
- let pointer = this.map.get(dictKey);
730
- if (!pointer) {
731
- pointer = new JsonPointer(null, res, value);
732
- }
733
- pointer.debug = this.debug;
734
- pointer.register(obj, key, fullPath);
735
- this.map.set(dictKey, pointer);
736
- prop.setReference(dictKey, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
737
- }
738
- const materialMatch = this.VRM_0_MaterialIndexRegex.exec(fullPath);
739
- if (materialMatch) {
740
- const materialIndex = parseInt(materialMatch.groups.material_index);
741
- const material = context.materials[materialIndex];
742
- prop.setReference(`/materials/${materialIndex}`, material);
743
- // const res = new MaterialIndexResolver(this.document, material, context.jsonDoc.json.materials![materialIndex]);
744
- // res.debug = this.debug;
745
- // res.init(context);
746
- // const dictKey = `/materials/${materialIndex}`
747
- // let pointer = this.map.get(dictKey);
748
- // if (!pointer) {
749
- // pointer = new JsonPointer(null, res, value);
750
- // }
751
- // pointer.debug = this.debug;
752
- // pointer.register(obj, key, fullPath);
753
- // this.map.set(dictKey, pointer);
754
- }
755
- }
756
- break;
757
- default:
758
- break;
759
- }
760
- }
761
- resolveAndWrite(context, step) {
762
- this.map?.forEach(ptr => {
763
- if (ptr.resolve(context, step))
764
- ptr.write();
765
- });
766
- }
767
- }
768
- class JsonPointer {
769
- debug = false;
770
- resolver;
771
- pointerBase;
772
- oldPointer;
773
- constructor(pointerBase, resolver, oldPointer) {
774
- this.pointerBase = pointerBase;
775
- this.resolver = resolver;
776
- this.oldPointer = oldPointer;
777
- }
778
- _objects = [];
779
- _props = [];
780
- _debugInfo = [];
781
- register(obj, prop, fullpath) {
782
- this._objects.push(obj);
783
- this._props.push(prop);
784
- this._debugInfo.push(fullpath);
785
- }
786
- newPointer = undefined;
787
- resolve(context, step) {
788
- // If the pointer was already resolved, we don't need to do it again
789
- if (this.newPointer !== undefined) {
790
- return true;
791
- }
792
- //@ts-ignore
793
- this.resolver.debug = this.debug;
794
- const index = this.resolver.resolve(context);
795
- if (index != undefined) {
796
- if (this.pointerBase != null) {
797
- this.newPointer = this.pointerBase + index;
798
- }
799
- else {
800
- this.newPointer = index;
801
- }
802
- return true;
803
- }
804
- else {
805
- // If the pointer could not be resolved during write we need to log an error
806
- if (step === "write") {
807
- console.log(`ERR: 🧨 FAILED to resolve pointer \"${this.oldPointer}\" used by ${this._debugInfo.join(", ")}`);
808
- if (this.debug)
809
- debugger;
810
- }
811
- }
812
- return false;
813
- }
814
- _didWrite = false;
815
- write() {
816
- if (this._didWrite)
817
- return;
818
- this._didWrite = false;
819
- for (let i = 0; i < this._objects.length; i++) {
820
- const obj = this._objects[i];
821
- const prop = this._props[i];
822
- if (this.debug) {
823
- console.log(`< Resolved pointer ${this._debugInfo[i]}: ${this.oldPointer} → ${this.newPointer}`);
824
- }
825
- obj[prop] = this.newPointer;
826
- }
827
- }
828
- }
829
- class PointerResolver {
830
- document;
831
- /* the object originally being referenced by the pointer */
832
- inputObject;
833
- /* the original json definition */
834
- inputDefinition;
835
- /** The graph property this pointer belongs to */
836
- property;
837
- constructor(doc, obj, def, prop) {
838
- this.document = doc;
839
- this.inputObject = obj;
840
- this.inputDefinition = def;
841
- this.property = prop;
842
- }
843
- debug = false;
844
- }
845
- class TextureIndexResolver extends PointerResolver {
846
- _debugName;
847
- _samplerDef;
848
- init(context) {
849
- this._debugName = this.inputDefinition.name ?? this.inputDefinition.source?.toString() ?? "unknown";
850
- const samplerIndex = this.inputDefinition.sampler;
851
- if (samplerIndex !== undefined) {
852
- this._samplerDef = context.jsonDoc.json.samplers[samplerIndex];
853
- }
854
- if (this.debug)
855
- console.log(this._debugName, this._samplerDef);
856
- }
857
- resolve(context) {
858
- const imgIndex = context.imageIndexMap.get(this.inputObject);
859
- if (imgIndex === undefined) {
860
- console.error("ERR: image is missing", imgIndex);
861
- return -1;
862
- }
863
- // ensure we have a texture array
864
- context.jsonDoc.json.textures = context.jsonDoc.json.textures || [];
865
- // try find the texture that points to the same image source
866
- for (let i = 0; i < context.jsonDoc.json.textures.length; i++) {
867
- const textureDef = context.jsonDoc.json.textures[i];
868
- if (findSource(textureDef) === imgIndex) {
869
- if (textureDef.sampler === undefined) {
870
- textureDef.sampler = this.findOrCreateSampler(context.jsonDoc.json);
871
- }
872
- if (this.debug)
873
- console.log("Found texture def at " + i, "using image", imgIndex, textureDef);
874
- return i;
875
- }
876
- }
877
- // the texture was not found - add it
878
- // create a new texture def entry
879
- if (this.debug)
880
- console.log("Could not find texture, creating a new one: " + this._debugName);
881
- const hasExtensionWithSource = this.inputDefinition.extensions && Object.keys(this.inputDefinition.extensions).some(key => this.inputDefinition.extensions[key]["source"] !== undefined);
882
- const textureDef = {
883
- source: hasExtensionWithSource ? undefined : imgIndex,
884
- sampler: this.findOrCreateSampler(context.jsonDoc.json),
885
- extensions: this.inputDefinition.extensions,
886
- extras: this.inputDefinition.extras,
887
- };
888
- context.jsonDoc.json.textures.push(textureDef);
889
- return context.jsonDoc.json.textures.length - 1;
890
- }
891
- findOrCreateSampler(json) {
892
- json.samplers = json.samplers || [];
893
- if (this._samplerDef === undefined) {
894
- console.log("WARN: Creating default sampler", this._debugName);
895
- // create a default sampler
896
- this._samplerDef = {
897
- magFilter: 9729,
898
- minFilter: 9729,
899
- wrapS: 33648,
900
- wrapT: 33648
901
- };
902
- }
903
- else {
904
- for (let i = 0; i < json.samplers.length; i++) {
905
- const sampler = json.samplers[i];
906
- if (sampler.minFilter === this._samplerDef.minFilter &&
907
- sampler.magFilter === this._samplerDef.magFilter &&
908
- sampler.wrapS === this._samplerDef.wrapS &&
909
- sampler.wrapT === this._samplerDef.wrapT) {
910
- return i;
911
- }
912
- }
913
- }
914
- // append previous sampler
915
- json.samplers.push(this._samplerDef);
916
- return json.samplers.length - 1;
917
- }
918
- }
919
- function findSource(texDef) {
920
- if (texDef.source !== undefined) {
921
- return texDef.source;
922
- }
923
- if (texDef.extensions) {
924
- const keys = Object.keys(texDef.extensions);
925
- for (let i = 0; i < keys.length; i++) {
926
- const key = keys[i];
927
- const ext = texDef.extensions[key];
928
- if (ext["source"] !== undefined) {
929
- return ext["source"];
930
- }
931
- }
932
- }
933
- return undefined;
934
- }
935
- class MaterialIndexResolver extends PointerResolver {
936
- init(context) {
937
- }
938
- resolve(context) {
939
- return context.materialIndexMap.get(this.inputObject);
940
- }
941
- }
942
- class NodeIndexResolver extends PointerResolver {
943
- init(context) {
944
- }
945
- resolve(context) {
946
- return context.nodeIndexMap.get(this.inputObject);
947
- }
948
- }
949
- class MeshIndexResolver extends PointerResolver {
950
- init(context) {
951
- }
952
- resolve(context) {
953
- let index = context.meshIndexMap.get(this.inputObject);
954
- if (index >= 0) {
955
- return index;
956
- }
957
- let obj = null;
958
- if (this.inputObject.isDisposed()) {
959
- // If the mesh was deduplicated we try to get the mesh from a parent node
960
- // This is a HACK, we need to get the reference to the mesh this one was deduplicated with since the parent node potentially has many children
961
- const parents = this.property?.listParents();
962
- for (const parent of parents) {
963
- if (parent instanceof Node) {
964
- obj = parent.getMesh();
965
- if (obj) {
966
- this.document.getLogger().info(`INFO: resolved mesh \"${this.inputObject.getName()}\" from parent node \"${parent.getName()}\"`);
967
- break;
968
- }
969
- }
970
- }
971
- if (obj.isDisposed()) {
972
- console.warn("WARN: mesh is disposed", this.inputObject.getName());
973
- }
974
- }
975
- if (!obj) {
976
- return undefined;
977
- }
978
- return context.meshIndexMap.get(obj);
979
- }
980
- }
1
+ import { Extension, ExtensionProperty, PropertyType, Node, TextureChannel } from '@gltf-transform/core';
2
+ import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
3
+ import { isLightmap } from '../../utils/texture.js';
4
+ const ALL_PROPERTY_TYPES = [];
5
+ for (const key in PropertyType) {
6
+ ALL_PROPERTY_TYPES.push(PropertyType[key]);
7
+ }
8
+ export let currentGenerator = "";
9
+ // an opaque extension can be used to pass data through the gltf transform process
10
+ // it should also be able to re-write/fix json pointers after the transformation which might have shuffled them around
11
+ export function createOpaqueExtension(name, types, opts) {
12
+ if (!Array.isArray(types))
13
+ types = [types];
14
+ const EXTENSION_NAME = name;
15
+ const ExtensionPropertyTypes = ALL_PROPERTY_TYPES;
16
+ const debugLog = opts?.debug ?? false;
17
+ class NEEDLE_opaque extends Extension {
18
+ extensionName = EXTENSION_NAME;
19
+ static EXTENSION_NAME = EXTENSION_NAME;
20
+ prewriteTypes = [PropertyType.MATERIAL];
21
+ // private roots: Array<OpaqueProperty> = [];
22
+ jsonPointerHandler;
23
+ read(context) {
24
+ if (opts?.breakpoint === true)
25
+ debugger;
26
+ currentGenerator = context.jsonDoc.json.asset?.generator;
27
+ if (debugLog)
28
+ console.log("READ " + EXTENSION_NAME);
29
+ this.jsonPointerHandler = new JsonPointerHandler(this.document, EXTENSION_NAME);
30
+ this.jsonPointerHandler.debug = debugLog;
31
+ this.jsonPointerHandler.breakPoint = opts?.breakpoint;
32
+ for (const type of types) {
33
+ switch (type) {
34
+ case PropertyType.ACCESSOR:
35
+ this.onRead(context, "accessors", "accessorIndexMap");
36
+ break;
37
+ case PropertyType.ANIMATION:
38
+ this.onRead(context, "animations", "animationIndexMap");
39
+ break;
40
+ case PropertyType.ANIMATION_CHANNEL:
41
+ if (EXTENSION_NAME === "KHR_animation_pointer") {
42
+ context.jsonDoc.json.animations?.forEach((animDef, animIndex) => {
43
+ const anim = context.animations[animIndex];
44
+ const channels = anim.listChannels();
45
+ animDef.channels?.forEach((channel, channelIndex) => {
46
+ const jsonTarget = channel.target;
47
+ const khr_animation = jsonTarget.extensions && jsonTarget.extensions[EXTENSION_NAME];
48
+ if (!khr_animation)
49
+ return;
50
+ const pointer = khr_animation.pointer;
51
+ if (pointer) {
52
+ const sections = pointer.split("/");
53
+ const nodeIndex = parseInt(sections[2]);
54
+ const targetName = sections[3];
55
+ if (targetName === "scale" || targetName === "translation" || targetName === "rotation") {
56
+ channels[channelIndex].setTargetNode(context.nodes[nodeIndex]);
57
+ channels[channelIndex].setTargetPath(targetName);
58
+ }
59
+ }
60
+ const existing = anim.getExtension(EXTENSION_NAME);
61
+ if (debugLog)
62
+ console.log(">>>", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
63
+ if (existing) {
64
+ assignChannels(existing);
65
+ return;
66
+ }
67
+ const prop = this.createExtension();
68
+ // there seems no way (yet?) to add extensions to channel.targets (?)
69
+ // https://github.com/donmccurdy/glTF-Transform/discussions/613
70
+ // so we add it to an object by keys to the animation itself and read it back in WRITE
71
+ const khr_animation_data = {};
72
+ khr_animation_data[channelIndex] = khr_animation;
73
+ anim.setExtension(EXTENSION_NAME, prop);
74
+ assignChannels(prop);
75
+ function assignChannels(prop) {
76
+ prop.add(ctx => {
77
+ const animations = ctx.jsonDoc.json.animations;
78
+ if (!animations)
79
+ return;
80
+ const animDef = animations[animIndex];
81
+ const channel = animDef?.channels[channelIndex];
82
+ if (channel) {
83
+ if (debugLog)
84
+ console.log("<<<", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
85
+ channel.target = jsonTarget;
86
+ }
87
+ else if (debugLog)
88
+ console.log("WARN: ", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, "not found");
89
+ });
90
+ }
91
+ ;
92
+ });
93
+ });
94
+ }
95
+ break;
96
+ case PropertyType.ANIMATION_SAMPLER:
97
+ break;
98
+ case PropertyType.BUFFER:
99
+ this.onRead(context, "buffers", "bufferIndexMap");
100
+ break;
101
+ case PropertyType.CAMERA:
102
+ this.onRead(context, "cameras", "cameraIndexMap");
103
+ break;
104
+ case PropertyType.MATERIAL:
105
+ this.onRead(context, "materials", "materialIndexMap");
106
+ break;
107
+ case PropertyType.TEXTURE:
108
+ context.jsonDoc.json.textures?.forEach((textureDefinition, index) => {
109
+ if (!textureDefinition.extensions)
110
+ return;
111
+ const ext = textureDefinition.extensions[EXTENSION_NAME];
112
+ if (!ext)
113
+ return;
114
+ // TODO: textures and textureDefinition are not in the same order?
115
+ // see https://github.com/donmccurdy/glTF-Transform/blob/0e99c426b8ef41536228fe860ab8329be5d1d50f/packages/core/src/io/reader.ts#L132
116
+ // textures are in image order
117
+ // TODO: we need to use the image index to find the texture
118
+ let texture = getTexture(context, textureDefinition);
119
+ if (!texture) {
120
+ console.log("WARN: could potentially not find gltf transform texture for " + index);
121
+ texture = context.textures[index];
122
+ }
123
+ const prop = this.createExtension();
124
+ this.jsonPointerHandler.read(context, texture, ext, prop);
125
+ texture.setExtension(EXTENSION_NAME, prop);
126
+ const handler = new TextureHandler(this.document, textureDefinition, EXTENSION_NAME, ext, debugLog);
127
+ prop.add(handler);
128
+ });
129
+ break;
130
+ case PropertyType.NODE:
131
+ this.onRead(context, "nodes", "nodeIndexMap");
132
+ break;
133
+ case PropertyType.MESH:
134
+ this.onRead(context, "meshes", "meshIndexMap");
135
+ break;
136
+ case PropertyType.PRIMITIVE:
137
+ context.jsonDoc.json.meshes?.forEach((meshDefinition, meshIndex) => {
138
+ const mesh = context.meshes[meshIndex];
139
+ const primitives = mesh.listPrimitives();
140
+ meshDefinition.primitives?.forEach((primitiveDefinition, primitiveIndex) => {
141
+ if (!primitiveDefinition.extensions)
142
+ return;
143
+ const ext = primitiveDefinition.extensions[EXTENSION_NAME];
144
+ if (!ext)
145
+ return;
146
+ const primitive = primitives[primitiveIndex];
147
+ const prop = this.createExtension();
148
+ this.jsonPointerHandler.read(context, primitive, ext, prop);
149
+ primitive.setExtension(EXTENSION_NAME, prop);
150
+ // this is invoked on write
151
+ prop.add(ctx => {
152
+ const newMeshIndex = ctx.meshIndexMap.get(mesh);
153
+ const newMesh = ctx.jsonDoc.json.meshes[newMeshIndex];
154
+ const newPrimitive = newMesh.primitives[primitiveIndex];
155
+ newPrimitive.extensions = newPrimitive.extensions || {};
156
+ newPrimitive.extensions[EXTENSION_NAME] = ext;
157
+ });
158
+ });
159
+ });
160
+ break;
161
+ case PropertyType.ROOT:
162
+ const sourceArray = context.jsonDoc.json.extensions;
163
+ if (!sourceArray)
164
+ continue;
165
+ const ext = sourceArray[EXTENSION_NAME];
166
+ if (ext) {
167
+ const root = this.document.getRoot();
168
+ const prop = this.createExtension();
169
+ this.jsonPointerHandler.read(context, root, ext, prop);
170
+ root.setExtension(EXTENSION_NAME, prop);
171
+ this.lightmapWorkaround(context, prop, this.document.getLogger());
172
+ prop.add(ctx => {
173
+ ctx.jsonDoc.json.extensions = ctx.jsonDoc.json.extensions || {};
174
+ ctx.jsonDoc.json.extensions[EXTENSION_NAME] = ext;
175
+ });
176
+ }
177
+ break;
178
+ }
179
+ }
180
+ return this;
181
+ }
182
+ // this is temporarely here to prevent compression on textures named Lightmap
183
+ // until we have better control over the compression process
184
+ lightmapWorkaround(context, prop, logger) {
185
+ const textureDefinitions = context.jsonDoc.json.textures;
186
+ if (!textureDefinitions)
187
+ return;
188
+ const textures = this.document.getRoot().listTextures();
189
+ for (let i = 0; i < textures.length; i++) {
190
+ const tex = textures[i];
191
+ const texDef = textureDefinitions[i];
192
+ const name = texDef?.name;
193
+ if (isLightmap(tex) && tex.getMimeType() !== "image/exr") {
194
+ prop.setReference("lightmapTexture", tex, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
195
+ logger.info(`Found lightmap texture ${name}: ${tex.getMimeType()} in ${EXTENSION_NAME}`);
196
+ }
197
+ }
198
+ }
199
+ createExtension(name = '') {
200
+ return new OpaqueProperty(this.document.getGraph(), name);
201
+ ;
202
+ }
203
+ onRead(context, gltfField, map) {
204
+ if (gltfField === undefined)
205
+ return;
206
+ try {
207
+ const sourceArray = context.jsonDoc.json[gltfField];
208
+ const target = context[gltfField];
209
+ sourceArray?.forEach((def, index) => {
210
+ if (!def.extensions || !def.extensions[EXTENSION_NAME])
211
+ return;
212
+ const ext = def.extensions[EXTENSION_NAME];
213
+ if (debugLog)
214
+ console.log("READ", index, ext);
215
+ const prop = this.createExtension();
216
+ // attach the property
217
+ target[index].setExtension(EXTENSION_NAME, prop);
218
+ // capture all the json pointers in the extension
219
+ this.jsonPointerHandler.read(context, target[index], ext, prop);
220
+ // make sure we write our extension back to the gltf
221
+ prop.add(new OpaqueWriter(map, gltfField, ext));
222
+ });
223
+ }
224
+ catch (error) {
225
+ console.error("ERR: " + error);
226
+ }
227
+ }
228
+ prewrite(context, _propertyType) {
229
+ // Attempt resolve textures here so that the json.textures is already populated for cases where a texture isn't used by any material
230
+ // this is e.g. the case for textures that are just referenced by a component but not used by any texture
231
+ // https://linear.app/needle/issue/NE-4928
232
+ this.jsonPointerHandler.resolveAndWrite(context, "prewrite");
233
+ return this;
234
+ }
235
+ write(context) {
236
+ if (opts?.breakpoint === true)
237
+ debugger;
238
+ if (opts?.write === false) {
239
+ return this;
240
+ }
241
+ if (debugLog)
242
+ console.log("WRITE " + EXTENSION_NAME);
243
+ this.jsonPointerHandler.resolveAndWrite(context, "write");
244
+ const root = this.document.getRoot();
245
+ // for (const rootExt of this.roots) {
246
+ // rootExt.onWrite(context, root);
247
+ // }
248
+ for (const type of types) {
249
+ switch (type) {
250
+ case PropertyType.ACCESSOR:
251
+ this.onWrite(context, () => root.listAccessors());
252
+ break;
253
+ case PropertyType.ANIMATION:
254
+ this.onWrite(context, () => root.listAnimations());
255
+ break;
256
+ case PropertyType.ANIMATION_CHANNEL:
257
+ break;
258
+ case PropertyType.ANIMATION_SAMPLER:
259
+ break;
260
+ case PropertyType.BUFFER:
261
+ this.onWrite(context, () => root.listBuffers());
262
+ break;
263
+ case PropertyType.CAMERA:
264
+ this.onWrite(context, () => root.listCameras());
265
+ break;
266
+ case PropertyType.MATERIAL:
267
+ this.onWrite(context, () => root.listMaterials());
268
+ break;
269
+ case PropertyType.TEXTURE:
270
+ this.onWrite(context, () => root.listTextures());
271
+ break;
272
+ case PropertyType.NODE:
273
+ this.onWrite(context, () => root.listNodes());
274
+ break;
275
+ case PropertyType.MESH:
276
+ this.onWrite(context, () => root.listMeshes());
277
+ break;
278
+ case PropertyType.PRIMITIVE:
279
+ const allPrimitives = root.listMeshes().flatMap(mesh => mesh.listPrimitives());
280
+ for (const primitive of allPrimitives) {
281
+ const ext = primitive.getExtension(EXTENSION_NAME);
282
+ if (ext instanceof OpaqueProperty) {
283
+ ext.onWrite(context, primitive);
284
+ }
285
+ }
286
+ break;
287
+ case PropertyType.ROOT:
288
+ const prop = root.getExtension(EXTENSION_NAME);
289
+ if (prop instanceof OpaqueProperty) {
290
+ prop.onWrite(context, root);
291
+ }
292
+ else if (debugLog)
293
+ console.warn("WARN: no extension found on root", EXTENSION_NAME);
294
+ break;
295
+ }
296
+ }
297
+ return this;
298
+ }
299
+ onWrite(context, get) {
300
+ const arr = get();
301
+ if (!arr)
302
+ return;
303
+ for (const entry of arr) {
304
+ const ext = entry.getExtension(EXTENSION_NAME);
305
+ if (ext instanceof OpaqueProperty) {
306
+ ext.onWrite(context, entry);
307
+ }
308
+ }
309
+ ;
310
+ }
311
+ }
312
+ // function setExtension(ext: ExtensibleProperty, name, extensionProperty) {
313
+ // // for opaque extension we need to bypass the type system
314
+ // // because we generate the OpaqueProperty class at runtime and thus the method name for _validateParent is different when compiled
315
+ // // there's likely a better way to do this... as usual
316
+ // const pext = ext as any as { setRefMap: (mapName: string, key: string, value: any) => any }
317
+ // return pext.setRefMap('extensions', name, extensionProperty);
318
+ // }
319
+ class OpaqueProperty extends ExtensionProperty {
320
+ static EXTENSION_NAME = EXTENSION_NAME;
321
+ extensionName = typeof EXTENSION_NAME;
322
+ parentTypes = ExtensionPropertyTypes;
323
+ propertyType = "NEEDLE_opaque_property";
324
+ init() {
325
+ }
326
+ setReference(name, value, attributes = { modifyChild: true }) {
327
+ this.setRef(name, value, attributes);
328
+ }
329
+ writers = [];
330
+ add(writer) {
331
+ if (typeof writer === "function") {
332
+ writer = new GenericWriter(writer);
333
+ }
334
+ this.writers.push(writer);
335
+ }
336
+ onWrite(context, prop) {
337
+ for (const writer of this.writers) {
338
+ writer.write?.call(writer, context, prop);
339
+ }
340
+ }
341
+ getExtensionDefinition() {
342
+ const ext = {};
343
+ for (const writer of this.writers) {
344
+ if (writer.assignTo)
345
+ writer.assignTo(ext);
346
+ }
347
+ return ext;
348
+ }
349
+ }
350
+ // called after the transformation
351
+ // writes the extension json back to the correct object
352
+ class OpaqueWriter {
353
+ map;
354
+ gltfField;
355
+ ext;
356
+ assignTo(obj) {
357
+ Object.assign(obj, this.ext);
358
+ }
359
+ constructor(map, gltfField, ext) {
360
+ this.map = map;
361
+ this.gltfField = gltfField;
362
+ this.ext = ext;
363
+ }
364
+ write(context, prop) {
365
+ if (prop && this.gltfField !== undefined) {
366
+ const map = context[this.map];
367
+ const index = map.get(prop);
368
+ const result = tryGetInArray(context.jsonDoc.json[this.gltfField], index);
369
+ writeExtension(result, EXTENSION_NAME, this.ext);
370
+ if (debugLog) {
371
+ // console.log("WRITE", EXTENSION_NAME, "to", this.gltfField, "at index", index);
372
+ // console.log("WRITE RESULT", result);
373
+ }
374
+ }
375
+ }
376
+ }
377
+ return NEEDLE_opaque;
378
+ }
379
+ /** use to register a generic callback */
380
+ class GenericWriter {
381
+ fn;
382
+ constructor(fn) {
383
+ this.fn = fn;
384
+ }
385
+ write(ctx) {
386
+ this.fn(ctx);
387
+ }
388
+ }
389
+ class TextureHandler {
390
+ document;
391
+ textureDefinition;
392
+ index;
393
+ key;
394
+ ext;
395
+ debug = false;
396
+ assignTo(obj) {
397
+ Object.assign(obj, this.ext);
398
+ }
399
+ constructor(doc, def, key, ext, debug) {
400
+ this.document = doc;
401
+ this.textureDefinition = def;
402
+ this.key = key;
403
+ this.ext = ext;
404
+ this.debug = debug;
405
+ this.index = this.tryGetSourceIndex(this.textureDefinition);
406
+ if (debug)
407
+ console.log(def);
408
+ }
409
+ write(ctx) {
410
+ let foundTexture = false;
411
+ const textureDefs = ctx.jsonDoc.json.textures || [];
412
+ const imageIndex = this.index !== undefined ? this.index : this.tryGetSourceIndex(this.textureDefinition);
413
+ if (imageIndex === undefined) {
414
+ console.warn("WARN: no source index found for texture", this.key);
415
+ }
416
+ textureDefs?.forEach((textureDef, i) => {
417
+ if (foundTexture)
418
+ return;
419
+ const textureImageIndex = this.tryGetSourceIndex(textureDef);
420
+ if (imageIndex === textureImageIndex) {
421
+ if (!textureDef.extensions || !textureDef.extensions[this.key]) {
422
+ // assign texture extension again
423
+ textureDef.extensions = textureDef.extensions || {};
424
+ foundTexture = true;
425
+ if (this.debug)
426
+ console.log("Re-Assign extension:", this.key, "at", i);
427
+ const newExt = { ...this.ext };
428
+ if (newExt.source !== undefined)
429
+ newExt.source = imageIndex;
430
+ textureDef.extensions[this.key] = newExt;
431
+ }
432
+ }
433
+ });
434
+ if (foundTexture === false) {
435
+ if (this.key !== "EXT_texture_exr" && this.debug)
436
+ console.warn("WARN: failed to re-assign extension:", this.key, this.index, this.tryGetSourceIndex(this.textureDefinition), textureDefs.length);
437
+ }
438
+ // this.document
439
+ // .getRoot()
440
+ // .listTextures()
441
+ // .forEach((texture, index) => {
442
+ // if (foundTexture) return;
443
+ // if (!texture.getExtension(this.key)) return;
444
+ // const imageIndex = ctx.imageIndexMap.get(texture);
445
+ // });
446
+ }
447
+ tryGetSourceIndex(def) {
448
+ let index = def.source;
449
+ if (index !== undefined)
450
+ return index;
451
+ // if a glb was already compressed the source index might be missing so we also need to search in extensions
452
+ if (def.extensions) {
453
+ for (const ext of Object.keys(def.extensions)) {
454
+ const extDef = def.extensions[ext];
455
+ if (extDef.source !== undefined) {
456
+ index = extDef.source;
457
+ break;
458
+ }
459
+ }
460
+ }
461
+ return index;
462
+ }
463
+ }
464
+ // declare type MaterialTextureCallback = (ctx: WriterContext, map: TemporaryMaterial, index: number) => void;
465
+ // class TemporaryMaterial implements IExtensionWriter {
466
+ // private readonly document: Document;
467
+ // private readonly material: Material;
468
+ // private readonly tex: Texture;
469
+ // private readonly callbacks: MaterialTextureCallback[] = [];
470
+ // constructor(doc: Document, tex: Texture, name: string) {
471
+ // console.log("Create material", name);
472
+ // this.document = doc;
473
+ // this.tex = tex;
474
+ // this.material = this.document
475
+ // .createMaterial(name)
476
+ // .setBaseColorTexture(tex);
477
+ // }
478
+ // add(cb: MaterialTextureCallback): TemporaryMaterial {
479
+ // this.callbacks.push(cb);
480
+ // return this;
481
+ // }
482
+ // write(context: WriterContext): void {
483
+ // const matIndex = context.materialIndexMap.get(this.material);
484
+ // if (matIndex !== undefined && matIndex !== -1) {
485
+ // console.log("Remove material", this.material.getName(), matIndex);
486
+ // context.jsonDoc.json.materials?.splice(matIndex, 1);
487
+ // if (this.callbacks.length > 0) {
488
+ // const texInfo = this.material.getBaseColorTextureInfo();
489
+ // if (texInfo) {
490
+ // const def = context.textureInfoDefMap.get(texInfo);
491
+ // if (def) {
492
+ // const index = def.index;
493
+ // for (const cb of this.callbacks) {
494
+ // cb(context, this, index);
495
+ // }
496
+ // }
497
+ // }
498
+ // }
499
+ // }
500
+ // }
501
+ // }
502
+ // json pointer -------------------------------------------------------------
503
+ // abstract class PointerProperty extends ExtensionProperty {
504
+ // abstract setPointer(texture: Texture, name: string);
505
+ // }
506
+ // const { R, G, B, A } = TextureChannel;
507
+ // function createPointerProperty(EXTENSION_NAME: string, doc: Document): PointerProperty {
508
+ // class TexturePointerPointerProperty extends ExtensionProperty<any> {
509
+ // static EXTENSION_NAME = EXTENSION_NAME;
510
+ // readonly extensionName: string = EXTENSION_NAME;
511
+ // parentTypes: PropertyType[] = ALL_PROPERTY_TYPES;
512
+ // propertyType = "NEEDLE_pointer_property";
513
+ // init() { }
514
+ // public setPointer(texture: Texture | null, name: string): this {
515
+ // console.log("SET REF", name);
516
+ // return this.setRef(name, texture, { channels: R | G | B | A });
517
+ // }
518
+ // }
519
+ // return new TexturePointerPointerProperty(doc.getGraph());
520
+ // }
521
+ class JsonPointerHandler {
522
+ extension_name;
523
+ document;
524
+ map = new Map();
525
+ debug = false;
526
+ breakPoint = false;
527
+ VRM_0_MaterialIndexRegex = new RegExp(/materialProperties\/(?<material_index>\d+)\//g);
528
+ constructor(document, extension_name) {
529
+ this.document = document;
530
+ this.extension_name = extension_name;
531
+ }
532
+ read(context, extProp, obj, prop, key, fullPath) {
533
+ const value = key !== undefined ? obj[key] : obj;
534
+ // the pointer path contains the full path from the root object to the json pointer
535
+ if (key !== undefined) {
536
+ fullPath ??= "";
537
+ fullPath += "/" + key;
538
+ if (this.breakPoint === "pointer:read") {
539
+ debugger;
540
+ }
541
+ }
542
+ // if(this.debug && key === "colliders") debugger;
543
+ switch (typeof value) {
544
+ case 'object':
545
+ if (value !== null && value !== undefined) {
546
+ if (Array.isArray(value)) {
547
+ for (let i = 0; i < value.length; i++) {
548
+ this.read(context, extProp, value, prop, i.toString(), fullPath);
549
+ }
550
+ }
551
+ else {
552
+ for (const key of Object.keys(value)) {
553
+ this.read(context, extProp, value, prop, key, fullPath);
554
+ }
555
+ }
556
+ }
557
+ break;
558
+ case "string":
559
+ if (value && key) {
560
+ let prefix = "/textures/";
561
+ let isTexturePointer = value.startsWith(prefix);
562
+ if (!isTexturePointer) {
563
+ // backwards compatibility for old format where it didnt start with /textures/ but with textures/
564
+ prefix = "textures/";
565
+ isTexturePointer = value.startsWith(prefix);
566
+ }
567
+ if (isTexturePointer) {
568
+ if (this.debug)
569
+ console.log("Read pointer", value);
570
+ let pointer = this.map.get(value);
571
+ if (!pointer) {
572
+ const length = prefix.length;
573
+ const index = parseInt(value.substring(length));
574
+ const path = value.substring(0, length);
575
+ const definition = context.jsonDoc.json.textures[index];
576
+ const imageIndex = findSource(definition);
577
+ if (this.debug)
578
+ console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
579
+ const object = tryGetInArray(context.textures, imageIndex);
580
+ if (object) {
581
+ const res = new TextureIndexResolver(this.document, object, definition);
582
+ res.debug = this.debug;
583
+ res.init(context); // < init here otherwise the fields defined in the TexturePointer will be undefined (this caused e.g. finding the previous texture filter to fail)
584
+ pointer = new JsonPointer(path, res, value);
585
+ pointer.debug = this.debug;
586
+ this.map.set(value, pointer);
587
+ // Assume all texture slots are being used. The referenceName is the json pointer right now - usually this is e.g. `baseColorTexture` (the name ends up in the slots array)
588
+ const referenceName = value;
589
+ // const slots = listTextureSlots(object);
590
+ prop.setReference(referenceName, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
591
+ // make sure there's a reference to the buffer to avoid dedup or the buffer being removed because it MIGHT not be used by any node
592
+ // TODO: how do we get the buffer that the texture/image is currently using - here we just take the first buffer but this might not be correct
593
+ const buffer = this.document.getRoot().listBuffers()[0];
594
+ if (buffer)
595
+ prop.setReference("buffer", buffer);
596
+ }
597
+ else {
598
+ console.warn("Texture not found", fullPath, value);
599
+ }
600
+ }
601
+ if (pointer) {
602
+ if (this.debug)
603
+ console.log("> Register pointer", this.extension_name + "/" + fullPath + " : " + value);
604
+ pointer?.register(obj, key, fullPath);
605
+ }
606
+ else
607
+ console.warn("WARN: failed registering pointer", fullPath, value);
608
+ }
609
+ if (value.startsWith("/materials/")) {
610
+ if (this.debug)
611
+ console.log("Read pointer", value);
612
+ const index = parseInt(value.substring("/materials/".length));
613
+ const object = context.materials[index];
614
+ if (object) {
615
+ // set a reference to the material to avoid dedup or the material being removed because it MIGHT not be used by any node
616
+ // BUT a component is referencing the material so we don't want it ever to be removed
617
+ // see https://github.com/donmccurdy/glTF-Transform/issues/980#issuecomment-1585748202
618
+ if (prop) {
619
+ prop.setReference(value, object);
620
+ }
621
+ let pointer = this.map.get(value);
622
+ if (!pointer) {
623
+ const definition = context.jsonDoc.json.materials[index];
624
+ const res = new MaterialIndexResolver(this.document, object, definition);
625
+ res.debug = this.debug;
626
+ res.init(context); // < init here otherwise the fields defined in the TexturePointer will be undefined (this caused e.g. finding the previous texture filter to fail)
627
+ pointer = new JsonPointer("/materials/", res, value);
628
+ pointer.debug = this.debug;
629
+ this.map.set(value, pointer);
630
+ prop.setReference(value, object);
631
+ }
632
+ pointer?.register(obj, key, fullPath);
633
+ }
634
+ }
635
+ if (value.startsWith("/animations/")) {
636
+ if (this.debug)
637
+ console.log("Read pointer", value);
638
+ const index = parseInt(value.substring("/animations/".length));
639
+ const object = context.animations[index];
640
+ if (object) {
641
+ let pointer = this.map.get(value);
642
+ if (!pointer) {
643
+ // TODO: resolve animations + we need to set the reference properly so we can use prune. See https://linear.app/needle/issue/NE-4982
644
+ // const definition = context.jsonDoc.json.animations![index];
645
+ // const res = new AnimationIndexResolver(this.document, object, definition);
646
+ // res.debug = this.debug;
647
+ // res.init(context);
648
+ // pointer = new JsonPointer("/animations/", res, value);
649
+ // pointer.debug = this.debug;
650
+ // this.map.set(value, pointer);
651
+ prop.setReference(value, object);
652
+ }
653
+ pointer?.register(obj, key, fullPath);
654
+ }
655
+ }
656
+ if (value.startsWith("/meshes/")) {
657
+ if (this.debug)
658
+ console.log("Read pointer", value);
659
+ const index = parseInt(value.substring("/meshes/".length));
660
+ const object = context.meshes[index];
661
+ if (object) {
662
+ let pointer = this.map.get(value);
663
+ if (!pointer) {
664
+ const definition = context.jsonDoc.json.meshes[index];
665
+ const res = new MeshIndexResolver(this.document, object, definition, prop);
666
+ res.debug = this.debug;
667
+ res.init(context);
668
+ pointer = new JsonPointer("/meshes/", res, value);
669
+ pointer.debug = this.debug;
670
+ this.map.set(value, pointer);
671
+ prop.setReference(value, object);
672
+ }
673
+ pointer?.register(obj, key, fullPath);
674
+ }
675
+ }
676
+ }
677
+ break;
678
+ case 'number':
679
+ if (key === "node" || key === "mesh"
680
+ // VRM 1.0 spring bone center https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_springBone-1.0/schema/VRMC_springBone.spring.schema.json
681
+ || fullPath.match(/springs\/\d+\/center/g)) {
682
+ const object = context.nodes[value];
683
+ if (object) {
684
+ const res = new NodeIndexResolver(this.document, object, context.jsonDoc.json.nodes[value]);
685
+ res.debug = this.debug;
686
+ res.init(context);
687
+ const dictKey = `/${key}/${value}`;
688
+ let pointer = this.map.get(dictKey);
689
+ if (!pointer) {
690
+ pointer = new JsonPointer(null, res, value);
691
+ }
692
+ pointer.debug = this.debug;
693
+ pointer.register(obj, key, fullPath);
694
+ this.map.set(dictKey, pointer);
695
+ prop.setReference(dictKey, object);
696
+ }
697
+ }
698
+ else if (
699
+ // key texture is matching for example for VRM meta/texture (AvatarSample_A)
700
+ key === "texture"
701
+ // VRM 0.0 material properties
702
+ || fullPath.match(/materialProperties\/(?<material_index>\d+)\/textureProperties/g)
703
+ // VRM 1.0 meta
704
+ || fullPath.match(/meta\/thumbnailImage/g)
705
+ // NEEDLE_materials_mtlx where the root extension has { textures: [0, 1, 2, ...] }
706
+ || fullPath.match(/^\/textures\/\d{1,}$/)) {
707
+ const index = value;
708
+ // e.g. in meta.texture for VRM avatars it seems like we can have negative values
709
+ if (index < 0) {
710
+ console.warn(`WARN: texture index is negative ${index} at ${fullPath}`);
711
+ return;
712
+ }
713
+ const definition = context.jsonDoc.json.textures[index];
714
+ if (!definition) {
715
+ console.warn(`WARN: texture definition with index ${index} not found or does not exist. Set in \"${fullPath}\"`);
716
+ return;
717
+ }
718
+ const imageIndex = findSource(definition);
719
+ if (this.debug)
720
+ console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
721
+ const object = tryGetInArray(context.textures, imageIndex);
722
+ if (object) {
723
+ if (this.debug)
724
+ console.log("Found texture index at " + fullPath + ": " + value);
725
+ const res = new TextureIndexResolver(this.document, object, definition);
726
+ res.debug = this.debug;
727
+ res.init(context);
728
+ const dictKey = `/textures/${value}`;
729
+ let pointer = this.map.get(dictKey);
730
+ if (!pointer) {
731
+ pointer = new JsonPointer(null, res, value);
732
+ }
733
+ pointer.debug = this.debug;
734
+ pointer.register(obj, key, fullPath);
735
+ this.map.set(dictKey, pointer);
736
+ prop.setReference(dictKey, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
737
+ }
738
+ const materialMatch = this.VRM_0_MaterialIndexRegex.exec(fullPath);
739
+ if (materialMatch) {
740
+ const materialIndex = parseInt(materialMatch.groups.material_index);
741
+ const material = context.materials[materialIndex];
742
+ prop.setReference(`/materials/${materialIndex}`, material);
743
+ // const res = new MaterialIndexResolver(this.document, material, context.jsonDoc.json.materials![materialIndex]);
744
+ // res.debug = this.debug;
745
+ // res.init(context);
746
+ // const dictKey = `/materials/${materialIndex}`
747
+ // let pointer = this.map.get(dictKey);
748
+ // if (!pointer) {
749
+ // pointer = new JsonPointer(null, res, value);
750
+ // }
751
+ // pointer.debug = this.debug;
752
+ // pointer.register(obj, key, fullPath);
753
+ // this.map.set(dictKey, pointer);
754
+ }
755
+ }
756
+ break;
757
+ default:
758
+ break;
759
+ }
760
+ }
761
+ resolveAndWrite(context, step) {
762
+ this.map?.forEach(ptr => {
763
+ if (ptr.resolve(context, step))
764
+ ptr.write();
765
+ });
766
+ }
767
+ }
768
+ class JsonPointer {
769
+ debug = false;
770
+ resolver;
771
+ pointerBase;
772
+ oldPointer;
773
+ constructor(pointerBase, resolver, oldPointer) {
774
+ this.pointerBase = pointerBase;
775
+ this.resolver = resolver;
776
+ this.oldPointer = oldPointer;
777
+ }
778
+ _objects = [];
779
+ _props = [];
780
+ _debugInfo = [];
781
+ register(obj, prop, fullpath) {
782
+ this._objects.push(obj);
783
+ this._props.push(prop);
784
+ this._debugInfo.push(fullpath);
785
+ }
786
+ newPointer = undefined;
787
+ resolve(context, step) {
788
+ // If the pointer was already resolved, we don't need to do it again
789
+ if (this.newPointer !== undefined) {
790
+ return true;
791
+ }
792
+ //@ts-ignore
793
+ this.resolver.debug = this.debug;
794
+ const index = this.resolver.resolve(context);
795
+ if (index != undefined) {
796
+ if (this.pointerBase != null) {
797
+ this.newPointer = this.pointerBase + index;
798
+ }
799
+ else {
800
+ this.newPointer = index;
801
+ }
802
+ return true;
803
+ }
804
+ else {
805
+ // If the pointer could not be resolved during write we need to log an error
806
+ if (step === "write") {
807
+ console.log(`ERR: 🧨 FAILED to resolve pointer \"${this.oldPointer}\" used by ${this._debugInfo.join(", ")}`);
808
+ if (this.debug)
809
+ debugger;
810
+ }
811
+ }
812
+ return false;
813
+ }
814
+ _didWrite = false;
815
+ write() {
816
+ if (this._didWrite)
817
+ return;
818
+ this._didWrite = false;
819
+ for (let i = 0; i < this._objects.length; i++) {
820
+ const obj = this._objects[i];
821
+ const prop = this._props[i];
822
+ if (this.debug) {
823
+ console.log(`< Resolved pointer ${this._debugInfo[i]}: ${this.oldPointer} → ${this.newPointer}`);
824
+ }
825
+ obj[prop] = this.newPointer;
826
+ }
827
+ }
828
+ }
829
+ class PointerResolver {
830
+ document;
831
+ /* the object originally being referenced by the pointer */
832
+ inputObject;
833
+ /* the original json definition */
834
+ inputDefinition;
835
+ /** The graph property this pointer belongs to */
836
+ property;
837
+ constructor(doc, obj, def, prop) {
838
+ this.document = doc;
839
+ this.inputObject = obj;
840
+ this.inputDefinition = def;
841
+ this.property = prop;
842
+ }
843
+ debug = false;
844
+ }
845
+ class TextureIndexResolver extends PointerResolver {
846
+ _debugName;
847
+ _samplerDef;
848
+ init(context) {
849
+ this._debugName = this.inputDefinition.name ?? this.inputDefinition.source?.toString() ?? "unknown";
850
+ const samplerIndex = this.inputDefinition.sampler;
851
+ if (samplerIndex !== undefined) {
852
+ this._samplerDef = context.jsonDoc.json.samplers[samplerIndex];
853
+ }
854
+ if (this.debug)
855
+ console.log(this._debugName, this._samplerDef);
856
+ }
857
+ resolve(context) {
858
+ const imgIndex = context.imageIndexMap.get(this.inputObject);
859
+ if (imgIndex === undefined) {
860
+ console.error("ERR: image is missing", imgIndex);
861
+ return -1;
862
+ }
863
+ // ensure we have a texture array
864
+ context.jsonDoc.json.textures = context.jsonDoc.json.textures || [];
865
+ // try find the texture that points to the same image source
866
+ for (let i = 0; i < context.jsonDoc.json.textures.length; i++) {
867
+ const textureDef = context.jsonDoc.json.textures[i];
868
+ if (findSource(textureDef) === imgIndex) {
869
+ if (textureDef.sampler === undefined) {
870
+ textureDef.sampler = this.findOrCreateSampler(context.jsonDoc.json);
871
+ }
872
+ if (this.debug)
873
+ console.log("Found texture def at " + i, "using image", imgIndex, textureDef);
874
+ return i;
875
+ }
876
+ }
877
+ // the texture was not found - add it
878
+ // create a new texture def entry
879
+ if (this.debug)
880
+ console.log("Could not find texture, creating a new one: " + this._debugName);
881
+ const hasExtensionWithSource = this.inputDefinition.extensions && Object.keys(this.inputDefinition.extensions).some(key => this.inputDefinition.extensions[key]["source"] !== undefined);
882
+ const textureDef = {
883
+ source: hasExtensionWithSource ? undefined : imgIndex,
884
+ sampler: this.findOrCreateSampler(context.jsonDoc.json),
885
+ extensions: this.inputDefinition.extensions,
886
+ extras: this.inputDefinition.extras,
887
+ };
888
+ context.jsonDoc.json.textures.push(textureDef);
889
+ return context.jsonDoc.json.textures.length - 1;
890
+ }
891
+ findOrCreateSampler(json) {
892
+ json.samplers = json.samplers || [];
893
+ if (this._samplerDef === undefined) {
894
+ console.log("WARN: Creating default sampler", this._debugName);
895
+ // create a default sampler
896
+ this._samplerDef = {
897
+ magFilter: 9729,
898
+ minFilter: 9729,
899
+ wrapS: 33648,
900
+ wrapT: 33648
901
+ };
902
+ }
903
+ else {
904
+ for (let i = 0; i < json.samplers.length; i++) {
905
+ const sampler = json.samplers[i];
906
+ if (sampler.minFilter === this._samplerDef.minFilter &&
907
+ sampler.magFilter === this._samplerDef.magFilter &&
908
+ sampler.wrapS === this._samplerDef.wrapS &&
909
+ sampler.wrapT === this._samplerDef.wrapT) {
910
+ return i;
911
+ }
912
+ }
913
+ }
914
+ // append previous sampler
915
+ json.samplers.push(this._samplerDef);
916
+ return json.samplers.length - 1;
917
+ }
918
+ }
919
+ function findSource(texDef) {
920
+ if (texDef.source !== undefined) {
921
+ return texDef.source;
922
+ }
923
+ if (texDef.extensions) {
924
+ const keys = Object.keys(texDef.extensions);
925
+ for (let i = 0; i < keys.length; i++) {
926
+ const key = keys[i];
927
+ const ext = texDef.extensions[key];
928
+ if (ext["source"] !== undefined) {
929
+ return ext["source"];
930
+ }
931
+ }
932
+ }
933
+ return undefined;
934
+ }
935
+ class MaterialIndexResolver extends PointerResolver {
936
+ init(context) {
937
+ }
938
+ resolve(context) {
939
+ return context.materialIndexMap.get(this.inputObject);
940
+ }
941
+ }
942
+ class NodeIndexResolver extends PointerResolver {
943
+ init(context) {
944
+ }
945
+ resolve(context) {
946
+ return context.nodeIndexMap.get(this.inputObject);
947
+ }
948
+ }
949
+ class MeshIndexResolver extends PointerResolver {
950
+ init(context) {
951
+ }
952
+ resolve(context) {
953
+ let index = context.meshIndexMap.get(this.inputObject);
954
+ if (index >= 0) {
955
+ return index;
956
+ }
957
+ let obj = null;
958
+ if (this.inputObject.isDisposed()) {
959
+ // If the mesh was deduplicated we try to get the mesh from a parent node
960
+ // This is a HACK, we need to get the reference to the mesh this one was deduplicated with since the parent node potentially has many children
961
+ const parents = this.property?.listParents();
962
+ for (const parent of parents) {
963
+ if (parent instanceof Node) {
964
+ obj = parent.getMesh();
965
+ if (obj) {
966
+ this.document.getLogger().info(`INFO: resolved mesh \"${this.inputObject.getName()}\" from parent node \"${parent.getName()}\"`);
967
+ break;
968
+ }
969
+ }
970
+ }
971
+ if (obj.isDisposed()) {
972
+ console.warn("WARN: mesh is disposed", this.inputObject.getName());
973
+ }
974
+ }
975
+ if (!obj) {
976
+ return undefined;
977
+ }
978
+ return context.meshIndexMap.get(obj);
979
+ }
980
+ }