import { unit } from "./math.js"; export function buildMesh(positions, indices, level = 5, masks) { positions = positions instanceof Float32Array ? positions : new Float32Array(positions.flat ? positions.flat(Infinity) : positions); indices = indices instanceof Uint32Array ? indices : new Uint32Array(indices.flat ? indices.flat(Infinity) : indices); if (positions.length % 3 || indices.length % 3) throw new Error( "Mesh positions and indices must contain complete triples.", ); const vertexCount = positions.length / 3; const incident = Array.from({ length: vertexCount }, () => []); const neighbors = Array.from({ length: vertexCount }, () => new Set()); for (let f = 0; f < indices.length / 3; f++) { const face = indices.subarray(f * 3, f * 3 + 3); for (const v of face) { if (v >= vertexCount) throw new Error("Mesh index is out of range."); incident[v].push(f); for (const other of face) if (other !== v) neighbors[v].add(other); } } if (masks && masks.length !== vertexCount) throw new Error("Mask count must match vertex count."); const mesh = { level, positions, indices, masks: masks instanceof Float32Array ? masks : new Float32Array(masks || vertexCount), normals: new Float32Array(positions.length), faceNormals: new Float32Array(indices.length), incident: incident.map((a) => Uint32Array.from(a)), neighbors: neighbors.map((a) => Uint32Array.from(a)), faceMarks: new Uint32Array(indices.length / 3), vertexMarks: new Uint32Array(vertexCount), generation: 0, }; updateNormals(mesh); return mesh; } export function sphere(level = 5) { if (!Number.isInteger(level) || level < 4 || level > 6) throw new Error("Choose a supported resolution."); const t = (1 + Math.sqrt(5)) / 2; const vertices = [ [-1, t, 0], [1, t, 0], [-1, -t, 0], [1, -t, 0], [0, -1, t], [0, 1, t], [0, -1, -t], [0, 1, -t], [t, 0, -1], [t, 0, 1], [-t, 0, -1], [-t, 0, 1], ].map(unit); let faces = [ [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1], ]; for (let l = 0; l < level; l++) { const cache = new Map(); const midpoint = (a, b) => { const key = Math.min(a, b) * 100000 + Math.max(a, b); if (!cache.has(key)) { cache.set(key, vertices.length); vertices.push(unit(vertices[a].map((v, i) => v + vertices[b][i]))); } return cache.get(key); }; faces = faces.flatMap(([a, b, c]) => { const ab = midpoint(a, b), bc = midpoint(b, c), ca = midpoint(c, a); return [ [a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca], ]; }); } const positions = new Float32Array(vertices.flat()); const indices = new Uint32Array(faces.flat()); return buildMesh(positions, indices, level); } // Include the unchanged incident faces when recomputing a boundary vertex normal. export function updateNormals(mesh, moved) { const { positions: p, indices: idx, normals: n, faceNormals: fn } = mesh; const faces = [], vertices = []; const gen = ++mesh.generation; if (moved) { for (const v of moved) for (const f of mesh.incident[v]) { if (mesh.faceMarks[f] !== gen) { mesh.faceMarks[f] = gen; faces.push(f); } } } else for (let f = 0; f < idx.length / 3; f++) faces.push(f); for (const f of faces) { const a = idx[f * 3], b = idx[f * 3 + 1], c = idx[f * 3 + 2]; const ax = p[b * 3] - p[a * 3], ay = p[b * 3 + 1] - p[a * 3 + 1], az = p[b * 3 + 2] - p[a * 3 + 2]; const bx = p[c * 3] - p[a * 3], by = p[c * 3 + 1] - p[a * 3 + 1], bz = p[c * 3 + 2] - p[a * 3 + 2]; fn[f * 3] = ay * bz - az * by; fn[f * 3 + 1] = az * bx - ax * bz; fn[f * 3 + 2] = ax * by - ay * bx; for (let j = 0; j < 3; j++) { const v = idx[f * 3 + j]; if (mesh.vertexMarks[v] !== gen) { mesh.vertexMarks[v] = gen; vertices.push(v); } } } for (const v of vertices) { let x = 0, y = 0, z = 0; for (const f of mesh.incident[v]) { x += fn[f * 3]; y += fn[f * 3 + 1]; z += fn[f * 3 + 2]; } const length = Math.hypot(x, y, z) || 1; n[v * 3] = x / length; n[v * 3 + 1] = y / length; n[v * 3 + 2] = z / length; } return { faces, vertices }; }