@needle-tools/gltf-build-pipeline 2.2.1-alpha.2 โ†’ 2.2.1-alpha.4

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 (104) hide show
  1. package/.temp/Go2.bin +0 -0
  2. package/.temp/textures/0_1.exr +0 -0
  3. package/.temp/textures/1_1.jpg +0 -0
  4. package/CHANGELOG.md +395 -394
  5. package/README.md +35 -35
  6. package/dist/cache/cache.d.ts +54 -54
  7. package/dist/cache/cache.js +257 -257
  8. package/dist/cache/index.d.ts +1 -1
  9. package/dist/cache/index.js +1 -1
  10. package/dist/cli/index.d.ts +2 -2
  11. package/dist/cli/index.js +154 -154
  12. package/dist/config/index.d.ts +53 -53
  13. package/dist/config/index.js +152 -152
  14. package/dist/constants.d.ts +5 -5
  15. package/dist/constants.js +5 -5
  16. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -24
  17. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -54
  18. package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -1
  19. package/dist/extensions/NEEDLE_compression_texture/index.js +1 -1
  20. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +26 -26
  21. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +85 -85
  22. package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -1
  23. package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -1
  24. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +27 -27
  25. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +897 -897
  26. package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -3
  27. package/dist/extensions/NEEDLE_opaque/index.js +28 -28
  28. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +48 -48
  29. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +69 -69
  30. package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -1
  31. package/dist/extensions/NEEDLE_progressive/index.js +1 -1
  32. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +23 -23
  33. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.js +41 -41
  34. package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +25 -25
  35. package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +67 -67
  36. package/dist/extensions/index.d.ts +7 -7
  37. package/dist/extensions/index.js +17 -17
  38. package/dist/extensions/utils.d.ts +6 -6
  39. package/dist/extensions/utils.js +72 -72
  40. package/dist/functions/weld.3.d.ts +76 -76
  41. package/dist/functions/weld.3.js +414 -414
  42. package/dist/index.d.ts +8 -8
  43. package/dist/index.js +8 -8
  44. package/dist/scripts/index.d.ts +2 -2
  45. package/dist/scripts/index.js +2 -2
  46. package/dist/scripts/pack-gltf.d.ts +16 -16
  47. package/dist/scripts/pack-gltf.js +120 -120
  48. package/dist/scripts/stats.d.ts +2 -2
  49. package/dist/scripts/stats.js +18 -18
  50. package/dist/transforms/index.d.ts +9 -9
  51. package/dist/transforms/index.js +9 -9
  52. package/dist/transforms/needle_animation_transform.d.ts +2 -2
  53. package/dist/transforms/needle_animation_transform.js +5 -5
  54. package/dist/transforms/needle_asset.d.ts +3 -3
  55. package/dist/transforms/needle_asset.js +35 -35
  56. package/dist/transforms/needle_common.d.ts +16 -16
  57. package/dist/transforms/needle_common.js +1 -1
  58. package/dist/transforms/needle_mesh_transform.d.ts +10 -10
  59. package/dist/transforms/needle_mesh_transform.js +140 -140
  60. package/dist/transforms/needle_progressive.d.ts +42 -42
  61. package/dist/transforms/needle_progressive.js +933 -921
  62. package/dist/transforms/needle_texture_transform.d.ts +13 -13
  63. package/dist/transforms/needle_texture_transform.js +202 -196
  64. package/dist/transforms/needle_toktx.d.ts +25 -25
  65. package/dist/transforms/needle_toktx.js +230 -230
  66. package/dist/transforms/needle_webp.d.ts +13 -13
  67. package/dist/transforms/needle_webp.js +37 -37
  68. package/dist/transforms/toktx.d.ts +52 -52
  69. package/dist/transforms/toktx.js +221 -221
  70. package/dist/transforms/toktx_types.d.ts +78 -78
  71. package/dist/transforms/toktx_types.js +62 -62
  72. package/dist/transforms/util.d.ts +21 -21
  73. package/dist/transforms/util.js +90 -90
  74. package/dist/utils/compression_utils.d.ts +2 -2
  75. package/dist/utils/compression_utils.js +6 -6
  76. package/dist/utils/fileutils.d.ts +6 -6
  77. package/dist/utils/fileutils.js +141 -141
  78. package/dist/utils/guid.d.ts +1 -1
  79. package/dist/utils/guid.js +5 -5
  80. package/dist/utils/index.d.ts +10 -9
  81. package/dist/utils/index.js +10 -9
  82. package/dist/utils/merge.d.ts +12 -12
  83. package/dist/utils/merge.js +31 -31
  84. package/dist/utils/mesh.d.ts +13 -13
  85. package/dist/utils/mesh.js +107 -107
  86. package/dist/utils/nodeio.d.ts +5 -5
  87. package/dist/utils/nodeio.js +14 -14
  88. package/dist/utils/stats.d.ts +54 -53
  89. package/dist/utils/stats.js +191 -166
  90. package/dist/utils/texture.d.ts +2 -2
  91. package/dist/utils/texture.js +8 -8
  92. package/dist/utils/validate.d.ts +5 -0
  93. package/dist/utils/validate.js +16 -0
  94. package/dist/utils/version.d.ts +1 -1
  95. package/dist/utils/version.js +5 -5
  96. package/package.json +77 -76
  97. package/scripts/clear-caches.mjs +3 -3
  98. package/scripts/defaults.mjs +11 -11
  99. package/scripts/fileutils.mjs +5 -5
  100. package/scripts/index.mjs +41 -41
  101. package/scripts/limit-caches.mjs +3 -3
  102. package/scripts/make-progressive.mjs +16 -16
  103. package/scripts/pack-gltf.mjs +107 -107
  104. package/tsconfig.json +19 -19
@@ -1,897 +1,897 @@
1
- import { Extension, ExtensionProperty, PropertyType, TextureChannel } from '@gltf-transform/core';
2
- import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
3
- const ALL_PROPERTY_TYPES = [];
4
- for (const key in PropertyType) {
5
- ALL_PROPERTY_TYPES.push(PropertyType[key]);
6
- }
7
- export let currentGenerator = "";
8
- // an opaque extension can be used to pass data through the gltf transform process
9
- // it should also be able to re-write/fix json pointers after the transformation which might have shuffled them around
10
- export function createOpaqueExtension(name, types, opts) {
11
- if (!Array.isArray(types))
12
- types = [types];
13
- const EXTENSION_NAME = name;
14
- const ExtensionPropertyTypes = ALL_PROPERTY_TYPES;
15
- const debugLog = opts?.debug ?? false;
16
- class NEEDLE_opaque extends Extension {
17
- extensionName = EXTENSION_NAME;
18
- static EXTENSION_NAME = EXTENSION_NAME;
19
- prewriteTypes = [PropertyType.MATERIAL];
20
- // private roots: Array<OpaqueProperty> = [];
21
- jsonPointerHandler;
22
- read(context) {
23
- if (opts?.breakpoint === true)
24
- debugger;
25
- currentGenerator = context.jsonDoc.json.asset?.generator;
26
- if (debugLog)
27
- console.log("READ " + EXTENSION_NAME);
28
- this.jsonPointerHandler = new JsonPointerHandler(this.document, EXTENSION_NAME);
29
- this.jsonPointerHandler.debug = debugLog;
30
- this.jsonPointerHandler.breakPoint = opts?.breakpoint;
31
- for (const type of types) {
32
- switch (type) {
33
- case PropertyType.ACCESSOR:
34
- this.onRead(context, "accessors", "accessorIndexMap");
35
- break;
36
- case PropertyType.ANIMATION:
37
- this.onRead(context, "animations", "animationIndexMap");
38
- break;
39
- case PropertyType.ANIMATION_CHANNEL:
40
- if (EXTENSION_NAME === "KHR_animation_pointer") {
41
- context.jsonDoc.json.animations?.forEach((animDef, animIndex) => {
42
- const anim = context.animations[animIndex];
43
- const channels = anim.listChannels();
44
- animDef.channels?.forEach((channel, channelIndex) => {
45
- const jsonTarget = channel.target;
46
- const khr_animation = jsonTarget.extensions && jsonTarget.extensions[EXTENSION_NAME];
47
- if (!khr_animation)
48
- return;
49
- const pointer = khr_animation.pointer;
50
- if (pointer) {
51
- const sections = pointer.split("/");
52
- const nodeIndex = parseInt(sections[2]);
53
- const targetName = sections[3];
54
- if (targetName === "scale" || targetName === "translation" || targetName === "rotation") {
55
- channels[channelIndex].setTargetNode(context.nodes[nodeIndex]);
56
- channels[channelIndex].setTargetPath(targetName);
57
- }
58
- }
59
- const existing = anim.getExtension(EXTENSION_NAME);
60
- if (debugLog)
61
- console.log(">>>", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
62
- if (existing) {
63
- assignChannels(existing);
64
- return;
65
- }
66
- const prop = this.createExtension();
67
- // there seems no way (yet?) to add extensions to channel.targets (?)
68
- // https://github.com/donmccurdy/glTF-Transform/discussions/613
69
- // so we add it to an object by keys to the animation itself and read it back in WRITE
70
- const khr_animation_data = {};
71
- khr_animation_data[channelIndex] = khr_animation;
72
- anim.setExtension(EXTENSION_NAME, prop);
73
- assignChannels(prop);
74
- function assignChannels(prop) {
75
- prop.add(ctx => {
76
- const animations = ctx.jsonDoc.json.animations;
77
- if (!animations)
78
- return;
79
- const animDef = animations[animIndex];
80
- const channel = animDef?.channels[channelIndex];
81
- if (channel) {
82
- if (debugLog)
83
- console.log("<<<", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
84
- channel.target = jsonTarget;
85
- }
86
- else if (debugLog)
87
- console.log("WARN: ", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, "not found");
88
- });
89
- }
90
- ;
91
- });
92
- });
93
- }
94
- break;
95
- case PropertyType.ANIMATION_SAMPLER:
96
- break;
97
- case PropertyType.BUFFER:
98
- this.onRead(context, "buffers", "bufferIndexMap");
99
- break;
100
- case PropertyType.CAMERA:
101
- this.onRead(context, "cameras", "cameraIndexMap");
102
- break;
103
- case PropertyType.MATERIAL:
104
- this.onRead(context, "materials", "materialIndexMap");
105
- break;
106
- case PropertyType.TEXTURE:
107
- context.jsonDoc.json.textures?.forEach((textureDefinition, index) => {
108
- if (!textureDefinition.extensions)
109
- return;
110
- const ext = textureDefinition.extensions[EXTENSION_NAME];
111
- if (!ext)
112
- return;
113
- // TODO: textures and textureDefinition are not in the same order?
114
- // see https://github.com/donmccurdy/glTF-Transform/blob/0e99c426b8ef41536228fe860ab8329be5d1d50f/packages/core/src/io/reader.ts#L132
115
- // textures are in image order
116
- // TODO: we need to use the image index to find the texture
117
- let texture = getTexture(context, textureDefinition);
118
- if (!texture) {
119
- console.log("WARN: could potentially not find gltf transform texture for " + index);
120
- texture = context.textures[index];
121
- }
122
- const prop = this.createExtension();
123
- this.jsonPointerHandler.read(context, texture, ext, prop);
124
- texture.setExtension(EXTENSION_NAME, prop);
125
- const handler = new TextureHandler(this.document, textureDefinition, EXTENSION_NAME, ext, debugLog);
126
- prop.add(handler);
127
- });
128
- break;
129
- case PropertyType.NODE:
130
- this.onRead(context, "nodes", "nodeIndexMap");
131
- break;
132
- case PropertyType.MESH:
133
- this.onRead(context, "meshes", "meshIndexMap");
134
- break;
135
- case PropertyType.PRIMITIVE:
136
- context.jsonDoc.json.meshes?.forEach((meshDefinition, meshIndex) => {
137
- const mesh = context.meshes[meshIndex];
138
- const primitives = mesh.listPrimitives();
139
- meshDefinition.primitives?.forEach((primitiveDefinition, primitiveIndex) => {
140
- if (!primitiveDefinition.extensions)
141
- return;
142
- const ext = primitiveDefinition.extensions[EXTENSION_NAME];
143
- if (!ext)
144
- return;
145
- const primitive = primitives[primitiveIndex];
146
- const prop = this.createExtension();
147
- this.jsonPointerHandler.read(context, primitive, ext, prop);
148
- primitive.setExtension(EXTENSION_NAME, prop);
149
- // this is invoked on write
150
- prop.add(ctx => {
151
- const newMeshIndex = ctx.meshIndexMap.get(mesh);
152
- const newMesh = ctx.jsonDoc.json.meshes[newMeshIndex];
153
- const newPrimitive = newMesh.primitives[primitiveIndex];
154
- newPrimitive.extensions = newPrimitive.extensions || {};
155
- newPrimitive.extensions[EXTENSION_NAME] = ext;
156
- });
157
- });
158
- });
159
- break;
160
- case PropertyType.ROOT:
161
- const sourceArray = context.jsonDoc.json.extensions;
162
- if (!sourceArray)
163
- continue;
164
- const ext = sourceArray[EXTENSION_NAME];
165
- if (ext) {
166
- this.lightmapWorkaround(context);
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
- prop.add(ctx => {
172
- ctx.jsonDoc.json.extensions = ctx.jsonDoc.json.extensions || {};
173
- ctx.jsonDoc.json.extensions[EXTENSION_NAME] = ext;
174
- });
175
- }
176
- break;
177
- }
178
- }
179
- return this;
180
- }
181
- // this is temporarely here to prevent compression on textures named Lightmap
182
- // until we have better control over the compression process
183
- lightmapWorkaround(context) {
184
- const textureDefinitions = context.jsonDoc.json.textures;
185
- if (!textureDefinitions)
186
- return;
187
- const textures = this.document.getRoot().listTextures();
188
- for (let i = 0; i < textures.length; i++) {
189
- const tex = textures[i];
190
- const texDef = textureDefinitions[i];
191
- const name = texDef?.name;
192
- if (name?.startsWith("Lightmap") && tex.getMimeType() !== "image/exr") {
193
- if (debugLog)
194
- console.log("Found lightmap texture", name, tex.getMimeType(), EXTENSION_NAME);
195
- tex.setMimeType("image/lightmap");
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(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
- id;
523
- document;
524
- map = new Map();
525
- debug = false;
526
- breakPoint = false;
527
- constructor(document, id) {
528
- this.document = document;
529
- this.id = id;
530
- }
531
- read(context, extProp, obj, prop, key, fullPath) {
532
- const value = key !== undefined ? obj[key] : obj;
533
- // the pointer path contains the full path from the root object to the json pointer
534
- if (key !== undefined) {
535
- fullPath ??= "";
536
- fullPath += "/" + key;
537
- if (this.breakPoint === "pointer:read") {
538
- debugger;
539
- }
540
- }
541
- // if(this.debug && key === "colliders") debugger;
542
- switch (typeof value) {
543
- case 'object':
544
- if (value !== null && value !== undefined) {
545
- for (const key of Object.keys(value)) {
546
- this.read(context, extProp, value, prop, key, fullPath);
547
- }
548
- }
549
- break;
550
- case "string":
551
- if (value && key) {
552
- let prefix = "/textures/";
553
- let isTexturePointer = value.startsWith(prefix);
554
- if (!isTexturePointer) {
555
- // backwards compatibility for old format where it didnt start with /textures/ but with textures/
556
- prefix = "textures/";
557
- isTexturePointer = value.startsWith(prefix);
558
- }
559
- if (isTexturePointer) {
560
- if (this.debug)
561
- console.log("Read pointer", value);
562
- let pointer = this.map.get(value);
563
- if (!pointer) {
564
- const length = prefix.length;
565
- const index = parseInt(value.substring(length));
566
- const path = value.substring(0, length);
567
- const definition = context.jsonDoc.json.textures[index];
568
- const imageIndex = findSource(definition);
569
- if (this.debug)
570
- console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
571
- const object = tryGetInArray(context.textures, imageIndex);
572
- if (object) {
573
- const res = new TextureIndexResolver(this.document, object, definition);
574
- res.debug = this.debug;
575
- 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)
576
- pointer = new JsonPointer(path, res, value);
577
- pointer.debug = this.debug;
578
- this.map.set(value, pointer);
579
- // 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)
580
- const referenceName = value;
581
- // const slots = listTextureSlots(object);
582
- prop.setReference(referenceName, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
583
- // 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
584
- // 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
585
- const buffer = this.document.getRoot().listBuffers()[0];
586
- if (buffer)
587
- prop.setReference("buffer", buffer);
588
- }
589
- else {
590
- console.warn("Texture not found", fullPath, value);
591
- }
592
- }
593
- if (pointer) {
594
- if (this.debug)
595
- console.log("> Register pointer", this.id + "/" + fullPath + " : " + value);
596
- pointer?.register(obj, key, fullPath);
597
- }
598
- else
599
- console.warn("WARN: failed registering pointer", fullPath, value);
600
- }
601
- if (value.startsWith("/materials/")) {
602
- if (this.debug)
603
- console.log("Read pointer", value);
604
- const index = parseInt(value.substring("/materials/".length));
605
- const object = context.materials[index];
606
- if (object) {
607
- // set a reference to the material to avoid dedup or the material being removed because it MIGHT not be used by any node
608
- // BUT a component is referencing the material so we don't want it ever to be removed
609
- // see https://github.com/donmccurdy/glTF-Transform/issues/980#issuecomment-1585748202
610
- if (prop) {
611
- prop.setReference(value, object);
612
- }
613
- let pointer = this.map.get(value);
614
- if (!pointer) {
615
- const definition = context.jsonDoc.json.materials[index];
616
- const res = new MaterialIndexResolver(this.document, object, definition);
617
- res.debug = this.debug;
618
- 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)
619
- pointer = new JsonPointer("/materials/", res, value);
620
- pointer.debug = this.debug;
621
- this.map.set(value, pointer);
622
- prop.setReference(value, object);
623
- }
624
- pointer?.register(obj, key, fullPath);
625
- }
626
- }
627
- if (value.startsWith("/animations/")) {
628
- if (this.debug)
629
- console.log("Read pointer", value);
630
- const index = parseInt(value.substring("/animations/".length));
631
- const object = context.animations[index];
632
- if (object) {
633
- let pointer = this.map.get(value);
634
- if (!pointer) {
635
- // TODO: resolve animations + we need to set the reference properly so we can use prune. See https://linear.app/needle/issue/NE-4982
636
- // const definition = context.jsonDoc.json.animations![index];
637
- // const res = new AnimationIndexResolver(this.document, object, definition);
638
- // res.debug = this.debug;
639
- // res.init(context);
640
- // pointer = new JsonPointer("/animations/", res, value);
641
- // pointer.debug = this.debug;
642
- // this.map.set(value, pointer);
643
- prop.setReference(value, object);
644
- }
645
- pointer?.register(obj, key, fullPath);
646
- }
647
- }
648
- }
649
- break;
650
- case 'number':
651
- if (key === "node" || key === "mesh"
652
- // 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
653
- || fullPath.match(/springs\/\d+\/center/g)) {
654
- const object = context.nodes[value];
655
- if (object) {
656
- const res = new NodeIndexResolver(this.document, object, context.jsonDoc.json.nodes[value]);
657
- res.debug = this.debug;
658
- res.init(context);
659
- const dictKey = `/${key}/${value}`;
660
- let pointer = this.map.get(dictKey);
661
- if (!pointer) {
662
- pointer = new JsonPointer(null, res, value);
663
- }
664
- pointer.debug = this.debug;
665
- pointer.register(obj, key, fullPath);
666
- this.map.set(dictKey, pointer);
667
- prop.setReference(dictKey, object);
668
- }
669
- }
670
- else if (
671
- // key texture is matching for example for VRM meta/texture (AvatarSample_A)
672
- key === "texture"
673
- // VRM 0.0 material properties
674
- || fullPath.match(/materialProperties\/\d+\/textureProperties/g)
675
- // VRM 1.0 meta
676
- || fullPath.match(/meta\/thumbnailImage/g)) {
677
- const index = value;
678
- // e.g. in meta.texture for VRM avatars it seems like we can have negative values
679
- if (index < 0) {
680
- console.warn(`WARN: texture index is negative ${index} at ${fullPath}`);
681
- return;
682
- }
683
- const definition = context.jsonDoc.json.textures[index];
684
- if (!definition) {
685
- console.warn(`WARN: texture definition with index ${index} not found or does not exist. Set in \"${fullPath}\"`);
686
- return;
687
- }
688
- const imageIndex = findSource(definition);
689
- if (this.debug)
690
- console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
691
- const object = tryGetInArray(context.textures, imageIndex);
692
- if (object) {
693
- if (this.debug)
694
- console.log("Found texture index at " + fullPath + ": " + value);
695
- const res = new TextureIndexResolver(this.document, object, definition);
696
- res.debug = this.debug;
697
- res.init(context);
698
- const dictKey = `/textures/${value}`;
699
- let pointer = this.map.get(dictKey);
700
- if (!pointer) {
701
- pointer = new JsonPointer(null, res, value);
702
- }
703
- pointer.debug = this.debug;
704
- pointer.register(obj, key, fullPath);
705
- this.map.set(dictKey, pointer);
706
- prop.setReference(dictKey, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
707
- }
708
- }
709
- break;
710
- default:
711
- break;
712
- }
713
- }
714
- resolveAndWrite(context, step) {
715
- this.map?.forEach(ptr => {
716
- if (ptr.resolve(context, step))
717
- ptr.write();
718
- });
719
- }
720
- }
721
- class JsonPointer {
722
- debug = false;
723
- resolver;
724
- pointerBase;
725
- oldPointer;
726
- constructor(pointerBase, resolver, oldPointer) {
727
- this.pointerBase = pointerBase;
728
- this.resolver = resolver;
729
- this.oldPointer = oldPointer;
730
- }
731
- _objects = [];
732
- _props = [];
733
- _debugInfo = [];
734
- register(obj, prop, fullpath) {
735
- this._objects.push(obj);
736
- this._props.push(prop);
737
- this._debugInfo.push(fullpath);
738
- }
739
- newPointer = undefined;
740
- resolve(context, step) {
741
- // If the pointer was already resolved, we don't need to do it again
742
- if (this.newPointer !== undefined) {
743
- return true;
744
- }
745
- //@ts-ignore
746
- this.resolver.debug = this.debug;
747
- const index = this.resolver.resolve(context);
748
- if (index != undefined) {
749
- if (this.pointerBase != null) {
750
- this.newPointer = this.pointerBase + index;
751
- }
752
- else {
753
- this.newPointer = index;
754
- }
755
- if (this.debug)
756
- console.log(`RESOLVED pointer ${this.oldPointer} to โ†’ ${this.newPointer}`);
757
- return true;
758
- }
759
- else {
760
- // If the pointer could not be resolved during write we need to log an error
761
- if (step === "write")
762
- console.log("ERR: ๐Ÿงจ FAILED to resolve pointer", this.pointerBase, ` used by ${this._debugInfo.join(", ")}`);
763
- }
764
- return false;
765
- }
766
- _didWrite = false;
767
- write() {
768
- if (this._didWrite)
769
- return;
770
- this._didWrite = false;
771
- for (let i = 0; i < this._objects.length; i++) {
772
- const obj = this._objects[i];
773
- const prop = this._props[i];
774
- if (this.debug) {
775
- console.log(`< Resolved pointer ${this._debugInfo[i]}: ${this.newPointer}`);
776
- }
777
- obj[prop] = this.newPointer;
778
- }
779
- }
780
- }
781
- class PointerResolver {
782
- document;
783
- /* the object originally being referenced by the pointer */
784
- inputObject;
785
- /* the original json definition */
786
- inputDefinition;
787
- constructor(doc, obj, def) {
788
- this.document = doc;
789
- this.inputObject = obj;
790
- this.inputDefinition = def;
791
- }
792
- debug = false;
793
- }
794
- class TextureIndexResolver extends PointerResolver {
795
- _debugName;
796
- _samplerDef;
797
- init(context) {
798
- this._debugName = this.inputDefinition.name ?? this.inputDefinition.source?.toString() ?? "unknown";
799
- const samplerIndex = this.inputDefinition.sampler;
800
- if (samplerIndex !== undefined) {
801
- this._samplerDef = context.jsonDoc.json.samplers[samplerIndex];
802
- }
803
- if (this.debug)
804
- console.log(this._debugName, this._samplerDef);
805
- }
806
- resolve(context) {
807
- const imgIndex = context.imageIndexMap.get(this.inputObject);
808
- if (imgIndex === undefined) {
809
- console.error("ERR: image is missing", imgIndex);
810
- return -1;
811
- }
812
- // ensure we have a texture array
813
- context.jsonDoc.json.textures = context.jsonDoc.json.textures || [];
814
- // try find the texture that points to the same image source
815
- for (let i = 0; i < context.jsonDoc.json.textures.length; i++) {
816
- const textureDef = context.jsonDoc.json.textures[i];
817
- if (findSource(textureDef) === imgIndex) {
818
- if (textureDef.sampler === undefined) {
819
- textureDef.sampler = this.findOrCreateSampler(context.jsonDoc.json);
820
- }
821
- if (this.debug)
822
- console.log("Found texture def at " + i, "using image", imgIndex, textureDef);
823
- return i;
824
- }
825
- }
826
- // the texture was not found - add it
827
- // create a new texture def entry
828
- if (this.debug)
829
- console.log("Could not find texture, creating a new one: " + this._debugName);
830
- const hasExtensionWithSource = this.inputDefinition.extensions && Object.keys(this.inputDefinition.extensions).some(key => this.inputDefinition.extensions[key]["source"] !== undefined);
831
- const textureDef = {
832
- source: hasExtensionWithSource ? undefined : imgIndex,
833
- sampler: this.findOrCreateSampler(context.jsonDoc.json),
834
- extensions: this.inputDefinition.extensions,
835
- extras: this.inputDefinition.extras,
836
- };
837
- context.jsonDoc.json.textures.push(textureDef);
838
- return context.jsonDoc.json.textures.length - 1;
839
- }
840
- findOrCreateSampler(json) {
841
- json.samplers = json.samplers || [];
842
- if (this._samplerDef === undefined) {
843
- console.log("WARN: Creating default sampler", this._debugName);
844
- // create a default sampler
845
- this._samplerDef = {
846
- magFilter: 9729,
847
- minFilter: 9729,
848
- wrapS: 33648,
849
- wrapT: 33648
850
- };
851
- }
852
- else {
853
- for (let i = 0; i < json.samplers.length; i++) {
854
- const sampler = json.samplers[i];
855
- if (sampler.minFilter === this._samplerDef.minFilter &&
856
- sampler.magFilter === this._samplerDef.magFilter &&
857
- sampler.wrapS === this._samplerDef.wrapS &&
858
- sampler.wrapT === this._samplerDef.wrapT) {
859
- return i;
860
- }
861
- }
862
- }
863
- // append previous sampler
864
- json.samplers.push(this._samplerDef);
865
- return json.samplers.length - 1;
866
- }
867
- }
868
- function findSource(texDef) {
869
- if (texDef.source !== undefined) {
870
- return texDef.source;
871
- }
872
- if (texDef.extensions) {
873
- const keys = Object.keys(texDef.extensions);
874
- for (let i = 0; i < keys.length; i++) {
875
- const key = keys[i];
876
- const ext = texDef.extensions[key];
877
- if (ext["source"] !== undefined) {
878
- return ext["source"];
879
- }
880
- }
881
- }
882
- return undefined;
883
- }
884
- class MaterialIndexResolver extends PointerResolver {
885
- init(context) {
886
- }
887
- resolve(context) {
888
- return context.materialIndexMap.get(this.inputObject);
889
- }
890
- }
891
- class NodeIndexResolver extends PointerResolver {
892
- init(context) {
893
- }
894
- resolve(context) {
895
- return context.nodeIndexMap.get(this.inputObject);
896
- }
897
- }
1
+ import { Extension, ExtensionProperty, PropertyType, TextureChannel } from '@gltf-transform/core';
2
+ import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
3
+ const ALL_PROPERTY_TYPES = [];
4
+ for (const key in PropertyType) {
5
+ ALL_PROPERTY_TYPES.push(PropertyType[key]);
6
+ }
7
+ export let currentGenerator = "";
8
+ // an opaque extension can be used to pass data through the gltf transform process
9
+ // it should also be able to re-write/fix json pointers after the transformation which might have shuffled them around
10
+ export function createOpaqueExtension(name, types, opts) {
11
+ if (!Array.isArray(types))
12
+ types = [types];
13
+ const EXTENSION_NAME = name;
14
+ const ExtensionPropertyTypes = ALL_PROPERTY_TYPES;
15
+ const debugLog = opts?.debug ?? false;
16
+ class NEEDLE_opaque extends Extension {
17
+ extensionName = EXTENSION_NAME;
18
+ static EXTENSION_NAME = EXTENSION_NAME;
19
+ prewriteTypes = [PropertyType.MATERIAL];
20
+ // private roots: Array<OpaqueProperty> = [];
21
+ jsonPointerHandler;
22
+ read(context) {
23
+ if (opts?.breakpoint === true)
24
+ debugger;
25
+ currentGenerator = context.jsonDoc.json.asset?.generator;
26
+ if (debugLog)
27
+ console.log("READ " + EXTENSION_NAME);
28
+ this.jsonPointerHandler = new JsonPointerHandler(this.document, EXTENSION_NAME);
29
+ this.jsonPointerHandler.debug = debugLog;
30
+ this.jsonPointerHandler.breakPoint = opts?.breakpoint;
31
+ for (const type of types) {
32
+ switch (type) {
33
+ case PropertyType.ACCESSOR:
34
+ this.onRead(context, "accessors", "accessorIndexMap");
35
+ break;
36
+ case PropertyType.ANIMATION:
37
+ this.onRead(context, "animations", "animationIndexMap");
38
+ break;
39
+ case PropertyType.ANIMATION_CHANNEL:
40
+ if (EXTENSION_NAME === "KHR_animation_pointer") {
41
+ context.jsonDoc.json.animations?.forEach((animDef, animIndex) => {
42
+ const anim = context.animations[animIndex];
43
+ const channels = anim.listChannels();
44
+ animDef.channels?.forEach((channel, channelIndex) => {
45
+ const jsonTarget = channel.target;
46
+ const khr_animation = jsonTarget.extensions && jsonTarget.extensions[EXTENSION_NAME];
47
+ if (!khr_animation)
48
+ return;
49
+ const pointer = khr_animation.pointer;
50
+ if (pointer) {
51
+ const sections = pointer.split("/");
52
+ const nodeIndex = parseInt(sections[2]);
53
+ const targetName = sections[3];
54
+ if (targetName === "scale" || targetName === "translation" || targetName === "rotation") {
55
+ channels[channelIndex].setTargetNode(context.nodes[nodeIndex]);
56
+ channels[channelIndex].setTargetPath(targetName);
57
+ }
58
+ }
59
+ const existing = anim.getExtension(EXTENSION_NAME);
60
+ if (debugLog)
61
+ console.log(">>>", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
62
+ if (existing) {
63
+ assignChannels(existing);
64
+ return;
65
+ }
66
+ const prop = this.createExtension();
67
+ // there seems no way (yet?) to add extensions to channel.targets (?)
68
+ // https://github.com/donmccurdy/glTF-Transform/discussions/613
69
+ // so we add it to an object by keys to the animation itself and read it back in WRITE
70
+ const khr_animation_data = {};
71
+ khr_animation_data[channelIndex] = khr_animation;
72
+ anim.setExtension(EXTENSION_NAME, prop);
73
+ assignChannels(prop);
74
+ function assignChannels(prop) {
75
+ prop.add(ctx => {
76
+ const animations = ctx.jsonDoc.json.animations;
77
+ if (!animations)
78
+ return;
79
+ const animDef = animations[animIndex];
80
+ const channel = animDef?.channels[channelIndex];
81
+ if (channel) {
82
+ if (debugLog)
83
+ console.log("<<<", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
84
+ channel.target = jsonTarget;
85
+ }
86
+ else if (debugLog)
87
+ console.log("WARN: ", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, "not found");
88
+ });
89
+ }
90
+ ;
91
+ });
92
+ });
93
+ }
94
+ break;
95
+ case PropertyType.ANIMATION_SAMPLER:
96
+ break;
97
+ case PropertyType.BUFFER:
98
+ this.onRead(context, "buffers", "bufferIndexMap");
99
+ break;
100
+ case PropertyType.CAMERA:
101
+ this.onRead(context, "cameras", "cameraIndexMap");
102
+ break;
103
+ case PropertyType.MATERIAL:
104
+ this.onRead(context, "materials", "materialIndexMap");
105
+ break;
106
+ case PropertyType.TEXTURE:
107
+ context.jsonDoc.json.textures?.forEach((textureDefinition, index) => {
108
+ if (!textureDefinition.extensions)
109
+ return;
110
+ const ext = textureDefinition.extensions[EXTENSION_NAME];
111
+ if (!ext)
112
+ return;
113
+ // TODO: textures and textureDefinition are not in the same order?
114
+ // see https://github.com/donmccurdy/glTF-Transform/blob/0e99c426b8ef41536228fe860ab8329be5d1d50f/packages/core/src/io/reader.ts#L132
115
+ // textures are in image order
116
+ // TODO: we need to use the image index to find the texture
117
+ let texture = getTexture(context, textureDefinition);
118
+ if (!texture) {
119
+ console.log("WARN: could potentially not find gltf transform texture for " + index);
120
+ texture = context.textures[index];
121
+ }
122
+ const prop = this.createExtension();
123
+ this.jsonPointerHandler.read(context, texture, ext, prop);
124
+ texture.setExtension(EXTENSION_NAME, prop);
125
+ const handler = new TextureHandler(this.document, textureDefinition, EXTENSION_NAME, ext, debugLog);
126
+ prop.add(handler);
127
+ });
128
+ break;
129
+ case PropertyType.NODE:
130
+ this.onRead(context, "nodes", "nodeIndexMap");
131
+ break;
132
+ case PropertyType.MESH:
133
+ this.onRead(context, "meshes", "meshIndexMap");
134
+ break;
135
+ case PropertyType.PRIMITIVE:
136
+ context.jsonDoc.json.meshes?.forEach((meshDefinition, meshIndex) => {
137
+ const mesh = context.meshes[meshIndex];
138
+ const primitives = mesh.listPrimitives();
139
+ meshDefinition.primitives?.forEach((primitiveDefinition, primitiveIndex) => {
140
+ if (!primitiveDefinition.extensions)
141
+ return;
142
+ const ext = primitiveDefinition.extensions[EXTENSION_NAME];
143
+ if (!ext)
144
+ return;
145
+ const primitive = primitives[primitiveIndex];
146
+ const prop = this.createExtension();
147
+ this.jsonPointerHandler.read(context, primitive, ext, prop);
148
+ primitive.setExtension(EXTENSION_NAME, prop);
149
+ // this is invoked on write
150
+ prop.add(ctx => {
151
+ const newMeshIndex = ctx.meshIndexMap.get(mesh);
152
+ const newMesh = ctx.jsonDoc.json.meshes[newMeshIndex];
153
+ const newPrimitive = newMesh.primitives[primitiveIndex];
154
+ newPrimitive.extensions = newPrimitive.extensions || {};
155
+ newPrimitive.extensions[EXTENSION_NAME] = ext;
156
+ });
157
+ });
158
+ });
159
+ break;
160
+ case PropertyType.ROOT:
161
+ const sourceArray = context.jsonDoc.json.extensions;
162
+ if (!sourceArray)
163
+ continue;
164
+ const ext = sourceArray[EXTENSION_NAME];
165
+ if (ext) {
166
+ this.lightmapWorkaround(context);
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
+ prop.add(ctx => {
172
+ ctx.jsonDoc.json.extensions = ctx.jsonDoc.json.extensions || {};
173
+ ctx.jsonDoc.json.extensions[EXTENSION_NAME] = ext;
174
+ });
175
+ }
176
+ break;
177
+ }
178
+ }
179
+ return this;
180
+ }
181
+ // this is temporarely here to prevent compression on textures named Lightmap
182
+ // until we have better control over the compression process
183
+ lightmapWorkaround(context) {
184
+ const textureDefinitions = context.jsonDoc.json.textures;
185
+ if (!textureDefinitions)
186
+ return;
187
+ const textures = this.document.getRoot().listTextures();
188
+ for (let i = 0; i < textures.length; i++) {
189
+ const tex = textures[i];
190
+ const texDef = textureDefinitions[i];
191
+ const name = texDef?.name;
192
+ if (name?.startsWith("Lightmap") && tex.getMimeType() !== "image/exr") {
193
+ if (debugLog)
194
+ console.log("Found lightmap texture", name, tex.getMimeType(), EXTENSION_NAME);
195
+ tex.setMimeType("image/lightmap");
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(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
+ id;
523
+ document;
524
+ map = new Map();
525
+ debug = false;
526
+ breakPoint = false;
527
+ constructor(document, id) {
528
+ this.document = document;
529
+ this.id = id;
530
+ }
531
+ read(context, extProp, obj, prop, key, fullPath) {
532
+ const value = key !== undefined ? obj[key] : obj;
533
+ // the pointer path contains the full path from the root object to the json pointer
534
+ if (key !== undefined) {
535
+ fullPath ??= "";
536
+ fullPath += "/" + key;
537
+ if (this.breakPoint === "pointer:read") {
538
+ debugger;
539
+ }
540
+ }
541
+ // if(this.debug && key === "colliders") debugger;
542
+ switch (typeof value) {
543
+ case 'object':
544
+ if (value !== null && value !== undefined) {
545
+ for (const key of Object.keys(value)) {
546
+ this.read(context, extProp, value, prop, key, fullPath);
547
+ }
548
+ }
549
+ break;
550
+ case "string":
551
+ if (value && key) {
552
+ let prefix = "/textures/";
553
+ let isTexturePointer = value.startsWith(prefix);
554
+ if (!isTexturePointer) {
555
+ // backwards compatibility for old format where it didnt start with /textures/ but with textures/
556
+ prefix = "textures/";
557
+ isTexturePointer = value.startsWith(prefix);
558
+ }
559
+ if (isTexturePointer) {
560
+ if (this.debug)
561
+ console.log("Read pointer", value);
562
+ let pointer = this.map.get(value);
563
+ if (!pointer) {
564
+ const length = prefix.length;
565
+ const index = parseInt(value.substring(length));
566
+ const path = value.substring(0, length);
567
+ const definition = context.jsonDoc.json.textures[index];
568
+ const imageIndex = findSource(definition);
569
+ if (this.debug)
570
+ console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
571
+ const object = tryGetInArray(context.textures, imageIndex);
572
+ if (object) {
573
+ const res = new TextureIndexResolver(this.document, object, definition);
574
+ res.debug = this.debug;
575
+ 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)
576
+ pointer = new JsonPointer(path, res, value);
577
+ pointer.debug = this.debug;
578
+ this.map.set(value, pointer);
579
+ // 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)
580
+ const referenceName = value;
581
+ // const slots = listTextureSlots(object);
582
+ prop.setReference(referenceName, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
583
+ // 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
584
+ // 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
585
+ const buffer = this.document.getRoot().listBuffers()[0];
586
+ if (buffer)
587
+ prop.setReference("buffer", buffer);
588
+ }
589
+ else {
590
+ console.warn("Texture not found", fullPath, value);
591
+ }
592
+ }
593
+ if (pointer) {
594
+ if (this.debug)
595
+ console.log("> Register pointer", this.id + "/" + fullPath + " : " + value);
596
+ pointer?.register(obj, key, fullPath);
597
+ }
598
+ else
599
+ console.warn("WARN: failed registering pointer", fullPath, value);
600
+ }
601
+ if (value.startsWith("/materials/")) {
602
+ if (this.debug)
603
+ console.log("Read pointer", value);
604
+ const index = parseInt(value.substring("/materials/".length));
605
+ const object = context.materials[index];
606
+ if (object) {
607
+ // set a reference to the material to avoid dedup or the material being removed because it MIGHT not be used by any node
608
+ // BUT a component is referencing the material so we don't want it ever to be removed
609
+ // see https://github.com/donmccurdy/glTF-Transform/issues/980#issuecomment-1585748202
610
+ if (prop) {
611
+ prop.setReference(value, object);
612
+ }
613
+ let pointer = this.map.get(value);
614
+ if (!pointer) {
615
+ const definition = context.jsonDoc.json.materials[index];
616
+ const res = new MaterialIndexResolver(this.document, object, definition);
617
+ res.debug = this.debug;
618
+ 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)
619
+ pointer = new JsonPointer("/materials/", res, value);
620
+ pointer.debug = this.debug;
621
+ this.map.set(value, pointer);
622
+ prop.setReference(value, object);
623
+ }
624
+ pointer?.register(obj, key, fullPath);
625
+ }
626
+ }
627
+ if (value.startsWith("/animations/")) {
628
+ if (this.debug)
629
+ console.log("Read pointer", value);
630
+ const index = parseInt(value.substring("/animations/".length));
631
+ const object = context.animations[index];
632
+ if (object) {
633
+ let pointer = this.map.get(value);
634
+ if (!pointer) {
635
+ // TODO: resolve animations + we need to set the reference properly so we can use prune. See https://linear.app/needle/issue/NE-4982
636
+ // const definition = context.jsonDoc.json.animations![index];
637
+ // const res = new AnimationIndexResolver(this.document, object, definition);
638
+ // res.debug = this.debug;
639
+ // res.init(context);
640
+ // pointer = new JsonPointer("/animations/", res, value);
641
+ // pointer.debug = this.debug;
642
+ // this.map.set(value, pointer);
643
+ prop.setReference(value, object);
644
+ }
645
+ pointer?.register(obj, key, fullPath);
646
+ }
647
+ }
648
+ }
649
+ break;
650
+ case 'number':
651
+ if (key === "node" || key === "mesh"
652
+ // 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
653
+ || fullPath.match(/springs\/\d+\/center/g)) {
654
+ const object = context.nodes[value];
655
+ if (object) {
656
+ const res = new NodeIndexResolver(this.document, object, context.jsonDoc.json.nodes[value]);
657
+ res.debug = this.debug;
658
+ res.init(context);
659
+ const dictKey = `/${key}/${value}`;
660
+ let pointer = this.map.get(dictKey);
661
+ if (!pointer) {
662
+ pointer = new JsonPointer(null, res, value);
663
+ }
664
+ pointer.debug = this.debug;
665
+ pointer.register(obj, key, fullPath);
666
+ this.map.set(dictKey, pointer);
667
+ prop.setReference(dictKey, object);
668
+ }
669
+ }
670
+ else if (
671
+ // key texture is matching for example for VRM meta/texture (AvatarSample_A)
672
+ key === "texture"
673
+ // VRM 0.0 material properties
674
+ || fullPath.match(/materialProperties\/\d+\/textureProperties/g)
675
+ // VRM 1.0 meta
676
+ || fullPath.match(/meta\/thumbnailImage/g)) {
677
+ const index = value;
678
+ // e.g. in meta.texture for VRM avatars it seems like we can have negative values
679
+ if (index < 0) {
680
+ console.warn(`WARN: texture index is negative ${index} at ${fullPath}`);
681
+ return;
682
+ }
683
+ const definition = context.jsonDoc.json.textures[index];
684
+ if (!definition) {
685
+ console.warn(`WARN: texture definition with index ${index} not found or does not exist. Set in \"${fullPath}\"`);
686
+ return;
687
+ }
688
+ const imageIndex = findSource(definition);
689
+ if (this.debug)
690
+ console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
691
+ const object = tryGetInArray(context.textures, imageIndex);
692
+ if (object) {
693
+ if (this.debug)
694
+ console.log("Found texture index at " + fullPath + ": " + value);
695
+ const res = new TextureIndexResolver(this.document, object, definition);
696
+ res.debug = this.debug;
697
+ res.init(context);
698
+ const dictKey = `/textures/${value}`;
699
+ let pointer = this.map.get(dictKey);
700
+ if (!pointer) {
701
+ pointer = new JsonPointer(null, res, value);
702
+ }
703
+ pointer.debug = this.debug;
704
+ pointer.register(obj, key, fullPath);
705
+ this.map.set(dictKey, pointer);
706
+ prop.setReference(dictKey, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
707
+ }
708
+ }
709
+ break;
710
+ default:
711
+ break;
712
+ }
713
+ }
714
+ resolveAndWrite(context, step) {
715
+ this.map?.forEach(ptr => {
716
+ if (ptr.resolve(context, step))
717
+ ptr.write();
718
+ });
719
+ }
720
+ }
721
+ class JsonPointer {
722
+ debug = false;
723
+ resolver;
724
+ pointerBase;
725
+ oldPointer;
726
+ constructor(pointerBase, resolver, oldPointer) {
727
+ this.pointerBase = pointerBase;
728
+ this.resolver = resolver;
729
+ this.oldPointer = oldPointer;
730
+ }
731
+ _objects = [];
732
+ _props = [];
733
+ _debugInfo = [];
734
+ register(obj, prop, fullpath) {
735
+ this._objects.push(obj);
736
+ this._props.push(prop);
737
+ this._debugInfo.push(fullpath);
738
+ }
739
+ newPointer = undefined;
740
+ resolve(context, step) {
741
+ // If the pointer was already resolved, we don't need to do it again
742
+ if (this.newPointer !== undefined) {
743
+ return true;
744
+ }
745
+ //@ts-ignore
746
+ this.resolver.debug = this.debug;
747
+ const index = this.resolver.resolve(context);
748
+ if (index != undefined) {
749
+ if (this.pointerBase != null) {
750
+ this.newPointer = this.pointerBase + index;
751
+ }
752
+ else {
753
+ this.newPointer = index;
754
+ }
755
+ if (this.debug)
756
+ console.log(`RESOLVED pointer ${this.oldPointer} to โ†’ ${this.newPointer}`);
757
+ return true;
758
+ }
759
+ else {
760
+ // If the pointer could not be resolved during write we need to log an error
761
+ if (step === "write")
762
+ console.log("ERR: ๐Ÿงจ FAILED to resolve pointer", this.pointerBase, ` used by ${this._debugInfo.join(", ")}`);
763
+ }
764
+ return false;
765
+ }
766
+ _didWrite = false;
767
+ write() {
768
+ if (this._didWrite)
769
+ return;
770
+ this._didWrite = false;
771
+ for (let i = 0; i < this._objects.length; i++) {
772
+ const obj = this._objects[i];
773
+ const prop = this._props[i];
774
+ if (this.debug) {
775
+ console.log(`< Resolved pointer ${this._debugInfo[i]}: ${this.newPointer}`);
776
+ }
777
+ obj[prop] = this.newPointer;
778
+ }
779
+ }
780
+ }
781
+ class PointerResolver {
782
+ document;
783
+ /* the object originally being referenced by the pointer */
784
+ inputObject;
785
+ /* the original json definition */
786
+ inputDefinition;
787
+ constructor(doc, obj, def) {
788
+ this.document = doc;
789
+ this.inputObject = obj;
790
+ this.inputDefinition = def;
791
+ }
792
+ debug = false;
793
+ }
794
+ class TextureIndexResolver extends PointerResolver {
795
+ _debugName;
796
+ _samplerDef;
797
+ init(context) {
798
+ this._debugName = this.inputDefinition.name ?? this.inputDefinition.source?.toString() ?? "unknown";
799
+ const samplerIndex = this.inputDefinition.sampler;
800
+ if (samplerIndex !== undefined) {
801
+ this._samplerDef = context.jsonDoc.json.samplers[samplerIndex];
802
+ }
803
+ if (this.debug)
804
+ console.log(this._debugName, this._samplerDef);
805
+ }
806
+ resolve(context) {
807
+ const imgIndex = context.imageIndexMap.get(this.inputObject);
808
+ if (imgIndex === undefined) {
809
+ console.error("ERR: image is missing", imgIndex);
810
+ return -1;
811
+ }
812
+ // ensure we have a texture array
813
+ context.jsonDoc.json.textures = context.jsonDoc.json.textures || [];
814
+ // try find the texture that points to the same image source
815
+ for (let i = 0; i < context.jsonDoc.json.textures.length; i++) {
816
+ const textureDef = context.jsonDoc.json.textures[i];
817
+ if (findSource(textureDef) === imgIndex) {
818
+ if (textureDef.sampler === undefined) {
819
+ textureDef.sampler = this.findOrCreateSampler(context.jsonDoc.json);
820
+ }
821
+ if (this.debug)
822
+ console.log("Found texture def at " + i, "using image", imgIndex, textureDef);
823
+ return i;
824
+ }
825
+ }
826
+ // the texture was not found - add it
827
+ // create a new texture def entry
828
+ if (this.debug)
829
+ console.log("Could not find texture, creating a new one: " + this._debugName);
830
+ const hasExtensionWithSource = this.inputDefinition.extensions && Object.keys(this.inputDefinition.extensions).some(key => this.inputDefinition.extensions[key]["source"] !== undefined);
831
+ const textureDef = {
832
+ source: hasExtensionWithSource ? undefined : imgIndex,
833
+ sampler: this.findOrCreateSampler(context.jsonDoc.json),
834
+ extensions: this.inputDefinition.extensions,
835
+ extras: this.inputDefinition.extras,
836
+ };
837
+ context.jsonDoc.json.textures.push(textureDef);
838
+ return context.jsonDoc.json.textures.length - 1;
839
+ }
840
+ findOrCreateSampler(json) {
841
+ json.samplers = json.samplers || [];
842
+ if (this._samplerDef === undefined) {
843
+ console.log("WARN: Creating default sampler", this._debugName);
844
+ // create a default sampler
845
+ this._samplerDef = {
846
+ magFilter: 9729,
847
+ minFilter: 9729,
848
+ wrapS: 33648,
849
+ wrapT: 33648
850
+ };
851
+ }
852
+ else {
853
+ for (let i = 0; i < json.samplers.length; i++) {
854
+ const sampler = json.samplers[i];
855
+ if (sampler.minFilter === this._samplerDef.minFilter &&
856
+ sampler.magFilter === this._samplerDef.magFilter &&
857
+ sampler.wrapS === this._samplerDef.wrapS &&
858
+ sampler.wrapT === this._samplerDef.wrapT) {
859
+ return i;
860
+ }
861
+ }
862
+ }
863
+ // append previous sampler
864
+ json.samplers.push(this._samplerDef);
865
+ return json.samplers.length - 1;
866
+ }
867
+ }
868
+ function findSource(texDef) {
869
+ if (texDef.source !== undefined) {
870
+ return texDef.source;
871
+ }
872
+ if (texDef.extensions) {
873
+ const keys = Object.keys(texDef.extensions);
874
+ for (let i = 0; i < keys.length; i++) {
875
+ const key = keys[i];
876
+ const ext = texDef.extensions[key];
877
+ if (ext["source"] !== undefined) {
878
+ return ext["source"];
879
+ }
880
+ }
881
+ }
882
+ return undefined;
883
+ }
884
+ class MaterialIndexResolver extends PointerResolver {
885
+ init(context) {
886
+ }
887
+ resolve(context) {
888
+ return context.materialIndexMap.get(this.inputObject);
889
+ }
890
+ }
891
+ class NodeIndexResolver extends PointerResolver {
892
+ init(context) {
893
+ }
894
+ resolve(context) {
895
+ return context.nodeIndexMap.get(this.inputObject);
896
+ }
897
+ }