import { describe, expect, it } from "vitest"; import { digestToolcraftModelDocument } from "../canonical/model-document-digest"; import { createModelMaterial, createModelTexture, createValidModelDocumentV2, } from "../canonical/model-document-test-support"; import { validateToolcraftModelDocument } from "../canonical/model-document-validation"; import { applyToolcraftModelRepair } from "./apply-model-repair"; import { analyzeToolcraftModel } from "./model-topology-analysis"; import { digestToolcraftTopologyDocument } from "./model-topology-digest"; import { boundsForPositions, createClosedSolidWithDuplicatePrimitive, createOpenPlanePrimitive, createRepairablePrimitive, createTopologyDocument, createTopologyPrimitive, } from "./model-topology-test-support"; describe("applyToolcraftModelRepair", () => { it("proves a duplicate-triangle solid repair clean after application", () => { const document = createTopologyDocument([ createClosedSolidWithDuplicatePrimitive(), ]); const analysis = analyzeToolcraftModel(document, "solid-mesh"); const repaired = applyToolcraftModelRepair(document, analysis.repairPlan!); expect(repaired.primitives[0]!.indices.length / 3).toBe(4); const proof = analyzeToolcraftModel(repaired, "solid-mesh"); expect(proof.outcome).toBe("clean"); expect(proof.repairPlan).toBeUndefined(); }); it("applies only the exact compiled operations and recalculates geometry", () => { const document = createTopologyDocument([createRepairablePrimitive()]); const analysis = analyzeToolcraftModel(document, "realtime-mesh"); const repaired = applyToolcraftModelRepair(document, analysis.repairPlan!); expect(repaired.primitives[0]!.indices).toEqual( new Uint32Array([0, 1, 2, 0, 2, 3]), ); expect(repaired.primitives[0]!.positions.length).toBe(12); expect(repaired.primitives[0]!.normals?.length).toBe(12); expect([...repaired.primitives[0]!.normals!].every(Number.isFinite)).toBe(true); expect(repaired.bounds).toEqual({ min: [0, 0, 0], max: [1, 1, 0] }); expect(repaired.provenance.repair?.operations).toEqual([ "remove-invalid-triangles", "remove-duplicate-triangles", "compact-unused-vertices", "regenerate-normals", "recalculate-bounds", ]); expect(validateToolcraftModelDocument(repaired)).toEqual({ ok: true }); expect(analyzeToolcraftModel(repaired, "realtime-mesh").outcome).toBe("clean"); }); it("repairs deterministic local winding", () => { const document = createTopologyDocument([ createOpenPlanePrimitive({ indices: new Uint32Array([0, 1, 2, 0, 3, 2]) }), ]); const analysis = analyzeToolcraftModel(document, "realtime-mesh"); const repaired = applyToolcraftModelRepair(document, analysis.repairPlan!); expect(repaired.primitives[0]!.indices).toEqual( new Uint32Array([0, 1, 2, 0, 2, 3]), ); expect(analyzeToolcraftModel(repaired, "realtime-mesh").diagnostics) .not.toContainEqual(expect.objectContaining({ code: "inconsistent-local-winding" })); }); it("preserves matrices, source scale, authored seams, and canonical input arrays", () => { const positions = new Float32Array([ 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -1, 0, ]); const primitive = createTopologyPrimitive({ bounds: boundsForPositions(positions), indices: new Uint32Array([0, 1, 2, 3, 4, 5]), normals: undefined, positions, }); const matrix = [ 2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 4, 0, 10, 20, 30, 1, ] as const; const document = createTopologyDocument([primitive], { localMatrix: matrix }); const originalPositions = primitive.positions.slice(); const originalIndices = primitive.indices.slice(); const originalBounds = structuredClone(document.bounds); const repaired = applyToolcraftModelRepair( document, analyzeToolcraftModel(document, "realtime-mesh").repairPlan!, ); expect(repaired.nodes[0]!.localMatrix).toEqual(matrix); expect(repaired.primitives[0]!.positions).toEqual(positions); expect(repaired.primitives[0]!.positions.length / 3).toBe(6); expect(repaired.primitives[0]!.positions).not.toBe(primitive.positions); expect(repaired.primitives[0]!.indices).not.toBe(primitive.indices); expect(primitive.positions).toEqual(originalPositions); expect(primitive.indices).toEqual(originalIndices); expect(document.bounds).toEqual(originalBounds); }); it("compacts every appearance vertex channel and preserves material bindings", () => { const sourcePrimitive = createRepairablePrimitive(); const normals = new Float32Array([ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ]); const uv0 = new Float32Array([ 0, 0, 1, 0, 1, 1, 0, 1, 2, 0, 3, 0, 5, 5, ]); const uv1 = new Float32Array([ 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.2, 0.3, 0.4, 0.5, 0.9, 0.9, ]); const colors = new Float32Array([ 1, 0, 0, 1, 0, 1, 0, 0.9, 0, 0, 1, 0.8, 1, 1, 0, 0.7, 1, 0, 1, 0.6, 0, 1, 1, 0.5, 0.25, 0.5, 0.75, 0.4, ]); const texture = createModelTexture(); const material = createModelMaterial(); const document = createValidModelDocumentV2({ bounds: sourcePrimitive.bounds, materials: [material], primitives: [{ ...sourcePrimitive, colors: { components: 4, values: colors }, materialId: material.id, normals, textureCoordinates: [ { set: 0, values: uv0 }, { set: 1, values: uv1 }, ], }], textures: [texture], }); const analysis = analyzeToolcraftModel(document, "realtime-mesh"); const repaired = applyToolcraftModelRepair(document, analysis.repairPlan!); expect(repaired.version).toBe(2); if (repaired.version !== 2) throw new Error("Expected repaired appearance document."); expect(repaired.primitives[0]!.positions.length / 3).toBe(4); expect(repaired.primitives[0]!.normals).toEqual(normals.slice(0, 12)); expect(repaired.primitives[0]!.colors).toEqual({ components: 4, values: colors.slice(0, 16), }); expect(repaired.primitives[0]!.textureCoordinates).toEqual([ { set: 0, values: uv0.slice(0, 8) }, { set: 1, values: uv1.slice(0, 8) }, ]); expect(repaired.primitives[0]!.materialId).toBe(material.id); expect(repaired.materials).toEqual([material]); expect(repaired.textures).toEqual([texture]); expect(validateToolcraftModelDocument(repaired)).toEqual({ ok: true }); }); it("keeps topology identity independent from appearance schema and bindings", () => { const geometry = createTopologyDocument(); const appearance = createValidModelDocumentV2({ bounds: geometry.bounds, nodes: geometry.nodes, primitives: geometry.primitives, rootNodeIds: geometry.rootNodeIds, }); expect(digestToolcraftTopologyDocument(appearance)).toBe( digestToolcraftTopologyDocument(geometry), ); expect(digestToolcraftModelDocument(appearance)).not.toBe( digestToolcraftModelDocument(geometry), ); }); it("produces deterministic plan digests, recipe ids, and repair provenance", () => { const firstDocument = createTopologyDocument([createRepairablePrimitive()]); const secondDocument = createTopologyDocument([createRepairablePrimitive()]); const firstAnalysis = analyzeToolcraftModel(firstDocument, "realtime-mesh"); const secondAnalysis = analyzeToolcraftModel(secondDocument, "realtime-mesh"); expect(firstAnalysis.repairPlan?.planDigest).toMatch(/^sha256:[0-9a-f]{64}$/u); expect(firstAnalysis.repairPlan?.planDigest).toBe(secondAnalysis.repairPlan?.planDigest); expect(firstAnalysis.repairPlan?.recipeId).toBe(secondAnalysis.repairPlan?.recipeId); const first = applyToolcraftModelRepair(firstDocument, firstAnalysis.repairPlan!); const second = applyToolcraftModelRepair(secondDocument, secondAnalysis.repairPlan!); expect(first.provenance.repair).toEqual(second.provenance.repair); expect(first.provenance.repair).toMatchObject({ algorithmVersion: expect.any(String), planDigest: firstAnalysis.repairPlan?.planDigest, recipeId: firstAnalysis.repairPlan?.recipeId, }); expect(first.primitives[0]!.positions).toEqual(second.primitives[0]!.positions); expect(first.primitives[0]!.indices).toEqual(second.primitives[0]!.indices); expect(first.primitives[0]!.normals).toEqual(second.primitives[0]!.normals); }); it("contains no unsupported repair vocabulary or effects", () => { const document = createTopologyDocument([createRepairablePrimitive()]); const plan = analyzeToolcraftModel(document, "realtime-mesh").repairPlan!; const serialized = JSON.stringify(plan); for (const unsupported of [ "weld", "fill-hole", "remesh", "decimate", "subdivide", "smooth", "merge-shell", "source-scale", "node-transform", "guess-index", ]) { expect(serialized).not.toContain(unsupported); } expect(new Set(plan.operations.map(({ type }) => type))).toEqual(new Set([ "remove-invalid-triangles", "remove-duplicate-triangles", "compact-unused-vertices", "regenerate-normals", "recalculate-bounds", ])); }); it("rejects a plan compiled for a different source document", () => { const source = createTopologyDocument([createRepairablePrimitive()]); const plan = analyzeToolcraftModel(source, "realtime-mesh").repairPlan!; const other = createTopologyDocument([createRepairablePrimitive()]); other.nodes[0]!.localMatrix = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, ]; expect(() => applyToolcraftModelRepair(other, plan)).toThrow( /source document digest/u, ); }); });