@needle-tools/gltf-build-pipeline 2.15.0-next.fd1fff4 → 2.15.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 +5 -0
- package/dist/cli/index.js +20 -1
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +1 -0
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +59 -0
- package/dist/scripts/pack-gltf.js +3 -2
- package/dist/utils/analytics.d.ts +24 -0
- package/dist/utils/analytics.js +126 -0
- package/dist/utils/stats.js +13 -6
- package/dist/utils/version.gen.d.ts +1 -1
- package/dist/utils/version.gen.js +1 -1
- package/package.json +1 -1
- package/tools/pmrem/basis/linux/SHA_d8f36fcbb09e79614e96baaed5b1e1cd5d2f5e73.txt +1 -0
- package/tools/pmrem/basis/linux/basisu +0 -0
- package/tools/pmrem/basis/osx/SHA_d8f36fcbb09e79614e96baaed5b1e1cd5d2f5e73.txt +1 -0
- package/tools/pmrem/basis/osx/basisu +0 -0
- package/tools/pmrem/basis/wasm/SHA_d8f36fcbb09e79614e96baaed5b1e1cd5d2f5e73.txt +1 -0
- package/tools/pmrem/basis/wasm/basis_encoder_threads.js +2 -6693
- package/tools/pmrem/basis/wasm/basis_encoder_threads.wasm +0 -0
- package/tools/pmrem/basis/win/SHA_d8f36fcbb09e79614e96baaed5b1e1cd5d2f5e73.txt +1 -0
- package/tools/pmrem/basis/win/basisu.exe +0 -0
- package/tools/pmrem/basis/win/ocl_kernels.cl +66 -90
- package/tools/pmrem/pkg/pmrem_wasm_bg.wasm +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ 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.15.0] - 2026-03-20
|
|
8
|
+
- add: EXR environment map compression (PMREM / FASTHDR) for Needle Engine, supporting both Unity and Blender exports
|
|
9
|
+
- fix: cache hashing stability (circular reference handling)
|
|
10
|
+
- fix: progressive texture GUID generation now based on texture content hash
|
|
11
|
+
|
|
7
12
|
## [2.14.0] - 2026-01-30
|
|
8
13
|
- add: KHR_materials_pbrSpecularGlossiness extension support
|
|
9
14
|
- fix: reduce `resample` tolerance to `1e-6` to avoid animation issues with some assets
|
package/dist/cli/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync } from 'fs';
|
|
|
10
10
|
import { ensureIsDirectory, foreachGLTF, isDirectory } from '../utils/fileutils.js';
|
|
11
11
|
import path, { resolve } from 'path';
|
|
12
12
|
import { ERROR_CODES } from '../constants.js';
|
|
13
|
+
import { trackPipelineStart, trackPipelineEnd, trackError } from '../utils/analytics.js';
|
|
13
14
|
// For testing / dev you can run `npm link` in the package directory
|
|
14
15
|
// For removing the link run `npm rm --global @needle-tools/gltf-build-pipeline`
|
|
15
16
|
program
|
|
@@ -39,7 +40,7 @@ Caches are limited to ${cacheSizeLimit} MB disc space by default.
|
|
|
39
40
|
const stats = await calculateStats(file);
|
|
40
41
|
filestats.push(stats);
|
|
41
42
|
});
|
|
42
|
-
let output = args.
|
|
43
|
+
let output = args.output?.toString();
|
|
43
44
|
if (!output) {
|
|
44
45
|
if (isDirectory(input)) {
|
|
45
46
|
output = path.join(input, "stats.needle.json");
|
|
@@ -73,6 +74,8 @@ Each version will be compressed and written to the output directory.
|
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
76
|
logger.info(`[Needle Build Pipeline] v${getVersion()} — Transform '${args.input}'`);
|
|
77
|
+
const pipelineMode = [options.progressive && 'progressive', options.compress && 'compress'].filter(Boolean).join('+');
|
|
78
|
+
trackPipelineStart({ mode: pipelineMode, usecase: options.usecase?.toString() });
|
|
76
79
|
const stats = {
|
|
77
80
|
startTime: Date.now(),
|
|
78
81
|
totalGPUMemoryInMB: 0,
|
|
@@ -103,6 +106,7 @@ Each version will be compressed and written to the output directory.
|
|
|
103
106
|
const outputIsDirectory = output && isDirectory(output);
|
|
104
107
|
if (inputIsDirectory && output && !outputIsDirectory) {
|
|
105
108
|
logger.error("When <input> is a directory then <output> must also be a directory");
|
|
109
|
+
trackError(new Error('Invalid args: input is directory but output is not'), { mode: pipelineMode });
|
|
106
110
|
process.exit(ERROR_CODES.INVALID_ARGS);
|
|
107
111
|
}
|
|
108
112
|
// Handle the output bath being relative to the input path
|
|
@@ -126,6 +130,7 @@ Each version will be compressed and written to the output directory.
|
|
|
126
130
|
}
|
|
127
131
|
else {
|
|
128
132
|
logger.error(`ERR: Previously found file is now missing: \"${file}\"`);
|
|
133
|
+
trackError(new Error(`Lost file: ${file}`), { mode: pipelineMode, usecase: options.usecase?.toString(), file });
|
|
129
134
|
process.exit(ERROR_CODES.LOST_FILE);
|
|
130
135
|
}
|
|
131
136
|
}
|
|
@@ -148,8 +153,10 @@ Each version will be compressed and written to the output directory.
|
|
|
148
153
|
logger,
|
|
149
154
|
}).catch(err => {
|
|
150
155
|
logger.error(err);
|
|
156
|
+
trackError(err, { mode: pipelineMode, usecase: options.usecase?.toString(), file });
|
|
151
157
|
process.exit(ERROR_CODES.PROGRESSIVE_FAILED);
|
|
152
158
|
}) === false) {
|
|
159
|
+
trackError(new Error('Progressive processing returned false'), { mode: pipelineMode, usecase: options.usecase?.toString(), file });
|
|
153
160
|
process.exit(ERROR_CODES.PROGRESSIVE_FAILED);
|
|
154
161
|
}
|
|
155
162
|
if (verbose) {
|
|
@@ -170,6 +177,7 @@ Each version will be compressed and written to the output directory.
|
|
|
170
177
|
if (progressive_results.length > 0) {
|
|
171
178
|
for (const res of progressive_results) {
|
|
172
179
|
if (await packGLTF(res, res, opts) === false) {
|
|
180
|
+
trackError(new Error('Packing failed'), { mode: pipelineMode, usecase: options.usecase?.toString(), file: res });
|
|
173
181
|
process.exit(ERROR_CODES.PACKING_FAILED);
|
|
174
182
|
}
|
|
175
183
|
}
|
|
@@ -178,12 +186,22 @@ Each version will be compressed and written to the output directory.
|
|
|
178
186
|
// We can not just check if the file is in the list of results because the output path might have changed the name
|
|
179
187
|
else {
|
|
180
188
|
if (await packGLTF(file, output, opts) === false) {
|
|
189
|
+
trackError(new Error('Packing failed'), { mode: pipelineMode, usecase: options.usecase?.toString(), file });
|
|
181
190
|
process.exit(ERROR_CODES.PACKING_FAILED);
|
|
182
191
|
}
|
|
183
192
|
}
|
|
184
193
|
}
|
|
185
194
|
});
|
|
186
195
|
printStats(stats, logger, config);
|
|
196
|
+
trackPipelineEnd({
|
|
197
|
+
mode: pipelineMode,
|
|
198
|
+
usecase: config?.usecase?.toString(),
|
|
199
|
+
filesProcessed: stats.totalFilesProcessed,
|
|
200
|
+
sizeBefore_MB: stats.totalFileSizeInMBBefore,
|
|
201
|
+
sizeAfter_MB: stats.totalFileSizeInMB,
|
|
202
|
+
gpuMemory_MB: stats.totalGPUMemoryInMB,
|
|
203
|
+
duration_ms: Date.now() - stats.startTime,
|
|
204
|
+
});
|
|
187
205
|
if (useCache)
|
|
188
206
|
limitCacheSize();
|
|
189
207
|
});
|
|
@@ -194,5 +212,6 @@ program.run().catch(err => {
|
|
|
194
212
|
// ignore
|
|
195
213
|
return;
|
|
196
214
|
}
|
|
215
|
+
trackError(err);
|
|
197
216
|
throw err;
|
|
198
217
|
});
|
|
@@ -23,5 +23,6 @@ export interface IExtensionWriter {
|
|
|
23
23
|
write(context: WriterContext, prop: IExtensibleProperty): void;
|
|
24
24
|
}
|
|
25
25
|
export declare let currentGenerator: string;
|
|
26
|
+
export declare function setOpaqueInputFile(path: string): void;
|
|
26
27
|
export declare function createOpaqueExtension(name: string, types: PropertyType | PropertyType[], opts?: Options): IOpaqueExtension;
|
|
27
28
|
export {};
|
|
@@ -3,11 +3,16 @@ import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
|
|
|
3
3
|
import { isLightmap } from '../../utils/texture.js';
|
|
4
4
|
import { NEEDLE_progressive_texture_settings } from '../NEEDLE_progressive_texture_settings/index.js';
|
|
5
5
|
import { testAssert } from '../../utils/test-assert.js';
|
|
6
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
6
8
|
const ALL_PROPERTY_TYPES = [];
|
|
7
9
|
for (const key in PropertyType) {
|
|
8
10
|
ALL_PROPERTY_TYPES.push(PropertyType[key]);
|
|
9
11
|
}
|
|
10
12
|
export let currentGenerator = "";
|
|
13
|
+
/** Set before io.read() to allow resolving external file paths (e.g. .exr) in component data */
|
|
14
|
+
let _opaqueInputFile = "";
|
|
15
|
+
export function setOpaqueInputFile(path) { _opaqueInputFile = path; }
|
|
11
16
|
// an opaque extension can be used to pass data through the gltf transform process
|
|
12
17
|
// it should also be able to re-write/fix json pointers after the transformation which might have shuffled them around
|
|
13
18
|
export function createOpaqueExtension(name, types, opts) {
|
|
@@ -539,6 +544,10 @@ class JsonPointerHandler {
|
|
|
539
544
|
debug = false;
|
|
540
545
|
breakPoint = false;
|
|
541
546
|
VRM_0_MaterialIndexRegex = new RegExp(/materialProperties\/(?<material_index>\d+)\//g);
|
|
547
|
+
/** Cache of external EXR files already injected as Textures (keyed by filename) */
|
|
548
|
+
externalExrTextures = new Map();
|
|
549
|
+
/** External EXR references that need URI resolution at write time */
|
|
550
|
+
externalExrPointers = [];
|
|
542
551
|
constructor(document, extension_name) {
|
|
543
552
|
this.document = document;
|
|
544
553
|
this.extension_name = extension_name;
|
|
@@ -620,6 +629,37 @@ class JsonPointerHandler {
|
|
|
620
629
|
else
|
|
621
630
|
console.warn("WARN: failed registering pointer", fullPath, value);
|
|
622
631
|
}
|
|
632
|
+
// External EXR file reference (e.g. "StudioHDRI_ferndale_studio_04_1k.exr" in a ReflectionProbe)
|
|
633
|
+
// Inject the file into the document as a Texture so PMREM can process it
|
|
634
|
+
if (!isTexturePointer && value.match(/\.exr$/i)) {
|
|
635
|
+
const inputDir = _opaqueInputFile ? dirname(_opaqueInputFile) : '';
|
|
636
|
+
const exrPath = inputDir ? join(inputDir, value) : '';
|
|
637
|
+
if (exrPath && existsSync(exrPath)) {
|
|
638
|
+
// Check if we already injected this file
|
|
639
|
+
let texture = this.externalExrTextures.get(value);
|
|
640
|
+
if (!texture) {
|
|
641
|
+
const exrBytes = readFileSync(exrPath);
|
|
642
|
+
texture = this.document.createTexture(value)
|
|
643
|
+
.setImage(new Uint8Array(exrBytes))
|
|
644
|
+
.setMimeType('image/exr')
|
|
645
|
+
.setURI(value);
|
|
646
|
+
this.externalExrTextures.set(value, texture);
|
|
647
|
+
if (this.debug)
|
|
648
|
+
console.log(`Injected external EXR "${value}" as Texture`);
|
|
649
|
+
}
|
|
650
|
+
// Set a reference so gltf-transform doesn't prune it
|
|
651
|
+
prop.setReference(`exr_${value}`, texture, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
|
|
652
|
+
const buffer = this.document.getRoot().listBuffers()[0];
|
|
653
|
+
if (buffer)
|
|
654
|
+
prop.setReference(`exr_buffer_${value}`, buffer);
|
|
655
|
+
// At write time, update the property to the texture's current URI
|
|
656
|
+
// (PMREM will have updated it from "foo.exr" to "foo.pmrem.ktx2")
|
|
657
|
+
this.externalExrPointers.push({ obj, key, texture });
|
|
658
|
+
}
|
|
659
|
+
else if (exrPath) {
|
|
660
|
+
console.warn(`WARN: External EXR not found: ${exrPath}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
623
663
|
if (value.startsWith("/materials/")) {
|
|
624
664
|
if (this.debug)
|
|
625
665
|
console.log("Read pointer", value);
|
|
@@ -777,6 +817,25 @@ class JsonPointerHandler {
|
|
|
777
817
|
if (ptr.resolve(context, step))
|
|
778
818
|
ptr.write();
|
|
779
819
|
});
|
|
820
|
+
// Resolve external EXR pointers: update component data with the texture's current URI
|
|
821
|
+
for (const { obj, key, texture } of this.externalExrPointers) {
|
|
822
|
+
const uri = texture.getURI() || texture.getName() || '';
|
|
823
|
+
if (uri) {
|
|
824
|
+
if (this.debug)
|
|
825
|
+
console.log(`< Resolved external EXR: ${obj[key]} → ${uri}`);
|
|
826
|
+
obj[key] = uri;
|
|
827
|
+
// Ensure the image definition in the output JSON has the URI set.
|
|
828
|
+
// When PMREM clears image data (setImage(null)) for external textures,
|
|
829
|
+
// gltf-transform's core writer won't set a uri or bufferView on the image.
|
|
830
|
+
const imageIndex = context.imageIndexMap.get(texture);
|
|
831
|
+
if (imageIndex !== undefined) {
|
|
832
|
+
const images = context.jsonDoc.json.images;
|
|
833
|
+
if (images && images[imageIndex] && !images[imageIndex].uri) {
|
|
834
|
+
images[imageIndex].uri = uri;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
780
839
|
}
|
|
781
840
|
}
|
|
782
841
|
class JsonPointer {
|
|
@@ -5,7 +5,7 @@ import draco3d from 'draco3dgltf';
|
|
|
5
5
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
6
6
|
import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';
|
|
7
7
|
import { dedup, metalRough, prune, resample } from '@gltf-transform/functions';
|
|
8
|
-
import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS, NEEDLE_compression_texture, NEEDLE_mesh_compression, NEEDLE_pmrem, NEEDLE_lightmaps_ext, } from '../extensions/index.js';
|
|
8
|
+
import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS, NEEDLE_compression_texture, NEEDLE_mesh_compression, NEEDLE_pmrem, NEEDLE_lightmaps_ext, setOpaqueInputFile, } 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
10
|
import { getOutputPath, getVersion, ioTryReadWithMissingResources, writeNodeIO } from "../utils/index.js";
|
|
11
11
|
import { addToCache, getHash, tryGetFromCache, } from "../cache/index.js";
|
|
@@ -66,8 +66,9 @@ export async function packGLTF(inputFile, outputFile, options) {
|
|
|
66
66
|
'meshopt.decoder': MeshoptDecoder,
|
|
67
67
|
'meshopt.encoder': MeshoptEncoder,
|
|
68
68
|
});
|
|
69
|
-
// Set input file path so
|
|
69
|
+
// Set input file path so extensions can resolve external EXR files
|
|
70
70
|
NEEDLE_lightmaps_ext.inputFile = inputFile;
|
|
71
|
+
setOpaqueInputFile(inputFile);
|
|
71
72
|
const document = await ioTryReadWithMissingResources(io, inputFile);
|
|
72
73
|
if (options.logger) {
|
|
73
74
|
document.setLogger(options.logger);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface PipelineAnalyticsInfo {
|
|
2
|
+
/** 'compress' | 'progressive' | 'compress+progressive' */
|
|
3
|
+
mode: string;
|
|
4
|
+
usecase?: string;
|
|
5
|
+
filesProcessed: number;
|
|
6
|
+
sizeBefore_MB: number;
|
|
7
|
+
sizeAfter_MB: number;
|
|
8
|
+
gpuMemory_MB: number;
|
|
9
|
+
duration_ms: number;
|
|
10
|
+
extensionsUsed?: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Send a "pipeline_start" event. */
|
|
13
|
+
export declare function trackPipelineStart(info: {
|
|
14
|
+
mode: string;
|
|
15
|
+
usecase?: string;
|
|
16
|
+
}): void;
|
|
17
|
+
/** Send a pipeline error event. Deduplicates identical errors within 1 minute. */
|
|
18
|
+
export declare function trackError(error: unknown, context?: {
|
|
19
|
+
mode?: string;
|
|
20
|
+
usecase?: string;
|
|
21
|
+
file?: string;
|
|
22
|
+
}): void;
|
|
23
|
+
/** Send a "pipeline_end" event with size reduction and extension info. */
|
|
24
|
+
export declare function trackPipelineEnd(info: PipelineAnalyticsInfo): void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import { getVersion } from './version.js';
|
|
4
|
+
const RYBBIT_URL = 'https://needle.tools/api/v1/rum/t';
|
|
5
|
+
const SITE_ID = '5c44f60eb2d7';
|
|
6
|
+
/** Build a User-Agent string that UA parsers recognize as desktop + correct OS. */
|
|
7
|
+
function getUserAgent() {
|
|
8
|
+
const v = getVersion();
|
|
9
|
+
const platform = os.platform();
|
|
10
|
+
const release = os.release();
|
|
11
|
+
const arch = os.arch();
|
|
12
|
+
const node = process.version;
|
|
13
|
+
// Map Node's os.platform() to a UA-parser-friendly OS token
|
|
14
|
+
let osToken;
|
|
15
|
+
if (platform === 'darwin') {
|
|
16
|
+
// macOS: "Macintosh; Intel Mac OS X 25_3_0"
|
|
17
|
+
const ver = release.replace(/\./g, '_');
|
|
18
|
+
osToken = `Macintosh; Intel Mac OS X ${ver}`;
|
|
19
|
+
}
|
|
20
|
+
else if (platform === 'win32') {
|
|
21
|
+
// Windows: "Windows NT 10.0; Win64; x64"
|
|
22
|
+
osToken = `Windows NT ${release}; Win64; ${arch}`;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// Linux / other: "X11; Linux x86_64"
|
|
26
|
+
osToken = `X11; Linux ${arch}`;
|
|
27
|
+
}
|
|
28
|
+
return `Mozilla/5.0 (${osToken}) NeedleBuildPipeline/${v} Node/${node}`;
|
|
29
|
+
}
|
|
30
|
+
/** Fire-and-forget analytics event. Never throws, never blocks. */
|
|
31
|
+
function sendEvent(eventName, properties, type = 'custom_event') {
|
|
32
|
+
try {
|
|
33
|
+
const body = JSON.stringify({
|
|
34
|
+
site_id: SITE_ID,
|
|
35
|
+
type,
|
|
36
|
+
event_name: eventName,
|
|
37
|
+
properties: JSON.stringify(properties),
|
|
38
|
+
user_agent: getUserAgent(),
|
|
39
|
+
});
|
|
40
|
+
const url = new URL(RYBBIT_URL);
|
|
41
|
+
const req = https.request({
|
|
42
|
+
hostname: url.hostname,
|
|
43
|
+
path: url.pathname,
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
'Content-Length': Buffer.byteLength(body),
|
|
48
|
+
},
|
|
49
|
+
timeout: 5000,
|
|
50
|
+
});
|
|
51
|
+
req.on('error', () => { });
|
|
52
|
+
req.on('timeout', () => { req.destroy(); });
|
|
53
|
+
req.end(body);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// silent
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Send a "pipeline_start" event. */
|
|
60
|
+
export function trackPipelineStart(info) {
|
|
61
|
+
sendEvent('pipeline_start', {
|
|
62
|
+
version: getVersion(),
|
|
63
|
+
mode: info.mode,
|
|
64
|
+
usecase: info.usecase,
|
|
65
|
+
platform: process.platform,
|
|
66
|
+
arch: process.arch,
|
|
67
|
+
node: process.version,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** Dedup cache to avoid logging the same error repeatedly in one session */
|
|
71
|
+
const recentErrors = new Map();
|
|
72
|
+
const ERROR_DEDUP_MS = 60_000;
|
|
73
|
+
/** Send a pipeline error event. Deduplicates identical errors within 1 minute. */
|
|
74
|
+
export function trackError(error, context) {
|
|
75
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
76
|
+
const message = (err.message || 'Unknown error').substring(0, 500);
|
|
77
|
+
const stack = (err.stack || '').substring(0, 2000);
|
|
78
|
+
const errorName = err.name || 'Error';
|
|
79
|
+
// Dedup
|
|
80
|
+
const key = `${errorName}|${message}`;
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const cached = recentErrors.get(key);
|
|
83
|
+
if (cached && now - cached.time < ERROR_DEDUP_MS) {
|
|
84
|
+
cached.count++;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
recentErrors.set(key, { time: now, count: (cached?.count || 0) + 1 });
|
|
88
|
+
const properties = {
|
|
89
|
+
version: getVersion(),
|
|
90
|
+
error_name: errorName + (cached && cached.count > 1 ? ` x${cached.count}` : ''),
|
|
91
|
+
message,
|
|
92
|
+
platform: process.platform,
|
|
93
|
+
arch: process.arch,
|
|
94
|
+
node: process.version,
|
|
95
|
+
};
|
|
96
|
+
if (stack)
|
|
97
|
+
properties.stack = stack;
|
|
98
|
+
if (context?.mode)
|
|
99
|
+
properties.mode = context.mode;
|
|
100
|
+
if (context?.usecase)
|
|
101
|
+
properties.usecase = context.usecase;
|
|
102
|
+
if (context?.file)
|
|
103
|
+
properties.file = context.file;
|
|
104
|
+
sendEvent(errorName, properties, 'error');
|
|
105
|
+
}
|
|
106
|
+
/** Send a "pipeline_end" event with size reduction and extension info. */
|
|
107
|
+
export function trackPipelineEnd(info) {
|
|
108
|
+
const reductionPct = info.sizeBefore_MB > 0
|
|
109
|
+
? ((1 - info.sizeAfter_MB / info.sizeBefore_MB) * 100).toFixed(1)
|
|
110
|
+
: '0';
|
|
111
|
+
sendEvent('pipeline_end', {
|
|
112
|
+
version: getVersion(),
|
|
113
|
+
mode: info.mode,
|
|
114
|
+
usecase: info.usecase,
|
|
115
|
+
files: info.filesProcessed,
|
|
116
|
+
size_before_mb: +info.sizeBefore_MB.toFixed(2),
|
|
117
|
+
size_after_mb: +info.sizeAfter_MB.toFixed(2),
|
|
118
|
+
reduction_pct: +reductionPct,
|
|
119
|
+
gpu_memory_mb: +info.gpuMemory_MB.toFixed(2),
|
|
120
|
+
duration_ms: info.duration_ms,
|
|
121
|
+
extensions: info.extensionsUsed?.join(','),
|
|
122
|
+
platform: process.platform,
|
|
123
|
+
arch: process.arch,
|
|
124
|
+
node: process.version,
|
|
125
|
+
});
|
|
126
|
+
}
|
package/dist/utils/stats.js
CHANGED
|
@@ -32,7 +32,11 @@ class Stats {
|
|
|
32
32
|
copyright: asset.copyright,
|
|
33
33
|
extras: asset.extras,
|
|
34
34
|
};
|
|
35
|
-
|
|
35
|
+
let report = null;
|
|
36
|
+
try {
|
|
37
|
+
report = inspect(document);
|
|
38
|
+
}
|
|
39
|
+
catch { /* e.g. unsupported KTX2 colorModel */ }
|
|
36
40
|
this.size = size;
|
|
37
41
|
this.json = calculateJSONDocumentInfo(json);
|
|
38
42
|
this.nodes = root.listNodes().length;
|
|
@@ -163,8 +167,8 @@ function calculateMeshInfo(document, report) {
|
|
|
163
167
|
const meshes = root.listMeshes();
|
|
164
168
|
const res = new MeshInfo();
|
|
165
169
|
res.count = meshes.length;
|
|
166
|
-
res.errors = report
|
|
167
|
-
res.warnings = report
|
|
170
|
+
res.errors = report?.meshes.errors;
|
|
171
|
+
res.warnings = report?.meshes.warnings;
|
|
168
172
|
for (const mesh of root.listMeshes()) {
|
|
169
173
|
const primitives = mesh.listPrimitives();
|
|
170
174
|
res.primitives += primitives.length;
|
|
@@ -209,15 +213,18 @@ function calculateTextureInfo(document, report) {
|
|
|
209
213
|
const res = new TextureInfo();
|
|
210
214
|
res.count = textures.length;
|
|
211
215
|
res.formats = textures.map(t => t.getMimeType());
|
|
212
|
-
res.errors = report
|
|
213
|
-
res.warnings = report
|
|
216
|
+
res.errors = report?.textures.errors;
|
|
217
|
+
res.warnings = report?.textures.warnings;
|
|
214
218
|
res.gpu_memory = 0;
|
|
215
219
|
res.memory = 0;
|
|
216
220
|
for (const texture of textures) {
|
|
217
221
|
const image = texture.getImage();
|
|
218
222
|
const mimetype = texture.getMimeType();
|
|
219
223
|
if (image && mimetype) {
|
|
220
|
-
|
|
224
|
+
try {
|
|
225
|
+
res.gpu_memory += ImageUtils.getVRAMByteLength(image, mimetype);
|
|
226
|
+
}
|
|
227
|
+
catch { /* unsupported KTX2 colorModel (e.g. ASTC HDR) */ }
|
|
221
228
|
res.memory += image.byteLength;
|
|
222
229
|
}
|
|
223
230
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "2.15.
|
|
1
|
+
export declare const version = "2.15.1";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "2.15.
|
|
1
|
+
export const version = "2.15.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.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",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
d8f36fcbb09e79614e96baaed5b1e1cd5d2f5e73
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
d8f36fcbb09e79614e96baaed5b1e1cd5d2f5e73
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
d8f36fcbb09e79614e96baaed5b1e1cd5d2f5e73
|