Publish Forma Engine 0.3.0 source with documentation and CI
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
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));
|
||||
});
|
||||
Reference in New Issue
Block a user