@needle-tools/gltf-build-pipeline 2.5.0 → 2.6.0-alpha.1

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,32 +4,39 @@ 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
- ## [2.4.0] - 2023-09-11
7
+ ## [2.6.0-alpha] - 2024-12-16
8
+ - add: create placeholder textures for missing jpg, png, webp, gif, tif on disc
9
+ - fix: compressing a gltf file to a different output directory does not falsely delete old external textures on disc in the input directory
10
+
11
+ ## [2.5.0] - 2024-12-16
12
+ - release
13
+
14
+ ## [2.4.0] - 2024-09-11
8
15
  - release
9
16
 
10
- ## [2.4.0-beta.5] - 2023-09-06
17
+ ## [2.4.0-beta.5] - 2024-09-06
11
18
  - fix: issue where mesh LODs would in some cases be compressed with "draco" while the root file would be compressed with "mesh-opt" if not explicitly specified in the root file. This would cause invalid rendering and randomly scaled LOD meshes. Now mesh LODs will always use the same mesh compression. [NE 5297](https://linear.app/needle/issue/NE-5297)
12
19
  - change: update gltf-transform dependency to 4.0.8
13
20
 
14
- ## [2.4.0-beta] - 2023-08-13
21
+ ## [2.4.0-beta] - 2024-08-13
15
22
  - fix: issue where a mesh lod is removed during second processing of a directory that contains existing mesh lods
16
23
 
17
- ## [2.4.0-alpha.3] - 2023-08-11
24
+ ## [2.4.0-alpha.3] - 2024-08-11
18
25
  - fix: issue where files would not correctly be overriden in subdirectories when no output directory was specified
19
26
 
20
- ## [2.4.0-alpha.2] - 2023-06-29
27
+ ## [2.4.0-alpha.2] - 2024-06-29
21
28
  - fix: prevent joining/flattening of progressive assets
22
29
 
23
- ## [2.4.0-alpha.1] - 2023-06-26
30
+ ## [2.4.0-alpha.1] - 2024-06-26
24
31
  - add: heuristic for choosing mesh compression if not explictly configured
25
32
 
26
- ## [2.3.0-alpha.2] - 2023-06-26
33
+ ## [2.3.0-alpha.2] - 2024-06-26
27
34
  - change: usecase `product` does compress with UASTC again (for certain slots)
28
35
 
29
- ## [2.3.0-alpha.1] - 2023-06-23
36
+ ## [2.3.0-alpha.1] - 2024-06-23
30
37
  - change: bump gltf-transform to 4.0.2 to fix meshopt skinning regression [1439](https://github.com/donmccurdy/glTF-Transform/pull/1439)
31
38
 
32
- ## [2.3.0-alpha] - 2023-06-22
39
+ ## [2.3.0-alpha] - 2024-06-22
33
40
  - add: flatten and join for `product` usecase
34
41
 
35
42
  ## [2.2.1-alpha.8] - 2023-06-22
@@ -7,7 +7,7 @@ import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';
7
7
  import { dedup, flatten, join, 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
9
  import { isLOD, isMeshLOD, needle_animation_transform, needle_asset, needle_mesh_transform, needle_texture_transform, } from '../transforms/index.js';
10
- import { getOutputPath, writeNodeIO } from "../utils/index.js";
10
+ import { getOutputPath, ioTryReadWithMissingResources, writeNodeIO } from "../utils/index.js";
11
11
  import { addToCache, getHash, tryGetFromCache, } from "../cache/index.js";
12
12
  /** Compress a glTF file
13
13
  */
@@ -42,6 +42,11 @@ export async function packGLTF(inputFile, outputFile, options) {
42
42
  }
43
43
  try {
44
44
  const config = options.config;
45
+ if (!outputFile)
46
+ outputFile = inputFile;
47
+ else {
48
+ outputFile = getOutputPath(inputFile, outputFile);
49
+ }
45
50
  const prevSizeInMB = statSync(inputFile).size / 1024 / 1024;
46
51
  logger.info(`→ Compressing ${prevSizeInMB.toFixed(1)} MB: ${inputFile}`);
47
52
  const startTime = Date.now();
@@ -58,7 +63,7 @@ export async function packGLTF(inputFile, outputFile, options) {
58
63
  'meshopt.decoder': MeshoptDecoder,
59
64
  'meshopt.encoder': MeshoptEncoder,
60
65
  });
61
- const document = await io.read(inputFile);
66
+ const document = await ioTryReadWithMissingResources(io, inputFile); // await io.read(inputFile);
62
67
  /** @ts-ignore */
63
68
  document.getLogger().verbosity = options.verbose ? Verbosity.DEBUG : options.debug ? Verbosity.INFO : Verbosity.WARN;
64
69
  let totalGPUTextureMemory = 0;
@@ -67,6 +72,7 @@ export async function packGLTF(inputFile, outputFile, options) {
67
72
  needle_asset(),
68
73
  needle_texture_transform({
69
74
  file: inputFile,
75
+ outfile: outputFile,
70
76
  useCache,
71
77
  noToktx: false,
72
78
  debug: debug,
@@ -96,12 +102,7 @@ export async function packGLTF(inputFile, outputFile, options) {
96
102
  transforms.push(join({ keepNamed: false }));
97
103
  }
98
104
  await document.transform(...transforms);
99
- if (!outputFile)
100
- outputFile = inputFile;
101
- else {
102
- outputFile = getOutputPath(inputFile, outputFile);
103
- logger.info(`⇄ Writing to ${outputFile}`);
104
- }
105
+ logger.info(`⇄ Writing to ${outputFile}`);
105
106
  await writeNodeIO(io, outputFile, document);
106
107
  if (useCache && cacheKey != undefined && existsSync(outputFile))
107
108
  addToCache(cacheKey, readFileSync(outputFile));
@@ -119,7 +120,7 @@ export async function packGLTF(inputFile, outputFile, options) {
119
120
  return true;
120
121
  }
121
122
  catch (err) {
122
- logger.error(`packing ${path.basename(inputFile)} failed: ${err}`);
123
+ logger.error(`Packing \"${path.basename(inputFile)}\" failed. Error: ${err.message}`);
123
124
  if (debug)
124
125
  throw err;
125
126
  return false;
@@ -4,6 +4,7 @@ import { Config } from '../config/index.js';
4
4
  export interface NeedleTransformContext {
5
5
  /** full path to the file being processed */
6
6
  file: string;
7
+ outfile: string;
7
8
  config: Config;
8
9
  debug?: boolean;
9
10
  useCache: boolean;
@@ -7,7 +7,7 @@ import path, { basename, resolve } from 'path';
7
7
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
8
8
  import { addMetaToCache, addToCache, getHash, getKey, tryGetFromCache, tryGetMetaFromCache } from '../cache/index.js';
9
9
  import sharp from "sharp";
10
- import { detectTextureCompressionMode, ensureIsDirectory, foreachGLTF, generateGuid, getOutputPath, isLightmap, setLightmapName, validateImageMimeType, writeNodeIO } from '../utils/index.js';
10
+ import { detectTextureCompressionMode, ensureIsDirectory, foreachGLTF, generateGuid, getOutputPath, isLightmap, setLightmapName, ioTryReadWithMissingResources, validateImageMimeType, writeNodeIO } from '../utils/index.js';
11
11
  import { needle_asset, writeNeedleAsset } from './needle_asset.js';
12
12
  import { MeshoptDecoder, MeshoptSimplifier } from 'meshoptimizer';
13
13
  import { compressPrimitive, determineMeshCompression } from './needle_mesh_transform.js';
@@ -61,7 +61,7 @@ async function onProcess(file, opts) {
61
61
  'draco3d.decoder': await draco3d.createDecoderModule(),
62
62
  'draco3d.encoder': await draco3d.createEncoderModule(),
63
63
  });
64
- let document = await io.read(file);
64
+ let document = await ioTryReadWithMissingResources(io, file); // await io.read(file);
65
65
  if (document) {
66
66
  const logger = document.getLogger();
67
67
  //@ts-ignore
@@ -195,10 +195,15 @@ async function processTexture(texture, index, document, logger, context) {
195
195
  if (successFullyProcessed) {
196
196
  const newUri = texture.getURI();
197
197
  if (originalUri && originalUri != newUri) {
198
- const fileDirectory = dirname(context.file);
199
- const originalPath = fileDirectory + "/" + originalUri;
200
- if (existsSync(originalPath)) {
201
- console.log(`INFO: Texture moved \"${originalUri}\" → \"${newUri}\"`);
198
+ const inDirectory = dirname(context.file);
199
+ const outDirectory = dirname(context.outfile);
200
+ const originalPath = inDirectory + "/" + originalUri;
201
+ // if the file output directory is the same as the input directory
202
+ // (meaning compression runs in the same directory)
203
+ // we remove the original file (since it effectively gets replaced)
204
+ // otherwise we dont touch the original file
205
+ if (inDirectory == outDirectory && existsSync(originalPath)) {
206
+ console.log(`INFO: Remove old texture \"${originalUri}\" → \"${newUri}\"`);
202
207
  rmSync(originalPath);
203
208
  }
204
209
  }
@@ -1,5 +1,6 @@
1
- import { NodeIO, Document } from "@gltf-transform/core";
1
+ import { NodeIO, Document, PlatformIO } from "@gltf-transform/core";
2
2
  /**
3
3
  * Handle writing glTf, GLB and VRM files
4
4
  */
5
5
  export declare function writeNodeIO(nodeio: NodeIO, filename: string, document: Document): Promise<string>;
6
+ export declare function ioTryReadWithMissingResources(io: PlatformIO, filepath: string, iteration?: number): Promise<Document>;
@@ -1,4 +1,5 @@
1
1
  import { writeFile } from "fs/promises";
2
+ import sharp from "sharp";
2
3
  /**
3
4
  * Handle writing glTf, GLB and VRM files
4
5
  */
@@ -12,3 +13,68 @@ export async function writeNodeIO(nodeio, filename, document) {
12
13
  await nodeio.write(filename, document);
13
14
  return filename;
14
15
  }
16
+ const openFileRegex = /open \'(?<path>.+?)'/;
17
+ const createdFiles = new Set();
18
+ export async function ioTryReadWithMissingResources(io, filepath, iteration = 0) {
19
+ const document = await io.read(filepath)
20
+ .catch(err => { return err; });
21
+ if (document instanceof Error) {
22
+ const err = document;
23
+ // Handle glTF files where resource are missing
24
+ if (iteration < 100 && err.message?.includes("ENOENT")) {
25
+ try {
26
+ const match = err.message.match(openFileRegex);
27
+ if (match.groups?.path) {
28
+ let missingPath = match.groups.path;
29
+ // check if it's an URL
30
+ if (missingPath.startsWith("http")) {
31
+ throw err;
32
+ }
33
+ if (createdFiles.has(missingPath)) {
34
+ throw err;
35
+ }
36
+ createdFiles.add(missingPath);
37
+ console.warn(`Creating empty file for missing resource: ${missingPath}`);
38
+ const ext = missingPath.split('.').pop();
39
+ if (!ext) {
40
+ console.warn(`Could not determine extension for ${missingPath}`);
41
+ throw err;
42
+ }
43
+ const extLower = ext?.toLowerCase();
44
+ let retry = false;
45
+ // https://sharp.pixelplumbing.com/api-output#tofile
46
+ switch (extLower) {
47
+ case "jpeg":
48
+ case "jpg":
49
+ case "png":
50
+ case "webp":
51
+ case "tiff":
52
+ case "gif":
53
+ {
54
+ await sharp({
55
+ create: {
56
+ width: 64,
57
+ height: 64,
58
+ channels: 4,
59
+ background: { r: 255, g: 255, b: 255, alpha: 1 }
60
+ }
61
+ })
62
+ .toFormat(ext)
63
+ .toFile(missingPath);
64
+ retry = true;
65
+ }
66
+ }
67
+ if (retry) {
68
+ console.warn(`Created empty file for missing resource: ${missingPath}`);
69
+ return ioTryReadWithMissingResources(io, filepath, iteration + 1);
70
+ }
71
+ }
72
+ }
73
+ catch (internal_error) {
74
+ console.warn(internal_error);
75
+ }
76
+ }
77
+ throw err;
78
+ }
79
+ return document;
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "2.5.0",
3
+ "version": "2.6.0-alpha.1",
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",