diff --git a/.gitignore b/.gitignore index 2a01956..2888e41 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ /dist/ /.cache/ /.parcel-cache/ -/.idea/ \ No newline at end of file +/.idea/ + +# TODO: Figure out if we want to keep a big default skybox image in the repo +/img/st_peters_square_night_8k.jpg diff --git a/package.json b/package.json index 2308186..44b3095 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,13 @@ "build": "parcel build src/index.html" }, "dependencies": { - "stats.js": "^0.17.0", + "@google/model-viewer": "^3.4.0", "three": "^0.160.1", "three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo" }, "devDependencies": { "@types/three": "^0.160.0", + "buffer": "^5.5.0||^6.0.0", "parcel": "^2.11.0" } } diff --git a/src/app.ts b/src/app.ts index 1bc9fdc..cafd590 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,105 +1,52 @@ -import * as THREE from "three"; -import {Box3, Matrix4, Vector3} from "three"; -import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; -import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader"; +import {ModelViewerElement} from '@google/model-viewer'; +import {settings} from "./settings"; +import {Renderer} from "@google/model-viewer/lib/three-components/Renderer"; +import {$scene} from "@google/model-viewer/lib/model-viewer-base"; import {OrientationGizmo} from "./orientation"; -import * as Stats from "stats.js"; +import {$controls} from "@google/model-viewer/lib/features/controls"; +import {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene"; export class App { - renderer = new THREE.WebGLRenderer({antialias: true}); - //camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 1000); - camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.01, 1000); - private controls = new OrbitControls(this.camera, this.renderer.domElement); - // CAD has Z up, so rotate the scene to match - scene = new THREE.Scene(); - private helperGroup = new THREE.Group(); - private modelGroup = new THREE.Group(); - loader = new GLTFLoader(); - private gizmo = new OrientationGizmo(this.camera, this.controls); - private stats = new Stats(); + element: ModelViewerElement install() { - // Prepare camera and scene - //this.setupSceneHelpers(new THREE.Box3().setFromCenterAndSize(new THREE.Vector3(), new THREE.Vector3(10, 10, 10))); - // this.helperGroup.setRotationFromMatrix(this.threeToCad) - this.scene.add(this.helperGroup); - // this.modelGroup.setRotationFromMatrix(this.threeToCad) - this.scene.add(this.modelGroup); - // Set up renderer - document.body.appendChild(this.renderer.domElement); - this.renderer.setAnimationLoop(this._loop.bind(this)); - // On window resize, also resize the renderer - let onResize = () => { - this.renderer.setSize(window.innerWidth, window.innerHeight); - if (this.camera instanceof THREE.PerspectiveCamera) { - this.camera.aspect = window.innerWidth / window.innerHeight; - } else { - const aspect = window.innerWidth / window.innerHeight; - const frustumSize = 2 - this.camera.left = - frustumSize * aspect / 2; - this.camera.right = frustumSize * aspect / 2; - this.camera.top = frustumSize / 2; - this.camera.bottom = - frustumSize / 2; - } - this.camera.updateProjectionMatrix(); - }; - window.addEventListener('resize', onResize); - onResize() + this.element = new ModelViewerElement(); + this.element.setAttribute('alt', 'The CAD Viewer is not supported on this browser.'); + this.element.setAttribute('camera-controls', ''); + this.element.setAttribute('max-camera-orbit', 'Infinity 180deg auto'); + this.element.setAttribute('min-camera-orbit', '-Infinity 0deg auto'); + this.element.setAttribute('interaction-prompt', 'none'); // Quits selected views from gizmo + // this.element.setAttribute('auto-rotate', ''); // Messes with the gizmo (rotates model instead of camera) + if (settings.arModes) { + this.element.setAttribute('ar', ''); + this.element.setAttribute('ar-modes', settings.arModes); + } + if (settings.shadowIntensity) { + this.element.setAttribute('shadow-intensity', '1'); + } + if (settings.background) { + this.element.setAttribute('skybox-image', settings.background); + this.element.setAttribute('environment-image', settings.background); + } + console.log('ModelViewerElement', this.element) + document.body.appendChild(this.element); // Misc installation - this.gizmo.install(); - document.body.appendChild(this.stats.dom) - this.stats.dom.style.left = ''; - this.stats.dom.style.right = '0px'; - this.stats.dom.style.top = '120px'; - this.stats.showPanel(1); // 0: fps, 1: ms, 2: mb, 3+: custom + let scene: ModelScene = this.element[$scene]; + let gizmo = new OrientationGizmo(scene); + gizmo.install(); + function updateGizmo() { + gizmo.update(); + requestAnimationFrame(updateGizmo); + } + updateGizmo(); + // document.body.appendChild(this.stats.dom) + // this.stats.dom.style.left = ''; + // this.stats.dom.style.right = '0px'; + // this.stats.dom.style.top = '120px'; + // this.stats.showPanel(1); // 0: fps, 1: ms, 2: mb, 3+: custom } - private setupSceneHelpers(bb: Box3) { // The bounding box in three.js coordinates - this.helperGroup.clear(); - let center = bb.getCenter(new THREE.Vector3()); - this.helperGroup.applyMatrix4(new Matrix4().makeTranslation(center)) - let size = bb.getSize(new THREE.Vector3()); - console.log(center, size) - this.controls.target.set(center.x, center.y, center.z); - this.camera.position.set(center.x, center.y, center.z); - this.camera.position.x += size.x * 0.75; - this.camera.position.y += size.y * 0.5; - this.camera.position.z += size.z; - this.controls.update() - this.helperGroup.add(new THREE.HemisphereLight(0xffffff, 0x444444)) - let gridXZ = new THREE.GridHelper(1, 10); - gridXZ.applyMatrix4(new Matrix4().makeTranslation(new Vector3(0, -size.y / 2, 0))) - gridXZ.scale.set(size.x, 1, size.z) - this.helperGroup.add(gridXZ) - let gridXY = new THREE.GridHelper(1, 10); - gridXY.applyMatrix4(new Matrix4().makeRotationX(Math.PI / 2)) - gridXY.applyMatrix4(new Matrix4().makeTranslation(new Vector3(0, 0, -size.z / 2))) - gridXY.scale.set(size.x, 1, size.y) - this.helperGroup.add(gridXY) - let gridYZ = new THREE.GridHelper(1, 10); - gridYZ.applyMatrix4(new Matrix4().makeRotationZ(Math.PI / 2)) - gridYZ.applyMatrix4(new Matrix4().makeTranslation(new Vector3(-size.x / 2, 0, 0))) - // noinspection JSSuspiciousNameCombination - gridYZ.scale.set(size.y, 1, size.z) - this.helperGroup.add(gridYZ) - let axes = new THREE.AxesHelper(size.length() / 4); - axes.applyMatrix4(new THREE.Matrix4().makeRotationX(-Math.PI / 2)) // Y-up to Z-up (reference-only) - this.helperGroup.add(axes) - } - - addModel(url: string) { - this.loader.loadAsync(url, console.log).then((model) => { - this.modelGroup.add(model.scene) - this.setupSceneHelpers(new THREE.Box3().setFromObject(model.scene)); - }); - } - - _loop(time) { - this.stats.begin(); - this.controls.update(); - this.gizmo.update(); - this.renderer.render(this.scene, this.camera); - this.stats.end(); - this.stats.update(); + replaceModel(url: string) { + this.element.setAttribute('src', url) } } \ No newline at end of file diff --git a/src/index.css b/src/index.css index 1956751..22acc79 100644 --- a/src/index.css +++ b/src/index.css @@ -1,5 +1,12 @@ -body { - background: black; +html, body, model-viewer { + height: 100%; + width: 100%; margin: 0; - overflow: hidden; -} \ No newline at end of file + padding: 0; +} + +@media (prefers-color-scheme: dark) { + body { + background-color: #000; + } +} diff --git a/src/index.html b/src/index.html index 2c72c50..ab673ce 100644 --- a/src/index.html +++ b/src/index.html @@ -3,7 +3,7 @@ Yet Another CAD Viewer - + diff --git a/src/index.ts b/src/index.ts index 317d6ab..735aa8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ import {App} from "./app"; const app = new App() + app.install(); -app.addModel(`https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF-Binary/Duck.glb`) \ No newline at end of file + +app.replaceModel(`https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF-Binary/Duck.glb`) diff --git a/src/orientation.ts b/src/orientation.ts index 395ef0b..238bc4b 100644 --- a/src/orientation.ts +++ b/src/orientation.ts @@ -1,15 +1,15 @@ -import {Camera} from "three"; import * as OrientationGizmoRaw from "three-orientation-gizmo/src/OrientationGizmo"; -import THREE = require("three"); -import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; +import * as THREE from "three"; +import {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene"; window.THREE = THREE // HACK: Required for the gizmo to work export class OrientationGizmo { element: OrientationGizmoRaw - constructor(camera: Camera, controls: OrbitControls) { - this.element = new OrientationGizmoRaw(camera, { + constructor(scene: ModelScene) { + // noinspection SpellCheckingInspection + this.element = new OrientationGizmoRaw(scene.camera, { size: 120, bubbleSizePrimary: 12, bubbleSizeSeconday: 10, @@ -30,12 +30,16 @@ export class OrientationGizmo { this.element.bubbles[indexB].direction.copy(dirA); } // Append and listen for events - this.element.onAxisSelected = (axis) => { - let magnitude = camera.position.clone().sub(controls.target).length() + this.element.onAxisSelected = (axis: { direction: { x: any; y: any; z: any; }; }) => { + let lookFrom = scene.getCamera().position.clone(); + let lookAt = scene.getTarget().clone().add(scene.target.position); + let magnitude = lookFrom.clone().sub(lookAt).length() let direction = new THREE.Vector3(axis.direction.x, axis.direction.y, axis.direction.z); - direction.normalize(); - console.log(controls.target, direction, magnitude) - camera.position.copy(controls.target.clone().add(direction.multiplyScalar(magnitude))); + let newLookFrom = lookAt.clone().add(direction.clone().multiplyScalar(magnitude)); + //console.log("New camera position", newLookFrom) + scene.getCamera().position.copy(newLookFrom); + scene.getCamera().lookAt(lookAt); + scene.queueRender(); } } diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..04ed650 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,25 @@ +// @ts-ignore +import skyboxUrl from './../img/st_peters_square_night_8k.jpg'; + +export const settings = { + arModes: 'webxr scene-viewer quick-look', + shadowIntensity: 1, + background: skyboxUrl, +} + +// Auto-override any settings from the URL +const url = new URL(window.location.href); +url.searchParams.forEach((value, key) => { + if (key in settings) { + switch (typeof settings[key]) { + case 'boolean': + settings[key] = value === 'true'; + break; + case 'number': + settings[key] = Number(value); + break; + default: + settings[key] = value; + } + } +}) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ed8c06a..758817a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,6 +24,14 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@google/model-viewer@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@google/model-viewer/-/model-viewer-3.4.0.tgz#dd3fd098b85ae5953a93f8eeef0e62434a0e7cc0" + integrity sha512-ZGZktE+uy7CiNOSrFdR1bTZDe9nfel2h8gWiRY6D5Eit77RAzIRYnPlkdQBoLSo8nnYS0faL/Z1tKP7nntvoGg== + dependencies: + "@monogrid/gainmap-js" "^3.0.1" + lit "^2.7.2" + "@lezer/common@^1.0.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049" @@ -36,6 +44,18 @@ dependencies: "@lezer/common" "^1.0.0" +"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz#d693d972974a354034454ec1317eb6afd0b00312" + integrity sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g== + +"@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.3.tgz#25b4eece2592132845d303e091bad9b04cdcfe03" + integrity sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.0.0" + "@lmdb/lmdb-darwin-arm64@2.8.5": version "2.8.5" resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.8.5.tgz#895d8cb16a9d709ce5fedd8b60022903b875e08e" @@ -75,6 +95,13 @@ "@lezer/lr" "^1.0.0" json5 "^2.2.1" +"@monogrid/gainmap-js@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@monogrid/gainmap-js/-/gainmap-js-3.0.1.tgz#4094a8c0223398affd313ce4f31d23d9e1f211c0" + integrity sha512-ds5/oRGp6bW11W6iHHHAQiEFaBMUDTuszXHxZvkLH4YdcRd1eRFt925XDLcE/ucT3HhYq7rr1xS1Yjz26QOB6A== + dependencies: + promise-worker-transferable "^1.0.4" + "@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38" @@ -873,6 +900,11 @@ fflate "~0.6.10" meshoptimizer "~0.18.1" +"@types/trusted-types@^2.0.2": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/webxr@*": version "0.5.11" resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.11.tgz#e142a8fc99e939d7349262a7764a173e486e61d9" @@ -914,6 +946,11 @@ base-x@^3.0.8: dependencies: safe-buffer "^5.0.1" +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -936,6 +973,14 @@ browserslist@^4.6.6: node-releases "^2.0.14" update-browserslist-db "^1.0.13" +buffer@^5.5.0||^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1190,6 +1235,16 @@ htmlparser2@^7.1.1: domutils "^2.8.0" entities "^3.0.1" +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -1230,6 +1285,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-promise@^2.1.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1252,6 +1312,13 @@ json5@^2.2.0, json5@^2.2.1: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +lie@^3.0.2: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lightningcss-darwin-arm64@1.23.0: version "1.23.0" resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.23.0.tgz#11780f37158a458cead5e89202f74cd99b926e36" @@ -1319,6 +1386,31 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +lit-element@^3.3.0: + version "3.3.3" + resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.3.3.tgz#10bc19702b96ef5416cf7a70177255bfb17b3209" + integrity sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.1.0" + "@lit/reactive-element" "^1.3.0" + lit-html "^2.8.0" + +lit-html@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.8.0.tgz#96456a4bb4ee717b9a7d2f94562a16509d39bffa" + integrity sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q== + dependencies: + "@types/trusted-types" "^2.0.2" + +lit@^2.7.2: + version "2.8.0" + resolved "https://registry.yarnpkg.com/lit/-/lit-2.8.0.tgz#4d838ae03059bf9cafa06e5c61d8acc0081e974e" + integrity sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA== + dependencies: + "@lit/reactive-element" "^1.6.0" + lit-element "^3.3.0" + lit-html "^2.8.0" + lmdb@2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.8.5.tgz#ce191110c755c0951caa062722e300c703973837" @@ -1513,6 +1605,14 @@ posthtml@^0.16.4, posthtml@^0.16.5: posthtml-parser "^0.11.0" posthtml-render "^3.0.0" +promise-worker-transferable@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz#2c72861ba053e5ae42b487b4a83b1ed3ae3786e8" + integrity sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw== + dependencies: + is-promise "^2.1.0" + lie "^3.0.2" + react-error-overlay@6.0.9: version "6.0.9" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" @@ -1560,11 +1660,6 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== -stats.js@^0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/stats.js/-/stats.js-0.17.0.tgz#b1c3dc46d94498b578b7fd3985b81ace7131cc7d" - integrity sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw== - string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"