Files
yawn/server/web/assets/sculpt/document.test.js
T
Ampandheaust d279ec4123 Add clay pieces, masking, and topology editing to sculpt
Add pastel presets, piece selection and colors, mask brushes, subdivision and conservative decimation with bounded worker operations and mixed undo history.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-10-01 07:55:48 +00:00

175 lines
5.1 KiB
JavaScript

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);
});