@needle-tools/gltf-build-pipeline 2.9.5-release.0dfa267 → 2.10.0-next

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 (93) hide show
  1. package/dist/cache/cache.d.ts +55 -0
  2. package/dist/cache/cache.js +289 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +189 -0
  7. package/dist/config/index.d.ts +53 -0
  8. package/dist/config/index.js +152 -0
  9. package/dist/constants.d.ts +7 -0
  10. package/dist/constants.js +7 -0
  11. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -0
  12. package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -0
  13. package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -0
  14. package/dist/extensions/NEEDLE_compression_texture/index.js +1 -0
  15. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +27 -0
  16. package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +88 -0
  17. package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -0
  18. package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -0
  19. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +27 -0
  20. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +980 -0
  21. package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -0
  22. package/dist/extensions/NEEDLE_opaque/index.js +30 -0
  23. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +48 -0
  24. package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +69 -0
  25. package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -0
  26. package/dist/extensions/NEEDLE_progressive/index.js +1 -0
  27. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +23 -0
  28. package/dist/extensions/NEEDLE_progressive_mesh_settings/index.js +41 -0
  29. package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +25 -0
  30. package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +67 -0
  31. package/dist/extensions/index.d.ts +7 -0
  32. package/dist/extensions/index.js +17 -0
  33. package/dist/extensions/utils.d.ts +6 -0
  34. package/dist/extensions/utils.js +72 -0
  35. package/dist/functions/weld.3.d.ts +76 -0
  36. package/dist/functions/weld.3.js +414 -0
  37. package/dist/index.d.ts +8 -0
  38. package/dist/index.js +8 -0
  39. package/dist/scripts/index.d.ts +2 -0
  40. package/dist/scripts/index.js +2 -0
  41. package/dist/scripts/pack-gltf.d.ts +15 -0
  42. package/dist/scripts/pack-gltf.js +137 -0
  43. package/dist/scripts/stats.d.ts +2 -0
  44. package/dist/scripts/stats.js +19 -0
  45. package/dist/transforms/index.d.ts +9 -0
  46. package/dist/transforms/index.js +9 -0
  47. package/dist/transforms/needle_animation_transform.d.ts +2 -0
  48. package/dist/transforms/needle_animation_transform.js +5 -0
  49. package/dist/transforms/needle_asset.d.ts +3 -0
  50. package/dist/transforms/needle_asset.js +50 -0
  51. package/dist/transforms/needle_common.d.ts +17 -0
  52. package/dist/transforms/needle_common.js +1 -0
  53. package/dist/transforms/needle_mesh_transform.d.ts +15 -0
  54. package/dist/transforms/needle_mesh_transform.js +218 -0
  55. package/dist/transforms/needle_progressive.d.ts +44 -0
  56. package/dist/transforms/needle_progressive.js +1005 -0
  57. package/dist/transforms/needle_texture_transform.d.ts +13 -0
  58. package/dist/transforms/needle_texture_transform.js +257 -0
  59. package/dist/transforms/needle_toktx.d.ts +26 -0
  60. package/dist/transforms/needle_toktx.js +239 -0
  61. package/dist/transforms/needle_webp.d.ts +15 -0
  62. package/dist/transforms/needle_webp.js +53 -0
  63. package/dist/transforms/toktx.d.ts +52 -0
  64. package/dist/transforms/toktx.js +221 -0
  65. package/dist/transforms/toktx_types.d.ts +78 -0
  66. package/dist/transforms/toktx_types.js +62 -0
  67. package/dist/transforms/util.d.ts +21 -0
  68. package/dist/transforms/util.js +90 -0
  69. package/dist/utils/compression_utils.d.ts +2 -0
  70. package/dist/utils/compression_utils.js +6 -0
  71. package/dist/utils/fileutils.d.ts +19 -0
  72. package/dist/utils/fileutils.js +177 -0
  73. package/dist/utils/guid.d.ts +1 -0
  74. package/dist/utils/guid.js +5 -0
  75. package/dist/utils/index.d.ts +10 -0
  76. package/dist/utils/index.js +10 -0
  77. package/dist/utils/merge.d.ts +12 -0
  78. package/dist/utils/merge.js +31 -0
  79. package/dist/utils/mesh.d.ts +13 -0
  80. package/dist/utils/mesh.js +124 -0
  81. package/dist/utils/nodeio.d.ts +6 -0
  82. package/dist/utils/nodeio.js +86 -0
  83. package/dist/utils/stats.d.ts +70 -0
  84. package/dist/utils/stats.js +258 -0
  85. package/dist/utils/texture.d.ts +5 -0
  86. package/dist/utils/texture.js +14 -0
  87. package/dist/utils/validate.d.ts +5 -0
  88. package/dist/utils/validate.js +17 -0
  89. package/dist/utils/version.d.ts +1 -0
  90. package/dist/utils/version.gen.d.ts +1 -0
  91. package/dist/utils/version.gen.js +1 -0
  92. package/dist/utils/version.js +4 -0
  93. package/package.json +2 -2
@@ -0,0 +1,55 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import { ExtensibleProperty } from "@gltf-transform/core";
3
+ import { Logger } from "@donmccurdy/caporal";
4
+ /**
5
+ * Clear the cache.
6
+ * This will remove all files in the cache directory.
7
+ */
8
+ export declare function clearCache(logger?: Logger): void;
9
+ export declare const cacheSizeLimit = 1024;
10
+ /**
11
+ * Ensure that the cache size does not exceed the given maximum size in MB.
12
+ * @param maxSizeMB The maximum size in MB.
13
+ */
14
+ export declare function limitCacheSize(maxSizeMB?: number): void;
15
+ /**
16
+ * Add data to the cache.
17
+ * @param key The key to store the data under.
18
+ * @param bytes The data to store as a Uint8Array.
19
+ */
20
+ export declare function addToCache(key: string, bytes: Uint8Array | Buffer): void;
21
+ /**
22
+ * Try to get the data from the cache.
23
+ * @param key The key to look up.
24
+ * @returns The data as a Uint8Array or null if not found.
25
+ * @throws If the cache file is corrupted.
26
+ */
27
+ export declare function tryGetFromCache(key: string): Uint8Array | null;
28
+ /** Add meta data to the cache.
29
+ * @param key The key to store the meta data for.
30
+ * @param meta The meta data object to store. It will be serialized to JSON and stored in a file with the key as the name with the extension ".meta".
31
+ */
32
+ export declare function addMetaToCache(key: string, meta: object): void;
33
+ /** Try to get the meta data from the cache.
34
+ * @param key The key to look up.
35
+ * @param ignoreVersion If true, the version check is skipped. If false, the version must match the current build pipeline version.
36
+ * @returns The meta data object or null if not found.
37
+ */
38
+ export declare function tryGetMetaFromCache(key: string, ignoreVersion?: boolean): object | null;
39
+ /**
40
+ * Get the cache key for a property.
41
+ * @param property The property to get the key for.
42
+ * @returns The key as a string.
43
+ */
44
+ export declare function getKey(property: ExtensibleProperty): string;
45
+ export declare function getKeyWithHash(property: ExtensibleProperty): {
46
+ key: string;
47
+ hash: string;
48
+ };
49
+ /**
50
+ * Get the hash for a property or an object.
51
+ * @param property The property to get the hash for.
52
+ * @param level The current level of recursion.
53
+ * @returns The hash as a number.
54
+ */
55
+ export declare function getHash(property: ExtensibleProperty | object, level?: number): string;
@@ -0,0 +1,289 @@
1
+ import { ExtensibleProperty, Texture, Primitive, Accessor, Mesh } from "@gltf-transform/core";
2
+ import tmp from 'tmp';
3
+ import fs, { statSync } from 'fs';
4
+ import path from 'path';
5
+ import { getVersion } from "../utils/version.js";
6
+ // import * as CHECKDISCSPACE from 'check-disk-space'
7
+ /**
8
+ * Get the available space on the cache directory in bytes.
9
+ */
10
+ // export async function getCacheAvailableSpaceOnDisc(): Promise<number> {
11
+ // try {
12
+ // const dir = getCacheDirectory();
13
+ // const res = await CHECKDISCSPACE.default(dir).catch(() => { return { free: 0 } });
14
+ // return res.free;
15
+ // }
16
+ // catch (e) {
17
+ // console.error(`Error checking disk space: ${e}`);
18
+ // }
19
+ // return 0;
20
+ // }
21
+ const buildPipelineVersion = getVersion(false);
22
+ const debugLogs = process.argv.includes("--debug");
23
+ /**
24
+ * Clear the cache.
25
+ * This will remove all files in the cache directory.
26
+ */
27
+ export function clearCache(logger) {
28
+ const dir = getCacheDirectory(true);
29
+ fs.rmSync(dir, { recursive: true });
30
+ if (!logger)
31
+ console.log(`Cleared cache at ${dir}`);
32
+ else
33
+ logger.info(`Cleared cache at ${dir}`);
34
+ }
35
+ export const cacheSizeLimit = 1024;
36
+ /**
37
+ * Ensure that the cache size does not exceed the given maximum size in MB.
38
+ * @param maxSizeMB The maximum size in MB.
39
+ */
40
+ export function limitCacheSize(maxSizeMB = cacheSizeLimit) {
41
+ const dir = getCacheDirectory(true);
42
+ console.log(`Limit cache size to ${maxSizeMB.toFixed(2)} MB at ${dir}...`);
43
+ let totalSize = 0;
44
+ const files = new Array();
45
+ function recurseDirectories(dir) {
46
+ fs.readdirSync(dir).forEach(file => {
47
+ const path = `${dir}/${file}`;
48
+ const stats = statSync(path);
49
+ if (stats.isDirectory()) {
50
+ recurseDirectories(path);
51
+ }
52
+ else {
53
+ totalSize += stats.size;
54
+ files.push({ path, size: stats.size });
55
+ }
56
+ });
57
+ }
58
+ recurseDirectories(dir);
59
+ const maxSizeInBytes = maxSizeMB * 1024 * 1024;
60
+ console.log(`Current cache size is ${(totalSize / (1024 * 1024)).toFixed(1)} MB`);
61
+ if (totalSize < maxSizeInBytes)
62
+ return;
63
+ files.sort((a, b) => {
64
+ const mtimeA = fs.statSync(a.path).mtimeMs;
65
+ const mtimeB = fs.statSync(b.path).mtimeMs;
66
+ return mtimeA - mtimeB;
67
+ });
68
+ const targetSize = maxSizeInBytes * 0.9;
69
+ // remove oldest files first
70
+ let filesRemoved = 0;
71
+ while (totalSize > targetSize) {
72
+ const file = files.shift();
73
+ if (!file)
74
+ break;
75
+ if (fs.statSync(file.path).isDirectory()) {
76
+ continue;
77
+ }
78
+ filesRemoved++;
79
+ fs.rmSync(file.path);
80
+ totalSize -= file.size;
81
+ }
82
+ console.log(`Cache size reduced to ${(totalSize / (1024 * 1024)).toFixed(1)} MB (${filesRemoved} files removed)`);
83
+ }
84
+ /**
85
+ * Add data to the cache.
86
+ * @param key The key to store the data under.
87
+ * @param bytes The data to store as a Uint8Array.
88
+ */
89
+ export function addToCache(key, bytes) {
90
+ const dir = getCacheDirectory();
91
+ const path = `${dir}/${key}.bin`;
92
+ if (debugLogs)
93
+ console.log(`Added ${(bytes.byteLength / 1024).toFixed(1)} KB to cache at \"${path}\"`);
94
+ if (bytes instanceof Buffer)
95
+ bytes = new Uint8Array(bytes);
96
+ fs.writeFileSync(path, bytes);
97
+ }
98
+ /**
99
+ * Try to get the data from the cache.
100
+ * @param key The key to look up.
101
+ * @returns The data as a Uint8Array or null if not found.
102
+ * @throws If the cache file is corrupted.
103
+ */
104
+ export function tryGetFromCache(key) {
105
+ const dir = getCacheDirectory();
106
+ const path = `${dir}/${key}.bin`;
107
+ if (fs.existsSync(path)) {
108
+ const bytes = fs.readFileSync(path);
109
+ if (bytes instanceof Buffer)
110
+ return new Uint8Array(bytes);
111
+ return bytes;
112
+ }
113
+ return null;
114
+ }
115
+ /** Add meta data to the cache.
116
+ * @param key The key to store the meta data for.
117
+ * @param meta The meta data object to store. It will be serialized to JSON and stored in a file with the key as the name with the extension ".meta".
118
+ */
119
+ export function addMetaToCache(key, meta) {
120
+ const dir = getCacheDirectory();
121
+ const metaPath = `${dir}/${key}.meta`;
122
+ meta['needle-build-pipeline:version'] = buildPipelineVersion;
123
+ fs.writeFileSync(metaPath, JSON.stringify(meta));
124
+ }
125
+ /** Try to get the meta data from the cache.
126
+ * @param key The key to look up.
127
+ * @param ignoreVersion If true, the version check is skipped. If false, the version must match the current build pipeline version.
128
+ * @returns The meta data object or null if not found.
129
+ */
130
+ export function tryGetMetaFromCache(key, ignoreVersion = false) {
131
+ try {
132
+ const dir = getCacheDirectory();
133
+ const path = `${dir}/${key}.meta`;
134
+ if (fs.existsSync(path)) {
135
+ const bytes = fs.readFileSync(path);
136
+ if (bytes?.length > 0) {
137
+ const text = bytes.toString('utf-8');
138
+ if (text) {
139
+ const obj = JSON.parse(text);
140
+ if (ignoreVersion)
141
+ return obj;
142
+ if (obj['needle-build-pipeline:version'] === buildPipelineVersion)
143
+ return obj;
144
+ }
145
+ }
146
+ }
147
+ return null;
148
+ }
149
+ catch (e) {
150
+ console.error(`Error reading cache meta: ${e.message}`);
151
+ return null;
152
+ }
153
+ }
154
+ /**
155
+ * Get the cache key for a property.
156
+ * @param property The property to get the key for.
157
+ * @returns The key as a string.
158
+ */
159
+ export function getKey(property) {
160
+ let prefix = "prop";
161
+ if (property instanceof Texture) {
162
+ prefix = "image";
163
+ }
164
+ let hash = getHash(property);
165
+ const key = `${prefix}-${hash}`;
166
+ return key;
167
+ }
168
+ export function getKeyWithHash(property) {
169
+ let prefix = "prop";
170
+ if (property instanceof Texture) {
171
+ prefix = "image";
172
+ }
173
+ let hash = getHash(property);
174
+ const key = `${prefix}-${hash}`;
175
+ return { key, hash };
176
+ }
177
+ /**
178
+ * Get the hash for a property or an object.
179
+ * @param property The property to get the hash for.
180
+ * @param level The current level of recursion.
181
+ * @returns The hash as a number.
182
+ */
183
+ export function getHash(property, level = 0) {
184
+ let hashStr = "";
185
+ let hash = 0;
186
+ if (property instanceof ExtensibleProperty) {
187
+ const extensions = property.listExtensions();
188
+ const extras = property.getExtras();
189
+ // const slots = listTextureSlots(doc, texture);
190
+ // const channels = getTextureChannelMask(doc, texture);
191
+ hash += hashObject({
192
+ extensions,
193
+ extras,
194
+ });
195
+ if (property instanceof Texture) {
196
+ const image = property.getImage();
197
+ const bytes = image?.buffer;
198
+ if (bytes)
199
+ hash += hashBuffer(bytes, image.byteOffset, image.byteLength);
200
+ }
201
+ else if (property instanceof Mesh) {
202
+ const weights = property.getWeights();
203
+ hash += hashObject(weights);
204
+ const primitives = property.listPrimitives();
205
+ for (const prim of primitives) {
206
+ hash += hashString(getHash(prim, level + 1));
207
+ }
208
+ }
209
+ else if (property instanceof Primitive) {
210
+ const indices = property.getIndices();
211
+ const attributes = property.listAttributes();
212
+ const targets = property.listTargets();
213
+ // TODO: hash buffer view if possible
214
+ const semantics = property.listSemantics();
215
+ const semanticsNames = semantics.map(s => {
216
+ return {
217
+ name: s,
218
+ };
219
+ });
220
+ hash += hashObject({
221
+ indices,
222
+ attributes,
223
+ targets,
224
+ semantics: semanticsNames
225
+ });
226
+ if (indices) {
227
+ const indexBuffer = indices.getArray();
228
+ if (indexBuffer) {
229
+ hash += hashBuffer(indexBuffer.buffer, indexBuffer.byteOffset, indexBuffer.byteLength);
230
+ }
231
+ }
232
+ // We can not pass the whole buffer into hashObject since very large meshes will cause errors when trying to stringify
233
+ // See https://linear.app/needle/issue/NE-6414
234
+ for (const sem of semantics) {
235
+ const arr = property.getAttribute(sem)?.getArray();
236
+ if (arr) {
237
+ hash += hashBuffer(arr.buffer, arr.byteOffset, arr.byteLength);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ else {
243
+ hash += hashObject(property);
244
+ }
245
+ return hashStr + hash;
246
+ }
247
+ const cleanBuildPipelineVersion = getVersion(false).replace(/[^a-zA-Z0-9]/g, "_");
248
+ function getCacheDirectory(root) {
249
+ const tmpDir = path.join(tmp.tmpdir, "Needle", "build-pipeline-cache", root ? "" : cleanBuildPipelineVersion);
250
+ if (!fs.existsSync(tmpDir)) {
251
+ fs.mkdirSync(tmpDir, { recursive: true });
252
+ }
253
+ return tmpDir;
254
+ }
255
+ function hashObject(obj) {
256
+ if (obj instanceof Accessor) {
257
+ const buffer = obj.getArray();
258
+ return hashBuffer(buffer.buffer);
259
+ }
260
+ else if (obj == null) {
261
+ return 0;
262
+ }
263
+ try {
264
+ const str = JSON.stringify(obj);
265
+ return hashString(str);
266
+ }
267
+ catch (err) {
268
+ console.error(err);
269
+ debugger;
270
+ throw err;
271
+ }
272
+ }
273
+ function hashBuffer(buffer, offset, length) {
274
+ let hash = 0;
275
+ const view = new Uint8Array(buffer, offset, length);
276
+ for (let i = 0; i < view.length; i++) {
277
+ hash = ((hash << 5) - hash) + view[i];
278
+ hash |= 0;
279
+ }
280
+ return hash;
281
+ }
282
+ function hashString(str) {
283
+ let hash = 0;
284
+ for (let i = 0; i < str.length; i++) {
285
+ hash = ((hash << 5) - hash) + str.charCodeAt(i);
286
+ hash |= 0;
287
+ }
288
+ return hash;
289
+ }
@@ -0,0 +1 @@
1
+ export * from "./cache.js";
@@ -0,0 +1 @@
1
+ export * from "./cache.js";
@@ -0,0 +1,2 @@
1
+ #! /usr/bin/env node
2
+ export {};
@@ -0,0 +1,189 @@
1
+ #! /usr/bin/env node
2
+ import { program } from '@donmccurdy/caporal';
3
+ import { calculateStats, packGLTF } from "../scripts/index.js";
4
+ import { printStats, writeFileStatsToFile } from '../utils/stats.js';
5
+ import { getVersion } from '../utils/version.js';
6
+ import { UsecaseOptions, createConfig, getConfig } from '../config/index.js';
7
+ import { isLOD, make_progressive } from '../transforms/needle_progressive.js';
8
+ import { cacheSizeLimit, clearCache, limitCacheSize } from '../cache/cache.js';
9
+ import { existsSync } from 'fs';
10
+ import { ensureIsDirectory, foreachGLTF, isDirectory } from '../utils/fileutils.js';
11
+ import path, { resolve } from 'path';
12
+ import { ERROR_CODES } from '../constants.js';
13
+ // For testing / dev you can run `npm link` in the package directory
14
+ // For removing the link run `npm rm --global @needle-tools/gltf-build-pipeline`
15
+ program
16
+ .command("version", "Print current verison")
17
+ .action(async ({ logger }) => {
18
+ logger.info(getVersion());
19
+ })
20
+ .command("clear-caches", "Clear all caches")
21
+ .help(`Clears all cached files
22
+ Caches are limited to ${cacheSizeLimit} MB disc space by default.
23
+ `)
24
+ .action(async ({ logger }) => {
25
+ logger.info("Clearing caches");
26
+ clearCache(logger);
27
+ })
28
+ .command("stats", "Calculate glTF stats")
29
+ .help(`
30
+ `)
31
+ .argument('<input>', "Path to a glTF, GLB or VRM file OR a directory")
32
+ .argument('<output>', "Output path for the stats", { default: null, validator: program.STRING })
33
+ .action(async ({ logger, args, options }) => {
34
+ logger.level = "info";
35
+ logger.info("Calculating stats for \"" + args.input + "\"");
36
+ const input = args.input.toString();
37
+ const filestats = new Array();
38
+ await foreachGLTF(input, async (file) => {
39
+ const stats = await calculateStats(file);
40
+ filestats.push(stats);
41
+ });
42
+ let output = args.ourput?.toString();
43
+ if (!output) {
44
+ if (isDirectory(input)) {
45
+ output = path.join(input, "stats.needle.json");
46
+ }
47
+ else {
48
+ output = input + ".needle.stats.json";
49
+ }
50
+ }
51
+ logger.info("Writing stats to: " + output);
52
+ writeFileStatsToFile(filestats, output);
53
+ })
54
+ .command("transform", "Transform (progressive loading) and compress glTF, GLB or VRM files")
55
+ .help(`
56
+ This will produce multiple versions of the input file, each with a different level of optimization.
57
+ Each version will be compressed and written to the output directory.
58
+ `)
59
+ .argument('<input>', "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed")
60
+ .argument('<output>', "The output file or directory. It can be an absolute path or a relative path to the input file. If not provided, the input file will be overwritten.", { default: null })
61
+ .option("--compress", `When enabled files will be compressed`, { validator: program.BOOLEAN, default: true })
62
+ .option("--progressive", `When enabled files will processed to be progressively loaded`, { validator: program.BOOLEAN, default: true })
63
+ .option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}]`, { validator: program.STRING })
64
+ .option("--config <config>", "The path to a JSON file containing the compression settings. This can also be a needle.config.json file with a \"compression\" object or usecase string.", { validator: program.STRING })
65
+ .option("--cache", "Use \"--cache False\" to disable caching.\nWhen caching is enabled then previously processed files will be detected and loaded from disc cache instead of being processed again if settings or the file contents have not changed.", { validator: program.BOOLEAN, default: true })
66
+ .option("--debug", "Enable for more verbose outout with \"--debug True\"", { validator: program.BOOLEAN, default: false })
67
+ .option("--verbose", "Enable for more verbose outout with \"--verbose True\"", { validator: program.BOOLEAN, default: false })
68
+ .action(async ({ logger, args, options }) => {
69
+ if (options.compress === false && options.progressive === false) {
70
+ logger.error("You need to enable at least one of the options: --compress or --progressive");
71
+ return;
72
+ }
73
+ logger.info("Transforming glTF file: " + args.input);
74
+ const stats = {
75
+ startTime: Date.now(),
76
+ totalGPUMemoryInMB: 0,
77
+ totalFilesProcessed: 0,
78
+ totalFileSizeInMB: 0,
79
+ totalFileSizeInMBBefore: 0,
80
+ };
81
+ const input = args.input.toString();
82
+ let config = getConfig(options.config?.toString(), input, {
83
+ logger
84
+ });
85
+ if (options.usecase) {
86
+ logger.info("Using usecase: " + options.usecase);
87
+ config = { ...createConfig(options.usecase.toString()), ...config, };
88
+ if (config.usecase != "default" && config.usecase != options.usecase.toString()) {
89
+ logger.warn(`Conflicting usecase. Your specific config at \"${options.config}\" has a different usecase than the one provided by the CLI: \"${options.usecase}\". Using the usecase from the config file.`);
90
+ }
91
+ else {
92
+ config.usecase = options.usecase.toString();
93
+ }
94
+ }
95
+ const inputIsDirectory = isDirectory(input);
96
+ let output = args.output?.toString();
97
+ // If the input path is a filepath and no specific output is defined we set output to the input file
98
+ if (!inputIsDirectory && !output) {
99
+ output = input;
100
+ }
101
+ const outputIsDirectory = output && isDirectory(output);
102
+ if (inputIsDirectory && output && !outputIsDirectory) {
103
+ logger.error("When <input> is a directory then <output> must also be a directory");
104
+ process.exit(ERROR_CODES.INVALID_ARGS);
105
+ }
106
+ // Handle the output bath being relative to the input path
107
+ if (output && path.isAbsolute(output) === false) {
108
+ const base = ensureIsDirectory(input);
109
+ output = resolve(base, output);
110
+ }
111
+ const set = new Set();
112
+ const useCache = options.cache !== false;
113
+ const debug = options.debug === true;
114
+ const verbose = options.verbose === true;
115
+ await foreachGLTF(input, async (file) => {
116
+ if (!existsSync(file)) {
117
+ // Safeguard if we run compression in a souce directory that already has progressive assets
118
+ // Here it can happen that a LOD file is generated with the same GUID at the moment
119
+ // And then later removed because simplification is too small.
120
+ // See https://linear.app/needle/issue/NE-5518
121
+ if (isLOD(file)) {
122
+ logger.warn(`WARN: Ignore previously found file that does not exist anymore: \"${file}\"`);
123
+ return;
124
+ }
125
+ else {
126
+ logger.error(`ERR: Previously found file is now missing: \"${file}\"`);
127
+ process.exit(ERROR_CODES.LOST_FILE);
128
+ }
129
+ }
130
+ logger.info("→ Transform " + file);
131
+ const progressive_results = new Array();
132
+ if (options.progressive === true) {
133
+ if (await make_progressive({
134
+ path: file,
135
+ outpath: output,
136
+ results: progressive_results,
137
+ config,
138
+ debug,
139
+ verbose,
140
+ useCache,
141
+ }).catch(err => {
142
+ logger.error(err);
143
+ process.exit(ERROR_CODES.PROGRESSIVE_FAILED);
144
+ }) === false) {
145
+ process.exit(ERROR_CODES.PROGRESSIVE_FAILED);
146
+ }
147
+ if (verbose) {
148
+ console.log("Progressive results:\n", progressive_results);
149
+ }
150
+ }
151
+ if (options.compress === true) {
152
+ const opts = {
153
+ config,
154
+ stats,
155
+ packedFiles: set,
156
+ debug,
157
+ verbose,
158
+ useCache,
159
+ logger: logger,
160
+ };
161
+ // If we have produced progressive assets then the array already contains the input
162
+ if (progressive_results.length > 0) {
163
+ for (const res of progressive_results) {
164
+ if (await packGLTF(res, res, opts) === false) {
165
+ process.exit(ERROR_CODES.PACKING_FAILED);
166
+ }
167
+ }
168
+ }
169
+ // If we have not produced progressive assets then we just need to compress the input
170
+ // We can not just check if the file is in the list of results because the output path might have changed the name
171
+ else {
172
+ if (await packGLTF(file, output, opts) === false) {
173
+ process.exit(ERROR_CODES.PACKING_FAILED);
174
+ }
175
+ }
176
+ }
177
+ });
178
+ printStats(stats, logger, config);
179
+ limitCacheSize();
180
+ });
181
+ program.run().catch(err => {
182
+ // Since we still invoke this package with paths when running npm scripts we need to ignore this error
183
+ // We might want to move this code here into an extra package.
184
+ if (err.message.includes("Unknown command") && !err.message.includes("Did you mean")) {
185
+ // ignore
186
+ return;
187
+ }
188
+ throw err;
189
+ });
@@ -0,0 +1,53 @@
1
+ import { ILogger } from "@gltf-transform/core";
2
+ declare type Usecase = "default" | "product" | "world";
3
+ export declare const UsecaseOptions: string[];
4
+ /**
5
+ * The config object contains information about how a file should be processed.
6
+ */
7
+ export type Config = {
8
+ /** High-level configuration */
9
+ usecase: Usecase;
10
+ textures?: {
11
+ lods: boolean;
12
+ };
13
+ meshes?: {
14
+ lods: boolean;
15
+ };
16
+ };
17
+ /**
18
+ * Create a config object with the given usecase.
19
+ */
20
+ export declare function createConfig(usecase: string): {
21
+ usecase: Usecase;
22
+ };
23
+ /**
24
+ * Get the config object either provided by a CLI path or a needle.config.json file in the parent directory.
25
+ *
26
+ * @example CLI argument where the path points to a json file containing the config as `Config`
27
+ * ```sh
28
+ * node script.js --config path/to/my-optimization-config.json
29
+ * ```
30
+ *
31
+ * @example needle.config.json - the usecase can be provided as a string
32
+ * ```json
33
+ * {
34
+ * "optimization": "product"
35
+ * }
36
+ * ```
37
+ *
38
+ * @example needle.config.json - the whole config object can be provided
39
+ * ```json
40
+ * {
41
+ * "optimization": {
42
+ * "usecase": "product"
43
+ * }
44
+ * }
45
+ * ```
46
+ *
47
+ *
48
+ */
49
+ export declare function getConfig(configPath?: string, fallbackSearchDirectory?: string, opts?: {
50
+ logger: ILogger;
51
+ }): Config;
52
+ export declare function searchConfig(directory: string, logger: ILogger): Config | null;
53
+ export {};