Skip to content

Commit ee84629

Browse files
authored
Web: Support resources in dropped directories (#236)
1 parent abf276a commit ee84629

File tree

3 files changed

+101
-59
lines changed

3 files changed

+101
-59
lines changed

web/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@
5050
</div>
5151
<div id="outputZone">
5252
<p><em>Validation is performed locally in your browser. Submitted assets are not uploaded.</em></p>
53-
<p id="truncatedWarning" style="display: none"><em>Validation report is truncated because it contains too many
53+
<p id="truncatedWarning" class="warning" style="display: none"><em>Validation report is truncated because it contains too many
5454
issues.</em></p>
55+
<p id="fileWarning" class="warning" style="display: none"><em>The Validator is opened with the "file" URI Scheme.
56+
Resources in directories are not accessible.</em></p>
5557
<pre><code class="language-json" id="output"></code></pre>
5658
</div>
5759

web/scripts/validator.dart

Lines changed: 97 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,6 @@
1-
/*
2-
* # Copyright (c) 2016-2019 The Khronos Group Inc.
3-
* #
4-
* # Licensed under the Apache License, Version 2.0 (the "License");
5-
* # you may not use this file except in compliance with the License.
6-
* # You may obtain a copy of the License at
7-
* #
8-
* # http://www.apache.org/licenses/LICENSE-2.0
9-
* #
10-
* # Unless required by applicable law or agreed to in writing, software
11-
* # distributed under the License is distributed on an "AS IS" BASIS,
12-
* # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
* # See the License for the specific language governing permissions and
14-
* # limitations under the License.
15-
*/
1+
// Copyright 2016-2024 The Khronos Group Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
164

175
// ignore_for_file: avoid_print
186
// ignore_for_file: avoid_dynamic_calls
@@ -21,7 +9,17 @@
219

2210
import 'dart:async';
2311
import 'dart:convert';
24-
import 'dart:html' show querySelector, InputElement, File, FileReader, window;
12+
import 'dart:html'
13+
show
14+
querySelector,
15+
DataTransferItemList,
16+
DirectoryEntry,
17+
Entry,
18+
InputElement,
19+
File,
20+
FileEntry,
21+
FileReader,
22+
window;
2523
import 'dart:js';
2624
import 'dart:math';
2725

@@ -38,9 +36,13 @@ final _dropZone = querySelector('#dropZone');
3836
final _output = querySelector('#output');
3937
final _input = querySelector('#input') as InputElement;
4038
final _inputLink = querySelector('#inputLink');
39+
final _fileWarning = querySelector('#fileWarning');
4140
final _truncatedWarning = querySelector('#truncatedWarning');
4241
final _validityLabel = querySelector('#validityLabel');
4342

43+
final _isFileUri = window.location.protocol == 'file:';
44+
final _assetPattern = RegExp(r'^[^\/]*\.gl(?:tf|b)$', caseSensitive: false);
45+
4446
final _sw = Stopwatch();
4547

4648
void main() {
@@ -64,7 +66,13 @@ void main() {
6466

6567
_dropZone.onDrop.listen((e) {
6668
e.preventDefault();
67-
_validate(e.dataTransfer.files);
69+
// File and Directory Entries API may not work
70+
// when the page is opened from a local path.
71+
if (_isFileUri) {
72+
_validateFiles(e.dataTransfer.files);
73+
} else {
74+
_validateItems(e.dataTransfer.items);
75+
}
6876
});
6977

7078
_inputLink.onClick.listen((e) {
@@ -76,66 +84,101 @@ void main() {
7684
_input.onChange.listen((e) {
7785
e.preventDefault();
7886
if (_input.files.isNotEmpty) {
79-
_validate(_input.files);
87+
_validateFiles(_input.files);
8088
}
8189
});
8290

8391
print('glTF Validator ver. $kGltfValidatorVersion.');
8492
print('Supported extensions: ${Context.defaultExtensionNames.join(', ')}');
8593
}
8694

87-
void _validate(List<File> files) {
95+
void _preValidate() {
8896
_output.text = '';
97+
_fileWarning.style.display = 'none';
8998
_truncatedWarning.style.display = 'none';
9099
_validityLabel.text = 'Validating...';
91100
_dropZone.classes
92101
..clear()
93102
..add('drop');
103+
}
94104

95-
_doValidate(files).then((result) {
96-
_dropZone.classes.remove('drop');
97-
if (result != null) {
98-
if (result.context.isTruncated) {
99-
_truncatedWarning.style.display = 'block';
100-
}
105+
void _postValidate(ValidationResult result) {
106+
_dropZone.classes.remove('drop');
107+
if (result != null) {
108+
if (_isFileUri) {
109+
_fileWarning.style.display = 'block';
110+
}
101111

102-
if (result.context.errors.isEmpty) {
103-
_dropZone.classes.add('valid');
104-
_validityLabel.text = 'The asset is valid.';
105-
} else {
106-
_dropZone.classes.add('invalid');
107-
_validityLabel.text = 'The asset contains errors.';
108-
}
112+
if (result.context.isTruncated) {
113+
_truncatedWarning.style.display = 'block';
114+
}
115+
116+
if (result.context.errors.isEmpty) {
117+
_dropZone.classes.add('valid');
118+
_validityLabel.text = 'The asset is valid.';
109119
} else {
110-
_validityLabel.text = 'No glTF asset provided.';
120+
_dropZone.classes.add('invalid');
121+
_validityLabel.text = 'The asset contains errors.';
111122
}
112-
});
123+
} else {
124+
_validityLabel.text =
125+
'No glTF asset was found or a file access error has occurred.';
126+
}
127+
}
128+
129+
void _validateFiles(List<File> files) {
130+
_preValidate();
131+
final filesMap = <String, File>{for (final file in files) file.name: file};
132+
_doValidate(filesMap).then(_postValidate);
113133
}
114134

115-
Future<ValidationResult> _doValidate(List<File> files) async {
135+
void _validateItems(DataTransferItemList items) {
136+
_preValidate();
137+
_getFilesMapFromItems(items)
138+
.then(_doValidate, onError: (Object _) => null)
139+
.then(_postValidate);
140+
}
141+
142+
Future<Map<String, File>> _getFilesMapFromItems(DataTransferItemList items) {
143+
final entries = List.generate(items.length, (i) => items[i].getAsEntry(),
144+
growable: false);
145+
return _traverseDirectory(entries, <String, File>{});
146+
}
147+
148+
Future<Map<String, File>> _traverseDirectory(
149+
List<Entry> entries, Map<String, File> result) async {
150+
for (final entry in entries) {
151+
if (entry.isFile) {
152+
final fileEntry = entry as FileEntry;
153+
result[fileEntry.fullPath.substring(1)] = await fileEntry.file();
154+
} else if (entry.isDirectory) {
155+
final directoryEntry = entry as DirectoryEntry;
156+
await _traverseDirectory(
157+
await directoryEntry.createReader().readEntries(), result);
158+
}
159+
}
160+
return result;
161+
}
162+
163+
Future<ValidationResult> _doValidate(Map<String, File> files) async {
116164
_sw
117165
..reset()
118166
..start();
119-
File gltfFile;
120167
GltfReader reader;
121168

122169
final context =
123170
Context(options: ValidationOptions(maxIssues: _kMaxIssuesCount));
124171

125-
for (gltfFile in files) {
126-
final lowerCaseName = gltfFile.name.toLowerCase();
127-
if (lowerCaseName.endsWith('.gltf')) {
128-
reader = GltfJsonReader(_getFileStream(gltfFile), context);
129-
break;
130-
}
131-
if (lowerCaseName.endsWith('.glb')) {
132-
reader = GlbReader(_getFileStream(gltfFile), context);
133-
break;
134-
}
172+
final assetFilename =
173+
files.keys.firstWhere(_assetPattern.hasMatch, orElse: () => null);
174+
if (assetFilename == null) {
175+
return null;
135176
}
136177

137-
if (reader == null) {
138-
return null;
178+
if (assetFilename.toLowerCase().endsWith('.gltf')) {
179+
reader = GltfJsonReader(_getFileStream(files[assetFilename]), context);
180+
} else {
181+
reader = GlbReader(_getFileStream(files[assetFilename]), context);
139182
}
140183

141184
final readerResult = await reader.read();
@@ -149,7 +192,7 @@ Future<ValidationResult> _doValidate(List<File> files) async {
149192
}
150193
final file = _getFileByUri(files, uri);
151194
if (file != null) {
152-
return _getFile(file);
195+
return _getFileBytes(file);
153196
} else {
154197
throw GltfExternalResourceNotFoundException(uri.toString());
155198
}
@@ -174,7 +217,7 @@ Future<ValidationResult> _doValidate(List<File> files) async {
174217
await resourcesLoader.load();
175218
}
176219
final validationResult =
177-
ValidationResult(Uri.parse(gltfFile.name), context, readerResult);
220+
ValidationResult(Uri.parse(assetFilename), context, readerResult);
178221

179222
_sw.stop();
180223
print('Validation: ${_sw.elapsedMilliseconds}ms.');
@@ -188,10 +231,8 @@ Future<ValidationResult> _doValidate(List<File> files) async {
188231
return validationResult;
189232
}
190233

191-
File _getFileByUri(List<File> files, Uri uri) {
192-
final fileName = Uri.decodeComponent(uri.path);
193-
return files.firstWhere((file) => file.name == fileName, orElse: () => null);
194-
}
234+
File _getFileByUri(Map<String, File> files, Uri uri) =>
235+
files[Uri.decodeComponent(uri.path)];
195236

196237
Stream<Uint8List> _getFileStream(File file) {
197238
var isCanceled = false;
@@ -227,7 +268,7 @@ Stream<Uint8List> _getFileStream(File file) {
227268
return controller.stream;
228269
}
229270

230-
Future<Uint8List> _getFile(File file) async {
271+
Future<Uint8List> _getFileBytes(File file) async {
231272
final fileReader = FileReader()..readAsArrayBuffer(file);
232273
await fileReader.onLoadEnd.first;
233274
final result = fileReader.result;
@@ -241,8 +282,7 @@ void _writeMap(Map<String, Object> jsonMap) {
241282
final report = _kJsonEncoder.convert(jsonMap);
242283
_output.text = report;
243284
if (report.length < _kMaxReportLength) {
244-
context['Prism']
245-
.callMethod('highlightAll', [window.location.protocol != 'file:']);
285+
context['Prism'].callMethod('highlightAll', [!_isFileUri]);
246286
} else {
247287
print('Report is too big: ${report.length} bytes. '
248288
'Syntax highlighting disabled.');

web/styles/styles.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,6 @@ html, body {
9797
cursor: pointer;
9898
}
9999

100-
#truncatedWarning {
100+
.warning {
101101
color: #c92c2c;
102102
}

0 commit comments

Comments
 (0)