@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in
194 lines • 8.66 kB
JavaScript
import { isDevEnvironment } from "./debug/index.js";
import { getParam } from "./engine_utils.js";
const debug = getParam("debugfileformat");
/**
* Tries to determine the file type of a file from its URL
* This method does perform a range request to the server to get the first few bytes of the file
* If the file type can not be determined it will return "unknown"
* @param url The URL of the file
* @param useExtension If true the file type will be determined by the file extension first - if the file extension is not known it will then check the header
* @example
* ```typescript
* const url = "https://example.com/model.glb";
* const fileType = await tryDetermineFileTypeFromURL(url);
* console.log(fileType); // "glb"
*/
export async function tryDetermineFileTypeFromURL(url, useExtension = true) {
if (useExtension) {
// We want to save on requests so we first check the file extension if there's any
// In some scenarios we might not have one (e.g. if we're dealing with blob: files or if the URL doesn't contain the filename)
// In that case we need to check the header
const _url = url;
// if (!_url.startsWith("http") && !url.startsWith("blob:")) {
// // _url = "file:" + url;
// }
const urlobj = new URL(_url, globalThis.location.origin);
let ext = null;
const query = urlobj.searchParams.get("filetype");
if (query)
ext = query.toUpperCase();
if (!ext?.length) {
ext = urlobj.pathname.split(".").pop()?.toUpperCase();
}
if (debug)
console.debug("Use file extension to determine type: " + ext);
switch (ext) {
case "GLTF":
return "gltf";
case "VRM":
return "vrm";
case "GLB":
return "glb";
case "FBX":
return "fbx";
case "USD":
return "usd";
case "USDA":
return "usda";
case "USDZ":
return "usdz";
case "OBJ":
return "obj";
}
}
// If the URL doesnt contain a filetype we need to check the header
// This is the case for example if we load a file from a data url
if (url.startsWith("blob:")) {
// We can't modify the blob URL
}
else {
const newUrl = new URL(url);
// Adding a URL parameter to avoid the brower to bust the full cache
// If we don't do this the file that might already be disc cached will be deleted from the cache
newUrl.searchParams.append("range", "true");
url = newUrl.toString();
}
const header = await fetch(url, {
method: "GET",
headers: {
"range": "bytes=0-32"
}
}).catch(_ => {
return null;
});
if (header?.ok) {
const data = await header.arrayBuffer();
const res = tryDetermineFileTypeFromBinary(data, header);
if (debug)
console.log("Determined file type from header: " + res);
return res;
}
return "unknown";
}
/** Attempts to determine the file type of a binary file by looking at the first few bytes of the file.
* @hidden
*/
export function tryDetermineFileTypeFromBinary(data, response) {
if (data.byteLength < 4) {
return "unknown";
}
const bytes = new Uint8Array(data);
if (debug) {
console.warn("Trying to determine file type from binary data\n", "\"" + new TextDecoder().decode(data) + "\"\n", bytes);
}
// GLTF or GLB
if (bytes[0] == 103 && bytes[1] == 108 && bytes[2] == 84 && bytes[3] == 70) {
console.debug("GLTF detected");
return "glb";
}
// USDZ
if (bytes[0] == 80 && bytes[1] == 75 && bytes[2] == 3 && bytes[3] == 4) {
console.debug("USDZ detected");
return "usdz";
}
// USD
if (bytes[0] == 80 && bytes[1] == 88 && bytes[2] == 82 && bytes[3] == 45 && bytes[4] == 85 && bytes[5] == 83 && bytes[6] == 68 && bytes[7] == 67) {
console.debug("Binary USD detected");
return "usd";
}
// USDA: check if the file starts with #usda
else if (bytes[0] == 35 && bytes[1] == 117 && bytes[2] == 115 && bytes[3] == 100 && bytes[4] == 97) {
console.debug("ASCII USD detected");
return "usda";
}
// FBX
if (bytes[0] == 75 && bytes[1] == 97 && bytes[2] == 121 && bytes[3] == 100 && bytes[4] == 97 && bytes[5] == 114 && bytes[6] == 97 && bytes[7] == 32) {
console.debug("Binary FBX detected");
return "fbx";
}
// ASCII FBX
else if (bytes[0] == 59 && bytes[1] == 32 && bytes[2] == 70 && bytes[3] == 66 && bytes[4] == 88 && bytes[5] == 32) {
console.debug("ASCII FBX detected");
return "fbx";
}
// OBJ - in this case exported from blender it starts with "# Blender" - we only check the first 10 bytes, technically it could still be a different file so we should do this check at the end
else if (bytes[0] == 35 && bytes[1] == 32 && bytes[2] == 66 && bytes[3] == 108 && bytes[4] == 101 && bytes[5] == 110 && bytes[6] == 100 && bytes[7] == 101 && bytes[8] == 114 && bytes[9] == 32) {
console.debug("OBJ detected");
return "obj";
}
// Check if it starts "# Alias OBJ"
else if (bytes[0] == 35 && bytes[1] == 32 && bytes[2] == 65 && bytes[3] == 108 && bytes[4] == 105 && bytes[5] == 97 && bytes[6] == 115 && bytes[7] == 32 && bytes[8] == 79 && bytes[9] == 66 && bytes[10] == 74) {
console.debug("OBJ detected");
return "obj";
}
else if (response.headers.has("content-type")) {
const content_type = response.headers.get("content-type");
console.debug("Content-Type: " + content_type);
switch (content_type) {
case "model/gltf+json":
return "gltf";
case "model/gltf-binary":
return "glb";
case "model/vrm":
return "vrm";
case "model/vnd.usdz+zip":
return "usdz";
case "model/vnd.usd+zip":
return "usd";
case "model/vnd.usda+zip":
return "usda";
case "model/fbx":
case "model/vnd.autodesk.fbx":
return "fbx";
case "model/obj":
return "obj";
// case "model/stl":
// return "stl";
case "text/plain":
// Should we assume obj here?
break;
}
}
// Check if it starts with "v " (vertex) or "f " (face) indicating that it's an OBJ file
// If the OBJ file does lack a header altogether and no other check matched
if ((bytes[0] == 118 && bytes[1] == 32) || (bytes[0] == 102 && bytes[1] == 32)) {
console.debug("OBJ detected (the file has no header and starts with vertex or face)");
return "obj";
}
// Check if the file starts with "# File exported by ZBrush"
else if (bytes[0] == 35 && bytes[1] == 32 && bytes[2] == 70 && bytes[3] == 105 && bytes[4] == 108 && bytes[5] == 101 && bytes[6] == 32 && bytes[7] == 101 && bytes[8] == 120 && bytes[9] == 112 && bytes[10] == 111 && bytes[11] == 114 && bytes[12] == 116 && bytes[13] == 101 && bytes[14] == 100 && bytes[15] == 32 && bytes[16] == 98 && bytes[17] == 121 && bytes[18] == 32 && bytes[19] == 90 && bytes[20] == 66 && bytes[21] == 114 && bytes[22] == 117 && bytes[23] == 115 && bytes[24] == 104) {
console.debug("OBJ detected (exported by ZBrush)");
return "obj";
}
// Check if the file starts with mtllib (indicating that it's an OBJ file)
else if (bytes[0] == 109 && bytes[1] == 116 && bytes[2] == 108 && bytes[3] == 108 && bytes[4] == 105 && bytes[5] == 98) {
console.debug("OBJ detected (mtllib)");
return "obj";
}
if (isDevEnvironment() || debug) {
const text = new TextDecoder().decode(data.slice(0, 16));
console.debug("Could not determine file type from binary data: \"" + text + "...\"", bytes);
}
else {
console.debug("Could not determine file type from binary data", bytes);
}
// const text = new TextDecoder().decode(data);
// if (text.startsWith("Kaydara FBX")) {
// return "fbx";
// }
// else if (text.startsWith("glTF")) {
// return "gltf";
// }
return "unknown";
}
//# sourceMappingURL=engine_utils_format.js.map