import test from "node:test"; import assert from "node:assert/strict"; import { sphere, buildMesh } from "./mesh.js"; import { snapshot, ensureDocument, splitPiece } from "./document.js"; import { operate } from "./operations.js"; import { Sculpt } from "./engine.js"; import { decimate } from "./topology.js"; test("add, color, mask, subdivision, decimation and delete round-trip mixed history", () => { const engine = new Sculpt(sphere(4)); const original = snapshot(engine.mesh); const add = operate(original, "add", engine.activeId, { preset: "torus", id: "ring", color: [0.7, 0.4, 0.5], }); engine.install(add.document, add.activeId); assert.equal(engine.activeId, "ring"); assert.equal(engine.mesh.pieces.length, 2); assert.deepEqual( engine.mesh.positions.subarray(0, original.positions.length), original.positions, ); engine.setColor([0.3, 0.6, 0.7]); engine.editMask(true); const beforeSubdivision = snapshot(engine.mesh); const divided = operate(beforeSubdivision, "subdivide", "ring"); engine.install(divided.document); assert.equal( engine.mesh.pieces[1].indexCount, beforeSubdivision.pieces[1].indexCount * 4, ); const masked = snapshot(engine.mesh); const unchanged = operate(masked, "decimate", "ring"); assert.equal(engine.install(unchanged.document), false); engine.editMask(); const simplified = operate(snapshot(engine.mesh), "decimate", "ring"); assert.ok(simplified.document.indices.length < engine.mesh.indices.length); engine.install(simplified.document); const removed = operate(snapshot(engine.mesh), "delete", "ring"); engine.install(removed.document, removed.activeId); assert.equal(engine.mesh.pieces.length, 1); const final = snapshot(engine.mesh), finalState = engine.state; while (engine.restore()) {} assert.deepEqual(snapshot(engine.mesh), original); assert.equal(engine.state, 0); while (engine.restore(true)) {} assert.deepEqual(snapshot(engine.mesh), final); assert.equal(engine.state, finalState); const empty = operate(snapshot(engine.mesh), "delete", engine.activeId); engine.install(empty.document, empty.activeId); assert.equal(engine.activeId, null); assert.equal(engine.mesh.positions.length, 0); }); test("piece sculpting and local symmetry cannot change neighboring clay", () => { const first = ensureDocument(sphere(4)); const result = operate(snapshot(first), "add", "clay-1", { preset: "sphere", id: "second", color: [0.5, 0.7, 0.4], }); const e = new Sculpt(); e.install(result.document, "second"); const before = e.mesh.positions.slice(), piece = e.mesh.pieces[1]; assert.equal(piece.symmetryX, Math.fround(2.15)); const seam = []; for ( let v = piece.vertexStart; v < piece.vertexStart + piece.vertexCount; v++ ) if (before[v * 3] === piece.symmetryX) seam.push(v); assert.ok(seam.length > 0); const hit = e.bvh.raycast([piece.symmetryX + 0.2, 0, 5], [0, 0, -1]); e.begin({ brush: "draw", radius: 0.5, symmetry: true }); e.dab(hit); e.end(); for (const v of seam) assert.equal(e.mesh.positions[v * 3], piece.symmetryX); assert.deepEqual( e.mesh.positions.subarray(0, first.positions.length), before.subarray(0, first.positions.length), ); assert.notDeepEqual( splitPiece(e.mesh, piece).positions, before.subarray(piece.vertexStart * 3), ); }); test("decimator keeps tetrahedron components and open boundary fan intact", () => { const tetra = [1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1]; const faces = [0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]; const mesh = buildMesh( [...tetra, ...tetra.map((x, i) => x + (i % 3 === 0 ? 4 : 0))], [...faces, ...faces.map((i) => i + 4)], ); const result = decimate(mesh); assert.deepEqual(result.indices, mesh.indices); assert.deepEqual(result.positions, mesh.positions); const p = [0, 0, 0], f = []; for (let i = 0; i < 6; i++) { p.push(Math.cos((i * Math.PI) / 3), Math.sin((i * Math.PI) / 3), 0); f.push(0, i + 1, ((i + 1) % 6) + 1); } const fan = buildMesh(p, f), reduced = decimate(fan); assert.deepEqual(reduced.positions, fan.positions); assert.deepEqual(reduced.indices, fan.indices); }); test("every geometry brush respects protection; mask erase and cancellation are exact", () => { const e = new Sculpt(sphere(4)); const hit = e.bvh.raycast([0.2, 0.1, 5], [0, 0, -1]); const positions = e.mesh.positions.slice(), normals = e.mesh.normals.slice(); e.editMask(true); for (const brush of ["draw", "inflate", "smooth", "flatten", "grab"]) { e.begin({ brush, strength: 1, radius: 0.5 }); e.dab(hit, 1, [0.3, -0.2, 0.1]); e.end(); assert.deepEqual(e.mesh.positions, positions, brush); } const protectedMasks = e.mesh.masks.slice(); const geometryChange = e.changed; e.changed = () => { throw new Error("Mask edit performed geometry maintenance"); }; e.begin({ brush: "mask", invert: true, radius: 0.5 }); e.dab(hit); e.end(true); assert.deepEqual(e.mesh.masks, protectedMasks); e.begin({ brush: "mask", invert: true, radius: 0.5 }); e.dab(hit); e.end(); assert.ok(e.mesh.masks.some((x) => x < 1)); const erased = e.mesh.masks.slice(); e.restore(); assert.deepEqual(e.mesh.masks, protectedMasks); e.restore(true); assert.deepEqual(e.mesh.masks, erased); e.editMask(); assert.ok(e.mesh.masks.every((x) => x === 0)); e.restore(); assert.deepEqual(e.mesh.masks, erased); assert.deepEqual(e.mesh.normals, normals); assert.deepEqual(e.mesh.positions, positions); e.changed = geometryChange; });