diff --git a/javascript/MaterialXView/source/dropHandling.js b/javascript/MaterialXView/source/dropHandling.js new file mode 100644 index 0000000000..adcbef2ca1 --- /dev/null +++ b/javascript/MaterialXView/source/dropHandling.js @@ -0,0 +1,281 @@ +import * as THREE from 'three'; +import * as fflate from 'three/examples/jsm/libs/fflate.module.js'; + +const debugFileHandling = false; +let loadingCallback; + +export function setLoadingCallback(cb) { + loadingCallback = cb; +} + +export function dropHandler(ev) { + if (debugFileHandling) console.log('File(s) dropped', ev.dataTransfer.items, ev.dataTransfer.files); + + // Prevent default behavior (Prevent file from being opened) + ev.preventDefault(); + + if (ev.dataTransfer.items) + { + const allEntries = []; + + let haveGetAsEntry = false; + if (ev.dataTransfer.items.length > 0) + haveGetAsEntry = + ("getAsEntry" in ev.dataTransfer.items[0]) || + ("webkitGetAsEntry" in ev.dataTransfer.items[0]); + + // Useful for debugging file handling on platforms that don't support newer file system APIs + // haveGetAsEntry = false; + + if (haveGetAsEntry) { + for (var i = 0; i < ev.dataTransfer.items.length; i++) + { + let item = ev.dataTransfer.items[i]; + let entry = ("getAsEntry" in item) ? item.getAsEntry() : item.webkitGetAsEntry(); + allEntries.push(entry); + } + handleFilesystemEntries(allEntries); + return; + } + + for (var i = 0; i < ev.dataTransfer.items.length; i++) + { + let item = ev.dataTransfer.items[i]; + + // API when there's no "getAsEntry" support + console.log(item.kind, item); + if (item.kind === 'file') + { + var file = item.getAsFile(); + testAndLoadFile(file); + } + // could also be a directory + else if (item.kind === 'directory') + { + var dirReader = item.createReader(); + dirReader.readEntries(function(entries) { + for (var i = 0; i < entries.length; i++) { + console.log(entries[i].name); + var entry = entries[i]; + if (entry.isFile) { + entry.file(function(file) { + testAndLoadFile(file); + }); + } + } + }); + } + } + } else { + for (var i = 0; i < ev.dataTransfer.files.length; i++) { + let file = ev.dataTransfer.files[i]; + testAndLoadFile(file); + } + } +} + +export function dragOverHandler(ev) { + ev.preventDefault(); +} + +async function getBufferFromFile(fileEntry) { + + if (fileEntry instanceof ArrayBuffer) return fileEntry; + if (fileEntry instanceof String) return fileEntry; + + const name = fileEntry.fullPath || fileEntry.name; + const ext = name.split('.').pop(); + const readAsText = ext === 'mtlx'; + + if (debugFileHandling) console.log("reading ", fileEntry, "as text?", readAsText); + + if (debugFileHandling) console.log("getBufferFromFile", fileEntry); + const buffer = await new Promise((resolve, reject) => { + function readFile(file) { + var reader = new FileReader(); + reader.onloadend = function(e) { + if (debugFileHandling) console.log("loaded", "should be text?", readAsText, this.result); + resolve(this.result); + }; + + if (readAsText) + reader.readAsText(file); + else + reader.readAsArrayBuffer(file); + } + + if ("file" in fileEntry) { + fileEntry.file(function(file) { + readFile(file); + }, (e) => { + console.error("Error reading file ", e); + }); + } + else { + readFile(fileEntry); + } + }); + return buffer; +} + +async function handleFilesystemEntries(entries) { + const allFiles = []; + const fileIgnoreList = [ + '.gitignore', + 'README.md', + '.DS_Store', + ] + const dirIgnoreList = [ + '.git', + 'node_modules', + ] + + for (let entry of entries) { + if (debugFileHandling) console.log("file entry", entry) + if (entry.isFile) { + if (debugFileHandling) console.log("single file", entry); + if (fileIgnoreList.includes(entry.name)) { + continue; + } + allFiles.push(entry); + } + else if (entry.isDirectory) { + if (dirIgnoreList.includes(entry.name)) { + continue; + } + const files = await readDirectory(entry); + if (debugFileHandling) console.log("all files", files); + for (const file of files) { + if (fileIgnoreList.includes(file.name)) { + continue; + } + allFiles.push(file); + } + } + } + + const imageLoader = new THREE.ImageLoader(); + + // unpack zip files first + for (const fileEntry of allFiles) { + // special case: zip archives + if (fileEntry.fullPath.toLowerCase().endsWith('.zip')) { + await new Promise(async (resolve, reject) => { + const arrayBuffer = await getBufferFromFile(fileEntry); + + // use fflate to unpack them and add the files to the cache + fflate.unzip(new Uint8Array(arrayBuffer), (error, unzipped) => { + // push these files into allFiles + for (const [filePath, buffer] of Object.entries(unzipped)) { + + // mock FileEntry for easier usage downstream + const blob = new Blob([buffer]); + const newFileEntry = { + fullPath: "/" + filePath, + name: filePath.split('/').pop(), + file: (callback) => { + callback(blob); + }, + isFile: true, + }; + allFiles.push(newFileEntry); + } + + resolve(); + }); + }); + } + } + + // sort so mtlx files come first + allFiles.sort((a, b) => { + if (a.name.endsWith('.mtlx') && !b.name.endsWith('.mtlx')) { + return -1; + } + if (!a.name.endsWith('.mtlx') && b.name.endsWith('.mtlx')) { + return 1; + } + return 0; + }); + + if (debugFileHandling) console.log("all files", allFiles); + + // put all files in three' Cache + for (const fileEntry of allFiles) { + + const allowedFileTypes = [ + 'png', 'jpg', 'jpeg' + ]; + + const ext = fileEntry.fullPath.split('.').pop(); + if (!allowedFileTypes.includes(ext)) { + // console.log("skipping file", fileEntry.fullPath); + continue; + } + + const buffer = await getBufferFromFile(fileEntry); + const img = await imageLoader.loadAsync(URL.createObjectURL(new Blob([buffer]))); + if (debugFileHandling) console.log("caching file", fileEntry.fullPath, img); + THREE.Cache.add(fileEntry.fullPath, img); + } + + // TODO we could also allow dropping of multiple MaterialX files (or folders with them inside) + // and seed the dropdown from that. + // At that point, actually reading files and textures into memory should be deferred until they are actually used. + const rootFile = allFiles[0]; + THREE.Cache.add(rootFile.fullPath, await getBufferFromFile(rootFile)); + + if (debugFileHandling) console.log("CACHE", THREE.Cache.files); + + loadingCallback(rootFile); +} + +async function readDirectory(directory) { + let entries = []; + let getEntries = async (directory) => { + let dirReader = directory.createReader(); + await new Promise((resolve, reject) => { + dirReader.readEntries( + async (results) => { + if (results.length) { + // entries = entries.concat(results); + for (let entry of results) { + if (entry.isDirectory) { + await getEntries(entry); + } + else { + entries.push(entry); + } + } + } + resolve(); + }, + (error) => { + /* handle error — error is a FileError object */ + }, + )} + )}; + + await getEntries(directory); + return entries; +} + +async function testAndLoadFile(file) { + let ext = file.name.split('.').pop(); + if (debugFileHandling) console.log(file.name + ", " + file.size + ", " + ext); + + const arrayBuffer = await getBufferFromFile(file); + console.log(arrayBuffer) + + // mock a fileEntry and pass through the same loading logic + const newFileEntry = { + fullPath: "/" + file.name, + name: file.name.split('/').pop(), + isFile: true, + file: (callback) => { + callback(file); + } + }; + + handleFilesystemEntries([newFileEntry]); +} \ No newline at end of file diff --git a/javascript/MaterialXView/source/index.js b/javascript/MaterialXView/source/index.js index 424f63ec43..56e2e1f5a2 100644 --- a/javascript/MaterialXView/source/index.js +++ b/javascript/MaterialXView/source/index.js @@ -12,6 +12,7 @@ import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'; import { GammaCorrectionShader } from 'three/examples/jsm/shaders/GammaCorrectionShader.js'; import { Viewer } from './viewer.js' +import { dropHandler, dragOverHandler, setLoadingCallback } from './dropHandling.js'; let renderer, composer, orbitControls; @@ -115,6 +116,20 @@ function init() console.error(Number.isInteger(err) ? this.getMx().getExceptionMessage(err) : err); }) + // allow dropping files and directories + document.addEventListener('drop', dropHandler, false); + document.addEventListener('dragover', dragOverHandler, false); + + setLoadingCallback(file => { + materialFilename = file.fullPath || file.name; + viewer.getEditor().clearFolders(); + viewer.getMaterial().loadMaterials(viewer, materialFilename); + viewer.getEditor().updateProperties(0.9); + viewer.getScene().setUpdateTransforms(); + }); + + // enable three.js Cache so that dropped files can reference each other + THREE.Cache.enabled = true; } function onWindowResize() diff --git a/javascript/MaterialXView/source/viewer.js b/javascript/MaterialXView/source/viewer.js index 0bc537faf0..d1adfd9ef4 100644 --- a/javascript/MaterialXView/source/viewer.js +++ b/javascript/MaterialXView/source/viewer.js @@ -589,6 +589,7 @@ export class Material // Set search path. Assumes images are relative to current file // location. + if (!materialFilename) materialFilename = "/"; const paths = materialFilename.split('/'); paths.pop(); const searchPath = paths.join('/');