├── .github
└── FUNDING.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── cors.json
├── index.html
├── package.json
├── public
└── favicon.ico
├── src
├── app.js
├── components
│ ├── footer.jsx
│ ├── validator-report.jsx
│ ├── validator-table.jsx
│ └── validator-toggle.jsx
├── environments.js
├── validator.js
└── viewer.js
├── style.css
├── vercel.json
└── yarn.lock
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [donmccurdy]
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .cache
4 | .vercel
5 | .DS_Store
6 | npm-debug.log*
7 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "useTabs": true,
4 | "tabWidth": 4,
5 | "printWidth": 100,
6 | "bracketSpacing": true
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Don McCurdy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # glTF Viewer
2 |
3 | Preview glTF 2.0 models in WebGL using three.js and a drag-and-drop interface.
4 |
5 | Viewer: [gltf-viewer.donmccurdy.com](https://gltf-viewer.donmccurdy.com/)
6 |
7 | 
8 |
9 | ## Quickstart
10 |
11 | ```
12 | npm install
13 | npm run dev
14 | ```
15 |
16 | ## glTF 2.0 Resources
17 |
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/)
21 |
22 | ## Known Issues
23 |
24 | - [ ] Limited drag-and-drop support in Safari.
25 |
--------------------------------------------------------------------------------
/cors.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "method": ["GET"],
4 | "origin": ["https://*.donmccurdy.com", "http://localhost:*", "https://localhost:*"],
5 | "responseHeader": ["Content-Type"],
6 | "maxAgeSeconds": 3600
7 | }
8 | ]
9 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | glTF Viewer
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
Drag glTF 2.0 file or folder here
26 |
27 |
28 |
29 |
30 |
36 |
39 |
40 | Choose file
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
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 (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": "rm -rf dist/* || true",
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.10",
31 | "query-string": "^8.1.0",
32 | "simple-dropzone": "^0.8.3",
33 | "three": "^0.176.0",
34 | "vhtml": "^2.2.0"
35 | },
36 | "devDependencies": {
37 | "prettier": "^3.2.5",
38 | "vite": "^5.1.8"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/three-gltf-viewer/bd83b39339a525708387006aba976342f036713e/public/favicon.ico
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import WebGL from 'three/addons/capabilities/WebGL.js';
3 | import { Viewer } from './viewer.js';
4 | import { SimpleDropzone } from 'simple-dropzone';
5 | import { Validator } from './validator.js';
6 | import { Footer } from './components/footer';
7 | import queryString from 'query-string';
8 |
9 | window.THREE = THREE;
10 | window.VIEWER = {};
11 |
12 | if (!(window.File && window.FileReader && window.FileList && window.Blob)) {
13 | console.error('The File APIs are not fully supported in this browser.');
14 | } else if (!WebGL.isWebGL2Available()) {
15 | console.error('WebGL is not supported in this browser.');
16 | }
17 |
18 | class App {
19 | /**
20 | * @param {Element} el
21 | * @param {Location} location
22 | */
23 | constructor(el, location) {
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 ? hash.cameraPosition.split(',').map(Number) : null,
30 | };
31 |
32 | this.el = el;
33 | this.viewer = null;
34 | this.viewerEl = null;
35 | this.spinnerEl = el.querySelector('.spinner');
36 | this.dropEl = el.querySelector('.dropzone');
37 | this.inputEl = el.querySelector('#file-input');
38 | this.validator = new Validator(el);
39 |
40 | this.createDropzone();
41 | this.hideSpinner();
42 |
43 | const options = this.options;
44 |
45 | if (options.kiosk) {
46 | const headerEl = document.querySelector('header');
47 | headerEl.style.display = 'none';
48 | }
49 |
50 | if (options.model) {
51 | this.view(options.model, '', new Map());
52 | }
53 | }
54 |
55 | /**
56 | * Sets up the drag-and-drop controller.
57 | */
58 | createDropzone() {
59 | const dropCtrl = new SimpleDropzone(this.dropEl, this.inputEl);
60 | dropCtrl.on('drop', ({ files }) => this.load(files));
61 | dropCtrl.on('dropstart', () => this.showSpinner());
62 | dropCtrl.on('droperror', () => this.hideSpinner());
63 | }
64 |
65 | /**
66 | * Sets up the view manager.
67 | * @return {Viewer}
68 | */
69 | createViewer() {
70 | this.viewerEl = document.createElement('div');
71 | this.viewerEl.classList.add('viewer');
72 | this.dropEl.innerHTML = '';
73 | this.dropEl.appendChild(this.viewerEl);
74 | this.viewer = new Viewer(this.viewerEl, this.options);
75 | return this.viewer;
76 | }
77 |
78 | /**
79 | * Loads a fileset provided by user action.
80 | * @param {Map} fileMap
81 | */
82 | load(fileMap) {
83 | let rootFile;
84 | let rootPath;
85 | Array.from(fileMap).forEach(([path, file]) => {
86 | if (file.name.match(/\.(gltf|glb)$/)) {
87 | rootFile = file;
88 | rootPath = path.replace(file.name, '');
89 | }
90 | });
91 |
92 | if (!rootFile) {
93 | this.onError('No .gltf or .glb asset found.');
94 | }
95 |
96 | this.view(rootFile, rootPath, fileMap);
97 | }
98 |
99 | /**
100 | * Passes a model to the viewer, given file and resources.
101 | * @param {File|string} rootFile
102 | * @param {string} rootPath
103 | * @param {Map} fileMap
104 | */
105 | view(rootFile, rootPath, fileMap) {
106 | if (this.viewer) this.viewer.clear();
107 |
108 | const viewer = this.viewer || this.createViewer();
109 |
110 | const fileURL = typeof rootFile === 'string' ? rootFile : URL.createObjectURL(rootFile);
111 |
112 | const cleanup = () => {
113 | this.hideSpinner();
114 | if (typeof rootFile === 'object') URL.revokeObjectURL(fileURL);
115 | };
116 |
117 | viewer
118 | .load(fileURL, rootPath, fileMap)
119 | .catch((e) => this.onError(e))
120 | .then((gltf) => {
121 | // TODO: GLTFLoader parsing can fail on invalid files. Ideally,
122 | // we could run the validator either way.
123 | if (!this.options.kiosk) {
124 | this.validator.validate(fileURL, rootPath, fileMap, gltf);
125 | }
126 | cleanup();
127 | });
128 | }
129 |
130 | /**
131 | * @param {Error} error
132 | */
133 | onError(error) {
134 | let message = (error || {}).message || error.toString();
135 | if (message.match(/ProgressEvent/)) {
136 | message = 'Unable to retrieve this file. Check JS console and browser network tab.';
137 | } else if (message.match(/Unexpected token/)) {
138 | message = `Unable to parse file content. Verify that this file is valid. Error: "${message}"`;
139 | } else if (error && error.target && error.target instanceof Image) {
140 | message = 'Missing texture: ' + error.target.src.split('/').pop();
141 | }
142 | window.alert(message);
143 | console.error(error);
144 | }
145 |
146 | showSpinner() {
147 | this.spinnerEl.style.display = '';
148 | }
149 |
150 | hideSpinner() {
151 | this.spinnerEl.style.display = 'none';
152 | }
153 | }
154 |
155 | document.body.innerHTML += Footer();
156 |
157 | document.addEventListener('DOMContentLoaded', () => {
158 | const app = new App(document.body, location);
159 |
160 | window.VIEWER.app = app;
161 |
162 | console.info('[glTF Viewer] Debugging data exported as `window.VIEWER`.');
163 | });
164 |
--------------------------------------------------------------------------------
/src/components/footer.jsx:
--------------------------------------------------------------------------------
1 | import vhtml from 'vhtml';
2 | import { REVISION } from 'three';
3 |
4 | /** @jsx vhtml */
5 |
6 | export function Footer() {
7 | return (
8 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/validator-report.jsx:
--------------------------------------------------------------------------------
1 | import vhtml from 'vhtml';
2 | import { ValidatorTable } from './validator-table';
3 |
4 | /** @jsx vhtml */
5 |
6 | export function ValidatorReport({
7 | info,
8 | validatorVersion,
9 | issues,
10 | errors,
11 | warnings,
12 | hints,
13 | infos,
14 | }) {
15 | return (
16 |
17 |
Validation report
18 |
19 |
20 | Format: glTF {info.version}
21 |
22 |
23 | Generator: {info.generator}
24 |
25 | {info?.extras?.title && (
26 |
27 | Title: {' '}
28 |
29 |
30 | )}
31 | {info?.extras?.author && (
32 |
33 | Author: {' '}
34 |
35 |
36 | )}
37 | {info?.extras?.license && (
38 |
39 | License: {' '}
40 |
41 |
42 | )}
43 | {info?.extras?.source && (
44 |
45 | Source: {' '}
46 |
47 |
48 | )}
49 |
50 | Stats:
51 |
52 | {info.drawCallCount || '0'} draw calls
53 | {info.animationCount || '0'} animations
54 | {info.materialCount || '0'} materials
55 | {info.totalVertexCount || '0'} vertices
56 | {info.totalTriangleCount || '0'} triangles
57 |
58 |
59 |
60 | Extensions:
61 |
62 | {info.extensionsUsed?.length ? (
63 | info.extensionsUsed.map((extension) => {extension} )
64 | ) : (
65 | None
66 | )}
67 |
68 | {info.extensionsUsed?.length && (
69 |
70 |
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 |
79 | three-gltf-viewer#122
80 |
81 | .
82 |
83 |
84 | )}
85 |
86 |
87 |
88 |
89 | Report generated by
90 |
91 | KhronosGroup/glTF-Validator
92 | {' '}
93 | {validatorVersion}.
94 |
95 | {issues.numErrors &&
}
96 | {issues.numWarnings && (
97 |
98 | )}
99 | {issues.numHints &&
}
100 | {issues.numInfos &&
}
101 |
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/src/components/validator-table.jsx:
--------------------------------------------------------------------------------
1 | import vhtml from 'vhtml';
2 |
3 | /** @jsx vhtml */
4 |
5 | export function ValidatorTable({ title, color, messages }) {
6 | return (
7 |
8 |
9 |
10 | {title}
11 | Message
12 | Pointer
13 |
14 |
15 |
16 | {messages.map(({ code, message, pointer }) => {
17 | return (
18 |
19 |
20 | {code}
21 |
22 | {message}
23 |
24 | {pointer}
25 |
26 |
27 | );
28 | })}
29 | {messages.length === 0 && (
30 |
31 | No issues found.
32 |
33 | )}
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/validator-toggle.jsx:
--------------------------------------------------------------------------------
1 | import vhtml from 'vhtml';
2 |
3 | /** @jsx vhtml */
4 |
5 | export function ValidatorToggle({ issues, reportError }) {
6 | let levelClassName = '';
7 | let message = '';
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 | }
27 |
28 | return (
29 |
30 |
{message}
31 |
32 | ×
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/environments.js:
--------------------------------------------------------------------------------
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 | },
24 | ];
25 |
--------------------------------------------------------------------------------
/src/validator.js:
--------------------------------------------------------------------------------
1 | import { LoaderUtils } from 'three';
2 | import { validateBytes } from 'gltf-validator';
3 | import { ValidatorToggle } from './components/validator-toggle';
4 | import { ValidatorReport } from './components/validator-report';
5 |
6 | const SEVERITY_MAP = ['Errors', 'Warnings', 'Infos', 'Hints'];
7 |
8 | export class Validator {
9 | /**
10 | * @param {Element} el
11 | */
12 | constructor(el) {
13 | this.el = el;
14 | this.report = null;
15 |
16 | this.toggleEl = document.createElement('div');
17 | this.toggleEl.classList.add('report-toggle-wrap', 'hidden');
18 | this.el.appendChild(this.toggleEl);
19 | }
20 |
21 | /**
22 | * Runs validation against the given file URL and extra resources.
23 | * @param {string} rootFile
24 | * @param {string} rootPath
25 | * @param {Map} 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 | }
43 |
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} assetMap
50 | * @return {Promise}
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(/^(\.?\/)/, '');
59 |
60 | let objectURL;
61 |
62 | if (assetMap.has(normalizedURL)) {
63 | const object = assetMap.get(normalizedURL);
64 | objectURL = URL.createObjectURL(object);
65 | }
66 |
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 | }
74 |
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;
93 |
94 | this.setResponse(response);
95 |
96 | this.toggleEl.innerHTML = ValidatorToggle(report);
97 | this.showToggle();
98 | this.bindListeners();
99 |
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 | };
112 |
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 | }
139 |
140 | /**
141 | * @param {Object} response
142 | */
143 | setResponse(response) {
144 | const json = response && response.parser && response.parser.json;
145 |
146 | if (!json) return;
147 |
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 | }
165 |
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 | }
175 |
176 | bindListeners() {
177 | const reportToggleBtn = this.toggleEl.querySelector('.report-toggle');
178 | reportToggleBtn.addEventListener('click', () => this.showLightbox());
179 |
180 | const reportToggleCloseBtn = this.toggleEl.querySelector('.report-toggle-close');
181 | reportToggleCloseBtn.addEventListener('click', (e) => {
182 | this.hideToggle();
183 | e.stopPropagation();
184 | });
185 | }
186 |
187 | showToggle() {
188 | this.toggleEl.classList.remove('hidden');
189 | }
190 |
191 | hideToggle() {
192 | this.toggleEl.classList.add('hidden');
193 | }
194 |
195 | showLightbox() {
196 | if (!this.report) return;
197 | const tab = window.open('', '_blank');
198 | tab.document.body.innerHTML = `
199 |
200 | glTF 2.0 validation report
201 |
202 |
203 |
207 | ${ValidatorReport({ ...this.report, location })}`;
208 | }
209 | }
210 |
211 | function escapeHTML(unsafe) {
212 | return unsafe
213 | .replace(/&/g, '&')
214 | .replace(//g, '>')
216 | .replace(/"/g, '"')
217 | .replace(/'/g, ''');
218 | }
219 |
220 | function linkify(text) {
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, '$& ')
225 | .replace(emailAddressPattern, '$1 ');
226 | }
227 |
--------------------------------------------------------------------------------
/src/viewer.js:
--------------------------------------------------------------------------------
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,
23 | } from 'three';
24 | import Stats from 'three/addons/libs/stats.module.js';
25 | import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
26 | import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
27 | import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
28 | import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
29 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
30 | import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
31 | import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
32 |
33 | import { GUI } from 'dat.gui';
34 |
35 | import { environments } from './environments.js';
36 |
37 | const DEFAULT_CAMERA = '[default]';
38 |
39 | const MANAGER = new LoadingManager();
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 | );
47 |
48 | const IS_IOS = isIOS();
49 |
50 | const Preset = { ASSET_GENERATOR: 'assetgenerator' };
51 |
52 | Cache.enabled = true;
53 |
54 | export class Viewer {
55 | constructor(el, options) {
56 | this.el = el;
57 | this.options = options;
58 |
59 | this.lights = [];
60 | this.content = null;
61 | this.mixer = null;
62 | this.clips = [];
63 | this.gui = null;
64 |
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,
78 |
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',
88 |
89 | pointSize: 1.0,
90 | };
91 |
92 | this.prevTime = 0;
93 |
94 | this.stats = new Stats();
95 | this.stats.dom.height = '48px';
96 | [].forEach.call(this.stats.dom.children, (child) => (child.style.display = ''));
97 |
98 | this.backgroundColor = new Color(this.state.bgColor);
99 |
100 | this.scene = new Scene();
101 | this.scene.background = this.backgroundColor;
102 |
103 | const fov = options.preset === Preset.ASSET_GENERATOR ? (0.8 * 180) / Math.PI : 60;
104 | const aspect = el.clientWidth / el.clientHeight;
105 | this.defaultCamera = new PerspectiveCamera(fov, aspect, 0.01, 1000);
106 | this.activeCamera = this.defaultCamera;
107 | this.scene.add(this.defaultCamera);
108 |
109 | this.renderer = window.renderer = new WebGLRenderer({ antialias: true });
110 | this.renderer.setClearColor(0xcccccc);
111 | this.renderer.setPixelRatio(window.devicePixelRatio);
112 | this.renderer.setSize(el.clientWidth, el.clientHeight);
113 |
114 | this.pmremGenerator = new PMREMGenerator(this.renderer);
115 | this.pmremGenerator.compileEquirectangularShader();
116 |
117 | this.neutralEnvironment = this.pmremGenerator.fromScene(new RoomEnvironment()).texture;
118 |
119 | this.controls = new OrbitControls(this.defaultCamera, this.renderer.domElement);
120 | this.controls.screenSpacePanning = true;
121 |
122 | this.el.appendChild(this.renderer.domElement);
123 |
124 | this.cameraCtrl = null;
125 | this.cameraFolder = null;
126 | this.animFolder = null;
127 | this.animCtrls = [];
128 | this.morphFolder = null;
129 | this.morphCtrls = [];
130 | this.skeletonHelpers = [];
131 | this.gridHelper = null;
132 | this.axesHelper = null;
133 |
134 | this.addAxesHelper();
135 | this.addGUI();
136 | if (options.kiosk) this.gui.close();
137 |
138 | this.animate = this.animate.bind(this);
139 | requestAnimationFrame(this.animate);
140 | window.addEventListener('resize', this.resize.bind(this), false);
141 | }
142 |
143 | animate(time) {
144 | requestAnimationFrame(this.animate);
145 |
146 | const dt = (time - this.prevTime) / 1000;
147 |
148 | this.controls.update();
149 | this.stats.update();
150 | this.mixer && this.mixer.update(dt);
151 | this.render();
152 |
153 | this.prevTime = time;
154 | }
155 |
156 | render() {
157 | this.renderer.render(this.scene, this.activeCamera);
158 | if (this.state.grid) {
159 | this.axesCamera.position.copy(this.defaultCamera.position);
160 | this.axesCamera.lookAt(this.axesScene.position);
161 | this.axesRenderer.render(this.axesScene, this.axesCamera);
162 | }
163 | }
164 |
165 | resize() {
166 | const { clientHeight, clientWidth } = this.el.parentElement;
167 |
168 | this.defaultCamera.aspect = clientWidth / clientHeight;
169 | this.defaultCamera.updateProjectionMatrix();
170 | this.renderer.setSize(clientWidth, clientHeight);
171 |
172 | this.axesCamera.aspect = this.axesDiv.clientWidth / this.axesDiv.clientHeight;
173 | this.axesCamera.updateProjectionMatrix();
174 | this.axesRenderer.setSize(this.axesDiv.clientWidth, this.axesDiv.clientHeight);
175 | }
176 |
177 | load(url, rootPath, assetMap) {
178 | const baseURL = LoaderUtils.extractUrlBase(url);
179 |
180 | // Load.
181 | return new Promise((resolve, reject) => {
182 | // Intercept and override relative URLs.
183 | MANAGER.setURLModifier((url, path) => {
184 | // URIs in a glTF file may be escaped, or not. Assume that assetMap is
185 | // from an un-escaped source, and decode all URIs before lookups.
186 | // See: https://github.com/donmccurdy/three-gltf-viewer/issues/146
187 | const normalizedURL =
188 | rootPath +
189 | decodeURI(url)
190 | .replace(baseURL, '')
191 | .replace(/^(\.?\/)/, '');
192 |
193 | if (assetMap.has(normalizedURL)) {
194 | const blob = assetMap.get(normalizedURL);
195 | const blobURL = URL.createObjectURL(blob);
196 | blobURLs.push(blobURL);
197 | return blobURL;
198 | }
199 |
200 | return (path || '') + url;
201 | });
202 |
203 | const loader = new GLTFLoader(MANAGER)
204 | .setCrossOrigin('anonymous')
205 | .setDRACOLoader(DRACO_LOADER)
206 | .setKTX2Loader(KTX2_LOADER.detectSupport(this.renderer))
207 | .setMeshoptDecoder(MeshoptDecoder);
208 |
209 | const blobURLs = [];
210 |
211 | loader.load(
212 | url,
213 | (gltf) => {
214 | window.VIEWER.json = gltf;
215 |
216 | const scene = gltf.scene || gltf.scenes[0];
217 | const clips = gltf.animations || [];
218 |
219 | if (!scene) {
220 | // Valid, but not supported by this viewer.
221 | throw new Error(
222 | 'This model contains no scene, and cannot be viewed here. However,' +
223 | ' it may contain individual 3D resources.',
224 | );
225 | }
226 |
227 | this.setContent(scene, clips);
228 |
229 | blobURLs.forEach(URL.revokeObjectURL);
230 |
231 | // See: https://github.com/google/draco/issues/349
232 | // DRACOLoader.releaseDecoderModule();
233 |
234 | resolve(gltf);
235 | },
236 | undefined,
237 | reject,
238 | );
239 | });
240 | }
241 |
242 | /**
243 | * @param {THREE.Object3D} object
244 | * @param {Array {
295 | if (node.isLight) {
296 | this.state.punctualLights = false;
297 | }
298 | });
299 |
300 | this.setClips(clips);
301 |
302 | this.updateLights();
303 | this.updateGUI();
304 | this.updateEnvironment();
305 | this.updateDisplay();
306 |
307 | window.VIEWER.scene = this.content;
308 |
309 | this.printGraph(this.content);
310 | }
311 |
312 | printGraph(node) {
313 | console.group(' <' + node.type + '> ' + node.name);
314 | node.children.forEach((child) => this.printGraph(child));
315 | console.groupEnd();
316 | }
317 |
318 | /**
319 | * @param {Array {
336 | this.mixer.clipAction(clip).reset().play();
337 | this.state.actionStates[clip.name] = true;
338 | });
339 | }
340 |
341 | /**
342 | * @param {string} name
343 | */
344 | setCamera(name) {
345 | if (name === DEFAULT_CAMERA) {
346 | this.controls.enabled = true;
347 | this.activeCamera = this.defaultCamera;
348 | } else {
349 | this.controls.enabled = false;
350 | this.content.traverse((node) => {
351 | if (node.isCamera && node.name === name) {
352 | this.activeCamera = node;
353 | }
354 | });
355 | }
356 | }
357 |
358 | updateLights() {
359 | const state = this.state;
360 | const lights = this.lights;
361 |
362 | if (state.punctualLights && !lights.length) {
363 | this.addLights();
364 | } else if (!state.punctualLights && lights.length) {
365 | this.removeLights();
366 | }
367 |
368 | this.renderer.toneMapping = Number(state.toneMapping);
369 | this.renderer.toneMappingExposure = Math.pow(2, state.exposure);
370 |
371 | if (lights.length === 2) {
372 | lights[0].intensity = state.ambientIntensity;
373 | lights[0].color.set(state.ambientColor);
374 | lights[1].intensity = state.directIntensity;
375 | lights[1].color.set(state.directColor);
376 | }
377 | }
378 |
379 | addLights() {
380 | const state = this.state;
381 |
382 | if (this.options.preset === Preset.ASSET_GENERATOR) {
383 | const hemiLight = new HemisphereLight();
384 | hemiLight.name = 'hemi_light';
385 | this.scene.add(hemiLight);
386 | this.lights.push(hemiLight);
387 | return;
388 | }
389 |
390 | const light1 = new AmbientLight(state.ambientColor, state.ambientIntensity);
391 | light1.name = 'ambient_light';
392 | this.defaultCamera.add(light1);
393 |
394 | const light2 = new DirectionalLight(state.directColor, state.directIntensity);
395 | light2.position.set(0.5, 0, 0.866); // ~60º
396 | light2.name = 'main_light';
397 | this.defaultCamera.add(light2);
398 |
399 | this.lights.push(light1, light2);
400 | }
401 |
402 | removeLights() {
403 | this.lights.forEach((light) => light.parent.remove(light));
404 | this.lights.length = 0;
405 | }
406 |
407 | updateEnvironment() {
408 | const environment = environments.filter(
409 | (entry) => entry.name === this.state.environment,
410 | )[0];
411 |
412 | this.getCubeMapTexture(environment).then(({ envMap }) => {
413 | this.scene.environment = envMap;
414 | this.scene.background = this.state.background ? envMap : this.backgroundColor;
415 | });
416 | }
417 |
418 | getCubeMapTexture(environment) {
419 | const { id, path } = environment;
420 |
421 | // neutral (THREE.RoomEnvironment)
422 | if (id === 'neutral') {
423 | return Promise.resolve({ envMap: this.neutralEnvironment });
424 | }
425 |
426 | // none
427 | if (id === '') {
428 | return Promise.resolve({ envMap: null });
429 | }
430 |
431 | return new Promise((resolve, reject) => {
432 | new EXRLoader().load(
433 | path,
434 | (texture) => {
435 | const envMap = this.pmremGenerator.fromEquirectangular(texture).texture;
436 | this.pmremGenerator.dispose();
437 |
438 | resolve({ envMap });
439 | },
440 | undefined,
441 | reject,
442 | );
443 | });
444 | }
445 |
446 | updateDisplay() {
447 | if (this.skeletonHelpers.length) {
448 | this.skeletonHelpers.forEach((helper) => this.scene.remove(helper));
449 | }
450 |
451 | traverseMaterials(this.content, (material) => {
452 | material.wireframe = this.state.wireframe;
453 |
454 | if (material instanceof PointsMaterial) {
455 | material.size = this.state.pointSize;
456 | }
457 | });
458 |
459 | this.content.traverse((node) => {
460 | if (node.geometry && node.skeleton && this.state.skeleton) {
461 | const helper = new SkeletonHelper(node.skeleton.bones[0].parent);
462 | helper.material.linewidth = 3;
463 | this.scene.add(helper);
464 | this.skeletonHelpers.push(helper);
465 | }
466 | });
467 |
468 | if (this.state.grid !== Boolean(this.gridHelper)) {
469 | if (this.state.grid) {
470 | this.gridHelper = new GridHelper();
471 | this.axesHelper = new AxesHelper();
472 | this.axesHelper.renderOrder = 999;
473 | this.axesHelper.onBeforeRender = (renderer) => renderer.clearDepth();
474 | this.scene.add(this.gridHelper);
475 | this.scene.add(this.axesHelper);
476 | } else {
477 | this.scene.remove(this.gridHelper);
478 | this.scene.remove(this.axesHelper);
479 | this.gridHelper = null;
480 | this.axesHelper = null;
481 | this.axesRenderer.clear();
482 | }
483 | }
484 |
485 | this.controls.autoRotate = this.state.autoRotate;
486 | }
487 |
488 | updateBackground() {
489 | this.backgroundColor.set(this.state.bgColor);
490 | }
491 |
492 | /**
493 | * Adds AxesHelper.
494 | *
495 | * See: https://stackoverflow.com/q/16226693/1314762
496 | */
497 | addAxesHelper() {
498 | this.axesDiv = document.createElement('div');
499 | this.el.appendChild(this.axesDiv);
500 | this.axesDiv.classList.add('axes');
501 |
502 | const { clientWidth, clientHeight } = this.axesDiv;
503 |
504 | this.axesScene = new Scene();
505 | this.axesCamera = new PerspectiveCamera(50, clientWidth / clientHeight, 0.1, 10);
506 | this.axesScene.add(this.axesCamera);
507 |
508 | this.axesRenderer = new WebGLRenderer({ alpha: true });
509 | this.axesRenderer.setPixelRatio(window.devicePixelRatio);
510 | this.axesRenderer.setSize(this.axesDiv.clientWidth, this.axesDiv.clientHeight);
511 |
512 | this.axesCamera.up = this.defaultCamera.up;
513 |
514 | this.axesCorner = new AxesHelper(5);
515 | this.axesScene.add(this.axesCorner);
516 | this.axesDiv.appendChild(this.axesRenderer.domElement);
517 | }
518 |
519 | addGUI() {
520 | const gui = (this.gui = new GUI({
521 | autoPlace: false,
522 | width: 260,
523 | hideable: true,
524 | }));
525 |
526 | // Display controls.
527 | const dispFolder = gui.addFolder('Display');
528 | const envBackgroundCtrl = dispFolder.add(this.state, 'background');
529 | envBackgroundCtrl.onChange(() => this.updateEnvironment());
530 | const autoRotateCtrl = dispFolder.add(this.state, 'autoRotate');
531 | autoRotateCtrl.onChange(() => this.updateDisplay());
532 | const wireframeCtrl = dispFolder.add(this.state, 'wireframe');
533 | wireframeCtrl.onChange(() => this.updateDisplay());
534 | const skeletonCtrl = dispFolder.add(this.state, 'skeleton');
535 | skeletonCtrl.onChange(() => this.updateDisplay());
536 | const gridCtrl = dispFolder.add(this.state, 'grid');
537 | gridCtrl.onChange(() => this.updateDisplay());
538 | dispFolder.add(this.controls, 'screenSpacePanning');
539 | const pointSizeCtrl = dispFolder.add(this.state, 'pointSize', 1, 16);
540 | pointSizeCtrl.onChange(() => this.updateDisplay());
541 | const bgColorCtrl = dispFolder.addColor(this.state, 'bgColor');
542 | bgColorCtrl.onChange(() => this.updateBackground());
543 |
544 | // Lighting controls.
545 | const lightFolder = gui.addFolder('Lighting');
546 | const envMapCtrl = lightFolder.add(
547 | this.state,
548 | 'environment',
549 | environments.map((env) => env.name),
550 | );
551 | envMapCtrl.onChange(() => this.updateEnvironment());
552 | [
553 | lightFolder.add(this.state, 'toneMapping', {
554 | Linear: LinearToneMapping,
555 | 'ACES Filmic': ACESFilmicToneMapping,
556 | }),
557 | lightFolder.add(this.state, 'exposure', -10, 10, 0.01),
558 | lightFolder.add(this.state, 'punctualLights').listen(),
559 | lightFolder.add(this.state, 'ambientIntensity', 0, 2),
560 | lightFolder.addColor(this.state, 'ambientColor'),
561 | lightFolder.add(this.state, 'directIntensity', 0, 4), // TODO(#116)
562 | lightFolder.addColor(this.state, 'directColor'),
563 | ].forEach((ctrl) => ctrl.onChange(() => this.updateLights()));
564 |
565 | // Animation controls.
566 | this.animFolder = gui.addFolder('Animation');
567 | this.animFolder.domElement.style.display = 'none';
568 | const playbackSpeedCtrl = this.animFolder.add(this.state, 'playbackSpeed', 0, 1);
569 | playbackSpeedCtrl.onChange((speed) => {
570 | if (this.mixer) this.mixer.timeScale = speed;
571 | });
572 | this.animFolder.add({ playAll: () => this.playAllClips() }, 'playAll');
573 |
574 | // Morph target controls.
575 | this.morphFolder = gui.addFolder('Morph Targets');
576 | this.morphFolder.domElement.style.display = 'none';
577 |
578 | // Camera controls.
579 | this.cameraFolder = gui.addFolder('Cameras');
580 | this.cameraFolder.domElement.style.display = 'none';
581 |
582 | // Stats.
583 | const perfFolder = gui.addFolder('Performance');
584 | const perfLi = document.createElement('li');
585 | this.stats.dom.style.position = 'static';
586 | perfLi.appendChild(this.stats.dom);
587 | perfLi.classList.add('gui-stats');
588 | perfFolder.__ul.appendChild(perfLi);
589 |
590 | const guiWrap = document.createElement('div');
591 | this.el.appendChild(guiWrap);
592 | guiWrap.classList.add('gui-wrap');
593 | guiWrap.appendChild(gui.domElement);
594 | gui.open();
595 | }
596 |
597 | updateGUI() {
598 | this.cameraFolder.domElement.style.display = 'none';
599 |
600 | this.morphCtrls.forEach((ctrl) => ctrl.remove());
601 | this.morphCtrls.length = 0;
602 | this.morphFolder.domElement.style.display = 'none';
603 |
604 | this.animCtrls.forEach((ctrl) => ctrl.remove());
605 | this.animCtrls.length = 0;
606 | this.animFolder.domElement.style.display = 'none';
607 |
608 | const cameraNames = [];
609 | const morphMeshes = [];
610 | this.content.traverse((node) => {
611 | if (node.geometry && node.morphTargetInfluences) {
612 | morphMeshes.push(node);
613 | }
614 | if (node.isCamera) {
615 | node.name = node.name || `VIEWER__camera_${cameraNames.length + 1}`;
616 | cameraNames.push(node.name);
617 | }
618 | });
619 |
620 | if (cameraNames.length) {
621 | this.cameraFolder.domElement.style.display = '';
622 | if (this.cameraCtrl) this.cameraCtrl.remove();
623 | const cameraOptions = [DEFAULT_CAMERA].concat(cameraNames);
624 | this.cameraCtrl = this.cameraFolder.add(this.state, 'camera', cameraOptions);
625 | this.cameraCtrl.onChange((name) => this.setCamera(name));
626 | }
627 |
628 | if (morphMeshes.length) {
629 | this.morphFolder.domElement.style.display = '';
630 | morphMeshes.forEach((mesh) => {
631 | if (mesh.morphTargetInfluences.length) {
632 | const nameCtrl = this.morphFolder.add(
633 | { name: mesh.name || 'Untitled' },
634 | 'name',
635 | );
636 | this.morphCtrls.push(nameCtrl);
637 | }
638 | for (let i = 0; i < mesh.morphTargetInfluences.length; i++) {
639 | const ctrl = this.morphFolder
640 | .add(mesh.morphTargetInfluences, i, 0, 1, 0.01)
641 | .listen();
642 | Object.keys(mesh.morphTargetDictionary).forEach((key) => {
643 | if (key && mesh.morphTargetDictionary[key] === i) ctrl.name(key);
644 | });
645 | this.morphCtrls.push(ctrl);
646 | }
647 | });
648 | }
649 |
650 | if (this.clips.length) {
651 | this.animFolder.domElement.style.display = '';
652 | const actionStates = (this.state.actionStates = {});
653 | this.clips.forEach((clip, clipIndex) => {
654 | clip.name = `${clipIndex + 1}. ${clip.name}`;
655 |
656 | // Autoplay the first clip.
657 | let action;
658 | if (clipIndex === 0) {
659 | actionStates[clip.name] = true;
660 | action = this.mixer.clipAction(clip);
661 | action.play();
662 | } else {
663 | actionStates[clip.name] = false;
664 | }
665 |
666 | // Play other clips when enabled.
667 | const ctrl = this.animFolder.add(actionStates, clip.name).listen();
668 | ctrl.onChange((playAnimation) => {
669 | action = action || this.mixer.clipAction(clip);
670 | action.setEffectiveTimeScale(1);
671 | playAnimation ? action.play() : action.stop();
672 | });
673 | this.animCtrls.push(ctrl);
674 | });
675 | }
676 | }
677 |
678 | clear() {
679 | if (!this.content) return;
680 |
681 | this.scene.remove(this.content);
682 |
683 | // dispose geometry
684 | this.content.traverse((node) => {
685 | if (!node.geometry) return;
686 |
687 | node.geometry.dispose();
688 | });
689 |
690 | // dispose textures
691 | traverseMaterials(this.content, (material) => {
692 | for (const key in material) {
693 | if (key !== 'envMap' && material[key] && material[key].isTexture) {
694 | material[key].dispose();
695 | }
696 | }
697 | });
698 | }
699 | }
700 |
701 | function traverseMaterials(object, callback) {
702 | object.traverse((node) => {
703 | if (!node.geometry) return;
704 | const materials = Array.isArray(node.material) ? node.material : [node.material];
705 | materials.forEach(callback);
706 | });
707 | }
708 |
709 | // https://stackoverflow.com/a/9039885/1314762
710 | function isIOS() {
711 | return (
712 | ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes(
713 | navigator.platform,
714 | ) ||
715 | // iPad on iOS 13 detection
716 | (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
717 | );
718 | }
719 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: 'Raleway', sans-serif;
5 | background: #191919;
6 | height: 100%;
7 | overflow: hidden;
8 | }
9 |
10 | * {
11 | box-sizing: border-box;
12 | }
13 |
14 | body {
15 | display: flex;
16 | flex-direction: column;
17 | }
18 |
19 | .wrap {
20 | display: flex;
21 | width: 100vw;
22 | flex-grow: 1;
23 | position: relative;
24 | }
25 |
26 | .dropzone {
27 | display: flex;
28 | flex-grow: 1;
29 | flex-direction: column;
30 | justify-content: center;
31 | align-items: center;
32 | }
33 |
34 | .placeholder {
35 | width: 100%;
36 | max-width: 500px;
37 | border-radius: 0.5em;
38 | background: #252525;
39 | padding: 2em;
40 | text-align: center;
41 | }
42 |
43 | .placeholder p {
44 | font-size: 1.2rem;
45 | color: #999;
46 | }
47 |
48 | .viewer {
49 | width: 100%;
50 | height: 100%;
51 | flex-grow: 1;
52 | flex-shrink: 1;
53 | position: absolute;
54 | top: 0;
55 | z-index: 0;
56 | }
57 |
58 | .axes {
59 | width: 100px;
60 | height: 100px;
61 | margin: 20px;
62 | padding: 0px;
63 | position: absolute;
64 | left: 0px;
65 | bottom: 0px;
66 | z-index: 10;
67 | pointer-events: none;
68 | }
69 |
70 | /******************************************************************************
71 | * Header
72 | */
73 |
74 | header {
75 | display: flex;
76 | background: #353535;
77 | padding: 0 2em;
78 | height: 4rem;
79 | line-height: 4rem;
80 | align-items: center;
81 | overflow-x: auto;
82 | overflow-y: hidden;
83 | white-space: nowrap;
84 | box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, 0.3);
85 | z-index: 1;
86 |
87 | -webkit-app-region: drag;
88 | }
89 |
90 | header h1 {
91 | color: #F5F5F5;
92 | font-size: 1.4rem;
93 | font-weight: 300;
94 | line-height: 4rem;
95 | margin: 0;
96 | }
97 |
98 | header h1 > a {
99 | color: inherit;
100 | font-size: inherit;
101 | text-decoration: inherit;
102 | }
103 |
104 | .gui-wrap {
105 | position: absolute;
106 | top: 0;
107 | right: 0;
108 | bottom: 0;
109 | pointer-events: none;
110 | }
111 |
112 | .gui-wrap > .main {
113 | pointer-events: all;
114 | max-height: 100%;
115 | overflow: auto;
116 | }
117 |
118 | .dg li.gui-stats:not(.folder) {
119 | height: auto;
120 | }
121 |
122 | @media screen and (max-width: 700px) {
123 | header h1 {
124 | font-size: 1em;
125 | }
126 | }
127 |
128 | /******************************************************************************
129 | * Footer
130 | */
131 |
132 | footer {
133 | position: absolute;
134 | bottom: 0.5em;
135 | right: 0.5em;
136 | font-family: monospace;
137 | color: #fff;
138 | }
139 |
140 | footer a {
141 | color: inherit;
142 | opacity: 0.5;
143 | text-decoration: inherit;
144 | }
145 |
146 | footer a:hover {
147 | opacity: 1;
148 | text-decoration: underline;
149 | }
150 |
151 | footer .separator {
152 | margin: 0 0.5em;
153 | opacity: 0.2;
154 | }
155 |
156 | /******************************************************************************
157 | * Upload Button
158 | *
159 | * https://tympanus.net/Tutorials/CustomFileInputs/
160 | */
161 |
162 | .upload-btn {
163 | margin-top: 2em;
164 | }
165 |
166 | .upload-btn input {
167 | width: 0.1px;
168 | height: 0.1px;
169 | opacity: 0;
170 | overflow: hidden;
171 | position: absolute;
172 | z-index: -1;
173 | }
174 |
175 | .upload-btn label {
176 | color: #808080;
177 | border: 0;
178 | border-radius: 3px;
179 | transition: ease 0.2s background;
180 | font-size: 1rem;
181 | font-weight: 700;
182 | text-overflow: ellipsis;
183 | white-space: nowrap;
184 | cursor: pointer;
185 | display: inline-block;
186 | overflow: hidden;
187 | padding: 0.625rem 1.25rem;
188 | }
189 |
190 | .upload-btn label:hover {
191 | background: #252525;
192 | }
193 |
194 | .upload-btn svg {
195 | width: 1em;
196 | height: 1em;
197 | vertical-align: middle;
198 | fill: currentColor;
199 | margin-top: -0.25em;
200 | margin-right: 0.25em;
201 | }
202 |
203 |
204 | /******************************************************************************
205 | * Validation report
206 | */
207 |
208 | .report {
209 | padding: 2em;
210 | max-width: 860px;
211 | }
212 |
213 | .report h1 {
214 | margin-top: 0;
215 | }
216 |
217 | .report p,
218 | .report ul {
219 | line-height: 1.5em;
220 | }
221 |
222 | .report-table {
223 | text-align: left;
224 | border-collapse: collapse;
225 | width: 100%;
226 | }
227 |
228 | .report-table thead tr {
229 | background: #404040;
230 | color: #FFF;
231 | }
232 |
233 | .report-table th,
234 | .report-table td {
235 | padding: 0.5em 1em;
236 | }
237 |
238 | .report-table tr:nth-child(2n) {
239 | background: #F0F0F0;
240 | }
241 |
242 | /******************************************************************************
243 | * Validation toggle
244 | */
245 |
246 | .report-toggle-wrap.hidden { display: none; }
247 |
248 | .report-toggle {
249 | cursor: pointer;
250 | display: flex;
251 | position: absolute;
252 | bottom: 0;
253 | left: 20px;
254 | height: 30px;
255 | box-shadow: 0px 0px 5px 0 rgba(0, 0, 0, 0.25);
256 | background: #FFF;
257 | box-sizing: border-box;
258 |
259 | color: #f0f0f0;
260 | background: #000;
261 | border-left: 6px solid #000;
262 | }
263 |
264 | .report-toggle.level-1 { color: #444; background: #ffeda0; border-left-color: #feb24c; }
265 | .report-toggle.level-0 { color: #444; background: #f4c2be; border-left-color: #b10026; }
266 |
267 | .report-toggle-text {
268 | line-height: 32px;
269 | padding: 0 0.5em;
270 | font-weight: 300;
271 | font-size: 0.8em;
272 | }
273 |
274 | .report-toggle-close {
275 | width: 30px;
276 | height: 30px;
277 | line-height: 30px;
278 | font-size: 1.5em;
279 | text-align: center;
280 | }
281 |
282 | /******************************************************************************
283 | * CSS Spinner
284 | *
285 | * http://tobiasahlin.com/spinkit/
286 | */
287 |
288 | .spinner {
289 | width: 40px;
290 | height: 40px;
291 | position: absolute;
292 | left: 50%;
293 | top: 50%;
294 | margin: -20px;
295 |
296 | background-color: #333;
297 |
298 | border-radius: 100%;
299 | -webkit-animation: sk-scaleout 1.0s infinite ease-in-out;
300 | animation: sk-scaleout 1.0s infinite ease-in-out;
301 | }
302 |
303 | @-webkit-keyframes sk-scaleout {
304 | 0% { -webkit-transform: scale(0) }
305 | 100% {
306 | -webkit-transform: scale(1.0);
307 | opacity: 0;
308 | }
309 | }
310 |
311 | @keyframes sk-scaleout {
312 | 0% {
313 | -webkit-transform: scale(0);
314 | transform: scale(0);
315 | } 100% {
316 | -webkit-transform: scale(1.0);
317 | transform: scale(1.0);
318 | opacity: 0;
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
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 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@esbuild/aix-ppc64@0.21.5":
6 | version "0.21.5"
7 | resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
8 | integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
9 |
10 | "@esbuild/android-arm64@0.21.5":
11 | version "0.21.5"
12 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
13 | integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
14 |
15 | "@esbuild/android-arm@0.21.5":
16 | version "0.21.5"
17 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
18 | integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
19 |
20 | "@esbuild/android-x64@0.21.5":
21 | version "0.21.5"
22 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
23 | integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
24 |
25 | "@esbuild/darwin-arm64@0.21.5":
26 | version "0.21.5"
27 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
28 | integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
29 |
30 | "@esbuild/darwin-x64@0.21.5":
31 | version "0.21.5"
32 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
33 | integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
34 |
35 | "@esbuild/freebsd-arm64@0.21.5":
36 | version "0.21.5"
37 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
38 | integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
39 |
40 | "@esbuild/freebsd-x64@0.21.5":
41 | version "0.21.5"
42 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
43 | integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
44 |
45 | "@esbuild/linux-arm64@0.21.5":
46 | version "0.21.5"
47 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
48 | integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
49 |
50 | "@esbuild/linux-arm@0.21.5":
51 | version "0.21.5"
52 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
53 | integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
54 |
55 | "@esbuild/linux-ia32@0.21.5":
56 | version "0.21.5"
57 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
58 | integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
59 |
60 | "@esbuild/linux-loong64@0.21.5":
61 | version "0.21.5"
62 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
63 | integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
64 |
65 | "@esbuild/linux-mips64el@0.21.5":
66 | version "0.21.5"
67 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
68 | integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
69 |
70 | "@esbuild/linux-ppc64@0.21.5":
71 | version "0.21.5"
72 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
73 | integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
74 |
75 | "@esbuild/linux-riscv64@0.21.5":
76 | version "0.21.5"
77 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
78 | integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
79 |
80 | "@esbuild/linux-s390x@0.21.5":
81 | version "0.21.5"
82 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
83 | integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
84 |
85 | "@esbuild/linux-x64@0.21.5":
86 | version "0.21.5"
87 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
88 | integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
89 |
90 | "@esbuild/netbsd-x64@0.21.5":
91 | version "0.21.5"
92 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
93 | integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
94 |
95 | "@esbuild/openbsd-x64@0.21.5":
96 | version "0.21.5"
97 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
98 | integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
99 |
100 | "@esbuild/sunos-x64@0.21.5":
101 | version "0.21.5"
102 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
103 | integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
104 |
105 | "@esbuild/win32-arm64@0.21.5":
106 | version "0.21.5"
107 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
108 | integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
109 |
110 | "@esbuild/win32-ia32@0.21.5":
111 | version "0.21.5"
112 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
113 | integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
114 |
115 | "@esbuild/win32-x64@0.21.5":
116 | version "0.21.5"
117 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
118 | integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
119 |
120 | "@rollup/rollup-android-arm-eabi@4.40.2":
121 | version "4.40.2"
122 | resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz#c228d00a41f0dbd6fb8b7ea819bbfbf1c1157a10"
123 | integrity sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==
124 |
125 | "@rollup/rollup-android-arm64@4.40.2":
126 | version "4.40.2"
127 | resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz#e2b38d0c912169fd55d7e38d723aada208d37256"
128 | integrity sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==
129 |
130 | "@rollup/rollup-darwin-arm64@4.40.2":
131 | version "4.40.2"
132 | resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz#1fddb3690f2ae33df16d334c613377f05abe4878"
133 | integrity sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==
134 |
135 | "@rollup/rollup-darwin-x64@4.40.2":
136 | version "4.40.2"
137 | resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz#818298d11c8109e1112590165142f14be24b396d"
138 | integrity sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==
139 |
140 | "@rollup/rollup-freebsd-arm64@4.40.2":
141 | version "4.40.2"
142 | resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz#91a28dc527d5bed7f9ecf0e054297b3012e19618"
143 | integrity sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==
144 |
145 | "@rollup/rollup-freebsd-x64@4.40.2":
146 | version "4.40.2"
147 | resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz#28acadefa76b5c7bede1576e065b51d335c62c62"
148 | integrity sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==
149 |
150 | "@rollup/rollup-linux-arm-gnueabihf@4.40.2":
151 | version "4.40.2"
152 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz#819691464179cbcd9a9f9d3dc7617954840c6186"
153 | integrity sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==
154 |
155 | "@rollup/rollup-linux-arm-musleabihf@4.40.2":
156 | version "4.40.2"
157 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz#d149207039e4189e267e8724050388effc80d704"
158 | integrity sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==
159 |
160 | "@rollup/rollup-linux-arm64-gnu@4.40.2":
161 | version "4.40.2"
162 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz#fa72ebddb729c3c6d88973242f1a2153c83e86ec"
163 | integrity sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==
164 |
165 | "@rollup/rollup-linux-arm64-musl@4.40.2":
166 | version "4.40.2"
167 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz#2054216e34469ab8765588ebf343d531fc3c9228"
168 | integrity sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==
169 |
170 | "@rollup/rollup-linux-loongarch64-gnu@4.40.2":
171 | version "4.40.2"
172 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz#818de242291841afbfc483a84f11e9c7a11959bc"
173 | integrity sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==
174 |
175 | "@rollup/rollup-linux-powerpc64le-gnu@4.40.2":
176 | version "4.40.2"
177 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz#0bb4cb8fc4a2c635f68c1208c924b2145eb647cb"
178 | integrity sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==
179 |
180 | "@rollup/rollup-linux-riscv64-gnu@4.40.2":
181 | version "4.40.2"
182 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz#4b3b8e541b7b13e447ae07774217d98c06f6926d"
183 | integrity sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==
184 |
185 | "@rollup/rollup-linux-riscv64-musl@4.40.2":
186 | version "4.40.2"
187 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz#e065405e67d8bd64a7d0126c931bd9f03910817f"
188 | integrity sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==
189 |
190 | "@rollup/rollup-linux-s390x-gnu@4.40.2":
191 | version "4.40.2"
192 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz#dda3265bbbfe16a5d0089168fd07f5ebb2a866fe"
193 | integrity sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==
194 |
195 | "@rollup/rollup-linux-x64-gnu@4.40.2":
196 | version "4.40.2"
197 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz#90993269b8b995b4067b7b9d72ff1c360ef90a17"
198 | integrity sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==
199 |
200 | "@rollup/rollup-linux-x64-musl@4.40.2":
201 | version "4.40.2"
202 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz#fdf5b09fd121eb8d977ebb0fda142c7c0167b8de"
203 | integrity sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==
204 |
205 | "@rollup/rollup-win32-arm64-msvc@4.40.2":
206 | version "4.40.2"
207 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz#6397e1e012db64dfecfed0774cb9fcf89503d716"
208 | integrity sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==
209 |
210 | "@rollup/rollup-win32-ia32-msvc@4.40.2":
211 | version "4.40.2"
212 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz#df0991464a52a35506103fe18d29913bf8798a0c"
213 | integrity sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==
214 |
215 | "@rollup/rollup-win32-x64-msvc@4.40.2":
216 | version "4.40.2"
217 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz#8dae04d01a2cbd84d6297d99356674c6b993f0fc"
218 | integrity sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==
219 |
220 | "@types/estree@1.0.7":
221 | version "1.0.7"
222 | resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
223 | integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
224 |
225 | dat.gui@^0.7.9:
226 | version "0.7.9"
227 | resolved "https://registry.yarnpkg.com/dat.gui/-/dat.gui-0.7.9.tgz#860cab06053b028e327820eabdf25a13cf07b17e"
228 | integrity sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==
229 |
230 | decode-uri-component@^0.4.1:
231 | version "0.4.1"
232 | resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz#2ac4859663c704be22bf7db760a1494a49ab2cc5"
233 | integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==
234 |
235 | esbuild@^0.21.3:
236 | version "0.21.5"
237 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
238 | integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
239 | optionalDependencies:
240 | "@esbuild/aix-ppc64" "0.21.5"
241 | "@esbuild/android-arm" "0.21.5"
242 | "@esbuild/android-arm64" "0.21.5"
243 | "@esbuild/android-x64" "0.21.5"
244 | "@esbuild/darwin-arm64" "0.21.5"
245 | "@esbuild/darwin-x64" "0.21.5"
246 | "@esbuild/freebsd-arm64" "0.21.5"
247 | "@esbuild/freebsd-x64" "0.21.5"
248 | "@esbuild/linux-arm" "0.21.5"
249 | "@esbuild/linux-arm64" "0.21.5"
250 | "@esbuild/linux-ia32" "0.21.5"
251 | "@esbuild/linux-loong64" "0.21.5"
252 | "@esbuild/linux-mips64el" "0.21.5"
253 | "@esbuild/linux-ppc64" "0.21.5"
254 | "@esbuild/linux-riscv64" "0.21.5"
255 | "@esbuild/linux-s390x" "0.21.5"
256 | "@esbuild/linux-x64" "0.21.5"
257 | "@esbuild/netbsd-x64" "0.21.5"
258 | "@esbuild/openbsd-x64" "0.21.5"
259 | "@esbuild/sunos-x64" "0.21.5"
260 | "@esbuild/win32-arm64" "0.21.5"
261 | "@esbuild/win32-ia32" "0.21.5"
262 | "@esbuild/win32-x64" "0.21.5"
263 |
264 | filter-obj@^5.1.0:
265 | version "5.1.0"
266 | resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-5.1.0.tgz#5bd89676000a713d7db2e197f660274428e524ed"
267 | integrity sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==
268 |
269 | fsevents@~2.3.2, fsevents@~2.3.3:
270 | version "2.3.3"
271 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
272 | integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
273 |
274 | gltf-validator@^2.0.0-dev.3.10:
275 | version "2.0.0-dev.3.10"
276 | resolved "https://registry.yarnpkg.com/gltf-validator/-/gltf-validator-2.0.0-dev.3.10.tgz#9b09225db864fe3f0a584259f65d087e2213a93a"
277 | integrity sha512-odJ4k0tRkGXiDGn78yDBg+fBbAIvBnXxh3RwAta0emSxGtyagFE8B4xELB1oYe3S5RD8Ci3uZAsZaascH2LAEQ==
278 |
279 | nanoid@^3.3.8:
280 | version "3.3.11"
281 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
282 | integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
283 |
284 | picocolors@^1.1.1:
285 | version "1.1.1"
286 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
287 | integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
288 |
289 | postcss@^8.4.43:
290 | version "8.5.3"
291 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb"
292 | integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
293 | dependencies:
294 | nanoid "^3.3.8"
295 | picocolors "^1.1.1"
296 | source-map-js "^1.2.1"
297 |
298 | prettier@^3.2.5:
299 | version "3.5.3"
300 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5"
301 | integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==
302 |
303 | query-string@^8.1.0:
304 | version "8.2.0"
305 | resolved "https://registry.yarnpkg.com/query-string/-/query-string-8.2.0.tgz#f0b0ef6caa85f525dbdb745a67d3f8c08d71cc6b"
306 | integrity sha512-tUZIw8J0CawM5wyGBiDOAp7ObdRQh4uBor/fUR9ZjmbZVvw95OD9If4w3MQxr99rg0DJZ/9CIORcpEqU5hQG7g==
307 | dependencies:
308 | decode-uri-component "^0.4.1"
309 | filter-obj "^5.1.0"
310 | split-on-first "^3.0.0"
311 |
312 | rollup@^4.20.0:
313 | version "4.40.2"
314 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.2.tgz#778e88b7a197542682b3e318581f7697f55f0619"
315 | integrity sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==
316 | dependencies:
317 | "@types/estree" "1.0.7"
318 | optionalDependencies:
319 | "@rollup/rollup-android-arm-eabi" "4.40.2"
320 | "@rollup/rollup-android-arm64" "4.40.2"
321 | "@rollup/rollup-darwin-arm64" "4.40.2"
322 | "@rollup/rollup-darwin-x64" "4.40.2"
323 | "@rollup/rollup-freebsd-arm64" "4.40.2"
324 | "@rollup/rollup-freebsd-x64" "4.40.2"
325 | "@rollup/rollup-linux-arm-gnueabihf" "4.40.2"
326 | "@rollup/rollup-linux-arm-musleabihf" "4.40.2"
327 | "@rollup/rollup-linux-arm64-gnu" "4.40.2"
328 | "@rollup/rollup-linux-arm64-musl" "4.40.2"
329 | "@rollup/rollup-linux-loongarch64-gnu" "4.40.2"
330 | "@rollup/rollup-linux-powerpc64le-gnu" "4.40.2"
331 | "@rollup/rollup-linux-riscv64-gnu" "4.40.2"
332 | "@rollup/rollup-linux-riscv64-musl" "4.40.2"
333 | "@rollup/rollup-linux-s390x-gnu" "4.40.2"
334 | "@rollup/rollup-linux-x64-gnu" "4.40.2"
335 | "@rollup/rollup-linux-x64-musl" "4.40.2"
336 | "@rollup/rollup-win32-arm64-msvc" "4.40.2"
337 | "@rollup/rollup-win32-ia32-msvc" "4.40.2"
338 | "@rollup/rollup-win32-x64-msvc" "4.40.2"
339 | fsevents "~2.3.2"
340 |
341 | simple-dropzone@^0.8.3:
342 | version "0.8.3"
343 | resolved "https://registry.yarnpkg.com/simple-dropzone/-/simple-dropzone-0.8.3.tgz#1f46245b7ca8f3d840c3fbef7ab683ae45cf82d1"
344 | integrity sha512-y0i8Tf1O9whdRh2NXE2a7y3U0wbQXTbPnRJeHD6XP/tWoLEIwqYxPtnI/Fst3mRASGqMD8hXaRFjgKsO1nbvcg==
345 | dependencies:
346 | zip-js-esm "^1.1.1"
347 |
348 | source-map-js@^1.2.1:
349 | version "1.2.1"
350 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
351 | integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
352 |
353 | split-on-first@^3.0.0:
354 | version "3.0.0"
355 | resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-3.0.0.tgz#f04959c9ea8101b9b0bbf35a61b9ebea784a23e7"
356 | integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==
357 |
358 | three@^0.176.0:
359 | version "0.176.0"
360 | resolved "https://registry.yarnpkg.com/three/-/three-0.176.0.tgz#a30c1974e46db5745e4f96dd9ee2028d71e16ecf"
361 | integrity sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==
362 |
363 | vhtml@^2.2.0:
364 | version "2.2.0"
365 | resolved "https://registry.yarnpkg.com/vhtml/-/vhtml-2.2.0.tgz#369e6823ed6c32cbb9f6e33395bae7c65faa014c"
366 | integrity sha512-TPXrXrxBOslRUVnlVkiAqhoXneiertIg86bdvzionrUYhEuiROvyPZNiiP6GIIJ2Q7oPNVyEtIx8gMAZZE9lCQ==
367 |
368 | vite@^5.1.8:
369 | version "5.4.19"
370 | resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959"
371 | integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==
372 | dependencies:
373 | esbuild "^0.21.3"
374 | postcss "^8.4.43"
375 | rollup "^4.20.0"
376 | optionalDependencies:
377 | fsevents "~2.3.3"
378 |
379 | zip-js-esm@^1.1.1:
380 | version "1.1.1"
381 | resolved "https://registry.yarnpkg.com/zip-js-esm/-/zip-js-esm-1.1.1.tgz#9726cd6cf1e97f05ee1b1ea8e06529a59ac67003"
382 | integrity sha512-8fSLIpssGjX8mqTduIjlUStH1uBmAFWCmkiAo7/G8uDAY+rEnwE6nll5Cyvu+Ytqmw5FIQ7XAhohNrrZL2PheQ==
383 |
--------------------------------------------------------------------------------