import test from "node:test"; import assert from "node:assert/strict"; import { buildMesh, sphere } from "./mesh.js"; import { BVH } from "./bvh.js"; import { Sculpt } from "./engine.js"; import { ensureDocument, snapshot, hydrate, splitPiece, combinePieces, saveDocument, loadDocument, } from "./document.js"; test("document v2 roundtrip owns canonical arrays and supports empty documents", () => { const mesh = ensureDocument(sphere(4)); mesh.masks[3] = 0.75; const copy = loadDocument(saveDocument(mesh)); assert.deepEqual(snapshot(copy), snapshot(mesh)); copy.positions[0] = 7; assert.notEqual(copy.positions[0], mesh.positions[0]); const empty = buildMesh([], [], 0); empty.pieces = []; assert.equal(loadDocument(saveDocument(empty)).positions.length, 0); }); test("piece splitting localizes indices and combination restores contiguous ranges", () => { const a = ensureDocument( buildMesh([0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2], 0), ); a.pieces[0].id = "a"; const b = hydrate(snapshot(a)); b.pieces[0].id = "b"; const combined = combinePieces([ { mesh: a, piece: a.pieces[0] }, { mesh: b, piece: b.pieces[0] }, ]); assert.deepEqual( combined.pieces.map((p) => [p.vertexStart, p.indexStart]), [ [0, 0], [3, 3], ], ); assert.deepEqual( splitPiece(combined, combined.pieces[1]).indices, new Uint32Array([0, 1, 2]), ); }); test("combining a level 6 sphere avoids argument limits and preserves its level", () => { const mesh = ensureDocument(sphere(6)); mesh.masks[40000] = 0.625; const combined = combinePieces([{ mesh, piece: mesh.pieces[0] }]); assert.equal(combined.indices.length, 245760); assert.equal(combined.level, 6); assert.deepEqual(snapshot(combined), snapshot(mesh)); }); test("combination preflights aggregate limits and accepts no pieces", () => { const mesh = ensureDocument(sphere(6)); const other = hydrate(snapshot(mesh)); other.pieces[0].id = "other"; assert.throws( () => combinePieces([{ mesh }, { mesh: other }]), /topology limits/, ); const empty = combinePieces([]); assert.equal(empty.positions.length, 0); assert.deepEqual(empty.pieces, []); }); test("hydrate rejects invalid ranges, cross-piece indices, and coordinates", () => { const base = snapshot( ensureDocument(buildMesh([0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2], 0)), ); assert.throws(() => hydrate({ ...base, positions: Float32Array.from([9, ...base.positions.slice(1)]), }), ); assert.throws(() => hydrate({ ...base, pieces: [{ ...base.pieces[0], vertexCount: 2 }] }), ); const left = hydrate(base), right = hydrate(base); right.pieces[0].id = "other"; const two = combinePieces([ { mesh: left, piece: left.pieces[0] }, { mesh: right, piece: right.pieces[0] }, ]); const bad = snapshot(two); bad.indices[3] = 0; assert.throws(() => hydrate(bad), /outside/); }); test("canonical no-op installs preserve redo and every live identity", () => { const sculpt = new Sculpt(sphere(4)); sculpt.setColor([0.2, 0.4, 0.6]); sculpt.restore(); const identities = { mesh: sculpt.mesh, bvh: sculpt.bvh, dirty: sculpt.dirty, undo: sculpt.undoStack, redo: sculpt.redoStack, }; const values = { state: sculpt.state, nextState: sculpt.nextState, revision: sculpt.revision, }; assert.equal(sculpt.install(hydrate(snapshot(sculpt.mesh))), false); for (const [name, value] of Object.entries(identities)) assert.equal( sculpt[ name === "undo" ? "undoStack" : name === "redo" ? "redoStack" : name ], value, ); assert.deepEqual( { state: sculpt.state, nextState: sculpt.nextState, revision: sculpt.revision, }, values, ); assert.equal(sculpt.redoStack.length, 1); }); test("structural install and restore failures are atomic", () => { const sculpt = new Sculpt(sphere(4)); const changed = hydrate(snapshot(sculpt.mesh)); changed.positions[0] += 0.01; const before = () => ({ mesh: sculpt.mesh, bvh: sculpt.bvh, dirty: sculpt.dirty, undo: sculpt.undoStack, redo: sculpt.redoStack, state: sculpt.state, nextState: sculpt.nextState, revision: sculpt.revision, }); const assertSame = (value) => { assert.equal(sculpt.mesh, value.mesh); assert.equal(sculpt.bvh, value.bvh); assert.equal(sculpt.dirty, value.dirty); assert.equal(sculpt.undoStack, value.undo); assert.equal(sculpt.redoStack, value.redo); assert.equal(sculpt.state, value.state); assert.equal(sculpt.nextState, value.nextState); assert.equal(sculpt.revision, value.revision); }; const originalBounds = BVH.prototype.bounds; BVH.prototype.bounds = () => { throw new Error("injected BVH failure"); }; const installState = before(); try { assert.throws(() => sculpt.install(changed), /injected BVH failure/); } finally { BVH.prototype.bounds = originalBounds; } assertSame(installState); assert.equal(sculpt.install(changed), true); sculpt.undoStack.at(-1).before.activeId = "missing"; const restoreState = before(); assert.throws(() => sculpt.restore(), /active piece/); assertSame(restoreState); });