@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.
- package/dist/cache/cache.d.ts +1 -0
- package/dist/cache/cache.js +4 -4
- package/dist/cli/index.js +0 -0
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +43 -27
- package/dist/scripts/pack-gltf.js +5 -1
- package/dist/transforms/needle_progressive.js +15 -3
- package/dist/utils/test-assert.d.ts +8 -0
- package/dist/utils/test-assert.js +18 -0
- package/dist/utils/version.gen.d.ts +1 -1
- package/dist/utils/version.gen.js +1 -1
- package/package.json +16 -4
- package/tsconfig.json +0 -20
package/dist/cache/cache.d.ts
CHANGED
package/dist/cache/cache.js
CHANGED
|
@@ -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
|
|
8
|
+
let xxHashModule = null;
|
|
9
9
|
xxhash().then(module => {
|
|
10
|
-
|
|
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 (
|
|
307
|
+
if (xxHashModule) {
|
|
308
308
|
const view = new Uint8Array(buffer, offset, length);
|
|
309
|
-
return
|
|
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
|
-
|
|
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.
|
|
406
|
-
if (debug)
|
|
407
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
const imageIndex =
|
|
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
|
-
|
|
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.
|
|
436
|
-
console.warn("WARN: failed to re-assign extension:", this.key,
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
1
|
+
export declare const version = "2.15.0-next.75193b8";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "2.
|
|
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.
|
|
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": "^
|
|
48
|
+
"@types/node": "^22.19.15",
|
|
49
|
+
"gltf-validator": "^2.0.0-dev.3.9",
|
|
40
50
|
"npm-watch": "^0.13.0",
|
|
41
|
-
"
|
|
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
|
-
}
|