@carbonenginejs/runtime-utils 0.1.1 → 0.1.3

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 (104) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +23 -23
  3. package/README.md +69 -69
  4. package/THIRD-PARTY-NOTICES.md +64 -64
  5. package/docs/README.md +74 -73
  6. package/docs/architecture.md +101 -101
  7. package/docs/concepts/foundation-consolidation.md +85 -85
  8. package/docs/concepts/model-lifecycle.md +111 -0
  9. package/docs/const-kb.md +85 -87
  10. package/docs/core-types/DECORATOR-TODOS.md +25 -25
  11. package/docs/core-types/README.md +229 -168
  12. package/docs/reference/api.md +105 -102
  13. package/docs/reference/classes/README.md +135 -135
  14. package/package.json +139 -139
  15. package/src/arrays.js +5 -5
  16. package/src/audio/audioFormats.js +34 -34
  17. package/src/audio/index.js +1 -1
  18. package/src/box3.js +331 -331
  19. package/src/bytes.js +56 -56
  20. package/src/compression.js +56 -56
  21. package/src/constants/index.js +7 -7
  22. package/src/constants/trinity.js +13 -13
  23. package/src/constants.js +15 -15
  24. package/src/curve.js +79 -79
  25. package/src/d3d/dxgiFormats.js +46 -46
  26. package/src/d3d/index.js +2 -2
  27. package/src/d3d/primitiveTopology.js +11 -11
  28. package/src/document/CjsCarbonDocument.js +212 -212
  29. package/src/document/CjsClassRegistry.js +373 -373
  30. package/src/document/CjsDocumentDehydrator.js +142 -142
  31. package/src/document/CjsDocumentHydrator.js +156 -156
  32. package/src/document/CjsStructRegistry.js +348 -348
  33. package/src/document/hydrationAdapter.js +129 -129
  34. package/src/document/index.js +6 -6
  35. package/src/errors/CjsError.js +286 -286
  36. package/src/errors/index.js +5 -5
  37. package/src/geometry/box.js +22 -22
  38. package/src/geometry/cylinder.js +22 -22
  39. package/src/geometry/helpers/earcut.js +1 -1
  40. package/src/geometry/helpers/misc.js +3 -3
  41. package/src/geometry/index.js +8 -8
  42. package/src/geometry/json.js +67 -67
  43. package/src/geometry/lathe.js +44 -44
  44. package/src/geometry/plane.js +14 -14
  45. package/src/geometry/shape.js +4 -4
  46. package/src/geometry/sphere.js +24 -24
  47. package/src/geometry/torus.js +14 -14
  48. package/src/graphics/colorSpaces.js +22 -22
  49. package/src/graphics/index.js +4 -4
  50. package/src/graphics/pixelFormats.js +158 -158
  51. package/src/graphics/textureDimensions.js +22 -22
  52. package/src/graphics/trinityEnums.js +87 -87
  53. package/src/index.js +62 -62
  54. package/src/is.js +108 -46
  55. package/src/json.js +23 -23
  56. package/src/lifecycle/CjsLifecycleState.js +77 -77
  57. package/src/lifecycle/index.js +1 -1
  58. package/src/lne3.js +70 -70
  59. package/src/lookup.js +48 -48
  60. package/src/mat3.js +51 -51
  61. package/src/mat4.js +699 -699
  62. package/src/math/index.js +25 -25
  63. package/src/math/scalar.js +63 -63
  64. package/src/media/index.js +1 -1
  65. package/src/media/mediaTypes.js +50 -50
  66. package/src/mesh.js +424 -424
  67. package/src/model/CjsEventEmitter.js +333 -333
  68. package/src/model/CjsModel.js +1589 -1544
  69. package/src/model/CjsModelState.js +72 -72
  70. package/src/model/index.js +4 -4
  71. package/src/model/sourceRecordUtils.js +54 -54
  72. package/src/noise.js +310 -310
  73. package/src/num.js +827 -827
  74. package/src/object.js +39 -39
  75. package/src/path.js +53 -53
  76. package/src/pln.js +125 -125
  77. package/src/pool.js +9 -9
  78. package/src/quat.js +144 -144
  79. package/src/ray3.js +188 -188
  80. package/src/renderContext/formats.js +145 -145
  81. package/src/renderContext/index.js +5 -5
  82. package/src/renderContext/presentation.js +125 -125
  83. package/src/renderContext/resources.js +27 -27
  84. package/src/renderContext/upscaling.js +22 -22
  85. package/src/renderContext/window.js +20 -20
  86. package/src/runtime/CjsRuntimeState.js +50 -50
  87. package/src/schema/CjsSchema.js +555 -318
  88. package/src/schema/index.js +4 -4
  89. package/src/shader/index.js +1 -1
  90. package/src/shader/shaderStages.js +37 -37
  91. package/src/sph3.js +181 -181
  92. package/src/tangent.js +288 -288
  93. package/src/text.js +40 -40
  94. package/src/tri3.js +98 -98
  95. package/src/types/carbonTypes.js +635 -635
  96. package/src/types/index.js +2 -2
  97. package/src/utils.js +58 -58
  98. package/src/validation.js +46 -46
  99. package/src/vec2.js +229 -229
  100. package/src/vec3.js +1188 -1172
  101. package/src/vec4.js +347 -347
  102. package/src/vertex.js +108 -108
  103. package/src/webgpu/index.js +1 -1
  104. package/src/webgpu/textureFormats.js +121 -121
package/src/mesh.js CHANGED
@@ -1,424 +1,424 @@
1
- /**
2
- * Small mesh rebuild helpers for shared CarbonEngineJS mesh JSON.
3
- *
4
- * These helpers are deliberately framework-free and browser-safe. They accept
5
- * plain arrays or typed arrays and return plain arrays unless otherwise noted.
6
- */
7
-
8
- import {
9
- cross,
10
- length as vec3Length,
11
- normalize
12
- } from "gl-matrix/esm/vec3.js";
13
-
14
- function validatePositions(positions)
15
- {
16
- if (!positions || positions.length % 3 !== 0)
17
- {
18
- throw new Error("Positions must contain complete xyz vertices");
19
- }
20
- }
21
-
22
- function validateIndices(indices, vertexCount)
23
- {
24
- if (!indices || indices.length % 3 !== 0)
25
- {
26
- throw new Error("Indices must contain complete triangles");
27
- }
28
-
29
- for (let i = 0; i < indices.length; i++)
30
- {
31
- if (!Number.isInteger(indices[i]) || indices[i] < 0 || indices[i] >= vertexCount)
32
- {
33
- throw new Error(`Invalid vertex index at ${i}`);
34
- }
35
- }
36
- }
37
-
38
- /**
39
- * Calculate the unit face normal for a triangle.
40
- *
41
- * @param {ArrayLike<number>} a First xyz vertex.
42
- * @param {ArrayLike<number>} b Second xyz vertex.
43
- * @param {ArrayLike<number>} c Third xyz vertex.
44
- * @returns {number[]} Unit triangle normal.
45
- */
46
- export function triangleNormal(a, b, c)
47
- {
48
- return triangleNormalTo([ 0, 0, 0 ], a, b, c);
49
- }
50
-
51
- /**
52
- * Calculate a unit face normal into caller-owned storage.
53
- *
54
- * @param {ArrayLike<number>} out Receiving xyz vector.
55
- * @param {ArrayLike<number>} a First xyz vertex.
56
- * @param {ArrayLike<number>} b Second xyz vertex.
57
- * @param {ArrayLike<number>} c Third xyz vertex.
58
- * @returns {ArrayLike<number>} The receiving vector.
59
- */
60
- export function triangleNormalTo(out, a, b, c)
61
- {
62
- const
63
- abX = b[0] - a[0],
64
- abY = b[1] - a[1],
65
- abZ = b[2] - a[2],
66
- acX = c[0] - a[0],
67
- acY = c[1] - a[1],
68
- acZ = c[2] - a[2];
69
-
70
- out[0] = abY * acZ - abZ * acY;
71
- out[1] = abZ * acX - abX * acZ;
72
- out[2] = abX * acY - abY * acX;
73
-
74
- const length = Math.hypot(out[0], out[1], out[2]);
75
-
76
- if (length > 0)
77
- {
78
- out[0] /= length;
79
- out[1] /= length;
80
- out[2] /= length;
81
- }
82
-
83
- return out;
84
- }
85
-
86
- /**
87
- * Twice the area of a triangle.
88
- *
89
- * @param {ArrayLike<number>} a First xyz vertex.
90
- * @param {ArrayLike<number>} b Second xyz vertex.
91
- * @param {ArrayLike<number>} c Third xyz vertex.
92
- * @returns {number} Twice the triangle area.
93
- */
94
- export function triangleArea2(a, b, c)
95
- {
96
- const normal = [ 0, 0, 0 ];
97
- cross(
98
- normal,
99
- [ b[0] - a[0], b[1] - a[1], b[2] - a[2] ],
100
- [ c[0] - a[0], c[1] - a[1], c[2] - a[2] ]
101
- );
102
- return vec3Length(normal);
103
- }
104
-
105
- /**
106
- * Whether a triangle is degenerate.
107
- *
108
- * @param {ArrayLike<number>} a First xyz vertex.
109
- * @param {ArrayLike<number>} b Second xyz vertex.
110
- * @param {ArrayLike<number>} c Third xyz vertex.
111
- * @param {number} [epsilon] Area epsilon.
112
- * @returns {boolean} True when the triangle has no useful area.
113
- */
114
- export function isDegenerateTriangle(a, b, c, epsilon = 1e-12)
115
- {
116
- return triangleArea2(a, b, c) <= epsilon;
117
- }
118
-
119
- /**
120
- * Compute axis-aligned bounds from flat xyz positions.
121
- *
122
- * @param {ArrayLike<number>} positions Flat xyz positions.
123
- * @returns {{minBounds: number[], maxBounds: number[]}} Bounds.
124
- */
125
- export function computeBoundsFromPositions(positions)
126
- {
127
- validatePositions(positions);
128
-
129
- if (!positions.length)
130
- {
131
- return { minBounds: [ 0, 0, 0 ], maxBounds: [ 0, 0, 0 ] };
132
- }
133
-
134
- const
135
- minBounds = [ positions[0], positions[1], positions[2] ],
136
- maxBounds = [ positions[0], positions[1], positions[2] ];
137
-
138
- for (let i = 3; i < positions.length; i += 3)
139
- {
140
- for (let c = 0; c < 3; c++)
141
- {
142
- const value = positions[i + c];
143
- if (value < minBounds[c]) minBounds[c] = value;
144
- if (value > maxBounds[c]) maxBounds[c] = value;
145
- }
146
- }
147
-
148
- return { minBounds, maxBounds };
149
- }
150
-
151
- /**
152
- * Compute axis-aligned bounds from `{ vertices: [[x,y,z], ...] }` triangles.
153
- *
154
- * @param {Array<{vertices: Array<ArrayLike<number>>}>} triangles Triangle records.
155
- * @returns {{minBounds: number[], maxBounds: number[]}} Bounds.
156
- */
157
- export function computeBoundsFromTriangles(triangles)
158
- {
159
- if (!triangles.length)
160
- {
161
- return { minBounds: [ 0, 0, 0 ], maxBounds: [ 0, 0, 0 ] };
162
- }
163
-
164
- const
165
- minBounds = [ Infinity, Infinity, Infinity ],
166
- maxBounds = [ -Infinity, -Infinity, -Infinity ];
167
-
168
- let hasVertex = false;
169
- for (const triangle of triangles)
170
- {
171
- for (const vertex of triangle.vertices)
172
- {
173
- hasVertex = true;
174
- for (let c = 0; c < 3; c++)
175
- {
176
- if (vertex[c] < minBounds[c]) minBounds[c] = vertex[c];
177
- if (vertex[c] > maxBounds[c]) maxBounds[c] = vertex[c];
178
- }
179
- }
180
- }
181
-
182
- if (!hasVertex)
183
- {
184
- return { minBounds: [ 0, 0, 0 ], maxBounds: [ 0, 0, 0 ] };
185
- }
186
-
187
- return { minBounds, maxBounds };
188
- }
189
-
190
- /**
191
- * Generate area-weighted vertex normals from positions and triangle indices.
192
- *
193
- * @param {ArrayLike<number>} positions Flat xyz positions.
194
- * @param {ArrayLike<number>} indices Flat triangle indices.
195
- * @returns {Float32Array} Flat xyz normals.
196
- */
197
- export function generateNormals(positions, indices)
198
- {
199
- validatePositions(positions);
200
- validateIndices(indices, positions.length / 3);
201
-
202
- const
203
- vertexCount = positions.length / 3,
204
- normals = new Float32Array(positions.length);
205
-
206
- for (let t = 0; t < indices.length; t += 3)
207
- {
208
- const
209
- ia = indices[t] * 3,
210
- ib = indices[t + 1] * 3,
211
- ic = indices[t + 2] * 3,
212
- ax = positions[ia],
213
- ay = positions[ia + 1],
214
- az = positions[ia + 2],
215
- faceNormal = [ 0, 0, 0 ];
216
-
217
- cross(
218
- faceNormal,
219
- [ positions[ib] - ax, positions[ib + 1] - ay, positions[ib + 2] - az ],
220
- [ positions[ic] - ax, positions[ic + 1] - ay, positions[ic + 2] - az ]
221
- );
222
-
223
- for (const offset of [ ia, ib, ic ])
224
- {
225
- normals[offset] += faceNormal[0];
226
- normals[offset + 1] += faceNormal[1];
227
- normals[offset + 2] += faceNormal[2];
228
- }
229
- }
230
-
231
- for (let i = 0; i < vertexCount; i++)
232
- {
233
- const
234
- offset = i * 3,
235
- length = Math.hypot(normals[offset], normals[offset + 1], normals[offset + 2]) || 1;
236
-
237
- normals[offset] /= length;
238
- normals[offset + 1] /= length;
239
- normals[offset + 2] /= length;
240
- }
241
-
242
- return normals;
243
- }
244
-
245
- /**
246
- * Generate per-vertex tangents from positions, normals, UVs and indices.
247
- *
248
- * @param {ArrayLike<number>} positions Flat xyz positions.
249
- * @param {ArrayLike<number>} normals Flat xyz normals.
250
- * @param {ArrayLike<number>} uvs Flat uv coordinates.
251
- * @param {ArrayLike<number>} indices Flat triangle indices.
252
- * @returns {Float32Array} Flat xyz tangents.
253
- */
254
- export function generateTangents(positions, normals, uvs, indices)
255
- {
256
- validatePositions(positions);
257
-
258
- const
259
- vertexCount = positions.length / 3,
260
- tan1 = new Float32Array(vertexCount * 3),
261
- tan2 = new Float32Array(vertexCount * 3);
262
-
263
- if (!normals || normals.length !== positions.length ||
264
- !uvs || uvs.length !== vertexCount * 2)
265
- {
266
- throw new Error("Tangent channels do not match the vertex count");
267
- }
268
- validateIndices(indices, vertexCount);
269
-
270
- for (let t = 0; t < indices.length; t += 3)
271
- {
272
- const
273
- i0 = indices[t],
274
- i1 = indices[t + 1],
275
- i2 = indices[t + 2],
276
- p0 = i0 * 3,
277
- p1 = i1 * 3,
278
- p2 = i2 * 3,
279
- t0 = i0 * 2,
280
- t1 = i1 * 2,
281
- t2 = i2 * 2,
282
- x1 = positions[p1] - positions[p0],
283
- y1 = positions[p1 + 1] - positions[p0 + 1],
284
- z1 = positions[p1 + 2] - positions[p0 + 2],
285
- x2 = positions[p2] - positions[p0],
286
- y2 = positions[p2 + 1] - positions[p0 + 1],
287
- z2 = positions[p2 + 2] - positions[p0 + 2],
288
- s1 = uvs[t1] - uvs[t0],
289
- v1 = uvs[t1 + 1] - uvs[t0 + 1],
290
- s2 = uvs[t2] - uvs[t0],
291
- v2 = uvs[t2 + 1] - uvs[t0 + 1],
292
- divisor = s1 * v2 - s2 * v1,
293
- scale = divisor ? 1 / divisor : 0,
294
- sx = (v2 * x1 - v1 * x2) * scale,
295
- sy = (v2 * y1 - v1 * y2) * scale,
296
- sz = (v2 * z1 - v1 * z2) * scale,
297
- tx = (s1 * x2 - s2 * x1) * scale,
298
- ty = (s1 * y2 - s2 * y1) * scale,
299
- tz = (s1 * z2 - s2 * z1) * scale;
300
-
301
- for (const offset of [ p0, p1, p2 ])
302
- {
303
- tan1[offset] += sx;
304
- tan1[offset + 1] += sy;
305
- tan1[offset + 2] += sz;
306
- tan2[offset] += tx;
307
- tan2[offset + 1] += ty;
308
- tan2[offset + 2] += tz;
309
- }
310
- }
311
-
312
- const
313
- tangents = new Float32Array(vertexCount * 3),
314
- handedness = new Float32Array(vertexCount);
315
-
316
- for (let i = 0; i < vertexCount; i++)
317
- {
318
- const
319
- offset = i * 3,
320
- nx = normals[offset],
321
- ny = normals[offset + 1],
322
- nz = normals[offset + 2],
323
- tx = tan1[offset],
324
- ty = tan1[offset + 1],
325
- tz = tan1[offset + 2],
326
- normalDotTangent = nx * tx + ny * ty + nz * tz;
327
-
328
- let ox = tx - nx * normalDotTangent,
329
- oy = ty - ny * normalDotTangent,
330
- oz = tz - nz * normalDotTangent;
331
-
332
- const length = Math.hypot(ox, oy, oz) || 1;
333
-
334
- ox /= length;
335
- oy /= length;
336
- oz /= length;
337
-
338
- tangents[offset] = ox;
339
- tangents[offset + 1] = oy;
340
- tangents[offset + 2] = oz;
341
- handedness[i] = (
342
- (ny * oz - nz * oy) * tan2[offset] +
343
- (nz * ox - nx * oz) * tan2[offset + 1] +
344
- (nx * oy - ny * ox) * tan2[offset + 2]
345
- ) < 0 ? -1 : 1;
346
- }
347
-
348
- Object.defineProperty(tangents, "handedness", { value: handedness });
349
- return tangents;
350
- }
351
-
352
- /**
353
- * Generate a complete tangent frame while preserving per-vertex UV handedness.
354
- *
355
- * @param {ArrayLike<number>} positions Flat xyz positions.
356
- * @param {ArrayLike<number>} normals Flat xyz normals.
357
- * @param {ArrayLike<number>} uvs Flat uv coordinates.
358
- * @param {ArrayLike<number>} indices Flat triangle indices.
359
- * @param {object} [options] Generation options.
360
- * @returns {{tangents: Float32Array, binormals: number[], handedness: Float32Array}}
361
- */
362
- export function generateTangentFrames(positions, normals, uvs, indices, options)
363
- {
364
- const
365
- tangents = generateTangents(positions, normals, uvs, indices),
366
- handedness = tangents.handedness,
367
- binormals = generateBiNormals(normals, tangents, { ...options, handedness });
368
-
369
- return { tangents, binormals, handedness };
370
- }
371
-
372
- /**
373
- * Generate binormals as normalized `normal x tangent`.
374
- *
375
- * @param {ArrayLike<number>} normals Flat xyz normals.
376
- * @param {ArrayLike<number>} tangents Flat xyz tangents.
377
- * @param {object} [options] Generation options.
378
- * @param {"right"|"left"} [options.uvHandedness] Handedness of generated basis.
379
- * @returns {number[]} Flat xyz binormals.
380
- */
381
- export function generateBiNormals(normals, tangents, options = {})
382
- {
383
- if (normals.length !== tangents.length || normals.length % 3 !== 0)
384
- {
385
- throw new Error("generateBiNormals requires matching complete xyz channels");
386
- }
387
-
388
- const
389
- conventionSign = options.uvHandedness === "left" ? -1 : 1,
390
- binormals = new Array(normals.length);
391
-
392
- for (let i = 0; i < normals.length; i += 3)
393
- {
394
- const
395
- vertexSign = options.handedness?.[i / 3] ?? tangents.handedness?.[i / 3] ?? 1,
396
- sign = conventionSign * vertexSign;
397
- const b = normalize(
398
- [ 0, 0, 0 ],
399
- [
400
- normals[i + 1] * tangents[i + 2] - normals[i + 2] * tangents[i + 1],
401
- normals[i + 2] * tangents[i] - normals[i] * tangents[i + 2],
402
- normals[i] * tangents[i + 1] - normals[i + 1] * tangents[i]
403
- ]
404
- );
405
- binormals[i] = b[0] * sign;
406
- binormals[i + 1] = b[1] * sign;
407
- binormals[i + 2] = b[2] * sign;
408
- }
409
-
410
- return binormals;
411
- }
412
-
413
- export const mesh = Object.freeze({
414
- triangleNormal,
415
- triangleNormalTo,
416
- triangleArea2,
417
- isDegenerateTriangle,
418
- computeBoundsFromPositions,
419
- computeBoundsFromTriangles,
420
- generateNormals,
421
- generateTangents,
422
- generateTangentFrames,
423
- generateBiNormals
424
- });
1
+ /**
2
+ * Small mesh rebuild helpers for shared CarbonEngineJS mesh JSON.
3
+ *
4
+ * These helpers are deliberately framework-free and browser-safe. They accept
5
+ * plain arrays or typed arrays and return plain arrays unless otherwise noted.
6
+ */
7
+
8
+ import {
9
+ cross,
10
+ length as vec3Length,
11
+ normalize
12
+ } from "gl-matrix/esm/vec3.js";
13
+
14
+ function validatePositions(positions)
15
+ {
16
+ if (!positions || positions.length % 3 !== 0)
17
+ {
18
+ throw new Error("Positions must contain complete xyz vertices");
19
+ }
20
+ }
21
+
22
+ function validateIndices(indices, vertexCount)
23
+ {
24
+ if (!indices || indices.length % 3 !== 0)
25
+ {
26
+ throw new Error("Indices must contain complete triangles");
27
+ }
28
+
29
+ for (let i = 0; i < indices.length; i++)
30
+ {
31
+ if (!Number.isInteger(indices[i]) || indices[i] < 0 || indices[i] >= vertexCount)
32
+ {
33
+ throw new Error(`Invalid vertex index at ${i}`);
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Calculate the unit face normal for a triangle.
40
+ *
41
+ * @param {ArrayLike<number>} a First xyz vertex.
42
+ * @param {ArrayLike<number>} b Second xyz vertex.
43
+ * @param {ArrayLike<number>} c Third xyz vertex.
44
+ * @returns {number[]} Unit triangle normal.
45
+ */
46
+ export function triangleNormal(a, b, c)
47
+ {
48
+ return triangleNormalTo([ 0, 0, 0 ], a, b, c);
49
+ }
50
+
51
+ /**
52
+ * Calculate a unit face normal into caller-owned storage.
53
+ *
54
+ * @param {ArrayLike<number>} out Receiving xyz vector.
55
+ * @param {ArrayLike<number>} a First xyz vertex.
56
+ * @param {ArrayLike<number>} b Second xyz vertex.
57
+ * @param {ArrayLike<number>} c Third xyz vertex.
58
+ * @returns {ArrayLike<number>} The receiving vector.
59
+ */
60
+ export function triangleNormalTo(out, a, b, c)
61
+ {
62
+ const
63
+ abX = b[0] - a[0],
64
+ abY = b[1] - a[1],
65
+ abZ = b[2] - a[2],
66
+ acX = c[0] - a[0],
67
+ acY = c[1] - a[1],
68
+ acZ = c[2] - a[2];
69
+
70
+ out[0] = abY * acZ - abZ * acY;
71
+ out[1] = abZ * acX - abX * acZ;
72
+ out[2] = abX * acY - abY * acX;
73
+
74
+ const length = Math.hypot(out[0], out[1], out[2]);
75
+
76
+ if (length > 0)
77
+ {
78
+ out[0] /= length;
79
+ out[1] /= length;
80
+ out[2] /= length;
81
+ }
82
+
83
+ return out;
84
+ }
85
+
86
+ /**
87
+ * Twice the area of a triangle.
88
+ *
89
+ * @param {ArrayLike<number>} a First xyz vertex.
90
+ * @param {ArrayLike<number>} b Second xyz vertex.
91
+ * @param {ArrayLike<number>} c Third xyz vertex.
92
+ * @returns {number} Twice the triangle area.
93
+ */
94
+ export function triangleArea2(a, b, c)
95
+ {
96
+ const normal = [ 0, 0, 0 ];
97
+ cross(
98
+ normal,
99
+ [ b[0] - a[0], b[1] - a[1], b[2] - a[2] ],
100
+ [ c[0] - a[0], c[1] - a[1], c[2] - a[2] ]
101
+ );
102
+ return vec3Length(normal);
103
+ }
104
+
105
+ /**
106
+ * Whether a triangle is degenerate.
107
+ *
108
+ * @param {ArrayLike<number>} a First xyz vertex.
109
+ * @param {ArrayLike<number>} b Second xyz vertex.
110
+ * @param {ArrayLike<number>} c Third xyz vertex.
111
+ * @param {number} [epsilon] Area epsilon.
112
+ * @returns {boolean} True when the triangle has no useful area.
113
+ */
114
+ export function isDegenerateTriangle(a, b, c, epsilon = 1e-12)
115
+ {
116
+ return triangleArea2(a, b, c) <= epsilon;
117
+ }
118
+
119
+ /**
120
+ * Compute axis-aligned bounds from flat xyz positions.
121
+ *
122
+ * @param {ArrayLike<number>} positions Flat xyz positions.
123
+ * @returns {{minBounds: number[], maxBounds: number[]}} Bounds.
124
+ */
125
+ export function computeBoundsFromPositions(positions)
126
+ {
127
+ validatePositions(positions);
128
+
129
+ if (!positions.length)
130
+ {
131
+ return { minBounds: [ 0, 0, 0 ], maxBounds: [ 0, 0, 0 ] };
132
+ }
133
+
134
+ const
135
+ minBounds = [ positions[0], positions[1], positions[2] ],
136
+ maxBounds = [ positions[0], positions[1], positions[2] ];
137
+
138
+ for (let i = 3; i < positions.length; i += 3)
139
+ {
140
+ for (let c = 0; c < 3; c++)
141
+ {
142
+ const value = positions[i + c];
143
+ if (value < minBounds[c]) minBounds[c] = value;
144
+ if (value > maxBounds[c]) maxBounds[c] = value;
145
+ }
146
+ }
147
+
148
+ return { minBounds, maxBounds };
149
+ }
150
+
151
+ /**
152
+ * Compute axis-aligned bounds from `{ vertices: [[x,y,z], ...] }` triangles.
153
+ *
154
+ * @param {Array<{vertices: Array<ArrayLike<number>>}>} triangles Triangle records.
155
+ * @returns {{minBounds: number[], maxBounds: number[]}} Bounds.
156
+ */
157
+ export function computeBoundsFromTriangles(triangles)
158
+ {
159
+ if (!triangles.length)
160
+ {
161
+ return { minBounds: [ 0, 0, 0 ], maxBounds: [ 0, 0, 0 ] };
162
+ }
163
+
164
+ const
165
+ minBounds = [ Infinity, Infinity, Infinity ],
166
+ maxBounds = [ -Infinity, -Infinity, -Infinity ];
167
+
168
+ let hasVertex = false;
169
+ for (const triangle of triangles)
170
+ {
171
+ for (const vertex of triangle.vertices)
172
+ {
173
+ hasVertex = true;
174
+ for (let c = 0; c < 3; c++)
175
+ {
176
+ if (vertex[c] < minBounds[c]) minBounds[c] = vertex[c];
177
+ if (vertex[c] > maxBounds[c]) maxBounds[c] = vertex[c];
178
+ }
179
+ }
180
+ }
181
+
182
+ if (!hasVertex)
183
+ {
184
+ return { minBounds: [ 0, 0, 0 ], maxBounds: [ 0, 0, 0 ] };
185
+ }
186
+
187
+ return { minBounds, maxBounds };
188
+ }
189
+
190
+ /**
191
+ * Generate area-weighted vertex normals from positions and triangle indices.
192
+ *
193
+ * @param {ArrayLike<number>} positions Flat xyz positions.
194
+ * @param {ArrayLike<number>} indices Flat triangle indices.
195
+ * @returns {Float32Array} Flat xyz normals.
196
+ */
197
+ export function generateNormals(positions, indices)
198
+ {
199
+ validatePositions(positions);
200
+ validateIndices(indices, positions.length / 3);
201
+
202
+ const
203
+ vertexCount = positions.length / 3,
204
+ normals = new Float32Array(positions.length);
205
+
206
+ for (let t = 0; t < indices.length; t += 3)
207
+ {
208
+ const
209
+ ia = indices[t] * 3,
210
+ ib = indices[t + 1] * 3,
211
+ ic = indices[t + 2] * 3,
212
+ ax = positions[ia],
213
+ ay = positions[ia + 1],
214
+ az = positions[ia + 2],
215
+ faceNormal = [ 0, 0, 0 ];
216
+
217
+ cross(
218
+ faceNormal,
219
+ [ positions[ib] - ax, positions[ib + 1] - ay, positions[ib + 2] - az ],
220
+ [ positions[ic] - ax, positions[ic + 1] - ay, positions[ic + 2] - az ]
221
+ );
222
+
223
+ for (const offset of [ ia, ib, ic ])
224
+ {
225
+ normals[offset] += faceNormal[0];
226
+ normals[offset + 1] += faceNormal[1];
227
+ normals[offset + 2] += faceNormal[2];
228
+ }
229
+ }
230
+
231
+ for (let i = 0; i < vertexCount; i++)
232
+ {
233
+ const
234
+ offset = i * 3,
235
+ length = Math.hypot(normals[offset], normals[offset + 1], normals[offset + 2]) || 1;
236
+
237
+ normals[offset] /= length;
238
+ normals[offset + 1] /= length;
239
+ normals[offset + 2] /= length;
240
+ }
241
+
242
+ return normals;
243
+ }
244
+
245
+ /**
246
+ * Generate per-vertex tangents from positions, normals, UVs and indices.
247
+ *
248
+ * @param {ArrayLike<number>} positions Flat xyz positions.
249
+ * @param {ArrayLike<number>} normals Flat xyz normals.
250
+ * @param {ArrayLike<number>} uvs Flat uv coordinates.
251
+ * @param {ArrayLike<number>} indices Flat triangle indices.
252
+ * @returns {Float32Array} Flat xyz tangents.
253
+ */
254
+ export function generateTangents(positions, normals, uvs, indices)
255
+ {
256
+ validatePositions(positions);
257
+
258
+ const
259
+ vertexCount = positions.length / 3,
260
+ tan1 = new Float32Array(vertexCount * 3),
261
+ tan2 = new Float32Array(vertexCount * 3);
262
+
263
+ if (!normals || normals.length !== positions.length ||
264
+ !uvs || uvs.length !== vertexCount * 2)
265
+ {
266
+ throw new Error("Tangent channels do not match the vertex count");
267
+ }
268
+ validateIndices(indices, vertexCount);
269
+
270
+ for (let t = 0; t < indices.length; t += 3)
271
+ {
272
+ const
273
+ i0 = indices[t],
274
+ i1 = indices[t + 1],
275
+ i2 = indices[t + 2],
276
+ p0 = i0 * 3,
277
+ p1 = i1 * 3,
278
+ p2 = i2 * 3,
279
+ t0 = i0 * 2,
280
+ t1 = i1 * 2,
281
+ t2 = i2 * 2,
282
+ x1 = positions[p1] - positions[p0],
283
+ y1 = positions[p1 + 1] - positions[p0 + 1],
284
+ z1 = positions[p1 + 2] - positions[p0 + 2],
285
+ x2 = positions[p2] - positions[p0],
286
+ y2 = positions[p2 + 1] - positions[p0 + 1],
287
+ z2 = positions[p2 + 2] - positions[p0 + 2],
288
+ s1 = uvs[t1] - uvs[t0],
289
+ v1 = uvs[t1 + 1] - uvs[t0 + 1],
290
+ s2 = uvs[t2] - uvs[t0],
291
+ v2 = uvs[t2 + 1] - uvs[t0 + 1],
292
+ divisor = s1 * v2 - s2 * v1,
293
+ scale = divisor ? 1 / divisor : 0,
294
+ sx = (v2 * x1 - v1 * x2) * scale,
295
+ sy = (v2 * y1 - v1 * y2) * scale,
296
+ sz = (v2 * z1 - v1 * z2) * scale,
297
+ tx = (s1 * x2 - s2 * x1) * scale,
298
+ ty = (s1 * y2 - s2 * y1) * scale,
299
+ tz = (s1 * z2 - s2 * z1) * scale;
300
+
301
+ for (const offset of [ p0, p1, p2 ])
302
+ {
303
+ tan1[offset] += sx;
304
+ tan1[offset + 1] += sy;
305
+ tan1[offset + 2] += sz;
306
+ tan2[offset] += tx;
307
+ tan2[offset + 1] += ty;
308
+ tan2[offset + 2] += tz;
309
+ }
310
+ }
311
+
312
+ const
313
+ tangents = new Float32Array(vertexCount * 3),
314
+ handedness = new Float32Array(vertexCount);
315
+
316
+ for (let i = 0; i < vertexCount; i++)
317
+ {
318
+ const
319
+ offset = i * 3,
320
+ nx = normals[offset],
321
+ ny = normals[offset + 1],
322
+ nz = normals[offset + 2],
323
+ tx = tan1[offset],
324
+ ty = tan1[offset + 1],
325
+ tz = tan1[offset + 2],
326
+ normalDotTangent = nx * tx + ny * ty + nz * tz;
327
+
328
+ let ox = tx - nx * normalDotTangent,
329
+ oy = ty - ny * normalDotTangent,
330
+ oz = tz - nz * normalDotTangent;
331
+
332
+ const length = Math.hypot(ox, oy, oz) || 1;
333
+
334
+ ox /= length;
335
+ oy /= length;
336
+ oz /= length;
337
+
338
+ tangents[offset] = ox;
339
+ tangents[offset + 1] = oy;
340
+ tangents[offset + 2] = oz;
341
+ handedness[i] = (
342
+ (ny * oz - nz * oy) * tan2[offset] +
343
+ (nz * ox - nx * oz) * tan2[offset + 1] +
344
+ (nx * oy - ny * ox) * tan2[offset + 2]
345
+ ) < 0 ? -1 : 1;
346
+ }
347
+
348
+ Object.defineProperty(tangents, "handedness", { value: handedness });
349
+ return tangents;
350
+ }
351
+
352
+ /**
353
+ * Generate a complete tangent frame while preserving per-vertex UV handedness.
354
+ *
355
+ * @param {ArrayLike<number>} positions Flat xyz positions.
356
+ * @param {ArrayLike<number>} normals Flat xyz normals.
357
+ * @param {ArrayLike<number>} uvs Flat uv coordinates.
358
+ * @param {ArrayLike<number>} indices Flat triangle indices.
359
+ * @param {object} [options] Generation options.
360
+ * @returns {{tangents: Float32Array, binormals: number[], handedness: Float32Array}}
361
+ */
362
+ export function generateTangentFrames(positions, normals, uvs, indices, options)
363
+ {
364
+ const
365
+ tangents = generateTangents(positions, normals, uvs, indices),
366
+ handedness = tangents.handedness,
367
+ binormals = generateBiNormals(normals, tangents, { ...options, handedness });
368
+
369
+ return { tangents, binormals, handedness };
370
+ }
371
+
372
+ /**
373
+ * Generate binormals as normalized `normal x tangent`.
374
+ *
375
+ * @param {ArrayLike<number>} normals Flat xyz normals.
376
+ * @param {ArrayLike<number>} tangents Flat xyz tangents.
377
+ * @param {object} [options] Generation options.
378
+ * @param {"right"|"left"} [options.uvHandedness] Handedness of generated basis.
379
+ * @returns {number[]} Flat xyz binormals.
380
+ */
381
+ export function generateBiNormals(normals, tangents, options = {})
382
+ {
383
+ if (normals.length !== tangents.length || normals.length % 3 !== 0)
384
+ {
385
+ throw new Error("generateBiNormals requires matching complete xyz channels");
386
+ }
387
+
388
+ const
389
+ conventionSign = options.uvHandedness === "left" ? -1 : 1,
390
+ binormals = new Array(normals.length);
391
+
392
+ for (let i = 0; i < normals.length; i += 3)
393
+ {
394
+ const
395
+ vertexSign = options.handedness?.[i / 3] ?? tangents.handedness?.[i / 3] ?? 1,
396
+ sign = conventionSign * vertexSign;
397
+ const b = normalize(
398
+ [ 0, 0, 0 ],
399
+ [
400
+ normals[i + 1] * tangents[i + 2] - normals[i + 2] * tangents[i + 1],
401
+ normals[i + 2] * tangents[i] - normals[i] * tangents[i + 2],
402
+ normals[i] * tangents[i + 1] - normals[i + 1] * tangents[i]
403
+ ]
404
+ );
405
+ binormals[i] = b[0] * sign;
406
+ binormals[i + 1] = b[1] * sign;
407
+ binormals[i + 2] = b[2] * sign;
408
+ }
409
+
410
+ return binormals;
411
+ }
412
+
413
+ export const mesh = Object.freeze({
414
+ triangleNormal,
415
+ triangleNormalTo,
416
+ triangleArea2,
417
+ isDegenerateTriangle,
418
+ computeBoundsFromPositions,
419
+ computeBoundsFromTriangles,
420
+ generateNormals,
421
+ generateTangents,
422
+ generateTangentFrames,
423
+ generateBiNormals
424
+ });