Files
UnrealEngine/Engine/Source/ThirdParty/MaterialX/MaterialX-1.38.10/javascript/MaterialXView/source/dropHandling.js
2025-05-18 13:04:45 +08:00

375 lines
10 KiB
JavaScript

import * as THREE from 'three';
import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
const debugFileHandling = false;
let loadingCallback = null;
let sceneLoadingCallback = null;
export function setLoadingCallback(cb)
{
loadingCallback = cb;
}
export function setSceneLoadingCallback(cb)
{
sceneLoadingCallback = 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',
]
let isGLB = false;
let haveMtlx = false;
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);
if (entry.name.endsWith('glb'))
{
isGLB = true;
break;
}
}
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 (isGLB)
{
console.log("Load GLB file", allFiles[0]);
const rootFile = allFiles[0];
THREE.Cache.add(rootFile.fullPath, await getBufferFromFile(rootFile));
if (debugFileHandling) console.log("CACHE", THREE.Cache.files);
sceneLoadingCallback(rootFile);
return;
}
if (!allFiles[0].name.endsWith('mtlx'))
{
console.log("No MaterialX files dropped. Skipping content.");
return;
}
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.
if (allFiles.length > 0)
{
const rootFile = allFiles[0];
THREE.Cache.add(rootFile.fullPath, await getBufferFromFile(rootFile));
if (debugFileHandling) console.log("CACHE", THREE.Cache.files);
loadingCallback(rootFile);
}
else
{
console.log('No files to add cache.')
}
}
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]);
}