@needle-tools/gltf-build-pipeline 2.14.0 → 2.15.0-next.75193b8

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.
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node" resolution-mode="require"/>
2
3
  import { ExtensibleProperty } from "@gltf-transform/core";
3
4
  import { Logger } from "@donmccurdy/caporal";
4
5
  /**
@@ -5,9 +5,9 @@ import path from 'path';
5
5
  import { getVersion } from "../utils/version.js";
6
6
  // import * as CHECKDISCSPACE from 'check-disk-space'
7
7
  import xxhash from "xxhash-wasm";
8
- let hasher = null;
8
+ let xxHashModule = null;
9
9
  xxhash().then(module => {
10
- hasher = module.create32();
10
+ xxHashModule = module;
11
11
  });
12
12
  /**
13
13
  * Get the available space on the cache directory in bytes.
@@ -304,9 +304,9 @@ function hashObject(obj, depth = 0) {
304
304
  }
305
305
  }
306
306
  function hashBuffer(buffer, offset, length) {
307
- if (hasher) {
307
+ if (xxHashModule) {
308
308
  const view = new Uint8Array(buffer, offset, length);
309
- return hasher.update(view).digest();
309
+ return xxHashModule.h32Raw(view);
310
310
  }
311
311
  let hash = 0;
312
312
  const view = new Uint8Array(buffer, offset, length);
package/dist/cli/index.js CHANGED
File without changes
@@ -1,6 +1,8 @@
1
1
  import { Extension, ExtensionProperty, PropertyType, Node, TextureChannel } from '@gltf-transform/core';
2
2
  import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
3
3
  import { isLightmap } from '../../utils/texture.js';
4
+ import { NEEDLE_progressive_texture_settings } from '../NEEDLE_progressive_texture_settings/index.js';
5
+ import { testAssert } from '../../utils/test-assert.js';
4
6
  const ALL_PROPERTY_TYPES = [];
5
7
  for (const key in PropertyType) {
6
8
  ALL_PROPERTY_TYPES.push(PropertyType[key]);
@@ -123,8 +125,11 @@ export function createOpaqueExtension(name, types, opts) {
123
125
  const prop = this.createExtension();
124
126
  this.jsonPointerHandler.read(context, texture, ext, prop);
125
127
  texture.setExtension(EXTENSION_NAME, prop);
126
- const handler = new TextureHandler(this.document, textureDefinition, EXTENSION_NAME, ext, debugLog);
128
+ const handler = new TextureHandler(this.document, textureDefinition, EXTENSION_NAME, ext, debugLog, texture);
127
129
  prop.add(handler);
130
+ const sourceIndex = textureDefinition.source;
131
+ if (debugLog)
132
+ console.log("Found texture with extension", EXTENSION_NAME, "at index", index, "source image index", sourceIndex, "name", texture.getName() || "<unnamed>");
128
133
  });
129
134
  break;
130
135
  case PropertyType.NODE:
@@ -188,11 +193,11 @@ export function createOpaqueExtension(name, types, opts) {
188
193
  const textures = this.document.getRoot().listTextures();
189
194
  for (let i = 0; i < textures.length; i++) {
190
195
  const tex = textures[i];
191
- const texDef = textureDefinitions[i];
192
- const name = texDef?.name;
193
196
  if (isLightmap(tex) && tex.getMimeType() !== "image/exr") {
194
197
  prop.setReference("lightmapTexture", tex, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
195
- logger.info(`Found lightmap texture ${name}: ${tex.getMimeType()} in ${EXTENSION_NAME}`);
198
+ const texDef = textureDefinitions[i]; // @TODO: textureDefinitions and textures might not be in the same order - we need to find the texture by source index instead of assuming the order is the same
199
+ const name = tex.getName() || texDef?.name;
200
+ logger.info(`Found lightmap texture \"${name}\" (${tex.getMimeType()}) in ${EXTENSION_NAME}`);
196
201
  }
197
202
  }
198
203
  }
@@ -389,30 +394,39 @@ class GenericWriter {
389
394
  class TextureHandler {
390
395
  document;
391
396
  textureDefinition;
392
- index;
393
397
  key;
394
398
  ext;
395
399
  debug = false;
400
+ /** Reference to the gltf-transform Texture object, used to resolve the current image index at write time via imageIndexMap */
401
+ textureRef;
396
402
  assignTo(obj) {
397
403
  Object.assign(obj, this.ext);
398
404
  }
399
- constructor(doc, def, key, ext, debug) {
405
+ constructor(doc, def, key, ext, debug, textureRef) {
400
406
  this.document = doc;
401
407
  this.textureDefinition = def;
402
408
  this.key = key;
403
409
  this.ext = ext;
404
410
  this.debug = debug;
405
- this.index = this.tryGetSourceIndex(this.textureDefinition);
406
- if (debug)
407
- console.log(def);
411
+ this.textureRef = textureRef;
412
+ if (debug) {
413
+ const index = this.tryGetSourceIndex(this.textureDefinition);
414
+ console.log(index, this.textureDefinition);
415
+ }
408
416
  }
409
417
  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);
418
+ // Resolve the current image index from the gltf-transform graph reference.
419
+ // This is stable even after dedup/prune operations that re-index images.
420
+ const imageIndex = ctx.imageIndexMap.get(this.textureRef);
421
+ testAssert(imageIndex !== undefined || this.key === "EXT_texture_exr", `imageIndexMap missing texture ref for "${this.key}": ${this.textureRef.getName() || "<unnamed>"}`);
413
422
  if (imageIndex === undefined) {
414
- console.warn("WARN: no source index found for texture", this.key);
423
+ if (this.debug) {
424
+ console.warn("WARN: failed to resolve image index for texture via imageIndexMap:", this.key, this.textureRef.getName() || "<unnamed>");
425
+ }
426
+ return;
415
427
  }
428
+ let foundTexture = false;
429
+ const textureDefs = ctx.jsonDoc.json.textures || [];
416
430
  textureDefs?.forEach((textureDef, i) => {
417
431
  if (foundTexture)
418
432
  return;
@@ -431,18 +445,12 @@ class TextureHandler {
431
445
  }
432
446
  }
433
447
  });
448
+ testAssert(foundTexture || this.key === "EXT_texture_exr", `failed to match texture for extension "${this.key}", imageIndex=${imageIndex}, textures=${textureDefs.length}, name=${this.textureRef.getName() || "<unnamed>"}`);
434
449
  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
- // });
450
+ if (this.debug) {
451
+ console.warn("WARN: failed to re-assign extension:", this.key, "imageIndex", imageIndex, textureDefs.length, this.textureRef.getName() || "<unnamed>", this.textureDefinition);
452
+ }
453
+ }
446
454
  }
447
455
  tryGetSourceIndex(def) {
448
456
  let index = def.source;
@@ -857,7 +865,8 @@ class TextureIndexResolver extends PointerResolver {
857
865
  resolve(context) {
858
866
  const imgIndex = context.imageIndexMap.get(this.inputObject);
859
867
  if (imgIndex === undefined) {
860
- console.error("ERR: image is missing", imgIndex);
868
+ testAssert(false, `imageIndexMap missing texture: ${this.inputObject.getName() || "<unnamed>"}`);
869
+ console.error("ERR: image is missing");
861
870
  return -1;
862
871
  }
863
872
  // ensure we have a texture array
@@ -878,11 +887,18 @@ class TextureIndexResolver extends PointerResolver {
878
887
  // create a new texture def entry
879
888
  if (this.debug)
880
889
  console.log("Could not find texture, creating a new one: " + this._debugName);
881
- const hasExtensionWithSource = this.inputDefinition.extensions && Object.keys(this.inputDefinition.extensions).some(key => this.inputDefinition.extensions[key]["source"] !== undefined);
890
+ // Remove settings-only extensions that are consumed during processing and should not appear in the output.
891
+ let extensions = this.inputDefinition.extensions ? { ...this.inputDefinition.extensions } : undefined;
892
+ if (extensions) {
893
+ delete extensions[NEEDLE_progressive_texture_settings.EXTENSION_NAME];
894
+ if (Object.keys(extensions).length === 0)
895
+ extensions = undefined;
896
+ }
897
+ const hasExtensionWithSource = extensions && Object.keys(extensions).some(key => extensions[key]?.["source"] !== undefined);
882
898
  const textureDef = {
883
899
  source: hasExtensionWithSource ? undefined : imgIndex,
884
900
  sampler: this.findOrCreateSampler(context.jsonDoc.json),
885
- extensions: this.inputDefinition.extensions,
901
+ extensions: extensions,
886
902
  extras: this.inputDefinition.extras,
887
903
  };
888
904
  context.jsonDoc.json.textures.push(textureDef);
@@ -9,6 +9,7 @@ import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS, NEEDLE_compression_texture, NEEDLE
9
9
  import { isLOD, isMeshLOD, needle_animation_transform, needle_asset, needle_mesh_transform, needle_texture_transform, } from '../transforms/index.js';
10
10
  import { getOutputPath, getVersion, ioTryReadWithMissingResources, writeNodeIO } from "../utils/index.js";
11
11
  import { addToCache, getHash, tryGetFromCache, } from "../cache/index.js";
12
+ import { TestAssertionError } from '../utils/test-assert.js';
12
13
  /** Compress a glTF file
13
14
  */
14
15
  export async function packGLTF(inputFile, outputFile, options) {
@@ -128,7 +129,10 @@ export async function packGLTF(inputFile, outputFile, options) {
128
129
  return true;
129
130
  }
130
131
  catch (err) {
131
- logger.error(`Packing of "${path.basename(inputFile)}" using version ${getVersion()} failed with error:\n${err.message}`);
132
+ if (err instanceof TestAssertionError)
133
+ throw err;
134
+ const shortedStackTrace = err instanceof Error && err.stack ? err.stack.split('\n').slice(0, 2).join('\n') : '';
135
+ logger.error(`Packing of "${path.basename(inputFile)}" using version ${getVersion()} failed with error:\n${err.message}\n${shortedStackTrace}`);
132
136
  if (debug)
133
137
  throw err;
134
138
  return false;
@@ -13,6 +13,7 @@ import { needle_asset, writeNeedleAsset } from './needle_asset.js';
13
13
  import { MeshoptDecoder, MeshoptEncoder, MeshoptSimplifier } from 'meshoptimizer';
14
14
  import { compressPrimitive, determineMeshCompression } from './needle_mesh_transform.js';
15
15
  import { calculateMeshDensity, getMeshInformation } from "../utils/mesh.js";
16
+ import { TestAssertionError } from '../utils/test-assert.js';
16
17
  export async function make_progressive(opts) {
17
18
  const file = opts.path;
18
19
  let success = true;
@@ -120,6 +121,8 @@ async function onProcess(file, opts) {
120
121
  }
121
122
  }
122
123
  catch (err) {
124
+ if (err instanceof TestAssertionError)
125
+ throw err;
123
126
  console.log("ERR: failed to process file \"" + file + "\", Reason: " + err.message, "\nStack:\n" + err.stack);
124
127
  return false;
125
128
  }
@@ -151,6 +154,7 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
151
154
  return Promise.resolve();
152
155
  }
153
156
  let index = 0;
157
+ const textureHashCache = new Map();
154
158
  for (const texture of doc.getRoot().listTextures()) {
155
159
  const textureIndex = index++;
156
160
  const name = texture.getName();
@@ -177,8 +181,10 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
177
181
  continue;
178
182
  }
179
183
  else {
184
+ if (!textureHashCache.has(texture))
185
+ textureHashCache.set(texture, getHash(texture));
180
186
  settings = {
181
- guid: generateGuid(filename + "_texture_" + index.toString()),
187
+ guid: generateGuid(textureHashCache.get(texture)),
182
188
  maxSize: _options.size[0],
183
189
  };
184
190
  logger.debug(`[${NAME}] No settings found for texture[${index}] - will use default settings (maxSize: ${settings.maxSize}, guid: ${settings.guid})`);
@@ -213,7 +219,13 @@ export function make_progressive_textures(filePath, _options = TEXTURE_RESIZE_DE
213
219
  logger.debug(`[${NAME}] Skipping, texture[${textureIndex}] is already smaller than the requested size: ${currentSize}px <= ${settings.maxSize}px`);
214
220
  continue;
215
221
  }
216
- const textureGuid = settings.guid?.length ? settings.guid : generateGuid(filename + "_texture_" + index.toString());
222
+ if (!settings.guid?.length) {
223
+ if (!textureHashCache.has(texture))
224
+ textureHashCache.set(texture, getHash(texture));
225
+ const hash = textureHashCache.get(texture);
226
+ settings.guid = generateGuid(hash);
227
+ }
228
+ const textureGuid = settings.guid;
217
229
  /** This is the size of the texture that is embedded in the glTF. No LOD should be smaller than this */
218
230
  const maxSize = settings.maxSize;
219
231
  const LOD_LEVEL = new Array();
@@ -524,7 +536,7 @@ async function resizeTexture(options, texture, logger, uri, slots) {
524
536
  }
525
537
  dstWidth = Math.floor(dstWidth);
526
538
  dstHeight = Math.floor(dstHeight);
527
- logger.info(`→ Resize texture (${uri || texture.getName() || slots.join(", ")}) from ${srcWidth}x${srcHeight} to ${dstWidth}x${dstHeight}px`);
539
+ logger.info(`→ Resize texture (${uri || texture.getName() || slots?.join(", ")}) from ${srcWidth}x${srcHeight} to ${dstWidth}x${dstHeight}px`);
528
540
  // https://sharp.pixelplumbing.com/api-resize
529
541
  const buffer = await sharp(texture.getImage())
530
542
  .resize(dstWidth, dstHeight, {
@@ -0,0 +1,8 @@
1
+ export declare class TestAssertionError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ /**
5
+ * Lightweight assertion that throws under vitest and is a no-op in production.
6
+ * Use as a one-liner in pipeline code to catch regressions during tests.
7
+ */
8
+ export declare function testAssert(condition: boolean, message: string): void;
@@ -0,0 +1,18 @@
1
+ const IS_TEST = process.env.VITEST === 'true';
2
+ export class TestAssertionError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'TestAssertionError';
6
+ }
7
+ }
8
+ /**
9
+ * Lightweight assertion that throws under vitest and is a no-op in production.
10
+ * Use as a one-liner in pipeline code to catch regressions during tests.
11
+ */
12
+ export function testAssert(condition, message) {
13
+ if (condition)
14
+ return;
15
+ if (IS_TEST) {
16
+ throw new TestAssertionError(`Assertion failed: ${message}`);
17
+ }
18
+ }
@@ -1 +1 @@
1
- export declare const version = "2.14.0";
1
+ export declare const version = "2.15.0-next.75193b8";
@@ -1 +1 @@
1
- export const version = "2.14.0";
1
+ export const version = "2.15.0-next.75193b8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "2.14.0",
3
+ "version": "2.15.0-next.75193b8",
4
4
  "description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -18,6 +18,12 @@
18
18
  "gltf-build-pipeline": "dist/cli/index.js",
19
19
  "needle-gltf": "dist/cli/index.js"
20
20
  },
21
+ "files": [
22
+ "dist",
23
+ "scripts",
24
+ "README.md",
25
+ "CHANGELOG.md"
26
+ ],
21
27
  "imports": {
22
28
  "#package.json": "./package.json"
23
29
  },
@@ -33,12 +39,18 @@
33
39
  "transform:make-progressive": "node scripts/make-progressive.mjs",
34
40
  "transform:pack-gltf": "node --no-experimental-fetch scripts/pack-gltf.mjs",
35
41
  "pack-gltf": "node --no-experimental-fetch scripts/pack-gltf.mjs",
36
- "dev:pack-gltf": "npm run compile && node scripts/pack-gltf.mjs"
42
+ "dev:pack-gltf": "npm run compile && node scripts/pack-gltf.mjs",
43
+ "test": "tsx tests/utils/download-fixtures.ts && vitest run",
44
+ "test:watch": "vitest",
45
+ "test:setup": "tsx tests/utils/download-fixtures.ts"
37
46
  },
38
47
  "devDependencies": {
39
- "@types/node": "^18.7.23",
48
+ "@types/node": "^22.19.15",
49
+ "gltf-validator": "^2.0.0-dev.3.9",
40
50
  "npm-watch": "^0.13.0",
41
- "typescript": "^4.9.5"
51
+ "tsx": "^4.19.0",
52
+ "typescript": "^4.9.5",
53
+ "vitest": "^3.1.1"
42
54
  },
43
55
  "optionalDependencies": {
44
56
  "@img/sharp-darwin-arm64": "^0.33.4",
package/tsconfig.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "rootDir": "src",
5
- "outDir": "dist",
6
- "declaration": true,
7
- "moduleResolution": "NodeNext",
8
- "module": "NodeNext",
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "resolveJsonModule": true,
12
- "lib": [
13
- "ESNext",
14
- "DOM"
15
- ],
16
- },
17
- "include": [
18
- "src/index.ts",
19
- ]
20
- }