import fs from "node:fs/promises";
import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import request from "supertest";
import { createApp } from "../src/app";
import { prisma } from "../src/db";
import { writeZip } from "../src/utils/zipWriter";
import { renderModelPreviewGlb } from "../src/services/modelPreviewRender";
import { dropPreviewsAffectedBySimplification, modelPreviewGlbPath } from "../src/services/modelPreviewCache";
import { getSimplifyPreviews, setSimplifyPreviews } from "../src/services/settingsService";
const app = createApp();
const stamp = Date.now();
const auth = (token: string) => ({ Authorization: `Bearer ${token}` });
/** 2(n-1)^2 triangles sharing vertices, like a real 3MF mesh. */
function gridMeshXml(n: number): string {
const vertices: string[] = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++)
vertices.push(``);
}
const triangles: string[] = [];
for (let i = 0; i < n - 1; i++) {
for (let j = 0; j < n - 1; j++) {
const a = i * n + j;
triangles.push(
``,
``,
);
}
}
return `
`;
}
function readGlbJson(file: string): any {
const buf = fsSync.readFileSync(file);
return JSON.parse(buf.subarray(20, 20 + buf.readUInt32LE(12)).toString("utf-8"));
}
function glbTriangles(gltf: any): number {
let total = 0;
for (const mesh of gltf.meshes)
for (const primitive of mesh.primitives) total += gltf.accessors[primitive.indices].count / 3;
return total;
}
describe("renderModelPreviewGlb with simplification", () => {
let fixture: string;
let outDir: string;
beforeAll(async () => {
outDir = await fs.mkdtemp(path.join(os.tmpdir(), "thingport-simplify-"));
const modelPath = path.join(outDir, "3dmodel.model");
await fs.writeFile(modelPath, gridMeshXml(101)); // 20,000 triangles
fixture = path.join(outDir, "grid.3mf");
await writeZip(fixture, [{ arcname: "3D/3dmodel.model", filePath: modelPath }]);
});
it("reduces a model over the budget to about the budget, and notes it in the preview", async () => {
const dest = path.join(outDir, "simplified.glb");
expect(await renderModelPreviewGlb(fixture, dest, { simplifyTo: 5_000 })).toBe("ok");
const gltf = readGlbJson(dest);
const triangles = glbTriangles(gltf);
expect(triangles).toBeLessThanOrEqual(5_000);
expect(triangles).toBeGreaterThan(2_000);
const positions = gltf.accessors[gltf.meshes[0].primitives[0].attributes.POSITION].count;
expect(positions).toBeLessThan((101 * 101) / 2);
const meta = JSON.parse(gltf.nodes.find((n: any) => n.extras?.thingportPreview).extras.thingportPreview);
expect(meta.simplified).toEqual({ from: 20_000, to: triangles });
});
it("leaves a model within the budget exactly as it is", async () => {
const dest = path.join(outDir, "untouched.glb");
await renderModelPreviewGlb(fixture, dest, { simplifyTo: 50_000 });
const gltf = readGlbJson(dest);
expect(glbTriangles(gltf)).toBe(20_000);
const meta = JSON.parse(gltf.nodes.find((n: any) => n.extras?.thingportPreview).extras.thingportPreview);
expect(meta.simplified).toBeUndefined();
});
});
/** Holds only a JSON chunk, which is all the simplification check reads. */
async function writeFakeGlb(file: string, triangles: number, simplified: boolean): Promise {
const json = Buffer.from(
JSON.stringify({
asset: { version: "2.0" },
accessors: [{ count: triangles * 3 }],
meshes: [{ primitives: [{ indices: 0 }] }],
nodes: [
{
extras: {
thingportPreview: JSON.stringify(simplified ? { simplified: { from: triangles * 2, to: triangles } } : {}),
},
},
],
}),
);
const padded = Buffer.concat([json, Buffer.alloc((4 - (json.length % 4)) % 4, 0x20)]);
const header = Buffer.alloc(20);
header.writeUInt32LE(0x46546c67, 0);
header.writeUInt32LE(2, 4);
header.writeUInt32LE(20 + padded.length, 8);
header.writeUInt32LE(padded.length, 12);
header.writeUInt32LE(0x4e4f534a, 16);
await fs.writeFile(file, Buffer.concat([header, padded]));
}
describe("dropPreviewsAffectedBySimplification", () => {
const ids = {
heavy: `test-fixture-simplify-heavy-${stamp}`,
light: `test-fixture-simplify-light-${stamp}`,
simplified: `test-fixture-simplify-done-${stamp}`,
};
beforeAll(async () => {
await fs.mkdir(path.dirname(modelPreviewGlbPath("x")), { recursive: true });
});
afterAll(async () => {
for (const id of Object.values(ids)) await fs.rm(modelPreviewGlbPath(id), { force: true });
});
async function writeAll() {
await writeFakeGlb(modelPreviewGlbPath(ids.heavy), 3_000_000, false);
await writeFakeGlb(modelPreviewGlbPath(ids.light), 200_000, false);
await writeFakeGlb(modelPreviewGlbPath(ids.simplified), 1_000_000, true);
}
it("turned on: removes only unsimplified previews over the budget", async () => {
await writeAll();
await dropPreviewsAffectedBySimplification(true);
expect(fsSync.existsSync(modelPreviewGlbPath(ids.heavy))).toBe(false);
expect(fsSync.existsSync(modelPreviewGlbPath(ids.light))).toBe(true);
expect(fsSync.existsSync(modelPreviewGlbPath(ids.simplified))).toBe(true);
});
it("turned off: removes only the simplified previews", async () => {
await writeAll();
await dropPreviewsAffectedBySimplification(false);
expect(fsSync.existsSync(modelPreviewGlbPath(ids.heavy))).toBe(true);
expect(fsSync.existsSync(modelPreviewGlbPath(ids.light))).toBe(true);
expect(fsSync.existsSync(modelPreviewGlbPath(ids.simplified))).toBe(false);
});
});
describe("/settings/rendering", () => {
let adminToken: string;
let memberToken: string;
let previous: boolean;
beforeAll(async () => {
previous = await getSimplifyPreviews();
const admin = await request(app)
.post("/api/register")
.send({ displayName: "Rendering Admin", email: `rendering-admin-${stamp}@example.com`, password: "password123" });
await prisma.user.update({ where: { id: admin.body.user.id }, data: { role: "ADMIN" } });
adminToken = (
await request(app)
.post("/api/login")
.send({ email: `rendering-admin-${stamp}@example.com`, password: "password123" })
).body.token;
const member = await request(app)
.post("/api/register")
.send({
displayName: "Rendering Member",
email: `rendering-member-${stamp}@example.com`,
password: "password123",
});
memberToken = member.body.token;
});
afterAll(async () => {
await setSimplifyPreviews(previous);
});
it("is off by default and admin-only", async () => {
await prisma.setting.deleteMany({ where: { key: "simplify_previews" } });
expect((await request(app).get("/api/settings/rendering").set(auth(memberToken))).status).toBe(403);
const res = await request(app).get("/api/settings/rendering").set(auth(adminToken));
expect(res.body).toEqual({ simplify_previews: false });
expect(
(await request(app).patch("/api/settings/rendering").set(auth(memberToken)).send({ simplify_previews: true }))
.status,
).toBe(403);
});
it("saves the admin's choice", async () => {
const res = await request(app)
.patch("/api/settings/rendering")
.set(auth(adminToken))
.send({ simplify_previews: true });
expect(res.body).toEqual({ simplify_previews: true });
expect(await getSimplifyPreviews()).toBe(true);
});
});