@es3d/es3d 1.0.0 → 1.0.2

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 (80) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +95 -86
  3. package/docs/SPEC.md +426 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +390 -377
  6. package/src/cache/geometry-store.js +392 -392
  7. package/src/cache/mesh-bundle.js +123 -123
  8. package/src/cache/sha256.js +73 -73
  9. package/src/format/ast.js +238 -238
  10. package/src/format/colors.js +93 -93
  11. package/src/format/compact-parser.js +171 -171
  12. package/src/format/compact-tokenizer.js +165 -165
  13. package/src/format/errors.js +247 -247
  14. package/src/format/json.js +55 -55
  15. package/src/format/limits.js +14 -14
  16. package/src/format/parse.js +132 -92
  17. package/src/format/schema.js +617 -617
  18. package/src/format/serializer.js +333 -333
  19. package/src/format/validator.js +330 -330
  20. package/src/format/value-error.js +12 -12
  21. package/src/format/value-format.js +314 -314
  22. package/src/format/values.js +459 -459
  23. package/src/format/xml-parser.js +136 -136
  24. package/src/format/xml-tokenizer.js +237 -237
  25. package/src/geometry/contour.js +86 -86
  26. package/src/geometry/extrude.js +175 -175
  27. package/src/geometry/gear.js +85 -85
  28. package/src/geometry/geometry-cache.js +159 -159
  29. package/src/geometry/lod-error.js +178 -178
  30. package/src/geometry/mesh.js +458 -458
  31. package/src/geometry/primitives.js +299 -299
  32. package/src/geometry/registry.js +645 -645
  33. package/src/geometry/revolve.js +136 -136
  34. package/src/geometry/sweep.js +441 -441
  35. package/src/index.js +47 -47
  36. package/src/io/compression.js +67 -67
  37. package/src/io/convert.js +236 -236
  38. package/src/io/download.js +13 -13
  39. package/src/io/external-scene.js +127 -127
  40. package/src/io/file-loader.js +28 -28
  41. package/src/io/gltf-pack.js +1461 -0
  42. package/src/io/gltf.js +630 -630
  43. package/src/io/mesh-import.js +104 -104
  44. package/src/io/obj.js +352 -352
  45. package/src/io/step-brep.js +439 -439
  46. package/src/io/step-exact.js +1149 -1149
  47. package/src/io/step.js +1418 -1418
  48. package/src/io/stl.js +193 -193
  49. package/src/io/svg-import.js +787 -787
  50. package/src/material/materials.js +154 -154
  51. package/src/math/mat4.js +475 -475
  52. package/src/math/random.js +30 -30
  53. package/src/math/trig.js +65 -65
  54. package/src/math/vec3.js +81 -81
  55. package/src/optimize/optimizer.js +604 -579
  56. package/src/path/path-flatten.js +322 -322
  57. package/src/path/path-parser.js +332 -332
  58. package/src/path/shapes.js +129 -129
  59. package/src/path/triangulate.js +517 -517
  60. package/src/pipeline.js +74 -74
  61. package/src/render/camera.js +168 -168
  62. package/src/render/frame-builder.js +362 -362
  63. package/src/render/geometry-pool.js +149 -149
  64. package/src/render/lighting.js +149 -149
  65. package/src/render/orbit-controls.js +237 -237
  66. package/src/render/renderer.js +258 -249
  67. package/src/render/scroll-timeline.js +104 -104
  68. package/src/render/shaders/glsl.js +285 -285
  69. package/src/render/shaders/wgsl.js +252 -252
  70. package/src/render/stage.js +365 -352
  71. package/src/render/texture-cache.js +40 -40
  72. package/src/render/textures.js +223 -223
  73. package/src/render/webgl2/webgl2-backend.js +586 -532
  74. package/src/render/webgpu/webgpu-backend.js +482 -480
  75. package/src/scene/flatten.js +754 -754
  76. package/src/scene/picking.js +161 -161
  77. package/src/scene/scene-graph.js +320 -320
  78. package/src/transform/transform.js +211 -211
  79. package/src/wasm/kernels-binary.js +3 -3
  80. package/src/wasm/kernels.js +385 -385
@@ -1,458 +1,458 @@
1
- // Mesh data structure and building helpers.
2
- //
3
- // A Mesh is an indexed triangle (or line) list with interleavable attributes:
4
- // positions (xyz), normals (xyz), uvs (uv). Generators produce meshes in a
5
- // canonical local space; the renderer packs them into shared GPU buffers.
6
-
7
- import { computeNormalsWasm } from '../wasm/kernels.js';
8
-
9
- export class Mesh {
10
- /**
11
- * @param {{positions: Float32Array, normals?: Float32Array, uvs?: Float32Array, indices: Uint32Array, kind?: 'triangles'|'lines'}} o
12
- */
13
- constructor(o) {
14
- this.positions = o.positions;
15
- const n = this.positions.length / 3;
16
- this.normals = o.normals || new Float32Array(n * 3);
17
- this.uvs = o.uvs || new Float32Array(n * 2);
18
- this.indices = o.indices;
19
- this.kind = o.kind || 'triangles';
20
- this._bounds = null;
21
- this._edges = null;
22
- this._normalLines = null;
23
- }
24
-
25
- get vertexCount() {
26
- return this.positions.length / 3;
27
- }
28
-
29
- get primitiveCount() {
30
- return this.kind === 'lines' ? this.indices.length / 2 : this.indices.length / 3;
31
- }
32
-
33
- get byteSize() {
34
- return this.positions.byteLength + this.normals.byteLength + this.uvs.byteLength + this.indices.byteLength;
35
- }
36
-
37
- get bounds() {
38
- if (!this._bounds) {
39
- const p = this.positions;
40
- const min = [Infinity, Infinity, Infinity];
41
- const max = [-Infinity, -Infinity, -Infinity];
42
- for (let i = 0; i < p.length; i += 3) {
43
- for (let k = 0; k < 3; k++) {
44
- const v = p[i + k];
45
- if (v < min[k]) min[k] = v;
46
- if (v > max[k]) max[k] = v;
47
- }
48
- }
49
- if (p.length === 0) {
50
- min.fill(0);
51
- max.fill(0);
52
- }
53
- this._bounds = { min, max };
54
- }
55
- return this._bounds;
56
- }
57
-
58
- /** Unique edges as a line index list (for wireframe display). */
59
- edges() {
60
- if (this._edges) return this._edges;
61
- if (this.kind === 'lines') return (this._edges = this.indices);
62
- // weld positions so that seams do not show as edges twice
63
- const weld = weldMap(this.positions);
64
- const seen = new Set();
65
- const out = [];
66
- const idx = this.indices;
67
- const nv = this.vertexCount;
68
- for (let i = 0; i < idx.length; i += 3) {
69
- for (let k = 0; k < 3; k++) {
70
- const a = idx[i + k];
71
- const b = idx[i + ((k + 1) % 3)];
72
- const wa = weld[a], wb = weld[b];
73
- if (wa === wb) continue;
74
- const key = wa < wb ? wa * nv + wb : wb * nv + wa;
75
- if (seen.has(key)) continue;
76
- seen.add(key);
77
- out.push(a, b);
78
- }
79
- }
80
- this._edges = Uint32Array.from(out);
81
- return this._edges;
82
- }
83
-
84
- /** Line vertices for normal display: [p, n, flag] where flag 0 = base, 1 = tip. */
85
- normalLines() {
86
- if (this._normalLines) return this._normalLines;
87
- const n = this.vertexCount;
88
- const positions = new Float32Array(n * 6);
89
- const normals = new Float32Array(n * 6);
90
- const uvs = new Float32Array(n * 4);
91
- const indices = new Uint32Array(n * 2);
92
- for (let i = 0; i < n; i++) {
93
- for (let k = 0; k < 3; k++) {
94
- positions[i * 6 + k] = this.positions[i * 3 + k];
95
- positions[i * 6 + 3 + k] = this.positions[i * 3 + k];
96
- normals[i * 6 + k] = this.normals[i * 3 + k];
97
- normals[i * 6 + 3 + k] = this.normals[i * 3 + k];
98
- }
99
- uvs[i * 4 + 2] = 1; // tip flag in u
100
- indices[i * 2] = i * 2;
101
- indices[i * 2 + 1] = i * 2 + 1;
102
- }
103
- this._normalLines = new Mesh({ positions, normals, uvs, indices, kind: 'lines' });
104
- return this._normalLines;
105
- }
106
- }
107
-
108
- /** Map each vertex to the first vertex with the same position (quantized to 1e-6). */
109
- export function weldMap(positions) {
110
- const n = positions.length / 3;
111
- const map = new Uint32Array(n);
112
- // open addressing hash table over quantized coordinates (no string keys)
113
- let size = 16;
114
- while (size < n * 2) size *= 2;
115
- const mask = size - 1;
116
- const table = new Int32Array(size).fill(-1);
117
- const q = new Float64Array(n * 3);
118
- for (let i = 0; i < n * 3; i++) q[i] = Math.round(positions[i] * 1e6);
119
- for (let i = 0; i < n; i++) {
120
- const x = q[i * 3], y = q[i * 3 + 1], z = q[i * 3 + 2];
121
- let h = (Math.imul(x | 0, 73856093) ^ Math.imul(y | 0, 19349663) ^ Math.imul(z | 0, 83492791)) & mask;
122
- for (;;) {
123
- const f = table[h];
124
- if (f < 0) {
125
- table[h] = i;
126
- map[i] = i;
127
- break;
128
- }
129
- if (q[f * 3] === x && q[f * 3 + 1] === y && q[f * 3 + 2] === z) {
130
- map[i] = f;
131
- break;
132
- }
133
- h = (h + 1) & mask;
134
- }
135
- }
136
- return map;
137
- }
138
-
139
- /** Growable mesh builder. */
140
- export class MeshBuilder {
141
- /**
142
- * @param {'triangles'|'lines'} kind
143
- * @param {number} vertexCapacity vertices to allocate for; a generator that
144
- * knows its exact counts passes them, so nothing grows or is copied
145
- * @param {number} indexCapacity
146
- */
147
- constructor(kind = 'triangles', vertexCapacity = 256, indexCapacity = 768) {
148
- this.kind = kind;
149
- vertexCapacity = Math.max(1, vertexCapacity);
150
- indexCapacity = Math.max(1, indexCapacity);
151
- this.p = new Float32Array(vertexCapacity * 3);
152
- this.n = new Float32Array(vertexCapacity * 3);
153
- this.t = new Float32Array(vertexCapacity * 2);
154
- this.i = new Uint32Array(indexCapacity);
155
- this.vc = 0;
156
- this.ic = 0;
157
- }
158
-
159
- growV(extra) {
160
- const need = this.vc + extra;
161
- if (need * 3 <= this.p.length) return;
162
- let cap = this.p.length / 3;
163
- while (cap < need) cap *= 2;
164
- const p = new Float32Array(cap * 3);
165
- p.set(this.p);
166
- const n = new Float32Array(cap * 3);
167
- n.set(this.n);
168
- const t = new Float32Array(cap * 2);
169
- t.set(this.t);
170
- this.p = p;
171
- this.n = n;
172
- this.t = t;
173
- }
174
-
175
- growI(extra) {
176
- const need = this.ic + extra;
177
- if (need <= this.i.length) return;
178
- let cap = this.i.length;
179
- while (cap < need) cap *= 2;
180
- const i = new Uint32Array(cap);
181
- i.set(this.i);
182
- this.i = i;
183
- }
184
-
185
- vertex(px, py, pz, nx = 0, ny = 0, nz = 0, u = 0, v = 0) {
186
- if (this.vc * 3 >= this.p.length) this.growV(1);
187
- const k = this.vc;
188
- this.p[k * 3] = px;
189
- this.p[k * 3 + 1] = py;
190
- this.p[k * 3 + 2] = pz;
191
- this.n[k * 3] = nx;
192
- this.n[k * 3 + 1] = ny;
193
- this.n[k * 3 + 2] = nz;
194
- this.t[k * 2] = u;
195
- this.t[k * 2 + 1] = v;
196
- this.vc++;
197
- return k;
198
- }
199
-
200
- tri(a, b, c) {
201
- if (this.ic + 3 > this.i.length) this.growI(3);
202
- this.i[this.ic++] = a;
203
- this.i[this.ic++] = b;
204
- this.i[this.ic++] = c;
205
- }
206
-
207
- /** Quad a-b-c-d in counter-clockwise order (as seen from the front). */
208
- quad(a, b, c, d) {
209
- this.tri(a, b, c);
210
- this.tri(a, c, d);
211
- }
212
-
213
- line(a, b) {
214
- if (this.ic + 2 > this.i.length) this.growI(2);
215
- this.i[this.ic++] = a;
216
- this.i[this.ic++] = b;
217
- }
218
-
219
- get vertexCount() {
220
- return this.vc;
221
- }
222
-
223
- build() {
224
- // a builder sized exactly (generators that count first) hands over its arrays
225
- const exact = (a, n) => (a.length === n ? a : a.slice(0, n));
226
- return new Mesh({
227
- positions: exact(this.p, this.vc * 3),
228
- normals: exact(this.n, this.vc * 3),
229
- uvs: exact(this.t, this.vc * 2),
230
- indices: exact(this.i, this.ic),
231
- kind: this.kind,
232
- });
233
- }
234
- }
235
-
236
- // Every generator writes through a builder, and a builder lives only while
237
- // one mesh is made. When a full collection finds no builder alive, V8 drops
238
- // the hidden class of the instances and discards all optimized code that
239
- // relied on it, so the next scene would run the generators unoptimized again.
240
- // This one never dies, and neither does the class.
241
- MeshBuilder.shapeKeeper = new MeshBuilder('triangles', 1, 1);
242
-
243
- /**
244
- * Compute vertex normals for an indexed triangle soup with a crease angle:
245
- * faces meeting at an angle larger than the crease get split normals.
246
- * Returns a new mesh (vertices may be duplicated at creases).
247
- */
248
- export function computeNormals(positions, indices, creaseDeg = 30, uvs = null) {
249
- const crease = Math.min(Math.max(creaseDeg, 0), 180);
250
- const cosCrease = Math.cos((crease * Math.PI) / 180);
251
- if (crease === crease) {
252
- const w = computeNormalsWasm(positions, indices, crease, cosCrease, uvs);
253
- if (w) return new Mesh({ ...w, kind: 'triangles' });
254
- }
255
- const nt = indices.length / 3;
256
- const faceN = new Float64Array(nt * 3); // area weighted (not normalized)
257
- const faceU = new Float64Array(nt * 3); // unit length
258
- for (let t = 0; t < nt; t++) {
259
- const a = indices[t * 3] * 3, b = indices[t * 3 + 1] * 3, c = indices[t * 3 + 2] * 3;
260
- const e1x = positions[b] - positions[a], e1y = positions[b + 1] - positions[a + 1], e1z = positions[b + 2] - positions[a + 2];
261
- const e2x = positions[c] - positions[a], e2y = positions[c + 1] - positions[a + 1], e2z = positions[c + 2] - positions[a + 2];
262
- const x = e1y * e2z - e1z * e2y, y = e1z * e2x - e1x * e2z, z = e1x * e2y - e1y * e2x;
263
- faceN[t * 3] = x;
264
- faceN[t * 3 + 1] = y;
265
- faceN[t * 3 + 2] = z;
266
- const l = Math.sqrt(x * x + y * y + z * z) || 1;
267
- faceU[t * 3] = x / l;
268
- faceU[t * 3 + 1] = y / l;
269
- faceU[t * 3 + 2] = z / l;
270
- }
271
- const weld = weldMap(positions);
272
- const nv = positions.length / 3;
273
- // faces incident to each welded vertex (CSR layout), needed only to test the crease angle
274
- let counts = null, inc = null;
275
- if (crease > 0 && crease < 180) {
276
- counts = new Uint32Array(nv + 1);
277
- for (let i = 0; i < indices.length; i++) counts[weld[indices[i]] + 1]++;
278
- for (let i = 0; i < nv; i++) counts[i + 1] += counts[i];
279
- inc = new Uint32Array(indices.length);
280
- const fill = counts.slice(0, nv);
281
- for (let i = 0; i < indices.length; i++) inc[fill[weld[indices[i]]]++] = (i / 3) | 0;
282
- }
283
- // without creases every corner of a welded vertex gets the same normal:
284
- // sum the faces, then normalize once per welded vertex
285
- let smoothN = null;
286
- if (crease >= 180) {
287
- smoothN = new Float64Array(nv * 3);
288
- for (let i = 0; i < indices.length; i++) {
289
- const w = weld[indices[i]] * 3, f = ((i / 3) | 0) * 3;
290
- smoothN[w] += faceN[f];
291
- smoothN[w + 1] += faceN[f + 1];
292
- smoothN[w + 2] += faceN[f + 2];
293
- }
294
- for (let w = 0; w < nv * 3; w += 3) {
295
- const x = smoothN[w], y = smoothN[w + 1], z = smoothN[w + 2];
296
- const l = Math.sqrt(x * x + y * y + z * z) || 1;
297
- smoothN[w] = x / l;
298
- smoothN[w + 1] = y / l;
299
- smoothN[w + 2] = z / l;
300
- }
301
- }
302
- // output vertices: as many as the input until creases split some
303
- let cap = Math.max(1, nv);
304
- let outP = new Float32Array(cap * 3), outN = new Float32Array(cap * 3), outT = new Float32Array(cap * 2);
305
- const outI = new Uint32Array(indices.length);
306
- let vc = 0;
307
- let smoothOk = smoothN !== null;
308
- for (let k = 0; smoothOk && k < nv * 3; k++) if (smoothN[k] !== smoothN[k]) smoothOk = false;
309
- if (smoothOk) {
310
- // one normal per welded vertex, so every input vertex becomes exactly one
311
- // output vertex, in the order the triangles first use it
312
- const out = new Int32Array(nv).fill(-1);
313
- for (let i = 0; i < indices.length; i++) {
314
- const vi = indices[i];
315
- let idx = out[vi];
316
- if (idx < 0) {
317
- idx = out[vi] = vc++;
318
- const w = weld[vi] * 3;
319
- outP[idx * 3] = positions[vi * 3];
320
- outP[idx * 3 + 1] = positions[vi * 3 + 1];
321
- outP[idx * 3 + 2] = positions[vi * 3 + 2];
322
- outN[idx * 3] = smoothN[w];
323
- outN[idx * 3 + 1] = smoothN[w + 1];
324
- outN[idx * 3 + 2] = smoothN[w + 2];
325
- if (uvs) {
326
- outT[idx * 2] = uvs[vi * 2];
327
- outT[idx * 2 + 1] = uvs[vi * 2 + 1];
328
- }
329
- }
330
- outI[i] = idx;
331
- }
332
- } else {
333
- // output vertices already emitted for each input vertex, as a linked list
334
- // keyed by the normal quantized to 1e-4 (corners with equal normals share)
335
- const head = new Int32Array(nv).fill(-1);
336
- let next = new Int32Array(cap);
337
- let qn = new Int32Array(cap * 3);
338
- for (let i = 0; i < indices.length; i++) {
339
- const t = (i / 3) | 0;
340
- const vi = indices[i];
341
- const w = weld[vi];
342
- let nx = 0, ny = 0, nz = 0;
343
- if (crease <= 0) {
344
- nx = faceU[t * 3];
345
- ny = faceU[t * 3 + 1];
346
- nz = faceU[t * 3 + 2];
347
- } else if (smoothN) {
348
- nx = smoothN[w * 3];
349
- ny = smoothN[w * 3 + 1];
350
- nz = smoothN[w * 3 + 2];
351
- } else {
352
- // faces within the crease angle of this corner's face
353
- const fx = faceU[t * 3], fy = faceU[t * 3 + 1], fz = faceU[t * 3 + 2];
354
- for (let k = counts[w]; k < counts[w + 1]; k++) {
355
- const f = inc[k];
356
- if (f === t || faceU[f * 3] * fx + faceU[f * 3 + 1] * fy + faceU[f * 3 + 2] * fz >= cosCrease) {
357
- nx += faceN[f * 3];
358
- ny += faceN[f * 3 + 1];
359
- nz += faceN[f * 3 + 2];
360
- }
361
- }
362
- const l = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
363
- nx /= l;
364
- ny /= l;
365
- nz /= l;
366
- }
367
- const qx = Math.round(nx * 1e4), qy = Math.round(ny * 1e4), qz = Math.round(nz * 1e4);
368
- let idx = head[vi];
369
- while (idx >= 0 && !(qn[idx * 3] === qx && qn[idx * 3 + 1] === qy && qn[idx * 3 + 2] === qz)) idx = next[idx];
370
- if (idx < 0) {
371
- idx = vc++;
372
- if (idx >= cap) {
373
- cap *= 2;
374
- const grow = (a, size) => {
375
- const g = new a.constructor(cap * size);
376
- g.set(a);
377
- return g;
378
- };
379
- outP = grow(outP, 3);
380
- outN = grow(outN, 3);
381
- outT = grow(outT, 2);
382
- next = grow(next, 1);
383
- qn = grow(qn, 3);
384
- }
385
- outP[idx * 3] = positions[vi * 3];
386
- outP[idx * 3 + 1] = positions[vi * 3 + 1];
387
- outP[idx * 3 + 2] = positions[vi * 3 + 2];
388
- outN[idx * 3] = nx;
389
- outN[idx * 3 + 1] = ny;
390
- outN[idx * 3 + 2] = nz;
391
- if (uvs) {
392
- outT[idx * 2] = uvs[vi * 2];
393
- outT[idx * 2 + 1] = uvs[vi * 2 + 1];
394
- }
395
- next[idx] = head[vi];
396
- head[vi] = idx;
397
- qn[idx * 3] = qx;
398
- qn[idx * 3 + 1] = qy;
399
- qn[idx * 3 + 2] = qz;
400
- }
401
- outI[i] = idx;
402
- }
403
- }
404
- const exact = (a, n) => (a.length === n ? a : a.slice(0, n));
405
- return new Mesh({ positions: exact(outP, vc * 3), normals: exact(outN, vc * 3), uvs: exact(outT, vc * 2), indices: outI, kind: 'triangles' });
406
- }
407
-
408
- /** Merge meshes transformed by matrices (used by exporters). */
409
- export function mergeTransformed(items) {
410
- let vc = 0;
411
- let ic = 0;
412
- for (const { mesh } of items) {
413
- vc += mesh.vertexCount;
414
- ic += mesh.indices.length;
415
- }
416
- const positions = new Float32Array(vc * 3);
417
- const normals = new Float32Array(vc * 3);
418
- const uvs = new Float32Array(vc * 2);
419
- const indices = new Uint32Array(ic);
420
- let vo = 0;
421
- let io = 0;
422
- for (const { mesh, matrix: m } of items) {
423
- const p = mesh.positions, n = mesh.normals;
424
- // normal matrix = cofactor of the upper 3x3 (with determinant sign)
425
- const c0 = [m[5] * m[10] - m[6] * m[9], m[6] * m[8] - m[4] * m[10], m[4] * m[9] - m[5] * m[8]];
426
- const c1 = [m[9] * m[2] - m[10] * m[1], m[10] * m[0] - m[8] * m[2], m[8] * m[1] - m[9] * m[0]];
427
- const c2 = [m[1] * m[6] - m[2] * m[5], m[2] * m[4] - m[0] * m[6], m[0] * m[5] - m[1] * m[4]];
428
- const det = m[0] * c0[0] + m[1] * c0[1] + m[2] * c0[2];
429
- const sgn = det < 0 ? -1 : 1;
430
- for (let i = 0; i < mesh.vertexCount; i++) {
431
- const x = p[i * 3], y = p[i * 3 + 1], z = p[i * 3 + 2];
432
- positions[(vo + i) * 3] = m[0] * x + m[4] * y + m[8] * z + m[12];
433
- positions[(vo + i) * 3 + 1] = m[1] * x + m[5] * y + m[9] * z + m[13];
434
- positions[(vo + i) * 3 + 2] = m[2] * x + m[6] * y + m[10] * z + m[14];
435
- const nx = n[i * 3], ny = n[i * 3 + 1], nz = n[i * 3 + 2];
436
- let ox = (c0[0] * nx + c1[0] * ny + c2[0] * nz) * sgn;
437
- let oy = (c0[1] * nx + c1[1] * ny + c2[1] * nz) * sgn;
438
- let oz = (c0[2] * nx + c1[2] * ny + c2[2] * nz) * sgn;
439
- const l = Math.hypot(ox, oy, oz) || 1;
440
- normals[(vo + i) * 3] = ox / l;
441
- normals[(vo + i) * 3 + 1] = oy / l;
442
- normals[(vo + i) * 3 + 2] = oz / l;
443
- uvs[(vo + i) * 2] = mesh.uvs[i * 2];
444
- uvs[(vo + i) * 2 + 1] = mesh.uvs[i * 2 + 1];
445
- }
446
- const flip = det < 0 && mesh.kind === 'triangles';
447
- for (let i = 0; i < mesh.indices.length; i += 3) {
448
- if (mesh.kind === 'lines') break;
449
- indices[io + i] = mesh.indices[i] + vo;
450
- indices[io + i + 1] = mesh.indices[flip ? i + 2 : i + 1] + vo;
451
- indices[io + i + 2] = mesh.indices[flip ? i + 1 : i + 2] + vo;
452
- }
453
- if (mesh.kind === 'lines') for (let i = 0; i < mesh.indices.length; i++) indices[io + i] = mesh.indices[i] + vo;
454
- vo += mesh.vertexCount;
455
- io += mesh.indices.length;
456
- }
457
- return new Mesh({ positions, normals, uvs, indices, kind: items.length ? items[0].mesh.kind : 'triangles' });
458
- }
1
+ // Mesh data structure and building helpers.
2
+ //
3
+ // A Mesh is an indexed triangle (or line) list with interleavable attributes:
4
+ // positions (xyz), normals (xyz), uvs (uv). Generators produce meshes in a
5
+ // canonical local space; the renderer packs them into shared GPU buffers.
6
+
7
+ import { computeNormalsWasm } from '../wasm/kernels.js';
8
+
9
+ export class Mesh {
10
+ /**
11
+ * @param {{positions: Float32Array, normals?: Float32Array, uvs?: Float32Array, indices: Uint32Array, kind?: 'triangles'|'lines'}} o
12
+ */
13
+ constructor(o) {
14
+ this.positions = o.positions;
15
+ const n = this.positions.length / 3;
16
+ this.normals = o.normals || new Float32Array(n * 3);
17
+ this.uvs = o.uvs || new Float32Array(n * 2);
18
+ this.indices = o.indices;
19
+ this.kind = o.kind || 'triangles';
20
+ this._bounds = null;
21
+ this._edges = null;
22
+ this._normalLines = null;
23
+ }
24
+
25
+ get vertexCount() {
26
+ return this.positions.length / 3;
27
+ }
28
+
29
+ get primitiveCount() {
30
+ return this.kind === 'lines' ? this.indices.length / 2 : this.indices.length / 3;
31
+ }
32
+
33
+ get byteSize() {
34
+ return this.positions.byteLength + this.normals.byteLength + this.uvs.byteLength + this.indices.byteLength;
35
+ }
36
+
37
+ get bounds() {
38
+ if (!this._bounds) {
39
+ const p = this.positions;
40
+ const min = [Infinity, Infinity, Infinity];
41
+ const max = [-Infinity, -Infinity, -Infinity];
42
+ for (let i = 0; i < p.length; i += 3) {
43
+ for (let k = 0; k < 3; k++) {
44
+ const v = p[i + k];
45
+ if (v < min[k]) min[k] = v;
46
+ if (v > max[k]) max[k] = v;
47
+ }
48
+ }
49
+ if (p.length === 0) {
50
+ min.fill(0);
51
+ max.fill(0);
52
+ }
53
+ this._bounds = { min, max };
54
+ }
55
+ return this._bounds;
56
+ }
57
+
58
+ /** Unique edges as a line index list (for wireframe display). */
59
+ edges() {
60
+ if (this._edges) return this._edges;
61
+ if (this.kind === 'lines') return (this._edges = this.indices);
62
+ // weld positions so that seams do not show as edges twice
63
+ const weld = weldMap(this.positions);
64
+ const seen = new Set();
65
+ const out = [];
66
+ const idx = this.indices;
67
+ const nv = this.vertexCount;
68
+ for (let i = 0; i < idx.length; i += 3) {
69
+ for (let k = 0; k < 3; k++) {
70
+ const a = idx[i + k];
71
+ const b = idx[i + ((k + 1) % 3)];
72
+ const wa = weld[a], wb = weld[b];
73
+ if (wa === wb) continue;
74
+ const key = wa < wb ? wa * nv + wb : wb * nv + wa;
75
+ if (seen.has(key)) continue;
76
+ seen.add(key);
77
+ out.push(a, b);
78
+ }
79
+ }
80
+ this._edges = Uint32Array.from(out);
81
+ return this._edges;
82
+ }
83
+
84
+ /** Line vertices for normal display: [p, n, flag] where flag 0 = base, 1 = tip. */
85
+ normalLines() {
86
+ if (this._normalLines) return this._normalLines;
87
+ const n = this.vertexCount;
88
+ const positions = new Float32Array(n * 6);
89
+ const normals = new Float32Array(n * 6);
90
+ const uvs = new Float32Array(n * 4);
91
+ const indices = new Uint32Array(n * 2);
92
+ for (let i = 0; i < n; i++) {
93
+ for (let k = 0; k < 3; k++) {
94
+ positions[i * 6 + k] = this.positions[i * 3 + k];
95
+ positions[i * 6 + 3 + k] = this.positions[i * 3 + k];
96
+ normals[i * 6 + k] = this.normals[i * 3 + k];
97
+ normals[i * 6 + 3 + k] = this.normals[i * 3 + k];
98
+ }
99
+ uvs[i * 4 + 2] = 1; // tip flag in u
100
+ indices[i * 2] = i * 2;
101
+ indices[i * 2 + 1] = i * 2 + 1;
102
+ }
103
+ this._normalLines = new Mesh({ positions, normals, uvs, indices, kind: 'lines' });
104
+ return this._normalLines;
105
+ }
106
+ }
107
+
108
+ /** Map each vertex to the first vertex with the same position (quantized to 1e-6). */
109
+ export function weldMap(positions) {
110
+ const n = positions.length / 3;
111
+ const map = new Uint32Array(n);
112
+ // open addressing hash table over quantized coordinates (no string keys)
113
+ let size = 16;
114
+ while (size < n * 2) size *= 2;
115
+ const mask = size - 1;
116
+ const table = new Int32Array(size).fill(-1);
117
+ const q = new Float64Array(n * 3);
118
+ for (let i = 0; i < n * 3; i++) q[i] = Math.round(positions[i] * 1e6);
119
+ for (let i = 0; i < n; i++) {
120
+ const x = q[i * 3], y = q[i * 3 + 1], z = q[i * 3 + 2];
121
+ let h = (Math.imul(x | 0, 73856093) ^ Math.imul(y | 0, 19349663) ^ Math.imul(z | 0, 83492791)) & mask;
122
+ for (;;) {
123
+ const f = table[h];
124
+ if (f < 0) {
125
+ table[h] = i;
126
+ map[i] = i;
127
+ break;
128
+ }
129
+ if (q[f * 3] === x && q[f * 3 + 1] === y && q[f * 3 + 2] === z) {
130
+ map[i] = f;
131
+ break;
132
+ }
133
+ h = (h + 1) & mask;
134
+ }
135
+ }
136
+ return map;
137
+ }
138
+
139
+ /** Growable mesh builder. */
140
+ export class MeshBuilder {
141
+ /**
142
+ * @param {'triangles'|'lines'} kind
143
+ * @param {number} vertexCapacity vertices to allocate for; a generator that
144
+ * knows its exact counts passes them, so nothing grows or is copied
145
+ * @param {number} indexCapacity
146
+ */
147
+ constructor(kind = 'triangles', vertexCapacity = 256, indexCapacity = 768) {
148
+ this.kind = kind;
149
+ vertexCapacity = Math.max(1, vertexCapacity);
150
+ indexCapacity = Math.max(1, indexCapacity);
151
+ this.p = new Float32Array(vertexCapacity * 3);
152
+ this.n = new Float32Array(vertexCapacity * 3);
153
+ this.t = new Float32Array(vertexCapacity * 2);
154
+ this.i = new Uint32Array(indexCapacity);
155
+ this.vc = 0;
156
+ this.ic = 0;
157
+ }
158
+
159
+ growV(extra) {
160
+ const need = this.vc + extra;
161
+ if (need * 3 <= this.p.length) return;
162
+ let cap = this.p.length / 3;
163
+ while (cap < need) cap *= 2;
164
+ const p = new Float32Array(cap * 3);
165
+ p.set(this.p);
166
+ const n = new Float32Array(cap * 3);
167
+ n.set(this.n);
168
+ const t = new Float32Array(cap * 2);
169
+ t.set(this.t);
170
+ this.p = p;
171
+ this.n = n;
172
+ this.t = t;
173
+ }
174
+
175
+ growI(extra) {
176
+ const need = this.ic + extra;
177
+ if (need <= this.i.length) return;
178
+ let cap = this.i.length;
179
+ while (cap < need) cap *= 2;
180
+ const i = new Uint32Array(cap);
181
+ i.set(this.i);
182
+ this.i = i;
183
+ }
184
+
185
+ vertex(px, py, pz, nx = 0, ny = 0, nz = 0, u = 0, v = 0) {
186
+ if (this.vc * 3 >= this.p.length) this.growV(1);
187
+ const k = this.vc;
188
+ this.p[k * 3] = px;
189
+ this.p[k * 3 + 1] = py;
190
+ this.p[k * 3 + 2] = pz;
191
+ this.n[k * 3] = nx;
192
+ this.n[k * 3 + 1] = ny;
193
+ this.n[k * 3 + 2] = nz;
194
+ this.t[k * 2] = u;
195
+ this.t[k * 2 + 1] = v;
196
+ this.vc++;
197
+ return k;
198
+ }
199
+
200
+ tri(a, b, c) {
201
+ if (this.ic + 3 > this.i.length) this.growI(3);
202
+ this.i[this.ic++] = a;
203
+ this.i[this.ic++] = b;
204
+ this.i[this.ic++] = c;
205
+ }
206
+
207
+ /** Quad a-b-c-d in counter-clockwise order (as seen from the front). */
208
+ quad(a, b, c, d) {
209
+ this.tri(a, b, c);
210
+ this.tri(a, c, d);
211
+ }
212
+
213
+ line(a, b) {
214
+ if (this.ic + 2 > this.i.length) this.growI(2);
215
+ this.i[this.ic++] = a;
216
+ this.i[this.ic++] = b;
217
+ }
218
+
219
+ get vertexCount() {
220
+ return this.vc;
221
+ }
222
+
223
+ build() {
224
+ // a builder sized exactly (generators that count first) hands over its arrays
225
+ const exact = (a, n) => (a.length === n ? a : a.slice(0, n));
226
+ return new Mesh({
227
+ positions: exact(this.p, this.vc * 3),
228
+ normals: exact(this.n, this.vc * 3),
229
+ uvs: exact(this.t, this.vc * 2),
230
+ indices: exact(this.i, this.ic),
231
+ kind: this.kind,
232
+ });
233
+ }
234
+ }
235
+
236
+ // Every generator writes through a builder, and a builder lives only while
237
+ // one mesh is made. When a full collection finds no builder alive, V8 drops
238
+ // the hidden class of the instances and discards all optimized code that
239
+ // relied on it, so the next scene would run the generators unoptimized again.
240
+ // This one never dies, and neither does the class.
241
+ MeshBuilder.shapeKeeper = new MeshBuilder('triangles', 1, 1);
242
+
243
+ /**
244
+ * Compute vertex normals for an indexed triangle soup with a crease angle:
245
+ * faces meeting at an angle larger than the crease get split normals.
246
+ * Returns a new mesh (vertices may be duplicated at creases).
247
+ */
248
+ export function computeNormals(positions, indices, creaseDeg = 30, uvs = null) {
249
+ const crease = Math.min(Math.max(creaseDeg, 0), 180);
250
+ const cosCrease = Math.cos((crease * Math.PI) / 180);
251
+ if (crease === crease) {
252
+ const w = computeNormalsWasm(positions, indices, crease, cosCrease, uvs);
253
+ if (w) return new Mesh({ ...w, kind: 'triangles' });
254
+ }
255
+ const nt = indices.length / 3;
256
+ const faceN = new Float64Array(nt * 3); // area weighted (not normalized)
257
+ const faceU = new Float64Array(nt * 3); // unit length
258
+ for (let t = 0; t < nt; t++) {
259
+ const a = indices[t * 3] * 3, b = indices[t * 3 + 1] * 3, c = indices[t * 3 + 2] * 3;
260
+ const e1x = positions[b] - positions[a], e1y = positions[b + 1] - positions[a + 1], e1z = positions[b + 2] - positions[a + 2];
261
+ const e2x = positions[c] - positions[a], e2y = positions[c + 1] - positions[a + 1], e2z = positions[c + 2] - positions[a + 2];
262
+ const x = e1y * e2z - e1z * e2y, y = e1z * e2x - e1x * e2z, z = e1x * e2y - e1y * e2x;
263
+ faceN[t * 3] = x;
264
+ faceN[t * 3 + 1] = y;
265
+ faceN[t * 3 + 2] = z;
266
+ const l = Math.sqrt(x * x + y * y + z * z) || 1;
267
+ faceU[t * 3] = x / l;
268
+ faceU[t * 3 + 1] = y / l;
269
+ faceU[t * 3 + 2] = z / l;
270
+ }
271
+ const weld = weldMap(positions);
272
+ const nv = positions.length / 3;
273
+ // faces incident to each welded vertex (CSR layout), needed only to test the crease angle
274
+ let counts = null, inc = null;
275
+ if (crease > 0 && crease < 180) {
276
+ counts = new Uint32Array(nv + 1);
277
+ for (let i = 0; i < indices.length; i++) counts[weld[indices[i]] + 1]++;
278
+ for (let i = 0; i < nv; i++) counts[i + 1] += counts[i];
279
+ inc = new Uint32Array(indices.length);
280
+ const fill = counts.slice(0, nv);
281
+ for (let i = 0; i < indices.length; i++) inc[fill[weld[indices[i]]]++] = (i / 3) | 0;
282
+ }
283
+ // without creases every corner of a welded vertex gets the same normal:
284
+ // sum the faces, then normalize once per welded vertex
285
+ let smoothN = null;
286
+ if (crease >= 180) {
287
+ smoothN = new Float64Array(nv * 3);
288
+ for (let i = 0; i < indices.length; i++) {
289
+ const w = weld[indices[i]] * 3, f = ((i / 3) | 0) * 3;
290
+ smoothN[w] += faceN[f];
291
+ smoothN[w + 1] += faceN[f + 1];
292
+ smoothN[w + 2] += faceN[f + 2];
293
+ }
294
+ for (let w = 0; w < nv * 3; w += 3) {
295
+ const x = smoothN[w], y = smoothN[w + 1], z = smoothN[w + 2];
296
+ const l = Math.sqrt(x * x + y * y + z * z) || 1;
297
+ smoothN[w] = x / l;
298
+ smoothN[w + 1] = y / l;
299
+ smoothN[w + 2] = z / l;
300
+ }
301
+ }
302
+ // output vertices: as many as the input until creases split some
303
+ let cap = Math.max(1, nv);
304
+ let outP = new Float32Array(cap * 3), outN = new Float32Array(cap * 3), outT = new Float32Array(cap * 2);
305
+ const outI = new Uint32Array(indices.length);
306
+ let vc = 0;
307
+ let smoothOk = smoothN !== null;
308
+ for (let k = 0; smoothOk && k < nv * 3; k++) if (smoothN[k] !== smoothN[k]) smoothOk = false;
309
+ if (smoothOk) {
310
+ // one normal per welded vertex, so every input vertex becomes exactly one
311
+ // output vertex, in the order the triangles first use it
312
+ const out = new Int32Array(nv).fill(-1);
313
+ for (let i = 0; i < indices.length; i++) {
314
+ const vi = indices[i];
315
+ let idx = out[vi];
316
+ if (idx < 0) {
317
+ idx = out[vi] = vc++;
318
+ const w = weld[vi] * 3;
319
+ outP[idx * 3] = positions[vi * 3];
320
+ outP[idx * 3 + 1] = positions[vi * 3 + 1];
321
+ outP[idx * 3 + 2] = positions[vi * 3 + 2];
322
+ outN[idx * 3] = smoothN[w];
323
+ outN[idx * 3 + 1] = smoothN[w + 1];
324
+ outN[idx * 3 + 2] = smoothN[w + 2];
325
+ if (uvs) {
326
+ outT[idx * 2] = uvs[vi * 2];
327
+ outT[idx * 2 + 1] = uvs[vi * 2 + 1];
328
+ }
329
+ }
330
+ outI[i] = idx;
331
+ }
332
+ } else {
333
+ // output vertices already emitted for each input vertex, as a linked list
334
+ // keyed by the normal quantized to 1e-4 (corners with equal normals share)
335
+ const head = new Int32Array(nv).fill(-1);
336
+ let next = new Int32Array(cap);
337
+ let qn = new Int32Array(cap * 3);
338
+ for (let i = 0; i < indices.length; i++) {
339
+ const t = (i / 3) | 0;
340
+ const vi = indices[i];
341
+ const w = weld[vi];
342
+ let nx = 0, ny = 0, nz = 0;
343
+ if (crease <= 0) {
344
+ nx = faceU[t * 3];
345
+ ny = faceU[t * 3 + 1];
346
+ nz = faceU[t * 3 + 2];
347
+ } else if (smoothN) {
348
+ nx = smoothN[w * 3];
349
+ ny = smoothN[w * 3 + 1];
350
+ nz = smoothN[w * 3 + 2];
351
+ } else {
352
+ // faces within the crease angle of this corner's face
353
+ const fx = faceU[t * 3], fy = faceU[t * 3 + 1], fz = faceU[t * 3 + 2];
354
+ for (let k = counts[w]; k < counts[w + 1]; k++) {
355
+ const f = inc[k];
356
+ if (f === t || faceU[f * 3] * fx + faceU[f * 3 + 1] * fy + faceU[f * 3 + 2] * fz >= cosCrease) {
357
+ nx += faceN[f * 3];
358
+ ny += faceN[f * 3 + 1];
359
+ nz += faceN[f * 3 + 2];
360
+ }
361
+ }
362
+ const l = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
363
+ nx /= l;
364
+ ny /= l;
365
+ nz /= l;
366
+ }
367
+ const qx = Math.round(nx * 1e4), qy = Math.round(ny * 1e4), qz = Math.round(nz * 1e4);
368
+ let idx = head[vi];
369
+ while (idx >= 0 && !(qn[idx * 3] === qx && qn[idx * 3 + 1] === qy && qn[idx * 3 + 2] === qz)) idx = next[idx];
370
+ if (idx < 0) {
371
+ idx = vc++;
372
+ if (idx >= cap) {
373
+ cap *= 2;
374
+ const grow = (a, size) => {
375
+ const g = new a.constructor(cap * size);
376
+ g.set(a);
377
+ return g;
378
+ };
379
+ outP = grow(outP, 3);
380
+ outN = grow(outN, 3);
381
+ outT = grow(outT, 2);
382
+ next = grow(next, 1);
383
+ qn = grow(qn, 3);
384
+ }
385
+ outP[idx * 3] = positions[vi * 3];
386
+ outP[idx * 3 + 1] = positions[vi * 3 + 1];
387
+ outP[idx * 3 + 2] = positions[vi * 3 + 2];
388
+ outN[idx * 3] = nx;
389
+ outN[idx * 3 + 1] = ny;
390
+ outN[idx * 3 + 2] = nz;
391
+ if (uvs) {
392
+ outT[idx * 2] = uvs[vi * 2];
393
+ outT[idx * 2 + 1] = uvs[vi * 2 + 1];
394
+ }
395
+ next[idx] = head[vi];
396
+ head[vi] = idx;
397
+ qn[idx * 3] = qx;
398
+ qn[idx * 3 + 1] = qy;
399
+ qn[idx * 3 + 2] = qz;
400
+ }
401
+ outI[i] = idx;
402
+ }
403
+ }
404
+ const exact = (a, n) => (a.length === n ? a : a.slice(0, n));
405
+ return new Mesh({ positions: exact(outP, vc * 3), normals: exact(outN, vc * 3), uvs: exact(outT, vc * 2), indices: outI, kind: 'triangles' });
406
+ }
407
+
408
+ /** Merge meshes transformed by matrices (used by exporters). */
409
+ export function mergeTransformed(items) {
410
+ let vc = 0;
411
+ let ic = 0;
412
+ for (const { mesh } of items) {
413
+ vc += mesh.vertexCount;
414
+ ic += mesh.indices.length;
415
+ }
416
+ const positions = new Float32Array(vc * 3);
417
+ const normals = new Float32Array(vc * 3);
418
+ const uvs = new Float32Array(vc * 2);
419
+ const indices = new Uint32Array(ic);
420
+ let vo = 0;
421
+ let io = 0;
422
+ for (const { mesh, matrix: m } of items) {
423
+ const p = mesh.positions, n = mesh.normals;
424
+ // normal matrix = cofactor of the upper 3x3 (with determinant sign)
425
+ const c0 = [m[5] * m[10] - m[6] * m[9], m[6] * m[8] - m[4] * m[10], m[4] * m[9] - m[5] * m[8]];
426
+ const c1 = [m[9] * m[2] - m[10] * m[1], m[10] * m[0] - m[8] * m[2], m[8] * m[1] - m[9] * m[0]];
427
+ const c2 = [m[1] * m[6] - m[2] * m[5], m[2] * m[4] - m[0] * m[6], m[0] * m[5] - m[1] * m[4]];
428
+ const det = m[0] * c0[0] + m[1] * c0[1] + m[2] * c0[2];
429
+ const sgn = det < 0 ? -1 : 1;
430
+ for (let i = 0; i < mesh.vertexCount; i++) {
431
+ const x = p[i * 3], y = p[i * 3 + 1], z = p[i * 3 + 2];
432
+ positions[(vo + i) * 3] = m[0] * x + m[4] * y + m[8] * z + m[12];
433
+ positions[(vo + i) * 3 + 1] = m[1] * x + m[5] * y + m[9] * z + m[13];
434
+ positions[(vo + i) * 3 + 2] = m[2] * x + m[6] * y + m[10] * z + m[14];
435
+ const nx = n[i * 3], ny = n[i * 3 + 1], nz = n[i * 3 + 2];
436
+ let ox = (c0[0] * nx + c1[0] * ny + c2[0] * nz) * sgn;
437
+ let oy = (c0[1] * nx + c1[1] * ny + c2[1] * nz) * sgn;
438
+ let oz = (c0[2] * nx + c1[2] * ny + c2[2] * nz) * sgn;
439
+ const l = Math.hypot(ox, oy, oz) || 1;
440
+ normals[(vo + i) * 3] = ox / l;
441
+ normals[(vo + i) * 3 + 1] = oy / l;
442
+ normals[(vo + i) * 3 + 2] = oz / l;
443
+ uvs[(vo + i) * 2] = mesh.uvs[i * 2];
444
+ uvs[(vo + i) * 2 + 1] = mesh.uvs[i * 2 + 1];
445
+ }
446
+ const flip = det < 0 && mesh.kind === 'triangles';
447
+ for (let i = 0; i < mesh.indices.length; i += 3) {
448
+ if (mesh.kind === 'lines') break;
449
+ indices[io + i] = mesh.indices[i] + vo;
450
+ indices[io + i + 1] = mesh.indices[flip ? i + 2 : i + 1] + vo;
451
+ indices[io + i + 2] = mesh.indices[flip ? i + 1 : i + 2] + vo;
452
+ }
453
+ if (mesh.kind === 'lines') for (let i = 0; i < mesh.indices.length; i++) indices[io + i] = mesh.indices[i] + vo;
454
+ vo += mesh.vertexCount;
455
+ io += mesh.indices.length;
456
+ }
457
+ return new Mesh({ positions, normals, uvs, indices, kind: items.length ? items[0].mesh.kind : 'triangles' });
458
+ }