+7
.prettierrc.json
+7
.prettierrc.json
+4
-5
README.md
+4
-5
README.md
···
4
4
5
5
Viewer: [gltf-viewer.donmccurdy.com](https://gltf-viewer.donmccurdy.com/)
6
6
7
-
8
7

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
-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
+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
+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
+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
});
+98
-51
src/components/validator-report.jsx
+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
+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
+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">×</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
+
×
33
+
</div>
34
+
</div>
35
+
);
36
+
}
+22
-22
src/environments.js
+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
+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, '&')
207
-
.replace(/</g, '<')
208
-
.replace(/>/g, '>')
209
-
.replace(/"/g, '"')
210
-
.replace(/'/g, ''');
212
+
return unsafe
213
+
.replace(/&/g, '&')
214
+
.replace(/</g, '<')
215
+
.replace(/>/g, '>')
216
+
.replace(/"/g, '"')
217
+
.replace(/'/g, ''');
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
+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
+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
+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"