UNPKG

mdx-m3-viewer

Version:

A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.

210 lines (176 loc) 6.13 kB
import * as resemble from '../../thirdparty/resemble'; import seededRandom from '../common/seededrandom'; import {imageToImageData, blobToImage} from '../common/canvas'; import {downloadBlob} from '../common/download'; import ModelViewer from '../viewer/viewer'; import Mdx from '../viewer/handlers/mdx/handler'; import M3 from '../viewer/handlers/m3/handler'; import Geo from '../viewer/handlers/geo/handler'; /** * A unit tester designed for the model viewer. * The input of each test is a pre-defined scene, and the output is the rendered image. * The image is then compared to another image generated from the same test, at a time when rendering it was considered "correct". */ export default class UnitTester { /** * */ constructor() { let canvas = document.createElement('canvas'); canvas.width = canvas.height = 256; let viewer = new ModelViewer(canvas, {alpha: false, antialias: false}); viewer.gl.clearColor(0.05, 0.05, 0.05, 1); viewer.on('error', (target, error, reason) => console.log(target, error, reason)); viewer.addHandler(Mdx); viewer.addHandler(M3); viewer.addHandler(Geo); this.viewer = viewer; this.mathRandom = Math.random; this.tests = []; } /** * Add a test or a hierarchy of tests. * * @param {Object} test */ add(test) { if (test.tests) { this.addBaseName(test.tests, test.name); } else { this.tests.push({name: test.name, test}); } } /** * Run all of the tests that were added. * The callback will be called with the result of each one. * The results look like iterators: {done: true/false, value: undefine/result }. * * @param {function} callback */ async test(callback) { for (let test of this.tests) { let testBlob = await this.getTestBlob(test); let comparisonBlob = await this.getComparisonBlob(test); if (testBlob && comparisonBlob) { let comparisonPromise = new Promise((resolve) => resemble(testBlob).compareTo(comparisonBlob).ignoreColors().onComplete((data) => resolve(data))); let [testImage, comparisonImage, testResult] = await Promise.all([blobToImage(testBlob), blobToImage(comparisonBlob), comparisonPromise]); callback({done: false, value: {name: test.name, testImage, comparisonImage, result: testResult.rawMisMatchPercentage}}); } else { // Fail modes. // 1) The test blob exists, but comparison doesn't. This happens when adding new tests. // 2) The comparison blob exists, but the test doesn't. This happens when having issues with fetching the files needed for the tests. // 3) Neither exists. if (testBlob) { callback({done: false, value: {name: test.name, testImage: await blobToImage(testBlob), result: 100}}); } else if (comparisonBlob) { callback({done: false, value: {name: test.name, comparisonImage: await blobToImage(comparisonBlob), result: 100}}); } else { callback({done: false, value: {name: test.name, result: 100}}); } } } callback({done: true}); } /** * Run all of the tests that were added, and download them. * The tests are not compared against anything. * This is used to update the "correct" results. * * @param {function} callback */ async download(callback) { for (let test of this.tests) { let name = test.name; let testBlob = await this.getTestBlob(test); let ok = !!testBlob; if (ok) { downloadBlob(testBlob, `${test.name}.png`); } callback({done: false, value: {name, ok}}); } callback({done: true}); } /** * Is the given resource or array of resources ok? * * @param {Resource|Array<Resource>} data * @return {boolean} */ isDataAGo(data) { if (Array.isArray(data)) { for (let resource of data) { if (!resource.ok) { return false; } } return true; } return data.ok; } /** * Given a test, return a promise that will resolve to the blob that resulted from running the test. * * @param {Object} test * @return {Promise<?Blob>} */ async getTestBlob(test) { let loadHandler = test.test.load; let testHandler = test.test.test; let viewer = this.viewer; // Clear the viewer viewer.clear(); let scene = viewer.addScene(); let camera = scene.camera; // Setup the camera camera.viewport([0, 0, viewer.canvas.width, viewer.canvas.height]); camera.perspective(Math.PI / 4, 1, 8, 100000); // Start loading the test. let data = loadHandler(viewer); // Wait until everything loaded. await viewer.whenAllLoaded(); if (this.isDataAGo(data)) { // Replace Math.random with a custom seeded random generator. // This allows to run the viewer in a deterministic environment for tests. // For example, particles have some randomized data, which can make tests mismatch. Math.random = seededRandom(6); // Run the test. testHandler(viewer, scene, camera, data); // Update and render. viewer.updateAndRender(); // Put back Math.random in its place. Math.random = this.mathRandom; // Return the viewer's canvas' blob. return await viewer.toBlob(); } else { return null; } } /** * Given a test, return a promise that will resolve to the comparison image of this test. * * @param {Object} test * @return {Promise} */ async getComparisonBlob(test) { let response = await fetch(`compare/${test.name}.png`); if (response.ok) { return await response.blob(); } } /** * Adds tests from an hierarchy will appending their names. * Called automatically by add() if needed. * * @param {Array<Object>} tests * @param {string} baseName */ addBaseName(tests, baseName) { for (let test of tests) { if (test.tests) { this.addBaseName(test.tests, baseName + '-' + test.name); } else { this.tests.push({name: baseName + '-' + test.name, test}); } } } }