import { describe, expect, it } from "vitest"; import { contextFor, createGlbTransfer, createGltfTransfer, } from "../test-fixtures/model-fixtures"; import { GLTF_APPEARANCE_JPEG, GLTF_APPEARANCE_PNG, createEmbeddedGltfPbrFixture, createLocalImageGltfTransfer, } from "../test-fixtures/gltf-appearance-fixtures"; import { toolcraftGlbModelFormatAdapter, toolcraftGltfModelFormatAdapter, } from "./gltf-model-format-adapter"; function imageDataUri(bytes: Uint8Array, mimeType: string): string { let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return `data:${mimeType};base64,${btoa(binary)}`; } describe("glTF canonical appearance", () => { it("preserves embedded PNG/JPEG PBR, UV sets, RGBA colors, and samplers", async () => { const result = await toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(createEmbeddedGltfPbrFixture())), ); expect(result.document.version).toBe(2); if (result.document.version !== 2) throw new Error("Expected v2 document."); expect(result.document.materials).toHaveLength(1); expect(result.document.materials[0]).toMatchObject({ alphaCutoff: 0.4, alphaMode: "mask", baseColor: [0.25, 0.5, 0.75], doubleSided: true, emissive: [0.1, 0.2, 0.3], metallic: 0.7, opacity: 0.6, roughness: 0.2, usesVertexColor: true, }); expect(result.document.materials[0]!.normalTexture).toMatchObject({ scale: 0.35, texCoord: 0, }); expect(result.document.materials[0]!.occlusionTexture).toMatchObject({ strength: 0.45, texCoord: 1, }); expect(result.document.textures).toHaveLength(4); expect(result.document.textures.map((texture) => [ texture.colorSpace, texture.mimeType, texture.width, texture.height, ])).toEqual([ ["srgb", "image/png", 2, 3], ["srgb", "image/jpeg", 5, 4], ["linear", "image/jpeg", 5, 4], ["linear", "image/png", 2, 3], ]); expect(result.document.textures[0]!.sampler).toEqual({ magFilter: "nearest", minFilter: "nearest-mipmap-nearest", wrapS: "clamp-to-edge", wrapT: "mirrored-repeat", }); expect(result.appearanceResources.map(({ mimeType }) => mimeType).sort()) .toEqual(["image/jpeg", "image/png"]); const primitive = result.document.primitives[0]!; expect(primitive.materialId).toBe("material:0"); expect(primitive.textureCoordinates?.map(({ set }) => set)).toEqual([0, 1]); expect(primitive.colors).toMatchObject({ components: 4 }); expect(primitive.colors?.values).toEqual(new Float32Array([ 1, 0, 0, 1, 0, 1, 0, 128 / 255, 0, 0, 1, 64 / 255, ])); }); it("resolves exact local image dependencies without network access", async () => { const result = await toolcraftGltfModelFormatAdapter.decode( contextFor(createLocalImageGltfTransfer()), ); expect(result.appearanceResources).toHaveLength(2); expect(result.diagnostics).not.toEqual( expect.arrayContaining([ expect.objectContaining({ code: "missing-appearance-resource" }), ]), ); }); it("tolerates unreferenced appearance sidecars in a selected package", async () => { const transfer = createLocalImageGltfTransfer(); const result = await toolcraftGltfModelFormatAdapter.decode( contextFor({ ...transfer, sourceFiles: [ ...transfer.sourceFiles, { bytes: GLTF_APPEARANCE_PNG.slice().buffer, contentDigest: "fixture-unused-sidecar", mimeType: "image/png", path: "models/textures/unused.png", }, ], }), ); expect(result.appearanceResources).toHaveLength(2); }); it("omits only slots backed by a missing local image", async () => { const result = await toolcraftGltfModelFormatAdapter.decode( contextFor(createLocalImageGltfTransfer("jpeg")), ); if (result.document.version !== 2) throw new Error("Expected v2 document."); const material = result.document.materials[0]!; expect(material.baseColorTexture).toBeDefined(); expect(material.normalTexture).toBeDefined(); expect(material.emissiveTexture).toBeUndefined(); expect(material.metallicRoughnessTexture).toBeUndefined(); expect(material.occlusionTexture).toBeUndefined(); expect(material).toMatchObject({ baseColor: [0.25, 0.5, 0.75], emissive: [0.1, 0.2, 0.3], metallic: 0.7, roughness: 0.2, }); expect(result.appearanceResources).toHaveLength(1); expect(result.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ code: "missing-appearance-resource", explanation: expect.stringContaining("textures/packed.jpg"), severity: "warning", }), ]), ); }); it("preserves RGB vertex colors and blend semantics", async () => { const fixture = createEmbeddedGltfPbrFixture(); fixture.json.accessors![3]!.type = "VEC3"; fixture.json.bufferViews![3]!.byteLength = 9; fixture.json.materials![0]!.alphaMode = "BLEND"; const result = await toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(fixture)), ); if (result.document.version !== 2) throw new Error("Expected v2 document."); expect(result.document.materials[0]!.alphaMode).toBe("blend"); expect(result.document.primitives[0]!.colors).toMatchObject({ components: 3 }); }); it("accepts bounded embedded PNG/JPEG data URIs without fetch", async () => { const fixture = createEmbeddedGltfPbrFixture(); fixture.json.images = [ { uri: imageDataUri(GLTF_APPEARANCE_PNG, "image/png") }, { uri: imageDataUri(GLTF_APPEARANCE_JPEG, "image/jpeg") }, ]; const result = await toolcraftGltfModelFormatAdapter.decode( contextFor(createGltfTransfer(fixture)), ); expect(result.appearanceResources).toHaveLength(2); }); it.each([ ["bytes", { maxTextureBytes: 30 }, "max-texture-bytes-exceeded"], ["count", { maxTextureCount: 1 }, "max-textures-exceeded"], ["dimension", { maxTextureDimension: 2 }, "max-texture-dimension-exceeded"], ["pixels", { maxTexturePixels: 10 }, "max-texture-pixels-exceeded"], ] as const)("preflights texture %s limits", async (_name, limits, code) => { await expect( toolcraftGlbModelFormatAdapter.decode( contextFor(createGlbTransfer(createEmbeddedGltfPbrFixture()), { limits }), ), ).rejects.toMatchObject({ category: "resource-limit", code }); }); });