@needle-tools/gltf-build-pipeline 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/clear-caches.mjs +4 -0
  3. package/defaults.mjs +12 -0
  4. package/dist/cache/cache.d.ts +5 -0
  5. package/dist/cache/cache.js +82 -0
  6. package/dist/cache/index.d.ts +1 -0
  7. package/dist/cache/index.js +1 -0
  8. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -0
  9. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -0
  10. package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -0
  11. package/dist/extensions/NEEDLE_compression_texture/index.js +1 -0
  12. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +23 -0
  13. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +43 -0
  14. package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -0
  15. package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -0
  16. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +20 -0
  17. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +699 -0
  18. package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -0
  19. package/dist/extensions/NEEDLE_opaque/index.js +18 -0
  20. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +23 -0
  21. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +53 -0
  22. package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -0
  23. package/dist/extensions/NEEDLE_progressive/index.js +1 -0
  24. package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +21 -0
  25. package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +39 -0
  26. package/dist/extensions/index.d.ts +6 -0
  27. package/dist/extensions/index.js +16 -0
  28. package/dist/extensions/utils.d.ts +6 -0
  29. package/dist/extensions/utils.js +72 -0
  30. package/dist/functions/list-texture-slots.d.ts +1 -0
  31. package/dist/functions/list-texture-slots.js +35 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.js +3 -0
  34. package/dist/transforms/index.d.ts +9 -0
  35. package/dist/transforms/index.js +9 -0
  36. package/dist/transforms/needle_animation_transform.d.ts +2 -0
  37. package/dist/transforms/needle_animation_transform.js +5 -0
  38. package/dist/transforms/needle_asset.d.ts +2 -0
  39. package/dist/transforms/needle_asset.js +28 -0
  40. package/dist/transforms/needle_common.d.ts +11 -0
  41. package/dist/transforms/needle_common.js +1 -0
  42. package/dist/transforms/needle_mesh_transform.d.ts +3 -0
  43. package/dist/transforms/needle_mesh_transform.js +83 -0
  44. package/dist/transforms/needle_progressive.d.ts +34 -0
  45. package/dist/transforms/needle_progressive.js +264 -0
  46. package/dist/transforms/needle_simplify.d.ts +3 -0
  47. package/dist/transforms/needle_simplify.js +76 -0
  48. package/dist/transforms/needle_texture_transform.d.ts +5 -0
  49. package/dist/transforms/needle_texture_transform.js +123 -0
  50. package/dist/transforms/needle_toktx.d.ts +24 -0
  51. package/dist/transforms/needle_toktx.js +144 -0
  52. package/dist/transforms/needle_webp.d.ts +13 -0
  53. package/dist/transforms/needle_webp.js +53 -0
  54. package/dist/transforms/toktx.d.ts +77 -0
  55. package/dist/transforms/toktx.js +230 -0
  56. package/dist/transforms/types/squoosh.d.ts +31 -0
  57. package/dist/transforms/types/squoosh.js +33 -0
  58. package/dist/transforms/util.d.ts +21 -0
  59. package/dist/transforms/util.js +90 -0
  60. package/dist/utils/fileutils.d.ts +2 -0
  61. package/dist/utils/fileutils.js +95 -0
  62. package/dist/utils/index.d.ts +1 -0
  63. package/dist/utils/index.js +1 -0
  64. package/fileutils.mjs +6 -0
  65. package/index.mjs +33 -0
  66. package/make-progressive.mjs +11 -0
  67. package/pack-gltf.mjs +119 -0
  68. package/package.json +60 -0
  69. package/tsconfig.json +15 -0
@@ -0,0 +1,699 @@
1
+ import { Extension, ExtensionProperty, PropertyType } 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
+ // an opaque extension can be used to pass data through the gltf transform process
8
+ // it should also be able to re-write/fix json pointers after the transformation which might have shuffled them around
9
+ export function createOpaqueExtension(name, types, opts) {
10
+ if (!Array.isArray(types))
11
+ types = [types];
12
+ const EXTENSION_NAME = name;
13
+ const ExtensionPropertyTypes = ALL_PROPERTY_TYPES;
14
+ const debugLog = opts?.debug ?? false;
15
+ class NEEDLE_opaque extends Extension {
16
+ extensionName = EXTENSION_NAME;
17
+ static EXTENSION_NAME = EXTENSION_NAME;
18
+ // private roots: Array<OpaqueProperty> = [];
19
+ jsonPointerHandler;
20
+ read(context) {
21
+ if (debugLog)
22
+ console.log("READ " + EXTENSION_NAME);
23
+ this.jsonPointerHandler = new JsonPointerHandler(this.document, EXTENSION_NAME);
24
+ this.jsonPointerHandler.debug = debugLog;
25
+ for (const type of types) {
26
+ switch (type) {
27
+ case PropertyType.ACCESSOR:
28
+ this.onRead(context, "accessors", "accessorIndexMap");
29
+ break;
30
+ case PropertyType.ANIMATION:
31
+ this.onRead(context, "animations", "animationIndexMap");
32
+ break;
33
+ case PropertyType.ANIMATION_CHANNEL:
34
+ if (EXTENSION_NAME === "KHR_animation_pointer") {
35
+ context.jsonDoc.json.animations?.forEach((animDef, animIndex) => {
36
+ animDef.channels?.forEach((channel, channelIndex) => {
37
+ const jsonTarget = channel.target;
38
+ const khr_animation = jsonTarget.extensions && jsonTarget.extensions[EXTENSION_NAME];
39
+ if (!khr_animation)
40
+ return;
41
+ const anim = context.animations[animIndex];
42
+ const existing = anim.getExtension(EXTENSION_NAME);
43
+ if (debugLog)
44
+ console.log(">>>", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
45
+ if (existing) {
46
+ assignChannels(existing);
47
+ return;
48
+ }
49
+ const prop = this.createExtension();
50
+ // there seems no way (yet?) to add extensions to channel.targets (?)
51
+ // https://github.com/donmccurdy/glTF-Transform/discussions/613
52
+ // so we add it to an object by keys to the animation itself and read it back in WRITE
53
+ const khr_animation_data = {};
54
+ khr_animation_data[channelIndex] = khr_animation;
55
+ anim.setExtension(EXTENSION_NAME, prop);
56
+ assignChannels(prop);
57
+ function assignChannels(prop) {
58
+ prop.add(ctx => {
59
+ const animations = ctx.jsonDoc.json.animations;
60
+ if (!animations)
61
+ return;
62
+ const animDef = animations[animIndex];
63
+ const channel = animDef?.channels[channelIndex];
64
+ if (channel) {
65
+ if (debugLog)
66
+ console.log("<<<", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
67
+ channel.target = jsonTarget;
68
+ }
69
+ else if (debugLog)
70
+ console.log("WARN: ", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, "not found");
71
+ });
72
+ }
73
+ ;
74
+ });
75
+ });
76
+ }
77
+ break;
78
+ case PropertyType.ANIMATION_SAMPLER:
79
+ break;
80
+ case PropertyType.BUFFER:
81
+ this.onRead(context, "buffers", "bufferIndexMap");
82
+ break;
83
+ case PropertyType.CAMERA:
84
+ this.onRead(context, "cameras", "cameraIndexMap");
85
+ break;
86
+ case PropertyType.MATERIAL:
87
+ this.onRead(context, "materials", "materialIndexMap");
88
+ break;
89
+ case PropertyType.TEXTURE:
90
+ context.jsonDoc.json.textures?.forEach((textureDefinition, index) => {
91
+ if (!textureDefinition.extensions)
92
+ return;
93
+ const ext = textureDefinition.extensions[EXTENSION_NAME];
94
+ if (!ext)
95
+ return;
96
+ // TODO: textures and textureDefinition are not in the same order?
97
+ // see https://github.com/donmccurdy/glTF-Transform/blob/0e99c426b8ef41536228fe860ab8329be5d1d50f/packages/core/src/io/reader.ts#L132
98
+ // textures are in image order
99
+ // TODO: we need to use the image index to find the texture
100
+ let texture = getTexture(context, textureDefinition);
101
+ if (!texture) {
102
+ console.log("WARN: could potentially not find gltf transform texture for " + index);
103
+ texture = context.textures[index];
104
+ }
105
+ this.jsonPointerHandler.read(context, texture, ext);
106
+ const prop = this.createExtension();
107
+ texture.setExtension(EXTENSION_NAME, prop);
108
+ const handler = new TextureHandler(this.document, textureDefinition, EXTENSION_NAME, ext, debugLog);
109
+ prop.add(handler);
110
+ });
111
+ break;
112
+ case PropertyType.NODE:
113
+ this.onRead(context, "nodes", "nodeIndexMap");
114
+ break;
115
+ case PropertyType.MESH:
116
+ this.onRead(context, "meshes", "meshIndexMap");
117
+ break;
118
+ case PropertyType.PRIMITIVE:
119
+ context.jsonDoc.json.meshes?.forEach((meshDefinition, meshIndex) => {
120
+ const mesh = context.meshes[meshIndex];
121
+ const primitives = mesh.listPrimitives();
122
+ meshDefinition.primitives?.forEach((primitiveDefinition, primitiveIndex) => {
123
+ if (!primitiveDefinition.extensions)
124
+ return;
125
+ const ext = primitiveDefinition.extensions[EXTENSION_NAME];
126
+ if (!ext)
127
+ return;
128
+ const primitive = primitives[primitiveIndex];
129
+ this.jsonPointerHandler.read(context, primitive, ext);
130
+ const prop = this.createExtension();
131
+ primitive.setExtension(EXTENSION_NAME, prop);
132
+ // this is invoked on write
133
+ prop.add(ctx => {
134
+ const newMeshIndex = ctx.meshIndexMap.get(mesh);
135
+ const newMesh = ctx.jsonDoc.json.meshes[newMeshIndex];
136
+ const newPrimitive = newMesh.primitives[primitiveIndex];
137
+ newPrimitive.extensions = newPrimitive.extensions || {};
138
+ newPrimitive.extensions[EXTENSION_NAME] = ext;
139
+ });
140
+ });
141
+ });
142
+ break;
143
+ case PropertyType.ROOT:
144
+ const sourceArray = context.jsonDoc.json.extensions;
145
+ if (!sourceArray)
146
+ continue;
147
+ const ext = sourceArray[EXTENSION_NAME];
148
+ if (ext) {
149
+ this.lightmapWorkaround(context);
150
+ const root = this.document.getRoot();
151
+ this.jsonPointerHandler.read(context, root, ext);
152
+ const prop = this.createExtension();
153
+ root.setExtension(EXTENSION_NAME, prop);
154
+ prop.add(ctx => {
155
+ ctx.jsonDoc.json.extensions = ctx.jsonDoc.json.extensions || {};
156
+ ctx.jsonDoc.json.extensions[EXTENSION_NAME] = ext;
157
+ });
158
+ }
159
+ break;
160
+ }
161
+ }
162
+ return this;
163
+ }
164
+ // this is temporarely here to prevent compression on textures named Lightmap
165
+ // until we have better control over the compression process
166
+ lightmapWorkaround(context) {
167
+ const textureDefinitions = context.jsonDoc.json.textures;
168
+ if (!textureDefinitions)
169
+ return;
170
+ const textures = this.document.getRoot().listTextures();
171
+ for (let i = 0; i < textures.length; i++) {
172
+ const tex = textures[i];
173
+ const texDef = textureDefinitions[i];
174
+ const name = texDef?.name;
175
+ if (name?.startsWith("Lightmap") && tex.getMimeType() !== "image/exr") {
176
+ if (debugLog)
177
+ console.log("Found lightmap texture", name, tex.getMimeType(), EXTENSION_NAME);
178
+ tex.setMimeType("image/lightmap");
179
+ }
180
+ }
181
+ }
182
+ createExtension(name = '') {
183
+ return new OpaqueProperty(this.document.getGraph(), name);
184
+ ;
185
+ }
186
+ onRead(context, gltfField, map) {
187
+ if (gltfField === undefined)
188
+ return;
189
+ try {
190
+ const sourceArray = context.jsonDoc.json[gltfField];
191
+ const target = context[gltfField];
192
+ sourceArray?.forEach((def, index) => {
193
+ if (!def.extensions || !def.extensions[EXTENSION_NAME])
194
+ return;
195
+ const ext = def.extensions[EXTENSION_NAME];
196
+ if (debugLog)
197
+ console.log("READ", index, ext);
198
+ const prop = this.createExtension();
199
+ // attach the property
200
+ target[index].setExtension(EXTENSION_NAME, prop);
201
+ // capture all the json pointers in the extension
202
+ this.jsonPointerHandler.read(context, target[index], ext);
203
+ // make sure we write our extension back to the gltf
204
+ prop.add(new OpaqueWriter(map, gltfField, ext));
205
+ });
206
+ }
207
+ catch (error) {
208
+ console.error("ERR: " + error);
209
+ }
210
+ }
211
+ write(context) {
212
+ if (opts?.write === false) {
213
+ return this;
214
+ }
215
+ if (debugLog)
216
+ console.log("WRITE " + EXTENSION_NAME);
217
+ this.jsonPointerHandler.resolveAndWrite(context);
218
+ const root = this.document.getRoot();
219
+ // for (const rootExt of this.roots) {
220
+ // rootExt.onWrite(context, root);
221
+ // }
222
+ for (const type of types) {
223
+ switch (type) {
224
+ case PropertyType.ACCESSOR:
225
+ this.onWrite(context, () => root.listAccessors());
226
+ break;
227
+ case PropertyType.ANIMATION:
228
+ this.onWrite(context, () => root.listAnimations());
229
+ break;
230
+ case PropertyType.ANIMATION_CHANNEL:
231
+ break;
232
+ case PropertyType.ANIMATION_SAMPLER:
233
+ break;
234
+ case PropertyType.BUFFER:
235
+ this.onWrite(context, () => root.listBuffers());
236
+ break;
237
+ case PropertyType.CAMERA:
238
+ this.onWrite(context, () => root.listCameras());
239
+ break;
240
+ case PropertyType.MATERIAL:
241
+ this.onWrite(context, () => root.listMaterials());
242
+ break;
243
+ case PropertyType.TEXTURE:
244
+ this.onWrite(context, () => root.listTextures());
245
+ break;
246
+ case PropertyType.NODE:
247
+ this.onWrite(context, () => root.listNodes());
248
+ break;
249
+ case PropertyType.MESH:
250
+ this.onWrite(context, () => root.listMeshes());
251
+ break;
252
+ case PropertyType.PRIMITIVE:
253
+ const allPrimitives = root.listMeshes().flatMap(mesh => mesh.listPrimitives());
254
+ for (const primitive of allPrimitives) {
255
+ const ext = primitive.getExtension(EXTENSION_NAME);
256
+ if (ext instanceof OpaqueProperty) {
257
+ ext.onWrite(context, primitive);
258
+ }
259
+ }
260
+ break;
261
+ case PropertyType.ROOT:
262
+ const prop = root.getExtension(EXTENSION_NAME);
263
+ if (prop instanceof OpaqueProperty) {
264
+ prop.onWrite(context, root);
265
+ }
266
+ break;
267
+ }
268
+ }
269
+ return this;
270
+ }
271
+ onWrite(context, get) {
272
+ const arr = get();
273
+ if (!arr)
274
+ return;
275
+ for (const entry of arr) {
276
+ const ext = entry.getExtension(EXTENSION_NAME);
277
+ if (ext instanceof OpaqueProperty) {
278
+ ext.onWrite(context, entry);
279
+ }
280
+ }
281
+ ;
282
+ }
283
+ }
284
+ // function setExtension(ext: ExtensibleProperty, name, extensionProperty) {
285
+ // // for opaque extension we need to bypass the type system
286
+ // // because we generate the OpaqueProperty class at runtime and thus the method name for _validateParent is different when compiled
287
+ // // there's likely a better way to do this... as usual
288
+ // const pext = ext as any as { setRefMap: (mapName: string, key: string, value: any) => any }
289
+ // return pext.setRefMap('extensions', name, extensionProperty);
290
+ // }
291
+ class OpaqueProperty extends ExtensionProperty {
292
+ static EXTENSION_NAME = EXTENSION_NAME;
293
+ extensionName = typeof EXTENSION_NAME;
294
+ parentTypes = ExtensionPropertyTypes;
295
+ propertyType = "NEEDLE_opaque_property";
296
+ init() { }
297
+ writers = [];
298
+ add(writer) {
299
+ if (typeof writer === "function") {
300
+ writer = new GenericWriter(writer);
301
+ }
302
+ this.writers.push(writer);
303
+ }
304
+ onWrite(context, prop) {
305
+ for (const writer of this.writers) {
306
+ writer.write?.call(writer, context, prop);
307
+ }
308
+ }
309
+ getExtensionDefinition() {
310
+ const ext = {};
311
+ for (const writer of this.writers) {
312
+ if (writer.assignTo)
313
+ writer.assignTo(ext);
314
+ }
315
+ return ext;
316
+ }
317
+ }
318
+ // called after the transformation
319
+ // writes the extension json back to the correct object
320
+ class OpaqueWriter {
321
+ map;
322
+ gltfField;
323
+ ext;
324
+ assignTo(obj) {
325
+ Object.assign(obj, this.ext);
326
+ }
327
+ constructor(map, gltfField, ext) {
328
+ this.map = map;
329
+ this.gltfField = gltfField;
330
+ this.ext = ext;
331
+ }
332
+ write(context, prop) {
333
+ if (prop && this.gltfField !== undefined) {
334
+ const map = context[this.map];
335
+ const index = map.get(prop);
336
+ const result = tryGetInArray(context.jsonDoc.json[this.gltfField], index);
337
+ writeExtension(result, EXTENSION_NAME, this.ext);
338
+ if (debugLog) {
339
+ // console.log("WRITE", EXTENSION_NAME, "to", this.gltfField, "at index", index);
340
+ console.log(result);
341
+ }
342
+ }
343
+ }
344
+ }
345
+ return NEEDLE_opaque;
346
+ }
347
+ /** use to register a generic callback */
348
+ class GenericWriter {
349
+ fn;
350
+ constructor(fn) {
351
+ this.fn = fn;
352
+ }
353
+ write(ctx) {
354
+ this.fn(ctx);
355
+ }
356
+ }
357
+ class TextureHandler {
358
+ document;
359
+ textureDefinition;
360
+ index;
361
+ key;
362
+ ext;
363
+ debug = false;
364
+ assignTo(obj) {
365
+ Object.assign(obj, this.ext);
366
+ }
367
+ constructor(doc, def, key, ext, debug) {
368
+ this.document = doc;
369
+ this.textureDefinition = def;
370
+ this.key = key;
371
+ this.ext = ext;
372
+ this.debug = debug;
373
+ this.index = this.tryGetSourceIndex(this.textureDefinition);
374
+ if (debug)
375
+ console.log(def);
376
+ }
377
+ write(ctx) {
378
+ let foundTexture = false;
379
+ const textureDefs = ctx.jsonDoc.json.textures || [];
380
+ const imageIndex = this.index !== undefined ? this.index : this.tryGetSourceIndex(this.textureDefinition);
381
+ if (imageIndex === undefined) {
382
+ console.warn("WARN: no source index found for texture", this.key);
383
+ }
384
+ textureDefs?.forEach((textureDef, i) => {
385
+ if (foundTexture)
386
+ return;
387
+ const textureImageIndex = this.tryGetSourceIndex(textureDef);
388
+ if (imageIndex === textureImageIndex) {
389
+ if (!textureDef.extensions || !textureDef.extensions[this.key]) {
390
+ // assign texture extension again
391
+ textureDef.extensions = textureDef.extensions || {};
392
+ foundTexture = true;
393
+ if (this.debug)
394
+ console.log("Re-Assign extension:", this.key, "at", i);
395
+ const newExt = { ...this.ext };
396
+ if (newExt.source !== undefined)
397
+ newExt.source = imageIndex;
398
+ textureDef.extensions[this.key] = newExt;
399
+ }
400
+ }
401
+ });
402
+ if (foundTexture === false) {
403
+ console.warn("WARN: failed to re-assign extension:", this.key, this.index, this.tryGetSourceIndex(this.textureDefinition), textureDefs.length);
404
+ }
405
+ // this.document
406
+ // .getRoot()
407
+ // .listTextures()
408
+ // .forEach((texture, index) => {
409
+ // if (foundTexture) return;
410
+ // if (!texture.getExtension(this.key)) return;
411
+ // const imageIndex = ctx.imageIndexMap.get(texture);
412
+ // });
413
+ }
414
+ tryGetSourceIndex(def) {
415
+ let index = def.source;
416
+ if (index !== undefined)
417
+ return index;
418
+ // if a glb was already compressed the source index might be missing so we also need to search in extensions
419
+ if (def.extensions) {
420
+ for (const ext of Object.keys(def.extensions)) {
421
+ const extDef = def.extensions[ext];
422
+ if (extDef.source !== undefined) {
423
+ index = extDef.source;
424
+ break;
425
+ }
426
+ }
427
+ }
428
+ return index;
429
+ }
430
+ }
431
+ // declare type MaterialTextureCallback = (ctx: WriterContext, map: TemporaryMaterial, index: number) => void;
432
+ // class TemporaryMaterial implements IExtensionWriter {
433
+ // private readonly document: Document;
434
+ // private readonly material: Material;
435
+ // private readonly tex: Texture;
436
+ // private readonly callbacks: MaterialTextureCallback[] = [];
437
+ // constructor(doc: Document, tex: Texture, name: string) {
438
+ // console.log("Create material", name);
439
+ // this.document = doc;
440
+ // this.tex = tex;
441
+ // this.material = this.document
442
+ // .createMaterial(name)
443
+ // .setBaseColorTexture(tex);
444
+ // }
445
+ // add(cb: MaterialTextureCallback): TemporaryMaterial {
446
+ // this.callbacks.push(cb);
447
+ // return this;
448
+ // }
449
+ // write(context: WriterContext): void {
450
+ // const matIndex = context.materialIndexMap.get(this.material);
451
+ // if (matIndex !== undefined && matIndex !== -1) {
452
+ // console.log("Remove material", this.material.getName(), matIndex);
453
+ // context.jsonDoc.json.materials?.splice(matIndex, 1);
454
+ // if (this.callbacks.length > 0) {
455
+ // const texInfo = this.material.getBaseColorTextureInfo();
456
+ // if (texInfo) {
457
+ // const def = context.textureInfoDefMap.get(texInfo);
458
+ // if (def) {
459
+ // const index = def.index;
460
+ // for (const cb of this.callbacks) {
461
+ // cb(context, this, index);
462
+ // }
463
+ // }
464
+ // }
465
+ // }
466
+ // }
467
+ // }
468
+ // }
469
+ // json pointer -------------------------------------------------------------
470
+ // abstract class PointerProperty extends ExtensionProperty {
471
+ // abstract setPointer(texture: Texture, name: string);
472
+ // }
473
+ // const { R, G, B, A } = TextureChannel;
474
+ // function createPointerProperty(EXTENSION_NAME: string, doc: Document): PointerProperty {
475
+ // class TexturePointerPointerProperty extends ExtensionProperty<any> {
476
+ // static EXTENSION_NAME = EXTENSION_NAME;
477
+ // readonly extensionName: string = EXTENSION_NAME;
478
+ // parentTypes: PropertyType[] = ALL_PROPERTY_TYPES;
479
+ // propertyType = "NEEDLE_pointer_property";
480
+ // init() { }
481
+ // public setPointer(texture: Texture | null, name: string): this {
482
+ // console.log("SET REF", name);
483
+ // return this.setRef(name, texture, { channels: R | G | B | A });
484
+ // }
485
+ // }
486
+ // return new TexturePointerPointerProperty(doc.getGraph());
487
+ // }
488
+ class JsonPointerHandler {
489
+ id;
490
+ document;
491
+ map = new Map();
492
+ debug = false;
493
+ constructor(document, id) {
494
+ this.document = document;
495
+ this.id = id;
496
+ }
497
+ read(context, prop, obj, key, fullPath) {
498
+ const value = key !== undefined ? obj[key] : obj;
499
+ // the pointer path contains the full path from the root object to the json pointer
500
+ if (key !== undefined) {
501
+ fullPath ??= "";
502
+ fullPath += "/" + key;
503
+ }
504
+ switch (typeof value) {
505
+ case 'object':
506
+ if (value !== null && value !== undefined) {
507
+ for (const key of Object.keys(value)) {
508
+ this.read(context, prop, value, key, fullPath);
509
+ }
510
+ }
511
+ break;
512
+ case "string":
513
+ if (value && key) {
514
+ let prefix = "/textures/";
515
+ let isTexturePointer = value.startsWith(prefix);
516
+ if (!isTexturePointer) {
517
+ // backwards compatibility for old format where it didnt start with /textures/ but with textures/
518
+ prefix = "textures/";
519
+ isTexturePointer = value.startsWith(prefix);
520
+ }
521
+ if (isTexturePointer) {
522
+ if (this.debug)
523
+ console.log("Read pointer", value);
524
+ let pointer = this.map.get(value);
525
+ if (!pointer) {
526
+ const length = prefix.length;
527
+ const index = parseInt(value.substring(length));
528
+ const path = value.substring(0, length);
529
+ const definition = context.jsonDoc.json.textures[index];
530
+ const imageIndex = findSource(definition);
531
+ if (this.debug)
532
+ console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
533
+ const object = tryGetInArray(context.textures, imageIndex);
534
+ if (object) {
535
+ const res = new TexturePointerResolver(this.document, object, definition);
536
+ 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)
537
+ pointer = new JsonPointer(path, res);
538
+ pointer.debug = this.debug;
539
+ this.map.set(value, pointer);
540
+ }
541
+ else {
542
+ console.warn("Texture not found", fullPath, value);
543
+ }
544
+ }
545
+ if (pointer) {
546
+ if (this.debug)
547
+ console.log("> Register pointer", this.id + "/" + fullPath + " : " + value);
548
+ pointer?.register(obj, key);
549
+ }
550
+ else
551
+ console.warn("WARN: failed registering pointer", fullPath, value);
552
+ }
553
+ }
554
+ break;
555
+ }
556
+ }
557
+ resolveAndWrite(context) {
558
+ this.map?.forEach(ptr => {
559
+ ptr.resolve(context);
560
+ ptr.write();
561
+ });
562
+ }
563
+ }
564
+ class JsonPointer {
565
+ debug = false;
566
+ resolver;
567
+ pointerBase;
568
+ constructor(pointerBase, resolver) {
569
+ this.pointerBase = pointerBase;
570
+ this.resolver = resolver;
571
+ }
572
+ _objects = [];
573
+ _props = [];
574
+ register(obj, prop) {
575
+ this._objects.push(obj);
576
+ this._props.push(prop);
577
+ }
578
+ newPointer;
579
+ resolve(context) {
580
+ //@ts-ignore
581
+ this.resolver.debug = this.debug;
582
+ const index = this.resolver.resolve(context);
583
+ this.newPointer = this.pointerBase + index;
584
+ if (this.debug)
585
+ console.log("RESOLVED", this.pointerBase, index, " to → ", this.newPointer);
586
+ }
587
+ write() {
588
+ for (let i = 0; i < this._objects.length; i++) {
589
+ const obj = this._objects[i];
590
+ const prop = this._props[i];
591
+ if (this.debug)
592
+ console.log("< Resolved pointer", this.newPointer, "in", prop);
593
+ obj[prop] = this.newPointer;
594
+ }
595
+ }
596
+ }
597
+ class PointerResolver {
598
+ document;
599
+ /* the object originally being referenced by the pointer */
600
+ inputObject;
601
+ /* the original json definition */
602
+ inputDefinition;
603
+ constructor(doc, obj, def) {
604
+ this.document = doc;
605
+ this.inputObject = obj;
606
+ this.inputDefinition = def;
607
+ }
608
+ debug = false;
609
+ }
610
+ class TexturePointerResolver extends PointerResolver {
611
+ _debugName;
612
+ _samplerDef;
613
+ init(context) {
614
+ this._debugName = this.inputDefinition.name ?? this.inputDefinition.source?.toString() ?? "unknown";
615
+ const samplerIndex = this.inputDefinition.sampler;
616
+ if (samplerIndex !== undefined) {
617
+ this._samplerDef = context.jsonDoc.json.samplers[samplerIndex];
618
+ }
619
+ if (this.debug)
620
+ console.log(this._debugName, this._samplerDef);
621
+ }
622
+ resolve(context) {
623
+ const imgIndex = context.imageIndexMap.get(this.inputObject);
624
+ if (imgIndex === undefined) {
625
+ console.error("ERR: image is missing", imgIndex);
626
+ return -1;
627
+ }
628
+ // ensure we have a texture array
629
+ context.jsonDoc.json.textures = context.jsonDoc.json.textures || [];
630
+ // try find the texture that points to the same image source
631
+ for (let i = 0; i < context.jsonDoc.json.textures.length; i++) {
632
+ const textureDef = context.jsonDoc.json.textures[i];
633
+ if (findSource(textureDef) === imgIndex) {
634
+ if (textureDef.sampler === undefined) {
635
+ textureDef.sampler = this.findOrCreateSampler(context.jsonDoc.json);
636
+ }
637
+ if (this.debug)
638
+ console.log("Found texture def at " + i, "using image", imgIndex, textureDef);
639
+ return i;
640
+ }
641
+ }
642
+ // the texture was not found - add it
643
+ // create a new texture def entry
644
+ if (this.debug)
645
+ console.log("Creating new texture", this._debugName);
646
+ const hasExtensionWithSource = this.inputDefinition.extensions && Object.keys(this.inputDefinition.extensions).some(key => this.inputDefinition.extensions[key]["source"] !== undefined);
647
+ const textureDef = {
648
+ source: hasExtensionWithSource ? undefined : imgIndex,
649
+ sampler: this.findOrCreateSampler(context.jsonDoc.json),
650
+ extensions: this.inputDefinition.extensions,
651
+ extras: this.inputDefinition.extras,
652
+ };
653
+ context.jsonDoc.json.textures.push(textureDef);
654
+ return context.jsonDoc.json.textures.length - 1;
655
+ }
656
+ findOrCreateSampler(json) {
657
+ json.samplers = json.samplers || [];
658
+ if (this._samplerDef === undefined) {
659
+ console.log("WARN: Creating default sampler", this._debugName);
660
+ // create a default sampler
661
+ this._samplerDef = {
662
+ magFilter: 9729,
663
+ minFilter: 9729,
664
+ wrapS: 33648,
665
+ wrapT: 33648
666
+ };
667
+ }
668
+ else {
669
+ for (let i = 0; i < json.samplers.length; i++) {
670
+ const sampler = json.samplers[i];
671
+ if (sampler.minFilter === this._samplerDef.minFilter &&
672
+ sampler.magFilter === this._samplerDef.magFilter &&
673
+ sampler.wrapS === this._samplerDef.wrapS &&
674
+ sampler.wrapT === this._samplerDef.wrapT) {
675
+ return i;
676
+ }
677
+ }
678
+ }
679
+ // append previous sampler
680
+ json.samplers.push(this._samplerDef);
681
+ return json.samplers.length - 1;
682
+ }
683
+ }
684
+ function findSource(texDef) {
685
+ if (texDef.source !== undefined) {
686
+ return texDef.source;
687
+ }
688
+ if (texDef.extensions) {
689
+ const keys = Object.keys(texDef.extensions);
690
+ for (let i = 0; i < keys.length; i++) {
691
+ const key = keys[i];
692
+ const ext = texDef.extensions[key];
693
+ if (ext["source"] !== undefined) {
694
+ return ext["source"];
695
+ }
696
+ }
697
+ }
698
+ return undefined;
699
+ }