@littlecarlito/blorktools 0.50.4 → 0.51.0

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 (114) hide show
  1. package/bin/cli.js +69 -0
  2. package/package.json +13 -7
  3. package/src/asset_debugger/axis-indicator/axis-indicator.css +6 -0
  4. package/src/asset_debugger/axis-indicator/axis-indicator.html +20 -0
  5. package/src/asset_debugger/axis-indicator/axis-indicator.js +822 -0
  6. package/src/asset_debugger/debugger-scene/debugger-scene.css +142 -0
  7. package/src/asset_debugger/debugger-scene/debugger-scene.html +80 -0
  8. package/src/asset_debugger/debugger-scene/debugger-scene.js +791 -0
  9. package/src/asset_debugger/header/header.css +73 -0
  10. package/src/asset_debugger/header/header.html +24 -0
  11. package/src/asset_debugger/header/header.js +224 -0
  12. package/src/asset_debugger/index.html +76 -0
  13. package/src/asset_debugger/landing-page/landing-page.css +396 -0
  14. package/src/asset_debugger/landing-page/landing-page.html +81 -0
  15. package/src/asset_debugger/landing-page/landing-page.js +611 -0
  16. package/src/asset_debugger/loading-splash/loading-splash.css +195 -0
  17. package/src/asset_debugger/loading-splash/loading-splash.html +22 -0
  18. package/src/asset_debugger/loading-splash/loading-splash.js +59 -0
  19. package/src/asset_debugger/loading-splash/preview-loading-splash.js +66 -0
  20. package/src/asset_debugger/main.css +14 -0
  21. package/src/asset_debugger/modals/examples-modal/examples-modal.css +41 -0
  22. package/src/asset_debugger/modals/examples-modal/examples-modal.html +18 -0
  23. package/src/asset_debugger/modals/examples-modal/examples-modal.js +111 -0
  24. package/src/asset_debugger/modals/examples-modal/examples.js +125 -0
  25. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.css +452 -0
  26. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.html +87 -0
  27. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.js +675 -0
  28. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.css +219 -0
  29. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.html +20 -0
  30. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.js +548 -0
  31. package/src/asset_debugger/modals/settings-modal/settings-modal.css +103 -0
  32. package/src/asset_debugger/modals/settings-modal/settings-modal.html +158 -0
  33. package/src/asset_debugger/modals/settings-modal/settings-modal.js +475 -0
  34. package/src/asset_debugger/panels/asset-panel/asset-panel.css +263 -0
  35. package/src/asset_debugger/panels/asset-panel/asset-panel.html +123 -0
  36. package/src/asset_debugger/panels/asset-panel/asset-panel.js +136 -0
  37. package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.css +94 -0
  38. package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.js +312 -0
  39. package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.css +129 -0
  40. package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.js +486 -0
  41. package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.css +545 -0
  42. package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.js +538 -0
  43. package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.css +70 -0
  44. package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.js +586 -0
  45. package/src/asset_debugger/panels/world-panel/world-panel.css +364 -0
  46. package/src/asset_debugger/panels/world-panel/world-panel.html +173 -0
  47. package/src/asset_debugger/panels/world-panel/world-panel.js +1891 -0
  48. package/src/asset_debugger/router.js +190 -0
  49. package/src/asset_debugger/util/animation/playback/animation-playback-controller.js +150 -0
  50. package/src/asset_debugger/util/animation/playback/animation-preview-controller.js +316 -0
  51. package/src/asset_debugger/util/animation/playback/css3d-bounce-controller.js +400 -0
  52. package/src/asset_debugger/util/animation/playback/css3d-reversal-controller.js +821 -0
  53. package/src/asset_debugger/util/animation/render/css3d-prerender-controller.js +696 -0
  54. package/src/asset_debugger/util/animation/render/debug-texture-factory.js +0 -0
  55. package/src/asset_debugger/util/animation/render/iframe2texture-render-controller.js +199 -0
  56. package/src/asset_debugger/util/animation/render/image2texture-prerender-controller.js +461 -0
  57. package/src/asset_debugger/util/animation/render/pbr-material-factory.js +82 -0
  58. package/src/asset_debugger/util/common.css +280 -0
  59. package/src/asset_debugger/util/data/animation-classifier.js +323 -0
  60. package/src/asset_debugger/util/data/duplicate-handler.js +20 -0
  61. package/src/asset_debugger/util/data/glb-buffer-manager.js +407 -0
  62. package/src/asset_debugger/util/data/glb-classifier.js +290 -0
  63. package/src/asset_debugger/util/data/html-formatter.js +76 -0
  64. package/src/asset_debugger/util/data/html-linter.js +276 -0
  65. package/src/asset_debugger/util/data/localstorage-manager.js +265 -0
  66. package/src/asset_debugger/util/data/mesh-html-manager.js +295 -0
  67. package/src/asset_debugger/util/data/string-serder.js +303 -0
  68. package/src/asset_debugger/util/data/texture-classifier.js +663 -0
  69. package/src/asset_debugger/util/data/upload/background-file-handler.js +292 -0
  70. package/src/asset_debugger/util/data/upload/dropzone-preview-controller.js +396 -0
  71. package/src/asset_debugger/util/data/upload/file-upload-manager.js +495 -0
  72. package/src/asset_debugger/util/data/upload/glb-file-handler.js +36 -0
  73. package/src/asset_debugger/util/data/upload/glb-preview-controller.js +317 -0
  74. package/src/asset_debugger/util/data/upload/lighting-file-handler.js +194 -0
  75. package/src/asset_debugger/util/data/upload/model-file-manager.js +104 -0
  76. package/src/asset_debugger/util/data/upload/texture-file-handler.js +166 -0
  77. package/src/asset_debugger/util/data/upload/zip-handler.js +686 -0
  78. package/src/asset_debugger/util/loaders/html2canvas-loader.js +107 -0
  79. package/src/asset_debugger/util/rig/bone-kinematics.js +403 -0
  80. package/src/asset_debugger/util/rig/rig-constraint-manager.js +618 -0
  81. package/src/asset_debugger/util/rig/rig-controller.js +612 -0
  82. package/src/asset_debugger/util/rig/rig-factory.js +628 -0
  83. package/src/asset_debugger/util/rig/rig-handle-factory.js +46 -0
  84. package/src/asset_debugger/util/rig/rig-label-factory.js +441 -0
  85. package/src/asset_debugger/util/rig/rig-mouse-handler.js +377 -0
  86. package/src/asset_debugger/util/rig/rig-state-manager.js +175 -0
  87. package/src/asset_debugger/util/rig/rig-tooltip-manager.js +267 -0
  88. package/src/asset_debugger/util/rig/rig-ui-factory.js +700 -0
  89. package/src/asset_debugger/util/scene/background-manager.js +284 -0
  90. package/src/asset_debugger/util/scene/camera-controller.js +243 -0
  91. package/src/asset_debugger/util/scene/css3d-debug-controller.js +406 -0
  92. package/src/asset_debugger/util/scene/css3d-frame-factory.js +113 -0
  93. package/src/asset_debugger/util/scene/css3d-scene-manager.js +529 -0
  94. package/src/asset_debugger/util/scene/glb-controller.js +208 -0
  95. package/src/asset_debugger/util/scene/lighting-manager.js +690 -0
  96. package/src/asset_debugger/util/scene/threejs-model-manager.js +437 -0
  97. package/src/asset_debugger/util/scene/threejs-preview-manager.js +207 -0
  98. package/src/asset_debugger/util/scene/threejs-preview-setup.js +478 -0
  99. package/src/asset_debugger/util/scene/threejs-scene-controller.js +286 -0
  100. package/src/asset_debugger/util/scene/ui-manager.js +107 -0
  101. package/src/asset_debugger/util/state/animation-state.js +128 -0
  102. package/src/asset_debugger/util/state/css3d-state.js +83 -0
  103. package/src/asset_debugger/util/state/glb-preview-state.js +31 -0
  104. package/src/asset_debugger/util/state/log-util.js +197 -0
  105. package/src/asset_debugger/util/state/scene-state.js +452 -0
  106. package/src/asset_debugger/util/state/threejs-state.js +54 -0
  107. package/src/asset_debugger/util/workers/lighting-worker.js +61 -0
  108. package/src/asset_debugger/util/workers/model-worker.js +109 -0
  109. package/src/asset_debugger/util/workers/texture-worker.js +54 -0
  110. package/src/asset_debugger/util/workers/worker-manager.js +212 -0
  111. package/src/asset_debugger/widgets/mesh-info-widget.js +280 -0
  112. package/src/index.html +261 -0
  113. package/src/index.js +8 -0
  114. package/vite.config.js +66 -0
@@ -0,0 +1,663 @@
1
+ /**
2
+ * Atlas Texture Type Classifier
3
+ *
4
+ * Identifies texture map types (base color, normal maps, ORM maps) from atlas files
5
+ * with high accuracy and configurable confidence thresholds.
6
+ */
7
+
8
+ // Confidence level enum for texture classification
9
+ export const ConfidenceLevel = {
10
+ HIGH: "high",
11
+ MEDIUM: "medium",
12
+ LOW: "low",
13
+ UNKNOWN: "unknown"
14
+ };
15
+
16
+ // Global constant for the acceptable confidence level
17
+ // This determines what's considered a valid match during classification
18
+ let ACCEPTABLE_CONFIDENCE = ConfidenceLevel.MEDIUM;
19
+
20
+ /**
21
+ * Texture classifier for identifying atlas texture types
22
+ */
23
+ export class TextureClassifier {
24
+ constructor() {
25
+ // Classification confidence thresholds
26
+ this.HIGH_CONFIDENCE = 0.8;
27
+ this.MEDIUM_CONFIDENCE = 0.6;
28
+ this.LOW_CONFIDENCE = 0.4;
29
+ this.UNKNOWN_CONFIDENCE = 0.0;
30
+
31
+ // Map from ConfidenceLevel enum to threshold values
32
+ this.CONFIDENCE_THRESHOLDS = {
33
+ [ConfidenceLevel.HIGH]: this.HIGH_CONFIDENCE,
34
+ [ConfidenceLevel.MEDIUM]: this.MEDIUM_CONFIDENCE,
35
+ [ConfidenceLevel.LOW]: this.LOW_CONFIDENCE,
36
+ [ConfidenceLevel.UNKNOWN]: this.UNKNOWN_CONFIDENCE
37
+ };
38
+
39
+ // Maximum pixel count to analyze for performance (will downsample larger textures)
40
+ this.MAX_SAMPLES = 100000;
41
+
42
+ // Statistical feature weights for each texture type
43
+ this.featureWeights = {
44
+ 'base_color': {
45
+ 'colorVariance': 0.25,
46
+ 'rgbDistribution': 0.20,
47
+ 'blueChannelAvg': -0.15,
48
+ 'channelCorrelation': 0.10,
49
+ 'entropy': 0.20,
50
+ 'grayness': -0.10
51
+ },
52
+ 'normal_map': {
53
+ 'blueChannelBias': 0.30,
54
+ 'rgbMeansNormal': 0.25,
55
+ 'normalVectorValidity': 0.25,
56
+ 'rgbDistribution': -0.10,
57
+ 'colorVariance': 0.10
58
+ },
59
+ 'orm_map': {
60
+ 'channelIndependence': 0.25,
61
+ 'binaryMetallicScore': 0.20,
62
+ 'roughnessPattern': 0.15,
63
+ 'entropy': -0.10,
64
+ 'grayness': 0.15,
65
+ 'colorFlatness': 0.15
66
+ }
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Classify the texture type of the given image
72
+ * @param {HTMLImageElement|ImageData|Uint8ClampedArray|URL} image - Image to classify
73
+ * @returns {Object} Classification results
74
+ */
75
+ async classifyTexture(image) {
76
+ try {
77
+ // Load and process the image
78
+ const imgData = await this._loadImage(image);
79
+ if (!imgData) {
80
+ return {
81
+ classification: 'invalid_file',
82
+ confidence: 1.0,
83
+ scores: {},
84
+ features: {},
85
+ message: 'File could not be loaded as an image'
86
+ };
87
+ }
88
+
89
+ // Extract features
90
+ const features = this._extractFeatures(imgData);
91
+
92
+ // Calculate scores for each texture type
93
+ const scores = this._calculateScores(features);
94
+
95
+ // Determine the classification and confidence
96
+ const [classification, confidence] = this._determineClassification(scores);
97
+
98
+ return {
99
+ classification,
100
+ confidence,
101
+ scores,
102
+ features
103
+ };
104
+ } catch (error) {
105
+ return {
106
+ classification: 'error',
107
+ confidence: 0.0,
108
+ scores: {},
109
+ features: {},
110
+ message: `Error during classification: ${error.message}`
111
+ };
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Load an image from various sources and return its pixel data
117
+ * @param {HTMLImageElement|ImageData|Uint8ClampedArray|URL|string} source - Image source
118
+ * @returns {Promise<ImageData>} Image data for analysis
119
+ */
120
+ async _loadImage(source) {
121
+ try {
122
+ // Create canvas for image processing
123
+ const canvas = document.createElement('canvas');
124
+ const ctx = canvas.getContext('2d');
125
+ let img;
126
+
127
+ // Handle different input types
128
+ if (source instanceof HTMLImageElement) {
129
+ img = source;
130
+ } else if (typeof source === 'string' || source instanceof URL) {
131
+ // Load image from URL or path
132
+ img = new Image();
133
+ img.crossOrigin = 'Anonymous';
134
+
135
+ // Create a promise to handle async image loading
136
+ await new Promise((resolve, reject) => {
137
+ img.onload = resolve;
138
+ img.onerror = reject;
139
+ img.src = source;
140
+ });
141
+ } else if (source instanceof ImageData || source instanceof Uint8ClampedArray) {
142
+ // Return existing image data
143
+ return source instanceof ImageData ? source : new ImageData(source, 1);
144
+ } else {
145
+ throw new Error('Unsupported image source');
146
+ }
147
+
148
+ // Resize large images for performance
149
+ let width = img.width;
150
+ let height = img.height;
151
+ const totalPixels = width * height;
152
+
153
+ if (totalPixels > this.MAX_SAMPLES) {
154
+ const scale = Math.sqrt(this.MAX_SAMPLES / totalPixels);
155
+ width = Math.floor(width * scale);
156
+ height = Math.floor(height * scale);
157
+ }
158
+
159
+ // Set canvas dimensions and draw the image
160
+ canvas.width = width;
161
+ canvas.height = height;
162
+ ctx.drawImage(img, 0, 0, width, height);
163
+
164
+ // Get image data
165
+ return ctx.getImageData(0, 0, width, height);
166
+ } catch (error) {
167
+ console.error('Error loading image:', error);
168
+ return null;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Extract statistical features from image data
174
+ * @param {ImageData} imgData - Image data
175
+ * @returns {Object} Features used for classification
176
+ */
177
+ _extractFeatures(imgData) {
178
+ const { data, width, height } = imgData;
179
+ const pixelCount = width * height;
180
+
181
+ // Extract R, G, B channels
182
+ const channels = [[], [], []];
183
+
184
+ for (let i = 0; i < data.length; i += 4) {
185
+ channels[0].push(data[i] / 255.0); // R
186
+ channels[1].push(data[i + 1] / 255.0); // G
187
+ channels[2].push(data[i + 2] / 255.0); // B
188
+ }
189
+
190
+ const [r, g, b] = channels;
191
+
192
+ // Calculate means
193
+ const rMean = this._mean(r);
194
+ const gMean = this._mean(g);
195
+ const bMean = this._mean(b);
196
+
197
+ // Calculate standard deviations
198
+ const rStd = this._standardDeviation(r, rMean);
199
+ const gStd = this._standardDeviation(g, gMean);
200
+ const bStd = this._standardDeviation(b, bMean);
201
+
202
+ // Calculate histograms (25 bins)
203
+ const rHist = this._histogram(r, 25);
204
+ const gHist = this._histogram(g, 25);
205
+ const bHist = this._histogram(b, 25);
206
+
207
+ // Calculate correlation between channels
208
+ const rgCorr = this._correlation(r, g);
209
+ const rbCorr = this._correlation(r, b);
210
+ const gbCorr = this._correlation(g, b);
211
+
212
+ // Calculate average correlation
213
+ const avgCorrelation = (Math.abs(rgCorr) + Math.abs(rbCorr) + Math.abs(gbCorr)) / 3;
214
+
215
+ // Calculate blue channel bias (common in normal maps)
216
+ const blueHighRatio = b.filter(val => val > 0.8).length / b.length;
217
+
218
+ // Check for normal map pattern (rgb around 0.5, 0.5, 1.0)
219
+ let normalPatternCount = 0;
220
+ for (let i = 0; i < pixelCount; i++) {
221
+ const distSquared = Math.pow(r[i] - 0.5, 2) + Math.pow(g[i] - 0.5, 2) + Math.pow(b[i] - 1.0, 2);
222
+ if (distSquared < 0.2) {
223
+ normalPatternCount++;
224
+ }
225
+ }
226
+ const normalPatternScore = normalPatternCount / pixelCount;
227
+
228
+ // Check for normal vector validity
229
+ let validNormalVectorsCount = 0;
230
+ for (let i = 0; i < pixelCount; i++) {
231
+ // Convert from [0,1] to [-1,1]
232
+ const rx = r[i] * 2 - 1;
233
+ const gy = g[i] * 2 - 1;
234
+ const bz = b[i] * 2 - 1;
235
+
236
+ // Calculate magnitude
237
+ const magnitude = Math.sqrt(rx*rx + gy*gy + bz*bz);
238
+
239
+ // Valid vectors should have magnitude close to 1
240
+ if (magnitude > 0.5 && magnitude < 1.5) {
241
+ validNormalVectorsCount++;
242
+ }
243
+ }
244
+ const validNormalVectors = validNormalVectorsCount / pixelCount;
245
+
246
+ // Calculate color variance
247
+ const colorVariance = (rStd + gStd + bStd) / 3;
248
+
249
+ // Calculate grayness (how close r,g,b values are to each other)
250
+ let graynessSum = 0;
251
+ for (let i = 0; i < pixelCount; i++) {
252
+ graynessSum += Math.abs(r[i] - g[i]) + Math.abs(r[i] - b[i]) + Math.abs(g[i] - b[i]);
253
+ }
254
+ const grayness = 1.0 - (graynessSum / (2.0 * pixelCount));
255
+
256
+ // Calculate entropy of each channel
257
+ const rEntropy = this._entropy(rHist);
258
+ const gEntropy = this._entropy(gHist);
259
+ const bEntropy = this._entropy(bHist);
260
+ const avgEntropy = (rEntropy + gEntropy + bEntropy) / 3;
261
+
262
+ // Calculate channel independence
263
+ const channelIndependence = 1.0 - avgCorrelation;
264
+
265
+ // Check for metallicity pattern (common in ORM maps)
266
+ const metallicPattern = b.filter(val => val < 0.12 || val > 0.88).length / b.length;
267
+
268
+ // Estimate binary metallic score
269
+ // This is simpler than the Laplacian in Python version but effective
270
+ let edgeCount = 0;
271
+ for (let y = 1; y < height - 1; y++) {
272
+ for (let x = 1; x < width - 1; x++) {
273
+ const idx = (y * width + x);
274
+ const center = b[idx];
275
+ const left = b[idx - 1];
276
+ const right = b[idx + 1];
277
+ const up = b[idx - width];
278
+ const down = b[idx + width];
279
+
280
+ // Simple edge detection
281
+ if (Math.abs(center - left) > 0.2 ||
282
+ Math.abs(center - right) > 0.2 ||
283
+ Math.abs(center - up) > 0.2 ||
284
+ Math.abs(center - down) > 0.2) {
285
+ edgeCount++;
286
+ }
287
+ }
288
+ }
289
+ const edgeRatio = edgeCount / (pixelCount * 0.25); // normalize
290
+ const binaryMetallicScore = metallicPattern * (1.0 / (1 + edgeRatio));
291
+
292
+ // Check for flat areas (common in roughness maps)
293
+ let gradientSum = 0;
294
+ for (let y = 1; y < height - 1; y++) {
295
+ for (let x = 1; x < width - 1; x++) {
296
+ const idx = (y * width + x);
297
+ const gx = Math.abs(g[idx + 1] - g[idx - 1]);
298
+ const gy = Math.abs(g[idx + width] - g[idx - width]);
299
+ gradientSum += gx + gy;
300
+ }
301
+ }
302
+ const avgGradient = gradientSum / ((width - 2) * (height - 2) * 2);
303
+ const roughnessPattern = 1.0 - Math.min(avgGradient * 10.0, 1.0);
304
+
305
+ // Calculate overall flatness of colors
306
+ const colorFlatness = 1.0 - Math.min(1.0, colorVariance * 4);
307
+
308
+ // Calculate how similar the RGB distribution is to common texture types
309
+ const rgbDistNormal = 1.0 - Math.sqrt(
310
+ Math.pow(rMean - 0.5, 2) +
311
+ Math.pow(gMean - 0.5, 2) +
312
+ Math.pow(bMean - 1.0, 2)
313
+ );
314
+
315
+ // Assemble feature dictionary
316
+ return {
317
+ rMean,
318
+ gMean,
319
+ bMean,
320
+ rStd,
321
+ gStd,
322
+ bStd,
323
+ colorVariance,
324
+ grayness,
325
+ avgEntropy: avgEntropy,
326
+ channelCorrelation: avgCorrelation,
327
+ channelIndependence,
328
+ blueChannelBias: blueHighRatio,
329
+ rgbMeansNormal: rgbDistNormal,
330
+ normalVectorValidity: validNormalVectors,
331
+ binaryMetallicScore,
332
+ roughnessPattern,
333
+ colorFlatness,
334
+ blueChannelAvg: bMean,
335
+ rgbDistribution: 1 - Math.abs(rMean - gMean) - Math.abs(gMean - bMean) - Math.abs(rMean - bMean),
336
+ normalPatternScore
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Calculate scores for each texture type based on extracted features
342
+ * @param {Object} features - Features extracted from the image
343
+ * @returns {Object} Scores for each texture type
344
+ */
345
+ _calculateScores(features) {
346
+ const scores = {};
347
+
348
+ for (const [textureType, weights] of Object.entries(this.featureWeights)) {
349
+ let score = 0.0;
350
+
351
+ for (const [featureName, weight] of Object.entries(weights)) {
352
+ if (featureName in features) {
353
+ score += features[featureName] * weight;
354
+ }
355
+ }
356
+
357
+ // Normalize to 0-1 range
358
+ scores[textureType] = Math.max(0.0, Math.min(1.0, score));
359
+ }
360
+
361
+ return scores;
362
+ }
363
+
364
+ /**
365
+ * Determine the final classification and confidence level
366
+ * @param {Object} scores - Scores for each texture type
367
+ * @returns {Array} [classification, confidence]
368
+ */
369
+ _determineClassification(scores) {
370
+ // Find the texture type with the highest score
371
+ let maxType = '';
372
+ let maxScore = -1;
373
+
374
+ for (const [type, score] of Object.entries(scores)) {
375
+ if (score > maxScore) {
376
+ maxScore = score;
377
+ maxType = type;
378
+ }
379
+ }
380
+
381
+ // Check if there's a clear winner
382
+ const sortedScores = Object.values(scores).sort((a, b) => b - a);
383
+ let scoreDiff = 0;
384
+
385
+ if (sortedScores.length > 1) {
386
+ scoreDiff = sortedScores[0] - sortedScores[1];
387
+ }
388
+
389
+ // Adjust confidence based on the score and difference from next best
390
+ const baseConfidence = maxScore;
391
+ const diffFactor = Math.min(scoreDiff * 3.0, 0.3); // Max 0.3 boost from diff
392
+ const confidence = Math.min(1.0, baseConfidence + diffFactor);
393
+
394
+ // Determine if we should classify or return unknown
395
+ if (confidence >= this.HIGH_CONFIDENCE) {
396
+ return [maxType, confidence];
397
+ } else if (confidence >= this.MEDIUM_CONFIDENCE) {
398
+ return [maxType, confidence];
399
+ } else if (confidence >= this.LOW_CONFIDENCE) {
400
+ return [`likely_${maxType}`, confidence];
401
+ } else {
402
+ return ["unknown", confidence];
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Calculate mean of an array
408
+ * @param {Array<number>} arr - Input array
409
+ * @returns {number} Mean value
410
+ */
411
+ _mean(arr) {
412
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
413
+ }
414
+
415
+ /**
416
+ * Calculate standard deviation of an array
417
+ * @param {Array<number>} arr - Input array
418
+ * @param {number} mean - Mean value (optional)
419
+ * @returns {number} Standard deviation
420
+ */
421
+ _standardDeviation(arr, mean = null) {
422
+ const mu = mean !== null ? mean : this._mean(arr);
423
+ const squareDiffs = arr.map(value => Math.pow(value - mu, 2));
424
+ return Math.sqrt(this._mean(squareDiffs));
425
+ }
426
+
427
+ /**
428
+ * Calculate correlation between two arrays
429
+ * @param {Array<number>} x - First array
430
+ * @param {Array<number>} y - Second array
431
+ * @returns {number} Correlation coefficient
432
+ */
433
+ _correlation(x, y) {
434
+ const xMean = this._mean(x);
435
+ const yMean = this._mean(y);
436
+ let num = 0;
437
+ let xSumSq = 0;
438
+ let ySumSq = 0;
439
+
440
+ for (let i = 0; i < x.length; i++) {
441
+ const xDiff = x[i] - xMean;
442
+ const yDiff = y[i] - yMean;
443
+ num += xDiff * yDiff;
444
+ xSumSq += xDiff * xDiff;
445
+ ySumSq += yDiff * yDiff;
446
+ }
447
+
448
+ return num / (Math.sqrt(xSumSq) * Math.sqrt(ySumSq) || 1);
449
+ }
450
+
451
+ /**
452
+ * Calculate histogram for an array
453
+ * @param {Array<number>} arr - Input array
454
+ * @param {number} bins - Number of bins
455
+ * @returns {Array<number>} Histogram
456
+ */
457
+ _histogram(arr, bins) {
458
+ const hist = new Array(bins).fill(0);
459
+ const binSize = 1.0 / bins;
460
+
461
+ for (const value of arr) {
462
+ const binIndex = Math.min(Math.floor(value / binSize), bins - 1);
463
+ hist[binIndex]++;
464
+ }
465
+
466
+ // Normalize
467
+ return hist.map(h => h / arr.length);
468
+ }
469
+
470
+ /**
471
+ * Calculate entropy from a histogram
472
+ * @param {Array<number>} hist - Histogram
473
+ * @returns {number} Entropy
474
+ */
475
+ _entropy(hist) {
476
+ return -hist
477
+ .filter(p => p > 0)
478
+ .reduce((sum, p) => sum + p * Math.log2(p), 0);
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Classify a list of atlas files and determine their texture types
484
+ * @param {Array<string|HTMLImageElement|ImageData>} files - List of image sources
485
+ * @param {Object} options - Options
486
+ * @param {boolean} [options.verbose=false] - Whether to include detailed feature and score information
487
+ * @param {string} [options.acceptableConfidence=null] - Override the global ACCEPTABLE_CONFIDENCE level
488
+ * @returns {Promise<Object>} Classification results
489
+ */
490
+ export async function classifyAtlasFiles(files, options = {}) {
491
+ const classifier = new TextureClassifier();
492
+ const { verbose = false, acceptableConfidence = null } = options;
493
+
494
+ // Use the provided confidence level or fall back to the global constant
495
+ const confidenceLevel = acceptableConfidence || ACCEPTABLE_CONFIDENCE;
496
+ const minConfidenceThreshold = classifier.CONFIDENCE_THRESHOLDS[confidenceLevel];
497
+
498
+ // Classify each file
499
+ const fileClassifications = [];
500
+
501
+ for (const fileSource of files) {
502
+ let fileName = "";
503
+ if (typeof fileSource === 'string') {
504
+ // Extract filename from path or URL
505
+ fileName = fileSource.split('/').pop().split('\\').pop();
506
+ } else if (fileSource instanceof File) {
507
+ fileName = fileSource.name;
508
+ } else {
509
+ fileName = "unnamed_texture";
510
+ }
511
+
512
+ const result = await classifier.classifyTexture(fileSource);
513
+ result.file = fileName;
514
+ result.path = fileSource;
515
+ fileClassifications.push(result);
516
+ }
517
+
518
+ // Assign texture types based on classification results
519
+ const bestMatches = {};
520
+ const assignedFiles = new Set();
521
+
522
+ // First pass: Assign high confidence matches
523
+ for (const textureType of ['base_color', 'normal_map', 'orm_map']) {
524
+ const candidates = fileClassifications.filter(res =>
525
+ res.classification === textureType &&
526
+ res.confidence >= classifier.HIGH_CONFIDENCE &&
527
+ !assignedFiles.has(res.path)
528
+ );
529
+
530
+ if (candidates.length > 0) {
531
+ // Sort by confidence
532
+ candidates.sort((a, b) => b.confidence - a.confidence);
533
+ const bestMatch = candidates[0];
534
+ bestMatches[textureType] = {
535
+ file: bestMatch.file,
536
+ path: bestMatch.path,
537
+ confidence: bestMatch.confidence,
538
+ confidenceLevel: 'high'
539
+ };
540
+ assignedFiles.add(bestMatch.path);
541
+ }
542
+ }
543
+
544
+ // Second pass: Handle medium confidence matches if they meet the acceptable threshold
545
+ if (minConfidenceThreshold <= classifier.MEDIUM_CONFIDENCE) {
546
+ for (const textureType of ['base_color', 'normal_map', 'orm_map']) {
547
+ if (textureType in bestMatches) continue;
548
+
549
+ const candidates = fileClassifications.filter(res =>
550
+ (res.classification === textureType || res.classification === `likely_${textureType}`) &&
551
+ res.confidence >= classifier.MEDIUM_CONFIDENCE &&
552
+ !assignedFiles.has(res.path)
553
+ );
554
+
555
+ if (candidates.length > 0) {
556
+ candidates.sort((a, b) => b.confidence - a.confidence);
557
+ const bestMatch = candidates[0];
558
+ bestMatches[textureType] = {
559
+ file: bestMatch.file,
560
+ path: bestMatch.path,
561
+ confidence: bestMatch.confidence,
562
+ confidenceLevel: 'medium',
563
+ uncertain: true
564
+ };
565
+ assignedFiles.add(bestMatch.path);
566
+ }
567
+ }
568
+ }
569
+
570
+ // Third pass: Handle low confidence matches if they meet the acceptable threshold
571
+ if (minConfidenceThreshold <= classifier.LOW_CONFIDENCE) {
572
+ for (const textureType of ['base_color', 'normal_map', 'orm_map']) {
573
+ if (textureType in bestMatches) continue;
574
+
575
+ const candidates = fileClassifications.filter(res =>
576
+ ((res.scores[textureType] || 0) >= classifier.LOW_CONFIDENCE) &&
577
+ !assignedFiles.has(res.path)
578
+ );
579
+
580
+ if (candidates.length > 0) {
581
+ candidates.sort((a, b) => (b.scores[textureType] || 0) - (a.scores[textureType] || 0));
582
+ const bestMatch = candidates[0];
583
+ bestMatches[textureType] = {
584
+ file: bestMatch.file,
585
+ path: bestMatch.path,
586
+ confidence: bestMatch.scores[textureType] || 0,
587
+ confidenceLevel: 'low',
588
+ bestGuess: true
589
+ };
590
+ assignedFiles.add(bestMatch.path);
591
+ }
592
+ }
593
+ }
594
+
595
+ // Handle remaining unassigned files as best guesses only if we're accepting UNKNOWN confidence
596
+ if (minConfidenceThreshold <= classifier.UNKNOWN_CONFIDENCE) {
597
+ const remainingFiles = fileClassifications.filter(res => !assignedFiles.has(res.path));
598
+
599
+ for (const res of remainingFiles) {
600
+ // Get the highest score texture type
601
+ let maxType = '';
602
+ let maxScore = -1;
603
+
604
+ for (const [type, score] of Object.entries(res.scores)) {
605
+ if (score > maxScore) {
606
+ maxScore = score;
607
+ maxType = type;
608
+ }
609
+ }
610
+
611
+ if (maxType && !(maxType in bestMatches)) {
612
+ bestMatches[maxType] = {
613
+ file: res.file,
614
+ path: res.path,
615
+ confidence: res.scores[maxType],
616
+ confidenceLevel: 'unknown',
617
+ bestGuess: true
618
+ };
619
+ }
620
+ }
621
+ }
622
+
623
+ // Mark missing texture types
624
+ for (const missingType of ['base_color', 'normal_map', 'orm_map']) {
625
+ if (!(missingType in bestMatches)) {
626
+ bestMatches[missingType] = {
627
+ file: null,
628
+ path: null,
629
+ confidence: 0.0,
630
+ missing: true
631
+ };
632
+ }
633
+ }
634
+
635
+ // Prepare the final result
636
+ const results = {
637
+ textureAssignments: bestMatches,
638
+ acceptableConfidenceLevel: confidenceLevel,
639
+ summary: {
640
+ baseColor: bestMatches['base_color'].file || "No file",
641
+ normalMap: bestMatches['normal_map'].file || "No file",
642
+ ormMap: bestMatches['orm_map'].file || "No file"
643
+ }
644
+ };
645
+
646
+ if (verbose) {
647
+ results.detailedClassifications = fileClassifications;
648
+ }
649
+
650
+ return results;
651
+ }
652
+
653
+ /**
654
+ * Set the global acceptable confidence level
655
+ * @param {string} level - Confidence level ('high', 'medium', 'low', 'unknown')
656
+ */
657
+ export function setAcceptableConfidence(level) {
658
+ if (Object.values(ConfidenceLevel).includes(level)) {
659
+ ACCEPTABLE_CONFIDENCE = level;
660
+ } else {
661
+ throw new Error(`Invalid confidence level: ${level}. Must be one of: high, medium, low, unknown`);
662
+ }
663
+ }