@needle-tools/gltf-build-pipeline 1.6.0-alpha.6 → 2.0.0-alpha

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/CHANGELOG.md CHANGED
@@ -4,8 +4,18 @@ All notable changes to this package will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [1.6.0-alpha.6] - 2023-05-30
7
+ ## [2.0.0-alpha] - 2023-06-01
8
+ - update gltf-transform dependency to 4.x
9
+ - change: ensure ETC1S and UASTC textures are power of 2
10
+
11
+ - CLI: commands can be run with `needle-gltf <command>`
12
+ - CLI: add `--cache` to allow disabling cache with `--cache False` (default true)
13
+ - CLI: add `--stats` to produce `.needle.stats.json` (default false)
14
+ - CLI: change `--output` to be the optional second argument
15
+
16
+ ## [1.6.0-alpha.7] - 2023-05-30
8
17
  - fix: npx cli command return wrong error due to `packGLTF` not returning a booelan
18
+ - fix: mjs script import paths
9
19
 
10
20
  ## [1.6.0-alpha.5] - 2023-05-28
11
21
  - fix: determine build pipeline version
package/README.md CHANGED
@@ -8,6 +8,24 @@ Includes support for generating LOD quality level for textures and meshes that c
8
8
  - [Example on Github](https://github.com/needle-engine/progressive-meshes-sample)
9
9
 
10
10
 
11
+ ## Usage
12
+
13
+ In general all files that have been processed by this package are valid glTF/GLB files.
14
+ The glTF Build Pipeline package is meant to be used as a last step before delivery.
15
+ That means:
16
+ - files are meant to be loaded e.g. in a web runtime like three.js
17
+ - files are not to be opened in Blender again to be further edited
18
+
19
+ To load progressively enhanced glTF files in any three.js based engine see examples at [@needle-tools/gltf-progressive](https://www.npmjs.com/package/@needle-tools/gltf-progressive)
20
+
21
+ ### Installation
22
+
23
+ `npx @needle-tools/gltf-build-pipeline@latest`
24
+
25
+ ### Commands
26
+ - `gltf-build-pipeline help` - use help to see options for each command
27
+ - `gltf-build-pipeline compress <input>`
28
+ - `gltf-build-pipeline transform <input>`
11
29
 
12
30
  # Contact ✒️
13
31
  <b>[🌵 needle — tools for creators](https://needle.tools)</b> •
package/dist/package.json CHANGED
@@ -1,30 +1,51 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "1.6.0-alpha.6",
3
+ "version": "2.0.0-alpha",
4
4
  "description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
5
5
  "main": "index.mjs",
6
6
  "type": "module",
7
7
  "readme": "README.md",
8
+ "author": {
9
+ "name": "Needle",
10
+ "email": "help@needle.tools",
11
+ "url": "https://needle.tools/"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/needle-tools/needle-gltf-extensions.git"
16
+ },
8
17
  "bin": {
9
- "gltf-build-pipeline": "./dist/src/cli/index.js"
18
+ "gltf-build-pipeline": "./dist/src/cli/index.js",
19
+ "needle-gltf": "./dist/src/cli/index.js"
10
20
  },
11
21
  "scripts": {
12
22
  "start": "node scripts/index.mjs",
13
23
  "dev": "npm-watch compile",
24
+ "compile": "tsc || exit 1",
25
+ "clear-caches": "node scripts/clear-caches.mjs",
26
+ "limit-caches": "node scripts/limit-caches.mjs",
14
27
  "transform": "node scripts/index.mjs",
15
28
  "transform:make-progressive": "node scripts/make-progressive.mjs",
16
29
  "transform:pack-gltf": "node --no-experimental-fetch scripts/pack-gltf.mjs",
17
30
  "pack-gltf": "node --no-experimental-fetch scripts/pack-gltf.mjs",
18
- "dev:pack-gltf": "npm run compile && node scripts/pack-gltf.mjs",
19
- "clear-caches": "node scripts/clear-caches.mjs",
20
- "limit-caches": "node scripts/limit-caches.mjs",
21
- "compile": "tsc || exit 1"
31
+ "dev:pack-gltf": "npm run compile && node scripts/pack-gltf.mjs"
32
+ },
33
+ "devDependencies": {
34
+ "@img/sharp-win32-x64": "^0.33.4",
35
+ "@types/node": "^18.7.23",
36
+ "npm-watch": "^0.13.0",
37
+ "typescript": "^4.9.5"
38
+ },
39
+ "optionalDependencies": {
40
+ "@img/sharp-linux-arm": "^0.33.4",
41
+ "@img/sharp-win32-x64": "^0.33.4"
22
42
  },
23
43
  "dependencies": {
24
44
  "@donmccurdy/caporal": "~0.0.10",
25
- "@gltf-transform/core": "^3.10.1",
26
- "@gltf-transform/extensions": "^3.10.1",
27
- "@gltf-transform/functions": "^3.10.1",
45
+ "@gltf-transform/cli": "^4.0.1",
46
+ "@gltf-transform/core": "^4.0.1",
47
+ "@gltf-transform/extensions": "^4.0.1",
48
+ "@gltf-transform/functions": "^4.0.1",
28
49
  "command-exists": "^1.2.9",
29
50
  "command-line-args": "^5.2.1",
30
51
  "draco3dgltf": "^1.5.3",
@@ -36,7 +57,7 @@
36
57
  "ndarray-pixels": "^2.0.1",
37
58
  "p-limit": "3.1.0",
38
59
  "semver": "^7.3.7",
39
- "sharp": "^0.32.6",
60
+ "sharp": "^0.33.4",
40
61
  "tmp": "^0.2.1",
41
62
  "uuid": "^9.0.1"
42
63
  },
@@ -48,20 +69,5 @@
48
69
  "extensions": "ts",
49
70
  "quiet": false
50
71
  }
51
- },
52
- "devDependencies": {
53
- "@types/node": "^18.7.23",
54
- "nodemon": "^3.1.2",
55
- "npm-watch": "^0.13.0",
56
- "typescript": "^4.9.5"
57
- },
58
- "author": {
59
- "name": "Needle",
60
- "email": "help@needle.tools",
61
- "url": "https://needle.tools/"
62
- },
63
- "repository": {
64
- "type": "git",
65
- "url": "git+https://github.com/needle-tools/needle-gltf-extensions.git"
66
72
  }
67
73
  }
@@ -7,8 +7,8 @@ import { UsecaseOptions, createConfig, getConfig } from '../config/index.js';
7
7
  import { make_progressive } from '../transforms/needle_progressive.js';
8
8
  import { cacheSizeLimit, clearCache } from '../cache/cache.js';
9
9
  import { existsSync, statSync } from 'fs';
10
- import { foreachGLTF } from '../utils/fileutils.js';
11
- import { dirname } from 'path';
10
+ import { foreachGLTF, getDirName } from '../utils/fileutils.js';
11
+ import path, { dirname } from 'path';
12
12
  import { ERROR_CODES } from '../constants.js';
13
13
  // For testing / dev you can run `npm link` in the package directory
14
14
  // For removing the link run `npm rm --global @needle-tools/gltf-build-pipeline`
@@ -30,10 +30,12 @@ Caches are limited to ${cacheSizeLimit} MB disc space by default.
30
30
  Compress a single glTF file. The input file is compressed and written to the output file.
31
31
  `)
32
32
  .argument('<input>', "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed")
33
- .option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}] e.g. \`--usecase ${UsecaseOptions[0]}\"`, { validator: program.STRING })
33
+ .argument('<output>', "The output file. If not provided, the input file will be overwritten.", { default: "" })
34
+ .option("--usecase <usecase>", `The usecase for the compression allows you to determine how your asset is supposed to be used:\nThis will set compression and transform settings on a high level if no other settings are found.\nPossible options: [${UsecaseOptions.join(", ")}] e.g. \"--usecase ${UsecaseOptions[0]}\"`, { validator: program.STRING })
34
35
  .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 })
35
- .option("--output <output>", "The output file. If not provided, the input file will be overwritten.", { validator: program.STRING })
36
- .option("--output-stats", "The path to a JSON file where the stats will be written to.", { validator: program.STRING })
36
+ .option("--stats", "Output a stats file next to input file (or inside the input directory). It will be named as the input file/directory \"<input>.needle.stats.json\"", { validator: program.BOOLEAN, default: false })
37
+ .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 })
38
+ .option("--debug", "Enable for more verbose outout with \"--debug True\"", { validator: program.BOOLEAN, default: false })
37
39
  .action(async ({ logger, args, options }) => {
38
40
  logger.info("Transforming glTF file: " + args.input);
39
41
  const stats = {
@@ -54,8 +56,10 @@ Compress a single glTF file. The input file is compressed and written to the out
54
56
  config.usecase = options.usecase.toString();
55
57
  }
56
58
  }
59
+ const debug = options.debug === true;
57
60
  const file = args.input.toString();
58
- const outDir = statSync(file).isDirectory() ? file : dirname(file);
61
+ const isDirectory = statSync(file).isDirectory();
62
+ const outDir = isDirectory ? file : dirname(file);
59
63
  let outputIsDirectory = false;
60
64
  if (statSync(file).isDirectory() && options.output) {
61
65
  if (statSync(options.output.toString()).isDirectory()) {
@@ -73,17 +77,20 @@ Compress a single glTF file. The input file is compressed and written to the out
73
77
  const file_stats = [];
74
78
  const set = new Set();
75
79
  await foreachGLTF(file, async (file) => {
76
- let outputFile = options.output?.toString() || file;
80
+ let outputFile = args.output?.toString();
81
+ if (!outputFile?.length) {
82
+ outputFile = file;
83
+ }
77
84
  if (outputIsDirectory) {
78
85
  const filename = file.replaceAll("\\", "/").split("/").pop();
79
- outputFile = options.output?.toString() + "/" + filename;
86
+ outputFile = args.output?.toString() + "/" + filename;
80
87
  }
81
88
  const res = await packGLTF(file, outputFile, {
82
89
  config,
83
90
  packedFiles: set,
84
91
  stats,
85
- debug: false,
86
- useCache: true,
92
+ debug,
93
+ useCache: options.cache !== false,
87
94
  logger: logger,
88
95
  file_stats: file_stats,
89
96
  });
@@ -93,8 +100,9 @@ Compress a single glTF file. The input file is compressed and written to the out
93
100
  });
94
101
  printStats(stats);
95
102
  console.log("Stats: ", options);
96
- if (options.outputStats) {
97
- writeFileStatsToFile(file_stats, options.outputStats.toString());
103
+ if (options.stats) {
104
+ const statsFile = (isDirectory ? (path.join(file, getDirName(file))) : file) + ".needle.stats.json";
105
+ writeFileStatsToFile(file_stats, statsFile);
98
106
  }
99
107
  })
100
108
  .command("transform", "Transform (progressive loading) and compress glTF, GLB or VRM files")
@@ -103,10 +111,12 @@ This will produce multiple versions of the input file, each with a different lev
103
111
  Each version will be compressed and written to the output directory.
104
112
  `)
105
113
  .argument('<input>', "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed")
114
+ // .argument('<output>', "The output file. If not provided, the input file will be overwritten.", { default: "" })
106
115
  .option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}]`, { validator: program.STRING })
107
116
  .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 })
108
- // .option("--output <output>", "The output directory. If not provided, the input files will be overwritten.", { validator: program.STRING })
109
- .option("--output-stats", "The path to a JSON file where the stats will be written to.", { validator: program.STRING })
117
+ .option("--stats", "Output a stats file next to input file (or inside the input directory). It will be named as the input file/directory \"<input>.needle.stats.json\"", { validator: program.BOOLEAN, default: false })
118
+ .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 })
119
+ .option("--debug", "Enable for more verbose outout with \"--debug True\"", { validator: program.BOOLEAN, default: false })
110
120
  .action(async ({ logger, args, options }) => {
111
121
  logger.info("Transforming glTF file: " + args.input);
112
122
  const stats = {
@@ -127,19 +137,22 @@ Each version will be compressed and written to the output directory.
127
137
  config.usecase = options.usecase.toString();
128
138
  }
129
139
  }
130
- const file = args.input.toString();
131
- const outDir = statSync(file).isDirectory() ? file : dirname(file);
140
+ let file = args.input.toString();
141
+ const isDirectory = statSync(file).isDirectory();
142
+ const outDir = isDirectory ? file : dirname(file);
132
143
  const file_stats = [];
133
144
  const set = new Set();
145
+ const useCache = options.cache !== false;
146
+ const debug = options.debug === true;
134
147
  await foreachGLTF(file, async (file) => {
135
- logger.info("> Transform " + file);
148
+ logger.info("→ Transform " + file);
136
149
  const progressive_results = new Array();
137
150
  await make_progressive({
138
151
  path: file,
139
152
  results: progressive_results,
140
153
  config,
141
- debug: false,
142
- useCache: true,
154
+ debug,
155
+ useCache,
143
156
  }).catch(err => {
144
157
  logger.error(err);
145
158
  process.exit(ERROR_CODES.PROGRESSIVE_FAILED);
@@ -148,25 +161,24 @@ Each version will be compressed and written to the output directory.
148
161
  config,
149
162
  stats,
150
163
  packedFiles: set,
151
- debug: false,
152
- useCache: true,
164
+ debug,
165
+ useCache,
153
166
  logger: logger,
154
167
  file_stats,
155
168
  };
156
169
  for (const res of progressive_results) {
157
- logger.info("> Compress " + res);
158
170
  if (await packGLTF(res, res, opts) === false) {
159
171
  process.exit(ERROR_CODES.PACKING_FAILED);
160
172
  }
161
173
  }
162
- logger.info("> Compress " + file);
163
174
  if (await packGLTF(file, file, opts) === false) {
164
175
  process.exit(ERROR_CODES.PACKING_FAILED);
165
176
  }
166
177
  });
167
178
  printStats(stats);
168
- if (options.outputStats) {
169
- writeFileStatsToFile(file_stats, options.outputStats.toString());
179
+ if (options.stats) {
180
+ const statsFile = (isDirectory ? (path.join(file, getDirName(file))) : file) + ".needle.stats.json";
181
+ writeFileStatsToFile(file_stats, statsFile);
170
182
  }
171
183
  });
172
184
  program.run();
@@ -15,7 +15,7 @@ export interface IOpaqueProperty {
15
15
  /** register a reference so gltf-transform will not remove it in e.g. dedup
16
16
  * https://github.com/donmccurdy/glTF-Transform/issues/980#issuecomment-1585748202
17
17
  */
18
- setReference(name: string, value: any): any;
18
+ setReference(name: string, value: any, attributes?: Record<string, any>): void;
19
19
  }
20
20
  export interface IExtensionWriter {
21
21
  /** used by progressive texture extension to pass an extension between documents */
@@ -1,4 +1,4 @@
1
- import { Extension, ExtensionProperty, PropertyType } from '@gltf-transform/core';
1
+ import { Extension, ExtensionProperty, PropertyType, TextureChannel } from '@gltf-transform/core';
2
2
  import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
3
3
  const ALL_PROPERTY_TYPES = [];
4
4
  for (const key in PropertyType) {
@@ -16,6 +16,7 @@ export function createOpaqueExtension(name, types, opts) {
16
16
  class NEEDLE_opaque extends Extension {
17
17
  extensionName = EXTENSION_NAME;
18
18
  static EXTENSION_NAME = EXTENSION_NAME;
19
+ prewriteTypes = [PropertyType.MATERIAL];
19
20
  // private roots: Array<OpaqueProperty> = [];
20
21
  jsonPointerHandler;
21
22
  read(context) {
@@ -118,8 +119,8 @@ export function createOpaqueExtension(name, types, opts) {
118
119
  console.log("WARN: could potentially not find gltf transform texture for " + index);
119
120
  texture = context.textures[index];
120
121
  }
121
- this.jsonPointerHandler.read(context, texture, ext);
122
122
  const prop = this.createExtension();
123
+ this.jsonPointerHandler.read(context, texture, ext, prop);
123
124
  texture.setExtension(EXTENSION_NAME, prop);
124
125
  const handler = new TextureHandler(this.document, textureDefinition, EXTENSION_NAME, ext, debugLog);
125
126
  prop.add(handler);
@@ -142,8 +143,8 @@ export function createOpaqueExtension(name, types, opts) {
142
143
  if (!ext)
143
144
  return;
144
145
  const primitive = primitives[primitiveIndex];
145
- this.jsonPointerHandler.read(context, primitive, ext);
146
146
  const prop = this.createExtension();
147
+ this.jsonPointerHandler.read(context, primitive, ext, prop);
147
148
  primitive.setExtension(EXTENSION_NAME, prop);
148
149
  // this is invoked on write
149
150
  prop.add(ctx => {
@@ -164,8 +165,8 @@ export function createOpaqueExtension(name, types, opts) {
164
165
  if (ext) {
165
166
  this.lightmapWorkaround(context);
166
167
  const root = this.document.getRoot();
167
- this.jsonPointerHandler.read(context, root, ext);
168
168
  const prop = this.createExtension();
169
+ this.jsonPointerHandler.read(context, root, ext, prop);
169
170
  root.setExtension(EXTENSION_NAME, prop);
170
171
  prop.add(ctx => {
171
172
  ctx.jsonDoc.json.extensions = ctx.jsonDoc.json.extensions || {};
@@ -224,6 +225,13 @@ export function createOpaqueExtension(name, types, opts) {
224
225
  console.error("ERR: " + error);
225
226
  }
226
227
  }
228
+ prewrite(context, _propertyType) {
229
+ // Attempt resolve textures here so that the json.textures is already populated for cases where a texture isn't used by any material
230
+ // this is e.g. the case for textures that are just referenced by a component but not used by any texture
231
+ // https://linear.app/needle/issue/NE-4928
232
+ this.jsonPointerHandler.resolveAndWrite(context, "prewrite");
233
+ return this;
234
+ }
227
235
  write(context) {
228
236
  if (opts?.breakpoint === true)
229
237
  debugger;
@@ -232,7 +240,7 @@ export function createOpaqueExtension(name, types, opts) {
232
240
  }
233
241
  if (debugLog)
234
242
  console.log("WRITE " + EXTENSION_NAME);
235
- this.jsonPointerHandler.resolveAndWrite(context);
243
+ this.jsonPointerHandler.resolveAndWrite(context, "write");
236
244
  const root = this.document.getRoot();
237
245
  // for (const rootExt of this.roots) {
238
246
  // rootExt.onWrite(context, root);
@@ -313,11 +321,11 @@ export function createOpaqueExtension(name, types, opts) {
313
321
  extensionName = typeof EXTENSION_NAME;
314
322
  parentTypes = ExtensionPropertyTypes;
315
323
  propertyType = "NEEDLE_opaque_property";
316
- setReference(name, value) {
317
- this.setRef(name, value, { modifyChild: true });
318
- }
319
324
  init() {
320
325
  }
326
+ setReference(name, value, attributes = { modifyChild: true }) {
327
+ this.setRef(name, value, attributes);
328
+ }
321
329
  writers = [];
322
330
  add(writer) {
323
331
  if (typeof writer === "function") {
@@ -520,7 +528,7 @@ class JsonPointerHandler {
520
528
  this.document = document;
521
529
  this.id = id;
522
530
  }
523
- read(context, prop, obj, ext, key, fullPath) {
531
+ read(context, extProp, obj, prop, key, fullPath) {
524
532
  const value = key !== undefined ? obj[key] : obj;
525
533
  // the pointer path contains the full path from the root object to the json pointer
526
534
  if (key !== undefined) {
@@ -535,7 +543,7 @@ class JsonPointerHandler {
535
543
  case 'object':
536
544
  if (value !== null && value !== undefined) {
537
545
  for (const key of Object.keys(value)) {
538
- this.read(context, prop, value, ext, key, fullPath);
546
+ this.read(context, extProp, value, prop, key, fullPath);
539
547
  }
540
548
  }
541
549
  break;
@@ -568,6 +576,15 @@ class JsonPointerHandler {
568
576
  pointer = new JsonPointer(path, res, value);
569
577
  pointer.debug = this.debug;
570
578
  this.map.set(value, pointer);
579
+ // Assume all texture slots are being used. The referenceName is the json pointer right now - usually this is e.g. `baseColorTexture` (the name ends up in the slots array)
580
+ const referenceName = value;
581
+ // const slots = listTextureSlots(object);
582
+ prop.setReference(referenceName, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
583
+ // make sure there's a reference to the buffer to avoid dedup or the buffer being removed because it MIGHT not be used by any node
584
+ // TODO: how do we get the buffer that the texture/image is currently using - here we just take the first buffer but this might not be correct
585
+ const buffer = this.document.getRoot().listBuffers()[0];
586
+ if (buffer)
587
+ prop.setReference("buffer", buffer);
571
588
  }
572
589
  else {
573
590
  console.warn("Texture not found", fullPath, value);
@@ -585,23 +602,24 @@ class JsonPointerHandler {
585
602
  if (this.debug)
586
603
  console.log("Read pointer", value);
587
604
  const index = parseInt(value.substring("/materials/".length));
588
- const mat = context.materials[index];
589
- if (mat) {
605
+ const object = context.materials[index];
606
+ if (object) {
590
607
  // set a reference to the material to avoid dedup or the material being removed because it MIGHT not be used by any node
591
608
  // BUT a component is referencing the material so we don't want it ever to be removed
592
609
  // see https://github.com/donmccurdy/glTF-Transform/issues/980#issuecomment-1585748202
593
- if (ext) {
594
- ext.setReference(value, mat);
610
+ if (prop) {
611
+ prop.setReference(value, object);
595
612
  }
596
613
  let pointer = this.map.get(value);
597
614
  if (!pointer) {
598
615
  const definition = context.jsonDoc.json.materials[index];
599
- const res = new MaterialIndexResolver(this.document, mat, definition);
616
+ const res = new MaterialIndexResolver(this.document, object, definition);
600
617
  res.debug = this.debug;
601
618
  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)
602
619
  pointer = new JsonPointer("/materials/", res, value);
603
620
  pointer.debug = this.debug;
604
621
  this.map.set(value, pointer);
622
+ prop.setReference(value, object);
605
623
  }
606
624
  pointer?.register(obj, key, fullPath);
607
625
  }
@@ -625,6 +643,7 @@ class JsonPointerHandler {
625
643
  pointer.debug = this.debug;
626
644
  pointer.register(obj, key, fullPath);
627
645
  this.map.set(dictKey, pointer);
646
+ prop.setReference(dictKey, object);
628
647
  }
629
648
  }
630
649
  else if (
@@ -663,9 +682,7 @@ class JsonPointerHandler {
663
682
  pointer.debug = this.debug;
664
683
  pointer.register(obj, key, fullPath);
665
684
  this.map.set(dictKey, pointer);
666
- if (ext) {
667
- ext.setReference(dictKey, object);
668
- }
685
+ prop.setReference(dictKey, object);
669
686
  }
670
687
  }
671
688
  break;
@@ -673,10 +690,10 @@ class JsonPointerHandler {
673
690
  break;
674
691
  }
675
692
  }
676
- resolveAndWrite(context) {
693
+ resolveAndWrite(context, step) {
677
694
  this.map?.forEach(ptr => {
678
- ptr.resolve(context);
679
- ptr.write();
695
+ if (ptr.resolve(context, step))
696
+ ptr.write();
680
697
  });
681
698
  }
682
699
  }
@@ -698,8 +715,12 @@ class JsonPointer {
698
715
  this._props.push(prop);
699
716
  this._debugInfo.push(fullpath);
700
717
  }
701
- newPointer;
702
- resolve(context) {
718
+ newPointer = undefined;
719
+ resolve(context, step) {
720
+ // If the pointer was already resolved, we don't need to do it again
721
+ if (this.newPointer !== undefined) {
722
+ return true;
723
+ }
703
724
  //@ts-ignore
704
725
  this.resolver.debug = this.debug;
705
726
  const index = this.resolver.resolve(context);
@@ -712,12 +733,20 @@ class JsonPointer {
712
733
  }
713
734
  if (this.debug)
714
735
  console.log(`RESOLVED pointer ${this.oldPointer} to → ${this.newPointer}`);
736
+ return true;
715
737
  }
716
738
  else {
717
- console.log("ERR: 🧨 FAILED to resolve pointer", this.pointerBase, ` used by ${this._debugInfo.join(", ")}`);
739
+ // If the pointer could not be resolved during write we need to log an error
740
+ if (step === "write")
741
+ console.log("ERR: 🧨 FAILED to resolve pointer", this.pointerBase, ` used by ${this._debugInfo.join(", ")}`);
718
742
  }
743
+ return false;
719
744
  }
745
+ _didWrite = false;
720
746
  write() {
747
+ if (this._didWrite)
748
+ return;
749
+ this._didWrite = false;
721
750
  for (let i = 0; i < this._objects.length; i++) {
722
751
  const obj = this._objects[i];
723
752
  const prop = this._props[i];
@@ -4,9 +4,9 @@ import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
4
4
  import draco3d from 'draco3dgltf';
5
5
  import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
6
6
  import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';
7
- import { resample } from '@gltf-transform/functions';
7
+ import { dedup, prune, resample } from '@gltf-transform/functions';
8
8
  import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS, NEEDLE_compression_texture, NEEDLE_mesh_compression, } from '../extensions/index.js';
9
- import { isMeshLOD, needle_animation_transform, needle_asset, needle_mesh_transform, needle_texture_transform, } from '../transforms/index.js';
9
+ import { isLOD, isMeshLOD, needle_animation_transform, needle_asset, needle_mesh_transform, needle_texture_transform, } from '../transforms/index.js';
10
10
  import { createNewFilestats, writeNodeIO } from "../utils/index.js";
11
11
  import { addToCache, getHash, tryGetFromCache, } from "../cache/index.js";
12
12
  import { getConfig } from '../config/index.js';
@@ -19,10 +19,7 @@ export async function packGLTF(inputFile, outputFile, options) {
19
19
  const { debug, stats, packedFiles, useCache, logger } = options;
20
20
  if (!existsSync(inputFile)) {
21
21
  logger.error("WARN: can not pack, file does not exist: " + inputFile);
22
- return;
23
- }
24
- if (debug && inputFile.includes(".debug.")) {
25
- return true;
22
+ return false;
26
23
  }
27
24
  // check if it's in the cache.
28
25
  // We only need to check mesh LOD files because image LODs are cache via our toktx transform
@@ -48,7 +45,6 @@ export async function packGLTF(inputFile, outputFile, options) {
48
45
  const config = options.config || getConfig();
49
46
  const prevSizeInMB = statSync(inputFile).size / 1024 / 1024;
50
47
  logger.info(`→ Compressing ${prevSizeInMB.toFixed(1)} MB: ${inputFile}`);
51
- // const isProgressiveAsset = isLOD(file);
52
48
  const startTime = Date.now();
53
49
  const io = new NodeIO()
54
50
  .registerExtensions(ALL_EXTENSIONS)
@@ -65,7 +61,7 @@ export async function packGLTF(inputFile, outputFile, options) {
65
61
  });
66
62
  const document = await io.read(inputFile);
67
63
  /** @ts-ignore */
68
- document.getLogger().verbosity = Verbosity.ERROR;
64
+ document.getLogger().verbosity = options.debug ? Verbosity.DEBUG : Verbosity.WARN;
69
65
  let totalGPUTextureMemory = 0;
70
66
  /** @type {Array<import('@gltf-transform/core').Transform>} */
71
67
  const transforms = [
@@ -83,18 +79,17 @@ export async function packGLTF(inputFile, outputFile, options) {
83
79
  needle_mesh_transform({ file: inputFile, config }, { ratio: 0.75, error: 0.0005 }),
84
80
  needle_animation_transform({}),
85
81
  resample(),
86
- // TODO: dedup removes skybox texture
87
- // dedup(),
82
+ dedup(),
83
+ // prune(),
88
84
  ];
89
- // TODO: progressive assets need to make sure that e.g. textures are used
90
- // if (!isProgressiveAsset) {
91
- // transforms.push(prune());
92
- // }
85
+ const isProgressiveAsset = isLOD(inputFile);
86
+ // TODO: progressive mesh LODs should not be pruned right now because it will remove e.g. UVs from meshes (and probably blend shapes too) (so we just not do it for any progressive asset)
87
+ if (!isProgressiveAsset) {
88
+ transforms.push(prune());
89
+ }
93
90
  await document.transform(...transforms);
94
91
  if (!outputFile)
95
92
  outputFile = inputFile;
96
- if (debug)
97
- outputFile += ".debug.glb";
98
93
  await writeNodeIO(io, outputFile, document);
99
94
  if (useCache && cacheKey != undefined && existsSync(outputFile))
100
95
  addToCache(cacheKey, readFileSync(outputFile));
@@ -116,7 +111,7 @@ export async function packGLTF(inputFile, outputFile, options) {
116
111
  return true;
117
112
  }
118
113
  catch (err) {
119
- logger.error("ERR: packing " + inputFile + " failed: " + err.message);
114
+ logger.error(`packing ${path.basename(inputFile)} failed: ${err}`);
120
115
  if (debug)
121
116
  throw err;
122
117
  return false;
@@ -52,7 +52,6 @@ async function simplifierTransform(document, _context, _options) {
52
52
  options.simplifier = simplifier;
53
53
  const weldOptions = {
54
54
  ...WELD_DEFAULTS,
55
- tolerance: 0.00000000001,
56
55
  overwrite: true,
57
56
  };
58
57
  for (const mesh of document.getRoot().listMeshes()) {
@@ -99,8 +98,8 @@ async function simplifierTransform(document, _context, _options) {
99
98
  }
100
99
  export async function compressPrimitive(document, mesh, prim, index, weldOptions, meshOptions) {
101
100
  const logger = document.getLogger();
102
- logger.debug(`weld: primitive[${index}] of mesh "${mesh.getName()}" → tolerance: ${weldOptions.tolerance}`);
103
- weldPrimitive(document, prim, weldOptions);
101
+ logger.debug(`weld: primitive[${index}] of mesh "${mesh.getName()}"`);
102
+ weldPrimitive(prim, weldOptions);
104
103
  // make sure we wait for the mesh simplifier to be ready
105
104
  if (meshOptions.simplifier && meshOptions.simplifier?.ready instanceof Promise) {
106
105
  const res = await meshOptions.simplifier?.ready
@@ -116,7 +115,7 @@ export async function compressPrimitive(document, mesh, prim, index, weldOptions
116
115
  return;
117
116
  }
118
117
  logger.debug(`simplify: primitive[${index}] of mesh "${mesh.getName()}" → ratio: ${meshOptions.ratio}, error: ${meshOptions.error}, lockBorder: ${meshOptions.lockBorder}`);
119
- return simplifyPrimitive(document, prim, meshOptions);
118
+ return simplifyPrimitive(prim, meshOptions);
120
119
  }
121
120
  // function updatePrimitive(prim: Primitive, simplifier: any, positionArray, dstIndicesArray) {
122
121
  // const position = prim.getAttribute('POSITION')!;