Initial commit

This commit is contained in:
Yeicor
2024-01-29 21:48:33 +01:00
parent ff629bc006
commit 24d9af17ee
9 changed files with 202 additions and 118 deletions

5
.gitignore vendored
View File

@@ -5,4 +5,7 @@
/dist/
/.cache/
/.parcel-cache/
/.idea/
/.idea/
# TODO: Figure out if we want to keep a big default skybox image in the repo
/img/st_peters_square_night_8k.jpg

View File

@@ -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"
}
}

View File

@@ -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)
}
}

View File

@@ -1,5 +1,12 @@
body {
background: black;
html, body, model-viewer {
height: 100%;
width: 100%;
margin: 0;
overflow: hidden;
}
padding: 0;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<title>Yet Another CAD Viewer</title>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<link rel="stylesheet" type="text/css" href="./index.css">
<script type="module" src="./index.ts"></script>
</head>

View File

@@ -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`)
app.replaceModel(`https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF-Binary/Duck.glb`)

View File

@@ -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();
}
}

25
src/settings.ts Normal file
View File

@@ -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;
}
}
})

105
yarn.lock
View File

@@ -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"