Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add drag & drop loading to Web Viewer #1482

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 281 additions & 0 deletions javascript/MaterialXView/source/dropHandling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import * as THREE from 'three';
import * as fflate from 'three/examples/jsm/libs/fflate.module.js';

const debugFileHandling = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a general comment, can you add some function header comments for this new file.
You can follow the convention user for the other files.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this to the top along with the other debug flag?

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 = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to not include these, but curious as to why these specific files are specified ?

'.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 = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why only these file types ? Is this all the three.js loader can handle?
Shouldn't this just try and load and catch if it fails.

Also should the extension check be case insensitive?

'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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Should this line be removed?

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]);
}
15 changes: 15 additions & 0 deletions javascript/MaterialXView/source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions javascript/MaterialXView/source/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('/');
Expand Down