@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.
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/meshGeometry.d.ts +15 -0
- package/dist/meshGeometry.d.ts.map +1 -0
- package/dist/meshGeometry.js +128 -0
- package/dist/meshGeometry.js.map +1 -0
- package/dist/meshGeometryAttributes.d.ts +28 -0
- package/dist/meshGeometryAttributes.d.ts.map +1 -0
- package/dist/meshGeometryAttributes.js +144 -0
- package/dist/meshGeometryAttributes.js.map +1 -0
- package/dist/meshGeometryBuilders.d.ts +19 -0
- package/dist/meshGeometryBuilders.d.ts.map +1 -0
- package/dist/meshGeometryBuilders.js +772 -0
- package/dist/meshGeometryBuilders.js.map +1 -0
- package/dist/meshGeometryCompute.d.ts +7 -0
- package/dist/meshGeometryCompute.d.ts.map +1 -0
- package/dist/meshGeometryCompute.js +299 -0
- package/dist/meshGeometryCompute.js.map +1 -0
- package/dist/meshGeometryIndex.d.ts +4 -0
- package/dist/meshGeometryIndex.d.ts.map +1 -0
- package/dist/meshGeometryIndex.js +70 -0
- package/dist/meshGeometryIndex.js.map +1 -0
- package/dist/meshGeometryLayout.d.ts +4 -0
- package/dist/meshGeometryLayout.d.ts.map +1 -0
- package/dist/meshGeometryLayout.js +83 -0
- package/dist/meshGeometryLayout.js.map +1 -0
- package/dist/meshGeometryOperations.d.ts +12 -0
- package/dist/meshGeometryOperations.d.ts.map +1 -0
- package/dist/meshGeometryOperations.js +209 -0
- package/dist/meshGeometryOperations.js.map +1 -0
- package/dist/meshGeometrySubset.d.ts +5 -0
- package/dist/meshGeometrySubset.d.ts.map +1 -0
- package/dist/meshGeometrySubset.js +40 -0
- package/dist/meshGeometrySubset.js.map +1 -0
- package/dist/meshGeometryTransforms.d.ts +7 -0
- package/dist/meshGeometryTransforms.d.ts.map +1 -0
- package/dist/meshGeometryTransforms.js +235 -0
- package/dist/meshGeometryTransforms.js.map +1 -0
- package/dist/meshGeometryUvs.d.ts +5 -0
- package/dist/meshGeometryUvs.d.ts.map +1 -0
- package/dist/meshGeometryUvs.js +58 -0
- package/dist/meshGeometryUvs.js.map +1 -0
- package/package.json +39 -0
- package/src/meshGeometry.test.ts +155 -0
- package/src/meshGeometryAttributes.test.ts +195 -0
- package/src/meshGeometryBuilders.test.ts +220 -0
- package/src/meshGeometryCompute.test.ts +218 -0
- package/src/meshGeometryIndex.test.ts +84 -0
- package/src/meshGeometryLayout.test.ts +132 -0
- package/src/meshGeometryOperations.test.ts +154 -0
- package/src/meshGeometrySubset.test.ts +75 -0
- package/src/meshGeometryTransforms.test.ts +169 -0
- 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
|