141 lines
3.4 KiB
TypeScript
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));
|
|
});
|