import assert from "node:assert/strict"; import test from "node:test"; import { buildMesh } from "./mesh.js"; import { MAX_TRIANGLES, MAX_VERTICES, decimate, preset, subdivide, } from "./topology.js"; function inspect(mesh) { const edges = new Map(); let volume = 0; for (let k = 0; k < mesh.indices.length; k += 3) { const face = Array.from(mesh.indices.subarray(k, k + 3)); assert.equal(new Set(face).size, 3); const [a, b, c] = face; const p = mesh.positions; volume += (p[a * 3] * (p[b * 3 + 1] * p[c * 3 + 2] - p[b * 3 + 2] * p[c * 3 + 1]) + p[a * 3 + 1] * (p[b * 3 + 2] * p[c * 3] - p[b * 3] * p[c * 3 + 2]) + p[a * 3 + 2] * (p[b * 3] * p[c * 3 + 1] - p[b * 3 + 1] * p[c * 3])) / 6; for (const [x, y] of [ [a, b], [b, c], [c, a], ]) { const key = x < y ? `${x},${y}` : `${y},${x}`; edges.set(key, (edges.get(key) || 0) + 1); } } assert.ok([...mesh.positions, ...mesh.normals].every(Number.isFinite)); assert.ok([...edges.values()].every((count) => count === 2)); assert.ok( volume > 0, `expected outward winding, signed volume was ${volume}`, ); } test("presets are bounded, outward, closed manifold meshes", () => { for (const name of ["sphere", "cube", "cylinder", "cone", "torus"]) { const mesh = preset(name); assert.ok(mesh.positions.length / 3 <= MAX_VERTICES); assert.ok(mesh.indices.length / 3 <= MAX_TRIANGLES); assert.ok(mesh.indices.length / 3 >= 2000); assert.equal(mesh.masks.length, mesh.positions.length / 3); inspect(mesh); } }); test("subdivision shares midpoints and interpolates masks without changing winding", () => { const tetra = buildMesh( [1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1], [0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3], 0, [0, 1, 0.5, 0], ); const result = subdivide(tetra); assert.equal(result.indices.length, tetra.indices.length * 4); assert.equal(result.positions.length / 3, 10); // 4 vertices + 6 unique edges. const midpoint = Array.from(result.positions).findIndex( (_, k, p) => k % 3 === 0 && p[k] === 0 && p[k + 1] === 0 && p[k + 2] === 1, ) / 3; assert.equal(result.masks[midpoint], 0.5); assert.deepEqual( Array.from(result.positions.slice(0, 12)), Array.from(tetra.positions), ); inspect(result); }); test("decimation keeps a closed mesh and protects masked neighborhoods", () => { const source = preset("sphere", 4); source.masks[0] = 1; const protectedFaces = Array.from(source.incident[0], (f) => Array.from(source.indices.slice(f * 3, f * 3 + 3)).map((v) => Array.from(source.positions.slice(v * 3, v * 3 + 3)).join(","), ), ); const result = decimate(source, 0.75); assert.ok(result.indices.length < source.indices.length); inspect(result); const coordinates = new Set(); for (let v = 0; v < result.positions.length / 3; v++) coordinates.add( Array.from(result.positions.slice(v * 3, v * 3 + 3)).join(","), ); assert.ok(protectedFaces.flat().every((point) => coordinates.has(point))); assert.equal([...result.masks].filter(Boolean).length, 1); }); test("fully masked decimation is a no-op", () => { const source = preset("torus", 2); source.masks.fill(1); const result = decimate(source, 0.25); assert.deepEqual(result.indices, source.indices); assert.deepEqual(result.positions, source.positions); assert.deepEqual(result.masks, source.masks); }); test("subdivision rejects cap overflow before allocating output", () => { const positions = new Float32Array((MAX_VERTICES - 1) * 3); const mesh = buildMesh(positions, new Uint32Array([0, 1, 2])); assert.throws(() => subdivide(mesh), /limits/); });