Files
Thingport/backend/tests/modelImageRender.test.ts

131 lines
3.4 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import sharp from "sharp";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { renderModelImage } from "../src/services/modelImageRender";
let dir: string;
// An ASCII STL box, `w` x `d` x `h`, Z-up as print files are.
function boxStl(w: number, d: number, h: number): string {
const v = (x: number, y: number, z: number) => `vertex ${x * w} ${y * d} ${z * h}`;
const faces: [number, number, number][][] = [
[
[0, 0, 0],
[1, 0, 0],
[1, 1, 0],
],
[
[0, 0, 0],
[1, 1, 0],
[0, 1, 0],
],
[
[0, 0, 1],
[1, 1, 1],
[1, 0, 1],
],
[
[0, 0, 1],
[0, 1, 1],
[1, 1, 1],
],
[
[0, 0, 0],
[1, 0, 1],
[1, 0, 0],
],
[
[0, 0, 0],
[0, 0, 1],
[1, 0, 1],
],
[
[0, 1, 0],
[1, 1, 0],
[1, 1, 1],
],
[
[0, 1, 0],
[1, 1, 1],
[0, 1, 1],
],
[
[0, 0, 0],
[0, 1, 0],
[0, 1, 1],
],
[
[0, 0, 0],
[0, 1, 1],
[0, 0, 1],
],
[
[1, 0, 0],
[1, 1, 1],
[1, 1, 0],
],
[
[1, 0, 0],
[1, 0, 1],
[1, 1, 1],
],
];
const facets = faces.map(
(f) => `facet normal 0 0 0\nouter loop\n${f.map((p) => v(...p)).join("\n")}\nendloop\nendfacet`,
);
return `solid box\n${facets.join("\n")}\nendsolid box\n`;
}
async function render(name: string, stl: string) {
const src = path.join(dir, `${name}.stl`);
const dest = path.join(dir, `${name}.png`);
await fs.writeFile(src, stl);
expect(await renderModelImage(src, dest, ".stl", { size: 64, color: "#00b800" })).toBe("ok");
const { data, info } = await sharp(dest).raw().toBuffer({ resolveWithObject: true });
return { data, info };
}
beforeAll(async () => {
dir = await fs.mkdtemp(path.join(os.tmpdir(), "thingport-model-image-"));
});
afterAll(async () => {
await fs.rm(dir, { recursive: true, force: true });
});
describe("model pictures", () => {
it("draws the model in the given colour on a transparent background", async () => {
const { data, info } = await render("cube", boxStl(20, 20, 20));
expect([info.width, info.height, info.channels]).toEqual([64, 64, 4]);
const corner = data.subarray(0, 4);
expect(corner[3]).toBe(0);
const middle = data.subarray((32 * 64 + 32) * 4, (32 * 64 + 32) * 4 + 4);
expect(middle[3]).toBe(255);
// Shaded, but always the accent's hue: green dominant, no red.
expect(middle[1]).toBeGreaterThan(middle[0]);
expect(middle[1]).toBeGreaterThan(middle[2]);
});
it("looks at a flat model from above as well, so it isn't a thin line", async () => {
const coverage = async (name: string, stl: string) => {
const { data } = await render(name, stl);
let covered = 0;
for (let i = 3; i < data.length; i += 4) if (data[i] > 0) covered++;
return covered / (data.length / 4);
};
// Seen straight from the front, a 1 mm high plate would cover almost nothing.
expect(await coverage("plate", boxStl(60, 60, 1))).toBeGreaterThan(0.2);
});
it("refuses a format it can't read", async () => {
expect(
await renderModelImage(path.join(dir, "x.step"), path.join(dir, "x.png"), ".step", {
size: 64,
color: "#00b800",
}),
).toBe("unsupported");
});
});