@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
149 lines ⢠7.32 kB
JavaScript
/*
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var _a;
const { promises: fs } = require('fs');
const puppeteer = require('puppeteer');
const path = require('path');
const { PNG } = require('pngjs');
const makeDir = require('make-dir');
import { ImageComparator } from './common.js';
import { ConfigReader } from './config-reader.js';
const $configReader = Symbol('configReader');
export class ArtifactCreator {
constructor(config, rootDirectory, baseUrl) {
this.config = config;
this.rootDirectory = rootDirectory;
this.baseUrl = baseUrl;
this[_a] = new ConfigReader(this.config);
console.log('š Preparing to capture screenshots for fidelity comparison');
}
get outputDirectory() {
return path.join(path.resolve(this.rootDirectory), 'results');
}
get goldens() {
return this.config.renderers.map(renderer => (Object.assign({}, renderer, { file: `${renderer.name}-golden.png` })));
}
async captureAndAnalyzeScreenshots(scenarioWhitelist = null) {
const { scenarios, analysisThresholds } = this.config;
const analyzedScenarios = [];
const { goldens, outputDirectory } = this;
for (const scenario of scenarios) {
const { name: scenarioName, dimensions } = scenario;
if (scenarioWhitelist != null && !scenarioWhitelist.has(scenarioName)) {
continue;
}
console.log(`\nšØ Scenario: ${scenarioName}`);
const scenarioOutputDirectory = path.join(outputDirectory, scenarioName);
await makeDir(scenarioOutputDirectory);
const screenshot = await this.captureScreenshot('model-viewer', scenarioName, dimensions, path.join(scenarioOutputDirectory, 'model-viewer.png'));
const analysisResults = await this.analyze(screenshot, goldens, scenario, dimensions, analysisThresholds);
const scenarioRecord = Object.assign({ analysisResults }, scenario);
console.log(`\nš¾ Recording analysis`);
await fs.writeFile(path.join(outputDirectory, scenarioName, 'analysis.json'), JSON.stringify(scenarioRecord));
analyzedScenarios.push(scenario);
}
console.log('š¾ Recording configuration');
const finalConfig = Object.assign({}, this.config, { scenarios: analyzedScenarios });
await fs.writeFile(path.join(outputDirectory, 'config.json'), JSON.stringify(finalConfig));
return scenarios;
}
async analyze(screenshot, goldens, scenario, dimensions, analysisThresholds) {
const analysisResults = [];
const { rootDirectory, outputDirectory } = this;
const { name: scenarioName, exclude } = scenario;
for (const goldenConfig of goldens) {
const { name: rendererName } = goldenConfig;
if (exclude != null && exclude.includes(rendererName)) {
continue;
}
console.log(`\nš Comparing <model-viewer> to ${goldenConfig.description}`);
const thresholdResults = [];
const goldenPath = path.join(rootDirectory, 'goldens', scenarioName, goldenConfig.file);
const golden = await fs.readFile(goldenPath);
const screenshotImage = PNG.sync.read(screenshot).data;
const goldenImage = PNG.sync.read(golden).data;
const comparator = new ImageComparator(screenshotImage, goldenImage, dimensions);
await fs.writeFile(path.join(outputDirectory, scenarioName, goldenConfig.file), golden);
for (const threshold of analysisThresholds) {
console.log(`\n š Using threshold ${threshold.toFixed(1)}`);
const { analysis } = comparator.analyze(threshold);
const { matchingRatio, averageDistanceRatio, mismatchingAverageDistanceRatio } = analysis;
thresholdResults.push(analysis);
console.log(` š Matching pixels: ${(matchingRatio * 100).toFixed(2)}%`);
console.log(` š Mean color distance: ${(averageDistanceRatio * 100).toFixed(2)}%`);
console.log(` š Mean color distance (mismatching pixels only): ${(mismatchingAverageDistanceRatio * 100).toFixed(2)}%`);
}
analysisResults.push(thresholdResults);
}
return analysisResults;
}
async captureScreenshot(renderer, scenarioName, dimensions, outputPath = path.join(this.outputDirectory, 'model-viewer.png')) {
const devicePixelRatio = 2;
const scaledWidth = dimensions.width;
const scaledHeight = dimensions.height;
const rendererConfig = this[$configReader].rendererConfig(renderer);
if (rendererConfig == null) {
console.log(`ā ļø Renderer "${renderer}" is not configured. Did you add it to the test config?`);
return;
}
console.log(`š Launching browser`);
const browser = await puppeteer.launch({
defaultViewport: {
width: scaledWidth,
height: scaledHeight,
deviceScaleFactor: devicePixelRatio
},
headless: false
});
const page = await browser.newPage();
const url = `${this.baseUrl}?hide-ui&config=../../config.json&scenario=${encodeURIComponent(scenarioName)}`;
page.on('error', (error) => {
console.log(`šØ ${error}`);
});
page.on('console', async (message) => {
const args = await Promise.all(message.args().map((arg) => arg.jsonValue()));
if (args.length) {
console.log(`ā”ļø`, ...args);
}
});
console.log(`šŗ Navigating to ${url}`);
await page.goto(url);
console.log(`š Rendering ${scenarioName} with ${rendererConfig.description}`);
await page.evaluate(async () => {
const modelBecomesReady = self.modelLoaded ?
Promise.resolve() :
new Promise((resolve, reject) => {
const timeout = setTimeout(reject, 10000);
self.addEventListener('model-ready', () => {
clearTimeout(timeout);
resolve();
}, { once: true });
});
await modelBecomesReady;
});
console.log(`š¼ Capturing screenshot`);
try {
await fs.mkdir(this.outputDirectory);
}
catch (e) {
// Ignored...
}
const screenshot = await page.screenshot({ path: outputPath });
await browser.close();
return screenshot;
}
}
_a = $configReader;
//# sourceMappingURL=artifact-creator.js.map