@flighthq/mesh 0.1.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 (55) hide show
  1. package/dist/index.d.ts +11 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +11 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/meshGeometry.d.ts +15 -0
  6. package/dist/meshGeometry.d.ts.map +1 -0
  7. package/dist/meshGeometry.js +128 -0
  8. package/dist/meshGeometry.js.map +1 -0
  9. package/dist/meshGeometryAttributes.d.ts +28 -0
  10. package/dist/meshGeometryAttributes.d.ts.map +1 -0
  11. package/dist/meshGeometryAttributes.js +144 -0
  12. package/dist/meshGeometryAttributes.js.map +1 -0
  13. package/dist/meshGeometryBuilders.d.ts +19 -0
  14. package/dist/meshGeometryBuilders.d.ts.map +1 -0
  15. package/dist/meshGeometryBuilders.js +772 -0
  16. package/dist/meshGeometryBuilders.js.map +1 -0
  17. package/dist/meshGeometryCompute.d.ts +7 -0
  18. package/dist/meshGeometryCompute.d.ts.map +1 -0
  19. package/dist/meshGeometryCompute.js +299 -0
  20. package/dist/meshGeometryCompute.js.map +1 -0
  21. package/dist/meshGeometryIndex.d.ts +4 -0
  22. package/dist/meshGeometryIndex.d.ts.map +1 -0
  23. package/dist/meshGeometryIndex.js +70 -0
  24. package/dist/meshGeometryIndex.js.map +1 -0
  25. package/dist/meshGeometryLayout.d.ts +4 -0
  26. package/dist/meshGeometryLayout.d.ts.map +1 -0
  27. package/dist/meshGeometryLayout.js +83 -0
  28. package/dist/meshGeometryLayout.js.map +1 -0
  29. package/dist/meshGeometryOperations.d.ts +12 -0
  30. package/dist/meshGeometryOperations.d.ts.map +1 -0
  31. package/dist/meshGeometryOperations.js +209 -0
  32. package/dist/meshGeometryOperations.js.map +1 -0
  33. package/dist/meshGeometrySubset.d.ts +5 -0
  34. package/dist/meshGeometrySubset.d.ts.map +1 -0
  35. package/dist/meshGeometrySubset.js +40 -0
  36. package/dist/meshGeometrySubset.js.map +1 -0
  37. package/dist/meshGeometryTransforms.d.ts +7 -0
  38. package/dist/meshGeometryTransforms.d.ts.map +1 -0
  39. package/dist/meshGeometryTransforms.js +235 -0
  40. package/dist/meshGeometryTransforms.js.map +1 -0
  41. package/dist/meshGeometryUvs.d.ts +5 -0
  42. package/dist/meshGeometryUvs.d.ts.map +1 -0
  43. package/dist/meshGeometryUvs.js +58 -0
  44. package/dist/meshGeometryUvs.js.map +1 -0
  45. package/package.json +39 -0
  46. package/src/meshGeometry.test.ts +155 -0
  47. package/src/meshGeometryAttributes.test.ts +195 -0
  48. package/src/meshGeometryBuilders.test.ts +220 -0
  49. package/src/meshGeometryCompute.test.ts +218 -0
  50. package/src/meshGeometryIndex.test.ts +84 -0
  51. package/src/meshGeometryLayout.test.ts +132 -0
  52. package/src/meshGeometryOperations.test.ts +154 -0
  53. package/src/meshGeometrySubset.test.ts +75 -0
  54. package/src/meshGeometryTransforms.test.ts +169 -0
  55. package/src/meshGeometryUvs.test.ts +128 -0
@@ -0,0 +1,772 @@
1
+ import { createAabb } from '@flighthq/geometry';
2
+ import { createMeshGeometry } from './meshGeometry';
3
+ import { computeMeshGeometryBounds, computeMeshGeometryTangents } from './meshGeometryCompute';
4
+ // Primitive builders for the canonical interleaved PBR vertex record:
5
+ // position(3) + normal(3) + tangent(4, w = handedness) + uv0(2) = 12 f32 / 48 bytes.
6
+ // Coordinate and winding convention is pinned across the 3D suite: RIGHT-HANDED coordinates,
7
+ // COUNTER-CLOCKWISE (CCW) front faces, and the tangent `w` component is the bitangent sign per
8
+ // glTF — bitangent = cross(normal, tangent.xyz) * tangent.w. Every builder writes outward-facing
9
+ // normals, generates UVs, computes per-vertex tangents (via computeMeshGeometryTangents, which
10
+ // sets the correct w-sign from the UV gradient), and fills the cached local-space bounds.
11
+ // Builds an axis-aligned box of the given dimensions centered at the origin, one quad per face
12
+ // with per-face outward normals and per-face 0..1 UVs (so each face textures independently).
13
+ export function createBoxMeshGeometry(width = 1, height = 1, depth = 1) {
14
+ const hx = width * 0.5, hy = height * 0.5, hz = depth * 0.5;
15
+ const positions = [];
16
+ const normals = [];
17
+ const uvs = [];
18
+ const indices = [];
19
+ // Each face: origin corner, then two edge vectors (u across, v up) spanning the quad.
20
+ const addFace = (ox, oy, oz, ux, uy, uz, vx, vy, vz, nx, ny, nz) => {
21
+ const start = positions.length / 3;
22
+ for (let iv = 0; iv < 2; iv++) {
23
+ for (let iu = 0; iu < 2; iu++) {
24
+ positions.push(ox + ux * iu + vx * iv, oy + uy * iu + vy * iv, oz + uz * iu + vz * iv);
25
+ normals.push(nx, ny, nz);
26
+ uvs.push(iu, iv);
27
+ }
28
+ }
29
+ indices.push(start, start + 1, start + 3, start, start + 3, start + 2);
30
+ };
31
+ // +X, -X, +Y, -Y, +Z, -Z (each winds CCW when viewed from outside along its normal).
32
+ addFace(hx, -hy, hz, 0, 0, -depth, 0, height, 0, 1, 0, 0);
33
+ addFace(-hx, -hy, -hz, 0, 0, depth, 0, height, 0, -1, 0, 0);
34
+ addFace(-hx, hy, hz, width, 0, 0, 0, 0, -depth, 0, 1, 0);
35
+ addFace(-hx, -hy, -hz, width, 0, 0, 0, 0, depth, 0, -1, 0);
36
+ addFace(-hx, -hy, hz, width, 0, 0, 0, height, 0, 0, 0, 1);
37
+ addFace(hx, -hy, -hz, -width, 0, 0, 0, height, 0, 0, 0, -1);
38
+ return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
39
+ }
40
+ // Builds a capsule (a cylinder capped by two hemispheres) of `radius` and cylindrical `height`
41
+ // (excluding the caps) centered at the origin, with `radialSegments` around the axis and
42
+ // `capSegments` latitudinal divisions per hemisphere. Normals are smooth across the seam between
43
+ // the caps and the side wall. UVs run v from 0 (top) to 1 (bottom) along the full length.
44
+ export function createCapsuleMeshGeometry(radius = 0.5, height = 1, radialSegments = 16, capSegments = 8) {
45
+ const rSeg = Math.max(3, radialSegments);
46
+ const cSeg = Math.max(1, capSegments);
47
+ const halfH = height * 0.5;
48
+ const positions = [];
49
+ const normals = [];
50
+ const uvs = [];
51
+ const indices = [];
52
+ // Helper: emit a ring of vertices at normalized latitude `phi` from the sphere pole.
53
+ // `yOffset` shifts the ring center (0 = top hemisphere center, -height = bottom center).
54
+ const addRing = (phi, yOffset) => {
55
+ const sinPhi = Math.sin(phi), cosPhi = Math.cos(phi);
56
+ for (let i = 0; i <= rSeg; i++) {
57
+ const theta = (i / rSeg) * Math.PI * 2;
58
+ const cosTheta = Math.cos(theta), sinTheta = Math.sin(theta);
59
+ const nx = sinPhi * cosTheta, ny = cosPhi, nz = sinPhi * sinTheta;
60
+ positions.push(radius * nx, radius * ny + yOffset, radius * nz);
61
+ normals.push(nx, ny, nz);
62
+ uvs.push(i / rSeg, 0); // v assigned after all rings are created
63
+ }
64
+ };
65
+ const ringVertexCount = rSeg + 1;
66
+ const vDivisor = 2 * cSeg + 1; // total number of rings minus 1
67
+ // Top hemisphere (phi from 0 to PI/2).
68
+ for (let j = 0; j <= cSeg; j++) {
69
+ addRing((j / cSeg) * (Math.PI * 0.5), halfH);
70
+ }
71
+ // Bottom hemisphere (phi from PI/2 to PI).
72
+ for (let j = 1; j <= cSeg; j++) {
73
+ addRing(Math.PI * 0.5 + (j / cSeg) * (Math.PI * 0.5), -halfH);
74
+ }
75
+ // Fix up the v UV coordinate.
76
+ const ringCount = 2 * cSeg + 1;
77
+ for (let j = 0; j < ringCount; j++) {
78
+ const v = j / vDivisor;
79
+ for (let i = 0; i <= rSeg; i++) {
80
+ uvs[(j * ringVertexCount + i) * 2 + 1] = v;
81
+ }
82
+ }
83
+ // Connect rings into quads.
84
+ const totalRings = 2 * cSeg + 1;
85
+ for (let j = 0; j < totalRings - 1; j++) {
86
+ for (let i = 0; i < rSeg; i++) {
87
+ const a = j * ringVertexCount + i;
88
+ const b = a + 1;
89
+ const c = a + ringVertexCount;
90
+ const d = c + 1;
91
+ indices.push(a, c, b, b, c, d);
92
+ }
93
+ }
94
+ return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
95
+ }
96
+ // Builds a flat filled circle (disc) in the XZ plane centered at the origin. Normal is +Y.
97
+ // `radius` is the outer radius; `segments` is the number of rim vertices. A center vertex fans
98
+ // out to `segments` rim vertices via a triangle fan, so the result is `segments` triangles.
99
+ // UVs map from the disc plane onto 0..1 with (0.5, 0.5) at the center.
100
+ export function createCircleMeshGeometry(radius = 0.5, segments = 32) {
101
+ const segs = Math.max(3, segments);
102
+ const positions = [];
103
+ const normals = [];
104
+ const uvs = [];
105
+ const indices = [];
106
+ addDisc(positions, normals, uvs, indices, segs, radius, 0, 1);
107
+ return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
108
+ }
109
+ // Builds a right circular cone of `radius` and `height` centered at the origin, apex at +Y, base
110
+ // at -Y, with `radialSegments` around the axis. A capped base disc is included when `capped` is
111
+ // true (default). Side normals are smooth around the ring and slanted by the cone half-angle.
112
+ export function createConeMeshGeometry(radius = 0.5, height = 1, radialSegments = 32, capped = true) {
113
+ return createCylinderMeshGeometry(0, radius, height, radialSegments, capped);
114
+ }
115
+ // Builds a right circular cylinder spanning -height/2..+height/2 on the Y axis, with independent
116
+ // top (`topRadius`) and bottom (`bottomRadius`) radii so it also serves as a truncated cone (a
117
+ // zero top radius yields a cone). `radialSegments` rings around the axis; `capped` adds top and
118
+ // bottom discs (skipping a zero-radius cap).
119
+ export function createCylinderMeshGeometry(topRadius = 0.5, bottomRadius = 0.5, height = 1, radialSegments = 32, capped = true) {
120
+ const segments = Math.max(3, radialSegments);
121
+ const halfHeight = height * 0.5;
122
+ const positions = [];
123
+ const normals = [];
124
+ const uvs = [];
125
+ const indices = [];
126
+ // Side wall: slope of the silhouette feeds the radial normal tilt.
127
+ const slope = (bottomRadius - topRadius) / height;
128
+ const sideStart = positions.length / 3;
129
+ for (let y = 0; y <= 1; y++) {
130
+ const radius = y === 0 ? bottomRadius : topRadius;
131
+ const py = y === 0 ? -halfHeight : halfHeight;
132
+ for (let s = 0; s <= segments; s++) {
133
+ const theta = (s / segments) * Math.PI * 2;
134
+ const cos = Math.cos(theta), sin = Math.sin(theta);
135
+ positions.push(radius * cos, py, radius * sin);
136
+ // Outward radial normal tilted by the cone slope; +Y component points toward the narrow end.
137
+ let nx = cos, ny = slope, nz = sin;
138
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
139
+ nx /= len;
140
+ ny /= len;
141
+ nz /= len;
142
+ normals.push(nx, ny, nz);
143
+ uvs.push(s / segments, y);
144
+ }
145
+ }
146
+ for (let s = 0; s < segments; s++) {
147
+ const a = sideStart + s;
148
+ const b = sideStart + s + 1;
149
+ const c = sideStart + (segments + 1) + s;
150
+ const d = sideStart + (segments + 1) + s + 1;
151
+ indices.push(a, c, b, b, c, d);
152
+ }
153
+ if (capped) {
154
+ if (bottomRadius > 0) {
155
+ addDisc(positions, normals, uvs, indices, segments, bottomRadius, -halfHeight, -1);
156
+ }
157
+ if (topRadius > 0) {
158
+ addDisc(positions, normals, uvs, indices, segments, topRadius, halfHeight, 1);
159
+ }
160
+ }
161
+ return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
162
+ }
163
+ // Builds a regular dodecahedron (12 pentagonal faces, triangulated) inscribed in a sphere of `radius`.
164
+ export function createDodecahedronMeshGeometry(radius = 0.5, detail = 0) {
165
+ return createPolyhedronMeshGeometry(DODECAHEDRON_VERTS, DODECAHEDRON_FACES, radius, detail);
166
+ }
167
+ // Builds a regular icosahedron (20 triangular faces) inscribed in a sphere of `radius`.
168
+ // This is the base shape for createIcosphereMeshGeometry at detail=0.
169
+ export function createIcosahedronMeshGeometry(radius = 0.5, detail = 0) {
170
+ return createPolyhedronMeshGeometry(ICOSAHEDRON_VERTS, ICOSAHEDRON_FACES, radius, detail);
171
+ }
172
+ // Builds a geodesic icosphere of `radius` centered at the origin. Unlike the UV sphere,
173
+ // an icosphere distributes triangles evenly over the surface without pole pinching. Starting
174
+ // from a regular icosahedron, each face is subdivided `subdivisions` times (0 = raw icosahedron,
175
+ // 1 = 80 triangles, 2 = 320, etc.). Normals are the unit-sphere position direction; UVs are
176
+ // spherical (longitude/latitude).
177
+ export function createIcosphereMeshGeometry(radius = 0.5, subdivisions = 2) {
178
+ const subs = Math.max(0, Math.min(subdivisions, 6)); // cap at 6 to prevent > 262k triangles
179
+ // Icosahedron base vertices (unit sphere).
180
+ const phi = (1 + Math.sqrt(5)) * 0.5;
181
+ const scale = 1 / Math.sqrt(1 + phi * phi);
182
+ const baseVerts = [
183
+ [-1, phi, 0],
184
+ [1, phi, 0],
185
+ [-1, -phi, 0],
186
+ [1, -phi, 0],
187
+ [0, -1, phi],
188
+ [0, 1, phi],
189
+ [0, -1, -phi],
190
+ [0, 1, -phi],
191
+ [phi, 0, -1],
192
+ [phi, 0, 1],
193
+ [-phi, 0, -1],
194
+ [-phi, 0, 1],
195
+ ].map(([x, y, z]) => [x * scale, y * scale, z * scale]);
196
+ const verts = baseVerts.map((v) => [...v]);
197
+ let faces = [
198
+ [0, 11, 5],
199
+ [0, 5, 1],
200
+ [0, 1, 7],
201
+ [0, 7, 10],
202
+ [0, 10, 11],
203
+ [1, 5, 9],
204
+ [5, 11, 4],
205
+ [11, 10, 2],
206
+ [10, 7, 6],
207
+ [7, 1, 8],
208
+ [3, 9, 4],
209
+ [3, 4, 2],
210
+ [3, 2, 6],
211
+ [3, 6, 8],
212
+ [3, 8, 9],
213
+ [4, 9, 5],
214
+ [2, 4, 11],
215
+ [6, 2, 10],
216
+ [8, 6, 7],
217
+ [9, 8, 1],
218
+ ];
219
+ // Midpoint cache to avoid duplicate vertices.
220
+ const midpointCache = new Map();
221
+ const getMidpoint = (a, b) => {
222
+ const key = a < b ? `${a}_${b}` : `${b}_${a}`;
223
+ const cached = midpointCache.get(key);
224
+ if (cached !== undefined)
225
+ return cached;
226
+ const va = verts[a], vb = verts[b];
227
+ let mx = (va[0] + vb[0]) * 0.5, my = (va[1] + vb[1]) * 0.5, mz = (va[2] + vb[2]) * 0.5;
228
+ const len = Math.sqrt(mx * mx + my * my + mz * mz);
229
+ mx /= len;
230
+ my /= len;
231
+ mz /= len;
232
+ const idx = verts.length;
233
+ verts.push([mx, my, mz]);
234
+ midpointCache.set(key, idx);
235
+ return idx;
236
+ };
237
+ for (let s = 0; s < subs; s++) {
238
+ const newFaces = [];
239
+ for (const [a, b, c] of faces) {
240
+ const ab = getMidpoint(a, b);
241
+ const bc = getMidpoint(b, c);
242
+ const ca = getMidpoint(c, a);
243
+ newFaces.push([a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]);
244
+ }
245
+ faces = newFaces;
246
+ midpointCache.clear();
247
+ }
248
+ // Build positions/normals/uvs/indices from the subdivided icosahedron.
249
+ const positions = [];
250
+ const normals = [];
251
+ const uvs = [];
252
+ const faceIndices = [];
253
+ // Emit unique vertices with spherical UVs. Since the seam is complex for an icosphere,
254
+ // we emit independent vertices per face (non-indexed, then buildCanonicalMeshGeometry).
255
+ for (const [a, b, c] of faces) {
256
+ for (const vi of [a, b, c]) {
257
+ const v = verts[vi];
258
+ const nx = v[0], ny = v[1], nz = v[2];
259
+ positions.push(radius * nx, radius * ny, radius * nz);
260
+ normals.push(nx, ny, nz);
261
+ // Spherical UV: u=longitude (0..1), v=latitude (0..1, 0=top).
262
+ const u = 0.5 + Math.atan2(nz, nx) / (Math.PI * 2);
263
+ const sv = 0.5 - Math.asin(Math.max(-1, Math.min(1, ny))) / Math.PI;
264
+ uvs.push(u, sv);
265
+ }
266
+ }
267
+ for (let i = 0; i < positions.length / 3; i++) {
268
+ faceIndices.push(i);
269
+ }
270
+ return buildCanonicalMeshGeometry(positions, normals, uvs, faceIndices);
271
+ }
272
+ // Builds a regular octahedron (8 triangular faces) inscribed in a sphere of `radius`.
273
+ export function createOctahedronMeshGeometry(radius = 0.5, detail = 0) {
274
+ return createPolyhedronMeshGeometry(OCTAHEDRON_VERTS, OCTAHEDRON_FACES, radius, detail);
275
+ }
276
+ // Builds a flat plane in the XZ plane (Y up, normal +Y) centered at the origin, subdivided into
277
+ // `widthSegments` x `depthSegments` quads with a 0..1 UV grid. Suitable as a ground plane or a
278
+ // textured "panel" mesh.
279
+ export function createPlaneMeshGeometry(width = 1, depth = 1, widthSegments = 1, depthSegments = 1) {
280
+ const wSeg = Math.max(1, widthSegments);
281
+ const dSeg = Math.max(1, depthSegments);
282
+ const hw = width * 0.5, hd = depth * 0.5;
283
+ const positions = [];
284
+ const normals = [];
285
+ const uvs = [];
286
+ const indices = [];
287
+ for (let iz = 0; iz <= dSeg; iz++) {
288
+ const v = iz / dSeg;
289
+ const z = -hd + v * depth;
290
+ for (let ix = 0; ix <= wSeg; ix++) {
291
+ const u = ix / wSeg;
292
+ const x = -hw + u * width;
293
+ positions.push(x, 0, z);
294
+ normals.push(0, 1, 0);
295
+ uvs.push(u, v);
296
+ }
297
+ }
298
+ const rowStride = wSeg + 1;
299
+ for (let iz = 0; iz < dSeg; iz++) {
300
+ for (let ix = 0; ix < wSeg; ix++) {
301
+ const a = iz * rowStride + ix;
302
+ const b = a + 1;
303
+ const c = a + rowStride;
304
+ const d = c + 1;
305
+ // CCW when viewed from +Y (above).
306
+ indices.push(a, c, b, b, c, d);
307
+ }
308
+ }
309
+ return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
310
+ }
311
+ // Builds a polyhedron from an explicit set of unit-sphere vertex positions and face triplets.
312
+ // `radius` scales the result; `detail` subdivides each face for a smoother sphere approximation.
313
+ // This is the shared backend for the Platonic solid builders and is also exported for custom
314
+ // polyhedra. Vertices are projected onto the unit sphere before scaling so the output is a
315
+ // geodesic sphere approximation.
316
+ export function createPolyhedronMeshGeometry(vertexPositions, faceIndices, radius = 0.5, detail = 0) {
317
+ const subs = Math.max(0, Math.min(detail, 5));
318
+ const verts = vertexPositions.map(([x, y, z]) => {
319
+ const len = Math.sqrt(x * x + y * y + z * z);
320
+ return [x / len, y / len, z / len];
321
+ });
322
+ let faces = faceIndices.map((f) => [f[0], f[1], f[2]]);
323
+ if (subs > 0) {
324
+ const midCache = new Map();
325
+ const getMid = (a, b) => {
326
+ const key = a < b ? `${a}_${b}` : `${b}_${a}`;
327
+ const hit = midCache.get(key);
328
+ if (hit !== undefined)
329
+ return hit;
330
+ const va = verts[a], vb = verts[b];
331
+ let mx = (va[0] + vb[0]) * 0.5, my = (va[1] + vb[1]) * 0.5, mz = (va[2] + vb[2]) * 0.5;
332
+ const mlen = Math.sqrt(mx * mx + my * my + mz * mz);
333
+ mx /= mlen;
334
+ my /= mlen;
335
+ mz /= mlen;
336
+ const idx = verts.length;
337
+ verts.push([mx, my, mz]);
338
+ midCache.set(key, idx);
339
+ return idx;
340
+ };
341
+ for (let s = 0; s < subs; s++) {
342
+ const newFaces = [];
343
+ for (const [a, b, c] of faces) {
344
+ const ab = getMid(a, b), bc = getMid(b, c), ca = getMid(c, a);
345
+ newFaces.push([a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]);
346
+ }
347
+ faces = newFaces;
348
+ midCache.clear();
349
+ }
350
+ }
351
+ const positions = [];
352
+ const normals = [];
353
+ const uvs = [];
354
+ const flatIndices = [];
355
+ for (const [a, b, c] of faces) {
356
+ for (const vi of [a, b, c]) {
357
+ const v = verts[vi];
358
+ const nx = v[0], ny = v[1], nz = v[2];
359
+ positions.push(radius * nx, radius * ny, radius * nz);
360
+ normals.push(nx, ny, nz);
361
+ const u = 0.5 + Math.atan2(nz, nx) / (Math.PI * 2);
362
+ const sv = 0.5 - Math.asin(Math.max(-1, Math.min(1, ny))) / Math.PI;
363
+ uvs.push(u, sv);
364
+ }
365
+ }
366
+ for (let i = 0; i < positions.length / 3; i++)
367
+ flatIndices.push(i);
368
+ return buildCanonicalMeshGeometry(positions, normals, uvs, flatIndices);
369
+ }
370
+ // Builds a single unit quad in the XY plane (Z up, normal +Z) centered at the origin, two
371
+ // triangles with a 0..1 UV. The minimal textured surface — the building block for screen-facing
372
+ // panels and sprites in 3D.
373
+ export function createQuadMeshGeometry(width = 1, height = 1) {
374
+ const hw = width * 0.5, hh = height * 0.5;
375
+ const positions = [-hw, -hh, 0, hw, -hh, 0, -hw, hh, 0, hw, hh, 0];
376
+ const normals = [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1];
377
+ const uvs = [0, 0, 1, 0, 0, 1, 1, 1];
378
+ const indices = [0, 1, 2, 2, 1, 3];
379
+ return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
380
+ }
381
+ // Builds a flat ring (annulus) in the XZ plane centered at the origin. `innerRadius` is the
382
+ // hole radius and `outerRadius` is the ring's outer edge. Normal is +Y. `segments` is the
383
+ // number of vertices around the ring. UVs map inner radius to u=0 and outer to u=1.
384
+ export function createRingMeshGeometry(innerRadius = 0.25, outerRadius = 0.5, segments = 32) {
385
+ const segs = Math.max(3, segments);
386
+ const positions = [];
387
+ const normals = [];
388
+ const uvs = [];
389
+ const indices = [];
390
+ for (let i = 0; i <= segs; i++) {
391
+ const theta = (i / segs) * Math.PI * 2;
392
+ const cos = Math.cos(theta), sin = Math.sin(theta);
393
+ // Inner vertex.
394
+ positions.push(innerRadius * cos, 0, innerRadius * sin);
395
+ normals.push(0, 1, 0);
396
+ uvs.push(0, i / segs);
397
+ // Outer vertex.
398
+ positions.push(outerRadius * cos, 0, outerRadius * sin);
399
+ normals.push(0, 1, 0);
400
+ uvs.push(1, i / segs);
401
+ }
402
+ for (let i = 0; i < segs; i++) {
403
+ const inner0 = i * 2;
404
+ const outer0 = i * 2 + 1;
405
+ const inner1 = (i + 1) * 2;
406
+ const outer1 = (i + 1) * 2 + 1;
407
+ // CCW from above (+Y).
408
+ indices.push(inner0, inner1, outer0, outer0, inner1, outer1);
409
+ }
410
+ return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
411
+ }
412
+ // Builds a UV sphere of `radius` centered at the origin, with `widthSegments` longitudinal and
413
+ // `heightSegments` latitudinal divisions. Normals are the unit position direction; UVs map
414
+ // longitude to u and latitude to v.
415
+ export function createSphereMeshGeometry(radius = 0.5, widthSegments = 32, heightSegments = 16) {
416
+ const wSeg = Math.max(3, widthSegments);
417
+ const hSeg = Math.max(2, heightSegments);
418
+ const positions = [];
419
+ const normals = [];
420
+ const uvs = [];
421
+ const indices = [];
422
+ for (let iy = 0; iy <= hSeg; iy++) {
423
+ const v = iy / hSeg;
424
+ const phi = v * Math.PI;
425
+ const sinPhi = Math.sin(phi), cosPhi = Math.cos(phi);
426
+ for (let ix = 0; ix <= wSeg; ix++) {
427
+ const u = ix / wSeg;
428
+ const theta = u * Math.PI * 2;
429
+ const sinTheta = Math.sin(theta), cosTheta = Math.cos(theta);
430
+ const nx = -sinPhi * cosTheta;
431
+ const ny = cosPhi;
432
+ const nz = sinPhi * sinTheta;
433
+ positions.push(radius * nx, radius * ny, radius * nz);
434
+ normals.push(nx, ny, nz);
435
+ uvs.push(u, v);
436
+ }
437
+ }
438
+ const rowStride = wSeg + 1;
439
+ for (let iy = 0; iy < hSeg; iy++) {
440
+ for (let ix = 0; ix < wSeg; ix++) {
441
+ const a = iy * rowStride + ix;
442
+ const b = a + 1;
443
+ const c = a + rowStride;
444
+ const d = c + 1;
445
+ indices.push(a, c, b, b, c, d);
446
+ }
447
+ }
448
+ return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
449
+ }
450
+ // Builds a regular tetrahedron (4 triangular faces) inscribed in a sphere of `radius`.
451
+ export function createTetrahedronMeshGeometry(radius = 0.5, detail = 0) {
452
+ return createPolyhedronMeshGeometry(TETRAHEDRON_VERTS, TETRAHEDRON_FACES, radius, detail);
453
+ }
454
+ // Builds a torus knot: a torus-shaped curve wound `p` times around the Z axis and `q` times
455
+ // around the tube axis. `radius` is the distance from the center to the tube center line;
456
+ // `tube` is the tube radius. Normals and UVs are generated per the standard torus knot geometry.
457
+ export function createTorusKnotMeshGeometry(radius = 0.5, tube = 0.15, tubularSegments = 64, radialSegments = 8, p = 2, q = 3) {
458
+ const tSeg = Math.max(3, tubularSegments);
459
+ const rSeg = Math.max(3, radialSegments);
460
+ const positions = [];
461
+ const normals = [];
462
+ const uvs = [];
463
+ const indices = [];
464
+ // Compute a point on the torus knot curve at parameter `t`.
465
+ const curvePoint = (t) => {
466
+ const angle = t * Math.PI * 2;
467
+ const x = (radius + tube) * Math.cos(p * angle) * Math.cos(q * angle);
468
+ const y = (radius + tube) * Math.cos(p * angle) * Math.sin(q * angle);
469
+ const z = (radius + tube) * Math.sin(p * angle);
470
+ return [x, y, z];
471
+ };
472
+ for (let i = 0; i <= tSeg; i++) {
473
+ const u = i / tSeg;
474
+ const [cx, cy, cz] = curvePoint(u);
475
+ // Tangent via finite difference.
476
+ const [tx1, ty1, tz1] = curvePoint(u + 0.001);
477
+ const [tx0, ty0, tz0] = curvePoint(u - 0.001);
478
+ let tgx = tx1 - tx0, tgy = ty1 - ty0, tgz = tz1 - tz0;
479
+ const tgLen = Math.sqrt(tgx * tgx + tgy * tgy + tgz * tgz) || 1;
480
+ tgx /= tgLen;
481
+ tgy /= tgLen;
482
+ tgz /= tgLen;
483
+ // Normal using Frenet-Serret: reference up vector.
484
+ let bx = tgx + cx, by = tgy + cy, bz = tgz + cz;
485
+ const bLen = Math.sqrt(bx * bx + by * by + bz * bz) || 1;
486
+ bx /= bLen;
487
+ by /= bLen;
488
+ bz /= bLen;
489
+ // Normal = cross(tangent, binormal).
490
+ let nnx = tgy * bz - tgz * by;
491
+ let nny = tgz * bx - tgx * bz;
492
+ let nnz = tgx * by - tgy * bx;
493
+ const nLen = Math.sqrt(nnx * nnx + nny * nny + nnz * nnz) || 1;
494
+ nnx /= nLen;
495
+ nny /= nLen;
496
+ nnz /= nLen;
497
+ // Binormal = cross(tangent, normal).
498
+ const bnx = tgy * nnz - tgz * nny;
499
+ const bny = tgz * nnx - tgx * nnz;
500
+ const bnz = tgx * nny - tgy * nnx;
501
+ for (let j = 0; j <= rSeg; j++) {
502
+ const v = j / rSeg;
503
+ const phi = v * Math.PI * 2;
504
+ const cosPhi = Math.cos(phi), sinPhi = Math.sin(phi);
505
+ const px = cx + tube * (cosPhi * nnx + sinPhi * bnx);
506
+ const py = cy + tube * (cosPhi * nny + sinPhi * bny);
507
+ const pz = cz + tube * (cosPhi * nnz + sinPhi * bnz);
508
+ positions.push(px, py, pz);
509
+ normals.push(cosPhi * nnx + sinPhi * bnx, cosPhi * nny + sinPhi * bny, cosPhi * nnz + sinPhi * bnz);
510
+ uvs.push(u, v);
511
+ }
512
+ }
513
+ const rowStride = rSeg + 1;
514
+ for (let i = 0; i < tSeg; i++) {
515
+ for (let j = 0; j < rSeg; j++) {
516
+ const a = i * rowStride + j;
517
+ const b = a + 1;
518
+ const c = a + rowStride;
519
+ const d = c + 1;
520
+ indices.push(a, c, b, b, c, d);
521
+ }
522
+ }
523
+ return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
524
+ }
525
+ // Builds a torus in the XY plane: a ring of `radius` (center to tube center) with a tube of
526
+ // `tube` radius, divided into `radialSegments` around the ring and `tubularSegments` around the
527
+ // tube. Normals point radially outward from the tube center.
528
+ export function createTorusMeshGeometry(radius = 0.5, tube = 0.2, radialSegments = 24, tubularSegments = 48) {
529
+ const rSeg = Math.max(3, radialSegments);
530
+ const tSeg = Math.max(3, tubularSegments);
531
+ const positions = [];
532
+ const normals = [];
533
+ const uvs = [];
534
+ const indices = [];
535
+ for (let j = 0; j <= rSeg; j++) {
536
+ const v = (j / rSeg) * Math.PI * 2;
537
+ const cosV = Math.cos(v), sinV = Math.sin(v);
538
+ for (let i = 0; i <= tSeg; i++) {
539
+ const u = (i / tSeg) * Math.PI * 2;
540
+ const cosU = Math.cos(u), sinU = Math.sin(u);
541
+ const cx = radius * cosU;
542
+ const cy = radius * sinU;
543
+ const px = (radius + tube * cosV) * cosU;
544
+ const py = (radius + tube * cosV) * sinU;
545
+ const pz = tube * sinV;
546
+ positions.push(px, py, pz);
547
+ let nx = px - cx, ny = py - cy, nz = pz;
548
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
549
+ nx /= len;
550
+ ny /= len;
551
+ nz /= len;
552
+ normals.push(nx, ny, nz);
553
+ uvs.push(i / tSeg, j / rSeg);
554
+ }
555
+ }
556
+ const rowStride = tSeg + 1;
557
+ for (let j = 0; j < rSeg; j++) {
558
+ for (let i = 0; i < tSeg; i++) {
559
+ const a = j * rowStride + i;
560
+ const b = a + 1;
561
+ const c = a + rowStride;
562
+ const d = c + 1;
563
+ indices.push(a, c, b, b, c, d);
564
+ }
565
+ }
566
+ return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
567
+ }
568
+ // Adds a flat circular cap disc at the given Y plane: a center vertex fanned to a ring of
569
+ // `segments` rim vertices. `direction` is +1 for a top cap (normal +Y, CCW from above) and -1
570
+ // for a bottom cap (normal -Y, CCW from below).
571
+ function addDisc(positions, normals, uvs, indices, segments, radius, y, direction) {
572
+ const center = positions.length / 3;
573
+ positions.push(0, y, 0);
574
+ normals.push(0, direction, 0);
575
+ uvs.push(0.5, 0.5);
576
+ const ringStart = positions.length / 3;
577
+ for (let s = 0; s <= segments; s++) {
578
+ const theta = (s / segments) * Math.PI * 2;
579
+ const cos = Math.cos(theta), sin = Math.sin(theta);
580
+ positions.push(radius * cos, y, radius * sin);
581
+ normals.push(0, direction, 0);
582
+ uvs.push(cos * 0.5 + 0.5, sin * 0.5 + 0.5);
583
+ }
584
+ for (let s = 0; s < segments; s++) {
585
+ const a = ringStart + s;
586
+ const b = ringStart + s + 1;
587
+ if (direction > 0) {
588
+ indices.push(center, a, b);
589
+ }
590
+ else {
591
+ indices.push(center, b, a);
592
+ }
593
+ }
594
+ }
595
+ // Interleaves separate position/normal/uv0 arrays into the canonical 12-float record, allocates
596
+ // the MeshGeometry (indices auto-promote past 65k), then fills tangents from the UV gradient and
597
+ // the cached local bounds. The single finalize path every builder funnels through.
598
+ function buildCanonicalMeshGeometry(positions, normals, uvs, indices) {
599
+ const vertexCount = positions.length / 3;
600
+ const vertices = new Float32Array(vertexCount * CANONICAL_FLOATS_PER_VERTEX);
601
+ for (let i = 0; i < vertexCount; i++) {
602
+ const base = i * CANONICAL_FLOATS_PER_VERTEX;
603
+ vertices[base] = positions[i * 3];
604
+ vertices[base + 1] = positions[i * 3 + 1];
605
+ vertices[base + 2] = positions[i * 3 + 2];
606
+ vertices[base + 3] = normals[i * 3];
607
+ vertices[base + 4] = normals[i * 3 + 1];
608
+ vertices[base + 5] = normals[i * 3 + 2];
609
+ // tangent (base+6..9) is filled by computeMeshGeometryTangents below.
610
+ vertices[base + 10] = uvs[i * 2];
611
+ vertices[base + 11] = uvs[i * 2 + 1];
612
+ }
613
+ const indexArray = new Uint32Array(indices.length);
614
+ indexArray.set(indices);
615
+ const geometry = createMeshGeometry({
616
+ indices: indexArray,
617
+ layout: CANONICAL_VERTEX_LAYOUT,
618
+ vertices: vertices,
619
+ });
620
+ computeMeshGeometryTangents(geometry, geometry);
621
+ const bounds = createAabb();
622
+ computeMeshGeometryBounds(bounds, geometry);
623
+ geometry.bounds = bounds;
624
+ return geometry;
625
+ }
626
+ const CANONICAL_FLOATS_PER_VERTEX = 12;
627
+ // The canonical interleaved PBR vertex layout shared by every builder: position + normal +
628
+ // tangent(w = handedness) + uv0, stride 48 bytes.
629
+ const CANONICAL_VERTEX_LAYOUT = {
630
+ attributes: [
631
+ { byteOffset: 0, format: 'float32x3', semantic: 'position' },
632
+ { byteOffset: 12, format: 'float32x3', semantic: 'normal' },
633
+ { byteOffset: 24, format: 'float32x4', semantic: 'tangent' },
634
+ { byteOffset: 40, format: 'float32x2', semantic: 'uv0' },
635
+ ],
636
+ stride: 48,
637
+ };
638
+ // Tetrahedron: 4 vertices, 4 triangular faces.
639
+ const TETRAHEDRON_VERTS = [
640
+ [1, 1, 1],
641
+ [-1, -1, 1],
642
+ [-1, 1, -1],
643
+ [1, -1, -1],
644
+ ];
645
+ const TETRAHEDRON_FACES = [
646
+ [2, 1, 0],
647
+ [0, 3, 2],
648
+ [1, 3, 0],
649
+ [2, 3, 1],
650
+ ];
651
+ // Octahedron: 6 vertices, 8 triangular faces.
652
+ const OCTAHEDRON_VERTS = [
653
+ [1, 0, 0],
654
+ [-1, 0, 0],
655
+ [0, 1, 0],
656
+ [0, -1, 0],
657
+ [0, 0, 1],
658
+ [0, 0, -1],
659
+ ];
660
+ const OCTAHEDRON_FACES = [
661
+ [0, 2, 4],
662
+ [0, 4, 3],
663
+ [0, 3, 5],
664
+ [0, 5, 2],
665
+ [1, 4, 2],
666
+ [1, 3, 4],
667
+ [1, 5, 3],
668
+ [1, 2, 5],
669
+ ];
670
+ // Icosahedron: 12 vertices, 20 triangular faces.
671
+ const _phi = (1 + Math.sqrt(5)) * 0.5;
672
+ const ICOSAHEDRON_VERTS = [
673
+ [-1, _phi, 0],
674
+ [1, _phi, 0],
675
+ [-1, -_phi, 0],
676
+ [1, -_phi, 0],
677
+ [0, -1, _phi],
678
+ [0, 1, _phi],
679
+ [0, -1, -_phi],
680
+ [0, 1, -_phi],
681
+ [_phi, 0, -1],
682
+ [_phi, 0, 1],
683
+ [-_phi, 0, -1],
684
+ [-_phi, 0, 1],
685
+ ];
686
+ const ICOSAHEDRON_FACES = [
687
+ [0, 11, 5],
688
+ [0, 5, 1],
689
+ [0, 1, 7],
690
+ [0, 7, 10],
691
+ [0, 10, 11],
692
+ [1, 5, 9],
693
+ [5, 11, 4],
694
+ [11, 10, 2],
695
+ [10, 7, 6],
696
+ [7, 1, 8],
697
+ [3, 9, 4],
698
+ [3, 4, 2],
699
+ [3, 2, 6],
700
+ [3, 6, 8],
701
+ [3, 8, 9],
702
+ [4, 9, 5],
703
+ [2, 4, 11],
704
+ [6, 2, 10],
705
+ [8, 6, 7],
706
+ [9, 8, 1],
707
+ ];
708
+ // Dodecahedron: 20 vertices, 12 pentagonal faces (each pentagon triangulated into 3 triangles).
709
+ // Vertex positions adapted from three.js DodecahedronGeometry (MIT). Each pentagon is split
710
+ // into 3 triangles by fanning from the first vertex. 12 faces × 3 = 36 triangles.
711
+ const _d = 1 / _phi;
712
+ const DODECAHEDRON_VERTS = [
713
+ [-1, -1, -1],
714
+ [-1, -1, 1],
715
+ [-1, 1, -1],
716
+ [-1, 1, 1],
717
+ [1, -1, -1],
718
+ [1, -1, 1],
719
+ [1, 1, -1],
720
+ [1, 1, 1],
721
+ [0, -_d, -_phi],
722
+ [0, -_d, _phi],
723
+ [0, _d, -_phi],
724
+ [0, _d, _phi],
725
+ [-_d, -_phi, 0],
726
+ [-_d, _phi, 0],
727
+ [_d, -_phi, 0],
728
+ [_d, _phi, 0],
729
+ [-_phi, 0, -_d],
730
+ [-_phi, 0, _d],
731
+ [_phi, 0, -_d],
732
+ [_phi, 0, _d],
733
+ ];
734
+ const DODECAHEDRON_FACES = [
735
+ [3, 11, 7],
736
+ [3, 7, 15],
737
+ [3, 15, 13],
738
+ [7, 19, 11],
739
+ [7, 11, 9],
740
+ [7, 9, 19],
741
+ [15, 7, 19],
742
+ [15, 19, 18],
743
+ [15, 18, 6],
744
+ [13, 15, 6],
745
+ [13, 6, 2],
746
+ [13, 2, 16],
747
+ [3, 13, 16],
748
+ [3, 16, 17],
749
+ [3, 17, 11],
750
+ [11, 17, 1],
751
+ [11, 1, 9],
752
+ [9, 1, 5],
753
+ [5, 1, 17],
754
+ [5, 17, 4],
755
+ [5, 4, 14],
756
+ [9, 5, 14],
757
+ [9, 14, 12],
758
+ [9, 12, 0],
759
+ [1, 0, 12],
760
+ [1, 12, 4],
761
+ [1, 4, 17],
762
+ [6, 18, 8],
763
+ [6, 8, 10],
764
+ [6, 10, 2],
765
+ [18, 19, 8],
766
+ [19, 7, 8],
767
+ [7, 15, 8],
768
+ [2, 10, 16],
769
+ [10, 8, 0],
770
+ [0, 8, 12],
771
+ ];
772
+ //# sourceMappingURL=meshGeometryBuilders.js.map