// // modelUploader.js // examples/libraries // // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // modelUploader = (function () { var that = {}, modelFile, modelName, modelURL, modelCallback, isProcessing, fstBuffer, fbxBuffer, //svoBuffer, mapping, geometry, API_URL = "https://metaverse.highfidelity.com/api/v1/models", MODEL_URL = "http://public.highfidelity.com/models/content", NAME_FIELD = "name", SCALE_FIELD = "scale", FILENAME_FIELD = "filename", TEXDIR_FIELD = "texdir", MAX_TEXTURE_SIZE = 1024; function info(message) { if (progressDialog.isOpen()) { progressDialog.update(message); } else { progressDialog.open(message); } print(message); } function error(message) { if (progressDialog.isOpen()) { progressDialog.close(); } print(message); Window.alert(message); } function randomChar(length) { var characters = "0123457689abcdefghijklmnopqrstuvwxyz", string = "", i; for (i = 0; i < length; i += 1) { string += characters[Math.floor(Math.random() * 36)]; } return string; } function resetDataObjects() { fstBuffer = null; fbxBuffer = null; //svoBuffer = null; mapping = {}; geometry = {}; geometry.textures = []; geometry.embedded = []; } function readFile(filename) { var url = "file:///" + filename, req = new XMLHttpRequest(); req.open("GET", url, false); req.responseType = "arraybuffer"; req.send(); if (req.status !== 200) { error("Could not read file: " + filename + " : " + req.statusText); return null; } return { filename: filename.fileName(), buffer: req.response }; } function readMapping(buffer) { var dv = new DataView(buffer.buffer), lines, line, tokens, i, name, value, remainder, existing; mapping = {}; // { name : value | name : { value : [remainder] } } lines = dv.string(0, dv.byteLength).split(/\r\n|\r|\n/); for (i = 0; i < lines.length; i += 1) { line = lines[i].trim(); if (line.length > 0 && line[0] !== "#") { tokens = line.split(/\s*=\s*/); if (tokens.length > 1) { name = tokens[0]; value = tokens[1]; if (tokens.length > 2) { remainder = tokens.slice(2, tokens.length).join(" = "); } else { remainder = null; } if (tokens.length === 2 && mapping[name] === undefined) { mapping[name] = value; } else { if (mapping[name] === undefined) { mapping[name] = {}; } else if (typeof mapping[name] !== "object") { existing = mapping[name]; mapping[name] = { existing : null }; } if (mapping[name][value] === undefined) { mapping[name][value] = []; } mapping[name][value].push(remainder); } } } } } function writeMapping(buffer) { var name, value, remainder, i, string = ""; for (name in mapping) { if (mapping.hasOwnProperty(name)) { if (typeof mapping[name] === "object") { for (value in mapping[name]) { if (mapping[name].hasOwnProperty(value)) { remainder = mapping[name][value]; if (remainder === null) { string += (name + " = " + value + "\n"); } else { for (i = 0; i < remainder.length; i += 1) { string += (name + " = " + value + " = " + remainder[i] + "\n"); } } } } } else { string += (name + " = " + mapping[name] + "\n"); } } } buffer.buffer = string.toArrayBuffer(); } function readGeometry(fbxBuffer) { var textures, view, index, EOF, previousNodeFilename; // Reference: // http://code.blender.org/index.php/2013/08/fbx-binary-file-format-specification/ textures = {}; view = new DataView(fbxBuffer.buffer); EOF = false; function parseBinaryFBX() { var endOffset, numProperties, propertyListLength, nameLength, name, filename; endOffset = view.getUint32(index, true); numProperties = view.getUint32(index + 4, true); propertyListLength = view.getUint32(index + 8, true); nameLength = view.getUint8(index + 12); index += 13; if (endOffset === 0) { return; } if (endOffset < index || endOffset > view.byteLength) { EOF = true; return; } name = view.string(index, nameLength).toLowerCase(); index += nameLength; if (name === "content" && previousNodeFilename !== "") { // Blender 2.71 exporter "embeds" external textures as empty binary blobs so ignore these if (propertyListLength > 5) { geometry.embedded.push(previousNodeFilename); } } if (name === "relativefilename") { filename = view.string(index + 5, view.getUint32(index + 1, true)).fileName(); if (!textures.hasOwnProperty(filename)) { textures[filename] = ""; geometry.textures.push(filename); } previousNodeFilename = filename; } else { previousNodeFilename = ""; } index += (propertyListLength); while (index < endOffset && !EOF) { parseBinaryFBX(); } } function readTextFBX() { var line, view, viewLength, charCode, charCodes, numCharCodes, filename, relativeFilename = "", MAX_CHAR_CODES = 250; view = new Uint8Array(fbxBuffer.buffer); viewLength = view.byteLength; charCodes = []; numCharCodes = 0; for (index = 0; index < viewLength; index += 1) { charCode = view[index]; if (charCode !== 9 && charCode !== 32) { if (charCode === 10) { // EOL. Can ignore EOF. line = String.fromCharCode.apply(String, charCodes).toLowerCase(); // For embedded textures, "Content:" line immediately follows "RelativeFilename:" line. if (line.slice(0, 8) === "content:" && relativeFilename !== "") { geometry.embedded.push(relativeFilename); } if (line.slice(0, 17) === "relativefilename:") { filename = line.slice(line.indexOf("\""), line.lastIndexOf("\"") - line.length).fileName(); if (!textures.hasOwnProperty(filename)) { textures[filename] = ""; geometry.textures.push(filename); } relativeFilename = filename; } else { relativeFilename = ""; } charCodes = []; numCharCodes = 0; } else { if (numCharCodes < MAX_CHAR_CODES) { // Only interested in start of line charCodes.push(charCode); numCharCodes += 1; } } } } } readTextFBX(); } function readModel() { var fbxFilename, //svoFilename, fileType; info("Reading model file"); print("Model file: " + modelFile); if (modelFile.toLowerCase().fileType() === "fst") { fstBuffer = readFile(modelFile); if (fstBuffer === null) { return false; } readMapping(fstBuffer); fileType = mapping[FILENAME_FIELD].toLowerCase().fileType(); if (mapping.hasOwnProperty(FILENAME_FIELD)) { if (fileType === "fbx") { fbxFilename = modelFile.path() + "\\" + mapping[FILENAME_FIELD]; //} else if (fileType === "svo") { // svoFilename = modelFile.path() + "\\" + mapping[FILENAME_FIELD]; } else { error("Unrecognized model type in FST file!"); return false; } } else { error("Model file name not found in FST file!"); return false; } } else { fstBuffer = { filename: "Interface." + randomChar(6), // Simulate avatar model uploading behaviour buffer: null }; if (modelFile.toLowerCase().fileType() === "fbx") { fbxFilename = modelFile; mapping[FILENAME_FIELD] = modelFile.fileName(); //} else if (modelFile.toLowerCase().fileType() === "svo") { // svoFilename = modelFile; // mapping[FILENAME_FIELD] = modelFile.fileName(); } else { error("Unrecognized file type: " + modelFile); return false; } } if (!isProcessing) { return false; } if (fbxFilename) { fbxBuffer = readFile(fbxFilename); if (fbxBuffer === null) { return false; } if (!isProcessing) { return false; } readGeometry(fbxBuffer); } //if (svoFilename) { // svoBuffer = readFile(svoFilename); // if (svoBuffer === null) { // return false; // } //} // Add any missing basic mappings if (!mapping.hasOwnProperty(NAME_FIELD)) { mapping[NAME_FIELD] = modelFile.fileName().fileBase(); } if (!mapping.hasOwnProperty(TEXDIR_FIELD)) { mapping[TEXDIR_FIELD] = "."; } if (!mapping.hasOwnProperty(SCALE_FIELD)) { mapping[SCALE_FIELD] = 1.0; } return true; } function setProperties() { var form = [], directory, displayAs, validateAs; progressDialog.close(); print("Setting model properties"); form.push({ label: "Name:", value: mapping[NAME_FIELD] }); directory = modelFile.path() + "/" + mapping[TEXDIR_FIELD]; displayAs = new RegExp("^" + modelFile.path().regExpEscape() + "[\\\\\\\/](.*)"); validateAs = new RegExp("^" + modelFile.path().regExpEscape() + "([\\\\\\\/].*)?"); form.push({ label: "Texture directory:", directory: modelFile.path() + "/" + mapping[TEXDIR_FIELD], title: "Choose Texture Directory", displayAs: displayAs, validateAs: validateAs, errorMessage: "Texture directory must be subdirectory of the model directory." }); form.push({ button: "Cancel" }); if (!Window.form("Set Model Properties", form)) { print("User cancelled uploading model"); return false; } mapping[NAME_FIELD] = form[0].value; mapping[TEXDIR_FIELD] = form[1].directory.slice(modelFile.path().length + 1); if (mapping[TEXDIR_FIELD] === "") { mapping[TEXDIR_FIELD] = "."; } writeMapping(fstBuffer); return true; } function createHttpMessage(callback) { var multiparts = [], lodCount, lodFile, lodBuffer, textureBuffer, textureSourceFormat, textureTargetFormat, embeddedTextures, i; info("Preparing to send model"); // Model name if (mapping.hasOwnProperty(NAME_FIELD)) { multiparts.push({ name : "model_name", string : mapping[NAME_FIELD] }); } else { error("Model name is missing"); httpMultiPart.clear(); return; } // FST file if (fstBuffer) { multiparts.push({ name : "fst", buffer: fstBuffer }); } // FBX file if (fbxBuffer) { multiparts.push({ name : "fbx", buffer: fbxBuffer }); } // SVO file //if (svoBuffer) { // multiparts.push({ // name : "svo", // buffer: svoBuffer // }); //} // LOD files lodCount = 0; for (lodFile in mapping.lod) { if (mapping.lod.hasOwnProperty(lodFile)) { lodBuffer = readFile(modelFile.path() + "\/" + lodFile); if (lodBuffer === null) { return; } multiparts.push({ name: "lod" + lodCount, buffer: lodBuffer }); lodCount += 1; } if (!isProcessing) { return; } } // Textures embeddedTextures = "|" + geometry.embedded.join("|") + "|"; for (i = 0; i < geometry.textures.length; i += 1) { if (embeddedTextures.indexOf("|" + geometry.textures[i].fileName() + "|") === -1) { textureBuffer = readFile(modelFile.path() + "\/" + (mapping[TEXDIR_FIELD] !== "." ? mapping[TEXDIR_FIELD] + "\/" : "") + geometry.textures[i]); if (textureBuffer === null) { return; } textureSourceFormat = geometry.textures[i].fileType().toLowerCase(); textureTargetFormat = (textureSourceFormat === "jpg" ? "jpg" : "png"); textureBuffer.buffer = textureBuffer.buffer.recodeImage(textureSourceFormat, textureTargetFormat, MAX_TEXTURE_SIZE); textureBuffer.filename = textureBuffer.filename.slice(0, -textureSourceFormat.length) + textureTargetFormat; multiparts.push({ name: "texture" + i, buffer: textureBuffer }); } if (!isProcessing) { return; } } // Model category multiparts.push({ name : "model_category", string : "content" }); // Create HTTP message httpMultiPart.clear(); Script.setTimeout(function addMultipart() { var multipart = multiparts.shift(); httpMultiPart.add(multipart); if (!isProcessing) { return; } if (multiparts.length > 0) { Script.setTimeout(addMultipart, 25); } else { callback(); } }, 25); } function sendToHighFidelity() { var req, uploadedChecks, HTTP_GET_TIMEOUT = 60, // 1 minute HTTP_SEND_TIMEOUT = 900, // 15 minutes UPLOADED_CHECKS = 30, CHECK_UPLOADED_TIMEOUT = 1, // 1 second handleCheckUploadedResponses, handleUploadModelResponses, handleRequestUploadResponses; function uploadTimedOut() { error("Model upload failed: Internet request timed out!"); } function debugResponse() { print("req.errorCode = " + req.errorCode); print("req.readyState = " + req.readyState); print("req.status = " + req.status); print("req.statusText = " + req.statusText); print("req.responseType = " + req.responseType); print("req.responseText = " + req.responseText); print("req.response = " + req.response); print("req.getAllResponseHeaders() = " + req.getAllResponseHeaders()); } function checkUploaded() { if (!isProcessing) { return; } info("Checking uploaded model"); req = new XMLHttpRequest(); req.open("HEAD", modelURL, true); req.timeout = HTTP_GET_TIMEOUT * 1000; req.onreadystatechange = handleCheckUploadedResponses; req.ontimeout = uploadTimedOut; req.send(); } handleCheckUploadedResponses = function () { //debugResponse(); if (req.readyState === req.DONE) { if (req.status === 200) { // Note: Unlike avatar models, for content models we don't need to refresh texture cache. print("Model uploaded: " + modelURL); progressDialog.close(); if (Window.confirm("Your model has been uploaded as: " + modelURL + "\nDo you want to rez it?")) { modelCallback(modelURL); } } else if (req.status === 404) { if (uploadedChecks > 0) { uploadedChecks -= 1; Script.setTimeout(checkUploaded, CHECK_UPLOADED_TIMEOUT * 1000); } else { print("Error: " + req.status + " " + req.statusText); error("We could not verify that your model was successfully uploaded but it may have been at: " + modelURL); } } else { print("Error: " + req.status + " " + req.statusText); error("There was a problem with your upload, please try again later."); } } }; function uploadModel(method) { var url; if (!isProcessing) { return; } req = new XMLHttpRequest(); if (method === "PUT") { url = API_URL + "\/" + modelName; req.open("PUT", url, true); //print("PUT " + url); } else { url = API_URL; req.open("POST", url, true); //print("POST " + url); } req.setRequestHeader("Content-Type", "multipart/form-data; boundary=\"" + httpMultiPart.boundary() + "\""); req.timeout = HTTP_SEND_TIMEOUT * 1000; req.onreadystatechange = handleUploadModelResponses; req.ontimeout = uploadTimedOut; req.send(httpMultiPart.response().buffer); } handleUploadModelResponses = function () { //debugResponse(); if (req.readyState === req.DONE) { if (req.status === 200) { uploadedChecks = UPLOADED_CHECKS; checkUploaded(); } else { print("Error: " + req.status + " " + req.statusText); error("There was a problem with your upload, please try again later."); } } }; function requestUpload() { var url; if (!isProcessing) { return; } url = API_URL + "\/" + modelName; // XMLHttpRequest automatically handles authorization of API requests. req = new XMLHttpRequest(); req.open("GET", url, true); //print("GET " + url); req.responseType = "json"; req.timeout = HTTP_GET_TIMEOUT * 1000; req.onreadystatechange = handleRequestUploadResponses; req.ontimeout = uploadTimedOut; req.send(); } handleRequestUploadResponses = function () { var response; //debugResponse(); if (req.readyState === req.DONE) { if (req.status === 200) { if (req.responseType === "json") { response = JSON.parse(req.responseText); if (response.status === "success") { if (response.exists === false) { uploadModel("POST"); } else if (response.can_update === true) { uploadModel("PUT"); } else { error("This model file already exists and is owned by someone else!"); } return; } } } else { print("Error: " + req.status + " " + req.statusText); } error("Model upload failed! Something went wrong at the data server."); } }; info("Sending model to High Fidelity"); requestUpload(); } that.upload = function (file, callback) { modelFile = file; modelCallback = callback; isProcessing = true; progressDialog.onCancel = function () { print("User cancelled uploading model"); isProcessing = false; }; resetDataObjects(); if (readModel()) { if (setProperties()) { modelName = mapping[NAME_FIELD]; modelURL = MODEL_URL + "\/" + mapping[NAME_FIELD] + ".fst"; // All models are uploaded as an FST createHttpMessage(sendToHighFidelity); } } resetDataObjects(); }; return that; }());