this repo has no description

Add PrettierJS

+7
.prettierrc.json
··· 1 + { 2 + "singleQuote": true, 3 + "useTabs": true, 4 + "tabWidth": 4, 5 + "printWidth": 100, 6 + "bracketSpacing": true 7 + }
+4 -5
README.md
··· 4 4 5 5 Viewer: [gltf-viewer.donmccurdy.com](https://gltf-viewer.donmccurdy.com/) 6 6 7 - 8 7 ![screenshot](https://user-images.githubusercontent.com/1848368/31580352-b7354096-b101-11e7-86d7-f07677835812.png) 9 8 10 9 ## Quickstart ··· 16 15 17 16 ## glTF 2.0 Resources 18 17 19 - - [THREE.GLTFLoader](https://threejs.org/docs/#examples/en/loaders/GLTFLoader) 20 - - [glTF 2.0 Specification](https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md) 21 - - [glTF 2.0 Sample Models](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/) 18 + - [THREE.GLTFLoader](https://threejs.org/docs/#examples/en/loaders/GLTFLoader) 19 + - [glTF 2.0 Specification](https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md) 20 + - [glTF 2.0 Sample Models](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/) 22 21 23 22 ## Known Issues 24 23 25 - - [ ] Limited drag-and-drop support in Safari. 24 + - [ ] Limited drag-and-drop support in Safari.
+1 -5
cors.json
··· 1 1 [ 2 2 { 3 3 "method": ["GET"], 4 - "origin": [ 5 - "https://*.donmccurdy.com", 6 - "http://localhost:*", 7 - "https://localhost:*" 8 - ], 4 + "origin": ["https://*.donmccurdy.com", "http://localhost:*", "https://localhost:*"], 9 5 "responseHeader": ["Content-Type"], 10 6 "maxAgeSeconds": 3600 11 7 }
+42 -30
index.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <head> 3 - <meta charset="utf-8"> 4 - <meta http-equiv="X-UA-Compatible" content="IE=edge"> 5 - <title>glTF Viewer</title> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 7 - <meta name="author" content="Don McCurdy"> 8 - <meta name="description" content="Drag-and-drop preview tool for glTF 2.0 3D models."> 9 - <link rel="canonical" href="https://gltf-viewer.donmccurdy.com/"> 10 - <link rel="shortcut icon" href="/favicon.ico"> 11 - <link rel="stylesheet" href="style.css"> 12 - <link href="https://fonts.googleapis.com/css?family=Raleway:300,400" rel="stylesheet"> 13 - <script defer type="module" src="src/app.js"></script> 3 + <meta charset="utf-8" /> 4 + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 5 + <title>glTF Viewer</title> 6 + <meta 7 + name="viewport" 8 + content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" 9 + /> 10 + <meta name="author" content="Don McCurdy" /> 11 + <meta name="description" content="Drag-and-drop preview tool for glTF 2.0 3D models." /> 12 + <link rel="canonical" href="https://gltf-viewer.donmccurdy.com/" /> 13 + <link rel="shortcut icon" href="/favicon.ico" /> 14 + <link rel="stylesheet" href="style.css" /> 15 + <link href="https://fonts.googleapis.com/css?family=Raleway:300,400" rel="stylesheet" /> 16 + <script defer type="module" src="src/app.js"></script> 14 17 </head> 15 18 <body> 16 - <header> 17 - <h1><a href="/">glTF Viewer</a></h1> 18 - </header> 19 - <main class="wrap"> 20 - <div class="dropzone"> 21 - <div class="placeholder"> 22 - <p>Drag glTF 2.0 file or folder here</p> 23 - </div> 24 - <div class="upload-btn"> 25 - <input type="file" name="file-input[]" id="file-input" multiple=""> 26 - <label for="file-input"> 27 - <svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" viewBox="0 0 20 17"><path d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"></path></svg> 28 - <span>Choose file</span> 29 - </label> 30 - </div> 31 - </div> 32 - <div class="spinner"></div> 33 - </main> 19 + <header> 20 + <h1><a href="/">glTF Viewer</a></h1> 21 + </header> 22 + <main class="wrap"> 23 + <div class="dropzone"> 24 + <div class="placeholder"> 25 + <p>Drag glTF 2.0 file or folder here</p> 26 + </div> 27 + <div class="upload-btn"> 28 + <input type="file" name="file-input[]" id="file-input" multiple="" /> 29 + <label for="file-input"> 30 + <svg 31 + xmlns="http://www.w3.org/2000/svg" 32 + width="20" 33 + height="17" 34 + viewBox="0 0 20 17" 35 + > 36 + <path 37 + d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z" 38 + ></path> 39 + </svg> 40 + <span>Choose file</span> 41 + </label> 42 + </div> 43 + </div> 44 + <div class="spinner"></div> 45 + </main> 34 46 </body>
+39 -38
package.json
··· 1 1 { 2 - "private": true, 3 - "version": "1.5.1", 4 - "description": "Preview glTF models using three.js and a drag-and-drop interface.", 5 - "author": "Don McCurdy <dm@donmccurdy.com> (https://www.donmccurdy.com)", 6 - "license": "MIT", 7 - "main": "public/app.js", 8 - "browserslist": [ 9 - ">1%", 10 - "not dead" 11 - ], 12 - "staticFiles": { 13 - "staticPath": [ 14 - { 15 - "staticPath": "assets", 16 - "staticOutDir": "assets" 17 - } 18 - ] 19 - }, 20 - "scripts": { 21 - "dev": "vite --port 3000", 22 - "build": "vite build", 23 - "clean": "rimraf dist/**", 24 - "test": "node test/gen_test.js", 25 - "deploy": "npm run build && vercel --local-config vercel.json --prod", 26 - "postversion": "git push && git push --tags" 27 - }, 28 - "dependencies": { 29 - "dat.gui": "^0.7.9", 30 - "gltf-validator": "^2.0.0-dev.3.9", 31 - "query-string": "^8.1.0", 32 - "simple-dropzone": "^0.8.3", 33 - "three": "^0.154.0", 34 - "vhtml": "^2.2.0" 35 - }, 36 - "devDependencies": { 37 - "rimraf": "^5.0.1", 38 - "vite": "^4.4.3" 39 - } 2 + "private": true, 3 + "version": "1.5.1", 4 + "description": "Preview glTF models using three.js and a drag-and-drop interface.", 5 + "author": "Don McCurdy <dm@donmccurdy.com> (https://www.donmccurdy.com)", 6 + "license": "MIT", 7 + "main": "public/app.js", 8 + "browserslist": [ 9 + ">1%", 10 + "not dead" 11 + ], 12 + "staticFiles": { 13 + "staticPath": [ 14 + { 15 + "staticPath": "assets", 16 + "staticOutDir": "assets" 17 + } 18 + ] 19 + }, 20 + "scripts": { 21 + "dev": "vite --port 3000", 22 + "build": "vite build", 23 + "clean": "rimraf dist/**", 24 + "test": "node test/gen_test.js", 25 + "deploy": "npm run build && vercel --local-config vercel.json --prod", 26 + "postversion": "git push && git push --tags" 27 + }, 28 + "dependencies": { 29 + "dat.gui": "^0.7.9", 30 + "gltf-validator": "^2.0.0-dev.3.9", 31 + "query-string": "^8.1.0", 32 + "simple-dropzone": "^0.8.3", 33 + "three": "^0.154.0", 34 + "vhtml": "^2.2.0" 35 + }, 36 + "devDependencies": { 37 + "prettier": "^3.0.3", 38 + "rimraf": "^5.0.1", 39 + "vite": "^4.4.3" 40 + } 40 41 }
+121 -130
src/app.js
··· 8 8 window.VIEWER = {}; 9 9 10 10 if (!(window.File && window.FileReader && window.FileList && window.Blob)) { 11 - console.error('The File APIs are not fully supported in this browser.'); 11 + console.error('The File APIs are not fully supported in this browser.'); 12 12 } else if (!WebGL.isWebGLAvailable()) { 13 - console.error('WebGL is not supported in this browser.'); 13 + console.error('WebGL is not supported in this browser.'); 14 14 } 15 15 16 16 class App { 17 + /** 18 + * @param {Element} el 19 + * @param {Location} location 20 + */ 21 + constructor(el, location) { 22 + const hash = location.hash ? queryString.parse(location.hash) : {}; 23 + this.options = { 24 + kiosk: Boolean(hash.kiosk), 25 + model: hash.model || '', 26 + preset: hash.preset || '', 27 + cameraPosition: hash.cameraPosition ? hash.cameraPosition.split(',').map(Number) : null, 28 + }; 17 29 18 - /** 19 - * @param {Element} el 20 - * @param {Location} location 21 - */ 22 - constructor (el, location) { 30 + this.el = el; 31 + this.viewer = null; 32 + this.viewerEl = null; 33 + this.spinnerEl = el.querySelector('.spinner'); 34 + this.dropEl = el.querySelector('.dropzone'); 35 + this.inputEl = el.querySelector('#file-input'); 36 + this.validator = new Validator(el); 23 37 24 - const hash = location.hash ? queryString.parse(location.hash) : {}; 25 - this.options = { 26 - kiosk: Boolean(hash.kiosk), 27 - model: hash.model || '', 28 - preset: hash.preset || '', 29 - cameraPosition: hash.cameraPosition 30 - ? hash.cameraPosition.split(',').map(Number) 31 - : null 32 - }; 38 + this.createDropzone(); 39 + this.hideSpinner(); 33 40 34 - this.el = el; 35 - this.viewer = null; 36 - this.viewerEl = null; 37 - this.spinnerEl = el.querySelector('.spinner'); 38 - this.dropEl = el.querySelector('.dropzone'); 39 - this.inputEl = el.querySelector('#file-input'); 40 - this.validator = new Validator(el); 41 + const options = this.options; 41 42 42 - this.createDropzone(); 43 - this.hideSpinner(); 43 + if (options.kiosk) { 44 + const headerEl = document.querySelector('header'); 45 + headerEl.style.display = 'none'; 46 + } 44 47 45 - const options = this.options; 48 + if (options.model) { 49 + this.view(options.model, '', new Map()); 50 + } 51 + } 46 52 47 - if (options.kiosk) { 48 - const headerEl = document.querySelector('header'); 49 - headerEl.style.display = 'none'; 50 - } 53 + /** 54 + * Sets up the drag-and-drop controller. 55 + */ 56 + createDropzone() { 57 + const dropCtrl = new SimpleDropzone(this.dropEl, this.inputEl); 58 + dropCtrl.on('drop', ({ files }) => this.load(files)); 59 + dropCtrl.on('dropstart', () => this.showSpinner()); 60 + dropCtrl.on('droperror', () => this.hideSpinner()); 61 + } 51 62 52 - if (options.model) { 53 - this.view(options.model, '', new Map()); 54 - } 55 - } 63 + /** 64 + * Sets up the view manager. 65 + * @return {Viewer} 66 + */ 67 + createViewer() { 68 + this.viewerEl = document.createElement('div'); 69 + this.viewerEl.classList.add('viewer'); 70 + this.dropEl.innerHTML = ''; 71 + this.dropEl.appendChild(this.viewerEl); 72 + this.viewer = new Viewer(this.viewerEl, this.options); 73 + return this.viewer; 74 + } 56 75 57 - /** 58 - * Sets up the drag-and-drop controller. 59 - */ 60 - createDropzone () { 61 - const dropCtrl = new SimpleDropzone(this.dropEl, this.inputEl); 62 - dropCtrl.on('drop', ({files}) => this.load(files)); 63 - dropCtrl.on('dropstart', () => this.showSpinner()); 64 - dropCtrl.on('droperror', () => this.hideSpinner()); 65 - } 76 + /** 77 + * Loads a fileset provided by user action. 78 + * @param {Map<string, File>} fileMap 79 + */ 80 + load(fileMap) { 81 + let rootFile; 82 + let rootPath; 83 + Array.from(fileMap).forEach(([path, file]) => { 84 + if (file.name.match(/\.(gltf|glb)$/)) { 85 + rootFile = file; 86 + rootPath = path.replace(file.name, ''); 87 + } 88 + }); 66 89 67 - /** 68 - * Sets up the view manager. 69 - * @return {Viewer} 70 - */ 71 - createViewer () { 72 - this.viewerEl = document.createElement('div'); 73 - this.viewerEl.classList.add('viewer'); 74 - this.dropEl.innerHTML = ''; 75 - this.dropEl.appendChild(this.viewerEl); 76 - this.viewer = new Viewer(this.viewerEl, this.options); 77 - return this.viewer; 78 - } 90 + if (!rootFile) { 91 + this.onError('No .gltf or .glb asset found.'); 92 + } 79 93 80 - /** 81 - * Loads a fileset provided by user action. 82 - * @param {Map<string, File>} fileMap 83 - */ 84 - load (fileMap) { 85 - let rootFile; 86 - let rootPath; 87 - Array.from(fileMap).forEach(([path, file]) => { 88 - if (file.name.match(/\.(gltf|glb)$/)) { 89 - rootFile = file; 90 - rootPath = path.replace(file.name, ''); 91 - } 92 - }); 94 + this.view(rootFile, rootPath, fileMap); 95 + } 93 96 94 - if (!rootFile) { 95 - this.onError('No .gltf or .glb asset found.'); 96 - } 97 + /** 98 + * Passes a model to the viewer, given file and resources. 99 + * @param {File|string} rootFile 100 + * @param {string} rootPath 101 + * @param {Map<string, File>} fileMap 102 + */ 103 + view(rootFile, rootPath, fileMap) { 104 + if (this.viewer) this.viewer.clear(); 97 105 98 - this.view(rootFile, rootPath, fileMap); 99 - } 106 + const viewer = this.viewer || this.createViewer(); 100 107 101 - /** 102 - * Passes a model to the viewer, given file and resources. 103 - * @param {File|string} rootFile 104 - * @param {string} rootPath 105 - * @param {Map<string, File>} fileMap 106 - */ 107 - view (rootFile, rootPath, fileMap) { 108 + const fileURL = typeof rootFile === 'string' ? rootFile : URL.createObjectURL(rootFile); 108 109 109 - if (this.viewer) this.viewer.clear(); 110 + const cleanup = () => { 111 + this.hideSpinner(); 112 + if (typeof rootFile === 'object') URL.revokeObjectURL(fileURL); 113 + }; 110 114 111 - const viewer = this.viewer || this.createViewer(); 115 + viewer 116 + .load(fileURL, rootPath, fileMap) 117 + .catch((e) => this.onError(e)) 118 + .then((gltf) => { 119 + // TODO: GLTFLoader parsing can fail on invalid files. Ideally, 120 + // we could run the validator either way. 121 + if (!this.options.kiosk) { 122 + this.validator.validate(fileURL, rootPath, fileMap, gltf); 123 + } 124 + cleanup(); 125 + }); 126 + } 112 127 113 - const fileURL = typeof rootFile === 'string' 114 - ? rootFile 115 - : URL.createObjectURL(rootFile); 116 - 117 - const cleanup = () => { 118 - this.hideSpinner(); 119 - if (typeof rootFile === 'object') URL.revokeObjectURL(fileURL); 120 - }; 121 - 122 - viewer 123 - .load(fileURL, rootPath, fileMap) 124 - .catch((e) => this.onError(e)) 125 - .then((gltf) => { 126 - // TODO: GLTFLoader parsing can fail on invalid files. Ideally, 127 - // we could run the validator either way. 128 - if (!this.options.kiosk) { 129 - this.validator.validate(fileURL, rootPath, fileMap, gltf); 130 - } 131 - cleanup(); 132 - }); 133 - } 134 - 135 - /** 136 - * @param {Error} error 137 - */ 138 - onError (error) { 139 - let message = (error||{}).message || error.toString(); 140 - if (message.match(/ProgressEvent/)) { 141 - message = 'Unable to retrieve this file. Check JS console and browser network tab.'; 142 - } else if (message.match(/Unexpected token/)) { 143 - message = `Unable to parse file content. Verify that this file is valid. Error: "${message}"`; 144 - } else if (error && error.target && error.target instanceof Image) { 145 - message = 'Missing texture: ' + error.target.src.split('/').pop(); 146 - } 147 - window.alert(message); 148 - console.error(error); 149 - } 128 + /** 129 + * @param {Error} error 130 + */ 131 + onError(error) { 132 + let message = (error || {}).message || error.toString(); 133 + if (message.match(/ProgressEvent/)) { 134 + message = 'Unable to retrieve this file. Check JS console and browser network tab.'; 135 + } else if (message.match(/Unexpected token/)) { 136 + message = `Unable to parse file content. Verify that this file is valid. Error: "${message}"`; 137 + } else if (error && error.target && error.target instanceof Image) { 138 + message = 'Missing texture: ' + error.target.src.split('/').pop(); 139 + } 140 + window.alert(message); 141 + console.error(error); 142 + } 150 143 151 - showSpinner () { 152 - this.spinnerEl.style.display = ''; 153 - } 144 + showSpinner() { 145 + this.spinnerEl.style.display = ''; 146 + } 154 147 155 - hideSpinner () { 156 - this.spinnerEl.style.display = 'none'; 157 - } 148 + hideSpinner() { 149 + this.spinnerEl.style.display = 'none'; 150 + } 158 151 } 159 152 160 153 document.body.innerHTML += Footer(); 161 154 162 155 document.addEventListener('DOMContentLoaded', () => { 156 + const app = new App(document.body, location); 163 157 164 - const app = new App(document.body, location); 158 + window.VIEWER.app = app; 165 159 166 - window.VIEWER.app = app; 167 - 168 - console.info('[glTF Viewer] Debugging data exported as `window.VIEWER`.'); 169 - 160 + console.info('[glTF Viewer] Debugging data exported as `window.VIEWER`.'); 170 161 });
+24 -16
src/components/footer.jsx
··· 3 3 4 4 /** @jsx vhtml */ 5 5 6 - export function Footer () { 7 - return ( 8 - <footer> 9 - <a class="item" target="_blank" href="https://threejs.org/"> 10 - three.js r{REVISION} 11 - </a> 6 + export function Footer() { 7 + return ( 8 + <footer> 9 + <a class="item" target="_blank" href="https://threejs.org/"> 10 + three.js r{REVISION} 11 + </a> 12 12 13 - <span class="separator" aria-hidden="true">|</span> 13 + <span class="separator" aria-hidden="true"> 14 + | 15 + </span> 14 16 15 - <a class="item" target="_blank" href="https://github.com/donmccurdy/three-gltf-viewer/issues/new"> 16 - help & feedback 17 - </a> 17 + <a 18 + class="item" 19 + target="_blank" 20 + href="https://github.com/donmccurdy/three-gltf-viewer/issues/new" 21 + > 22 + help & feedback 23 + </a> 18 24 19 - <span class="separator" aria-hidden="true">|</span> 25 + <span class="separator" aria-hidden="true"> 26 + | 27 + </span> 20 28 21 - <a class="item" target="_blank" href="https://github.com/donmccurdy/three-gltf-viewer"> 22 - github 23 - </a> 24 - </footer> 25 - ); 29 + <a class="item" target="_blank" href="https://github.com/donmccurdy/three-gltf-viewer"> 30 + github 31 + </a> 32 + </footer> 33 + ); 26 34 }
+98 -51
src/components/validator-report.jsx
··· 3 3 4 4 /** @jsx vhtml */ 5 5 6 - export function ValidatorReport ({info, validatorVersion, issues, errors, warnings, hints, infos}) { 7 - return ( 8 - <div class="report"> 9 - <h1>Validation report</h1> 10 - <ul> 11 - <li><b>Format:</b> glTF {info.version}</li> 12 - <li><b>Generator:</b> {info.generator}</li> 13 - {info?.extras?.title && <li><b>Title:</b> <span dangerouslySetInnerHTML={{__html: info.extras.title}} /></li>} 14 - {info?.extras?.author && <li><b>Author:</b> <span dangerouslySetInnerHTML={{__html: info.extras.author}} /></li>} 15 - {info?.extras?.license && <li><b>License:</b> <span dangerouslySetInnerHTML={{__html: info.extras.license}} /></li>} 16 - {info?.extras?.source && <li><b>Source:</b> <span dangerouslySetInnerHTML={{__html: info.extras.source}} /></li>} 17 - <li> 18 - <b>Stats:</b> 19 - <ul> 20 - <li>{info.drawCallCount || '0'} draw calls</li> 21 - <li>{info.animationCount || '0'} animations</li> 22 - <li>{info.materialCount || '0'} materials</li> 23 - <li>{info.totalVertexCount || '0'} vertices</li> 24 - <li>{info.totalTriangleCount || '0'} triangles</li> 25 - </ul> 26 - </li> 27 - <li> 28 - <b>Extensions:</b> 29 - <ul> 30 - {info.extensionsUsed?.length 31 - ? info.extensionsUsed.map((extension) => <li>{extension}</li>) 32 - : <li>None</li> 33 - } 34 - </ul> 35 - {info.extensionsUsed?.length && 36 - <p><i> 37 - NOTE: Extensions above are present in the model, but may or may not be recognized by this 38 - viewer. Any "UNSUPPORTED_EXTENSION" warnings below refer only to extensions that could not 39 - be scanned by the validation suite, and may still have rendered correctly. See:{' '} 40 - <a href="https://github.com/donmccurdy/three-gltf-viewer/issues/122" target="_blank">three-gltf-viewer#122</a>. 41 - </i></p> 42 - } 43 - </li> 44 - </ul> 45 - <hr/> 46 - <p> 47 - Report generated by 48 - <a href="https://github.com/KhronosGroup/glTF-Validator/">KhronosGroup/glTF-Validator</a> {validatorVersion}. 49 - </p> 50 - {issues.numErrors && <ValidatorTable messages={errors} color='#f44336' title='Error' />} 51 - {issues.numWarnings && <ValidatorTable messages={warnings} color='#f9a825' title='Warning' />} 52 - {issues.numHints && <ValidatorTable messages={hints} color='#8bc34a' title='Hint' />} 53 - {issues.numInfos && <ValidatorTable messages={infos} color='#2196f3' title='Info' />} 54 - </div> 55 - ); 56 - }; 6 + export function ValidatorReport({ 7 + info, 8 + validatorVersion, 9 + issues, 10 + errors, 11 + warnings, 12 + hints, 13 + infos, 14 + }) { 15 + return ( 16 + <div class="report"> 17 + <h1>Validation report</h1> 18 + <ul> 19 + <li> 20 + <b>Format:</b> glTF {info.version} 21 + </li> 22 + <li> 23 + <b>Generator:</b> {info.generator} 24 + </li> 25 + {info?.extras?.title && ( 26 + <li> 27 + <b>Title:</b>{' '} 28 + <span dangerouslySetInnerHTML={{ __html: info.extras.title }} /> 29 + </li> 30 + )} 31 + {info?.extras?.author && ( 32 + <li> 33 + <b>Author:</b>{' '} 34 + <span dangerouslySetInnerHTML={{ __html: info.extras.author }} /> 35 + </li> 36 + )} 37 + {info?.extras?.license && ( 38 + <li> 39 + <b>License:</b>{' '} 40 + <span dangerouslySetInnerHTML={{ __html: info.extras.license }} /> 41 + </li> 42 + )} 43 + {info?.extras?.source && ( 44 + <li> 45 + <b>Source:</b>{' '} 46 + <span dangerouslySetInnerHTML={{ __html: info.extras.source }} /> 47 + </li> 48 + )} 49 + <li> 50 + <b>Stats:</b> 51 + <ul> 52 + <li>{info.drawCallCount || '0'} draw calls</li> 53 + <li>{info.animationCount || '0'} animations</li> 54 + <li>{info.materialCount || '0'} materials</li> 55 + <li>{info.totalVertexCount || '0'} vertices</li> 56 + <li>{info.totalTriangleCount || '0'} triangles</li> 57 + </ul> 58 + </li> 59 + <li> 60 + <b>Extensions:</b> 61 + <ul> 62 + {info.extensionsUsed?.length ? ( 63 + info.extensionsUsed.map((extension) => <li>{extension}</li>) 64 + ) : ( 65 + <li>None</li> 66 + )} 67 + </ul> 68 + {info.extensionsUsed?.length && ( 69 + <p> 70 + <i> 71 + NOTE: Extensions above are present in the model, but may or may not 72 + be recognized by this viewer. Any "UNSUPPORTED_EXTENSION" warnings 73 + below refer only to extensions that could not be scanned by the 74 + validation suite, and may still have rendered correctly. See:{' '} 75 + <a 76 + href="https://github.com/donmccurdy/three-gltf-viewer/issues/122" 77 + target="_blank" 78 + > 79 + three-gltf-viewer#122 80 + </a> 81 + . 82 + </i> 83 + </p> 84 + )} 85 + </li> 86 + </ul> 87 + <hr /> 88 + <p> 89 + Report generated by 90 + <a href="https://github.com/KhronosGroup/glTF-Validator/"> 91 + KhronosGroup/glTF-Validator 92 + </a>{' '} 93 + {validatorVersion}. 94 + </p> 95 + {issues.numErrors && <ValidatorTable messages={errors} color="#f44336" title="Error" />} 96 + {issues.numWarnings && ( 97 + <ValidatorTable messages={warnings} color="#f9a825" title="Warning" /> 98 + )} 99 + {issues.numHints && <ValidatorTable messages={hints} color="#8bc34a" title="Hint" />} 100 + {issues.numInfos && <ValidatorTable messages={infos} color="#2196f3" title="Info" />} 101 + </div> 102 + ); 103 + }
+32 -24
src/components/validator-table.jsx
··· 2 2 3 3 /** @jsx vhtml */ 4 4 5 - export function ValidatorTable ({title, color, messages}) { 6 - return ( 7 - <table class="report-table"> 8 - <thead> 9 - <tr style={`background: ${color}`}> 10 - <th>{title}</th> 11 - <th>Message</th> 12 - <th>Pointer</th> 13 - </tr> 14 - </thead> 15 - <tbody> 16 - {messages.map(({code, message, pointer}) => { 17 - return ( 18 - <tr> 19 - <td><code>{code}</code></td> 20 - <td>{message}</td> 21 - <td><code>{pointer}</code></td> 22 - </tr> 23 - ); 24 - })} 25 - {messages.length === 0 && <tr><td colspan="3">No issues found.</td></tr>} 26 - </tbody> 27 - </table> 28 - ); 5 + export function ValidatorTable({ title, color, messages }) { 6 + return ( 7 + <table class="report-table"> 8 + <thead> 9 + <tr style={`background: ${color}`}> 10 + <th>{title}</th> 11 + <th>Message</th> 12 + <th>Pointer</th> 13 + </tr> 14 + </thead> 15 + <tbody> 16 + {messages.map(({ code, message, pointer }) => { 17 + return ( 18 + <tr> 19 + <td> 20 + <code>{code}</code> 21 + </td> 22 + <td>{message}</td> 23 + <td> 24 + <code>{pointer}</code> 25 + </td> 26 + </tr> 27 + ); 28 + })} 29 + {messages.length === 0 && ( 30 + <tr> 31 + <td colspan="3">No issues found.</td> 32 + </tr> 33 + )} 34 + </tbody> 35 + </table> 36 + ); 29 37 }
+30 -28
src/components/validator-toggle.jsx
··· 2 2 3 3 /** @jsx vhtml */ 4 4 5 - export function ValidatorToggle ({issues, reportError}) { 6 - let levelClassName = ''; 7 - let message = ''; 5 + export function ValidatorToggle({ issues, reportError }) { 6 + let levelClassName = ''; 7 + let message = ''; 8 8 9 - if (issues) { 10 - if (issues.numErrors) { 11 - message = `${issues.numErrors} errors.`; 12 - } else if (issues.numWarnings) { 13 - message = `${issues.numWarnings} warnings.`; 14 - } else if (issues.numHints) { 15 - message = `${issues.numHints} hints.`; 16 - } else if (issues.numInfos) { 17 - message = `${issues.numInfos} notes.`; 18 - } else { 19 - message = 'Model details'; 20 - } 21 - levelClassName = `level-${issues.maxSeverity}`; 22 - } else if (reportError) { 23 - message = `Validation could not run: ${reportError}.`; 24 - } else { 25 - message = 'Validation could not run.'; 26 - } 9 + if (issues) { 10 + if (issues.numErrors) { 11 + message = `${issues.numErrors} errors.`; 12 + } else if (issues.numWarnings) { 13 + message = `${issues.numWarnings} warnings.`; 14 + } else if (issues.numHints) { 15 + message = `${issues.numHints} hints.`; 16 + } else if (issues.numInfos) { 17 + message = `${issues.numInfos} notes.`; 18 + } else { 19 + message = 'Model details'; 20 + } 21 + levelClassName = `level-${issues.maxSeverity}`; 22 + } else if (reportError) { 23 + message = `Validation could not run: ${reportError}.`; 24 + } else { 25 + message = 'Validation could not run.'; 26 + } 27 27 28 - return ( 29 - <div className={`report-toggle ${levelClassName}`}> 30 - <div class="report-toggle-text">{message}</div> 31 - <div class="report-toggle-close" aria-label="Hide">&times;</div> 32 - </div> 33 - ); 34 - }; 28 + return ( 29 + <div className={`report-toggle ${levelClassName}`}> 30 + <div class="report-toggle-text">{message}</div> 31 + <div class="report-toggle-close" aria-label="Hide"> 32 + &times; 33 + </div> 34 + </div> 35 + ); 36 + }
+22 -22
src/environments.js
··· 1 1 export const environments = [ 2 - { 3 - id: '', 4 - name: 'None', 5 - path: null, 6 - }, 7 - { 8 - id: 'neutral', // THREE.RoomEnvironment 9 - name: 'Neutral', 10 - path: null, 11 - }, 12 - { 13 - id: 'venice-sunset', 14 - name: 'Venice Sunset', 15 - path: 'https://storage.googleapis.com/donmccurdy-static/venice_sunset_1k.exr', 16 - format: '.exr' 17 - }, 18 - { 19 - id: 'footprint-court', 20 - name: 'Footprint Court (HDR Labs)', 21 - path: 'https://storage.googleapis.com/donmccurdy-static/footprint_court_2k.exr', 22 - format: '.exr' 23 - } 2 + { 3 + id: '', 4 + name: 'None', 5 + path: null, 6 + }, 7 + { 8 + id: 'neutral', // THREE.RoomEnvironment 9 + name: 'Neutral', 10 + path: null, 11 + }, 12 + { 13 + id: 'venice-sunset', 14 + name: 'Venice Sunset', 15 + path: 'https://storage.googleapis.com/donmccurdy-static/venice_sunset_1k.exr', 16 + format: '.exr', 17 + }, 18 + { 19 + id: 'footprint-court', 20 + name: 'Footprint Court (HDR Labs)', 21 + path: 'https://storage.googleapis.com/donmccurdy-static/footprint_court_2k.exr', 22 + format: '.exr', 23 + }, 24 24 ];
+192 -185
src/validator.js
··· 1 1 import { LoaderUtils } from 'three'; 2 2 import { validateBytes } from 'gltf-validator'; 3 - import { ValidatorToggle } from './components/validator-toggle'; 3 + import { ValidatorToggle } from './components/validator-toggle'; 4 4 import { ValidatorReport } from './components/validator-report'; 5 5 6 6 const SEVERITY_MAP = ['Errors', 'Warnings', 'Infos', 'Hints']; 7 7 8 8 export class Validator { 9 + /** 10 + * @param {Element} el 11 + */ 12 + constructor(el) { 13 + this.el = el; 14 + this.report = null; 9 15 10 - /** 11 - * @param {Element} el 12 - */ 13 - constructor (el) { 14 - this.el = el; 15 - this.report = null; 16 + this.toggleEl = document.createElement('div'); 17 + this.toggleEl.classList.add('report-toggle-wrap', 'hidden'); 18 + this.el.appendChild(this.toggleEl); 19 + } 16 20 17 - this.toggleEl = document.createElement('div'); 18 - this.toggleEl.classList.add('report-toggle-wrap', 'hidden'); 19 - this.el.appendChild(this.toggleEl); 20 - } 21 + /** 22 + * Runs validation against the given file URL and extra resources. 23 + * @param {string} rootFile 24 + * @param {string} rootPath 25 + * @param {Map<string, File>} assetMap 26 + * @param {Object} response 27 + * @return {Promise} 28 + */ 29 + validate(rootFile, rootPath, assetMap, response) { 30 + // TODO: This duplicates a request of the three.js loader, and could 31 + // take advantage of THREE.Cache after r90. 32 + return fetch(rootFile) 33 + .then((response) => response.arrayBuffer()) 34 + .then((buffer) => 35 + validateBytes(new Uint8Array(buffer), { 36 + externalResourceFunction: (uri) => 37 + this.resolveExternalResource(uri, rootFile, rootPath, assetMap), 38 + }), 39 + ) 40 + .then((report) => this.setReport(report, response)) 41 + .catch((e) => this.setReportException(e)); 42 + } 21 43 22 - /** 23 - * Runs validation against the given file URL and extra resources. 24 - * @param {string} rootFile 25 - * @param {string} rootPath 26 - * @param {Map<string, File>} assetMap 27 - * @param {Object} response 28 - * @return {Promise} 29 - */ 30 - validate (rootFile, rootPath, assetMap, response) { 31 - // TODO: This duplicates a request of the three.js loader, and could 32 - // take advantage of THREE.Cache after r90. 33 - return fetch(rootFile) 34 - .then((response) => response.arrayBuffer()) 35 - .then((buffer) => validateBytes(new Uint8Array(buffer), { 36 - externalResourceFunction: (uri) => 37 - this.resolveExternalResource(uri, rootFile, rootPath, assetMap) 38 - })) 39 - .then((report) => this.setReport(report, response)) 40 - .catch((e) => this.setReportException(e)); 41 - } 44 + /** 45 + * Loads a resource (either locally or from the network) and returns it. 46 + * @param {string} uri 47 + * @param {string} rootFile 48 + * @param {string} rootPath 49 + * @param {Map<string, File>} assetMap 50 + * @return {Promise<Uint8Array>} 51 + */ 52 + resolveExternalResource(uri, rootFile, rootPath, assetMap) { 53 + const baseURL = LoaderUtils.extractUrlBase(rootFile); 54 + const normalizedURL = 55 + rootPath + 56 + decodeURI(uri) // validator applies URI encoding. 57 + .replace(baseURL, '') 58 + .replace(/^(\.?\/)/, ''); 42 59 43 - /** 44 - * Loads a resource (either locally or from the network) and returns it. 45 - * @param {string} uri 46 - * @param {string} rootFile 47 - * @param {string} rootPath 48 - * @param {Map<string, File>} assetMap 49 - * @return {Promise<Uint8Array>} 50 - */ 51 - resolveExternalResource (uri, rootFile, rootPath, assetMap) { 52 - const baseURL = LoaderUtils.extractUrlBase(rootFile); 53 - const normalizedURL = rootPath + decodeURI(uri) // validator applies URI encoding. 54 - .replace(baseURL, '') 55 - .replace(/^(\.?\/)/, ''); 60 + let objectURL; 56 61 57 - let objectURL; 62 + if (assetMap.has(normalizedURL)) { 63 + const object = assetMap.get(normalizedURL); 64 + objectURL = URL.createObjectURL(object); 65 + } 58 66 59 - if (assetMap.has(normalizedURL)) { 60 - const object = assetMap.get(normalizedURL); 61 - objectURL = URL.createObjectURL(object); 62 - } 67 + return fetch(objectURL || baseURL + uri) 68 + .then((response) => response.arrayBuffer()) 69 + .then((buffer) => { 70 + if (objectURL) URL.revokeObjectURL(objectURL); 71 + return new Uint8Array(buffer); 72 + }); 73 + } 63 74 64 - return fetch(objectURL || (baseURL + uri)) 65 - .then((response) => response.arrayBuffer()) 66 - .then((buffer) => { 67 - if (objectURL) URL.revokeObjectURL(objectURL); 68 - return new Uint8Array(buffer); 69 - }); 70 - } 75 + /** 76 + * @param {GLTFValidator.Report} report 77 + * @param {Object} response 78 + */ 79 + setReport(report, response) { 80 + report.generator = (report && report.info && report.info.generator) || ''; 81 + report.issues.maxSeverity = -1; 82 + SEVERITY_MAP.forEach((severity, index) => { 83 + if (report.issues[`num${severity}`] > 0 && report.issues.maxSeverity === -1) { 84 + report.issues.maxSeverity = index; 85 + } 86 + }); 87 + report.errors = report.issues.messages.filter((msg) => msg.severity === 0); 88 + report.warnings = report.issues.messages.filter((msg) => msg.severity === 1); 89 + report.infos = report.issues.messages.filter((msg) => msg.severity === 2); 90 + report.hints = report.issues.messages.filter((msg) => msg.severity === 3); 91 + groupMessages(report); 92 + this.report = report; 71 93 72 - /** 73 - * @param {GLTFValidator.Report} report 74 - * @param {Object} response 75 - */ 76 - setReport (report, response) { 77 - report.generator = report && report.info && report.info.generator || ''; 78 - report.issues.maxSeverity = -1; 79 - SEVERITY_MAP.forEach((severity, index) => { 80 - if (report.issues[`num${severity}`] > 0 && report.issues.maxSeverity === -1) { 81 - report.issues.maxSeverity = index; 82 - } 83 - }); 84 - report.errors = report.issues.messages.filter((msg) => msg.severity === 0); 85 - report.warnings = report.issues.messages.filter((msg) => msg.severity === 1); 86 - report.infos = report.issues.messages.filter((msg) => msg.severity === 2); 87 - report.hints = report.issues.messages.filter((msg) => msg.severity === 3); 88 - groupMessages(report); 89 - this.report = report; 94 + this.setResponse(response); 90 95 91 - this.setResponse(response); 96 + this.toggleEl.innerHTML = ValidatorToggle(report); 97 + this.showToggle(); 98 + this.bindListeners(); 92 99 93 - this.toggleEl.innerHTML = ValidatorToggle(report); 94 - this.showToggle(); 95 - this.bindListeners(); 100 + function groupMessages(report) { 101 + const CODES = { 102 + ACCESSOR_NON_UNIT: { 103 + message: '{count} accessor elements not of unit length: 0. [AGGREGATED]', 104 + pointerCounts: {}, 105 + }, 106 + ACCESSOR_ANIMATION_INPUT_NON_INCREASING: { 107 + message: 108 + '{count} animation input accessor elements not in ascending order. [AGGREGATED]', 109 + pointerCounts: {}, 110 + }, 111 + }; 96 112 97 - function groupMessages (report) { 98 - const CODES = { 99 - ACCESSOR_NON_UNIT: { 100 - message: '{count} accessor elements not of unit length: 0. [AGGREGATED]', 101 - pointerCounts: {} 102 - }, 103 - ACCESSOR_ANIMATION_INPUT_NON_INCREASING: { 104 - message: '{count} animation input accessor elements not in ascending order. [AGGREGATED]', 105 - pointerCounts: {} 106 - } 107 - }; 113 + report.errors.forEach((message) => { 114 + if (!CODES[message.code]) return; 115 + if (!CODES[message.code].pointerCounts[message.pointer]) { 116 + CODES[message.code].pointerCounts[message.pointer] = 0; 117 + } 118 + CODES[message.code].pointerCounts[message.pointer]++; 119 + }); 120 + report.errors = report.errors.filter((message) => { 121 + if (!CODES[message.code]) return true; 122 + if (!CODES[message.code].pointerCounts[message.pointer]) return true; 123 + return CODES[message.code].pointerCounts[message.pointer] < 2; 124 + }); 125 + Object.keys(CODES).forEach((code) => { 126 + Object.keys(CODES[code].pointerCounts).forEach((pointer) => { 127 + report.errors.push({ 128 + code: code, 129 + pointer: pointer, 130 + message: CODES[code].message.replace( 131 + '{count}', 132 + CODES[code].pointerCounts[pointer], 133 + ), 134 + }); 135 + }); 136 + }); 137 + } 138 + } 108 139 109 - report.errors.forEach((message) => { 110 - if (!CODES[message.code]) return; 111 - if (!CODES[message.code].pointerCounts[message.pointer]) { 112 - CODES[message.code].pointerCounts[message.pointer] = 0; 113 - } 114 - CODES[message.code].pointerCounts[message.pointer]++; 115 - }); 116 - report.errors = report.errors.filter((message) => { 117 - if (!CODES[message.code]) return true; 118 - if (!CODES[message.code].pointerCounts[message.pointer]) return true; 119 - return CODES[message.code].pointerCounts[message.pointer] < 2; 120 - }); 121 - Object.keys(CODES).forEach((code) => { 122 - Object.keys(CODES[code].pointerCounts).forEach((pointer) => { 123 - report.errors.push({ 124 - code: code, 125 - pointer: pointer, 126 - message: CODES[code].message.replace('{count}', CODES[code].pointerCounts[pointer]) 127 - }); 128 - }); 129 - }); 130 - } 131 - } 132 - 133 - /** 134 - * @param {Object} response 135 - */ 136 - setResponse (response) { 137 - const json = response && response.parser && response.parser.json; 140 + /** 141 + * @param {Object} response 142 + */ 143 + setResponse(response) { 144 + const json = response && response.parser && response.parser.json; 138 145 139 - if (!json) return; 146 + if (!json) return; 140 147 141 - if (json.asset && json.asset.extras) { 142 - const extras = json.asset.extras; 143 - this.report.info.extras = {}; 144 - if (extras.author) { 145 - this.report.info.extras.author = linkify(escapeHTML(extras.author)); 146 - } 147 - if (extras.license) { 148 - this.report.info.extras.license = linkify(escapeHTML(extras.license)); 149 - } 150 - if (extras.source) { 151 - this.report.info.extras.source = linkify(escapeHTML(extras.source)); 152 - } 153 - if (extras.title) { 154 - this.report.info.extras.title = extras.title; 155 - } 156 - } 157 - } 148 + if (json.asset && json.asset.extras) { 149 + const extras = json.asset.extras; 150 + this.report.info.extras = {}; 151 + if (extras.author) { 152 + this.report.info.extras.author = linkify(escapeHTML(extras.author)); 153 + } 154 + if (extras.license) { 155 + this.report.info.extras.license = linkify(escapeHTML(extras.license)); 156 + } 157 + if (extras.source) { 158 + this.report.info.extras.source = linkify(escapeHTML(extras.source)); 159 + } 160 + if (extras.title) { 161 + this.report.info.extras.title = extras.title; 162 + } 163 + } 164 + } 158 165 159 - /** 160 - * @param {Error} e 161 - */ 162 - setReportException (e) { 163 - this.report = null; 164 - this.toggleEl.innerHTML = this.toggleTpl({reportError: e, level: 0}); 165 - this.showToggle(); 166 - this.bindListeners(); 167 - } 166 + /** 167 + * @param {Error} e 168 + */ 169 + setReportException(e) { 170 + this.report = null; 171 + this.toggleEl.innerHTML = this.toggleTpl({ reportError: e, level: 0 }); 172 + this.showToggle(); 173 + this.bindListeners(); 174 + } 168 175 169 - bindListeners () { 170 - const reportToggleBtn = this.toggleEl.querySelector('.report-toggle'); 171 - reportToggleBtn.addEventListener('click', () => this.showLightbox()); 176 + bindListeners() { 177 + const reportToggleBtn = this.toggleEl.querySelector('.report-toggle'); 178 + reportToggleBtn.addEventListener('click', () => this.showLightbox()); 172 179 173 - const reportToggleCloseBtn = this.toggleEl.querySelector('.report-toggle-close'); 174 - reportToggleCloseBtn.addEventListener('click', (e) => { 175 - this.hideToggle(); 176 - e.stopPropagation(); 177 - }); 178 - } 180 + const reportToggleCloseBtn = this.toggleEl.querySelector('.report-toggle-close'); 181 + reportToggleCloseBtn.addEventListener('click', (e) => { 182 + this.hideToggle(); 183 + e.stopPropagation(); 184 + }); 185 + } 179 186 180 - showToggle () { 181 - this.toggleEl.classList.remove('hidden'); 182 - } 187 + showToggle() { 188 + this.toggleEl.classList.remove('hidden'); 189 + } 183 190 184 - hideToggle () { 185 - this.toggleEl.classList.add('hidden'); 186 - } 191 + hideToggle() { 192 + this.toggleEl.classList.add('hidden'); 193 + } 187 194 188 - showLightbox () { 189 - if (!this.report) return; 190 - const tab = window.open('', '_blank'); 191 - tab.document.body.innerHTML = ` 192 - <!DOCTYPE html> 193 - <title>glTF 2.0 validation report</title> 194 - <link href="https://fonts.googleapis.com/css?family=Raleway:300,400" rel="stylesheet"> 195 - <link rel="stylesheet" href="{{location.protocol}}//{{location.host}}/style.css"> 196 - <style> 197 - body { overflow-y: auto; } 198 - html, body { background: #FFFFFF; } 199 - </style> 200 - ${ValidatorReport({...this.report, location})}`; 201 - } 195 + showLightbox() { 196 + if (!this.report) return; 197 + const tab = window.open('', '_blank'); 198 + tab.document.body.innerHTML = ` 199 + <!DOCTYPE html> 200 + <title>glTF 2.0 validation report</title> 201 + <link href="https://fonts.googleapis.com/css?family=Raleway:300,400" rel="stylesheet"> 202 + <link rel="stylesheet" href="{{location.protocol}}//{{location.host}}/style.css"> 203 + <style> 204 + body { overflow-y: auto; } 205 + html, body { background: #FFFFFF; } 206 + </style> 207 + ${ValidatorReport({ ...this.report, location })}`; 208 + } 202 209 } 203 210 204 211 function escapeHTML(unsafe) { 205 - return unsafe 206 - .replace(/&/g, '&amp;') 207 - .replace(/</g, '&lt;') 208 - .replace(/>/g, '&gt;') 209 - .replace(/"/g, '&quot;') 210 - .replace(/'/g, '&#039;'); 212 + return unsafe 213 + .replace(/&/g, '&amp;') 214 + .replace(/</g, '&lt;') 215 + .replace(/>/g, '&gt;') 216 + .replace(/"/g, '&quot;') 217 + .replace(/'/g, '&#039;'); 211 218 } 212 219 213 220 function linkify(text) { 214 - const urlPattern = /\b(?:https?):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim; 215 - const emailAddressPattern = /(([a-zA-Z0-9_\-\.]+)@[a-zA-Z_]+?(?:\.[a-zA-Z]{2,6}))+/gim; 216 - return text 217 - .replace(urlPattern, '<a target="_blank" href="$&">$&</a>') 218 - .replace(emailAddressPattern, '<a target="_blank" href="mailto:$1">$1</a>'); 221 + const urlPattern = /\b(?:https?):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim; 222 + const emailAddressPattern = /(([a-zA-Z0-9_\-\.]+)@[a-zA-Z_]+?(?:\.[a-zA-Z]{2,6}))+/gim; 223 + return text 224 + .replace(urlPattern, '<a target="_blank" href="$&">$&</a>') 225 + .replace(emailAddressPattern, '<a target="_blank" href="mailto:$1">$1</a>'); 219 226 }
+578 -597
src/viewer.js
··· 1 1 import { 2 - AmbientLight, 3 - AnimationMixer, 4 - AxesHelper, 5 - Box3, 6 - Cache, 7 - Color, 8 - DirectionalLight, 9 - GridHelper, 10 - HemisphereLight, 11 - LoaderUtils, 12 - LoadingManager, 13 - PMREMGenerator, 14 - PerspectiveCamera, 15 - PointsMaterial, 16 - REVISION, 17 - Scene, 18 - SkeletonHelper, 19 - Vector3, 20 - WebGLRenderer, 21 - LinearToneMapping, 22 - ACESFilmicToneMapping 2 + AmbientLight, 3 + AnimationMixer, 4 + AxesHelper, 5 + Box3, 6 + Cache, 7 + Color, 8 + DirectionalLight, 9 + GridHelper, 10 + HemisphereLight, 11 + LoaderUtils, 12 + LoadingManager, 13 + PMREMGenerator, 14 + PerspectiveCamera, 15 + PointsMaterial, 16 + REVISION, 17 + Scene, 18 + SkeletonHelper, 19 + Vector3, 20 + WebGLRenderer, 21 + LinearToneMapping, 22 + ACESFilmicToneMapping, 23 23 } from 'three'; 24 24 import Stats from 'three/examples/jsm/libs/stats.module.js'; 25 25 import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; ··· 37 37 const DEFAULT_CAMERA = '[default]'; 38 38 39 39 const MANAGER = new LoadingManager(); 40 - const THREE_PATH = `https://unpkg.com/three@0.${REVISION}.x` 41 - const DRACO_LOADER = new DRACOLoader( MANAGER ).setDecoderPath( `${THREE_PATH}/examples/jsm/libs/draco/gltf/` ); 42 - const KTX2_LOADER = new KTX2Loader( MANAGER ).setTranscoderPath( `${THREE_PATH}/examples/jsm/libs/basis/` ); 40 + const THREE_PATH = `https://unpkg.com/three@0.${REVISION}.x`; 41 + const DRACO_LOADER = new DRACOLoader(MANAGER).setDecoderPath( 42 + `${THREE_PATH}/examples/jsm/libs/draco/gltf/`, 43 + ); 44 + const KTX2_LOADER = new KTX2Loader(MANAGER).setTranscoderPath( 45 + `${THREE_PATH}/examples/jsm/libs/basis/`, 46 + ); 43 47 44 48 const IS_IOS = isIOS(); 45 49 46 - const Preset = {ASSET_GENERATOR: 'assetgenerator'}; 50 + const Preset = { ASSET_GENERATOR: 'assetgenerator' }; 47 51 48 52 Cache.enabled = true; 49 53 50 54 export class Viewer { 55 + constructor(el, options) { 56 + this.el = el; 57 + this.options = options; 51 58 52 - constructor (el, options) { 53 - this.el = el; 54 - this.options = options; 59 + this.lights = []; 60 + this.content = null; 61 + this.mixer = null; 62 + this.clips = []; 63 + this.gui = null; 55 64 56 - this.lights = []; 57 - this.content = null; 58 - this.mixer = null; 59 - this.clips = []; 60 - this.gui = null; 65 + this.state = { 66 + environment: 67 + options.preset === Preset.ASSET_GENERATOR 68 + ? environments.find((e) => e.id === 'footprint-court').name 69 + : environments[1].name, 70 + background: false, 71 + playbackSpeed: 1.0, 72 + actionStates: {}, 73 + camera: DEFAULT_CAMERA, 74 + wireframe: false, 75 + skeleton: false, 76 + grid: false, 77 + autoRotate: false, 61 78 62 - this.state = { 63 - environment: options.preset === Preset.ASSET_GENERATOR 64 - ? environments.find((e) => e.id === 'footprint-court').name 65 - : environments[1].name, 66 - background: false, 67 - playbackSpeed: 1.0, 68 - actionStates: {}, 69 - camera: DEFAULT_CAMERA, 70 - wireframe: false, 71 - skeleton: false, 72 - grid: false, 73 - autoRotate: false, 79 + // Lights 80 + punctualLights: true, 81 + exposure: 0.0, 82 + toneMapping: LinearToneMapping, 83 + ambientIntensity: 0.3, 84 + ambientColor: '#FFFFFF', 85 + directIntensity: 0.8 * Math.PI, // TODO(#116) 86 + directColor: '#FFFFFF', 87 + bgColor: '#191919', 74 88 75 - // Lights 76 - punctualLights: true, 77 - exposure: 0.0, 78 - toneMapping: LinearToneMapping, 79 - ambientIntensity: 0.3, 80 - ambientColor: '#FFFFFF', 81 - directIntensity: 0.8 * Math.PI, // TODO(#116) 82 - directColor: '#FFFFFF', 83 - bgColor: '#191919', 89 + pointSize: 1.0, 90 + }; 84 91 85 - pointSize: 1.0, 86 - }; 92 + this.prevTime = 0; 87 93 88 - this.prevTime = 0; 94 + this.stats = new Stats(); 95 + this.stats.dom.height = '48px'; 96 + [].forEach.call(this.stats.dom.children, (child) => (child.style.display = '')); 89 97 90 - this.stats = new Stats(); 91 - this.stats.dom.height = '48px'; 92 - [].forEach.call(this.stats.dom.children, (child) => (child.style.display = '')); 98 + this.backgroundColor = new Color(this.state.bgColor); 93 99 94 - this.backgroundColor = new Color(this.state.bgColor); 100 + this.scene = new Scene(); 101 + this.scene.background = this.backgroundColor; 95 102 96 - this.scene = new Scene(); 97 - this.scene.background = this.backgroundColor; 103 + const fov = options.preset === Preset.ASSET_GENERATOR ? (0.8 * 180) / Math.PI : 60; 104 + this.defaultCamera = new PerspectiveCamera( 105 + fov, 106 + el.clientWidth / el.clientHeight, 107 + 0.01, 108 + 1000, 109 + ); 110 + this.activeCamera = this.defaultCamera; 111 + this.scene.add(this.defaultCamera); 98 112 99 - const fov = options.preset === Preset.ASSET_GENERATOR 100 - ? 0.8 * 180 / Math.PI 101 - : 60; 102 - this.defaultCamera = new PerspectiveCamera( fov, el.clientWidth / el.clientHeight, 0.01, 1000 ); 103 - this.activeCamera = this.defaultCamera; 104 - this.scene.add( this.defaultCamera ); 113 + this.renderer = window.renderer = new WebGLRenderer({ antialias: true }); 114 + this.renderer.useLegacyLights = false; 115 + this.renderer.setClearColor(0xcccccc); 116 + this.renderer.setPixelRatio(window.devicePixelRatio); 117 + this.renderer.setSize(el.clientWidth, el.clientHeight); 105 118 106 - this.renderer = window.renderer = new WebGLRenderer({antialias: true}); 107 - this.renderer.useLegacyLights = false; 108 - this.renderer.setClearColor( 0xcccccc ); 109 - this.renderer.setPixelRatio( window.devicePixelRatio ); 110 - this.renderer.setSize( el.clientWidth, el.clientHeight ); 119 + this.pmremGenerator = new PMREMGenerator(this.renderer); 120 + this.pmremGenerator.compileEquirectangularShader(); 111 121 112 - this.pmremGenerator = new PMREMGenerator( this.renderer ); 113 - this.pmremGenerator.compileEquirectangularShader(); 122 + this.neutralEnvironment = this.pmremGenerator.fromScene(new RoomEnvironment()).texture; 114 123 115 - this.neutralEnvironment = this.pmremGenerator.fromScene( new RoomEnvironment() ).texture; 124 + this.controls = new OrbitControls(this.defaultCamera, this.renderer.domElement); 125 + this.controls.screenSpacePanning = true; 116 126 117 - this.controls = new OrbitControls( this.defaultCamera, this.renderer.domElement ); 118 - this.controls.screenSpacePanning = true; 127 + this.el.appendChild(this.renderer.domElement); 119 128 120 - this.el.appendChild(this.renderer.domElement); 129 + this.cameraCtrl = null; 130 + this.cameraFolder = null; 131 + this.animFolder = null; 132 + this.animCtrls = []; 133 + this.morphFolder = null; 134 + this.morphCtrls = []; 135 + this.skeletonHelpers = []; 136 + this.gridHelper = null; 137 + this.axesHelper = null; 121 138 122 - this.cameraCtrl = null; 123 - this.cameraFolder = null; 124 - this.animFolder = null; 125 - this.animCtrls = []; 126 - this.morphFolder = null; 127 - this.morphCtrls = []; 128 - this.skeletonHelpers = []; 129 - this.gridHelper = null; 130 - this.axesHelper = null; 139 + this.addAxesHelper(); 140 + this.addGUI(); 141 + if (options.kiosk) this.gui.close(); 131 142 132 - this.addAxesHelper(); 133 - this.addGUI(); 134 - if (options.kiosk) this.gui.close(); 143 + this.animate = this.animate.bind(this); 144 + requestAnimationFrame(this.animate); 145 + window.addEventListener('resize', this.resize.bind(this), false); 146 + } 135 147 136 - this.animate = this.animate.bind(this); 137 - requestAnimationFrame( this.animate ); 138 - window.addEventListener('resize', this.resize.bind(this), false); 139 - } 148 + animate(time) { 149 + requestAnimationFrame(this.animate); 140 150 141 - animate (time) { 151 + const dt = (time - this.prevTime) / 1000; 142 152 143 - requestAnimationFrame( this.animate ); 153 + this.controls.update(); 154 + this.stats.update(); 155 + this.mixer && this.mixer.update(dt); 156 + this.render(); 144 157 145 - const dt = (time - this.prevTime) / 1000; 158 + this.prevTime = time; 159 + } 146 160 147 - this.controls.update(); 148 - this.stats.update(); 149 - this.mixer && this.mixer.update(dt); 150 - this.render(); 161 + render() { 162 + this.renderer.render(this.scene, this.activeCamera); 163 + if (this.state.grid) { 164 + this.axesCamera.position.copy(this.defaultCamera.position); 165 + this.axesCamera.lookAt(this.axesScene.position); 166 + this.axesRenderer.render(this.axesScene, this.axesCamera); 167 + } 168 + } 151 169 152 - this.prevTime = time; 170 + resize() { 171 + const { clientHeight, clientWidth } = this.el.parentElement; 153 172 154 - } 173 + this.defaultCamera.aspect = clientWidth / clientHeight; 174 + this.defaultCamera.updateProjectionMatrix(); 175 + this.renderer.setSize(clientWidth, clientHeight); 155 176 156 - render () { 177 + this.axesCamera.aspect = this.axesDiv.clientWidth / this.axesDiv.clientHeight; 178 + this.axesCamera.updateProjectionMatrix(); 179 + this.axesRenderer.setSize(this.axesDiv.clientWidth, this.axesDiv.clientHeight); 180 + } 157 181 158 - this.renderer.render( this.scene, this.activeCamera ); 159 - if (this.state.grid) { 160 - this.axesCamera.position.copy(this.defaultCamera.position) 161 - this.axesCamera.lookAt(this.axesScene.position) 162 - this.axesRenderer.render( this.axesScene, this.axesCamera ); 163 - } 164 - } 182 + load(url, rootPath, assetMap) { 183 + const baseURL = LoaderUtils.extractUrlBase(url); 165 184 166 - resize () { 185 + // Load. 186 + return new Promise((resolve, reject) => { 187 + // Intercept and override relative URLs. 188 + MANAGER.setURLModifier((url, path) => { 189 + // URIs in a glTF file may be escaped, or not. Assume that assetMap is 190 + // from an un-escaped source, and decode all URIs before lookups. 191 + // See: https://github.com/donmccurdy/three-gltf-viewer/issues/146 192 + const normalizedURL = 193 + rootPath + 194 + decodeURI(url) 195 + .replace(baseURL, '') 196 + .replace(/^(\.?\/)/, ''); 167 197 168 - const {clientHeight, clientWidth} = this.el.parentElement; 198 + if (assetMap.has(normalizedURL)) { 199 + const blob = assetMap.get(normalizedURL); 200 + const blobURL = URL.createObjectURL(blob); 201 + blobURLs.push(blobURL); 202 + return blobURL; 203 + } 169 204 170 - this.defaultCamera.aspect = clientWidth / clientHeight; 171 - this.defaultCamera.updateProjectionMatrix(); 172 - this.renderer.setSize(clientWidth, clientHeight); 205 + return (path || '') + url; 206 + }); 173 207 174 - this.axesCamera.aspect = this.axesDiv.clientWidth / this.axesDiv.clientHeight; 175 - this.axesCamera.updateProjectionMatrix(); 176 - this.axesRenderer.setSize(this.axesDiv.clientWidth, this.axesDiv.clientHeight); 177 - } 208 + const loader = new GLTFLoader(MANAGER) 209 + .setCrossOrigin('anonymous') 210 + .setDRACOLoader(DRACO_LOADER) 211 + .setKTX2Loader(KTX2_LOADER.detectSupport(this.renderer)) 212 + .setMeshoptDecoder(MeshoptDecoder); 178 213 179 - load ( url, rootPath, assetMap ) { 214 + const blobURLs = []; 180 215 181 - const baseURL = LoaderUtils.extractUrlBase(url); 216 + loader.load( 217 + url, 218 + (gltf) => { 219 + window.VIEWER.json = gltf; 182 220 183 - // Load. 184 - return new Promise((resolve, reject) => { 221 + const scene = gltf.scene || gltf.scenes[0]; 222 + const clips = gltf.animations || []; 185 223 186 - // Intercept and override relative URLs. 187 - MANAGER.setURLModifier((url, path) => { 224 + if (!scene) { 225 + // Valid, but not supported by this viewer. 226 + throw new Error( 227 + 'This model contains no scene, and cannot be viewed here. However,' + 228 + ' it may contain individual 3D resources.', 229 + ); 230 + } 188 231 189 - // URIs in a glTF file may be escaped, or not. Assume that assetMap is 190 - // from an un-escaped source, and decode all URIs before lookups. 191 - // See: https://github.com/donmccurdy/three-gltf-viewer/issues/146 192 - const normalizedURL = rootPath + decodeURI(url) 193 - .replace(baseURL, '') 194 - .replace(/^(\.?\/)/, ''); 232 + this.setContent(scene, clips); 195 233 196 - if (assetMap.has(normalizedURL)) { 197 - const blob = assetMap.get(normalizedURL); 198 - const blobURL = URL.createObjectURL(blob); 199 - blobURLs.push(blobURL); 200 - return blobURL; 201 - } 234 + blobURLs.forEach(URL.revokeObjectURL); 202 235 203 - return (path || '') + url; 236 + // See: https://github.com/google/draco/issues/349 237 + // DRACOLoader.releaseDecoderModule(); 204 238 205 - }); 239 + resolve(gltf); 240 + }, 241 + undefined, 242 + reject, 243 + ); 244 + }); 245 + } 206 246 207 - const loader = new GLTFLoader( MANAGER ) 208 - .setCrossOrigin('anonymous') 209 - .setDRACOLoader( DRACO_LOADER ) 210 - .setKTX2Loader( KTX2_LOADER.detectSupport( this.renderer ) ) 211 - .setMeshoptDecoder( MeshoptDecoder ); 247 + /** 248 + * @param {THREE.Object3D} object 249 + * @param {Array<THREE.AnimationClip} clips 250 + */ 251 + setContent(object, clips) { 252 + this.clear(); 212 253 213 - const blobURLs = []; 254 + object.updateMatrixWorld(); // donmccurdy/three-gltf-viewer#330 214 255 215 - loader.load(url, (gltf) => { 256 + const box = new Box3().setFromObject(object); 257 + const size = box.getSize(new Vector3()).length(); 258 + const center = box.getCenter(new Vector3()); 216 259 217 - window.VIEWER.json = gltf; 260 + this.controls.reset(); 218 261 219 - const scene = gltf.scene || gltf.scenes[0]; 220 - const clips = gltf.animations || []; 262 + object.position.x += object.position.x - center.x; 263 + object.position.y += object.position.y - center.y; 264 + object.position.z += object.position.z - center.z; 265 + this.controls.maxDistance = size * 10; 266 + this.defaultCamera.near = size / 100; 267 + this.defaultCamera.far = size * 100; 268 + this.defaultCamera.updateProjectionMatrix(); 221 269 222 - if (!scene) { 223 - // Valid, but not supported by this viewer. 224 - throw new Error( 225 - 'This model contains no scene, and cannot be viewed here. However,' 226 - + ' it may contain individual 3D resources.' 227 - ); 228 - } 270 + if (this.options.cameraPosition) { 271 + this.defaultCamera.position.fromArray(this.options.cameraPosition); 272 + this.defaultCamera.lookAt(new Vector3()); 273 + } else { 274 + this.defaultCamera.position.copy(center); 275 + this.defaultCamera.position.x += size / 2.0; 276 + this.defaultCamera.position.y += size / 5.0; 277 + this.defaultCamera.position.z += size / 2.0; 278 + this.defaultCamera.lookAt(center); 279 + } 229 280 230 - this.setContent(scene, clips); 281 + this.setCamera(DEFAULT_CAMERA); 231 282 232 - blobURLs.forEach(URL.revokeObjectURL); 283 + this.axesCamera.position.copy(this.defaultCamera.position); 284 + this.axesCamera.lookAt(this.axesScene.position); 285 + this.axesCamera.near = size / 100; 286 + this.axesCamera.far = size * 100; 287 + this.axesCamera.updateProjectionMatrix(); 288 + this.axesCorner.scale.set(size, size, size); 233 289 234 - // See: https://github.com/google/draco/issues/349 235 - // DRACOLoader.releaseDecoderModule(); 290 + this.controls.saveState(); 236 291 237 - resolve(gltf); 292 + this.scene.add(object); 293 + this.content = object; 238 294 239 - }, undefined, reject); 295 + this.state.punctualLights = true; 240 296 241 - }); 297 + this.content.traverse((node) => { 298 + if (node.isLight) { 299 + this.state.punctualLights = false; 300 + } 301 + }); 242 302 243 - } 303 + this.setClips(clips); 244 304 245 - /** 246 - * @param {THREE.Object3D} object 247 - * @param {Array<THREE.AnimationClip} clips 248 - */ 249 - setContent ( object, clips ) { 305 + this.updateLights(); 306 + this.updateGUI(); 307 + this.updateEnvironment(); 308 + this.updateDisplay(); 250 309 251 - this.clear(); 310 + window.VIEWER.scene = this.content; 252 311 253 - object.updateMatrixWorld(); // donmccurdy/three-gltf-viewer#330 254 - 255 - const box = new Box3().setFromObject(object); 256 - const size = box.getSize(new Vector3()).length(); 257 - const center = box.getCenter(new Vector3()); 312 + this.printGraph(this.content); 313 + } 258 314 259 - this.controls.reset(); 315 + printGraph(node) { 316 + console.group(' <' + node.type + '> ' + node.name); 317 + node.children.forEach((child) => this.printGraph(child)); 318 + console.groupEnd(); 319 + } 260 320 261 - object.position.x += (object.position.x - center.x); 262 - object.position.y += (object.position.y - center.y); 263 - object.position.z += (object.position.z - center.z); 264 - this.controls.maxDistance = size * 10; 265 - this.defaultCamera.near = size / 100; 266 - this.defaultCamera.far = size * 100; 267 - this.defaultCamera.updateProjectionMatrix(); 321 + /** 322 + * @param {Array<THREE.AnimationClip} clips 323 + */ 324 + setClips(clips) { 325 + if (this.mixer) { 326 + this.mixer.stopAllAction(); 327 + this.mixer.uncacheRoot(this.mixer.getRoot()); 328 + this.mixer = null; 329 + } 268 330 269 - if (this.options.cameraPosition) { 331 + this.clips = clips; 332 + if (!clips.length) return; 270 333 271 - this.defaultCamera.position.fromArray( this.options.cameraPosition ); 272 - this.defaultCamera.lookAt( new Vector3() ); 334 + this.mixer = new AnimationMixer(this.content); 335 + } 273 336 274 - } else { 337 + playAllClips() { 338 + this.clips.forEach((clip) => { 339 + this.mixer.clipAction(clip).reset().play(); 340 + this.state.actionStates[clip.name] = true; 341 + }); 342 + } 275 343 276 - this.defaultCamera.position.copy(center); 277 - this.defaultCamera.position.x += size / 2.0; 278 - this.defaultCamera.position.y += size / 5.0; 279 - this.defaultCamera.position.z += size / 2.0; 280 - this.defaultCamera.lookAt(center); 344 + /** 345 + * @param {string} name 346 + */ 347 + setCamera(name) { 348 + if (name === DEFAULT_CAMERA) { 349 + this.controls.enabled = true; 350 + this.activeCamera = this.defaultCamera; 351 + } else { 352 + this.controls.enabled = false; 353 + this.content.traverse((node) => { 354 + if (node.isCamera && node.name === name) { 355 + this.activeCamera = node; 356 + } 357 + }); 358 + } 359 + } 281 360 282 - } 361 + updateLights() { 362 + const state = this.state; 363 + const lights = this.lights; 283 364 284 - this.setCamera(DEFAULT_CAMERA); 365 + if (state.punctualLights && !lights.length) { 366 + this.addLights(); 367 + } else if (!state.punctualLights && lights.length) { 368 + this.removeLights(); 369 + } 285 370 286 - this.axesCamera.position.copy(this.defaultCamera.position) 287 - this.axesCamera.lookAt(this.axesScene.position) 288 - this.axesCamera.near = size / 100; 289 - this.axesCamera.far = size * 100; 290 - this.axesCamera.updateProjectionMatrix(); 291 - this.axesCorner.scale.set(size, size, size); 371 + this.renderer.toneMapping = Number(state.toneMapping); 372 + this.renderer.toneMappingExposure = Math.pow(2, state.exposure); 292 373 293 - this.controls.saveState(); 374 + if (lights.length === 2) { 375 + lights[0].intensity = state.ambientIntensity; 376 + lights[0].color.set(state.ambientColor); 377 + lights[1].intensity = state.directIntensity; 378 + lights[1].color.set(state.directColor); 379 + } 380 + } 294 381 295 - this.scene.add(object); 296 - this.content = object; 382 + addLights() { 383 + const state = this.state; 297 384 298 - this.state.punctualLights = true; 385 + if (this.options.preset === Preset.ASSET_GENERATOR) { 386 + const hemiLight = new HemisphereLight(); 387 + hemiLight.name = 'hemi_light'; 388 + this.scene.add(hemiLight); 389 + this.lights.push(hemiLight); 390 + return; 391 + } 299 392 300 - this.content.traverse((node) => { 301 - if (node.isLight) { 302 - this.state.punctualLights = false; 303 - } 304 - }); 393 + const light1 = new AmbientLight(state.ambientColor, state.ambientIntensity); 394 + light1.name = 'ambient_light'; 395 + this.defaultCamera.add(light1); 305 396 306 - this.setClips(clips); 397 + const light2 = new DirectionalLight(state.directColor, state.directIntensity); 398 + light2.position.set(0.5, 0, 0.866); // ~60º 399 + light2.name = 'main_light'; 400 + this.defaultCamera.add(light2); 307 401 308 - this.updateLights(); 309 - this.updateGUI(); 310 - this.updateEnvironment(); 311 - this.updateDisplay(); 402 + this.lights.push(light1, light2); 403 + } 312 404 313 - window.VIEWER.scene = this.content; 405 + removeLights() { 406 + this.lights.forEach((light) => light.parent.remove(light)); 407 + this.lights.length = 0; 408 + } 314 409 315 - this.printGraph(this.content); 410 + updateEnvironment() { 411 + const environment = environments.filter( 412 + (entry) => entry.name === this.state.environment, 413 + )[0]; 316 414 317 - } 415 + this.getCubeMapTexture(environment).then(({ envMap }) => { 416 + this.scene.environment = envMap; 417 + this.scene.background = this.state.background ? envMap : this.backgroundColor; 418 + }); 419 + } 318 420 319 - printGraph (node) { 421 + getCubeMapTexture(environment) { 422 + const { id, path } = environment; 320 423 321 - console.group(' <' + node.type + '> ' + node.name); 322 - node.children.forEach((child) => this.printGraph(child)); 323 - console.groupEnd(); 424 + // neutral (THREE.RoomEnvironment) 425 + if (id === 'neutral') { 426 + return Promise.resolve({ envMap: this.neutralEnvironment }); 427 + } 324 428 325 - } 429 + // none 430 + if (id === '') { 431 + return Promise.resolve({ envMap: null }); 432 + } 326 433 327 - /** 328 - * @param {Array<THREE.AnimationClip} clips 329 - */ 330 - setClips ( clips ) { 331 - if (this.mixer) { 332 - this.mixer.stopAllAction(); 333 - this.mixer.uncacheRoot(this.mixer.getRoot()); 334 - this.mixer = null; 335 - } 434 + return new Promise((resolve, reject) => { 435 + new EXRLoader().load( 436 + path, 437 + (texture) => { 438 + const envMap = this.pmremGenerator.fromEquirectangular(texture).texture; 439 + this.pmremGenerator.dispose(); 336 440 337 - this.clips = clips; 338 - if (!clips.length) return; 441 + resolve({ envMap }); 442 + }, 443 + undefined, 444 + reject, 445 + ); 446 + }); 447 + } 339 448 340 - this.mixer = new AnimationMixer( this.content ); 341 - } 449 + updateDisplay() { 450 + if (this.skeletonHelpers.length) { 451 + this.skeletonHelpers.forEach((helper) => this.scene.remove(helper)); 452 + } 342 453 343 - playAllClips () { 344 - this.clips.forEach((clip) => { 345 - this.mixer.clipAction(clip).reset().play(); 346 - this.state.actionStates[clip.name] = true; 347 - }); 348 - } 454 + traverseMaterials(this.content, (material) => { 455 + material.wireframe = this.state.wireframe; 349 456 350 - /** 351 - * @param {string} name 352 - */ 353 - setCamera ( name ) { 354 - if (name === DEFAULT_CAMERA) { 355 - this.controls.enabled = true; 356 - this.activeCamera = this.defaultCamera; 357 - } else { 358 - this.controls.enabled = false; 359 - this.content.traverse((node) => { 360 - if (node.isCamera && node.name === name) { 361 - this.activeCamera = node; 362 - } 363 - }); 364 - } 365 - } 457 + if (material instanceof PointsMaterial) { 458 + material.size = this.state.pointSize; 459 + } 460 + }); 366 461 367 - updateLights () { 368 - const state = this.state; 369 - const lights = this.lights; 462 + this.content.traverse((node) => { 463 + if (node.geometry && node.skeleton && this.state.skeleton) { 464 + const helper = new SkeletonHelper(node.skeleton.bones[0].parent); 465 + helper.material.linewidth = 3; 466 + this.scene.add(helper); 467 + this.skeletonHelpers.push(helper); 468 + } 469 + }); 370 470 371 - if (state.punctualLights && !lights.length) { 372 - this.addLights(); 373 - } else if (!state.punctualLights && lights.length) { 374 - this.removeLights(); 375 - } 471 + if (this.state.grid !== Boolean(this.gridHelper)) { 472 + if (this.state.grid) { 473 + this.gridHelper = new GridHelper(); 474 + this.axesHelper = new AxesHelper(); 475 + this.axesHelper.renderOrder = 999; 476 + this.axesHelper.onBeforeRender = (renderer) => renderer.clearDepth(); 477 + this.scene.add(this.gridHelper); 478 + this.scene.add(this.axesHelper); 479 + } else { 480 + this.scene.remove(this.gridHelper); 481 + this.scene.remove(this.axesHelper); 482 + this.gridHelper = null; 483 + this.axesHelper = null; 484 + this.axesRenderer.clear(); 485 + } 486 + } 376 487 377 - this.renderer.toneMapping = Number(state.toneMapping); 378 - this.renderer.toneMappingExposure = Math.pow(2, state.exposure); 488 + this.controls.autoRotate = this.state.autoRotate; 489 + } 379 490 380 - if (lights.length === 2) { 381 - lights[0].intensity = state.ambientIntensity; 382 - lights[0].color.set(state.ambientColor); 383 - lights[1].intensity = state.directIntensity; 384 - lights[1].color.set(state.directColor); 385 - } 386 - } 491 + updateBackground() { 492 + this.backgroundColor.set(this.state.bgColor); 493 + } 387 494 388 - addLights () { 389 - const state = this.state; 495 + /** 496 + * Adds AxesHelper. 497 + * 498 + * See: https://stackoverflow.com/q/16226693/1314762 499 + */ 500 + addAxesHelper() { 501 + this.axesDiv = document.createElement('div'); 502 + this.el.appendChild(this.axesDiv); 503 + this.axesDiv.classList.add('axes'); 390 504 391 - if (this.options.preset === Preset.ASSET_GENERATOR) { 392 - const hemiLight = new HemisphereLight(); 393 - hemiLight.name = 'hemi_light'; 394 - this.scene.add(hemiLight); 395 - this.lights.push(hemiLight); 396 - return; 397 - } 505 + const { clientWidth, clientHeight } = this.axesDiv; 398 506 399 - const light1 = new AmbientLight(state.ambientColor, state.ambientIntensity); 400 - light1.name = 'ambient_light'; 401 - this.defaultCamera.add( light1 ); 507 + this.axesScene = new Scene(); 508 + this.axesCamera = new PerspectiveCamera(50, clientWidth / clientHeight, 0.1, 10); 509 + this.axesScene.add(this.axesCamera); 402 510 403 - const light2 = new DirectionalLight(state.directColor, state.directIntensity); 404 - light2.position.set(0.5, 0, 0.866); // ~60º 405 - light2.name = 'main_light'; 406 - this.defaultCamera.add( light2 ); 511 + this.axesRenderer = new WebGLRenderer({ alpha: true }); 512 + this.axesRenderer.setPixelRatio(window.devicePixelRatio); 513 + this.axesRenderer.setSize(this.axesDiv.clientWidth, this.axesDiv.clientHeight); 407 514 408 - this.lights.push(light1, light2); 409 - } 515 + this.axesCamera.up = this.defaultCamera.up; 410 516 411 - removeLights () { 517 + this.axesCorner = new AxesHelper(5); 518 + this.axesScene.add(this.axesCorner); 519 + this.axesDiv.appendChild(this.axesRenderer.domElement); 520 + } 412 521 413 - this.lights.forEach((light) => light.parent.remove(light)); 414 - this.lights.length = 0; 522 + addGUI() { 523 + const gui = (this.gui = new GUI({ 524 + autoPlace: false, 525 + width: 260, 526 + hideable: true, 527 + })); 415 528 416 - } 529 + // Display controls. 530 + const dispFolder = gui.addFolder('Display'); 531 + const envBackgroundCtrl = dispFolder.add(this.state, 'background'); 532 + envBackgroundCtrl.onChange(() => this.updateEnvironment()); 533 + const autoRotateCtrl = dispFolder.add(this.state, 'autoRotate'); 534 + autoRotateCtrl.onChange(() => this.updateDisplay()); 535 + const wireframeCtrl = dispFolder.add(this.state, 'wireframe'); 536 + wireframeCtrl.onChange(() => this.updateDisplay()); 537 + const skeletonCtrl = dispFolder.add(this.state, 'skeleton'); 538 + skeletonCtrl.onChange(() => this.updateDisplay()); 539 + const gridCtrl = dispFolder.add(this.state, 'grid'); 540 + gridCtrl.onChange(() => this.updateDisplay()); 541 + dispFolder.add(this.controls, 'screenSpacePanning'); 542 + const pointSizeCtrl = dispFolder.add(this.state, 'pointSize', 1, 16); 543 + pointSizeCtrl.onChange(() => this.updateDisplay()); 544 + const bgColorCtrl = dispFolder.addColor(this.state, 'bgColor'); 545 + bgColorCtrl.onChange(() => this.updateBackground()); 417 546 418 - updateEnvironment () { 547 + // Lighting controls. 548 + const lightFolder = gui.addFolder('Lighting'); 549 + const envMapCtrl = lightFolder.add( 550 + this.state, 551 + 'environment', 552 + environments.map((env) => env.name), 553 + ); 554 + envMapCtrl.onChange(() => this.updateEnvironment()); 555 + [ 556 + lightFolder.add(this.state, 'toneMapping', { 557 + Linear: LinearToneMapping, 558 + 'ACES Filmic': ACESFilmicToneMapping, 559 + }), 560 + lightFolder.add(this.state, 'exposure', -10, 10, 0.01), 561 + lightFolder.add(this.state, 'punctualLights').listen(), 562 + lightFolder.add(this.state, 'ambientIntensity', 0, 2), 563 + lightFolder.addColor(this.state, 'ambientColor'), 564 + lightFolder.add(this.state, 'directIntensity', 0, 4), // TODO(#116) 565 + lightFolder.addColor(this.state, 'directColor'), 566 + ].forEach((ctrl) => ctrl.onChange(() => this.updateLights())); 419 567 420 - const environment = environments.filter((entry) => entry.name === this.state.environment)[0]; 568 + // Animation controls. 569 + this.animFolder = gui.addFolder('Animation'); 570 + this.animFolder.domElement.style.display = 'none'; 571 + const playbackSpeedCtrl = this.animFolder.add(this.state, 'playbackSpeed', 0, 1); 572 + playbackSpeedCtrl.onChange((speed) => { 573 + if (this.mixer) this.mixer.timeScale = speed; 574 + }); 575 + this.animFolder.add({ playAll: () => this.playAllClips() }, 'playAll'); 421 576 422 - this.getCubeMapTexture( environment ).then(( { envMap } ) => { 577 + // Morph target controls. 578 + this.morphFolder = gui.addFolder('Morph Targets'); 579 + this.morphFolder.domElement.style.display = 'none'; 423 580 424 - this.scene.environment = envMap; 425 - this.scene.background = this.state.background ? envMap : this.backgroundColor; 581 + // Camera controls. 582 + this.cameraFolder = gui.addFolder('Cameras'); 583 + this.cameraFolder.domElement.style.display = 'none'; 426 584 427 - }); 585 + // Stats. 586 + const perfFolder = gui.addFolder('Performance'); 587 + const perfLi = document.createElement('li'); 588 + this.stats.dom.style.position = 'static'; 589 + perfLi.appendChild(this.stats.dom); 590 + perfLi.classList.add('gui-stats'); 591 + perfFolder.__ul.appendChild(perfLi); 428 592 429 - } 593 + const guiWrap = document.createElement('div'); 594 + this.el.appendChild(guiWrap); 595 + guiWrap.classList.add('gui-wrap'); 596 + guiWrap.appendChild(gui.domElement); 597 + gui.open(); 598 + } 430 599 431 - getCubeMapTexture ( environment ) { 432 - const { id, path } = environment; 600 + updateGUI() { 601 + this.cameraFolder.domElement.style.display = 'none'; 433 602 434 - // neutral (THREE.RoomEnvironment) 435 - if ( id === 'neutral' ) { 603 + this.morphCtrls.forEach((ctrl) => ctrl.remove()); 604 + this.morphCtrls.length = 0; 605 + this.morphFolder.domElement.style.display = 'none'; 436 606 437 - return Promise.resolve( { envMap: this.neutralEnvironment } ); 607 + this.animCtrls.forEach((ctrl) => ctrl.remove()); 608 + this.animCtrls.length = 0; 609 + this.animFolder.domElement.style.display = 'none'; 438 610 439 - } 611 + const cameraNames = []; 612 + const morphMeshes = []; 613 + this.content.traverse((node) => { 614 + if (node.geometry && node.morphTargetInfluences) { 615 + morphMeshes.push(node); 616 + } 617 + if (node.isCamera) { 618 + node.name = node.name || `VIEWER__camera_${cameraNames.length + 1}`; 619 + cameraNames.push(node.name); 620 + } 621 + }); 440 622 441 - // none 442 - if ( id === '' ) { 623 + if (cameraNames.length) { 624 + this.cameraFolder.domElement.style.display = ''; 625 + if (this.cameraCtrl) this.cameraCtrl.remove(); 626 + const cameraOptions = [DEFAULT_CAMERA].concat(cameraNames); 627 + this.cameraCtrl = this.cameraFolder.add(this.state, 'camera', cameraOptions); 628 + this.cameraCtrl.onChange((name) => this.setCamera(name)); 629 + } 443 630 444 - return Promise.resolve( { envMap: null } ); 631 + if (morphMeshes.length) { 632 + this.morphFolder.domElement.style.display = ''; 633 + morphMeshes.forEach((mesh) => { 634 + if (mesh.morphTargetInfluences.length) { 635 + const nameCtrl = this.morphFolder.add( 636 + { name: mesh.name || 'Untitled' }, 637 + 'name', 638 + ); 639 + this.morphCtrls.push(nameCtrl); 640 + } 641 + for (let i = 0; i < mesh.morphTargetInfluences.length; i++) { 642 + const ctrl = this.morphFolder 643 + .add(mesh.morphTargetInfluences, i, 0, 1, 0.01) 644 + .listen(); 645 + Object.keys(mesh.morphTargetDictionary).forEach((key) => { 646 + if (key && mesh.morphTargetDictionary[key] === i) ctrl.name(key); 647 + }); 648 + this.morphCtrls.push(ctrl); 649 + } 650 + }); 651 + } 445 652 446 - } 653 + if (this.clips.length) { 654 + this.animFolder.domElement.style.display = ''; 655 + const actionStates = (this.state.actionStates = {}); 656 + this.clips.forEach((clip, clipIndex) => { 657 + clip.name = `${clipIndex + 1}. ${clip.name}`; 447 658 448 - return new Promise( ( resolve, reject ) => { 659 + // Autoplay the first clip. 660 + let action; 661 + if (clipIndex === 0) { 662 + actionStates[clip.name] = true; 663 + action = this.mixer.clipAction(clip); 664 + action.play(); 665 + } else { 666 + actionStates[clip.name] = false; 667 + } 449 668 450 - new EXRLoader() 451 - .load( path, ( texture ) => { 669 + // Play other clips when enabled. 670 + const ctrl = this.animFolder.add(actionStates, clip.name).listen(); 671 + ctrl.onChange((playAnimation) => { 672 + action = action || this.mixer.clipAction(clip); 673 + action.setEffectiveTimeScale(1); 674 + playAnimation ? action.play() : action.stop(); 675 + }); 676 + this.animCtrls.push(ctrl); 677 + }); 678 + } 679 + } 452 680 453 - const envMap = this.pmremGenerator.fromEquirectangular( texture ).texture; 454 - this.pmremGenerator.dispose(); 681 + clear() { 682 + if (!this.content) return; 455 683 456 - resolve( { envMap } ); 684 + this.scene.remove(this.content); 457 685 458 - }, undefined, reject ); 686 + // dispose geometry 687 + this.content.traverse((node) => { 688 + if (!node.geometry) return; 459 689 460 - }); 690 + node.geometry.dispose(); 691 + }); 461 692 462 - } 463 - 464 - updateDisplay () { 465 - if (this.skeletonHelpers.length) { 466 - this.skeletonHelpers.forEach((helper) => this.scene.remove(helper)); 467 - } 468 - 469 - traverseMaterials(this.content, (material) => { 470 - material.wireframe = this.state.wireframe; 471 - 472 - if (material instanceof PointsMaterial) { 473 - material.size = this.state.pointSize; 474 - } 475 - }); 476 - 477 - this.content.traverse((node) => { 478 - if (node.geometry && node.skeleton && this.state.skeleton) { 479 - const helper = new SkeletonHelper(node.skeleton.bones[0].parent); 480 - helper.material.linewidth = 3; 481 - this.scene.add(helper); 482 - this.skeletonHelpers.push(helper); 483 - } 484 - }); 485 - 486 - if (this.state.grid !== Boolean(this.gridHelper)) { 487 - if (this.state.grid) { 488 - this.gridHelper = new GridHelper(); 489 - this.axesHelper = new AxesHelper(); 490 - this.axesHelper.renderOrder = 999; 491 - this.axesHelper.onBeforeRender = (renderer) => renderer.clearDepth(); 492 - this.scene.add(this.gridHelper); 493 - this.scene.add(this.axesHelper); 494 - } else { 495 - this.scene.remove(this.gridHelper); 496 - this.scene.remove(this.axesHelper); 497 - this.gridHelper = null; 498 - this.axesHelper = null; 499 - this.axesRenderer.clear(); 500 - } 501 - } 693 + // dispose textures 694 + traverseMaterials(this.content, (material) => { 695 + for (const key in material) { 696 + if (key !== 'envMap' && material[key] && material[key].isTexture) { 697 + material[key].dispose(); 698 + } 699 + } 700 + }); 701 + } 702 + } 502 703 503 - this.controls.autoRotate = this.state.autoRotate; 504 - } 505 - 506 - updateBackground () { 507 - 508 - this.backgroundColor.set(this.state.bgColor); 509 - 510 - } 511 - 512 - /** 513 - * Adds AxesHelper. 514 - * 515 - * See: https://stackoverflow.com/q/16226693/1314762 516 - */ 517 - addAxesHelper () { 518 - this.axesDiv = document.createElement('div'); 519 - this.el.appendChild( this.axesDiv ); 520 - this.axesDiv.classList.add('axes'); 521 - 522 - const {clientWidth, clientHeight} = this.axesDiv; 523 - 524 - this.axesScene = new Scene(); 525 - this.axesCamera = new PerspectiveCamera( 50, clientWidth / clientHeight, 0.1, 10 ); 526 - this.axesScene.add( this.axesCamera ); 527 - 528 - this.axesRenderer = new WebGLRenderer( { alpha: true } ); 529 - this.axesRenderer.setPixelRatio( window.devicePixelRatio ); 530 - this.axesRenderer.setSize( this.axesDiv.clientWidth, this.axesDiv.clientHeight ); 531 - 532 - this.axesCamera.up = this.defaultCamera.up; 533 - 534 - this.axesCorner = new AxesHelper(5); 535 - this.axesScene.add( this.axesCorner ); 536 - this.axesDiv.appendChild(this.axesRenderer.domElement); 537 - } 538 - 539 - addGUI () { 540 - 541 - const gui = this.gui = new GUI({autoPlace: false, width: 260, hideable: true}); 542 - 543 - // Display controls. 544 - const dispFolder = gui.addFolder('Display'); 545 - const envBackgroundCtrl = dispFolder.add(this.state, 'background'); 546 - envBackgroundCtrl.onChange(() => this.updateEnvironment()); 547 - const autoRotateCtrl = dispFolder.add(this.state, 'autoRotate'); 548 - autoRotateCtrl.onChange(() => this.updateDisplay()); 549 - const wireframeCtrl = dispFolder.add(this.state, 'wireframe'); 550 - wireframeCtrl.onChange(() => this.updateDisplay()); 551 - const skeletonCtrl = dispFolder.add(this.state, 'skeleton'); 552 - skeletonCtrl.onChange(() => this.updateDisplay()); 553 - const gridCtrl = dispFolder.add(this.state, 'grid'); 554 - gridCtrl.onChange(() => this.updateDisplay()); 555 - dispFolder.add(this.controls, 'screenSpacePanning'); 556 - const pointSizeCtrl = dispFolder.add(this.state, 'pointSize', 1, 16); 557 - pointSizeCtrl.onChange(() => this.updateDisplay()); 558 - const bgColorCtrl = dispFolder.addColor(this.state, 'bgColor'); 559 - bgColorCtrl.onChange(() => this.updateBackground()); 560 - 561 - // Lighting controls. 562 - const lightFolder = gui.addFolder('Lighting'); 563 - const envMapCtrl = lightFolder.add(this.state, 'environment', environments.map((env) => env.name)); 564 - envMapCtrl.onChange(() => this.updateEnvironment()); 565 - [ 566 - lightFolder.add(this.state, 'toneMapping', {Linear: LinearToneMapping, 'ACES Filmic': ACESFilmicToneMapping}), 567 - lightFolder.add(this.state, 'exposure', -10, 10, 0.01), 568 - lightFolder.add(this.state, 'punctualLights').listen(), 569 - lightFolder.add(this.state, 'ambientIntensity', 0, 2), 570 - lightFolder.addColor(this.state, 'ambientColor'), 571 - lightFolder.add(this.state, 'directIntensity', 0, 4), // TODO(#116) 572 - lightFolder.addColor(this.state, 'directColor') 573 - ].forEach((ctrl) => ctrl.onChange(() => this.updateLights())); 574 - 575 - // Animation controls. 576 - this.animFolder = gui.addFolder('Animation'); 577 - this.animFolder.domElement.style.display = 'none'; 578 - const playbackSpeedCtrl = this.animFolder.add(this.state, 'playbackSpeed', 0, 1); 579 - playbackSpeedCtrl.onChange((speed) => { 580 - if (this.mixer) this.mixer.timeScale = speed; 581 - }); 582 - this.animFolder.add({playAll: () => this.playAllClips()}, 'playAll'); 583 - 584 - // Morph target controls. 585 - this.morphFolder = gui.addFolder('Morph Targets'); 586 - this.morphFolder.domElement.style.display = 'none'; 587 - 588 - // Camera controls. 589 - this.cameraFolder = gui.addFolder('Cameras'); 590 - this.cameraFolder.domElement.style.display = 'none'; 591 - 592 - // Stats. 593 - const perfFolder = gui.addFolder('Performance'); 594 - const perfLi = document.createElement('li'); 595 - this.stats.dom.style.position = 'static'; 596 - perfLi.appendChild(this.stats.dom); 597 - perfLi.classList.add('gui-stats'); 598 - perfFolder.__ul.appendChild( perfLi ); 599 - 600 - const guiWrap = document.createElement('div'); 601 - this.el.appendChild( guiWrap ); 602 - guiWrap.classList.add('gui-wrap'); 603 - guiWrap.appendChild(gui.domElement); 604 - gui.open(); 605 - 606 - } 607 - 608 - updateGUI () { 609 - this.cameraFolder.domElement.style.display = 'none'; 610 - 611 - this.morphCtrls.forEach((ctrl) => ctrl.remove()); 612 - this.morphCtrls.length = 0; 613 - this.morphFolder.domElement.style.display = 'none'; 614 - 615 - this.animCtrls.forEach((ctrl) => ctrl.remove()); 616 - this.animCtrls.length = 0; 617 - this.animFolder.domElement.style.display = 'none'; 618 - 619 - const cameraNames = []; 620 - const morphMeshes = []; 621 - this.content.traverse((node) => { 622 - if (node.geometry && node.morphTargetInfluences) { 623 - morphMeshes.push(node); 624 - } 625 - if (node.isCamera) { 626 - node.name = node.name || `VIEWER__camera_${cameraNames.length + 1}`; 627 - cameraNames.push(node.name); 628 - } 629 - }); 630 - 631 - if (cameraNames.length) { 632 - this.cameraFolder.domElement.style.display = ''; 633 - if (this.cameraCtrl) this.cameraCtrl.remove(); 634 - const cameraOptions = [DEFAULT_CAMERA].concat(cameraNames); 635 - this.cameraCtrl = this.cameraFolder.add(this.state, 'camera', cameraOptions); 636 - this.cameraCtrl.onChange((name) => this.setCamera(name)); 637 - } 638 - 639 - if (morphMeshes.length) { 640 - this.morphFolder.domElement.style.display = ''; 641 - morphMeshes.forEach((mesh) => { 642 - if (mesh.morphTargetInfluences.length) { 643 - const nameCtrl = this.morphFolder.add({name: mesh.name || 'Untitled'}, 'name'); 644 - this.morphCtrls.push(nameCtrl); 645 - } 646 - for (let i = 0; i < mesh.morphTargetInfluences.length; i++) { 647 - const ctrl = this.morphFolder.add(mesh.morphTargetInfluences, i, 0, 1, 0.01).listen(); 648 - Object.keys(mesh.morphTargetDictionary).forEach((key) => { 649 - if (key && mesh.morphTargetDictionary[key] === i) ctrl.name(key); 650 - }); 651 - this.morphCtrls.push(ctrl); 652 - } 653 - }); 654 - } 655 - 656 - if (this.clips.length) { 657 - this.animFolder.domElement.style.display = ''; 658 - const actionStates = this.state.actionStates = {}; 659 - this.clips.forEach((clip, clipIndex) => { 660 - clip.name = `${clipIndex + 1}. ${clip.name}`; 661 - 662 - // Autoplay the first clip. 663 - let action; 664 - if (clipIndex === 0) { 665 - actionStates[clip.name] = true; 666 - action = this.mixer.clipAction(clip); 667 - action.play(); 668 - } else { 669 - actionStates[clip.name] = false; 670 - } 671 - 672 - // Play other clips when enabled. 673 - const ctrl = this.animFolder.add(actionStates, clip.name).listen(); 674 - ctrl.onChange((playAnimation) => { 675 - action = action || this.mixer.clipAction(clip); 676 - action.setEffectiveTimeScale(1); 677 - playAnimation ? action.play() : action.stop(); 678 - }); 679 - this.animCtrls.push(ctrl); 680 - }); 681 - } 682 - } 683 - 684 - clear () { 685 - 686 - if ( !this.content ) return; 687 - 688 - this.scene.remove( this.content ); 689 - 690 - // dispose geometry 691 - this.content.traverse((node) => { 692 - 693 - if ( !node.geometry ) return; 694 - 695 - node.geometry.dispose(); 696 - 697 - } ); 698 - 699 - // dispose textures 700 - traverseMaterials( this.content, (material) => { 701 - 702 - for ( const key in material ) { 703 - 704 - if ( key !== 'envMap' && material[ key ] && material[ key ].isTexture ) { 705 - 706 - material[ key ].dispose(); 707 - 708 - } 709 - 710 - } 711 - 712 - } ); 713 - 714 - } 715 - 716 - }; 717 - 718 - function traverseMaterials (object, callback) { 719 - object.traverse((node) => { 720 - if (!node.geometry) return; 721 - const materials = Array.isArray(node.material) 722 - ? node.material 723 - : [node.material]; 724 - materials.forEach(callback); 725 - }); 704 + function traverseMaterials(object, callback) { 705 + object.traverse((node) => { 706 + if (!node.geometry) return; 707 + const materials = Array.isArray(node.material) ? node.material : [node.material]; 708 + materials.forEach(callback); 709 + }); 726 710 } 727 711 728 712 // https://stackoverflow.com/a/9039885/1314762 729 713 function isIOS() { 730 - return [ 731 - 'iPad Simulator', 732 - 'iPhone Simulator', 733 - 'iPod Simulator', 734 - 'iPad', 735 - 'iPhone', 736 - 'iPod' 737 - ].includes(navigator.platform) 738 - // iPad on iOS 13 detection 739 - || (navigator.userAgent.includes('Mac') && 'ontouchend' in document); 714 + return ( 715 + ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes( 716 + navigator.platform, 717 + ) || 718 + // iPad on iOS 13 detection 719 + (navigator.userAgent.includes('Mac') && 'ontouchend' in document) 720 + ); 740 721 }
+12 -12
vercel.json
··· 1 1 { 2 - "public": true, 3 - "routes": [ 4 - { 5 - "src": "/assets/(.*)", 6 - "headers": { "cache-control": "max-age=604800, public" }, 7 - "dest": "/assets/$1" 8 - }, 9 - { 10 - "src": "/(.*)", 11 - "dest": "/public/$1" 12 - } 13 - ] 2 + "public": true, 3 + "routes": [ 4 + { 5 + "src": "/assets/(.*)", 6 + "headers": { "cache-control": "max-age=604800, public" }, 7 + "dest": "/assets/$1" 8 + }, 9 + { 10 + "src": "/(.*)", 11 + "dest": "/public/$1" 12 + } 13 + ] 14 14 }
+58 -53
yarn.lock
··· 19 19 20 20 "@esbuild/darwin-arm64@0.18.11": 21 21 version "0.18.11" 22 - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.11.tgz#fcdcd2ef76ca656540208afdd84f284072f0d1f9" 22 + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.11.tgz" 23 23 integrity sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w== 24 24 25 25 "@esbuild/darwin-x64@0.18.11": ··· 114 114 115 115 "@isaacs/cliui@^8.0.2": 116 116 version "8.0.2" 117 - resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" 117 + resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" 118 118 integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== 119 119 dependencies: 120 120 string-width "^5.1.2" ··· 126 126 127 127 "@pkgjs/parseargs@^0.11.0": 128 128 version "0.11.0" 129 - resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" 129 + resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" 130 130 integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== 131 131 132 132 ansi-regex@^5.0.1: 133 133 version "5.0.1" 134 - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 134 + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" 135 135 integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 136 136 137 137 ansi-regex@^6.0.1: 138 138 version "6.0.1" 139 - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" 139 + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" 140 140 integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== 141 141 142 142 ansi-styles@^4.0.0: 143 143 version "4.3.0" 144 - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 144 + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" 145 145 integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 146 146 dependencies: 147 147 color-convert "^2.0.1" 148 148 149 149 ansi-styles@^6.1.0: 150 150 version "6.2.1" 151 - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" 151 + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" 152 152 integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== 153 153 154 154 balanced-match@^1.0.0: 155 155 version "1.0.2" 156 - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 156 + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" 157 157 integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 158 158 159 159 brace-expansion@^2.0.1: 160 160 version "2.0.1" 161 - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" 161 + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" 162 162 integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== 163 163 dependencies: 164 164 balanced-match "^1.0.0" 165 165 166 166 color-convert@^2.0.1: 167 167 version "2.0.1" 168 - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 168 + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" 169 169 integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 170 170 dependencies: 171 171 color-name "~1.1.4" 172 172 173 173 color-name@~1.1.4: 174 174 version "1.1.4" 175 - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 175 + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" 176 176 integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 177 177 178 178 cross-spawn@^7.0.0: 179 179 version "7.0.3" 180 - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" 180 + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" 181 181 integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== 182 182 dependencies: 183 183 path-key "^3.1.0" ··· 186 186 187 187 dat.gui@^0.7.9: 188 188 version "0.7.9" 189 - resolved "https://registry.yarnpkg.com/dat.gui/-/dat.gui-0.7.9.tgz#860cab06053b028e327820eabdf25a13cf07b17e" 189 + resolved "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz" 190 190 integrity sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ== 191 191 192 192 decode-uri-component@^0.4.1: 193 193 version "0.4.1" 194 - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz#2ac4859663c704be22bf7db760a1494a49ab2cc5" 194 + resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz" 195 195 integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ== 196 196 197 197 eastasianwidth@^0.2.0: 198 198 version "0.2.0" 199 - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" 199 + resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" 200 200 integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== 201 201 202 202 emoji-regex@^8.0.0: 203 203 version "8.0.0" 204 - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 204 + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" 205 205 integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 206 206 207 207 emoji-regex@^9.2.2: 208 208 version "9.2.2" 209 - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" 209 + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" 210 210 integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== 211 211 212 212 esbuild@^0.18.10: 213 213 version "0.18.11" 214 - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.11.tgz#cbf94dc3359d57f600a0dbf281df9b1d1b4a156e" 214 + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.18.11.tgz" 215 215 integrity sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA== 216 216 optionalDependencies: 217 217 "@esbuild/android-arm" "0.18.11" ··· 239 239 240 240 filter-obj@^5.1.0: 241 241 version "5.1.0" 242 - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-5.1.0.tgz#5bd89676000a713d7db2e197f660274428e524ed" 242 + resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz" 243 243 integrity sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng== 244 244 245 245 foreground-child@^3.1.0: 246 246 version "3.1.1" 247 - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" 247 + resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz" 248 248 integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== 249 249 dependencies: 250 250 cross-spawn "^7.0.0" ··· 252 252 253 253 fsevents@~2.3.2: 254 254 version "2.3.2" 255 - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 255 + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" 256 256 integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 257 257 258 258 glob@^10.2.5: 259 259 version "10.3.3" 260 - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.3.tgz#8360a4ffdd6ed90df84aa8d52f21f452e86a123b" 260 + resolved "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz" 261 261 integrity sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw== 262 262 dependencies: 263 263 foreground-child "^3.1.0" ··· 268 268 269 269 gltf-validator@^2.0.0-dev.3.9: 270 270 version "2.0.0-dev.3.9" 271 - resolved "https://registry.yarnpkg.com/gltf-validator/-/gltf-validator-2.0.0-dev.3.9.tgz#831cd4d95ce36bc8a2cc176b739c927012119e98" 271 + resolved "https://registry.npmjs.org/gltf-validator/-/gltf-validator-2.0.0-dev.3.9.tgz" 272 272 integrity sha512-9nPcAgYJwT6sbml7S3/tC+N/BkqTUSL1u8GcmUQLuwToLR0ZH8CF3i/BhVqDwlg7OmKS2GGjjEcnU/oMMeIQUQ== 273 273 274 274 is-fullwidth-code-point@^3.0.0: 275 275 version "3.0.0" 276 - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 276 + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" 277 277 integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 278 278 279 279 isexe@^2.0.0: 280 280 version "2.0.0" 281 - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 281 + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" 282 282 integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 283 283 284 284 jackspeak@^2.0.3: 285 285 version "2.2.1" 286 - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.1.tgz#655e8cf025d872c9c03d3eb63e8f0c024fef16a6" 286 + resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.1.tgz" 287 287 integrity sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw== 288 288 dependencies: 289 289 "@isaacs/cliui" "^8.0.2" ··· 292 292 293 293 "lru-cache@^9.1.1 || ^10.0.0": 294 294 version "10.0.0" 295 - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" 295 + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.0.tgz" 296 296 integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== 297 297 298 298 minimatch@^9.0.1: 299 299 version "9.0.3" 300 - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" 300 + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" 301 301 integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== 302 302 dependencies: 303 303 brace-expansion "^2.0.1" 304 304 305 305 "minipass@^5.0.0 || ^6.0.2 || ^7.0.0": 306 306 version "7.0.2" 307 - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.2.tgz#58a82b7d81c7010da5bd4b2c0c85ac4b4ec5131e" 307 + resolved "https://registry.npmjs.org/minipass/-/minipass-7.0.2.tgz" 308 308 integrity sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA== 309 309 310 310 nanoid@^3.3.6: 311 311 version "3.3.6" 312 - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" 312 + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz" 313 313 integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== 314 314 315 315 path-key@^3.1.0: 316 316 version "3.1.1" 317 - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 317 + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" 318 318 integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 319 319 320 320 path-scurry@^1.10.1: 321 321 version "1.10.1" 322 - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" 322 + resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz" 323 323 integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== 324 324 dependencies: 325 325 lru-cache "^9.1.1 || ^10.0.0" ··· 327 327 328 328 picocolors@^1.0.0: 329 329 version "1.0.0" 330 - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 330 + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" 331 331 integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 332 332 333 333 postcss@^8.4.25: 334 334 version "8.4.25" 335 - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.25.tgz#4a133f5e379eda7f61e906c3b1aaa9b81292726f" 335 + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz" 336 336 integrity sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw== 337 337 dependencies: 338 338 nanoid "^3.3.6" 339 339 picocolors "^1.0.0" 340 340 source-map-js "^1.0.2" 341 341 342 + prettier@^3.0.3: 343 + version "3.0.3" 344 + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" 345 + integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== 346 + 342 347 query-string@^8.1.0: 343 348 version "8.1.0" 344 - resolved "https://registry.yarnpkg.com/query-string/-/query-string-8.1.0.tgz#e7f95367737219544cd360a11a4f4ca03836e115" 349 + resolved "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz" 345 350 integrity sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw== 346 351 dependencies: 347 352 decode-uri-component "^0.4.1" ··· 350 355 351 356 rimraf@^5.0.1: 352 357 version "5.0.1" 353 - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.1.tgz#0881323ab94ad45fec7c0221f27ea1a142f3f0d0" 358 + resolved "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz" 354 359 integrity sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg== 355 360 dependencies: 356 361 glob "^10.2.5" 357 362 358 363 rollup@^3.25.2: 359 364 version "3.26.2" 360 - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.26.2.tgz#2e76a37606cb523fc9fef43e6f59c93f86d95e7c" 365 + resolved "https://registry.npmjs.org/rollup/-/rollup-3.26.2.tgz" 361 366 integrity sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA== 362 367 optionalDependencies: 363 368 fsevents "~2.3.2" 364 369 365 370 shebang-command@^2.0.0: 366 371 version "2.0.0" 367 - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 372 + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" 368 373 integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 369 374 dependencies: 370 375 shebang-regex "^3.0.0" 371 376 372 377 shebang-regex@^3.0.0: 373 378 version "3.0.0" 374 - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" 379 + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" 375 380 integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 376 381 377 382 signal-exit@^4.0.1: 378 383 version "4.0.2" 379 - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" 384 + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz" 380 385 integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== 381 386 382 387 simple-dropzone@^0.8.3: 383 388 version "0.8.3" 384 - resolved "https://registry.yarnpkg.com/simple-dropzone/-/simple-dropzone-0.8.3.tgz#1f46245b7ca8f3d840c3fbef7ab683ae45cf82d1" 389 + resolved "https://registry.npmjs.org/simple-dropzone/-/simple-dropzone-0.8.3.tgz" 385 390 integrity sha512-y0i8Tf1O9whdRh2NXE2a7y3U0wbQXTbPnRJeHD6XP/tWoLEIwqYxPtnI/Fst3mRASGqMD8hXaRFjgKsO1nbvcg== 386 391 dependencies: 387 392 zip-js-esm "^1.1.1" 388 393 389 394 source-map-js@^1.0.2: 390 395 version "1.0.2" 391 - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 396 + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" 392 397 integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 393 398 394 399 split-on-first@^3.0.0: 395 400 version "3.0.0" 396 - resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-3.0.0.tgz#f04959c9ea8101b9b0bbf35a61b9ebea784a23e7" 401 + resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz" 397 402 integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA== 398 403 399 404 "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: 400 405 version "4.2.3" 401 - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 406 + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" 402 407 integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 403 408 dependencies: 404 409 emoji-regex "^8.0.0" ··· 407 412 408 413 string-width@^5.0.1, string-width@^5.1.2: 409 414 version "5.1.2" 410 - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" 415 + resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" 411 416 integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== 412 417 dependencies: 413 418 eastasianwidth "^0.2.0" ··· 416 421 417 422 "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: 418 423 version "6.0.1" 419 - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 424 + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" 420 425 integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 421 426 dependencies: 422 427 ansi-regex "^5.0.1" 423 428 424 429 strip-ansi@^7.0.1: 425 430 version "7.1.0" 426 - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" 431 + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" 427 432 integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== 428 433 dependencies: 429 434 ansi-regex "^6.0.1" 430 435 431 436 three@^0.154.0: 432 437 version "0.154.0" 433 - resolved "https://registry.yarnpkg.com/three/-/three-0.154.0.tgz#dbef21e10fe6015ec283acc60d0eb58733991e27" 438 + resolved "https://registry.npmjs.org/three/-/three-0.154.0.tgz" 434 439 integrity sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug== 435 440 436 441 vhtml@^2.2.0: 437 442 version "2.2.0" 438 - resolved "https://registry.yarnpkg.com/vhtml/-/vhtml-2.2.0.tgz#369e6823ed6c32cbb9f6e33395bae7c65faa014c" 443 + resolved "https://registry.npmjs.org/vhtml/-/vhtml-2.2.0.tgz" 439 444 integrity sha512-TPXrXrxBOslRUVnlVkiAqhoXneiertIg86bdvzionrUYhEuiROvyPZNiiP6GIIJ2Q7oPNVyEtIx8gMAZZE9lCQ== 440 445 441 446 vite@^4.4.3: 442 447 version "4.4.3" 443 - resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.3.tgz#dfaf86f4cba3058bf2724e2e2c88254fb0f21a5a" 448 + resolved "https://registry.npmjs.org/vite/-/vite-4.4.3.tgz" 444 449 integrity sha512-IMnXQXXWgLi5brBQx/4WzDxdzW0X3pjO4nqFJAuNvwKtxzAmPzFE1wszW3VDpAGQJm3RZkm/brzRdyGsnwgJIA== 445 450 dependencies: 446 451 esbuild "^0.18.10" ··· 451 456 452 457 which@^2.0.1: 453 458 version "2.0.2" 454 - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 459 + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" 455 460 integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 456 461 dependencies: 457 462 isexe "^2.0.0" 458 463 459 464 "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": 460 465 version "7.0.0" 461 - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 466 + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" 462 467 integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 463 468 dependencies: 464 469 ansi-styles "^4.0.0" ··· 467 472 468 473 wrap-ansi@^8.1.0: 469 474 version "8.1.0" 470 - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" 475 + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" 471 476 integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== 472 477 dependencies: 473 478 ansi-styles "^6.1.0"