import { ComponentDatatype, DracoLoader, GltfBufferViewLoader, GltfDracoLoader, Resource, ResourceCache, ResourceLoaderState, when, } from "../../Source/Cesium.js"; import createScene from "../createScene.js"; import loaderProcess from "../loaderProcess.js"; import pollToPromise from "../pollToPromise.js"; import waitForLoaderProcess from "../waitForLoaderProcess.js"; describe("Scene/GltfDracoLoader", function () { var bufferTypedArray = new Uint8Array([1, 3, 7, 15, 31, 63, 127, 255]); var bufferArrayBuffer = bufferTypedArray.buffer; var decodedPositions = new Uint16Array([0, 0, 0, 65535, 65535, 65535, 0, 65535, 0]); // prettier-ignore var decodedNormals = new Uint8Array([0, 255, 128, 128, 255, 0]); var decodedIndices = new Uint16Array([0, 1, 2]); var gltfUri = "https://example.com/model.glb"; var gltfResource = new Resource({ url: gltfUri, }); var decodeDracoResults = { indexArray: { typedArray: decodedIndices, numberOfIndices: decodedIndices.length, }, attributeData: { POSITION: { array: decodedPositions, data: { byteOffset: 0, byteStride: 6, componentDatatype: ComponentDatatype.UNSIGNED_SHORT, componentsPerAttribute: 3, normalized: false, quantization: { octEncoded: false, quantizationBits: 14, minValues: [-1.0, -1.0, -1.0], range: 2.0, }, }, }, NORMAL: { array: decodedNormals, data: { byteOffset: 0, byteStride: 2, componentDatatype: ComponentDatatype.UNSIGNED_BYTE, componentsPerAttribute: 2, normalized: false, quantization: { octEncoded: true, quantizationBits: 10, }, }, }, }, }; var gltfDraco = { buffers: [ { uri: "external.bin", byteLength: 8, }, ], bufferViews: [ { buffer: 0, byteOffset: 4, byteLength: 4, }, ], accessors: [ { componentType: 5126, count: 3, max: [-1.0, -1.0, -1.0], min: [1.0, 1.0, 1.0], type: "VEC3", }, { componentType: 5126, count: 3, type: "VEC3", }, { componentType: 5123, count: 3, type: "SCALAR", }, ], meshes: [ { primitives: [ { attributes: { POSITION: 0, NORMAL: 1, }, indices: 2, extensions: { KHR_draco_mesh_compression: { bufferView: 0, attributes: { POSITION: 0, NORMAL: 1, }, }, }, }, ], }, ], }; var dracoExtension = gltfDraco.meshes[0].primitives[0].extensions.KHR_draco_mesh_compression; var scene; beforeAll(function () { scene = createScene(); }); afterAll(function () { scene.destroyForSpecs(); }); afterEach(function () { ResourceCache.clearForSpecs(); }); it("throws if resourceCache is undefined", function () { expect(function () { return new GltfDracoLoader({ resourceCache: undefined, gltf: gltfDraco, draco: dracoExtension, gltfResource: gltfResource, baseResource: gltfResource, }); }).toThrowDeveloperError(); }); it("throws if gltf is undefined", function () { expect(function () { return new GltfDracoLoader({ resourceCache: ResourceCache, gltf: undefined, draco: dracoExtension, gltfResource: gltfResource, baseResource: gltfResource, }); }).toThrowDeveloperError(); }); it("throws if draco is undefined", function () { expect(function () { return new GltfDracoLoader({ resourceCache: ResourceCache, gltf: gltfDraco, draco: undefined, gltfResource: gltfResource, baseResource: gltfResource, }); }).toThrowDeveloperError(); }); it("throws if gltfResource is undefined", function () { expect(function () { return new GltfDracoLoader({ resourceCache: ResourceCache, gltf: gltfDraco, draco: dracoExtension, gltfResource: undefined, baseResource: gltfResource, }); }).toThrowDeveloperError(); }); it("throws if baseResource is undefined", function () { expect(function () { return new GltfDracoLoader({ resourceCache: ResourceCache, gltf: gltfDraco, draco: dracoExtension, gltfResource: gltfResource, baseResource: undefined, }); }).toThrowDeveloperError(); }); it("rejects promise if buffer view fails to load", function () { var error = new Error("404 Not Found"); spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( when.reject(error) ); var dracoLoader = new GltfDracoLoader({ resourceCache: ResourceCache, gltf: gltfDraco, draco: dracoExtension, gltfResource: gltfResource, baseResource: gltfResource, }); dracoLoader.load(); return dracoLoader.promise .then(function (dracoLoader) { fail(); }) .otherwise(function (runtimeError) { expect(runtimeError.message).toBe( "Failed to load Draco\nFailed to load buffer view\nFailed to load external buffer: https://example.com/external.bin\n404 Not Found" ); }); }); it("rejects promise if draco decoding fails", function () { spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( when.resolve(bufferArrayBuffer) ); var error = new Error("Draco decode failed"); spyOn(DracoLoader, "decodeBufferView").and.returnValue(when.reject(error)); var dracoLoader = new GltfDracoLoader({ resourceCache: ResourceCache, gltf: gltfDraco, draco: dracoExtension, gltfResource: gltfResource, baseResource: gltfResource, }); dracoLoader.load(); return waitForLoaderProcess(dracoLoader, scene) .then(function (dracoLoader) { fail(); }) .otherwise(function (runtimeError) { expect(runtimeError.message).toBe( "Failed to load Draco\nDraco decode failed" ); }); }); it("loads draco", function () { spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( when.resolve(bufferArrayBuffer) ); // Simulate decodeBufferView not being ready for a few frames // Then simulate the promise not resolving for another few frames var deferredPromise = when.defer(); var decodeBufferViewCallsTotal = 3; var decodeBufferViewCallsCount = 0; var processCallsTotal = 6; var processCallsCount = 0; spyOn(DracoLoader, "decodeBufferView").and.callFake(function () { if (decodeBufferViewCallsCount++ === decodeBufferViewCallsTotal) { return deferredPromise.promise; } return undefined; }); var dracoLoader = new GltfDracoLoader({ resourceCache: ResourceCache, gltf: gltfDraco, draco: dracoExtension, gltfResource: gltfResource, baseResource: gltfResource, }); dracoLoader.load(); return pollToPromise(function () { loaderProcess(dracoLoader, scene); if (processCallsCount++ === processCallsTotal) { deferredPromise.resolve(decodeDracoResults); } return ( dracoLoader._state === ResourceLoaderState.READY || dracoLoader._state === ResourceLoaderState.FAILED ); }).then(function () { return dracoLoader.promise.then(function (dracoLoader) { loaderProcess(dracoLoader, scene); // Check that calling process after load doesn't break anything expect(dracoLoader.decodedData.indices).toBe( decodeDracoResults.indexArray ); expect(dracoLoader.decodedData.vertexAttributes).toBe( decodeDracoResults.attributeData ); }); }); }); it("destroys draco loader", function () { spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( when.resolve(bufferArrayBuffer) ); spyOn(DracoLoader, "decodeBufferView").and.returnValue( when.resolve(decodeDracoResults) ); var unloadBufferView = spyOn( GltfBufferViewLoader.prototype, "unload" ).and.callThrough(); var dracoLoader = new GltfDracoLoader({ resourceCache: ResourceCache, gltf: gltfDraco, draco: dracoExtension, gltfResource: gltfResource, baseResource: gltfResource, }); dracoLoader.load(); return waitForLoaderProcess(dracoLoader, scene).then(function ( dracoLoader ) { expect(dracoLoader.decodedData).toBeDefined(); expect(dracoLoader.isDestroyed()).toBe(false); dracoLoader.destroy(); expect(dracoLoader.decodedData).not.toBeDefined(); expect(dracoLoader.isDestroyed()).toBe(true); expect(unloadBufferView).toHaveBeenCalled(); }); }); function resolveBufferViewAfterDestroy(reject) { var deferredPromise = when.defer(); spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( deferredPromise.promise ); spyOn(DracoLoader, "decodeBufferView").and.returnValue( when.resolve(decodeDracoResults) ); // Load a copy of the buffer view into the cache so that the buffer view // promise resolves even if the draco loader is destroyed var bufferViewLoaderCopy = ResourceCache.loadBufferView({ gltf: gltfDraco, bufferViewId: 0, gltfResource: gltfResource, baseResource: gltfResource, }); var dracoLoader = new GltfDracoLoader({ resourceCache: ResourceCache, gltf: gltfDraco, draco: dracoExtension, gltfResource: gltfResource, baseResource: gltfResource, }); expect(dracoLoader.decodedData).not.toBeDefined(); dracoLoader.load(); dracoLoader.destroy(); if (reject) { deferredPromise.reject(new Error()); } else { deferredPromise.resolve(bufferArrayBuffer); } expect(dracoLoader.decodedData).not.toBeDefined(); expect(dracoLoader.isDestroyed()).toBe(true); ResourceCache.unload(bufferViewLoaderCopy); } it("handles resolving buffer view after destroy", function () { resolveBufferViewAfterDestroy(false); }); it("handles rejecting buffer view after destroy", function () { resolveBufferViewAfterDestroy(true); }); function resolveDracoAfterDestroy(reject) { spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue( when.resolve(bufferArrayBuffer) ); var deferredPromise = when.defer(); var decodeBufferView = spyOn(DracoLoader, "decodeBufferView").and.callFake( function () { return deferredPromise.promise; } ); var dracoLoader = new GltfDracoLoader({ resourceCache: ResourceCache, gltf: gltfDraco, draco: dracoExtension, gltfResource: gltfResource, baseResource: gltfResource, }); expect(dracoLoader.decodedData).not.toBeDefined(); dracoLoader.load(); loaderProcess(dracoLoader, scene); expect(decodeBufferView).toHaveBeenCalled(); // Make sure the decode actually starts dracoLoader.destroy(); if (reject) { deferredPromise.reject(new Error()); } else { deferredPromise.resolve(decodeDracoResults); } expect(dracoLoader.decodedData).not.toBeDefined(); expect(dracoLoader.isDestroyed()).toBe(true); } it("handles resolving draco after destroy", function () { resolveDracoAfterDestroy(false); }); it("handles rejecting draco after destroy", function () { resolveDracoAfterDestroy(true); }); });