UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

237 lines (180 loc) • 7.53 kB
/* * 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. */ const {promises: fs} = require('fs'); const puppeteer = require('puppeteer'); const path = require('path'); const {PNG} = require('pngjs'); const makeDir = require('make-dir'); import {ImageComparisonAnalysis, ImageComparator, ImageComparisonConfig, GoldenConfig, ScenarioConfig, Dimensions} from './common.js'; import {ConfigReader} from './config-reader.js'; const $configReader = Symbol('configReader'); export type AnalysisResults = Array<Array<ImageComparisonAnalysis>>; export interface ScenarioRecord extends ScenarioConfig { analysisResults: AnalysisResults; } export class ArtifactCreator { private[$configReader]: ConfigReader = new ConfigReader(this.config); constructor( protected config: ImageComparisonConfig, protected rootDirectory: string, protected baseUrl: string) { console.log('🌈 Preparing to capture screenshots for fidelity comparison'); } protected get outputDirectory(): string { return path.join(path.resolve(this.rootDirectory), 'results'); } protected get goldens(): Array<GoldenConfig> { return this.config.renderers.map( renderer => ({...renderer, file: `${renderer.name}-golden.png`})); } async captureAndAnalyzeScreenshots( scenarioWhitelist: Set<string>|null = null) { const {scenarios, analysisThresholds} = this.config; const analyzedScenarios: Array<ScenarioConfig> = []; 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: ImageComparisonConfig = Object.assign({}, this.config, {scenarios: analyzedScenarios}); await fs.writeFile( path.join(outputDirectory, 'config.json'), JSON.stringify(finalConfig)); return scenarios; } protected async analyze( screenshot: Buffer, goldens: Array<GoldenConfig>, scenario: ScenarioConfig, dimensions: Dimensions, analysisThresholds: Array<number>): Promise<AnalysisResults> { const analysisResults: 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: Array<ImageComparisonAnalysis> = []; 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: string, scenarioName: string, dimensions: Dimensions, outputPath: string = 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: any) => { console.log(`🚨 ${error}`); }); page.on('console', async (message: any) => { const args = await Promise.all(message.args().map((arg: any) => 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 as any).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; } }