@needle-tools/gltf-build-pipeline 2.15.10 → 2.16.0-next.85db3ce

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.
@@ -54,3 +54,5 @@ export declare function getKeyWithHash(property: ExtensibleProperty): {
54
54
  * @returns The hash as a number.
55
55
  */
56
56
  export declare function getHash(property: ExtensibleProperty | object, level?: number): string;
57
+ export declare function hashBuffer(buffer: ArrayBufferLike, offset?: number, length?: number): number;
58
+ export declare function hashString(str: string): number;
@@ -307,7 +307,7 @@ function hashObject(obj, depth = 0) {
307
307
  return fallbackHash;
308
308
  }
309
309
  }
310
- function hashBuffer(buffer, offset, length) {
310
+ export function hashBuffer(buffer, offset, length) {
311
311
  if (xxHashModule) {
312
312
  const view = new Uint8Array(buffer, offset, length);
313
313
  return xxHashModule.h32Raw(view);
@@ -320,7 +320,7 @@ function hashBuffer(buffer, offset, length) {
320
320
  }
321
321
  return hash;
322
322
  }
323
- function hashString(str) {
323
+ export function hashString(str) {
324
324
  let hash = 0;
325
325
  for (let i = 0; i < str.length; i++) {
326
326
  hash = ((hash << 5) - hash) + str.charCodeAt(i);
@@ -1 +1,2 @@
1
1
  export * from "./cache.js";
2
+ export * from "./job-cache.js";
@@ -1 +1,2 @@
1
1
  export * from "./cache.js";
2
+ export * from "./job-cache.js";
@@ -0,0 +1,26 @@
1
+ import { ILogger } from '@gltf-transform/core';
2
+ import { Config } from '../config/index.js';
3
+ interface JobCacheOptions {
4
+ compress: boolean;
5
+ progressive: boolean;
6
+ config: Config;
7
+ }
8
+ /**
9
+ * Compute a job cache key from pipeline options and input file content.
10
+ * The key uniquely identifies a specific input + configuration combination.
11
+ * Currently only supports .glb files (hashes the full file content).
12
+ * @returns The job key string, or null if the file format is not supported.
13
+ */
14
+ export declare function computeJobKey(options: JobCacheOptions, inputFilePath: string): string | null;
15
+ /**
16
+ * Save the result of a completed job to the cache.
17
+ * Stores the content of each output file and a manifest linking them.
18
+ */
19
+ export declare function saveJobResult(jobKey: string, outputFiles: string[], logger: ILogger): void;
20
+ /**
21
+ * Try to restore a previously cached job result.
22
+ * Loads all cached files into memory first, then writes them only if all are available.
23
+ * Returns the list of restored file paths, or null if the cache is incomplete.
24
+ */
25
+ export declare function tryRestoreJobResult(jobKey: string, outputDir: string, logger: ILogger): string[] | null;
26
+ export {};
@@ -0,0 +1,84 @@
1
+ // TODO: avoid storing LOD files twice in cache. Currently LODs are stored by the per-asset cache
2
+ // (e.g. "image-{hash}", "packed-{hash}") AND again by the job cache ("job-{key}-fN").
3
+ // Instead, progressive_results should carry { path, cacheKey? } so the job manifest can reference
4
+ // existing per-asset cache keys for LODs and only store the main GLB under a job-specific key.
5
+ // This requires threading cache key collection through make_progressive and packGLTF.
6
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
7
+ import path from 'path';
8
+ import { addMetaToCache, addToCache, tryGetFromCache, tryGetMetaFromCache, hashBuffer, hashString } from './cache.js';
9
+ /**
10
+ * Compute a job cache key from pipeline options and input file content.
11
+ * The key uniquely identifies a specific input + configuration combination.
12
+ * Currently only supports .glb files (hashes the full file content).
13
+ * @returns The job key string, or null if the file format is not supported.
14
+ */
15
+ // TODO: add support for .gltf files (need to hash referenced .bin files as well)
16
+ export function computeJobKey(options, inputFilePath) {
17
+ if (!inputFilePath.endsWith('.glb'))
18
+ return null;
19
+ const fileBytes = readFileSync(inputFilePath);
20
+ const fileHash = hashBuffer(fileBytes.buffer, fileBytes.byteOffset, fileBytes.byteLength);
21
+ const optionsStr = JSON.stringify({
22
+ compress: options.compress,
23
+ progressive: options.progressive,
24
+ config: options.config,
25
+ });
26
+ const optionsHash = hashString(optionsStr);
27
+ return `job-${optionsHash}-${fileHash}`;
28
+ }
29
+ /**
30
+ * Save the result of a completed job to the cache.
31
+ * Stores the content of each output file and a manifest linking them.
32
+ */
33
+ export function saveJobResult(jobKey, outputFiles, logger) {
34
+ const entries = [];
35
+ for (let i = 0; i < outputFiles.length; i++) {
36
+ const filePath = outputFiles[i];
37
+ if (!existsSync(filePath))
38
+ continue;
39
+ const cacheKey = `${jobKey}-f${i}`;
40
+ const content = readFileSync(filePath);
41
+ addToCache(cacheKey, content);
42
+ entries.push({
43
+ fileName: path.basename(filePath),
44
+ cacheKey,
45
+ });
46
+ }
47
+ if (entries.length === 0)
48
+ return;
49
+ const manifest = { entries };
50
+ addMetaToCache(jobKey, manifest);
51
+ logger.debug(`[Job Cache] Saved ${entries.length} file(s) for job ${jobKey}`);
52
+ }
53
+ /**
54
+ * Try to restore a previously cached job result.
55
+ * Loads all cached files into memory first, then writes them only if all are available.
56
+ * Returns the list of restored file paths, or null if the cache is incomplete.
57
+ */
58
+ export function tryRestoreJobResult(jobKey, outputDir, logger) {
59
+ const manifest = tryGetMetaFromCache(jobKey);
60
+ if (!manifest?.entries?.length)
61
+ return null;
62
+ // Load all entries into memory — fail fast if any are missing
63
+ const loaded = [];
64
+ for (const entry of manifest.entries) {
65
+ const cached = tryGetFromCache(entry.cacheKey);
66
+ if (!cached) {
67
+ logger.debug(`[Job Cache] Cache miss: missing entry ${entry.cacheKey}`);
68
+ return null;
69
+ }
70
+ loaded.push({ entry, data: cached });
71
+ }
72
+ // All entries present — write to output directory
73
+ if (!existsSync(outputDir)) {
74
+ mkdirSync(outputDir, { recursive: true });
75
+ }
76
+ const restoredPaths = [];
77
+ for (const { entry, data } of loaded) {
78
+ const outPath = path.join(outputDir, entry.fileName);
79
+ writeFileSync(outPath, data);
80
+ restoredPaths.push(outPath);
81
+ }
82
+ logger.info(`[Job Cache] Restored ${restoredPaths.length} file(s) from cache`);
83
+ return restoredPaths;
84
+ }
package/dist/cli/index.js CHANGED
@@ -6,9 +6,10 @@ import { getVersion } from '../utils/version.js';
6
6
  import { UsecaseOptions, createConfig, getConfig } from '../config/index.js';
7
7
  import { isLOD, make_progressive } from '../transforms/needle_progressive.js';
8
8
  import { cacheSizeLimit, clearCache, limitCacheSize } from '../cache/cache.js';
9
- import { existsSync } from 'fs';
9
+ import { computeJobKey, saveJobResult, tryRestoreJobResult } from '../cache/job-cache.js';
10
+ import { existsSync, statSync } from 'fs';
10
11
  import { ensureIsDirectory, foreachGLTF, isDirectory } from '../utils/fileutils.js';
11
- import path, { resolve } from 'path';
12
+ import path, { dirname, resolve } from 'path';
12
13
  import { ERROR_CODES } from '../constants.js';
13
14
  import { trackPipelineStart, trackPipelineEnd, trackError } from '../utils/analytics.js';
14
15
  // For testing / dev you can run `npm link` in the package directory
@@ -52,6 +53,7 @@ Caches are limited to ${cacheSizeLimit} MB disc space by default.
52
53
  }
53
54
  logger.info("Writing stats to: " + output);
54
55
  writeFileStatsToFile(filestats, output);
56
+ process.exit(0);
55
57
  })
56
58
  .command("transform", "Transform (progressive loading) and compress glTF, GLB or VRM files")
57
59
  .help(`
@@ -140,6 +142,32 @@ Each version will be compressed and written to the output directory.
140
142
  logger.debug(`→ Ignore existing LOD file at ${file}`);
141
143
  return;
142
144
  }
145
+ // Job-level cache: skip all work if the same input + options were already processed
146
+ const jobKey = useCache ? computeJobKey({
147
+ compress: options.compress === true,
148
+ progressive: options.progressive === true,
149
+ config,
150
+ }, file) : undefined;
151
+ if (jobKey) {
152
+ const outputDir = output
153
+ ? (existsSync(output) && isDirectory(output) ? output : dirname(output))
154
+ : dirname(file);
155
+ const restored = tryRestoreJobResult(jobKey, outputDir, logger);
156
+ if (restored) {
157
+ const inputFileSize = statSync(file).size / 1024 / 1024;
158
+ for (const f of restored) {
159
+ set.add(f);
160
+ if (stats) {
161
+ const sizeInMB = statSync(f).size / 1024 / 1024;
162
+ stats.totalFilesProcessed++;
163
+ stats.totalFileSizeInMB += sizeInMB;
164
+ stats.totalFileSizeInMBBefore += inputFileSize;
165
+ }
166
+ }
167
+ logger.info(`→ [CACHE] Restored ${restored.length} file(s)for job ${jobKey}`);
168
+ return;
169
+ }
170
+ }
143
171
  logger.info(`→ Transform ${file}`);
144
172
  const progressive_results = new Array();
145
173
  if (options.progressive === true) {
@@ -195,6 +223,13 @@ Each version will be compressed and written to the output directory.
195
223
  }
196
224
  }
197
225
  }
226
+ // Save job result to cache for future runs
227
+ if (jobKey) {
228
+ const allOutputFiles = progressive_results.length > 0
229
+ ? progressive_results
230
+ : [output || file];
231
+ saveJobResult(jobKey, allOutputFiles, logger);
232
+ }
198
233
  });
199
234
  printStats(stats, logger, config);
200
235
  trackPipelineEnd({
@@ -413,11 +413,11 @@ async function createTextureLod(args, opts) {
413
413
  // Pass compression settings to the new texture
414
414
  const compressionSettingsExt = texture.getExtension(NEEDLE_compression_texture.EXTENSION_NAME);
415
415
  if (compressionSettingsExt !== undefined && compressionSettingsExt !== null && compressionSettingsExt.getExtensionDefinition) {
416
- logger.info("> Pass compression extension to new texture");
416
+ logger.debug("> Pass compression extension to new texture");
417
417
  const newExt = newDoc.createExtension(NEEDLE_compression_texture);
418
418
  const compressionSettings = compressionSettingsExt.getExtensionDefinition();
419
- if (compressionSettings?.mode === "none") {
420
- // if the mode is set to None then we keep that setting for the new texture
419
+ if (compressionSettings?.mode === "none" || compressionSettings?.mode === "webp" || compressionSettings?.mode === "UASTC" || compressionSettings?.mode === "ETC1S") {
420
+ // explicit compression mode set by the user — keep it for LOD textures
421
421
  }
422
422
  else {
423
423
  switch (opts.config.usecase) {
@@ -1 +1 @@
1
- export declare const version = "2.15.10";
1
+ export declare const version = "2.16.0-next.85db3ce";
@@ -1 +1 @@
1
- export const version = "2.15.10";
1
+ export const version = "2.16.0-next.85db3ce";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "2.15.10",
3
+ "version": "2.16.0-next.85db3ce",
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",