import { describe, expect, it } from "vitest"; import { TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS } from "../model-import-limits"; import { contextFor, } from "../test-fixtures/model-fixtures"; import { createObjAppearanceTransfer, OBJ_APPEARANCE_MTL, OBJ_APPEARANCE_SOURCE, } from "../test-fixtures/obj-appearance-fixtures"; import { loadObjMaterialLibrary } from "./obj-material-library"; import { readThreeStaticGeometryBundle } from "./three-static-source-bundle"; const encoder = new TextEncoder(); function load( options: Parameters[0] = {}, limits = TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, ) { const context = contextFor(createObjAppearanceTransfer(options)); return loadObjMaterialLibrary( new TextDecoder().decode(context.bundle.sourceFiles[0]!.bytes), readThreeStaticGeometryBundle(context, "obj"), limits, context.signal, ); } describe("OBJ material library", () => { it("maps bounded MTL factors and local texture resources", () => { const library = load(); const red = library.materialsByName.get("Red"); const blue = library.materialsByName.get("Blue"); expect(red).toMatchObject({ baseColor: [0.2, 0.3, 0.4], emissive: [0.1, 0.2, 0.3], metallic: 0, opacity: 0.75, roughness: 0.1, }); expect(red?.baseColorTexture?.texture).toMatchObject({ height: 3, width: 2, }); expect(red?.normalTexture).toMatchObject({ scale: 0.4 }); expect(red?.normalTexture?.texture).toMatchObject({ height: 4, width: 5, }); expect(red?.emissiveTexture?.texture.resourceRef).toBe( red?.baseColorTexture?.texture.resourceRef, ); expect(blue).toMatchObject({ baseColor: [0.9, 0.8, 0.7], opacity: 0.75, roughness: 1, }); expect(blue?.baseColorTexture).toBeUndefined(); expect(library.diagnostics).toContainEqual(expect.objectContaining({ code: "missing-appearance-resource", severity: "warning", })); }); it.each([ [ "remote library", { objSource: encoder.encode("mtllib https://example.com/main.mtl\n") }, "unsafe-material-library-uri", ], [ "traversal texture", { materialSource: encoder.encode( new TextDecoder().decode(OBJ_APPEARANCE_MTL) .replace("textures/diffuse.png", "../diffuse.png"), ), }, "unsafe-appearance-uri", ], ])("rejects %s paths", (_name, options, code) => { expect(() => load(options)).toThrow(expect.objectContaining({ code })); }); it("warns when a library or material definition is absent", () => { const objSource = encoder.encode( new TextDecoder().decode(OBJ_APPEARANCE_SOURCE) .replace("materials/main.mtl", "materials/missing.mtl"), ); const library = load({ objSource }); expect(library.materialsByName.size).toBe(0); expect(library.diagnostics.map(({ code }) => code)).toEqual([ "missing-material-library", "missing-material-definition", "missing-material-definition", ]); }); it("omits only a map channel that uses unsupported options", () => { const source = new TextDecoder().decode(OBJ_APPEARANCE_MTL).replace( "map_Kd textures/diffuse.png", "map_Kd -s 1 1 1 textures/diffuse.png", ); const library = load({ materialSource: encoder.encode(source) }); expect(library.materialsByName.get("Red")?.baseColorTexture).toBeUndefined(); expect(library.materialsByName.get("Red")?.normalTexture).toBeDefined(); expect(library.diagnostics).toContainEqual(expect.objectContaining({ code: "unsupported-mtl-map-option", severity: "warning", })); }); it("enforces the canonical texture resource limits", () => { expect(() => load({}, { ...TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, maxTextureCount: 1, })).toThrow(expect.objectContaining({ category: "resource-limit", code: "max-textures-exceeded", })); }); });