// Minimal parametric mesh generators (box/sphere/cylinder), dependency-free. // Returns { positions: Float32Array, normals: Float32Array, indices: Uint32Array }. // skipNormals: normais (como "0,-1,0") das faces a NÃO gerar — usado pelo // bloco de pele em corte (openFrontBox abaixo) pra deixar a face da frente // aberta, como uma caixa de diorama, revelando o conteúdo interno em vez de // escondê-lo atrás de uma parede opaca. export function box(w, h, d, skipNormals = []) { const x = w / 2, y = h / 2, z = d / 2; const positions = []; const normals = []; const indices = []; const faces = [ // [normal, 4 corners CCW] [[0, 0, 1], [[-x, -y, z], [x, -y, z], [x, y, z], [-x, y, z]]], [[0, 0, -1], [[x, -y, -z], [-x, -y, -z], [-x, y, -z], [x, y, -z]]], [[0, 1, 0], [[-x, y, z], [x, y, z], [x, y, -z], [-x, y, -z]]], [[0, -1, 0], [[-x, -y, -z], [x, -y, -z], [x, -y, z], [-x, -y, z]]], [[1, 0, 0], [[x, -y, z], [x, -y, -z], [x, y, -z], [x, y, z]]], [[-1, 0, 0], [[-x, -y, -z], [-x, -y, z], [-x, y, z], [-x, y, -z]]], ]; const skip = new Set(skipNormals.map((n) => n.join(","))); for (const [n, corners] of faces) { if (skip.has(n.join(","))) continue; const base = positions.length / 3; for (const c of corners) { positions.push(...c); normals.push(...n); } indices.push(base, base + 1, base + 2, base, base + 2, base + 3); } return { positions: new Float32Array(positions), normals: new Float32Array(normals), indices: new Uint32Array(indices), }; } // Caixa "de diorama": todas as faces normais, exceto a de baixo em Y // (nosso eixo "frente/trás" no bloco de pele) — deixa a frente aberta pra // dar pra ver o conteúdo (folículos, glândulas, vasos) por dentro, em vez // de uma parede opaca escondendo tudo. export function openFrontBox(w, h, d) { return box(w, h, d, [[0, -1, 0]]); } export function sphere(radius, widthSegments = 16, heightSegments = 12) { const positions = []; const normals = []; const indices = []; for (let y = 0; y <= heightSegments; y++) { const v = y / heightSegments; const theta = v * Math.PI; for (let x = 0; x <= widthSegments; x++) { const u = x / widthSegments; const phi = u * Math.PI * 2; const nx = -Math.cos(phi) * Math.sin(theta); const ny = Math.cos(theta); const nz = Math.sin(phi) * Math.sin(theta); positions.push(radius * nx, radius * ny, radius * nz); normals.push(nx, ny, nz); } } const rowSize = widthSegments + 1; for (let y = 0; y < heightSegments; y++) { for (let x = 0; x < widthSegments; x++) { const a = y * rowSize + x; const b = a + rowSize; indices.push(a, b, a + 1, b, b + 1, a + 1); } } return { positions: new Float32Array(positions), normals: new Float32Array(normals), indices: new Uint32Array(indices), }; } export function cylinder(radiusTop, radiusBottom, height, radialSegments = 12) { const positions = []; const normals = []; const indices = []; const halfH = height / 2; // Side wall. for (let y = 0; y <= 1; y++) { const r = y === 0 ? radiusTop : radiusBottom; const py = y === 0 ? halfH : -halfH; for (let x = 0; x <= radialSegments; x++) { const u = (x / radialSegments) * Math.PI * 2; const nx = Math.sin(u); const nz = Math.cos(u); positions.push(r * nx, py, r * nz); const slope = (radiusBottom - radiusTop) / height; const len = Math.sqrt(1 + slope * slope); normals.push(nx / len, -slope / len, nz / len); } } const rowSize = radialSegments + 1; for (let x = 0; x < radialSegments; x++) { const a = x, b = a + rowSize; indices.push(a, b, a + 1, b, b + 1, a + 1); } // Caps. function cap(y, r, py, flip) { const base = positions.length / 3; const ny = flip ? 1 : -1; positions.push(0, py, 0); normals.push(0, ny, 0); for (let x = 0; x <= radialSegments; x++) { const u = (x / radialSegments) * Math.PI * 2; positions.push(r * Math.sin(u), py, r * Math.cos(u)); normals.push(0, ny, 0); } for (let x = 0; x < radialSegments; x++) { if (flip) indices.push(base, base + x + 2, base + x + 1); else indices.push(base, base + x + 1, base + x + 2); } } if (radiusTop > 0) cap(0, radiusTop, halfH, true); if (radiusBottom > 0) cap(1, radiusBottom, -halfH, false); return { positions: new Float32Array(positions), normals: new Float32Array(normals), indices: new Uint32Array(indices), }; } export function buildGeometry(shape) { switch (shape.type) { case "box": return box(shape.w, shape.h, shape.d); case "openFrontBox": return openFrontBox(shape.w, shape.h, shape.d); case "sphere": return sphere(shape.r); case "cylinder": return cylinder(shape.rt, shape.rb, shape.h); default: throw new Error(`Unknown shape type: ${shape.type}`); } }