import { statSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { beforeAll, describe, expect, it } from "vitest"; import { NodeIO } from "@gltf-transform/core"; import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; import { getBounds } from "@gltf-transform/functions"; import { MeshoptDecoder } from "meshoptimizer"; import { isSolidMaterial, PLACES, SOLID_MATERIALS } from "../src/world/places.js"; import { EXT_MATERIALS_BUMP, EXTMaterialsBump } from "../tools/assets/bumpExtension.mjs"; // Feste Materialien je Ort. SOLID_MATERIALS gilt für alle Orte zusammen, aber keine Datei enthält alle // Namen — geprüft wird deshalb jede Datei gegen ihre eigene Zeile. const SOLID_BY_PLACE = { "hood-yard": [ "gravel", "asphalt", "curbAsphalt", "padAsphalt", "puddleAsphalt", "cobble", "grass", "concrete", "cont_a_side", "cont_a_end", "cont_b_side", "cont_b_end", "cont_c_side", "cont_c_end", "contFrame", "ironPlain", "ironTeal", "ironBlue", "galv", "rust", "darkTrim", "osb", "osbB", "rollershutter_window_01", "rollershutter_window_01_graffiti", "chainlink_post", "chainlink_wire", "modular_electricity_poles", "barrel_03", "plastic_container", "quiver_tree_01_trunk", "quiver_tree_02", ], tribuene: ["Sichtbeton", "Estrich", "Verkleidung", "Stahl", "Trapezblech", "Riffelblech", "Verglasung", "utility_box_01"], }; // Materialien, deren Relief nur aus EXT_materials_bump kommt. Die Tribüne hat keine Bump-Maps. const BUMP_BY_PLACE = { "hood-yard": ["cont_a_side", "cont_a_end", "cont_b_side", "cont_b_end", "cont_c_side", "cont_c_end"], tribuene: [], }; // Bruch: ein festes Material steht in keiner Zeile — die Datei seines Orts würde nie darauf geprüft. Als // Mengen verglichen, damit ein Name, der in zwei Orten fest ist, keinen Fehlalarm auslöst. it("verteilt SOLID_MATERIALS vollständig auf die Orte", () => { expect(new Set(Object.values(SOLID_BY_PLACE).flat())).toEqual(new Set(SOLID_MATERIALS)); }); describe.each(PLACES.map((place) => [place.id, place]))("%s.glb", (id, place) => { const file = fileURLToPath(new URL(`../public/${place.url.slice(2)}`, import.meta.url)); // Kein "?? []": eine fehlende Zeile soll die Prüfung darunter rot machen (Bruch), nicht still // durchlaufen. it.runIf gleich unten sichert nur seine eigene Bedingung gegen "undefined" ab. const bump = BUMP_BY_PLACE[id]; let document; beforeAll(async () => { await MeshoptDecoder.ready; const io = new NodeIO() .registerExtensions([...ALL_EXTENSIONS, EXTMaterialsBump]) .registerDependencies({ "meshopt.decoder": MeshoptDecoder }); document = await io.read(file); }, 30000); // Bruch: Aufbereitung übersprungen oder falsch eingestellt — die Quelle des Hood Yard hatte 653 MB. it("ist kleiner als 30 MB", () => { expect(statSync(file).size).toBeLessThan(30e6); }); // Bruch: Geometrie unkomprimiert oder GPU-Instanzen, deren Matrizen die Kollision des Controllers nicht liest. it("nutzt meshopt und WebP und hat keine Instanzen", () => { const used = document.getRoot().listExtensionsUsed().map((extension) => extension.extensionName); expect(used).toEqual(expect.arrayContaining(["EXT_meshopt_compression", "EXT_texture_webp"])); expect(used).not.toContain("EXT_mesh_gpu_instancing"); }); it("hat nur WebP-Texturen bis 1024 px", () => { for (const texture of document.getRoot().listTextures()) { expect(texture.getMimeType()).toBe("image/webp"); expect(Math.max(...texture.getSize())).toBeLessThanOrEqual(1024); } }); // Bruch: der Ort hat keine Zeile in BUMP_BY_PLACE — ohne diesen Test liefe die Bump-Prüfung darunter // per it.runIf(bump?.length > 0) einfach übersprungen durch, statt rot zu werden. it("hat eine Zeile in BUMP_BY_PLACE", () => { expect(bump).toBeDefined(); }); // Bruch: die Aufbereitung verwirft EXT_materials_bump (wie die gltf-transform-CLI) — das Wellblech der // Container wäre flach. it.runIf((bump ?? []).length > 0)("gibt den Bump-Materialien ihre Bump-Textur", () => { const used = document.getRoot().listExtensionsUsed().map((extension) => extension.extensionName); const materials = new Map(document.getRoot().listMaterials().map((material) => [material.getName(), material])); expect(used).toContain(EXT_MATERIALS_BUMP); for (const name of bump) { expect(materials.get(name)?.getExtension(EXT_MATERIALS_BUMP)?.getBumpTexture()).toBeTruthy(); } }); // Bruch: dedup oder ein neues Werkzeug benennt ein Material um — der Ort wäre dort ohne Warnung // durchlässig, weil die Kollision nach Namen auswählt. Oder der Ort hat keine Zeile in SOLID_BY_PLACE. it("enthält jedes feste Material seines Orts", () => { const names = document.getRoot().listMaterials().map((material) => material.getName()); expect(SOLID_BY_PLACE[id]).toBeDefined(); expect(SOLID_BY_PLACE[id].filter((name) => !names.includes(name))).toEqual([]); }); // Bruch: ein Material der Datei ist laut isSolidMaterial fest (weil sein Name anderswo fest ist), ohne // in der Zeile seines Orts zu stehen — es würde hier still mitkollidieren, ungeprüft von SOLID_BY_PLACE. it("hat kein fremdes festes Material ohne Zeile in SOLID_BY_PLACE", () => { const names = document.getRoot().listMaterials().map((material) => material.getName()); const ownSolid = new Set(SOLID_BY_PLACE[id]); expect(names.filter((name) => isSolidMaterial(name) && !ownSolid.has(name))).toEqual([]); }); // Bruch: bounds in PLACES passt nicht mehr zur Datei — der Datentest prüfte dann die falsche Fläche. it("passt auf 5 cm zu den Außenmaßen in PLACES", () => { const { min, max } = getBounds(document.getRoot().listScenes()[0]); const { minX, maxX, minZ, maxZ } = place.bounds; expect(Math.abs(min[0] - minX)).toBeLessThan(0.05); expect(Math.abs(max[0] - maxX)).toBeLessThan(0.05); expect(Math.abs(min[2] - minZ)).toBeLessThan(0.05); expect(Math.abs(max[2] - maxZ)).toBeLessThan(0.05); }); });