@needle-tools/gltf-build-pipeline 2.14.0-alpha → 2.14.0-next.66c769b

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/README.md CHANGED
@@ -23,9 +23,103 @@ To load progressively enhanced glTF files in any three.js based engine see examp
23
23
 
24
24
  `npx @needle-tools/gltf-build-pipeline@latest`
25
25
 
26
+ ### CLI Commands
27
+
28
+ - `stats <input> <output>` - Calculate basic stats
29
+ - `detailed-stats <input>` - Analyze file structure, component counts, and mesh merging potential
30
+ - `mesh-merge <input> <output> [options]` - Merge meshes with shared materials to reduce draw calls
31
+ - `--min-vertices <n>` - Only merge meshes with >n vertices
32
+ - `--max-vertices <n>` - Target max vertices per merged mesh (default: 65535)
33
+ - `--dry-run` - Analyze potential without writing file
34
+ - `transform <input> <output> [options]` - Generate LODs and compress
35
+ - `clear-caches` - Clear all caches
36
+
26
37
  ### Commands
27
38
  - `help` - use help to view options
28
39
 
40
+ ## Development
41
+
42
+ ### Building
43
+
44
+ ```bash
45
+ cd package
46
+ npm install
47
+ npx tsc
48
+ ```
49
+
50
+ ### Running the CLI locally
51
+
52
+ After building, you can run the CLI directly with node:
53
+
54
+ ```bash
55
+ # Transform a GLB file
56
+ node dist/cli/index.js transform <input.glb> <output-directory> [options]
57
+
58
+ # Example with verbose output
59
+ node dist/cli/index.js transform "path/to/model.glb" "path/to/output" --verbose True
60
+
61
+ # View help
62
+ node dist/cli/index.js help
63
+ ```
64
+
65
+ **Note:** On Windows, using `node` directly avoids path issues with special characters (spaces, `&`, etc.) that can occur with `npx`.
66
+
67
+ ### Running Tests
68
+
69
+ ```bash
70
+ cd package
71
+
72
+ # Run all tests (uses Node.js built-in test runner)
73
+ npm test
74
+
75
+ # Run tests directly
76
+ node --test src/tests/
77
+ ```
78
+
79
+ ### Test Files
80
+
81
+ Test files are located in `src/tests/`:
82
+ - `transform-paths.test.mjs` - Tests path handling with special characters (spaces, commas, ampersands, parentheses)
83
+ - `path-handling.test.mjs` - Additional path handling tests
84
+
85
+ ### Clearing the Cache
86
+
87
+ The build pipeline caches processed meshes and textures. To force reprocessing:
88
+
89
+ ```powershell
90
+ # Windows
91
+ Remove-Item "$env:LOCALAPPDATA\Temp\Needle\build-pipeline-cache" -Recurse -Force
92
+
93
+ # Or find the cache location from the log output:
94
+ # "Limit cache size to X MB at C:\Users\...\Temp\Needle\build-pipeline-cache"
95
+ ```
96
+
97
+ ### Performance Profiling
98
+
99
+ When processing large files, the pipeline outputs detailed performance statistics:
100
+ - Mesh processing throughput (meshes/s, vertices/s)
101
+ - Cache hit/miss rates
102
+ - Time breakdown (simplification, I/O, hashing)
103
+
104
+ Example output:
105
+ ```
106
+ [NEEDLE_progressive]: ═══════════════════════════════════════════════════════════
107
+ [NEEDLE_progressive]: MESH PROCESSING SUMMARY for "model.glb"
108
+ [NEEDLE_progressive]: ───────────────────────────────────────────────────────────
109
+ [NEEDLE_progressive]: Total time: 45.23s
110
+ [NEEDLE_progressive]: Meshes: 150 processed, 20 skipped (of 170 total)
111
+ [NEEDLE_progressive]: Vertices: 1,234,567 input → 456,789 output
112
+ [NEEDLE_progressive]: LOD levels generated: 450
113
+ [NEEDLE_progressive]: Cache: 100 hits, 350 misses
114
+ [NEEDLE_progressive]: Time breakdown:
115
+ [NEEDLE_progressive]: - Simplification: 30.12s
116
+ [NEEDLE_progressive]: - I/O Write: 10.45s
117
+ [NEEDLE_progressive]: - I/O Read (cache): 2.34s
118
+ [NEEDLE_progressive]: - Hashing: 2.32s
119
+ [NEEDLE_progressive]: Throughput: 27,293 vertices/s
120
+ [NEEDLE_progressive]: ═══════════════════════════════════════════════════════════
121
+ ```
122
+
29
123
  # Contact
30
124
 
31
125
  For licensing questions please contact us.
package/dist/cli/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
  import { program } from '@donmccurdy/caporal';
3
3
  import { calculateStats, packGLTF } from "../scripts/index.js";
4
4
  import { printStats, writeFileStatsToFile } from '../utils/stats.js';
5
+ import { calculateDetailedStats, printDetailedStats } from '../scripts/detailed-stats.js';
6
+ import { runMeshMerge } from '../scripts/mesh-merge.js';
5
7
  import { getVersion } from '../utils/version.js';
6
8
  import { UsecaseOptions, createConfig, getConfig } from '../config/index.js';
7
9
  import { isLOD, make_progressive } from '../transforms/needle_progressive.js';
@@ -50,6 +52,58 @@ Caches are limited to ${cacheSizeLimit} MB disc space by default.
50
52
  }
51
53
  logger.info("Writing stats to: " + output);
52
54
  writeFileStatsToFile(filestats, output);
55
+ })
56
+ .command("detailed-stats", "Calculate detailed glTF stats with top N items")
57
+ .help(`
58
+ Analyzes a glTF/GLB/VRM file and prints detailed statistics including:
59
+ - File size and load time
60
+ - Extensions used
61
+ - Hierarchy depth/breadth
62
+ - Top 5 biggest meshes, accessors, textures
63
+ - Needle component counts
64
+ `)
65
+ .argument('<input>', "Path to a glTF, GLB or VRM file")
66
+ .action(async ({ logger, args }) => {
67
+ logger.level = "info";
68
+ const input = args.input.toString();
69
+ console.log(`\nAnalyzing: ${input}`);
70
+ console.log('Please wait...\n');
71
+ try {
72
+ const stats = await calculateDetailedStats(input);
73
+ printDetailedStats(stats);
74
+ // Explicitly exit to ensure hanging promises (like meshopt/draco) don't keep process alive
75
+ process.exit(0);
76
+ }
77
+ catch (error) {
78
+ logger.error(`Failed to analyze file: ${error}`);
79
+ process.exit(1);
80
+ }
81
+ })
82
+ .command("mesh-merge", "Merge meshes with shared materials")
83
+ .help(`
84
+ Merges primitives that share the same material and vertex attributes.
85
+ Reduces draw calls but may increase file size slightly due to less reuse of shared accessors.
86
+ `)
87
+ .argument('<input>', "Path to input glTF/GLB file")
88
+ .argument('<output>', "Path to output glTF/GLB file")
89
+ .option('--min-vertices <n>', 'Minimum vertices to include in merge', { validator: program.NUMBER, default: 0 })
90
+ .option('--max-vertices <n>', 'Maximum vertices per merged mesh', { validator: program.NUMBER, default: 65535 })
91
+ .option('--dry-run', 'Analyze only, do not merge', { validator: program.BOOLEAN, default: false })
92
+ .action(async ({ logger, args, options }) => {
93
+ logger.level = "info";
94
+ try {
95
+ await runMeshMerge(args.input.toString(), args.output.toString(), {
96
+ minVertices: Number(options.minVertices),
97
+ maxVerticesPerMesh: Number(options.maxVertices),
98
+ dryRun: !!options.dryRun,
99
+ logger
100
+ });
101
+ process.exit(0);
102
+ }
103
+ catch (error) {
104
+ logger.error(`Failed to merge meshes: ${error}`);
105
+ process.exit(1);
106
+ }
53
107
  })
54
108
  .command("transform", "Transform (progressive loading) and compress glTF, GLB or VRM files")
55
109
  .help(`
@@ -0,0 +1,180 @@
1
+ import { type MeshMergeAnalysis } from "../transforms/needle_mesh_merge.js";
2
+ /**
3
+ * Distribution band for histogram display
4
+ */
5
+ export interface DistributionBand {
6
+ label: string;
7
+ min: number;
8
+ max: number;
9
+ count: number;
10
+ percentage: number;
11
+ }
12
+ /**
13
+ * Detailed stats analysis for a glTF file
14
+ */
15
+ export interface DetailedStats {
16
+ file: {
17
+ path: string;
18
+ sizeBytes: number;
19
+ sizeMB: number;
20
+ loadTimeMs: number;
21
+ };
22
+ extensions: {
23
+ used: string[];
24
+ required: string[];
25
+ };
26
+ hierarchy: {
27
+ nodeCount: number;
28
+ maxDepth: number;
29
+ maxBreadth: number;
30
+ sceneCount: number;
31
+ rootNodeCount: number;
32
+ };
33
+ meshes: {
34
+ count: number;
35
+ primitiveCount: number;
36
+ totalVertices: number;
37
+ totalTriangles: number;
38
+ memoryBytes: number;
39
+ top5ByVertices: {
40
+ name: string;
41
+ vertices: number;
42
+ triangles: number;
43
+ primitives: number;
44
+ }[];
45
+ distribution: DistributionBand[];
46
+ };
47
+ accessors: {
48
+ count: number;
49
+ totalBytes: number;
50
+ top5BySize: {
51
+ name: string;
52
+ type: string;
53
+ count: number;
54
+ bytes: number;
55
+ }[];
56
+ };
57
+ buffers: {
58
+ count: number;
59
+ totalBytes: number;
60
+ top5BySize: {
61
+ name: string;
62
+ bytes: number;
63
+ }[];
64
+ };
65
+ textures: {
66
+ count: number;
67
+ totalMemoryBytes: number;
68
+ totalGPUBytes: number;
69
+ mimeTypeMismatchCount: number;
70
+ top5ByPixelArea: {
71
+ name: string;
72
+ width: number;
73
+ height: number;
74
+ pixelArea: number;
75
+ mimeType: string;
76
+ mismatch: boolean;
77
+ bytes: number;
78
+ gpuBytes: number;
79
+ }[];
80
+ top5ByFileSize: {
81
+ name: string;
82
+ bytes: number;
83
+ mimeType: string;
84
+ }[];
85
+ formats: string[];
86
+ distribution: DistributionBand[];
87
+ };
88
+ materials: {
89
+ count: number;
90
+ top5ByTextureCount: {
91
+ name: string;
92
+ textureCount: number;
93
+ }[];
94
+ top5ByUsage: {
95
+ name: string;
96
+ usageCount: number;
97
+ }[];
98
+ };
99
+ animations: {
100
+ count: number;
101
+ channelCount: number;
102
+ totalBytes: number;
103
+ };
104
+ nodesWithExtensions: {
105
+ count: number;
106
+ extensionCounts: Record<string, number>;
107
+ };
108
+ needleComponents: {
109
+ nodeCount: number;
110
+ componentCount: number;
111
+ componentTypes: Record<string, number>;
112
+ };
113
+ validation: {
114
+ errorCount: number;
115
+ warningCount: number;
116
+ infoCount: number;
117
+ top5Errors: string[];
118
+ top5Warnings: string[];
119
+ };
120
+ emptyNodes: {
121
+ count: number;
122
+ examples: string[];
123
+ };
124
+ tinyMeshes: {
125
+ count: number;
126
+ examples: {
127
+ name: string;
128
+ vertices: number;
129
+ triangles: number;
130
+ }[];
131
+ dedupAnalysis?: {
132
+ uniqueGeometries: number;
133
+ uniqueStructural: number;
134
+ uniqueByteHashes: number;
135
+ dedupPotentialPct: string;
136
+ topDuplicates: {
137
+ count: number;
138
+ uniqueMaterials: number;
139
+ materials: string[];
140
+ }[];
141
+ };
142
+ };
143
+ allMeshesDedupAnalysis?: {
144
+ totalMeshes: number;
145
+ totalVertices: number;
146
+ uniqueByContent: number;
147
+ uniqueByBytes: number;
148
+ uniqueByStructure: number;
149
+ dedupPotentialPct: string;
150
+ topDuplicates: {
151
+ hash: string;
152
+ count: number;
153
+ }[];
154
+ perBucketStats: {
155
+ bucket: string;
156
+ meshes: number;
157
+ uniqueContent: number;
158
+ uniqueStructural: number;
159
+ dedupPotentialPct: string;
160
+ }[];
161
+ };
162
+ meshMergeAnalysis?: MeshMergeAnalysis;
163
+ materialBuckets: {
164
+ bucketCount: number;
165
+ totalMaterialsInBuckets: number;
166
+ top5Buckets: {
167
+ baseColor: string;
168
+ materialCount: number;
169
+ names: string[];
170
+ }[];
171
+ };
172
+ }
173
+ /**
174
+ * Calculate detailed stats for a glTF file
175
+ */
176
+ export declare function calculateDetailedStats(inputFile: string): Promise<DetailedStats>;
177
+ /**
178
+ * Print detailed stats to console
179
+ */
180
+ export declare function printDetailedStats(stats: DetailedStats): void;