import { describe, expect, it } from "vitest"; import { createModelMaterial, createModelPrimitiveV2, createModelTexture, createValidModelDocument, createValidModelDocumentV2, } from "./model-document-test-support"; import { getValidatedToolcraftModelDocumentSnapshot, validateToolcraftModelDocument, } from "./model-document-validation"; function failureCode(document: unknown): string | undefined { const result = validateToolcraftModelDocument(document); return result.ok ? undefined : result.code; } describe("canonical model appearance validation", () => { it("accepts v1 fallback documents and complete v2 appearance records", () => { expect(validateToolcraftModelDocument(createValidModelDocument())).toEqual({ ok: true, }); expect(validateToolcraftModelDocument(createValidModelDocumentV2())).toEqual({ ok: true, }); }); it("snapshots UV and color arrays into owned memory", () => { const primitive = createModelPrimitiveV2(); const document = createValidModelDocumentV2({ primitives: [primitive] }); const snapshot = getValidatedToolcraftModelDocumentSnapshot(document); if (snapshot.version !== 2) throw new Error("Expected appearance snapshot"); const snapshotPrimitive = snapshot.primitives[0]!; expect(snapshotPrimitive.colors?.values).not.toBe(primitive.colors?.values); expect(snapshotPrimitive.textureCoordinates?.[0]?.values).not.toBe( primitive.textureCoordinates?.[0]?.values, ); primitive.colors?.values.fill(0); primitive.textureCoordinates?.[0]?.values.fill(0); expect([...snapshotPrimitive.colors!.values]).not.toEqual( [...primitive.colors!.values], ); }); it("keeps v1 snapshots geometry-only", () => { const document = createValidModelDocument(); const snapshot = getValidatedToolcraftModelDocumentSnapshot({ ...document, primitives: [{ ...document.primitives[0], colors: { components: 4, values: new Float32Array(12) }, materialId: "smuggled-material", }], }); expect(snapshot.version).toBe(1); expect(snapshot.primitives[0]).not.toHaveProperty("colors"); expect(snapshot.primitives[0]).not.toHaveProperty("materialId"); }); it.each([ ["duplicate texture ids", { ...createValidModelDocumentV2(), textures: [createModelTexture(), createModelTexture()], }, "duplicate-id"], ["duplicate material ids", { ...createValidModelDocumentV2(), materials: [createModelMaterial(), createModelMaterial()], }, "duplicate-id"], ["dangling material refs", { ...createValidModelDocumentV2(), primitives: [createModelPrimitiveV2({ materialId: "missing" })], }, "missing-material-reference"], ["dangling texture refs", { ...createValidModelDocumentV2(), materials: [createModelMaterial({ baseColorTexture: { texCoord: 0, textureId: "missing" }, })], }, "missing-texture-reference"], ["invalid color cardinality", { ...createValidModelDocumentV2(), primitives: [createModelPrimitiveV2({ colors: { components: 4, values: new Float32Array([1, 1, 1]) }, })], }, "color-count-mismatch"], ["invalid UV cardinality", { ...createValidModelDocumentV2(), primitives: [createModelPrimitiveV2({ textureCoordinates: [{ set: 0, values: new Float32Array([0, 0]) }], })], }, "texture-coordinate-count-mismatch"], ["duplicate UV sets", { ...createValidModelDocumentV2(), primitives: [createModelPrimitiveV2({ textureCoordinates: [ { set: 0, values: new Float32Array(6) }, { set: 0, values: new Float32Array(6) }, ], })], }, "duplicate-texture-coordinate-set"], ["more than UV0 and UV1", { ...createValidModelDocumentV2(), primitives: [createModelPrimitiveV2({ textureCoordinates: [ { set: 0, values: new Float32Array(6) }, { set: 1, values: new Float32Array(6) }, { set: 1, values: new Float32Array(6) }, ], })], }, "invalid-texture-coordinates"], ["non-finite UV values", { ...createValidModelDocumentV2(), primitives: [createModelPrimitiveV2({ textureCoordinates: [{ set: 0, values: new Float32Array([0, 0, 1, 0, 0, Number.NaN]), }], })], }, "non-finite-appearance-value"], ["out-of-range vertex colors", { ...createValidModelDocumentV2(), primitives: [createModelPrimitiveV2({ colors: { components: 3, values: new Float32Array([1, 0, 0, 0, 1.01, 0, 0, 0, 1]), }, })], }, "non-finite-appearance-value"], ["non-finite factors", { ...createValidModelDocumentV2(), materials: [createModelMaterial({ roughness: Number.NaN })], }, "invalid-material-factor"], ["out-of-range factors", { ...createValidModelDocumentV2(), materials: [createModelMaterial({ opacity: 1.01 })], }, "invalid-material-factor"], ["unsupported image MIME", { ...createValidModelDocumentV2(), textures: [{ ...createModelTexture(), mimeType: "image/gif" }], }, "unsupported-texture-mime"], ["invalid digest/ref pairing", { ...createValidModelDocumentV2(), textures: [{ ...createModelTexture(), resourceRef: "toolcraft:model-appearance:forged" }], }, "invalid-texture-resource-ref"], ["invalid sampler enum", { ...createValidModelDocumentV2(), textures: [{ ...createModelTexture(), sampler: { ...createModelTexture().sampler, wrapS: "border" }, }], }, "invalid-texture-sampler"], ["version/provenance mismatch", { ...createValidModelDocumentV2(), provenance: { ...createValidModelDocumentV2().provenance, canonicalSchemaVersion: 1, }, }, "invalid-canonical-schema-version"], ])("rejects %s", (_label, document, code) => { expect(failureCode(document)).toBe(code); }); it("enforces texture count, dimension, per-texture pixels, and aggregate pixels", () => { const document = createValidModelDocumentV2(); expect(validateToolcraftModelDocument(document, { maxTextureCount: 1, maxTextureDimension: 1, })).toMatchObject({ code: "max-texture-dimension-exceeded", ok: false }); expect(validateToolcraftModelDocument(document, { maxTextureCount: 1, maxTexturePixels: 3, })).toMatchObject({ code: "max-texture-pixels-exceeded", ok: false }); expect(validateToolcraftModelDocument({ ...document, textures: [ createModelTexture({ id: "texture-1" }), createModelTexture({ id: "texture-2" }), ], }, { maxTextureCount: 1 })).toMatchObject({ code: "max-textures-exceeded", ok: false, }); expect(validateToolcraftModelDocument({ ...document, textures: [ createModelTexture({ height: 1, id: "texture-1", width: 2 }), createModelTexture({ height: 1, id: "texture-2", width: 2 }), ], }, { maxTexturePixels: 3 })).toMatchObject({ code: "max-texture-pixels-exceeded", ok: false, }); }); });