Files

141 lines
3.4 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { extrude, transformed } from "../engine/geometry.ts";
import { validateGeometry } from "../engine/schema.ts";
function volume(g: any) {
let sum = 0;
const p = g.positions,
ix = g.indices;
for (let i = 0; i < ix.length; i += 3) {
const a = ix[i] * 3,
b = ix[i + 1] * 3,
c = ix[i + 2] * 3;
sum +=
(p[a] * (p[b + 1] * p[c + 2] - p[b + 2] * p[c + 1]) +
p[a + 1] * (p[b + 2] * p[c] - p[b] * p[c + 2]) +
p[a + 2] * (p[b] * p[c + 1] - p[b + 1] * p[c])) /
6;
}
return sum;
}
const near = (actual: number, expected: number) =>
assert.ok(
Math.abs(actual - expected) < 1e-5,
"expected " + actual + " to equal " + expected,
);
test("a rectangular extrusion produces a closed solid with correct volume", () => {
const g = extrude(
[
[0, 0],
[3, 0],
[3, 2],
[0, 2],
],
2.5,
);
validateGeometry(g);
near(Math.abs(volume(g)), 15);
});
test("concave profile preserves missing corner for both winding orders", () => {
const profile: [[number, number], ...[number, number][]] = [
[0, 0],
[3, 0],
[3, 1],
[1, 1],
[1, 3],
[0, 3],
];
for (const points of [profile, [...profile].reverse()]) {
const g = extrude(points, 2);
validateGeometry(g);
near(Math.abs(volume(g)), 10);
const p = g.positions;
for (let i = 0; i < g.indices.length; i += 3) {
const ids = g.indices.slice(i, i + 3).map((v: number) => v * 3);
const ys = ids.map((v: number) => p[v + 1]);
if (Math.max(...ys) - Math.min(...ys) > 1e-6) continue;
const x = ids.reduce((s: number, j: number) => s + p[j], 0) / 3,
z = ids.reduce((s: number, j: number) => s + p[j + 2], 0) / 3;
assert.ok(
!(x > 1 + 1e-6 && z > 1 + 1e-6),
"a cap triangle fills the concave cutout",
);
}
}
});
test("mirroring geometry preserves outward triangle orientation", () => {
const g = extrude(
[
[0, 0],
[2, 0],
[2, 1],
[0, 1],
],
1,
);
const mirrored = transformed(g, [7, 2, -3], [-2, 3, 4]);
validateGeometry(mirrored);
near(volume(mirrored), volume(g) * 24);
});
test("mesh validator rejects corrupt or non-finite input before runtime upload", () => {
const good = { positions: [0, 0, 0, 1, 0, 0, 0, 1, 0], indices: [0, 1, 2] };
validateGeometry(good);
for (const bad of [
{ ...good, positions: [0, 0, 0, 1, 0, 0, 0, 1, Infinity] },
{ ...good, positions: [0, 0, 0, 1] },
{ ...good, indices: [0, 1, 3] },
{ ...good, indices: [0, -1, 2] },
{ ...good, indices: [0, 0.5, 2] },
{ ...good, indices: [0, 1] },
{ ...good, normals: [0, 1, 0] },
{ ...good, uvs: [0, 0] },
])
assert.throws(() => validateGeometry(bad as any));
});
test("extrusion rejects degenerate profiles and nonpositive height", () => {
for (const [profile, depth] of [
[
[
[0, 0],
[1, 0],
],
1,
],
[
[
[0, 0],
[1, 0],
[2, 0],
],
1,
],
[
[
[0, 0],
[1, 0],
[0, 1],
],
0,
],
[
[
[0, 0],
[1, 0],
[0, 1],
],
-1,
],
[
[
[0, 0],
[1, 0],
[0, NaN],
],
1,
],
] as any[])
assert.throws(() => extrude(profile, depth));
});