From f16eb0fd896879165e8fbc203e1f352d47291ee6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 19 Jul 2014 14:33:48 -0700 Subject: [PATCH 01/62] Encapsulate and lint "new model" toolbar --- examples/editModels.js | 204 +++++++++++++++++++++++------------------ 1 file changed, 114 insertions(+), 90 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 458ddf7b4a..0c6ac8b426 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -9,12 +9,12 @@ // // If using the hydras : // grab grab models with the triggers, you can then move the models around or scale them with both hands. -// You can switch mode using the bumpers so that you can move models roud more easily. +// You can switch mode using the bumpers so that you can move models around more easily. // // If using the mouse : // - left click lets you move the model in the plane facing you. -// If pressing shift, it will move on the horizontale plane it's in. -// - right click lets you rotate the model. z and x give you access to more axix of rotation while shift allows for finer control. +// If pressing shift, it will move on the horizontal plane it's in. +// - right click lets you rotate the model. z and x give access to more axes of rotation while shift provides finer control. // - left + right click lets you scale the model. // - you can press r while holding the model to reset its rotation // @@ -39,27 +39,122 @@ var MAX_ANGULAR_SIZE = 45; var LEFT = 0; var RIGHT = 1; - var SPAWN_DISTANCE = 1; -var radiusDefault = 0.10; +var DEFAULT_RADIUS = 0.10; var modelURLs = [ - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Feisar_Ship.FBX", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/birarda/birarda_head.fbx", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/pug.fbx", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/newInvader16x16-large-purple.svo", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/minotaur/mino_full.fbx", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Combat_tank_V01.FBX", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/orc.fbx", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/slimer.fbx", - ]; - -var toolBar; + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Feisar_Ship.FBX", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/birarda/birarda_head.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/pug.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/newInvader16x16-large-purple.svo", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/minotaur/mino_full.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Combat_tank_V01.FBX", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/orc.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/slimer.fbx" + ]; var jointList = MyAvatar.getJointNames(); var mode = 0; + +var toolBar = (function () { + var that = {}, + toolBar, + newModelButton, + browseModelsButton; + + function initialize() { + toolBar = new ToolBar(0, 0, ToolBar.VERTICAL); + newModelButton = toolBar.addTool({ + imageURL: toolIconUrl + "add-model-tool.svg", + subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: toolWidth, + height: toolHeight, + visible: true, + alpha: 0.9 + }); + browseModelsButton = toolBar.addTool({ + imageURL: toolIconUrl + "list-icon.png", + width: toolWidth, + height: toolHeight, + visible: true, + alpha: 0.7 + }); + } + + function addModel(url) { + var position; + + position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE)); + + if (position.x > 0 && position.y > 0 && position.z > 0) { + Models.addModel({ + position: position, + radius: DEFAULT_RADIUS, + modelURL: url + }); + } else { + print("Can't create model: Model would be out of bounds."); + } + } + + that.move = function () { + var newViewPort, + toolsX, + toolsY; + + newViewPort = Controller.getViewportDimensions(); + + if (toolBar === undefined) { + initialize(); + + } else if (windowDimensions.x === newViewPort.x && + windowDimensions.y === newViewPort.y) { + return; + } + + windowDimensions = newViewPort; + toolsX = windowDimensions.x - 8 - toolBar.width; + toolsY = (windowDimensions.y - toolBar.height) / 2; + + toolBar.move(toolsX, toolsY); + }; + + that.mousePressEvent = function (event) { + var clickedOverlay, + url, + position; + + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + + if (newModelButton === toolBar.clicked(clickedOverlay)) { + url = Window.prompt("Model url", modelURLs[Math.floor(Math.random() * modelURLs.length)]); + if (url !== null && url !== "") { + addModel(url); + } + return true; + } + + if (browseModelsButton === toolBar.clicked(clickedOverlay)) { + url = Window.s3Browse(); + if (url !== null && url !== "") { + addModel(url); + } + return true; + } + + return false; + }; + + that.cleanup = function () { + toolBar.cleanup(); + }; + + return that; +}()); + + function isLocked(properties) { // special case to lock the ground plane model in hq. if (location.hostname == "hq.highfidelity.io" && @@ -663,45 +758,7 @@ function checkController(deltaTime) { } } - moveOverlays(); -} -var newModel; -var browser; -function initToolBar() { - toolBar = new ToolBar(0, 0, ToolBar.VERTICAL); - // New Model - newModel = toolBar.addTool({ - imageURL: toolIconUrl + "add-model-tool.svg", - subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, - width: toolWidth, height: toolHeight, - visible: true, - alpha: 0.9 - }); - browser = toolBar.addTool({ - imageURL: toolIconUrl + "list-icon.png", - width: toolWidth, height: toolHeight, - visible: true, - alpha: 0.7 - }); -} - -function moveOverlays() { - var newViewPort = Controller.getViewportDimensions(); - - if (typeof(toolBar) === 'undefined') { - initToolBar(); - - } else if (windowDimensions.x == newViewPort.x && - windowDimensions.y == newViewPort.y) { - return; - } - - - windowDimensions = newViewPort; - var toolsX = windowDimensions.x - 8 - toolBar.width; - var toolsY = (windowDimensions.y - toolBar.height) / 2; - - toolBar.move(toolsX, toolsY); + toolBar.move(); } @@ -784,42 +841,9 @@ function mousePressEvent(event) { mouseLastPosition = { x: event.x, y: event.y }; modelSelected = false; - var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); - if (newModel == toolBar.clicked(clickedOverlay)) { - var url = Window.prompt("Model URL", modelURLs[Math.floor(Math.random() * modelURLs.length)]); - if (url == null || url == "") { - return; - } - - var position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE)); - - if (position.x > 0 && position.y > 0 && position.z > 0) { - Models.addModel({ position: position, - radius: radiusDefault, - modelURL: url - }); - } else { - print("Can't create model: Model would be out of bounds."); - } - - } else if (browser == toolBar.clicked(clickedOverlay)) { - var url = Window.s3Browse(); - if (url == null || url == "") { - return; - } - - var position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE)); - - if (position.x > 0 && position.y > 0 && position.z > 0) { - Models.addModel({ position: position, - radius: radiusDefault, - modelURL: url - }); - } else { - print("Can't create model: Model would be out of bounds."); - } - + if (toolBar.mousePressEvent(event)) { + // Event handled; do nothing. } else { var pickRay = Camera.computePickRay(event.x, event.y); Vec3.print("[Mouse] Looking at: ", pickRay.origin); From c5faeadd818ce6a545cdc61cc760add0befb8f47 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 19 Jul 2014 15:27:41 -0700 Subject: [PATCH 02/62] Add button menu options for loading model from URL or file --- examples/editModels.js | 77 ++++++++++++++++++++++++++++++++++++++---- examples/toolBars.js | 8 +++++ 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 0c6ac8b426..721878c656 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -62,25 +62,72 @@ var toolBar = (function () { var that = {}, toolBar, newModelButton, - browseModelsButton; + browseModelsButton, + loadURLMenuItem, + loadFileMenuItem, + menuItemWidth = 90, + menuItemOffset = 2, + menuItemHeight = Tool.IMAGE_HEIGHT / 2 - menuItemOffset, + menuItemMargin = 5, + menuTextColor = { red: 255, green: 255, blue: 255 }, + menuBackgoundColor = { red: 18, green: 66, blue: 66 }; function initialize() { toolBar = new ToolBar(0, 0, ToolBar.VERTICAL); + newModelButton = toolBar.addTool({ imageURL: toolIconUrl + "add-model-tool.svg", subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, width: toolWidth, height: toolHeight, - visible: true, - alpha: 0.9 - }); + alpha: 0.9, + visible: true + }, true); + browseModelsButton = toolBar.addTool({ imageURL: toolIconUrl + "list-icon.png", width: toolWidth, height: toolHeight, - visible: true, - alpha: 0.7 + alpha: 0.7, + visible: true }); + + loadURLMenuItem = Overlays.addOverlay("text", { + x: newModelButton.x - menuItemWidth, + y: newModelButton.y + menuItemOffset, + width: menuItemWidth, + height: menuItemHeight, + backgroundColor: menuBackgoundColor, + topMargin: menuItemMargin, + text: "Model URL", + alpha: 0.9, + visible: false + }); + + loadFileMenuItem = Overlays.addOverlay("text", { + x: newModelButton.x - menuItemWidth, + y: newModelButton.y + menuItemOffset + menuItemHeight, + width: menuItemWidth, + height: menuItemHeight, + backgroundColor: menuBackgoundColor, + topMargin: menuItemMargin, + text: "Model File", + alpha: 0.9, + visible: false + }); + } + + function toggleToolbar(active) { + if (active === undefined) { + print("active === undefine"); + active = toolBar.toolSelected(newModelButton); + } else { + print("active !== undefine"); + toolBar.selectTool(newModelButton, active); + } + + Overlays.editOverlay(loadURLMenuItem, { visible: active }); + Overlays.editOverlay(loadFileMenuItem, { visible: active }); } function addModel(url) { @@ -119,6 +166,9 @@ var toolBar = (function () { toolsY = (windowDimensions.y - toolBar.height) / 2; toolBar.move(toolsX, toolsY); + + Overlays.editOverlay(loadURLMenuItem, { x: toolsX - menuItemWidth, y: toolsY + menuItemOffset }); + Overlays.editOverlay(loadFileMenuItem, { x: toolsX - menuItemWidth, y: toolsY + menuItemOffset + menuItemHeight }); }; that.mousePressEvent = function (event) { @@ -129,6 +179,12 @@ var toolBar = (function () { clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); if (newModelButton === toolBar.clicked(clickedOverlay)) { + toggleToolbar(); + return true; + } + + if (clickedOverlay === loadURLMenuItem) { + toggleToolbar(false); url = Window.prompt("Model url", modelURLs[Math.floor(Math.random() * modelURLs.length)]); if (url !== null && url !== "") { addModel(url); @@ -136,7 +192,14 @@ var toolBar = (function () { return true; } + if (clickedOverlay === loadFileMenuItem) { + toggleToolbar(false); + print("TODO: Upload model file"); + return true; + } + if (browseModelsButton === toolBar.clicked(clickedOverlay)) { + toggleToolbar(false); url = Window.s3Browse(); if (url !== null && url !== "") { addModel(url); @@ -149,6 +212,8 @@ var toolBar = (function () { that.cleanup = function () { toolBar.cleanup(); + Overlays.deleteOverlay(loadURLMenuItem); + Overlays.deleteOverlay(loadFileMenuItem); }; return that; diff --git a/examples/toolBars.js b/examples/toolBars.js index 1a464b4e4f..ede3b80062 100644 --- a/examples/toolBars.js +++ b/examples/toolBars.js @@ -186,6 +186,14 @@ ToolBar = function(x, y, direction) { return this.tools.length; } + this.selectTool = function (tool, select) { + this.tools[tool].select(select); + } + + this.toolSelected = function (tool) { + return this.tools[tool].selected(); + } + this.cleanup = function() { for(var tool in this.tools) { this.tools[tool].cleanup(); From 6027f4dad1bd0133cce90f8a53a190ade4c40ffd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 19 Jul 2014 15:33:54 -0700 Subject: [PATCH 03/62] Add model file selection dialog box --- examples/editModels.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 721878c656..71a4050176 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -174,6 +174,7 @@ var toolBar = (function () { that.mousePressEvent = function (event) { var clickedOverlay, url, + file, position; clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); @@ -185,7 +186,7 @@ var toolBar = (function () { if (clickedOverlay === loadURLMenuItem) { toggleToolbar(false); - url = Window.prompt("Model url", modelURLs[Math.floor(Math.random() * modelURLs.length)]); + url = Window.prompt("Model URL", modelURLs[Math.floor(Math.random() * modelURLs.length)]); if (url !== null && url !== "") { addModel(url); } @@ -194,7 +195,10 @@ var toolBar = (function () { if (clickedOverlay === loadFileMenuItem) { toggleToolbar(false); - print("TODO: Upload model file"); + file = Window.browse("Model File", "", "FST, FBX, or SVO files (*.fst *.fbx *.svo)"); + if (file !== null) { + print("TODO: Upload model file: " + file); + } return true; } From 33ffed71359c74346e45b2254e13a03f4ff0f5d2 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 19 Jul 2014 15:47:06 -0700 Subject: [PATCH 04/62] Add GET of local files to JavaScript XMLHttpRequest --- examples/Test.js | 7 + examples/testXMLHttpRequest.js | 95 +++++++++++++ .../script-engine/src/XMLHttpRequestClass.cpp | 125 +++++++++++++++--- .../script-engine/src/XMLHttpRequestClass.h | 2 + 4 files changed, 211 insertions(+), 18 deletions(-) diff --git a/examples/Test.js b/examples/Test.js index 36dee7bd90..612c56d10b 100644 --- a/examples/Test.js +++ b/examples/Test.js @@ -59,6 +59,13 @@ UnitTest.prototype.assertEquals = function(expected, actual, message) { } }; +UnitTest.prototype.assertContains = function (expected, actual, message) { + this.numAssertions++; + if (actual.indexOf(expected) == -1) { + throw new AssertionException(expected, actual, message); + } +}; + UnitTest.prototype.assertHasProperty = function(property, actual, message) { this.numAssertions++; if (actual[property] === undefined) { diff --git a/examples/testXMLHttpRequest.js b/examples/testXMLHttpRequest.js index 421eb458e4..ec5bcf6c4c 100644 --- a/examples/testXMLHttpRequest.js +++ b/examples/testXMLHttpRequest.js @@ -145,3 +145,98 @@ test("Test timeout", function() { this.assertEquals(0, req.status, "status should be `0`"); this.assertEquals(4, req.errorCode, "4 is the timeout error code for QNetworkReply::NetworkError"); }); + + +var localFile = Window.browse("Find defaultScripts.js file ...", "", "defaultScripts.js (defaultScripts.js)"); + +if (localFile !== null) { + + localFile = "file:///" + localFile; + + test("Test GET local file synchronously", function () { + var req = new XMLHttpRequest(); + + var statesVisited = [true, false, false, false, false] + req.onreadystatechange = function () { + statesVisited[req.readyState] = true; + }; + + req.open("GET", localFile, false); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(200, req.status, "status should be `200`"); + this.assertEquals("OK", req.statusText, "statusText should be `OK`"); + this.assertEquals(0, req.errorCode); + this.assertEquals("", req.getAllResponseHeaders(), "headers should be null"); + this.assertContains("High Fidelity", req.response.substring(0, 100), "expected text not found in response") + + for (var i = 0; i <= req.DONE; i++) { + this.assertEquals(true, statesVisited[i], i + " should be set"); + } + }); + + test("Test GET nonexistent local file", function () { + var nonexistentFile = localFile.replace(".js", "NoExist.js"); + + var req = new XMLHttpRequest(); + req.open("GET", nonexistentFile, false); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(404, req.status, "status should be `404`"); + this.assertEquals("Not Found", req.statusText, "statusText should be `Not Found`"); + this.assertNotEquals(0, req.errorCode); + }); + + test("Test GET local file already open", function () { + // Can't open file exclusively in order to test. + }); + + test("Test GET local file with data not implemented", function () { + var req = new XMLHttpRequest(); + req.open("GET", localFile, true); + req.send("data"); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(501, req.status, "status should be `501`"); + this.assertEquals("Not Implemented", req.statusText, "statusText should be `Not Implemented`"); + this.assertNotEquals(0, req.errorCode); + }); + + test("Test GET local file asynchronously not implemented", function () { + var req = new XMLHttpRequest(); + req.open("GET", localFile, true); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(501, req.status, "status should be `501`"); + this.assertEquals("Not Implemented", req.statusText, "statusText should be `Not Implemented`"); + this.assertNotEquals(0, req.errorCode); + }); + + test("Test POST local file not implemented", function () { + var req = new XMLHttpRequest(); + req.open("POST", localFile, false); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(501, req.status, "status should be `501`"); + this.assertEquals("Not Implemented", req.statusText, "statusText should be `Not Implemented`"); + this.assertNotEquals(0, req.errorCode); + }); + + test("Test local file username and password not implemented", function () { + var req = new XMLHttpRequest(); + req.open("GET", localFile, false, "username", "password"); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(501, req.status, "status should be `501`"); + this.assertEquals("Not Implemented", req.statusText, "statusText should be `Not Implemented`"); + this.assertNotEquals(0, req.errorCode); + }); + +} else { + print("Local file operation not tested"); +} diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index d9b7312bf4..6355b689ea 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -13,6 +13,7 @@ // #include +#include #include @@ -33,6 +34,7 @@ XMLHttpRequestClass::XMLHttpRequestClass(QScriptEngine* engine) : _onReadyStateChange(QScriptValue::NullValue), _readyState(XMLHttpRequestClass::UNSENT), _errorCode(QNetworkReply::NoError), + _file(NULL), _timeout(0), _timer(this), _numRedirects(0) { @@ -52,6 +54,19 @@ QScriptValue XMLHttpRequestClass::constructor(QScriptContext* context, QScriptEn QScriptValue XMLHttpRequestClass::getStatus() const { if (_reply) { return QScriptValue(_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } else if(_url.isLocalFile()) { + switch (_errorCode) { + case QNetworkReply::NoError: + return QScriptValue(200); + case QNetworkReply::ContentNotFoundError: + return QScriptValue(404); + case QNetworkReply::ContentAccessDenied: + return QScriptValue(409); + case QNetworkReply::TimeoutError: + return QScriptValue(408); + case QNetworkReply::ContentOperationNotPermittedError: + return QScriptValue(501); + } } return QScriptValue(0); } @@ -59,6 +74,19 @@ QScriptValue XMLHttpRequestClass::getStatus() const { QString XMLHttpRequestClass::getStatusText() const { if (_reply) { return _reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + } else if (_url.isLocalFile()) { + switch (_errorCode) { + case QNetworkReply::NoError: + return "OK"; + case QNetworkReply::ContentNotFoundError: + return "Not Found"; + case QNetworkReply::ContentAccessDenied: + return "Conflict"; + case QNetworkReply::TimeoutError: + return "Timeout"; + case QNetworkReply::ContentOperationNotPermittedError: + return "Not Implemented"; + } } return ""; } @@ -126,17 +154,42 @@ void XMLHttpRequestClass::setReadyState(ReadyState readyState) { void XMLHttpRequestClass::open(const QString& method, const QString& url, bool async, const QString& username, const QString& password) { if (_readyState == UNSENT) { - _async = async; - _url.setUrl(url); - if (!username.isEmpty()) { - _url.setUserName(username); - } - if (!password.isEmpty()) { - _url.setPassword(password); - } - _request.setUrl(_url); _method = method; - setReadyState(OPENED); + _url.setUrl(url); + _async = async; + + if (_url.isLocalFile()) { + if (_method.toUpper() == "GET" && !_async && username.isEmpty() && password.isEmpty()) { + _file = new QFile(_url.toLocalFile()); + if (!_file->exists()) { + qDebug() << "Can't find file " << _url.fileName(); + abortRequest(); + _errorCode = QNetworkReply::ContentNotFoundError; + setReadyState(DONE); + emit requestComplete(); + } else if (!_file->open(QIODevice::ReadOnly)) { + qDebug() << "Can't open file " << _url.fileName(); + abortRequest(); + //_errorCode = QNetworkReply::ContentConflictError; // TODO: Use this status when update to Qt 5.3 + _errorCode = QNetworkReply::ContentAccessDenied; + setReadyState(DONE); + emit requestComplete(); + } else { + setReadyState(OPENED); + } + } else { + notImplemented(); + } + } else { + if (!username.isEmpty()) { + _url.setUserName(username); + } + if (!password.isEmpty()) { + _url.setPassword(password); + } + _request.setUrl(_url); + setReadyState(OPENED); + } } } @@ -147,13 +200,18 @@ void XMLHttpRequestClass::send() { void XMLHttpRequestClass::send(const QString& data) { if (_readyState == OPENED && !_reply) { if (!data.isNull()) { - _sendData = new QBuffer(this); - _sendData->setData(data.toUtf8()); + if (_url.isLocalFile()) { + notImplemented(); + return; + } else { + _sendData = new QBuffer(this); + _sendData->setData(data.toUtf8()); + } } doSend(); - if (!_async) { + if (!_async && !_url.isLocalFile()) { QEventLoop loop; connect(this, SIGNAL(requestComplete()), &loop, SLOT(quit())); loop.exec(); @@ -162,14 +220,24 @@ void XMLHttpRequestClass::send(const QString& data) { } void XMLHttpRequestClass::doSend() { - _reply = NetworkAccessManager::getInstance().sendCustomRequest(_request, _method.toLatin1(), _sendData); - - connectToReply(_reply); + + if (!_url.isLocalFile()) { + _reply = NetworkAccessManager::getInstance().sendCustomRequest(_request, _method.toLatin1(), _sendData); + connectToReply(_reply); + } if (_timeout > 0) { _timer.start(_timeout); connect(&_timer, SIGNAL(timeout()), this, SLOT(requestTimeout())); } + + if (_url.isLocalFile()) { + setReadyState(HEADERS_RECEIVED); + setReadyState(LOADING); + _rawResponseData = _file->readAll(); + _file->close(); + requestFinished(); + } } void XMLHttpRequestClass::requestTimeout() { @@ -188,9 +256,16 @@ void XMLHttpRequestClass::requestError(QNetworkReply::NetworkError code) { void XMLHttpRequestClass::requestFinished() { disconnect(&_timer, SIGNAL(timeout()), this, SLOT(requestTimeout())); - _errorCode = _reply->error(); + if (!_url.isLocalFile()) { + _errorCode = _reply->error(); + } else { + _errorCode = QNetworkReply::NoError; + } + if (_errorCode == QNetworkReply::NoError) { - _rawResponseData.append(_reply->readAll()); + if (!_url.isLocalFile()) { + _rawResponseData.append(_reply->readAll()); + } if (_responseType == "json") { _responseData = _engine->evaluate("(" + QString(_rawResponseData.data()) + ")"); @@ -204,6 +279,7 @@ void XMLHttpRequestClass::requestFinished() { _responseData = QScriptValue(QString(_rawResponseData.data())); } } + setReadyState(DONE); emit requestComplete(); } @@ -217,6 +293,19 @@ void XMLHttpRequestClass::abortRequest() { delete _reply; _reply = NULL; } + + if (_file != NULL) { + _file->close(); + _file = NULL; + } +} + +void XMLHttpRequestClass::notImplemented() { + abortRequest(); + //_errorCode = QNetworkReply::OperationNotImplementedError; TODO: Use this status code when update to Qt 5.3 + _errorCode = QNetworkReply::ContentOperationNotPermittedError; + setReadyState(DONE); + emit requestComplete(); } void XMLHttpRequestClass::connectToReply(QNetworkReply* reply) { diff --git a/libraries/script-engine/src/XMLHttpRequestClass.h b/libraries/script-engine/src/XMLHttpRequestClass.h index 48f1a596e1..9052a627b7 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.h +++ b/libraries/script-engine/src/XMLHttpRequestClass.h @@ -97,6 +97,7 @@ private: void connectToReply(QNetworkReply* reply); void disconnectFromReply(QNetworkReply* reply); void abortRequest(); + void notImplemented(); QScriptEngine* _engine; bool _async; @@ -112,6 +113,7 @@ private: QScriptValue _onReadyStateChange; ReadyState _readyState; QNetworkReply::NetworkError _errorCode; + QFile* _file; int _timeout; QTimer _timer; int _numRedirects; From d05435db91664f7dee5b96c716c562ee1f31bd4d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 19 Jul 2014 23:27:36 -0700 Subject: [PATCH 05/62] Add arraybuffer binary data handling in JavaScript XMLHttpRequest --- libraries/script-engine/src/XMLHttpRequestClass.cpp | 13 +++++++++---- libraries/script-engine/src/XMLHttpRequestClass.h | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 6355b689ea..84e49f2364 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -194,10 +194,10 @@ void XMLHttpRequestClass::open(const QString& method, const QString& url, bool a } void XMLHttpRequestClass::send() { - send(QString::Null()); + send(QString()); } -void XMLHttpRequestClass::send(const QString& data) { +void XMLHttpRequestClass::send(const QVariant& data) { if (_readyState == OPENED && !_reply) { if (!data.isNull()) { if (_url.isLocalFile()) { @@ -205,7 +205,12 @@ void XMLHttpRequestClass::send(const QString& data) { return; } else { _sendData = new QBuffer(this); - _sendData->setData(data.toUtf8()); + if (_responseType == "arraybuffer") { + QByteArray ba = qvariant_cast(data); + _sendData->setData(ba); + } else { + _sendData->setData(data.toString().toUtf8()); + } } } @@ -274,7 +279,7 @@ void XMLHttpRequestClass::requestFinished() { _responseData = QScriptValue::NullValue; } } else if (_responseType == "arraybuffer") { - _responseData = QScriptValue(_rawResponseData.data()); + _responseData = _engine->newVariant(QVariant::fromValue(_rawResponseData)); } else { _responseData = QScriptValue(QString(_rawResponseData.data())); } diff --git a/libraries/script-engine/src/XMLHttpRequestClass.h b/libraries/script-engine/src/XMLHttpRequestClass.h index 9052a627b7..e482e57077 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.h +++ b/libraries/script-engine/src/XMLHttpRequestClass.h @@ -84,7 +84,7 @@ public slots: void open(const QString& method, const QString& url, bool async = true, const QString& username = "", const QString& password = ""); void send(); - void send(const QString& data); + void send(const QVariant& data); QScriptValue getAllResponseHeaders() const; QScriptValue getResponseHeader(const QString& name) const; From ed1b058cb14fdde8f810ba90ed4cf5aa2ab76657 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 20 Jul 2014 13:55:26 -0700 Subject: [PATCH 06/62] Upload selected model file directly to public folder for starters --- examples/editModels.js | 54 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 71a4050176..9304afbc49 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -58,6 +58,51 @@ var jointList = MyAvatar.getJointNames(); var mode = 0; +var modelUploader = (function () { + var that = {}, + urlBase = "http://public.highfidelity.io/meshes/"; + + that.upload = function (file, callback) { + var url, + reqRead, + reqSend; + + // Read model content ... + url = "file:///" + file; + print("Reading model from " + url); + reqRead = new XMLHttpRequest(); + reqRead.open("GET", url, false); + reqRead.responseType = "arraybuffer"; + reqRead.send(); + if (reqRead.status !== 200) { + print("Error reading file: " + reqRead.status + " " + reqRead.statusText); + Window.alert("Could not read file " + reqRead.status + " " + reqRead.statusText); + return; + } + + // Upload to High Fidelity ... + url = urlBase + file.replace(/^(.*[\/\\])*/, ""); + print("Uploading model to " + url); + reqSend = new XMLHttpRequest(); + reqSend.open("PUT", url, true); + reqSend.responseType = "arraybuffer"; + reqSend.onreadystatechange = function () { + if (reqSend.readyState === reqSend.DONE) { + if (reqSend.status === 200) { + print("Uploaded model"); + callback(url); + } else { + print("Error uploading file: " + reqSend.status + " " + reqSend.statusText); + Window.alert("Could not upload file: " + reqSend.status + " " + reqSend.statusText); + } + } + }; + reqSend.send(reqRead.response); + }; + + return that; +}()); + var toolBar = (function () { var that = {}, toolBar, @@ -119,10 +164,8 @@ var toolBar = (function () { function toggleToolbar(active) { if (active === undefined) { - print("active === undefine"); active = toolBar.toolSelected(newModelButton); } else { - print("active !== undefine"); toolBar.selectTool(newModelButton, active); } @@ -174,8 +217,7 @@ var toolBar = (function () { that.mousePressEvent = function (event) { var clickedOverlay, url, - file, - position; + file; clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); @@ -195,9 +237,9 @@ var toolBar = (function () { if (clickedOverlay === loadFileMenuItem) { toggleToolbar(false); - file = Window.browse("Model File", "", "FST, FBX, or SVO files (*.fst *.fbx *.svo)"); + file = Window.browse("Select your model file ...", "", "Model files (*.fst *.fbx *.svo)"); if (file !== null) { - print("TODO: Upload model file: " + file); + modelUploader.upload(file, addModel); } return true; } From 2be03ada9c86a390243a3cf1e37e9721d1af7442 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 20 Jul 2014 15:14:48 -0700 Subject: [PATCH 07/62] Prepare for reading and processing model file content --- examples/editModels.js | 90 +++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 9304afbc49..a7fe76abdf 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -60,44 +60,80 @@ var mode = 0; var modelUploader = (function () { var that = {}, - urlBase = "http://public.highfidelity.io/meshes/"; + urlBase = "http://public.highfidelity.io/meshes/", + model; - that.upload = function (file, callback) { + function readModel(file) { var url, - reqRead, - reqSend; + req; + + print("Reading model from " + file); - // Read model content ... url = "file:///" + file; - print("Reading model from " + url); - reqRead = new XMLHttpRequest(); - reqRead.open("GET", url, false); - reqRead.responseType = "arraybuffer"; - reqRead.send(); - if (reqRead.status !== 200) { - print("Error reading file: " + reqRead.status + " " + reqRead.statusText); - Window.alert("Could not read file " + reqRead.status + " " + reqRead.statusText); - return; + req = new XMLHttpRequest(); + req.open("GET", url, false); + req.responseType = "arraybuffer"; + req.send(); + if (req.status !== 200) { + print("Error reading file: " + req.status + " " + req.statusText); + Window.alert("Could not read file " + req.status + " " + req.statusText); + return false; } + model = req.response; - // Upload to High Fidelity ... - url = urlBase + file.replace(/^(.*[\/\\])*/, ""); - print("Uploading model to " + url); - reqSend = new XMLHttpRequest(); - reqSend.open("PUT", url, true); - reqSend.responseType = "arraybuffer"; - reqSend.onreadystatechange = function () { - if (reqSend.readyState === reqSend.DONE) { - if (reqSend.status === 200) { - print("Uploaded model"); + return true; + } + + function setProperties() { + print("Setting model properties"); + return true; + } + + function createHttpMessage() { + print("Putting model into HTTP message"); + return true; + } + + function sendToHighFidelity(url, callback) { + var req; + + print("Sending model to High Fidelity"); + + req = new XMLHttpRequest(); + req.open("PUT", url, true); + req.responseType = "arraybuffer"; + req.onreadystatechange = function () { + if (req.readyState === req.DONE) { + if (req.status === 200) { + print("Uploaded model: " + url); callback(url); } else { - print("Error uploading file: " + reqSend.status + " " + reqSend.statusText); - Window.alert("Could not upload file: " + reqSend.status + " " + reqSend.statusText); + print("Error uploading file: " + req.status + " " + req.statusText); + Window.alert("Could not upload file: " + req.status + " " + req.statusText); } } }; - reqSend.send(reqRead.response); + req.send(model); + } + + that.upload = function (file, callback) { + var url = urlBase + file.replace(/^(.*[\/\\])*/, ""), + ok; + + // Read model content ... + ok = readModel(file); + if (!ok) { return; } + + // Set model properties ... + ok = setProperties(); + if (!ok) { return; } + + // Put model in HTTP message ... + ok = createHttpMessage(); + if (!ok) { return; } + + // Send model to High Fidelity ... + sendToHighFidelity(url, callback); }; return that; From 3f24f61180dee38114919a817a1bdc7cca6d00c0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 20 Jul 2014 22:45:19 -0700 Subject: [PATCH 08/62] Provide Content-Type and -Length headers when reading local files --- examples/testXMLHttpRequest.js | 2 +- .../script-engine/src/XMLHttpRequestClass.cpp | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/examples/testXMLHttpRequest.js b/examples/testXMLHttpRequest.js index ec5bcf6c4c..79d2842464 100644 --- a/examples/testXMLHttpRequest.js +++ b/examples/testXMLHttpRequest.js @@ -168,7 +168,7 @@ if (localFile !== null) { this.assertEquals(200, req.status, "status should be `200`"); this.assertEquals("OK", req.statusText, "statusText should be `OK`"); this.assertEquals(0, req.errorCode); - this.assertEquals("", req.getAllResponseHeaders(), "headers should be null"); + this.assertNotEquals("", req.getAllResponseHeaders(), "headers should not be null"); this.assertContains("High Fidelity", req.response.substring(0, 100), "expected text not found in response") for (var i = 0; i <= req.DONE; i++) { diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 84e49f2364..8e48682ea0 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -54,7 +54,8 @@ QScriptValue XMLHttpRequestClass::constructor(QScriptContext* context, QScriptEn QScriptValue XMLHttpRequestClass::getStatus() const { if (_reply) { return QScriptValue(_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } else if(_url.isLocalFile()) { + } + if(_url.isLocalFile()) { switch (_errorCode) { case QNetworkReply::NoError: return QScriptValue(200); @@ -74,7 +75,8 @@ QScriptValue XMLHttpRequestClass::getStatus() const { QString XMLHttpRequestClass::getStatusText() const { if (_reply) { return _reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); - } else if (_url.isLocalFile()) { + } + if (_url.isLocalFile()) { switch (_errorCode) { case QNetworkReply::NoError: return "OK"; @@ -132,6 +134,13 @@ QScriptValue XMLHttpRequestClass::getAllResponseHeaders() const { } return QString(headers.data()); } + if (_url.isLocalFile()) { + QString headers = QString("Content-Type: application/octet-stream\n"); + headers.append("Content-Length: "); + headers.append(QString("%1").arg(_rawResponseData.length())); + headers.append("\n"); + return headers; + } return QScriptValue(""); } @@ -139,6 +148,14 @@ QScriptValue XMLHttpRequestClass::getResponseHeader(const QString& name) const { if (_reply && _reply->hasRawHeader(name.toLatin1())) { return QScriptValue(QString(_reply->rawHeader(name.toLatin1()))); } + if (_url.isLocalFile()) { + if (name.toLower() == "content-type") { + return QString("application/octet-stream"); + } + if (name.toLower() == "content-length") { + return QString("%1").arg(_rawResponseData.length()); + } + } return QScriptValue::NullValue; } From ab0ec9f4746be7f047dba960547aa4e8b294362c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 20 Jul 2014 22:55:07 -0700 Subject: [PATCH 09/62] Read raw data of different model file types --- examples/editModels.js | 136 +++++++++++++++++++++++++++++++++++------ 1 file changed, 116 insertions(+), 20 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index a7fe76abdf..54cf014dfe 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -58,28 +58,115 @@ var jointList = MyAvatar.getJointNames(); var mode = 0; +if (typeof String.prototype.fileName !== 'function') { + String.prototype.fileName = function (str) { + return this.replace(/^(.*[\/\\])*/, ""); + }; +} + +if (typeof String.prototype.path !== 'function') { + String.prototype.path = function (str) { + return this.replace(/[\\\/][^\\\/]*$/, ""); + }; +} + + var modelUploader = (function () { var that = {}, urlBase = "http://public.highfidelity.io/meshes/", - model; + fstBuffer, + fbxBuffer, + svoBuffer, + mapping = {}, + NAME_FIELD = "name", + SCALE_FIELD = "scale", + FILENAME_FIELD = "filename", + TEXDIR_FIELD = "texdir", + fbxDataView; - function readModel(file) { - var url, - req; + function error(message) { + Window.alert(message); + print(message); + } - print("Reading model from " + file); + function readFile(filename, buffer, length) { + var url = "file:///" + filename, + req = new XMLHttpRequest(); - url = "file:///" + file; - req = new XMLHttpRequest(); req.open("GET", url, false); req.responseType = "arraybuffer"; req.send(); if (req.status !== 200) { - print("Error reading file: " + req.status + " " + req.statusText); - Window.alert("Could not read file " + req.status + " " + req.statusText); - return false; + error("Could not read file: " + filename + " : " + req.status + " " + req.statusText); + return null; + } + + return { + buffer: req.response, + length: parseInt(req.getResponseHeader("Content-Length"), 10) + }; + } + + function readMapping(buffer) { + return {}; // DJRTODO + } + + function readGeometry(buffer) { + return {}; // DJRTODO + } + + function readModel(filename) { + var url, + req, + fbxFilename, + geometry; + + if (filename.toLowerCase().slice(-4) === ".svo") { + svoBuffer = readFile(filename); + if (svoBuffer === null) { + return false; + } + + } else { + + if (filename.toLowerCase().slice(-4) === ".fst") { + fstBuffer = readFile(filename); + if (fstBuffer === null) { + return false; + } + mapping = readMapping(fstBuffer); + fbxFilename = filename.path() + "\\" + mapping[FILENAME_FIELD]; + + } else if (filename.toLowerCase().slice(-4) === ".fbx") { + fbxFilename = filename; + mapping[FILENAME_FIELD] = filename.fileName(); + + } else { + error("Unrecognized file type: " + filename); + return false; + } + + fbxBuffer = readFile(fbxFilename); + if (fbxBuffer === null) { + return false; + } + + geometry = readGeometry(fbxBuffer); + if (!mapping.hasOwnProperty(SCALE_FIELD)) { + mapping[SCALE_FIELD] = (geometry.author === "www.makehuman.org" ? 150.0 : 15.0); + } + } + + // Add any missing basic mappings + if (!mapping.hasOwnProperty(NAME_FIELD)) { + mapping[NAME_FIELD] = filename.fileName().slice(0, -4); + } + if (!mapping.hasOwnProperty(TEXDIR_FIELD)) { + mapping[TEXDIR_FIELD] = "."; + } + if (!mapping.hasOwnProperty(SCALE_FIELD)) { + mapping[SCALE_FIELD] = 0.2; // For SVO models. } - model = req.response; return true; } @@ -99,6 +186,8 @@ var modelUploader = (function () { print("Sending model to High Fidelity"); + // DJRTODO + req = new XMLHttpRequest(); req.open("PUT", url, true); req.responseType = "arraybuffer"; @@ -113,24 +202,31 @@ var modelUploader = (function () { } } }; - req.send(model); + + if (fbxBuffer !== null) { + req.send(fbxBuffer.buffer); + } else { + req.send(svoBuffer.buffer); + } } that.upload = function (file, callback) { - var url = urlBase + file.replace(/^(.*[\/\\])*/, ""), - ok; + var url = urlBase + file.fileName(); // Read model content ... - ok = readModel(file); - if (!ok) { return; } + if (!readModel(file)) { + return; + } // Set model properties ... - ok = setProperties(); - if (!ok) { return; } + if (!setProperties()) { + return; + } // Put model in HTTP message ... - ok = createHttpMessage(); - if (!ok) { return; } + if (!createHttpMessage()) { + return; + } // Send model to High Fidelity ... sendToHighFidelity(url, callback); From f87a83f79e69664556a827dfa44ebb115d4e87ab Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Jul 2014 12:55:25 -0700 Subject: [PATCH 10/62] Read mappings from FST file --- examples/editModels.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/examples/editModels.js b/examples/editModels.js index 54cf014dfe..b8ebcd8a6c 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -108,7 +108,32 @@ var modelUploader = (function () { } function readMapping(buffer) { - return {}; // DJRTODO + var mapping = {}, + lines, + line, + values, + name, + i; + + // Simplified to target values relevant to model uploading. + lines = String(buffer.buffer).split(/\r\n|\r|\n/); + for (i = 0; i < lines.length; i += 1) { + line = lines[i].trim(); + if (line.length > 0 && line[0] !== "#") { + values = line.split(/\s*=\s*/); + name = values[0].toLowerCase(); + if (values.length === 2) { + mapping[name] = values[1]; + } else if (values.length === 3 && name === "lod") { + if (mapping[name] === undefined) { + mapping[name] = {}; + } + mapping[name][values[1]] = values[2]; + } + } + } + + return mapping; } function readGeometry(buffer) { @@ -121,6 +146,8 @@ var modelUploader = (function () { fbxFilename, geometry; + print("Reading model file: " + filename); + if (filename.toLowerCase().slice(-4) === ".svo") { svoBuffer = readFile(filename); if (svoBuffer === null) { From ef8ce8ad7d1848976cbaa8c0ad0d59d26a2696a5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Jul 2014 21:39:52 -0700 Subject: [PATCH 11/62] Remember location of model file last selected --- examples/editModels.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/editModels.js b/examples/editModels.js index b8ebcd8a6c..0363f29b05 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -396,8 +396,11 @@ var toolBar = (function () { if (clickedOverlay === loadFileMenuItem) { toggleToolbar(false); - file = Window.browse("Select your model file ...", "", "Model files (*.fst *.fbx *.svo)"); + file = Window.browse("Select your model file ...", + Settings.getValue("LastModelUploadLocation").path(), + "Model files (*.fst *.fbx *.svo)"); if (file !== null) { + Settings.setValue("LastModelUploadLocation", file); modelUploader.upload(file, addModel); } return true; From 09d52251ef04d1f9d9d1ad4c5868a22c5b4c27cf Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 22 Jul 2014 20:38:44 -0700 Subject: [PATCH 12/62] Fix merge --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8b00bface7..02f4842e88 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3628,7 +3628,7 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript scriptEngine->getModelsScriptingInterface()->setModelTree(_models.getTree()); // model has some custom types - Model::registerMetaTypes(scriptEngine->getEngine()); + Model::registerMetaTypes(scriptEngine); // hook our avatar object into this script engine scriptEngine->setAvatarData(_myAvatar, "MyAvatar"); // leave it as a MyAvatar class to expose thrust features From ed7bd9317e3d1cd4aef678828cc1bec2e7300276 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 22 Jul 2014 21:48:48 -0700 Subject: [PATCH 13/62] Make XMLHttpRequest return an ArrayBuffer object when requested --- libraries/script-engine/src/XMLHttpRequestClass.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 8e48682ea0..563e268222 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -18,6 +18,7 @@ #include #include "XMLHttpRequestClass.h" +#include "ScriptEngine.h" XMLHttpRequestClass::XMLHttpRequestClass(QScriptEngine* engine) : _engine(engine), @@ -296,7 +297,8 @@ void XMLHttpRequestClass::requestFinished() { _responseData = QScriptValue::NullValue; } } else if (_responseType == "arraybuffer") { - _responseData = _engine->newVariant(QVariant::fromValue(_rawResponseData)); + QScriptValue data = _engine->newVariant(QVariant::fromValue(_rawResponseData)); + _responseData = _engine->newObject(reinterpret_cast(_engine)->getArrayBufferClass(), data); } else { _responseData = QScriptValue(QString(_rawResponseData.data())); } From ec8b82bf6e5c9ffd69d864da1ada45741d12f42d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 23 Jul 2014 12:27:52 -0700 Subject: [PATCH 14/62] Read author and texture filenames from binary FBX files --- examples/editModels.js | 95 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 0363f29b05..51757f81bb 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -58,18 +58,62 @@ var jointList = MyAvatar.getJointNames(); var mode = 0; -if (typeof String.prototype.fileName !== 'function') { +if (typeof String.prototype.fileName !== "function") { String.prototype.fileName = function (str) { return this.replace(/^(.*[\/\\])*/, ""); }; } -if (typeof String.prototype.path !== 'function') { +if (typeof String.prototype.path !== "function") { String.prototype.path = function (str) { return this.replace(/[\\\/][^\\\/]*$/, ""); }; } +if (typeof DataView.prototype.indexOf !== "function") { + DataView.prototype.indexOf = function (searchString, position) { + var searchLength = searchString.length, + byteArrayLength = this.byteLength, + maxSearchIndex = byteArrayLength - searchLength, + searchCharCodes = [], + found, + i, + j; + + searchCharCodes[searchLength] = 0; + for (j = 0; j < searchLength; j += 1) { + searchCharCodes[j] = searchString.charCodeAt(j); + } + + i = position; + found = false; + while (i < maxSearchIndex && !found) { + j = 0; + while (j < searchLength && this.getUint8(i + j) === searchCharCodes[j]) { + j += 1; + } + found = (j === searchLength); + i += 1; + } + + return found ? i - 1 : -1; + }; +} + +if (typeof DataView.prototype.string !== "function") { + DataView.prototype.string = function (i, length) { + var charCodes = [], + end = i + length, + j; + + for (j = i; j < end; j += 1) { + charCodes.push(this.getUint8(j)); + } + + return String.fromCharCode.apply(String, charCodes); + }; +} + var modelUploader = (function () { var that = {}, @@ -136,8 +180,51 @@ var modelUploader = (function () { return mapping; } - function readGeometry(buffer) { - return {}; // DJRTODO + function readGeometry(fbxBuffer) { + var dv = new DataView(fbxBuffer.buffer), + geometry = {}, + pathLength, + filename, + author, + i; + + // Simple direct search of FBX file for relevant texture filenames (excl. paths) instead of interpreting FBX format. + // - "RelativeFilename" Record type + // - char Subtype + // - Uint8 Length of path string + // - 00 00 00 3 null chars + // - Path and name of texture file + geometry.textures = []; + i = 0; + while (i !== -1) { + i = dv.indexOf("RelativeFilename", i); + if (i !== -1) { + i += 17; // Record type and subtype + pathLength = dv.getUint8(i); + i += 4; // Path length and null chars + filename = dv.string(i, pathLength).fileName(); + geometry.textures.push(filename); + i += pathLength; + } + } + + // Simple direct search of FBX file for the first author record. + // - "Author" Record type + // - char Subtype + // - Uint8 Length of path string + // - 00 00 00 3 null chars + // - Path and name of texture file + i = dv.indexOf("Author", 0); + if (i !== -1) { + i += 7; + pathLength = dv.getUint8(i); + if (pathLength > 0) { + author = dv.string(i, pathLength); + geometry.author = author; + } + } + + return geometry; } function readModel(filename) { From 6b72274d219a69371d2df1e5792724becdfefa83 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 23 Jul 2014 16:05:34 -0700 Subject: [PATCH 15/62] Read author and texture filenames from text FBX files --- examples/editModels.js | 51 +++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 51757f81bb..bcfa0176cc 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -183,43 +183,68 @@ var modelUploader = (function () { function readGeometry(fbxBuffer) { var dv = new DataView(fbxBuffer.buffer), geometry = {}, - pathLength, + binary, + stringLength, filename, author, i; + binary = (dv.string(0, 18) === "Kaydara FBX Binary"); + // Simple direct search of FBX file for relevant texture filenames (excl. paths) instead of interpreting FBX format. - // - "RelativeFilename" Record type + // Binary format: + // - 'RelativeFilename' Record type // - char Subtype // - Uint8 Length of path string // - 00 00 00 3 null chars // - Path and name of texture file + // Text format: + // - 'RelativeFilename' Record type + // - ': " ' Pre-string colon and quote + // - Path and name of texture file + // - '"' End-of-string quote geometry.textures = []; i = 0; while (i !== -1) { i = dv.indexOf("RelativeFilename", i); if (i !== -1) { - i += 17; // Record type and subtype - pathLength = dv.getUint8(i); - i += 4; // Path length and null chars - filename = dv.string(i, pathLength).fileName(); + if (binary) { + i += 17; + stringLength = dv.getUint8(i); + i += 4; + } else { + i = dv.indexOf("\"", i) + 1; + stringLength = dv.indexOf("\"", i) - i; + } + filename = dv.string(i, stringLength).fileName(); geometry.textures.push(filename); - i += pathLength; + i += stringLength; } } // Simple direct search of FBX file for the first author record. - // - "Author" Record type + // Binary format: + // - 'Author' Record type // - char Subtype // - Uint8 Length of path string // - 00 00 00 3 null chars - // - Path and name of texture file + // - Author name + // Text format: + // - 'Author' Record type + // - ': "' Pre-string colon and quote + // - Author name; may be empty + // - '"' End-of-string quote i = dv.indexOf("Author", 0); if (i !== -1) { - i += 7; - pathLength = dv.getUint8(i); - if (pathLength > 0) { - author = dv.string(i, pathLength); + if (binary) { + i += 7; + stringLength = dv.getUint8(i); + } else { + i = dv.indexOf("\"", i) + 1; + stringLength = dv.indexOf("\"", i) - i; + } + if (stringLength > 0) { + author = dv.string(i, stringLength); geometry.author = author; } } From 401326ddd7c140ce932b6e852695006c83347f64 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 23 Jul 2014 16:59:52 -0700 Subject: [PATCH 16/62] Fix FST file processing after ArrayBuffer change --- examples/editModels.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index bcfa0176cc..917f9ad7fd 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -151,8 +151,9 @@ var modelUploader = (function () { }; } - function readMapping(buffer) { - var mapping = {}, + function readMapping(fstBuffer) { + var dv = new DataView(fstBuffer.buffer), + mapping = {}, lines, line, values, @@ -160,7 +161,7 @@ var modelUploader = (function () { i; // Simplified to target values relevant to model uploading. - lines = String(buffer.buffer).split(/\r\n|\r|\n/); + 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] !== "#") { From 7c6f1ff414d819e9d3b21b13b0d57d8286865923 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 23 Jul 2014 17:09:40 -0700 Subject: [PATCH 17/62] Handle FBX file name not found in FST file more gracefully --- examples/editModels.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/editModels.js b/examples/editModels.js index 917f9ad7fd..1498cff6d0 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -275,7 +275,12 @@ var modelUploader = (function () { return false; } mapping = readMapping(fstBuffer); - fbxFilename = filename.path() + "\\" + mapping[FILENAME_FIELD]; + if (mapping.hasOwnProperty(FILENAME_FIELD)) { + fbxFilename = filename.path() + "\\" + mapping[FILENAME_FIELD]; + } else { + error("FBX file name not found in FST file!"); + return false; + } } else if (filename.toLowerCase().slice(-4) === ".fbx") { fbxFilename = filename; From 040254a119ad8d651c642401ee31d0459b5f1929 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 24 Jul 2014 17:04:02 -0700 Subject: [PATCH 18/62] Add optional Cancel button to JavaScript Window.form() --- .../scripting/WindowScriptingInterface.cpp | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 827f66c8d5..a36d149a86 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -94,11 +94,14 @@ QScriptValue WindowScriptingInterface::showConfirm(const QString& message) { /// Display a form layout with an edit box /// \param const QString& title title to display /// \param const QScriptValue form to display (array containing labels and values) -/// \return QScriptValue result form (unchanged is dialog canceled) +/// \return QScriptValue `true` if 'OK' was clicked, `false` otherwise QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptValue form) { + if (form.isArray() && form.property("length").toInt32() > 0) { QDialog* editDialog = new QDialog(Application::getInstance()->getWindow()); editDialog->setWindowTitle(title); + + bool cancelButton = false; QVBoxLayout* layout = new QVBoxLayout(); editDialog->setLayout(layout); @@ -120,42 +123,60 @@ QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptVal QVector edits; for (int i = 0; i < form.property("length").toInt32(); ++i) { QScriptValue item = form.property(i); - edits.push_back(new QLineEdit(item.property("value").toString())); - formLayout->addRow(item.property("label").toString(), edits.back()); + + if (item.property("button").toString() != "") { + cancelButton = cancelButton || item.property("button").toString().toLower() == "cancel"; + } else { + edits.push_back(new QLineEdit(item.property("value").toString())); + formLayout->addRow(item.property("label").toString(), edits.back()); + } } - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok); + + QDialogButtonBox* buttons = new QDialogButtonBox( + QDialogButtonBox::Ok + | (cancelButton ? QDialogButtonBox::Cancel : QDialogButtonBox::NoButton) + ); connect(buttons, SIGNAL(accepted()), editDialog, SLOT(accept())); + connect(buttons, SIGNAL(rejected()), editDialog, SLOT(reject())); layout->addWidget(buttons); - if (editDialog->exec() == QDialog::Accepted) { + int result = editDialog->exec(); + if (result == QDialog::Accepted) { + int j = -1; for (int i = 0; i < form.property("length").toInt32(); ++i) { QScriptValue item = form.property(i); QScriptValue value = item.property("value"); bool ok = true; - if (value.isNumber()) { - value = edits.at(i)->text().toDouble(&ok); - } else if (value.isString()) { - value = edits.at(i)->text(); - } else if (value.isBool()) { - if (edits.at(i)->text() == "true") { - value = true; - } else if (edits.at(i)->text() == "false") { - value = false; - } else { - ok = false; + qDebug() << "item.property(""button"").toString() = " << item.property("button").toString(); + if (item.property("button").toString() == "") { + j += 1; + if (value.isNumber()) { + value = edits.at(j)->text().toDouble(&ok); + } else if (value.isString()) { + value = edits.at(j)->text(); + } else if (value.isBool()) { + if (edits.at(j)->text() == "true") { + value = true; + } else if (edits.at(j)->text() == "false") { + value = false; + } else { + ok = false; + } + } + if (ok) { + item.setProperty("value", value); + form.setProperty(i, item); } - } - if (ok) { - item.setProperty("value", value); - form.setProperty(i, item); } } } delete editDialog; + + return (result == QDialog::Accepted); } - return form; + return false; } /// Display a prompt with a text box From eecdc2dc7b942df35bfed245e3b513423915d268 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 24 Jul 2014 19:00:41 -0700 Subject: [PATCH 19/62] Increase initial width of the edit fields in JavaScript Window.form() --- interface/src/scripting/WindowScriptingInterface.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index a36d149a86..1889b283f8 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -127,8 +127,10 @@ QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptVal if (item.property("button").toString() != "") { cancelButton = cancelButton || item.property("button").toString().toLower() == "cancel"; } else { - edits.push_back(new QLineEdit(item.property("value").toString())); - formLayout->addRow(item.property("label").toString(), edits.back()); + QLineEdit* edit = new QLineEdit(item.property("value").toString()); + edit->setMinimumWidth(200); + edits.push_back(edit); + formLayout->addRow(new QLabel(item.property("label").toString()), edit); } } From 49e0d07ac8123eba72dae92e41f1fb9cd14fd10a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 25 Jul 2014 20:32:44 -0700 Subject: [PATCH 20/62] Add directory picker button option to JavaScript Window.form() --- .../scripting/WindowScriptingInterface.cpp | 94 +++++++++++++++++-- .../src/scripting/WindowScriptingInterface.h | 3 + 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 1889b283f8..19e2ba8194 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -91,9 +91,44 @@ QScriptValue WindowScriptingInterface::showConfirm(const QString& message) { return QScriptValue(response == QMessageBox::Yes); } +void WindowScriptingInterface::chooseDirectory() { + QPushButton* button = reinterpret_cast(sender()); + + QString title = button->property("title").toString(); + QString path = button->property("path").toString(); + QRegExp displayAs = button->property("displayAs").toRegExp(); + QRegExp validateAs = button->property("validateAs").toRegExp(); + QString errorMessage = button->property("errorMessage").toString(); + + QString directory = QFileDialog::getExistingDirectory(button, title, path); + if (directory.isEmpty()) { + return; + } + + if (!validateAs.exactMatch(directory)) { + QMessageBox::warning(NULL, "Invalid Directory", errorMessage); + return; + } + + button->setProperty("path", directory); + + displayAs.indexIn(directory); + QString buttonText = displayAs.cap(1) != "" ? displayAs.cap(1) : "."; + button->setText(buttonText); +} + +QString WindowScriptingInterface::jsRegExp2QtRegExp(QString string) { + // Converts string representation of RegExp from JavaScript format to Qt format. + return string.mid(1, string.length() - 2) // No enclosing slashes. + .replace("\\/", "/"); // No escaping of forward slash. +} + /// Display a form layout with an edit box /// \param const QString& title title to display -/// \param const QScriptValue form to display (array containing labels and values) +/// \param const QScriptValue form to display as an array of objects: +/// - label, value +/// - label, directory, title, display regexp, validate regexp, error message +/// - button ("Cancel") /// \return QScriptValue `true` if 'OK' was clicked, `false` otherwise QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptValue form) { @@ -121,11 +156,42 @@ QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptVal area->setWidget(container); QVector edits; + QVector directories; for (int i = 0; i < form.property("length").toInt32(); ++i) { QScriptValue item = form.property(i); if (item.property("button").toString() != "") { cancelButton = cancelButton || item.property("button").toString().toLower() == "cancel"; + + } else if (item.property("directory").toString() != "") { + QString path = item.property("directory").toString(); + QString title = item.property("title").toString(); + if (title == "") { + title = "Choose Directory"; + } + QString displayAsString = item.property("displayAs").toString(); + QRegExp displayAs = QRegExp(displayAsString != "" ? jsRegExp2QtRegExp(displayAsString) : "^(.*)$"); + QString validateAsString = item.property("validateAs").toString(); + QRegExp validateAs = QRegExp(validateAsString != "" ? jsRegExp2QtRegExp(validateAsString) : ".*"); + QString errorMessage = item.property("errorMessage").toString(); + if (errorMessage == "") { + errorMessage = "Invalid directory"; + } + + QPushButton* directory = new QPushButton(displayAs.cap(1)); + directory->setProperty("title", title); + directory->setProperty("path", path); + directory->setProperty("displayAs", displayAs); + directory->setProperty("validateAs", validateAs); + directory->setProperty("errorMessage", errorMessage); + directory->setText(displayAs.cap(1) != "" ? displayAs.cap(1) : "."); + + directory->setMinimumWidth(200); + directories.push_back(directory); + + formLayout->addRow(new QLabel(item.property("label").toString()), directory); + connect(directory, SIGNAL(clicked(bool)), SLOT(chooseDirectory())); + } else { QLineEdit* edit = new QLineEdit(item.property("value").toString()); edit->setMinimumWidth(200); @@ -144,22 +210,30 @@ QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptVal int result = editDialog->exec(); if (result == QDialog::Accepted) { - int j = -1; + int e = -1; + int d = -1; for (int i = 0; i < form.property("length").toInt32(); ++i) { QScriptValue item = form.property(i); QScriptValue value = item.property("value"); - bool ok = true; - qDebug() << "item.property(""button"").toString() = " << item.property("button").toString(); - if (item.property("button").toString() == "") { - j += 1; + + if (item.property("button").toString() != "") { + // Nothing to do + } else if (item.property("directory").toString() != "") { + d += 1; + value = directories.at(d)->property("path").toString(); + item.setProperty("directory", value); + form.setProperty(i, item); + } else { + e += 1; + bool ok = true; if (value.isNumber()) { - value = edits.at(j)->text().toDouble(&ok); + value = edits.at(e)->text().toDouble(&ok); } else if (value.isString()) { - value = edits.at(j)->text(); + value = edits.at(e)->text(); } else if (value.isBool()) { - if (edits.at(j)->text() == "true") { + if (edits.at(e)->text() == "true") { value = true; - } else if (edits.at(j)->text() == "false") { + } else if (edits.at(e)->text() == "false") { value = false; } else { ok = false; diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 654b048b24..025ee06ed7 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -40,9 +40,12 @@ private slots: QScriptValue showPrompt(const QString& message, const QString& defaultText); QScriptValue showBrowse(const QString& title, const QString& directory, const QString& nameFilter); QScriptValue showS3Browse(const QString& nameFilter); + void chooseDirectory(); private: WindowScriptingInterface(); + + QString jsRegExp2QtRegExp(QString string); }; #endif // hifi_WindowScriptingInterface_h From fcfaf6a9beb366c763bc8a79de83a14dff7000c0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 26 Jul 2014 08:49:42 -0700 Subject: [PATCH 21/62] Add Set Model Properties dialog for model uploading --- examples/editModels.js | 85 +++++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 1498cff6d0..58855f24cd 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -118,14 +118,19 @@ if (typeof DataView.prototype.string !== "function") { var modelUploader = (function () { var that = {}, urlBase = "http://public.highfidelity.io/meshes/", + modelFile, fstBuffer, fbxBuffer, svoBuffer, - mapping = {}, + mapping, NAME_FIELD = "name", SCALE_FIELD = "scale", FILENAME_FIELD = "filename", TEXDIR_FIELD = "texdir", + ANIMATION_URL_FIELD = "animationurl", + PITCH_FIELD = "pitch", + YAW_FIELD = "yaw", + ROLL_FIELD = "roll", fbxDataView; function error(message) { @@ -253,41 +258,41 @@ var modelUploader = (function () { return geometry; } - function readModel(filename) { - var url, - req, - fbxFilename, + function readModel() { + var fbxFilename, geometry; - print("Reading model file: " + filename); + print("Reading model file: " + modelFile); - if (filename.toLowerCase().slice(-4) === ".svo") { - svoBuffer = readFile(filename); + mapping = {}; + + if (modelFile.toLowerCase().slice(-4) === ".svo") { + svoBuffer = readFile(modelFile); if (svoBuffer === null) { return false; } } else { - if (filename.toLowerCase().slice(-4) === ".fst") { - fstBuffer = readFile(filename); + if (modelFile.toLowerCase().slice(-4) === ".fst") { + fstBuffer = readFile(modelFile); if (fstBuffer === null) { return false; } mapping = readMapping(fstBuffer); if (mapping.hasOwnProperty(FILENAME_FIELD)) { - fbxFilename = filename.path() + "\\" + mapping[FILENAME_FIELD]; + fbxFilename = modelFile.path() + "\\" + mapping[FILENAME_FIELD]; } else { error("FBX file name not found in FST file!"); return false; } - } else if (filename.toLowerCase().slice(-4) === ".fbx") { - fbxFilename = filename; - mapping[FILENAME_FIELD] = filename.fileName(); + } else if (modelFile.toLowerCase().slice(-4) === ".fbx") { + fbxFilename = modelFile; + mapping[FILENAME_FIELD] = modelFile.fileName(); } else { - error("Unrecognized file type: " + filename); + error("Unrecognized file type: " + modelFile); return false; } @@ -304,7 +309,7 @@ var modelUploader = (function () { // Add any missing basic mappings if (!mapping.hasOwnProperty(NAME_FIELD)) { - mapping[NAME_FIELD] = filename.fileName().slice(0, -4); + mapping[NAME_FIELD] = modelFile.fileName().slice(0, -4); } if (!mapping.hasOwnProperty(TEXDIR_FIELD)) { mapping[TEXDIR_FIELD] = "."; @@ -317,7 +322,50 @@ var modelUploader = (function () { } function setProperties() { - print("Setting model properties"); + var form = [], + decimals = 3, + directory, + displayAs, + validateAs; + + form.push({ label: "Name:", value: mapping[NAME_FIELD] }); + + directory = modelFile.path() + "/" + mapping[TEXDIR_FIELD]; + displayAs = new RegExp("^" + modelFile.path().replace(/[\\\\\\\/]/, "[\\\\\\\/]") + "[\\\\\\\/](.*)"); + validateAs = new RegExp("^" + modelFile.path().replace(/[\\\\\\\/]/, "[\\\\\\\/]") + "([\\\\\\\/].*)?"); + + 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 model directory." + }); + + form.push({ label: "Animation URL:", value: "" }); + form.push({ label: "Pitch:", value: (0).toFixed(decimals) }); + form.push({ label: "Yaw:", value: (0).toFixed(decimals) }); + form.push({ label: "Roll:", value: (0).toFixed(decimals) }); + form.push({ label: "Scale:", value: mapping[SCALE_FIELD].toFixed(decimals) }); + 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] = "."; + } + mapping[ANIMATION_URL_FIELD] = form[2].value; + mapping[PITCH_FIELD] = form[3].value; + mapping[YAW_FIELD] = form[4].value; + mapping[ROLL_FIELD] = form[5].value; + mapping[SCALE_FIELD] = form[6].value; + return true; } @@ -356,10 +404,11 @@ var modelUploader = (function () { } that.upload = function (file, callback) { + modelFile = file; var url = urlBase + file.fileName(); // Read model content ... - if (!readModel(file)) { + if (!readModel()) { return; } From f602f42189b8a22b3a084c4315f0054e3f79c57d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 26 Jul 2014 09:22:44 -0700 Subject: [PATCH 22/62] Add Cancel button to model editing dialog --- examples/editModels.js | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 58855f24cd..2b1aa190c6 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -1618,23 +1618,25 @@ function handeMenuEvent(menuItem){ array.push({ label: "Yaw:", value: angles.y.toFixed(decimals) }); array.push({ label: "Roll:", value: angles.z.toFixed(decimals) }); array.push({ label: "Scale:", value: 2 * selectedModelProperties.radius.toFixed(decimals) }); - - var propertyName = Window.form("Edit Properties", array); - modelSelected = false; - - selectedModelProperties.modelURL = array[0].value; - selectedModelProperties.animationURL = array[1].value; - selectedModelProperties.position.x = array[2].value; - selectedModelProperties.position.y = array[3].value; - selectedModelProperties.position.z = array[4].value; - angles.x = array[5].value; - angles.y = array[6].value; - angles.z = array[7].value; - selectedModelProperties.modelRotation = Quat.fromVec3Degrees(angles); - selectedModelProperties.radius = array[8].value / 2; - print(selectedModelProperties.radius); + array.push({ button: "Cancel" }); - Models.editModel(selectedModelID, selectedModelProperties); + if (Window.form("Edit Properties", array)) { + selectedModelProperties.modelURL = array[0].value; + selectedModelProperties.animationURL = array[1].value; + selectedModelProperties.position.x = array[2].value; + selectedModelProperties.position.y = array[3].value; + selectedModelProperties.position.z = array[4].value; + angles.x = array[5].value; + angles.y = array[6].value; + angles.z = array[7].value; + selectedModelProperties.modelRotation = Quat.fromVec3Degrees(angles); + selectedModelProperties.radius = array[8].value / 2; + print(selectedModelProperties.radius); + + Models.editModel(selectedModelID, selectedModelProperties); + } + + modelSelected = false; } } tooltip.show(false); From 61bb21cc00477865cde342c6330e3bfd4d227583 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 27 Jul 2014 22:05:18 -0700 Subject: [PATCH 23/62] Prepare multipart/form-date message with model file and attributes --- examples/editModels.js | 242 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 223 insertions(+), 19 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 2b1aa190c6..c5a8c5c1e8 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -70,6 +70,39 @@ if (typeof String.prototype.path !== "function") { }; } +if (typeof String.prototype.toArrayBuffer !== "function") { + String.prototype.toArrayBuffer = function () { + var length, + buffer, + view, + charCode, + charCodes, + i; + + charCodes = []; + + length = this.length; + for (i = 0; i < length; i += 1) { + charCode = this.charCodeAt(i); + if (i <= 255) { + charCodes.push(charCode); + } else { + charCodes.push(charCode / 256); + charCodes.push(charCode % 256); + } + } + + length = charCodes.length; + buffer = new ArrayBuffer(length); + view = new Uint8Array(buffer); + for (i = 0; i < length; i += 1) { + view[i] = charCodes[i]; + } + + return buffer; + }; +} + if (typeof DataView.prototype.indexOf !== "function") { DataView.prototype.indexOf = function (searchString, position) { var searchLength = searchString.length, @@ -101,19 +134,139 @@ if (typeof DataView.prototype.indexOf !== "function") { } if (typeof DataView.prototype.string !== "function") { - DataView.prototype.string = function (i, length) { + DataView.prototype.string = function (start, length) { var charCodes = [], - end = i + length, - j; + end, + i; - for (j = i; j < end; j += 1) { - charCodes.push(this.getUint8(j)); + if (start === undefined) { + start = 0; + } + if (length === undefined) { + length = this.length; + } + + end = start + length; + for (i = start; i < end; i += 1) { + charCodes.push(this.getUint8(i)); } return String.fromCharCode.apply(String, charCodes); }; } +var httpMultiPart = (function () { + var that = {}, + parts, + byteLength, + boundaryString, + crlf; + + function clear() { + boundaryString = "--boundary_" + Uuid.generate() + "="; + parts = []; + byteLength = 0; + crlf = ""; + } + that.clear = clear; + + function boundary() { + return boundaryString.slice(2); + } + that.boundary = boundary; + + function length() { + return byteLength; + } + that.length = length; + + function add(object) { + // - name, string + // - name, buffer + var buffer, + stringBuffer, + string, + length; + + if (object.name === undefined) { + + throw new Error("Item to add to HttpMultiPart must have a name"); + + } else if (object.string !== undefined) { + //--= + //Content-Disposition: form-data; name="model_name" + // + // + + string = crlf + boundaryString + "\r\n" + + "Content-Disposition: form-data; name=\"" + object.name + "\"\r\n" + + "\r\n" + + object.string; + buffer = string.toArrayBuffer(); + + } else if (object.buffer !== undefined) { + //--= + //Content-Disposition: form-data; name="fbx"; filename="" + //Content-Type: application/octet-stream + // + // + + string = crlf + boundaryString + "\r\n" + + "Content-Disposition: form-data; name=\"" + object.name + + "\"; filename=\"" + object.buffer.filename + "\"\r\n" + + "Content-Type: application/octet-stream\r\n" + + "\r\n"; + stringBuffer = string.toArrayBuffer(); + + buffer = new Uint8Array(stringBuffer.byteLength + object.buffer.buffer.byteLength); + buffer.set(new Uint8Array(stringBuffer)); + buffer.set(new Uint8Array(object.buffer.buffer), stringBuffer.byteLength); + + } else { + + throw new Error("Item to add to HttpMultiPart not recognized"); + } + + byteLength += buffer.byteLength; + parts.push(buffer); + + crlf = "\r\n"; + + return true; + } + that.add = add; + + function response() { + var buffer, + view, + charCodes, + str, + i, + j; + + str = crlf + boundaryString + "--\r\n"; + buffer = str.toArrayBuffer(); + byteLength += buffer.byteLength; + parts.push(buffer); + + charCodes = []; + for (i = 0; i < parts.length; i += 1) { + view = new Uint8Array(parts[i]); + for (j = 0; j < view.length; j += 1) { + charCodes.push(view[j]); + } + } + str = String.fromCharCode.apply(String, charCodes); + + return str; + } + that.response = response; + + clear(); + + return that; +}()); + var modelUploader = (function () { var that = {}, @@ -151,8 +304,8 @@ var modelUploader = (function () { } return { + filename: filename.fileName(), buffer: req.response, - length: parseInt(req.getResponseHeader("Content-Length"), 10) }; } @@ -259,8 +412,8 @@ var modelUploader = (function () { } function readModel() { - var fbxFilename, - geometry; + var geometry, + fbxFilename; print("Reading model file: " + modelFile); @@ -328,6 +481,8 @@ var modelUploader = (function () { displayAs, validateAs; + print("Setting model properties"); + form.push({ label: "Name:", value: mapping[NAME_FIELD] }); directory = modelFile.path() + "/" + mapping[TEXDIR_FIELD]; @@ -370,7 +525,60 @@ var modelUploader = (function () { } function createHttpMessage() { - print("Putting model into HTTP message"); + var i; + + print("Preparing to send model"); + + httpMultiPart.clear(); + + // Model name + if (mapping.hasOwnProperty(NAME_FIELD)) { + httpMultiPart.add({ + name : "model_name", + string : mapping[NAME_FIELD] + }); + } else { + error("Model name is missing"); + httpMultiPart.clear(); + return false; + } + + // FST file + if (fstBuffer) { + httpMultiPart.add({ + name : "fst", + buffer: fstBuffer + }); + } + + // FBX file + if (fbxBuffer) { + httpMultiPart.add({ + name : "fbx", + buffer: fbxBuffer + }); + } + + // SVO file + if (svoBuffer) { + httpMultiPart.add({ + name : "svo", + buffer: svoBuffer + }); + } + + // LOD files + // DJRTODO + + // Textures + // DJRTODO + + // Model category + httpMultiPart.add({ + name : "model_category", + string : "item" // DJRTODO: What model category to use? + }); + return true; } @@ -379,11 +587,10 @@ var modelUploader = (function () { print("Sending model to High Fidelity"); - // DJRTODO - req = new XMLHttpRequest(); - req.open("PUT", url, true); - req.responseType = "arraybuffer"; + req.open("POST", url, true); + req.setRequestHeader("Content-Type", "multipart/form-data; boundary=\"" + httpMultiPart.boundary() + "\""); + req.onreadystatechange = function () { if (req.readyState === req.DONE) { if (req.status === 200) { @@ -396,17 +603,14 @@ var modelUploader = (function () { } }; - if (fbxBuffer !== null) { - req.send(fbxBuffer.buffer); - } else { - req.send(svoBuffer.buffer); - } + req.send(httpMultiPart.response()); } that.upload = function (file, callback) { - modelFile = file; var url = urlBase + file.fileName(); + modelFile = file; + // Read model content ... if (!readModel()) { return; From f46c064e888ddf817fcba7e62b210b338fd7c3ad Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 28 Jul 2014 19:58:53 -0700 Subject: [PATCH 24/62] Speed up model reading --- examples/editModels.js | 150 +++++++++++++++++++++++++---------------- 1 file changed, 93 insertions(+), 57 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index c5a8c5c1e8..f4170b5655 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -340,72 +340,108 @@ var modelUploader = (function () { } function readGeometry(fbxBuffer) { - var dv = new DataView(fbxBuffer.buffer), - geometry = {}, - binary, - stringLength, - filename, - author, - i; + var geometry, + view, + index, + EOF; - binary = (dv.string(0, 18) === "Kaydara FBX Binary"); + // Reference: + // http://code.blender.org/index.php/2013/08/fbx-binary-file-format-specification/ - // Simple direct search of FBX file for relevant texture filenames (excl. paths) instead of interpreting FBX format. - // Binary format: - // - 'RelativeFilename' Record type - // - char Subtype - // - Uint8 Length of path string - // - 00 00 00 3 null chars - // - Path and name of texture file - // Text format: - // - 'RelativeFilename' Record type - // - ': " ' Pre-string colon and quote - // - Path and name of texture file - // - '"' End-of-string quote + geometry = {}; geometry.textures = []; - i = 0; - while (i !== -1) { - i = dv.indexOf("RelativeFilename", i); - if (i !== -1) { - if (binary) { - i += 17; - stringLength = dv.getUint8(i); - i += 4; - } else { - i = dv.indexOf("\"", i) + 1; - stringLength = dv.indexOf("\"", i) - i; - } - filename = dv.string(i, stringLength).fileName(); + view = new DataView(fbxBuffer.buffer); + EOF = false; + + function parseBinaryFBX() { + var endOffset, + numProperties, + propertyListLength, + nameLength, + name, + filename, + author; + + 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 === "relativefilename") { + filename = view.string(index + 5, view.getUint32(index + 1, true)).fileName(); geometry.textures.push(filename); - i += stringLength; + + } else if (name === "author") { + author = view.string(index + 5, view.getUint32(index + 1, true)); + geometry.author = author; + + } + + index += (propertyListLength); + + while (index < endOffset && !EOF) { + parseBinaryFBX(); } } - // Simple direct search of FBX file for the first author record. - // Binary format: - // - 'Author' Record type - // - char Subtype - // - Uint8 Length of path string - // - 00 00 00 3 null chars - // - Author name - // Text format: - // - 'Author' Record type - // - ': "' Pre-string colon and quote - // - Author name; may be empty - // - '"' End-of-string quote - i = dv.indexOf("Author", 0); - if (i !== -1) { - if (binary) { - i += 7; - stringLength = dv.getUint8(i); - } else { - i = dv.indexOf("\"", i) + 1; - stringLength = dv.indexOf("\"", i) - i; + function readTextFBX() { + var line, + view, + viewLength, + charCode, + charCodes, + author, + filename; + + view = new Uint8Array(fbxBuffer.buffer); + viewLength = view.byteLength; + charCodes = []; + + for (index = 0; index < viewLength; index += 1) { + charCode = view[index]; + if (charCode === 10) { // Can ignore EOF + line = String.fromCharCode.apply(String, charCodes).trim(); + + if (line.slice(0, 7).toLowerCase() === "author:") { + author = line.slice(line.indexOf("\""), line.lastIndexOf("\"") - line.length); + geometry.author = author; + + } + if (line.slice(0, 17).toLowerCase() === "relativefilename:") { + filename = line.slice(line.indexOf("\""), line.lastIndexOf("\"") - line.length).fileName(); + geometry.textures.push(filename); + } + + charCodes = []; + } else { + charCodes.push(charCode); + } } - if (stringLength > 0) { - author = dv.string(i, stringLength); - geometry.author = author; + } + + if (view.string(0, 18) === "Kaydara FBX Binary") { + + index = 27; + while (index < view.byteLength - 39 && !EOF) { + parseBinaryFBX(); } + + } else { + + readTextFBX(); + } return geometry; From 63d7ff0bdea225e61c67d71b2c35d00b8c2739f8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 28 Jul 2014 20:00:23 -0700 Subject: [PATCH 25/62] Tidying --- examples/editModels.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index f4170b5655..6bf0d54e1d 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -59,13 +59,13 @@ var mode = 0; if (typeof String.prototype.fileName !== "function") { - String.prototype.fileName = function (str) { + String.prototype.fileName = function () { return this.replace(/^(.*[\/\\])*/, ""); }; } if (typeof String.prototype.path !== "function") { - String.prototype.path = function (str) { + String.prototype.path = function () { return this.replace(/[\\\/][^\\\/]*$/, ""); }; } @@ -185,8 +185,7 @@ var httpMultiPart = (function () { // - name, buffer var buffer, stringBuffer, - string, - length; + string; if (object.name === undefined) { @@ -267,7 +266,6 @@ var httpMultiPart = (function () { return that; }()); - var modelUploader = (function () { var that = {}, urlBase = "http://public.highfidelity.io/meshes/", @@ -283,15 +281,14 @@ var modelUploader = (function () { ANIMATION_URL_FIELD = "animationurl", PITCH_FIELD = "pitch", YAW_FIELD = "yaw", - ROLL_FIELD = "roll", - fbxDataView; + ROLL_FIELD = "roll"; function error(message) { Window.alert(message); print(message); } - function readFile(filename, buffer, length) { + function readFile(filename) { var url = "file:///" + filename, req = new XMLHttpRequest(); @@ -305,7 +302,7 @@ var modelUploader = (function () { return { filename: filename.fileName(), - buffer: req.response, + buffer: req.response }; } @@ -561,7 +558,6 @@ var modelUploader = (function () { } function createHttpMessage() { - var i; print("Preparing to send model"); From ccf37c6c178eb297a6224a78a6f5fd5feb76e09a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 28 Jul 2014 21:53:00 -0700 Subject: [PATCH 26/62] Fix text displayed on Window.form() directory button --- interface/src/scripting/WindowScriptingInterface.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 19e2ba8194..c1be4f8a02 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -184,6 +184,7 @@ QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptVal directory->setProperty("displayAs", displayAs); directory->setProperty("validateAs", validateAs); directory->setProperty("errorMessage", errorMessage); + displayAs.indexIn(path); directory->setText(displayAs.cap(1) != "" ? displayAs.cap(1) : "."); directory->setMinimumWidth(200); From 0bb42ba5f26aee60560470d3d3ed72eda12cd14e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 28 Jul 2014 21:55:05 -0700 Subject: [PATCH 27/62] Fix call stack overflow when preparing large multipart/form-data --- examples/editModels.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 6bf0d54e1d..ce9b52c3a0 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -248,15 +248,15 @@ var httpMultiPart = (function () { byteLength += buffer.byteLength; parts.push(buffer); - charCodes = []; + str = ""; for (i = 0; i < parts.length; i += 1) { + charCodes = []; view = new Uint8Array(parts[i]); - for (j = 0; j < view.length; j += 1) { + for(j = 0; j < view.length; j += 1) { charCodes.push(view[j]); } + str = str + String.fromCharCode.apply(String, charCodes); } - str = String.fromCharCode.apply(String, charCodes); - return str; } that.response = response; From c7c2f3119253e4cfc928933313c82b059823f05b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 28 Jul 2014 21:59:44 -0700 Subject: [PATCH 28/62] Fix display of scale read from model file --- examples/editModels.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index ce9b52c3a0..b3e90d7995 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -488,8 +488,11 @@ var modelUploader = (function () { } geometry = readGeometry(fbxBuffer); - if (!mapping.hasOwnProperty(SCALE_FIELD)) { - mapping[SCALE_FIELD] = (geometry.author === "www.makehuman.org" ? 150.0 : 15.0); + + if (mapping.hasOwnProperty(SCALE_FIELD)) { + mapping[SCALE_FIELD] = parseFloat(mapping[SCALE_FIELD]); + } else { + mapping[SCALE_FIELD] = (geometry.author === "www.makehuman.org" ? 150.0: 15.0); } } From 00abf43faee07c8118571fe07b53cf892b8700b9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 28 Jul 2014 22:10:25 -0700 Subject: [PATCH 29/62] Add LOD files to multipart/form-data --- examples/editModels.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/examples/editModels.js b/examples/editModels.js index b3e90d7995..24b0745503 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -561,6 +561,8 @@ var modelUploader = (function () { } function createHttpMessage() { + var lodCount, + lodBuffer; print("Preparing to send model"); @@ -603,7 +605,15 @@ var modelUploader = (function () { } // LOD files - // DJRTODO + lodCount = 0; + for (var n in mapping["lod"]) { + lodBuffer = readFile(modelFile.path() + "\\" + n); + httpMultiPart.add({ + name: "lod" + lodCount, + buffer: lodBuffer + }) + lodCount += 1; + } // Textures // DJRTODO From edf96b749e3465d1d49c4560d450cbeb2743eab9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 28 Jul 2014 23:01:12 -0700 Subject: [PATCH 30/62] Add texture files to multipart/form-data --- examples/editModels.js | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 24b0745503..01871b8a60 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -281,7 +281,8 @@ var modelUploader = (function () { ANIMATION_URL_FIELD = "animationurl", PITCH_FIELD = "pitch", YAW_FIELD = "yaw", - ROLL_FIELD = "roll"; + ROLL_FIELD = "roll", + geometry; function error(message) { Window.alert(message); @@ -338,6 +339,7 @@ var modelUploader = (function () { function readGeometry(fbxBuffer) { var geometry, + textures, view, index, EOF; @@ -347,6 +349,7 @@ var modelUploader = (function () { geometry = {}; geometry.textures = []; + textures = {}; view = new DataView(fbxBuffer.buffer); EOF = false; @@ -378,7 +381,10 @@ var modelUploader = (function () { if (name === "relativefilename") { filename = view.string(index + 5, view.getUint32(index + 1, true)).fileName(); - geometry.textures.push(filename); + if (!textures.hasOwnProperty(filename)) { + textures[filename] = ""; + geometry.textures.push(filename); + } } else if (name === "author") { author = view.string(index + 5, view.getUint32(index + 1, true)); @@ -418,7 +424,10 @@ var modelUploader = (function () { } if (line.slice(0, 17).toLowerCase() === "relativefilename:") { filename = line.slice(line.indexOf("\""), line.lastIndexOf("\"") - line.length).fileName(); - geometry.textures.push(filename); + if (!textures.hasOwnProperty(filename)) { + textures[filename] = ""; + geometry.textures.push(filename); + } } charCodes = []; @@ -445,8 +454,7 @@ var modelUploader = (function () { } function readModel() { - var geometry, - fbxFilename; + var fbxFilename; print("Reading model file: " + modelFile); @@ -562,7 +570,10 @@ var modelUploader = (function () { function createHttpMessage() { var lodCount, - lodBuffer; + lodBuffer, + textureCount, + textureBuffer, + i; print("Preparing to send model"); @@ -607,7 +618,7 @@ var modelUploader = (function () { // LOD files lodCount = 0; for (var n in mapping["lod"]) { - lodBuffer = readFile(modelFile.path() + "\\" + n); + lodBuffer = readFile(modelFile.path() + "\/" + n); httpMultiPart.add({ name: "lod" + lodCount, buffer: lodBuffer @@ -616,7 +627,15 @@ var modelUploader = (function () { } // Textures - // DJRTODO + textureCount = 0; + for (i = 0; i < geometry.textures.length; i += 1) { + textureBuffer = readFile(modelFile.path() + "\/" + mapping[TEXDIR_FIELD] + "\/" + geometry.textures[i]); + httpMultiPart.add({ + name: "texture" + textureCount, + buffer: textureBuffer + }); + textureCount += 1; + } // Model category httpMultiPart.add({ From 5a5bbfd612ef0721aab2dab50b76f8cb131b1434 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 29 Jul 2014 09:46:54 -0700 Subject: [PATCH 31/62] Tidying --- examples/editModels.js | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 01871b8a60..31a55bf821 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -252,7 +252,7 @@ var httpMultiPart = (function () { for (i = 0; i < parts.length; i += 1) { charCodes = []; view = new Uint8Array(parts[i]); - for(j = 0; j < view.length; j += 1) { + for (j = 0; j < view.length; j += 1) { charCodes.push(view[j]); } str = str + String.fromCharCode.apply(String, charCodes); @@ -458,6 +458,9 @@ var modelUploader = (function () { print("Reading model file: " + modelFile); + fstBuffer = null; + fbxBuffer = null; + svoBuffer = null; mapping = {}; if (modelFile.toLowerCase().slice(-4) === ".svo") { @@ -500,7 +503,7 @@ var modelUploader = (function () { if (mapping.hasOwnProperty(SCALE_FIELD)) { mapping[SCALE_FIELD] = parseFloat(mapping[SCALE_FIELD]); } else { - mapping[SCALE_FIELD] = (geometry.author === "www.makehuman.org" ? 150.0: 15.0); + mapping[SCALE_FIELD] = (geometry.author === "www.makehuman.org" ? 150.0 : 15.0); } } @@ -570,8 +573,8 @@ var modelUploader = (function () { function createHttpMessage() { var lodCount, + lodFile, lodBuffer, - textureCount, textureBuffer, i; @@ -617,24 +620,26 @@ var modelUploader = (function () { // LOD files lodCount = 0; - for (var n in mapping["lod"]) { - lodBuffer = readFile(modelFile.path() + "\/" + n); - httpMultiPart.add({ - name: "lod" + lodCount, - buffer: lodBuffer - }) - lodCount += 1; + for (lodFile in mapping.lod) { + if (mapping.lod.hasOwnProperty(lodFile)) { + lodBuffer = readFile(modelFile.path() + "\/" + lodFile); + httpMultiPart.add({ + name: "lod" + lodCount, + buffer: lodBuffer + }); + lodCount += 1; + } } // Textures - textureCount = 0; for (i = 0; i < geometry.textures.length; i += 1) { - textureBuffer = readFile(modelFile.path() + "\/" + mapping[TEXDIR_FIELD] + "\/" + geometry.textures[i]); + textureBuffer = readFile(modelFile.path() + "\/" + + (mapping[TEXDIR_FIELD] !== "." ? mapping[TEXDIR_FIELD] + "\/" : "") + + geometry.textures[i]); httpMultiPart.add({ - name: "texture" + textureCount, + name: "texture" + i, buffer: textureBuffer }); - textureCount += 1; } // Model category From 287e3d6800c1ac04831213777a1906a414abf3ee Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 29 Jul 2014 13:52:52 -0700 Subject: [PATCH 32/62] Compress model and texture file data in multipart/form-data A compress() method is added to the JavaScript ArrayBuffer object. --- examples/editModels.js | 8 +++++--- .../script-engine/src/ArrayBufferPrototype.cpp | 15 +++++++++++++++ .../script-engine/src/ArrayBufferPrototype.h | 1 + 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 31a55bf821..1a242d16d2 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -184,8 +184,9 @@ var httpMultiPart = (function () { // - name, string // - name, buffer var buffer, + string, stringBuffer, - string; + compressedBuffer; if (object.name === undefined) { @@ -217,9 +218,10 @@ var httpMultiPart = (function () { + "\r\n"; stringBuffer = string.toArrayBuffer(); - buffer = new Uint8Array(stringBuffer.byteLength + object.buffer.buffer.byteLength); + compressedBuffer = object.buffer.buffer.compress(); + buffer = new Uint8Array(stringBuffer.byteLength + compressedBuffer.byteLength); buffer.set(new Uint8Array(stringBuffer)); - buffer.set(new Uint8Array(object.buffer.buffer), stringBuffer.byteLength); + buffer.set(new Uint8Array(compressedBuffer), stringBuffer.byteLength); } else { diff --git a/libraries/script-engine/src/ArrayBufferPrototype.cpp b/libraries/script-engine/src/ArrayBufferPrototype.cpp index 53ebebc740..6f78caad2d 100644 --- a/libraries/script-engine/src/ArrayBufferPrototype.cpp +++ b/libraries/script-engine/src/ArrayBufferPrototype.cpp @@ -14,6 +14,9 @@ #include "ArrayBufferClass.h" #include "ArrayBufferPrototype.h" +static const int QCOMPRESS_HEADER_POSITION = 0; +static const int QCOMPRESS_HEADER_SIZE = 4; + Q_DECLARE_METATYPE(QByteArray*) ArrayBufferPrototype::ArrayBufferPrototype(QObject* parent) : QObject(parent) { @@ -43,6 +46,18 @@ QByteArray ArrayBufferPrototype::slice(qint32 begin) const { return ba->mid(begin, -1); } +QByteArray ArrayBufferPrototype::compress() const { + QByteArray* ba = thisArrayBuffer(); + + QByteArray buffer = qCompress(*ba); + + // Qt's qCompress() default compression level (-1) is the standard zLib compression. + // Here remove Qt's custom header that prevents the data server from uncompressing the files with zLib. + buffer.remove(QCOMPRESS_HEADER_POSITION, QCOMPRESS_HEADER_SIZE); + + return buffer; +} + QByteArray* ArrayBufferPrototype::thisArrayBuffer() const { return qscriptvalue_cast(thisObject().data()); } diff --git a/libraries/script-engine/src/ArrayBufferPrototype.h b/libraries/script-engine/src/ArrayBufferPrototype.h index 09d4596f28..2ad9843571 100644 --- a/libraries/script-engine/src/ArrayBufferPrototype.h +++ b/libraries/script-engine/src/ArrayBufferPrototype.h @@ -23,6 +23,7 @@ public: public slots: QByteArray slice(qint32 begin, qint32 end) const; QByteArray slice(qint32 begin) const; + QByteArray compress() const; private: QByteArray* thisArrayBuffer() const; From f39aed37b674198a024cfeb8d492487c11293103 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 29 Jul 2014 17:26:42 -0700 Subject: [PATCH 33/62] Recode and rescale texture file data before uploading A recodeImage() method is added to the JavaScript ArrayBuffer object. --- examples/editModels.js | 15 ++++++++ .../src/ArrayBufferPrototype.cpp | 34 ++++++++++++++++--- .../script-engine/src/ArrayBufferPrototype.h | 1 + 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 1a242d16d2..589f4e4bfb 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -64,6 +64,12 @@ if (typeof String.prototype.fileName !== "function") { }; } +if (typeof String.prototype.fileType !== "function") { + String.prototype.fileType = function () { + return this.slice(this.lastIndexOf(".") + 1); + }; +} + if (typeof String.prototype.path !== "function") { String.prototype.path = function () { return this.replace(/[\\\/][^\\\/]*$/, ""); @@ -284,6 +290,7 @@ var modelUploader = (function () { PITCH_FIELD = "pitch", YAW_FIELD = "yaw", ROLL_FIELD = "roll", + MAX_TEXTURE_SIZE = 1024, geometry; function error(message) { @@ -578,6 +585,8 @@ var modelUploader = (function () { lodFile, lodBuffer, textureBuffer, + textureSourceFormat, + textureTargetFormat, i; print("Preparing to send model"); @@ -638,6 +647,12 @@ var modelUploader = (function () { textureBuffer = readFile(modelFile.path() + "\/" + (mapping[TEXDIR_FIELD] !== "." ? mapping[TEXDIR_FIELD] + "\/" : "") + geometry.textures[i]); + + 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; + httpMultiPart.add({ name: "texture" + i, buffer: textureBuffer diff --git a/libraries/script-engine/src/ArrayBufferPrototype.cpp b/libraries/script-engine/src/ArrayBufferPrototype.cpp index 6f78caad2d..9739f67381 100644 --- a/libraries/script-engine/src/ArrayBufferPrototype.cpp +++ b/libraries/script-engine/src/ArrayBufferPrototype.cpp @@ -11,6 +11,9 @@ #include +#include +#include + #include "ArrayBufferClass.h" #include "ArrayBufferPrototype.h" @@ -47,17 +50,40 @@ QByteArray ArrayBufferPrototype::slice(qint32 begin) const { } QByteArray ArrayBufferPrototype::compress() const { + // Compresses the ArrayBuffer data in Zlib format. QByteArray* ba = thisArrayBuffer(); QByteArray buffer = qCompress(*ba); - - // Qt's qCompress() default compression level (-1) is the standard zLib compression. - // Here remove Qt's custom header that prevents the data server from uncompressing the files with zLib. - buffer.remove(QCOMPRESS_HEADER_POSITION, QCOMPRESS_HEADER_SIZE); + buffer.remove(QCOMPRESS_HEADER_POSITION, QCOMPRESS_HEADER_SIZE); // Remove Qt's custom header to make it proper Zlib. return buffer; } +QByteArray ArrayBufferPrototype::recodeImage(const QString& sourceFormat, const QString& targetFormat, qint32 maxSize) const { + // Recodes image data if sourceFormat and targetFormat are different. + // Rescales image data if either dimension is greater than the specified maximum. + QByteArray* ba = thisArrayBuffer(); + + bool mustRecode = sourceFormat.toLower() != targetFormat.toLower(); + + QImage image = QImage::fromData(*ba); + if (image.width() > maxSize || image.height() > maxSize) { + image = image.scaled(maxSize, maxSize, Qt::KeepAspectRatio); + mustRecode = true; + } + + if (mustRecode) { + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + std::string str = targetFormat.toUpper().toStdString(); + const char* format = str.c_str(); + image.save(&buffer, format); + return buffer.data(); + } + + return *ba; +} + QByteArray* ArrayBufferPrototype::thisArrayBuffer() const { return qscriptvalue_cast(thisObject().data()); } diff --git a/libraries/script-engine/src/ArrayBufferPrototype.h b/libraries/script-engine/src/ArrayBufferPrototype.h index 2ad9843571..f9dd667dc4 100644 --- a/libraries/script-engine/src/ArrayBufferPrototype.h +++ b/libraries/script-engine/src/ArrayBufferPrototype.h @@ -24,6 +24,7 @@ public slots: QByteArray slice(qint32 begin, qint32 end) const; QByteArray slice(qint32 begin) const; QByteArray compress() const; + QByteArray recodeImage(const QString& sourceFormat, const QString& targetFormat, qint32 maxSize) const; private: QByteArray* thisArrayBuffer() const; From 0c589b73c473000dd7f59d2838d93d8ef40c5378 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 30 Jul 2014 22:12:11 -0700 Subject: [PATCH 34/62] Tidy model data handling --- examples/editModels.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 589f4e4bfb..f5183b16d6 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -282,6 +282,7 @@ var modelUploader = (function () { fbxBuffer, svoBuffer, mapping, + geometry, NAME_FIELD = "name", SCALE_FIELD = "scale", FILENAME_FIELD = "filename", @@ -290,14 +291,22 @@ var modelUploader = (function () { PITCH_FIELD = "pitch", YAW_FIELD = "yaw", ROLL_FIELD = "roll", - MAX_TEXTURE_SIZE = 1024, - geometry; + MAX_TEXTURE_SIZE = 1024; function error(message) { Window.alert(message); print(message); } + function resetDataObjects() { + fstBuffer = null; + fbxBuffer = null; + svoBuffer = null; + mapping = {}; + geometry = {}; + geometry.textures = []; + } + function readFile(filename) { var url = "file:///" + filename, req = new XMLHttpRequest(); @@ -318,7 +327,6 @@ var modelUploader = (function () { function readMapping(fstBuffer) { var dv = new DataView(fstBuffer.buffer), - mapping = {}, lines, line, values, @@ -342,13 +350,10 @@ var modelUploader = (function () { } } } - - return mapping; } function readGeometry(fbxBuffer) { - var geometry, - textures, + var textures, view, index, EOF; @@ -356,8 +361,6 @@ var modelUploader = (function () { // Reference: // http://code.blender.org/index.php/2013/08/fbx-binary-file-format-specification/ - geometry = {}; - geometry.textures = []; textures = {}; view = new DataView(fbxBuffer.buffer); EOF = false; @@ -458,8 +461,6 @@ var modelUploader = (function () { readTextFBX(); } - - return geometry; } function readModel() { @@ -467,11 +468,6 @@ var modelUploader = (function () { print("Reading model file: " + modelFile); - fstBuffer = null; - fbxBuffer = null; - svoBuffer = null; - mapping = {}; - if (modelFile.toLowerCase().slice(-4) === ".svo") { svoBuffer = readFile(modelFile); if (svoBuffer === null) { @@ -485,7 +481,7 @@ var modelUploader = (function () { if (fstBuffer === null) { return false; } - mapping = readMapping(fstBuffer); + readMapping(fstBuffer); if (mapping.hasOwnProperty(FILENAME_FIELD)) { fbxFilename = modelFile.path() + "\\" + mapping[FILENAME_FIELD]; } else { @@ -507,7 +503,7 @@ var modelUploader = (function () { return false; } - geometry = readGeometry(fbxBuffer); + readGeometry(fbxBuffer); if (mapping.hasOwnProperty(SCALE_FIELD)) { mapping[SCALE_FIELD] = parseFloat(mapping[SCALE_FIELD]); @@ -697,23 +693,29 @@ var modelUploader = (function () { modelFile = file; + resetDataObjects(); + // Read model content ... if (!readModel()) { + resetDataObjects(); return; } // Set model properties ... if (!setProperties()) { + resetDataObjects(); return; } // Put model in HTTP message ... if (!createHttpMessage()) { + resetDataObjects(); return; } // Send model to High Fidelity ... sendToHighFidelity(url, callback); + resetDataObjects(); }; return that; From 573ce7261b6a6291c03b6c0c23041a8762513224 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 31 Jul 2014 19:24:46 -0700 Subject: [PATCH 35/62] Add proper sending of ArrayBuffers via JavaScript XMLHttpRequest --- examples/editModels.js | 22 ++++++++----------- .../script-engine/src/XMLHttpRequestClass.cpp | 10 +++++---- .../script-engine/src/XMLHttpRequestClass.h | 2 +- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index f5183b16d6..b6d783700d 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -245,27 +245,23 @@ var httpMultiPart = (function () { function response() { var buffer, - view, - charCodes, + index, str, - i, - j; + i; str = crlf + boundaryString + "--\r\n"; buffer = str.toArrayBuffer(); byteLength += buffer.byteLength; parts.push(buffer); - str = ""; + buffer = new Uint8Array(byteLength); + index = 0; for (i = 0; i < parts.length; i += 1) { - charCodes = []; - view = new Uint8Array(parts[i]); - for (j = 0; j < view.length; j += 1) { - charCodes.push(view[j]); - } - str = str + String.fromCharCode.apply(String, charCodes); + buffer.set(new Uint8Array(parts[i]), index); + index += parts[i].byteLength; } - return str; + + return buffer; } that.response = response; @@ -685,7 +681,7 @@ var modelUploader = (function () { } }; - req.send(httpMultiPart.response()); + req.send(httpMultiPart.response().buffer); } that.upload = function (file, callback) { diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 563e268222..9d8988c43d 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -20,6 +20,8 @@ #include "XMLHttpRequestClass.h" #include "ScriptEngine.h" +Q_DECLARE_METATYPE(QByteArray*) + XMLHttpRequestClass::XMLHttpRequestClass(QScriptEngine* engine) : _engine(engine), _async(true), @@ -212,10 +214,10 @@ void XMLHttpRequestClass::open(const QString& method, const QString& url, bool a } void XMLHttpRequestClass::send() { - send(QString()); + send(QScriptValue::NullValue); } -void XMLHttpRequestClass::send(const QVariant& data) { +void XMLHttpRequestClass::send(const QScriptValue& data) { if (_readyState == OPENED && !_reply) { if (!data.isNull()) { if (_url.isLocalFile()) { @@ -223,8 +225,8 @@ void XMLHttpRequestClass::send(const QVariant& data) { return; } else { _sendData = new QBuffer(this); - if (_responseType == "arraybuffer") { - QByteArray ba = qvariant_cast(data); + if (data.isObject()) { + QByteArray ba = qscriptvalue_cast(data); _sendData->setData(ba); } else { _sendData->setData(data.toString().toUtf8()); diff --git a/libraries/script-engine/src/XMLHttpRequestClass.h b/libraries/script-engine/src/XMLHttpRequestClass.h index e482e57077..55bf646476 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.h +++ b/libraries/script-engine/src/XMLHttpRequestClass.h @@ -84,7 +84,7 @@ public slots: void open(const QString& method, const QString& url, bool async = true, const QString& username = "", const QString& password = ""); void send(); - void send(const QVariant& data); + void send(const QScriptValue& data); QScriptValue getAllResponseHeaders() const; QScriptValue getResponseHeader(const QString& name) const; From bd409f2de48d833f8f9ab8f61615acddc8baca70 Mon Sep 17 00:00:00 2001 From: Bennett Goble Date: Sun, 3 Aug 2014 14:01:37 -0400 Subject: [PATCH 36/62] importVoxels() JS override: specify file and location --- interface/interface_en.ts | 12 +- interface/src/Application.cpp | 21 +-- interface/src/Application.h | 5 +- .../scripting/ClipboardScriptingInterface.cpp | 31 ++++ .../scripting/ClipboardScriptingInterface.h | 3 + ...ImportDialog.cpp => VoxelImportDialog.cpp} | 133 +++++++++++++----- .../{ImportDialog.h => VoxelImportDialog.h} | 36 +++-- interface/src/voxels/VoxelImporter.cpp | 115 +++++---------- interface/src/voxels/VoxelImporter.h | 23 +-- 9 files changed, 217 insertions(+), 162 deletions(-) rename interface/src/ui/{ImportDialog.cpp => VoxelImportDialog.cpp} (77%) rename interface/src/ui/{ImportDialog.h => VoxelImportDialog.h} (74%) diff --git a/interface/interface_en.ts b/interface/interface_en.ts index 6c4426b2c6..b85628b104 100644 --- a/interface/interface_en.ts +++ b/interface/interface_en.ts @@ -245,28 +245,28 @@ QObject - - + + Import Voxels - + Loading ... - + Place voxels - + <b>Import</b> %1 as voxels - + Cancel diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 51f6076e7a..3caf9b2f5f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -137,7 +137,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _frameCount(0), _fps(60.0f), _justStarted(true), - _voxelImporter(NULL), + _voxelImportDialog(NULL), + _voxelImporter(), _importSucceded(false), _sharedVoxelSystem(TREE_SCALE, DEFAULT_MAX_VOXELS_PER_SYSTEM, &_clipboard), _wantToKillLocalVoxels(false), @@ -428,7 +429,7 @@ Application::~Application() { delete idleTimer; _sharedVoxelSystem.changeTree(new VoxelTree); - delete _voxelImporter; + delete _voxelImportDialog; // let the avatar mixer know we're out MyAvatar::sendKillAvatar(); @@ -463,8 +464,8 @@ void Application::saveSettings() { Menu::getInstance()->saveSettings(); _rearMirrorTools->saveSettings(_settings); - if (_voxelImporter) { - _voxelImporter->saveSettings(_settings); + if (_voxelImportDialog) { + _voxelImportDialog->saveSettings(_settings); } _settings->sync(); _numChangedSettings = 0; @@ -1568,17 +1569,17 @@ void Application::exportVoxels(const VoxelDetail& sourceVoxel) { void Application::importVoxels() { _importSucceded = false; - if (!_voxelImporter) { - _voxelImporter = new VoxelImporter(_window); - _voxelImporter->loadSettings(_settings); + if (!_voxelImportDialog) { + _voxelImportDialog = new VoxelImportDialog(_window); + _voxelImportDialog->loadSettings(_settings); } - if (!_voxelImporter->exec()) { + if (!_voxelImportDialog->exec()) { qDebug() << "Import succeeded." << endl; _importSucceded = true; } else { qDebug() << "Import failed." << endl; - if (_sharedVoxelSystem.getTree() == _voxelImporter->getVoxelTree()) { + if (_sharedVoxelSystem.getTree() == _voxelImporter.getVoxelTree()) { _sharedVoxelSystem.killLocalVoxels(); _sharedVoxelSystem.changeTree(&_clipboard); } @@ -1680,7 +1681,7 @@ void Application::init() { // Cleanup of the original shared tree _sharedVoxelSystem.init(); - _voxelImporter = new VoxelImporter(_window); + _voxelImportDialog = new VoxelImportDialog(_window); _environment.init(); diff --git a/interface/src/Application.h b/interface/src/Application.h index 54fb25839a..8c0a999370 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -86,6 +86,7 @@ #include "ui/overlays/Overlays.h" #include "ui/ApplicationOverlay.h" #include "ui/RunningScriptsWidget.h" +#include "ui/VoxelImportDialog.h" #include "voxels/VoxelFade.h" #include "voxels/VoxelHideShowThread.h" #include "voxels/VoxelImporter.h" @@ -192,6 +193,7 @@ public: Camera* getCamera() { return &_myCamera; } ViewFrustum* getViewFrustum() { return &_viewFrustum; } ViewFrustum* getShadowViewFrustum() { return &_shadowViewFrustum; } + VoxelImporter* getVoxelImporter() { return &_voxelImporter; } VoxelSystem* getVoxels() { return &_voxels; } VoxelTree* getVoxelTree() { return _voxels.getTree(); } const OctreePacketProcessor& getOctreePacketProcessor() const { return _octreeProcessor; } @@ -453,7 +455,8 @@ private: VoxelSystem _voxels; VoxelTree _clipboard; // if I copy/paste - VoxelImporter* _voxelImporter; + VoxelImportDialog* _voxelImportDialog; + VoxelImporter _voxelImporter; bool _importSucceded; VoxelSystem _sharedVoxelSystem; ViewFrustum _sharedVoxelSystemViewFrustum; diff --git a/interface/src/scripting/ClipboardScriptingInterface.cpp b/interface/src/scripting/ClipboardScriptingInterface.cpp index e8fb545343..0e63a386ed 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.cpp +++ b/interface/src/scripting/ClipboardScriptingInterface.cpp @@ -87,6 +87,37 @@ bool ClipboardScriptingInterface::importVoxels() { return Application::getInstance()->getImportSucceded(); } +bool ClipboardScriptingInterface::importVoxels(const QString& filename) { + qDebug() << "Importing ... "; + + VoxelImporter* importer = Application::getInstance()->getVoxelImporter(); + + if (!importer->validImportFile(filename)) { + return false; + } + + QEventLoop loop; + connect(importer, SIGNAL(importDone()), &loop, SLOT(quit())); + importer->import(filename); + loop.exec(); + + return true; +} + +bool ClipboardScriptingInterface::importVoxels(const QString& filename, float x, float y, float z, float s) { + bool success = importVoxels(filename); + + if (success) { + pasteVoxel(x, y, z, s); + } + + return success; +} + +bool ClipboardScriptingInterface::importVoxels(const QString& filename, const VoxelDetail& destinationVoxel) { + return importVoxels(filename, destinationVoxel.x, destinationVoxel.y, destinationVoxel.z, destinationVoxel.s); +} + void ClipboardScriptingInterface::nudgeVoxel(const VoxelDetail& sourceVoxel, const glm::vec3& nudgeVec) { nudgeVoxel(sourceVoxel.x, sourceVoxel.y, sourceVoxel.z, sourceVoxel.s, nudgeVec); } diff --git a/interface/src/scripting/ClipboardScriptingInterface.h b/interface/src/scripting/ClipboardScriptingInterface.h index f0258b0cc7..b7b1d85625 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.h +++ b/interface/src/scripting/ClipboardScriptingInterface.h @@ -39,6 +39,9 @@ public slots: void exportVoxel(float x, float y, float z, float s); bool importVoxels(); + bool importVoxels(const QString& filename); + bool importVoxels(const QString& filename, float x, float y, float z, float s); + bool importVoxels(const QString& filename, const VoxelDetail& destinationVoxel); void nudgeVoxel(const VoxelDetail& sourceVoxel, const glm::vec3& nudgeVec); void nudgeVoxel(float x, float y, float z, float s, const glm::vec3& nudgeVec); diff --git a/interface/src/ui/ImportDialog.cpp b/interface/src/ui/VoxelImportDialog.cpp similarity index 77% rename from interface/src/ui/ImportDialog.cpp rename to interface/src/ui/VoxelImportDialog.cpp index 67b89773fb..2d1b71ba7f 100644 --- a/interface/src/ui/ImportDialog.cpp +++ b/interface/src/ui/VoxelImportDialog.cpp @@ -1,5 +1,5 @@ // -// ImportDialog.cpp +// VoxelImportDialog.cpp // interface/src/ui // // Created by Clement Brisset on 8/12/13. @@ -20,7 +20,11 @@ #include "Application.h" -#include "ImportDialog.h" +#include "VoxelImportDialog.h" +#include "voxels/VoxelImporter.h" + +const QString SETTINGS_GROUP_NAME = "VoxelImport"; +const QString IMPORT_DIALOG_SETTINGS_KEY = "VoxelImportDialogSettings"; const QString WINDOW_NAME = QObject::tr("Import Voxels"); const QString IMPORT_BUTTON_NAME = QObject::tr("Import Voxels"); @@ -97,12 +101,14 @@ QString HiFiIconProvider::type(const QFileInfo &info) const { return QFileIconProvider::type(info); } -ImportDialog::ImportDialog(QWidget* parent) : +VoxelImportDialog::VoxelImportDialog(QWidget* parent) : QFileDialog(parent, WINDOW_NAME, DOWNLOAD_LOCATION, NULL), - _progressBar(this), - _importButton(IMPORT_BUTTON_NAME, this), _cancelButton(CANCEL_BUTTON_NAME, this), - _mode(importMode) { + _importButton(IMPORT_BUTTON_NAME, this), + _importer(Application::getInstance()->getVoxelImporter()), + _mode(importMode), + _progressBar(this), + _didImport(false) { setOption(QFileDialog::DontUseNativeDialog, true); setFileMode(QFileDialog::ExistingFile); @@ -113,41 +119,54 @@ ImportDialog::ImportDialog(QWidget* parent) : _progressBar.setRange(0, 100); - connect(&_importButton, SIGNAL(pressed()), SLOT(accept())); - connect(&_cancelButton, SIGNAL(pressed()), SIGNAL(canceled())); + connect(&_importButton, SIGNAL(pressed()), this, SLOT(accept())); + connect(&_cancelButton, SIGNAL(pressed()), this, SLOT(cancel())); connect(this, SIGNAL(currentChanged(QString)), SLOT(saveCurrentFile(QString))); } -void ImportDialog::reset() { - setMode(importMode); - _progressBar.setValue(0); +void VoxelImportDialog::cancel() { + switch (getMode()) { + case importMode: + _importer->cancel(); + close(); + break; + default: + _importer->reset(); + setMode(importMode); + break; + } + emit canceled(); } -void ImportDialog::setMode(dialogMode mode) { +void VoxelImportDialog::saveSettings(QSettings* settings) { + settings->beginGroup(SETTINGS_GROUP_NAME); + settings->setValue(IMPORT_DIALOG_SETTINGS_KEY, saveState()); + settings->endGroup(); +} + +void VoxelImportDialog::loadSettings(QSettings* settings) { + settings->beginGroup(SETTINGS_GROUP_NAME); + restoreState(settings->value(IMPORT_DIALOG_SETTINGS_KEY).toByteArray()); + settings->endGroup(); +} + +bool VoxelImportDialog::prompt() { + reset(); + exec(); + return _didImport; +} + +void VoxelImportDialog::reset() { + setMode(importMode); + _didImport = false; +} + +void VoxelImportDialog::setMode(dialogMode mode) { + dialogMode previousMode = _mode; _mode = mode; switch (_mode) { - case loadingMode: - _importButton.setEnabled(false); - _importButton.setText(LOADING_BUTTON_NAME); - findChild("sidebar")->setEnabled(false); - findChild("treeView")->setEnabled(false); - findChild("backButton")->setEnabled(false); - findChild("forwardButton")->setEnabled(false); - findChild("toParentButton")->setEnabled(false); - break; - case placeMode: - _progressBar.setValue(100); - _importButton.setEnabled(true); - _importButton.setText(PLACE_BUTTON_NAME); - findChild("sidebar")->setEnabled(false); - findChild("treeView")->setEnabled(false); - findChild("backButton")->setEnabled(false); - findChild("forwardButton")->setEnabled(false); - findChild("toParentButton")->setEnabled(false); - break; case importMode: - default: _progressBar.setValue(0); _importButton.setEnabled(true); _importButton.setText(IMPORT_BUTTON_NAME); @@ -157,22 +176,60 @@ void ImportDialog::setMode(dialogMode mode) { findChild("forwardButton")->setEnabled(true); findChild("toParentButton")->setEnabled(true); break; + case loadingMode: + // Connect to VoxelImporter signals + connect(_importer, SIGNAL(importProgress(int)), this, SLOT(updateProgressBar(int))); + connect(_importer, SIGNAL(importDone()), this, SLOT(afterImport())); + + _importButton.setEnabled(false); + _importButton.setText(LOADING_BUTTON_NAME); + findChild("sidebar")->setEnabled(false); + findChild("treeView")->setEnabled(false); + findChild("backButton")->setEnabled(false); + findChild("forwardButton")->setEnabled(false); + findChild("toParentButton")->setEnabled(false); + break; + case finishedMode: + if (previousMode == loadingMode) { + // Disconnect from VoxelImporter signals + disconnect(_importer, SIGNAL(importProgress(int)), this, SLOT(setProgressBarValue(int))); + disconnect(_importer, SIGNAL(importDone()), this, SLOT(afterImport())); + } + setMode(importMode); + break; } } -void ImportDialog::setProgressBarValue(int value) { +void VoxelImportDialog::setProgressBarValue(int value) { _progressBar.setValue(value); } -void ImportDialog::accept() { - emit accepted(); +void VoxelImportDialog::accept() { + if (getMode() == importMode) { + QString filename = getCurrentFile(); + + // If file is invalid we ignore the call + if (!_importer->validImportFile(filename)) { + return; + } + // Let's prepare the dialog window for import + setMode(loadingMode); + + _importer->import(filename); + } } -void ImportDialog::saveCurrentFile(QString filename) { +void VoxelImportDialog::afterImport() { + setMode(finishedMode); + _didImport = true; + close(); +} + +void VoxelImportDialog::saveCurrentFile(QString filename) { _currentFile = QFileInfo(filename).isFile() ? filename : ""; } -void ImportDialog::setLayout() { +void VoxelImportDialog::setLayout() { QGridLayout* gridLayout = (QGridLayout*) layout(); gridLayout->addWidget(&_progressBar, 2, 0, 2, 1); gridLayout->addWidget(&_cancelButton, 2, 1, 2, 1); @@ -258,7 +315,7 @@ void ImportDialog::setLayout() { } -void ImportDialog::setImportTypes() { +void VoxelImportDialog::setImportTypes() { QFile config(Application::resourcesPath() + "config/config.json"); config.open(QFile::ReadOnly | QFile::Text); QJsonDocument document = QJsonDocument::fromJson(config.readAll()); diff --git a/interface/src/ui/ImportDialog.h b/interface/src/ui/VoxelImportDialog.h similarity index 74% rename from interface/src/ui/ImportDialog.h rename to interface/src/ui/VoxelImportDialog.h index 88cfda7a7c..54faf7449a 100644 --- a/interface/src/ui/ImportDialog.h +++ b/interface/src/ui/VoxelImportDialog.h @@ -1,5 +1,5 @@ // -// ImportDialog.h +// VoxelImportDialog.h // interface/src/ui // // Created by Clement Brisset on 8/12/13. @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_ImportDialog_h -#define hifi_ImportDialog_h +#ifndef hifi_VoxelImportDialog_h +#define hifi_VoxelImportDialog_h #include "InterfaceConfig.h" @@ -23,6 +23,8 @@ #include +#include "voxels/VoxelImporter.h" + class HiFiIconProvider : public QFileIconProvider { public: HiFiIconProvider(const QHash map) { iconsMap = map; }; @@ -35,39 +37,45 @@ public: enum dialogMode { importMode, loadingMode, - placeMode + finishedMode }; -class ImportDialog : public QFileDialog { +class VoxelImportDialog : public QFileDialog { Q_OBJECT public: - ImportDialog(QWidget* parent = NULL); - void reset(); + VoxelImportDialog(QWidget* parent = NULL); QString getCurrentFile() const { return _currentFile; } dialogMode getMode() const { return _mode; } + void setMode(dialogMode mode); + void reset(); + bool prompt(); + void loadSettings(QSettings* settings); + void saveSettings(QSettings* settings); signals: void canceled(); - -public slots: - void setProgressBarValue(int value); private slots: + void setProgressBarValue(int value); void accept(); + void cancel(); void saveCurrentFile(QString filename); + void afterImport(); private: - QString _currentFile; - QProgressBar _progressBar; - QPushButton _importButton; QPushButton _cancelButton; + QString _currentFile; + QPushButton _importButton; + VoxelImporter* _importer; dialogMode _mode; + QProgressBar _progressBar; + bool _didImport; void setLayout(); void setImportTypes(); }; -#endif // hifi_ImportDialog_h +#endif // hifi_VoxelImportDialog_h diff --git a/interface/src/voxels/VoxelImporter.cpp b/interface/src/voxels/VoxelImporter.cpp index f7d5562c06..b713813fb0 100644 --- a/interface/src/voxels/VoxelImporter.cpp +++ b/interface/src/voxels/VoxelImporter.cpp @@ -20,8 +20,7 @@ #include "voxels/VoxelImporter.h" -const QString SETTINGS_GROUP_NAME = "VoxelImport"; -const QString IMPORT_DIALOG_SETTINGS_KEY = "ImportDialogSettings"; +const QStringList SUPPORTED_EXTENSIONS = QStringList() << "png" << "svo" << "schematic"; class ImportTask : public QObject, public QRunnable { public: @@ -32,104 +31,46 @@ private: QString _filename; }; -VoxelImporter::VoxelImporter(QWidget* parent) : - QObject(parent), +VoxelImporter::VoxelImporter() : _voxelTree(true), - _importDialog(parent), - _task(NULL), - _didImport(false) + _task(NULL) { LocalVoxelsList::getInstance()->addPersistantTree(IMPORT_TREE_NAME, &_voxelTree); - connect(&_voxelTree, SIGNAL(importProgress(int)), &_importDialog, SLOT(setProgressBarValue(int))); - connect(&_importDialog, SIGNAL(canceled()), this, SLOT(cancel())); - connect(&_importDialog, SIGNAL(accepted()), this, SLOT(import())); -} - -void VoxelImporter::saveSettings(QSettings* settings) { - settings->beginGroup(SETTINGS_GROUP_NAME); - settings->setValue(IMPORT_DIALOG_SETTINGS_KEY, _importDialog.saveState()); - settings->endGroup(); -} - -void VoxelImporter::loadSettings(QSettings* settings) { - settings->beginGroup(SETTINGS_GROUP_NAME); - _importDialog.restoreState(settings->value(IMPORT_DIALOG_SETTINGS_KEY).toByteArray()); - settings->endGroup(); + connect(&_voxelTree, SIGNAL(importProgress(int)), this, SIGNAL(importProgress(int))); } VoxelImporter::~VoxelImporter() { cleanupTask(); } +void VoxelImporter::cancel() { + if (_task) { + disconnect(_task, 0, 0, 0); + } + reset(); +} + void VoxelImporter::reset() { _voxelTree.eraseAllOctreeElements(); - _importDialog.reset(); - cleanupTask(); } -int VoxelImporter::exec() { - reset(); - _importDialog.exec(); - - if (!_didImport) { - // if the import is rejected, we make sure to cleanup before leaving +void VoxelImporter::import(const QString& filename) { + // If present, abort existing import + if (_task) { cleanupTask(); - return 1; - } else { - _didImport = false; - return 0; } -} -void VoxelImporter::import() { - switch (_importDialog.getMode()) { - case loadingMode: - _importDialog.setMode(placeMode); - return; - case placeMode: - // Means the user chose to import - _didImport = true; - _importDialog.close(); - return; - case importMode: - default: - QString filename = _importDialog.getCurrentFile(); - // if it's not a file, we ignore the call - if (!QFileInfo(filename).isFile()) { - return; - } - - // Let's prepare the dialog window for import - _importDialog.setMode(loadingMode); - - // If not already done, we switch to the local tree - if (Application::getInstance()->getSharedVoxelSystem()->getTree() != &_voxelTree) { - Application::getInstance()->getSharedVoxelSystem()->changeTree(&_voxelTree); - } - - // Creation and launch of the import task on the thread pool - _task = new ImportTask(filename); - connect(_task, SIGNAL(destroyed()), SLOT(import())); - QThreadPool::globalInstance()->start(_task); - break; + // If not already done, we switch to the local tree + if (Application::getInstance()->getSharedVoxelSystem()->getTree() != &_voxelTree) { + Application::getInstance()->getSharedVoxelSystem()->changeTree(&_voxelTree); } -} -void VoxelImporter::cancel() { - switch (_importDialog.getMode()) { - case loadingMode: - disconnect(_task, 0, 0, 0); - cleanupTask(); - case placeMode: - _importDialog.setMode(importMode); - break; - case importMode: - default: - _importDialog.close(); - break; - } + // Creation and launch of the import task on the thread pool + _task = new ImportTask(filename); + connect(_task, SIGNAL(destroyed()), SLOT(finishImport())); + QThreadPool::globalInstance()->start(_task); } void VoxelImporter::cleanupTask() { @@ -140,6 +81,16 @@ void VoxelImporter::cleanupTask() { } } +void VoxelImporter::finishImport() { + cleanupTask(); + emit importDone(); +} + +bool VoxelImporter::validImportFile(const QString& filename) { + QFileInfo fileInfo = QFileInfo(filename); + return fileInfo.isFile() && SUPPORTED_EXTENSIONS.indexOf(fileInfo.suffix().toLower()) != -1; +} + ImportTask::ImportTask(const QString &filename) : _filename(filename) { @@ -151,7 +102,7 @@ void ImportTask::run() { // We start by cleaning up the shared voxel system just in case voxelSystem->killLocalVoxels(); - // Then we call the righ method for the job + // Then we call the right method for the job if (_filename.endsWith(".png", Qt::CaseInsensitive)) { voxelSystem->getTree()->readFromSquareARGB32Pixels(_filename.toLocal8Bit().data()); } else if (_filename.endsWith(".svo", Qt::CaseInsensitive)) { @@ -163,6 +114,6 @@ void ImportTask::run() { qDebug() << "[ERROR] Invalid file extension." << endl; } - // Here we reaverage the tree so that he is ready for preview + // Here we reaverage the tree so that it is ready for preview voxelSystem->getTree()->reaverageOctreeElements(); } diff --git a/interface/src/voxels/VoxelImporter.h b/interface/src/voxels/VoxelImporter.h index 7da89c5a11..21ebbeea2e 100644 --- a/interface/src/voxels/VoxelImporter.h +++ b/interface/src/voxels/VoxelImporter.h @@ -14,8 +14,8 @@ #include #include +#include -#include "ui/ImportDialog.h" #include "voxels/VoxelSystem.h" class ImportTask; @@ -23,28 +23,29 @@ class ImportTask; class VoxelImporter : public QObject { Q_OBJECT public: - VoxelImporter(QWidget* parent = NULL); + VoxelImporter(); ~VoxelImporter(); void reset(); - void loadSettings(QSettings* settings); - void saveSettings(QSettings* settings); - + void cancel(); VoxelTree* getVoxelTree() { return &_voxelTree; } + bool validImportFile(const QString& filename); public slots: - int exec(); - void import(); - void cancel(); + void import(const QString& filename); + +signals: + void importDone(); + void importProgress(int); private: VoxelTree _voxelTree; - ImportDialog _importDialog; - ImportTask* _task; - bool _didImport; void cleanupTask(); + +private slots: + void finishImport(); }; #endif // hifi_VoxelImporter_h From 925270e7279895b4209f07e5d7a7b1e2e12f4d9a Mon Sep 17 00:00:00 2001 From: Bennett Goble Date: Sun, 3 Aug 2014 14:06:51 -0400 Subject: [PATCH 37/62] clipboardExample.js - Replace voxel feature --- examples/clipboardExample.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/clipboardExample.js b/examples/clipboardExample.js index e6db44054f..0b6371e2b7 100644 --- a/examples/clipboardExample.js +++ b/examples/clipboardExample.js @@ -24,6 +24,7 @@ function setupMenus() { Menu.removeMenuItem("Edit", "Paste"); Menu.removeMenuItem("Edit", "Delete"); Menu.removeMenuItem("Edit", "Nudge"); + Menu.removeMenuItem("Edit", "Replace from File"); Menu.removeMenuItem("File", "Export Voxels"); Menu.removeMenuItem("File", "Import Voxels"); @@ -32,6 +33,7 @@ function setupMenus() { Menu.addMenuItem({ menuName: "Edit", menuItemName: "Copy", shortcutKey: "CTRL+C", afterItem: "Cut" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Paste", shortcutKey: "CTRL+V", afterItem: "Copy" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Nudge", shortcutKey: "CTRL+N", afterItem: "Paste" }); + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Replace from File", shortcutKey: "CTRL+R", afterItem: "Nudge" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Delete", shortcutKeyEvent: { text: "backspace" }, afterItem: "Nudge" }); Menu.addMenuItem({ menuName: "File", menuItemName: "Export Voxels", shortcutKey: "CTRL+E", afterItem: "Voxels" }); Menu.addMenuItem({ menuName: "File", menuItemName: "Import Voxels", shortcutKey: "CTRL+I", afterItem: "Export Voxels" }); @@ -60,7 +62,6 @@ function menuItemEvent(menuItem) { print("deleting..."); Clipboard.deleteVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s); } - if (menuItem == "Export Voxels") { print("export"); Clipboard.exportVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s); @@ -73,6 +74,12 @@ function menuItemEvent(menuItem) { print("nudge"); Clipboard.nudgeVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s, { x: -1, y: 0, z: 0 }); } + if (menuItem == "Replace from File") { + var filename = Window.browse("Select file to load replacement", "", "Voxel Files (*.png *.svo *.schematic)"); + if (filename) { + Clipboard.importVoxel(filename, selectedVoxel); + } + } } var selectCube = Overlays.addOverlay("cube", { From 83a868d7418534bd0ee6a51cb78fe298b98b5470 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 7 Aug 2014 15:20:30 -0700 Subject: [PATCH 38/62] Make XMLHttpRequest automatically authorize API calls --- libraries/script-engine/src/XMLHttpRequestClass.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 9d8988c43d..cb891c2ab1 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -17,6 +17,7 @@ #include +#include #include "XMLHttpRequestClass.h" #include "ScriptEngine.h" @@ -201,6 +202,9 @@ void XMLHttpRequestClass::open(const QString& method, const QString& url, bool a notImplemented(); } } else { + if (url.toLower().left(33) == "https://data.highfidelity.io/api/") { + _url.setQuery("access_token=" + AccountManager::getInstance().getAccountInfo().getAccessToken().token); + } if (!username.isEmpty()) { _url.setUserName(username); } From 70548976dbec7f11f3d4e2c8f71a8fd2b6be29b0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 7 Aug 2014 15:27:30 -0700 Subject: [PATCH 39/62] Fix boundary string format to not include curly braces --- examples/editModels.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/editModels.js b/examples/editModels.js index b6d783700d..1f1c349738 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -169,7 +169,7 @@ var httpMultiPart = (function () { crlf; function clear() { - boundaryString = "--boundary_" + Uuid.generate() + "="; + boundaryString = "--boundary_" + String(Uuid.generate()).slice(1, 36) + "="; parts = []; byteLength = 0; crlf = ""; From eaf0b366d009807314de697d934846f3c1ebcbdc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 7 Aug 2014 15:30:27 -0700 Subject: [PATCH 40/62] Update model reading in preparation for API upload --- examples/editModels.js | 96 +++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 1f1c349738..0042fe29ab 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -64,6 +64,13 @@ if (typeof String.prototype.fileName !== "function") { }; } +if (typeof String.prototype.fileBase !== "function") { + String.prototype.fileBase = function () { + var filename = this.fileName(); + return filename.slice(0, filename.indexOf(".")); + }; +} + if (typeof String.prototype.fileType !== "function") { String.prototype.fileType = function () { return this.slice(this.lastIndexOf(".") + 1); @@ -272,13 +279,14 @@ var httpMultiPart = (function () { var modelUploader = (function () { var that = {}, - urlBase = "http://public.highfidelity.io/meshes/", modelFile, fstBuffer, fbxBuffer, svoBuffer, mapping, geometry, + API_URL = "https://data.highfidelity.io/api/v1/models", + MODEL_URL = "http://public.highfidelity.io/models/content", NAME_FIELD = "name", SCALE_FIELD = "scale", FILENAME_FIELD = "filename", @@ -290,8 +298,8 @@ var modelUploader = (function () { MAX_TEXTURE_SIZE = 1024; function error(message) { - Window.alert(message); print(message); + Window.alert(message); } function resetDataObjects() { @@ -460,57 +468,71 @@ var modelUploader = (function () { } function readModel() { - var fbxFilename; + var fbxFilename, + svoFilename, + fileType; print("Reading model file: " + modelFile); - if (modelFile.toLowerCase().slice(-4) === ".svo") { - svoBuffer = readFile(modelFile); - if (svoBuffer === null) { + if (modelFile.toLowerCase().slice(-4) === ".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 if (modelFile.toLowerCase().slice(-4) === ".fbx") { + fbxFilename = modelFile; + mapping[FILENAME_FIELD] = modelFile.fileName(); + + } else if (modelFile.toLowerCase().slice(-4) === ".svo") { + svoFilename = modelFile; + mapping[FILENAME_FIELD] = modelFile.fileName(); } else { + error("Unrecognized file type: " + modelFile); + return false; + } - if (modelFile.toLowerCase().slice(-4) === ".fst") { - fstBuffer = readFile(modelFile); - if (fstBuffer === null) { - return false; - } - readMapping(fstBuffer); - if (mapping.hasOwnProperty(FILENAME_FIELD)) { - fbxFilename = modelFile.path() + "\\" + mapping[FILENAME_FIELD]; - } else { - error("FBX file name not found in FST file!"); - return false; - } - - } else if (modelFile.toLowerCase().slice(-4) === ".fbx") { - fbxFilename = modelFile; - mapping[FILENAME_FIELD] = modelFile.fileName(); - - } else { - error("Unrecognized file type: " + modelFile); - return false; - } - + if (fbxFilename) { fbxBuffer = readFile(fbxFilename); if (fbxBuffer === null) { return false; } readGeometry(fbxBuffer); + } - if (mapping.hasOwnProperty(SCALE_FIELD)) { - mapping[SCALE_FIELD] = parseFloat(mapping[SCALE_FIELD]); - } else { - mapping[SCALE_FIELD] = (geometry.author === "www.makehuman.org" ? 150.0 : 15.0); + if (svoFilename) { + svoBuffer = readFile(svoFilename); + if (svoBuffer === null) { + return false; } } + if (mapping.hasOwnProperty(SCALE_FIELD)) { + mapping[SCALE_FIELD] = parseFloat(mapping[SCALE_FIELD]); + } else { + mapping[SCALE_FIELD] = (geometry.author === "www.makehuman.org" ? 150.0 : 15.0); + } + // Add any missing basic mappings if (!mapping.hasOwnProperty(NAME_FIELD)) { - mapping[NAME_FIELD] = modelFile.fileName().slice(0, -4); + mapping[NAME_FIELD] = modelFile.fileName().fileBase(); } if (!mapping.hasOwnProperty(TEXDIR_FIELD)) { mapping[TEXDIR_FIELD] = "."; @@ -626,6 +648,9 @@ var modelUploader = (function () { for (lodFile in mapping.lod) { if (mapping.lod.hasOwnProperty(lodFile)) { lodBuffer = readFile(modelFile.path() + "\/" + lodFile); + if (lodBuffer === null) { + return false; + } httpMultiPart.add({ name: "lod" + lodCount, buffer: lodBuffer @@ -639,6 +664,9 @@ var modelUploader = (function () { textureBuffer = readFile(modelFile.path() + "\/" + (mapping[TEXDIR_FIELD] !== "." ? mapping[TEXDIR_FIELD] + "\/" : "") + geometry.textures[i]); + if (textureBuffer === null) { + return false; + } textureSourceFormat = geometry.textures[i].fileType().toLowerCase(); textureTargetFormat = (textureSourceFormat === "jpg" ? "jpg" : "png"); @@ -654,7 +682,7 @@ var modelUploader = (function () { // Model category httpMultiPart.add({ name : "model_category", - string : "item" // DJRTODO: What model category to use? + string : "content" }); return true; From 7f2d33c4e43f7ad1e37e2b122e5d3f49eeaca1d9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 7 Aug 2014 15:31:44 -0700 Subject: [PATCH 41/62] Add in model uploading via API --- examples/editModels.js | 147 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 132 insertions(+), 15 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 0042fe29ab..67760acdb0 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -688,32 +688,148 @@ var modelUploader = (function () { return true; } - function sendToHighFidelity(url, callback) { - var req; + function sendToHighFidelity(addModelCallback) { + var req, + modelName, + modelURL, + 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; - print("Sending model to High Fidelity"); + function uploadTimedOut() { + error("Model upload failed: Internet request timed out!"); + } - req = new XMLHttpRequest(); - req.open("POST", url, true); - req.setRequestHeader("Content-Type", "multipart/form-data; boundary=\"" + httpMultiPart.boundary() + "\""); + 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()); + } - req.onreadystatechange = function () { + function checkUploaded() { + print("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) { - print("Uploaded model: " + url); - callback(url); + // Note: Unlike avatar models, for content models we don't need to refresh texture cache. + addModelCallback(modelURL); // Add model to the world + print("Model uploaded: " + modelURL); + Window.alert("Your model has been uploaded as: " + 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 uploading file: " + req.status + " " + req.statusText); - Window.alert("Could not upload file: " + req.status + " " + req.statusText); + print("Error: " + req.status + " " + req.statusText); + error("There was a problem with your upload, please try again later."); } } }; - req.send(httpMultiPart.response().buffer); + function uploadModel(method) { + var url; + + 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 = 30; + checkUploaded(); + } else { + print("Error: " + req.status + " " + req.statusText); + error("There was a problem with your upload, please try again later."); + } + } + }; + + function requestUpload() { + var url; + + 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."); + } + }; + + print("Sending model to High Fidelity"); + + modelName = mapping[NAME_FIELD]; + modelURL = MODEL_URL + "\/" + mapping[NAME_FIELD] + ".fst"; // DJRTODO: Do all models get a FST? + + requestUpload(); } - that.upload = function (file, callback) { - var url = urlBase + file.fileName(); + that.upload = function (file, addModelCallback) { modelFile = file; @@ -738,7 +854,8 @@ var modelUploader = (function () { } // Send model to High Fidelity ... - sendToHighFidelity(url, callback); + sendToHighFidelity(addModelCallback); + resetDataObjects(); }; From c82a2003a21d63b38ba66bf65b1f28d351892660 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Aug 2014 11:14:36 -0700 Subject: [PATCH 42/62] Add prompt asking user whether they want to rez their uploaded model A user updating an existing model may not want to rez a new copy. --- examples/editModels.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 67760acdb0..39dcea7cad 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -732,9 +732,10 @@ var modelUploader = (function () { if (req.readyState === req.DONE) { if (req.status === 200) { // Note: Unlike avatar models, for content models we don't need to refresh texture cache. - addModelCallback(modelURL); // Add model to the world print("Model uploaded: " + modelURL); - Window.alert("Your model has been uploaded as: " + modelURL); + if (Window.confirm("Your model has been uploaded as: " + modelURL + "\nDo you want to rez it?")) { + addModelCallback(modelURL); + } } else if (req.status === 404) { if (uploadedChecks > 0) { uploadedChecks -= 1; @@ -943,8 +944,9 @@ var toolBar = (function () { radius: DEFAULT_RADIUS, modelURL: url }); + print("Model added: " + url); } else { - print("Can't create model: Model would be out of bounds."); + print("Can't add model: Model would be out of bounds."); } } From d1701c12cab3182a1a26db5b14621559f0440a63 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Aug 2014 13:36:01 -0700 Subject: [PATCH 43/62] Rewrite FST content so that it includes changed and new properties --- examples/editModels.js | 64 +++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 39dcea7cad..02e7dbf8fb 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -329,33 +329,69 @@ var modelUploader = (function () { }; } - function readMapping(fstBuffer) { - var dv = new DataView(fstBuffer.buffer), + function readMapping(buffer) { + var dv = new DataView(buffer.buffer), lines, line, - values, + tokens, + i, name, - i; + value, + remainder; - // Simplified to target values relevant to model uploading. + 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] !== "#") { - values = line.split(/\s*=\s*/); - name = values[0].toLowerCase(); - if (values.length === 2) { - mapping[name] = values[1]; - } else if (values.length === 3 && name === "lod") { - if (mapping[name] === undefined) { - mapping[name] = {}; + tokens = line.split(/\s*=\s*/); + if (tokens.length > 1) { + name = tokens[0]; + value = tokens[1]; + if (tokens.length === 2) { + mapping[name] = value; + } else { + // We're only interested in the first two fields so put the rest in the remainder + remainder = tokens.slice(2, tokens.length).join(" = "); + if (mapping[name] === undefined) { + mapping[name] = {}; + } + mapping[name][value] = remainder; } - mapping[name][values[1]] = values[2]; } } } } + function writeMapping(buffer) { + var name, + value, + remainder, + string = ""; + + for (name in mapping) { + if (mapping.hasOwnProperty(name)) { + if (typeof mapping[name] === "string") { + string += (name + " = " + mapping[name] + "\n"); + } else { + for (value in mapping[name]) { + if (mapping[name].hasOwnProperty(value)) { + remainder = mapping[name][value]; + if (remainder === null) { + remainder = ""; + } else { + remainder = " = " + remainder; + } + string += (name + " = " + value + remainder + "\n"); + } + } + } + } + } + + buffer.buffer = string.toArrayBuffer(); + } + function readGeometry(fbxBuffer) { var textures, view, @@ -591,6 +627,8 @@ var modelUploader = (function () { mapping[ROLL_FIELD] = form[5].value; mapping[SCALE_FIELD] = form[6].value; + writeMapping(fstBuffer); + return true; } From 5b772749696c1c5b558ef4fe6738f69ed64093b9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Aug 2014 13:39:11 -0700 Subject: [PATCH 44/62] Remove animation, pitch, yaw, and roll from Set Model Properties dialog --- examples/editModels.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 02e7dbf8fb..1704368c6a 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -291,10 +291,6 @@ var modelUploader = (function () { SCALE_FIELD = "scale", FILENAME_FIELD = "filename", TEXDIR_FIELD = "texdir", - ANIMATION_URL_FIELD = "animationurl", - PITCH_FIELD = "pitch", - YAW_FIELD = "yaw", - ROLL_FIELD = "roll", MAX_TEXTURE_SIZE = 1024; function error(message) { @@ -604,10 +600,6 @@ var modelUploader = (function () { errorMessage: "Texture directory must be subdirectory of model directory." }); - form.push({ label: "Animation URL:", value: "" }); - form.push({ label: "Pitch:", value: (0).toFixed(decimals) }); - form.push({ label: "Yaw:", value: (0).toFixed(decimals) }); - form.push({ label: "Roll:", value: (0).toFixed(decimals) }); form.push({ label: "Scale:", value: mapping[SCALE_FIELD].toFixed(decimals) }); form.push({ button: "Cancel" }); @@ -621,10 +613,6 @@ var modelUploader = (function () { if (mapping[TEXDIR_FIELD] === "") { mapping[TEXDIR_FIELD] = "."; } - mapping[ANIMATION_URL_FIELD] = form[2].value; - mapping[PITCH_FIELD] = form[3].value; - mapping[YAW_FIELD] = form[4].value; - mapping[ROLL_FIELD] = form[5].value; mapping[SCALE_FIELD] = form[6].value; writeMapping(fstBuffer); From c50be5a872ecd19ac739b8515d43c7438828ac41 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Aug 2014 13:43:25 -0700 Subject: [PATCH 45/62] Miscellaneous fixes --- examples/editModels.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 1704368c6a..8aaeedd7f8 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -613,7 +613,7 @@ var modelUploader = (function () { if (mapping[TEXDIR_FIELD] === "") { mapping[TEXDIR_FIELD] = "."; } - mapping[SCALE_FIELD] = form[6].value; + mapping[SCALE_FIELD] = form[2].value; writeMapping(fstBuffer); @@ -800,7 +800,7 @@ var modelUploader = (function () { //debugResponse(); if (req.readyState === req.DONE) { if (req.status === 200) { - uploadedChecks = 30; + uploadedChecks = UPLOADED_CHECKS; checkUploaded(); } else { print("Error: " + req.status + " " + req.statusText); From 198fa47409642241a7ce06ace84c0965ecedbc72 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Aug 2014 16:38:27 -0700 Subject: [PATCH 46/62] Get "naked" FBX model content uploading working Include an automatically created FST file in the upload --- examples/editModels.js | 44 +++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 8aaeedd7f8..5525f0b128 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -298,6 +298,18 @@ var modelUploader = (function () { 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; @@ -506,7 +518,7 @@ var modelUploader = (function () { print("Reading model file: " + modelFile); - if (modelFile.toLowerCase().slice(-4) === ".fst") { + if (modelFile.toLowerCase().fileType() === "fst") { fstBuffer = readFile(modelFile); if (fstBuffer === null) { return false; @@ -526,18 +538,24 @@ var modelUploader = (function () { error("Model file name not found in FST file!"); return false; } - - } else if (modelFile.toLowerCase().slice(-4) === ".fbx") { - fbxFilename = modelFile; - mapping[FILENAME_FIELD] = modelFile.fileName(); - - } else if (modelFile.toLowerCase().slice(-4) === ".svo") { - svoFilename = modelFile; - mapping[FILENAME_FIELD] = modelFile.fileName(); - } else { - error("Unrecognized file type: " + modelFile); - return false; + 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 (fbxFilename) { @@ -851,7 +869,7 @@ var modelUploader = (function () { print("Sending model to High Fidelity"); modelName = mapping[NAME_FIELD]; - modelURL = MODEL_URL + "\/" + mapping[NAME_FIELD] + ".fst"; // DJRTODO: Do all models get a FST? + modelURL = MODEL_URL + "\/" + mapping[NAME_FIELD] + ".fst"; // All models are uploaded as an FST requestUpload(); } From 0f9da8e13fdd5b753fe5c11fcb330b59b45851c6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Aug 2014 17:34:08 -0700 Subject: [PATCH 47/62] Make mapping writing more robust --- examples/editModels.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 5525f0b128..a7eac56773 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -379,9 +379,7 @@ var modelUploader = (function () { for (name in mapping) { if (mapping.hasOwnProperty(name)) { - if (typeof mapping[name] === "string") { - string += (name + " = " + mapping[name] + "\n"); - } else { + if (typeof mapping[name] === "object") { for (value in mapping[name]) { if (mapping[name].hasOwnProperty(value)) { remainder = mapping[name][value]; @@ -393,6 +391,8 @@ var modelUploader = (function () { string += (name + " = " + value + remainder + "\n"); } } + } else { + string += (name + " = " + mapping[name] + "\n"); } } } From 35bc1a03af0506a7b612bbbad025cd104e52397d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Aug 2014 17:35:36 -0700 Subject: [PATCH 48/62] Simplify model scale to be that specified in FST or 1.0 No UI --- examples/editModels.js | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index a7eac56773..593c6d20ce 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -445,11 +445,6 @@ var modelUploader = (function () { textures[filename] = ""; geometry.textures.push(filename); } - - } else if (name === "author") { - author = view.string(index + 5, view.getUint32(index + 1, true)); - geometry.author = author; - } index += (propertyListLength); @@ -476,12 +471,6 @@ var modelUploader = (function () { charCode = view[index]; if (charCode === 10) { // Can ignore EOF line = String.fromCharCode.apply(String, charCodes).trim(); - - if (line.slice(0, 7).toLowerCase() === "author:") { - author = line.slice(line.indexOf("\""), line.lastIndexOf("\"") - line.length); - geometry.author = author; - - } if (line.slice(0, 17).toLowerCase() === "relativefilename:") { filename = line.slice(line.indexOf("\""), line.lastIndexOf("\"") - line.length).fileName(); if (!textures.hasOwnProperty(filename)) { @@ -489,7 +478,6 @@ var modelUploader = (function () { geometry.textures.push(filename); } } - charCodes = []; } else { charCodes.push(charCode); @@ -574,12 +562,6 @@ var modelUploader = (function () { } } - if (mapping.hasOwnProperty(SCALE_FIELD)) { - mapping[SCALE_FIELD] = parseFloat(mapping[SCALE_FIELD]); - } else { - mapping[SCALE_FIELD] = (geometry.author === "www.makehuman.org" ? 150.0 : 15.0); - } - // Add any missing basic mappings if (!mapping.hasOwnProperty(NAME_FIELD)) { mapping[NAME_FIELD] = modelFile.fileName().fileBase(); @@ -588,7 +570,7 @@ var modelUploader = (function () { mapping[TEXDIR_FIELD] = "."; } if (!mapping.hasOwnProperty(SCALE_FIELD)) { - mapping[SCALE_FIELD] = 0.2; // For SVO models. + mapping[SCALE_FIELD] = 1.0; } return true; @@ -618,7 +600,6 @@ var modelUploader = (function () { errorMessage: "Texture directory must be subdirectory of model directory." }); - form.push({ label: "Scale:", value: mapping[SCALE_FIELD].toFixed(decimals) }); form.push({ button: "Cancel" }); if (!Window.form("Set Model Properties", form)) { @@ -631,7 +612,6 @@ var modelUploader = (function () { if (mapping[TEXDIR_FIELD] === "") { mapping[TEXDIR_FIELD] = "."; } - mapping[SCALE_FIELD] = form[2].value; writeMapping(fstBuffer); From 54ea86d6be7bec8a9d0dad7d86588336683b8cbf Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Aug 2014 20:40:15 -0700 Subject: [PATCH 49/62] Fix texture directory display and validation --- examples/editModels.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 593c6d20ce..ffd3940814 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -83,6 +83,12 @@ if (typeof String.prototype.path !== "function") { }; } +if (typeof String.prototype.regExpEscape !== "function") { + String.prototype.regExpEscape = function () { + return this.replace(/([$^.+*?|\\\/{}()\[\]])/g, '\\$1'); + } +} + if (typeof String.prototype.toArrayBuffer !== "function") { String.prototype.toArrayBuffer = function () { var length, @@ -588,8 +594,8 @@ var modelUploader = (function () { form.push({ label: "Name:", value: mapping[NAME_FIELD] }); directory = modelFile.path() + "/" + mapping[TEXDIR_FIELD]; - displayAs = new RegExp("^" + modelFile.path().replace(/[\\\\\\\/]/, "[\\\\\\\/]") + "[\\\\\\\/](.*)"); - validateAs = new RegExp("^" + modelFile.path().replace(/[\\\\\\\/]/, "[\\\\\\\/]") + "([\\\\\\\/].*)?"); + displayAs = new RegExp("^" + modelFile.path().regExpEscape() + "[\\\\\\\/](.*)"); + validateAs = new RegExp("^" + modelFile.path().regExpEscape() + "([\\\\\\\/].*)?"); form.push({ label: "Texture directory:", @@ -597,7 +603,7 @@ var modelUploader = (function () { title: "Choose Texture Directory", displayAs: displayAs, validateAs: validateAs, - errorMessage: "Texture directory must be subdirectory of model directory." + errorMessage: "Texture directory must be subdirectory of the model directory." }); form.push({ button: "Cancel" }); From 9a21f1c355581ae607a548ef72ac120e335dd7ba Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Aug 2014 21:51:14 -0700 Subject: [PATCH 50/62] Fix FST file reading and writing --- examples/editModels.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index ffd3940814..5fda380d55 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -103,7 +103,7 @@ if (typeof String.prototype.toArrayBuffer !== "function") { length = this.length; for (i = 0; i < length; i += 1) { charCode = this.charCodeAt(i); - if (i <= 255) { + if (charCode <= 255) { charCodes.push(charCode); } else { charCodes.push(charCode / 256); @@ -351,7 +351,8 @@ var modelUploader = (function () { i, name, value, - remainder; + remainder, + existing; mapping = {}; // { name : value | name : { value : remainder } } lines = dv.string(0, dv.byteLength).split(/\r\n|\r|\n/); @@ -362,13 +363,19 @@ var modelUploader = (function () { if (tokens.length > 1) { name = tokens[0]; value = tokens[1]; - if (tokens.length === 2) { + 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 { - // We're only interested in the first two fields so put the rest in the remainder - remainder = tokens.slice(2, tokens.length).join(" = "); if (mapping[name] === undefined) { mapping[name] = {}; + } else if (typeof mapping[name] !== "object") { + existing = mapping[name]; + mapping[name] = { existing: null }; } mapping[name][value] = remainder; } From f12973d5d0ed88b0425cdcbe99b2786aeb4ec97d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 11 Aug 2014 10:12:47 -0700 Subject: [PATCH 51/62] Cater for multiple mapping values --- examples/editModels.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 5fda380d55..8b54b9e48a 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -354,7 +354,7 @@ var modelUploader = (function () { remainder, existing; - mapping = {}; // { name : value | name : { value : remainder } } + 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(); @@ -373,11 +373,16 @@ var modelUploader = (function () { } else { if (mapping[name] === undefined) { mapping[name] = {}; + } else if (typeof mapping[name] !== "object") { existing = mapping[name]; - mapping[name] = { existing: null }; + mapping[name] = { existing : null }; } - mapping[name][value] = remainder; + + if (mapping[name][value] === undefined) { + mapping[name][value] = []; + } + mapping[name][value].push(remainder); } } } @@ -388,6 +393,7 @@ var modelUploader = (function () { var name, value, remainder, + i, string = ""; for (name in mapping) { @@ -397,11 +403,12 @@ var modelUploader = (function () { if (mapping[name].hasOwnProperty(value)) { remainder = mapping[name][value]; if (remainder === null) { - remainder = ""; + string += (name + " = " + value + "\n"); } else { - remainder = " = " + remainder; + for (i = 0; i < remainder.length; i += 1) { + string += (name + " = " + value + " = " + remainder[i] + "\n"); + } } - string += (name + " = " + value + remainder + "\n"); } } } else { From 0b979d2e1ecd786b07733282afa5cca93e420156 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 11 Aug 2014 10:54:46 -0700 Subject: [PATCH 52/62] Disabled SVO model uploading: server doesn't support SVO file uploads --- examples/editModels.js | 43 +++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index 8b54b9e48a..a047562dcd 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -288,7 +288,7 @@ var modelUploader = (function () { modelFile, fstBuffer, fbxBuffer, - svoBuffer, + //svoBuffer, mapping, geometry, API_URL = "https://data.highfidelity.io/api/v1/models", @@ -319,7 +319,7 @@ var modelUploader = (function () { function resetDataObjects() { fstBuffer = null; fbxBuffer = null; - svoBuffer = null; + //svoBuffer = null; mapping = {}; geometry = {}; geometry.textures = []; @@ -521,7 +521,7 @@ var modelUploader = (function () { function readModel() { var fbxFilename, - svoFilename, + //svoFilename, fileType; print("Reading model file: " + modelFile); @@ -536,8 +536,8 @@ var modelUploader = (function () { 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 if (fileType === "svo") { + // svoFilename = modelFile.path() + "\\" + mapping[FILENAME_FIELD]; } else { error("Unrecognized model type in FST file!"); return false; @@ -556,9 +556,9 @@ var modelUploader = (function () { fbxFilename = modelFile; mapping[FILENAME_FIELD] = modelFile.fileName(); - } else if (modelFile.toLowerCase().fileType() === "svo") { - svoFilename = 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); @@ -575,12 +575,12 @@ var modelUploader = (function () { readGeometry(fbxBuffer); } - if (svoFilename) { - svoBuffer = readFile(svoFilename); - if (svoBuffer === null) { - return false; - } - } + //if (svoFilename) { + // svoBuffer = readFile(svoFilename); + // if (svoBuffer === null) { + // return false; + // } + //} // Add any missing basic mappings if (!mapping.hasOwnProperty(NAME_FIELD)) { @@ -680,12 +680,12 @@ var modelUploader = (function () { } // SVO file - if (svoBuffer) { - httpMultiPart.add({ - name : "svo", - buffer: svoBuffer - }); - } + //if (svoBuffer) { + // httpMultiPart.add({ + // name : "svo", + // buffer: svoBuffer + // }); + //} // LOD files lodCount = 0; @@ -1044,7 +1044,8 @@ var toolBar = (function () { toggleToolbar(false); file = Window.browse("Select your model file ...", Settings.getValue("LastModelUploadLocation").path(), - "Model files (*.fst *.fbx *.svo)"); + "Model files (*.fst *.fbx)"); + //"Model files (*.fst *.fbx *.svo)"); if (file !== null) { Settings.setValue("LastModelUploadLocation", file); modelUploader.upload(file, addModel); From 53602c2ef2dc1bb4216b6dec40c57bfd709123e1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 11 Aug 2014 10:55:31 -0700 Subject: [PATCH 53/62] Miscellaneous tidying --- examples/editModels.js | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index a047562dcd..3d93fca0bf 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -85,8 +85,8 @@ if (typeof String.prototype.path !== "function") { if (typeof String.prototype.regExpEscape !== "function") { String.prototype.regExpEscape = function () { - return this.replace(/([$^.+*?|\\\/{}()\[\]])/g, '\\$1'); - } + return this.replace(/([$\^.+*?|\\\/{}()\[\]])/g, '\\$1'); + }; } if (typeof String.prototype.toArrayBuffer !== "function") { @@ -333,7 +333,7 @@ var modelUploader = (function () { req.responseType = "arraybuffer"; req.send(); if (req.status !== 200) { - error("Could not read file: " + filename + " : " + req.status + " " + req.statusText); + error("Could not read file: " + filename + " : " + req.statusText); return null; } @@ -439,8 +439,7 @@ var modelUploader = (function () { propertyListLength, nameLength, name, - filename, - author; + filename; endOffset = view.getUint32(index, true); numProperties = view.getUint32(index + 4, true); @@ -480,7 +479,6 @@ var modelUploader = (function () { viewLength, charCode, charCodes, - author, filename; view = new Uint8Array(fbxBuffer.buffer); @@ -598,7 +596,6 @@ var modelUploader = (function () { function setProperties() { var form = [], - decimals = 3, directory, displayAs, validateAs; @@ -749,16 +746,16 @@ var modelUploader = (function () { 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 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() { print("Checking uploaded model"); From 8c913fe8c1a899677d947c3385d2fb6bae5f1364 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 13 Aug 2014 10:23:59 -0700 Subject: [PATCH 54/62] Add progress dialog and ability to cancel during the model upload --- examples/editModels.js | 262 +++++++++++++++++++++++++++++++++-------- 1 file changed, 216 insertions(+), 46 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index b606567ca6..dbae4fd08c 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -175,6 +175,140 @@ if (typeof DataView.prototype.string !== "function") { }; } +var progressDialog = (function () { + var that = {}, + progressBackground, + progressMessage, + cancelButton, + displayed = false, + backgroundWidth = 300, + backgroundHeight = 100, + messageHeight = 32, + cancelWidth = 70, + cancelHeight = 32, + textColor = { red: 255, green: 255, blue: 255 }, + textBackground = { red: 52, green: 52, blue: 52 }, + backgroundUrl = "http://ctrlaltstudio.com/hifi/progress-background.svg", // DJRTODO: Update with HiFi location. + //backgroundUrl = "http://public.highfidelity.io/images/tools/progress-background.svg", + windowDimensions; + + progressBackground = Overlays.addOverlay("image", { + width: backgroundWidth, + height: backgroundHeight, + imageURL: backgroundUrl, + alpha: 0.9, + visible: false + }); + + progressMessage = Overlays.addOverlay("text", { + width: backgroundWidth - 40, + height: messageHeight, + text: "", + textColor: textColor, + backgroundColor: textBackground, + alpha: 0.9, + visible: false + }); + + cancelButton = Overlays.addOverlay("text", { + width: cancelWidth, + height: cancelHeight, + text: "Cancel", + textColor: textColor, + backgroundColor: textBackground, + alpha: 0.9, + visible: false + }); + + function move() { + var progressX, + progressY; + + if (displayed) { + + if (windowDimensions.x === Window.innerWidth && windowDimensions.y === Window.innerHeight) { + return; + } + windowDimensions.x = Window.innerWidth; + windowDimensions.y = Window.innerHeight; + + progressX = (windowDimensions.x - backgroundWidth) / 2; // Center. + progressY = windowDimensions.y / 2 - backgroundHeight; // A little up from center. + + Overlays.editOverlay(progressBackground, { x: progressX, y: progressY }); + Overlays.editOverlay(progressMessage, { x: progressX + 20, y: progressY + 15 }); + Overlays.editOverlay(cancelButton, { + x: progressX + backgroundWidth - cancelWidth - 20, + y: progressY + backgroundHeight - cancelHeight - 15 + }); + } + } + that.move = move; + + that.onCancel = undefined; + + function open(message) { + if (!displayed) { + windowDimensions = { x: 0, y : 0 }; + displayed = true; + move(); + Overlays.editOverlay(progressBackground, { visible: true }); + Overlays.editOverlay(progressMessage, { visible: true, text: message }); + Overlays.editOverlay(cancelButton, { visible: true }); + } else { + throw new Error("open() called on progressDialog when already open"); + } + } + that.open = open; + + function isOpen() { + return displayed; + } + that.isOpen = isOpen; + + function update(message) { + if (displayed) { + Overlays.editOverlay(progressMessage, { text: message }); + } else { + throw new Error("update() called on progressDialog when not open"); + } + } + that.update = update; + + function close() { + if (displayed) { + Overlays.editOverlay(cancelButton, { visible: false }); + Overlays.editOverlay(progressMessage, { visible: false }); + Overlays.editOverlay(progressBackground, { visible: false }); + displayed = false; + } else { + throw new Error("close() called on progressDialog when not open"); + } + } + that.close = close; + + function mousePressEvent(event) { + if (Overlays.getOverlayAtPoint({ x: event.x, y: event.y }) === cancelButton) { + if (typeof this.onCancel === "function") { + close(); + this.onCancel(); + } + return true; + } + return false; + } + that.mousePressEvent = mousePressEvent; + + function cleanup() { + Overlays.deleteOverlay(cancelButton); + Overlays.deleteOverlay(progressMessage); + Overlays.deleteOverlay(progressBackground); + } + that.cleanup = cleanup; + + return that; +}()); + var httpMultiPart = (function () { var that = {}, parts, @@ -287,6 +421,10 @@ var httpMultiPart = (function () { var modelUploader = (function () { var that = {}, modelFile, + modelName, + modelURL, + modelCallback, + isProcessing, fstBuffer, fbxBuffer, //svoBuffer, @@ -300,7 +438,19 @@ var modelUploader = (function () { 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); } @@ -523,7 +673,8 @@ var modelUploader = (function () { //svoFilename, fileType; - print("Reading model file: " + modelFile); + info("Reading model file"); + print("Model file: " + modelFile); if (modelFile.toLowerCase().fileType() === "fst") { fstBuffer = readFile(modelFile); @@ -565,12 +716,16 @@ var modelUploader = (function () { } } + if (!isProcessing) { return false; } + if (fbxFilename) { fbxBuffer = readFile(fbxFilename); if (fbxBuffer === null) { return false; } + if (!isProcessing) { return false; } + readGeometry(fbxBuffer); } @@ -601,6 +756,7 @@ var modelUploader = (function () { displayAs, validateAs; + progressDialog.close(); print("Setting model properties"); form.push({ label: "Name:", value: mapping[NAME_FIELD] }); @@ -636,8 +792,9 @@ var modelUploader = (function () { return true; } - function createHttpMessage() { - var lodCount, + function createHttpMessage(callback) { + var multiparts = [], + lodCount, lodFile, lodBuffer, textureBuffer, @@ -645,25 +802,23 @@ var modelUploader = (function () { textureTargetFormat, i; - print("Preparing to send model"); - - httpMultiPart.clear(); + info("Preparing to send model"); // Model name if (mapping.hasOwnProperty(NAME_FIELD)) { - httpMultiPart.add({ + multiparts.push({ name : "model_name", string : mapping[NAME_FIELD] }); } else { error("Model name is missing"); httpMultiPart.clear(); - return false; + return; } // FST file if (fstBuffer) { - httpMultiPart.add({ + multiparts.push({ name : "fst", buffer: fstBuffer }); @@ -671,7 +826,7 @@ var modelUploader = (function () { // FBX file if (fbxBuffer) { - httpMultiPart.add({ + multiparts.push({ name : "fbx", buffer: fbxBuffer }); @@ -679,7 +834,7 @@ var modelUploader = (function () { // SVO file //if (svoBuffer) { - // httpMultiPart.add({ + // multiparts.push({ // name : "svo", // buffer: svoBuffer // }); @@ -691,14 +846,15 @@ var modelUploader = (function () { if (mapping.lod.hasOwnProperty(lodFile)) { lodBuffer = readFile(modelFile.path() + "\/" + lodFile); if (lodBuffer === null) { - return false; + return; } - httpMultiPart.add({ + multiparts.push({ name: "lod" + lodCount, buffer: lodBuffer }); lodCount += 1; } + if (!isProcessing) { return; } } // Textures @@ -707,7 +863,7 @@ var modelUploader = (function () { + (mapping[TEXDIR_FIELD] !== "." ? mapping[TEXDIR_FIELD] + "\/" : "") + geometry.textures[i]); if (textureBuffer === null) { - return false; + return; } textureSourceFormat = geometry.textures[i].fileType().toLowerCase(); @@ -715,25 +871,37 @@ var modelUploader = (function () { textureBuffer.buffer = textureBuffer.buffer.recodeImage(textureSourceFormat, textureTargetFormat, MAX_TEXTURE_SIZE); textureBuffer.filename = textureBuffer.filename.slice(0, -textureSourceFormat.length) + textureTargetFormat; - httpMultiPart.add({ + multiparts.push({ name: "texture" + i, buffer: textureBuffer }); + if (!isProcessing) { return; } } // Model category - httpMultiPart.add({ + multiparts.push({ name : "model_category", string : "content" }); - return true; + // 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(addModelCallback) { + function sendToHighFidelity() { var req, - modelName, - modelURL, uploadedChecks, HTTP_GET_TIMEOUT = 60, // 1 minute HTTP_SEND_TIMEOUT = 900, // 15 minutes @@ -759,7 +927,9 @@ var modelUploader = (function () { //} function checkUploaded() { - print("Checking uploaded model"); + if (!isProcessing) { return; } + + info("Checking uploaded model"); req = new XMLHttpRequest(); req.open("HEAD", modelURL, true); @@ -775,8 +945,9 @@ var modelUploader = (function () { 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?")) { - addModelCallback(modelURL); + modelCallback(modelURL); } } else if (req.status === 404) { if (uploadedChecks > 0) { @@ -797,6 +968,8 @@ var modelUploader = (function () { function uploadModel(method) { var url; + if (!isProcessing) { return; } + req = new XMLHttpRequest(); if (method === "PUT") { url = API_URL + "\/" + modelName; @@ -828,6 +1001,8 @@ var modelUploader = (function () { 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); @@ -864,41 +1039,34 @@ var modelUploader = (function () { } }; - print("Sending model to High Fidelity"); - - modelName = mapping[NAME_FIELD]; - modelURL = MODEL_URL + "\/" + mapping[NAME_FIELD] + ".fst"; // All models are uploaded as an FST + info("Sending model to High Fidelity"); requestUpload(); } - that.upload = function (file, addModelCallback) { + that.upload = function (file, callback) { modelFile = file; + modelCallback = callback; + + isProcessing = true; + + progressDialog.onCancel = function () { + print("User cancelled uploading model"); + isProcessing = false; + }; resetDataObjects(); - // Read model content ... - if (!readModel()) { - resetDataObjects(); - return; - } + if (readModel()) { + if (setProperties()) { + modelName = mapping[NAME_FIELD]; + modelURL = MODEL_URL + "\/" + mapping[NAME_FIELD] + ".fst"; // All models are uploaded as an FST - // Set model properties ... - if (!setProperties()) { - resetDataObjects(); - return; + createHttpMessage(sendToHighFidelity); + } } - // Put model in HTTP message ... - if (!createHttpMessage()) { - resetDataObjects(); - return; - } - - // Send model to High Fidelity ... - sendToHighFidelity(addModelCallback); - resetDataObjects(); }; @@ -2107,6 +2275,7 @@ function checkController(deltaTime) { } toolBar.move(); + progressDialog.move(); } var modelSelected = false; @@ -2190,7 +2359,7 @@ function mousePressEvent(event) { modelSelected = false; var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); - if (toolBar.mousePressEvent(event)) { + if (toolBar.mousePressEvent(event) || progressDialog.mousePressEvent(event)) { // Event handled; do nothing. return; } else { @@ -2484,6 +2653,7 @@ function cleanupModelMenus() { function scriptEnding() { leftController.cleanup(); rightController.cleanup(); + progressDialog.cleanup(); toolBar.cleanup(); cleanupModelMenus(); tooltip.cleanup(); From 29f4f5b21aaa14e8de2ef1c4162e89878ae1cd2b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Aug 2014 11:18:10 -0700 Subject: [PATCH 55/62] Support FBX files with embedded textures --- examples/editModels.js | 85 +++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index dbae4fd08c..b6552f67f2 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -474,6 +474,7 @@ var modelUploader = (function () { mapping = {}; geometry = {}; geometry.textures = []; + geometry.embedded = []; } function readFile(filename) { @@ -575,7 +576,8 @@ var modelUploader = (function () { var textures, view, index, - EOF; + EOF, + previousNodeFilename; // Reference: // http://code.blender.org/index.php/2013/08/fbx-binary-file-format-specification/ @@ -609,12 +611,19 @@ var modelUploader = (function () { name = view.string(index, nameLength).toLowerCase(); index += nameLength; + if (name === "content" && previousNodeFilename !== "") { + 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); @@ -630,31 +639,49 @@ var modelUploader = (function () { viewLength, charCode, charCodes, - filename; + 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 === 10) { // Can ignore EOF - line = String.fromCharCode.apply(String, charCodes).trim(); - if (line.slice(0, 17).toLowerCase() === "relativefilename:") { - filename = line.slice(line.indexOf("\""), line.lastIndexOf("\"") - line.length).fileName(); - if (!textures.hasOwnProperty(filename)) { - textures[filename] = ""; - geometry.textures.push(filename); + 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; } } - charCodes = []; - } else { - charCodes.push(charCode); } } } if (view.string(0, 18) === "Kaydara FBX Binary") { + previousNodeFilename = ""; index = 27; while (index < view.byteLength - 39 && !EOF) { @@ -800,6 +827,7 @@ var modelUploader = (function () { textureBuffer, textureSourceFormat, textureTargetFormat, + embeddedTextures, i; info("Preparing to send model"); @@ -858,23 +886,28 @@ var modelUploader = (function () { } // Textures + embeddedTextures = "|" + geometry.embedded.join("|") + "|"; for (i = 0; i < geometry.textures.length; i += 1) { - textureBuffer = readFile(modelFile.path() + "\/" - + (mapping[TEXDIR_FIELD] !== "." ? mapping[TEXDIR_FIELD] + "\/" : "") - + geometry.textures[i]); - if (textureBuffer === null) { - return; + 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 + }); } - 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; } } From f7addad5acdcc03a330ae7facc78c2e8f39b2e7b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Aug 2014 17:20:00 -0700 Subject: [PATCH 56/62] Update progress background image to use copy on S3 --- examples/editModels.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/editModels.js b/examples/editModels.js index b6552f67f2..b15ea58fc6 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -188,8 +188,7 @@ var progressDialog = (function () { cancelHeight = 32, textColor = { red: 255, green: 255, blue: 255 }, textBackground = { red: 52, green: 52, blue: 52 }, - backgroundUrl = "http://ctrlaltstudio.com/hifi/progress-background.svg", // DJRTODO: Update with HiFi location. - //backgroundUrl = "http://public.highfidelity.io/images/tools/progress-background.svg", + backgroundUrl = toolIconUrl + "progress-background.svg", windowDimensions; progressBackground = Overlays.addOverlay("image", { From 54851c5ced02f4125d449c18789e16c78bbacad5 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 18 Aug 2014 12:49:12 -0700 Subject: [PATCH 57/62] add Ragdoll::_accumulatedMovement --- libraries/shared/src/Ragdoll.cpp | 27 ++++++++++++++++++++++++++- libraries/shared/src/Ragdoll.h | 10 ++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/libraries/shared/src/Ragdoll.cpp b/libraries/shared/src/Ragdoll.cpp index 7eeaf0b609..70ea63930b 100644 --- a/libraries/shared/src/Ragdoll.cpp +++ b/libraries/shared/src/Ragdoll.cpp @@ -19,7 +19,8 @@ #include "PhysicsSimulation.h" #include "SharedUtil.h" // for EPSILON -Ragdoll::Ragdoll() : _massScale(1.0f), _translation(0.0f), _translationInSimulationFrame(0.0f), _simulation(NULL) { +Ragdoll::Ragdoll() : _massScale(1.0f), _translation(0.0f), _translationInSimulationFrame(0.0f), + _accumulatedMovement(0.0f), _simulation(NULL) { } Ragdoll::~Ragdoll() { @@ -116,3 +117,27 @@ void Ragdoll::setMassScale(float scale) { _massScale = scale; } } + +void Ragdoll::removeRootOffset(bool accumulateMovement) { + const int numPoints = _points.size(); + if (numPoints > 0) { + // shift all points so that the root aligns with the the ragdoll's position in the simulation + glm::vec3 offset = _translationInSimulationFrame - _points[0]._position; + float offsetLength = glm::length(offset); + if (offsetLength > EPSILON) { + for (int i = 0; i < numPoints; ++i) { + _points[i].shift(offset); + } + const float MIN_ROOT_OFFSET = 0.02f; + if (accumulateMovement && offsetLength > MIN_ROOT_OFFSET) { + _accumulatedMovement -= (1.0f - MIN_ROOT_OFFSET / offsetLength) * offset; + } + } + } +} + +glm::vec3 Ragdoll::getAndClearAccumulatedMovement() { + glm::vec3 movement = _accumulatedMovement; + _accumulatedMovement = glm::vec3(0.0f); + return movement; +} diff --git a/libraries/shared/src/Ragdoll.h b/libraries/shared/src/Ragdoll.h index c82295d9a5..1ffbdb29ab 100644 --- a/libraries/shared/src/Ragdoll.h +++ b/libraries/shared/src/Ragdoll.h @@ -56,6 +56,10 @@ public: virtual void initPoints() = 0; virtual void buildConstraints() = 0; + void removeRootOffset(bool accumulateMovement); + + glm::vec3 getAndClearAccumulatedMovement(); + protected: float _massScale; glm::vec3 _translation; // world-frame @@ -66,6 +70,12 @@ protected: QVector _points; QVector _boneConstraints; QVector _fixedConstraints; + + // The collisions are typically done in a simulation frame that is slaved to the center of one of the Ragdolls. + // To allow the Ragdoll to provide feedback of its own displacement we store it in _accumulatedMovement. + // The owner of the Ragdoll can harvest this displacement to update the rest of the object positions in the simulation. + glm::vec3 _accumulatedMovement; + private: void updateSimulationTransforms(const glm::vec3& translation, const glm::quat& rotation); From fe5f9f8fe5a283fcf208f368026e47216baf0eb9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 18 Aug 2014 12:49:47 -0700 Subject: [PATCH 58/62] use relative mass when enforcing ContactPoint --- libraries/shared/src/ContactPoint.cpp | 31 +++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/libraries/shared/src/ContactPoint.cpp b/libraries/shared/src/ContactPoint.cpp index 27a496d445..02cf896594 100644 --- a/libraries/shared/src/ContactPoint.cpp +++ b/libraries/shared/src/ContactPoint.cpp @@ -96,10 +96,10 @@ float ContactPoint::enforce() { bool constraintViolation = (pDotN > CONTACT_PENETRATION_ALLOWANCE); // the contact point will be the average of the two points on the shapes - _contactPoint = 0.5f * (pointA + pointB); + _contactPoint = _relativeMassA * pointA + _relativeMassB * pointB; if (constraintViolation) { - for (int i = 0; i < _numPoints; ++i) { + for (int i = 0; i < _numPointsA; ++i) { VerletPoint* point = _points[i]; glm::vec3 offset = _offsets[i]; @@ -111,8 +111,31 @@ float ContactPoint::enforce() { // use the relative sizes of the components to decide how much perpenducular delta to use // perpendicular < parallel ==> static friction ==> perpFactor = 1.0 // perpendicular > parallel ==> dynamic friction ==> cap to length of paraDelta ==> perpFactor < 1.0 - float paraLength = glm::length(paraDelta); - float perpLength = glm::length(perpDelta); + float paraLength = _relativeMassB * glm::length(paraDelta); + float perpLength = _relativeMassA * glm::length(perpDelta); + float perpFactor = (perpLength > paraLength && perpLength > EPSILON) ? (paraLength / perpLength) : 1.0f; + + // recombine the two components to get the final delta + delta = paraDelta + perpFactor * perpDelta; + + glm::vec3 targetPosition = point->_position + delta; + _distances[i] = glm::distance(_contactPoint, targetPosition); + point->_position += delta; + } + for (int i = _numPointsA; i < _numPoints; ++i) { + VerletPoint* point = _points[i]; + glm::vec3 offset = _offsets[i]; + + // split delta into parallel and perpendicular components + glm::vec3 delta = _contactPoint + offset - point->_position; + glm::vec3 paraDelta = glm::dot(delta, _normal) * _normal; + glm::vec3 perpDelta = delta - paraDelta; + + // use the relative sizes of the components to decide how much perpenducular delta to use + // perpendicular < parallel ==> static friction ==> perpFactor = 1.0 + // perpendicular > parallel ==> dynamic friction ==> cap to length of paraDelta ==> perpFactor < 1.0 + float paraLength = _relativeMassA * glm::length(paraDelta); + float perpLength = _relativeMassB * glm::length(perpDelta); float perpFactor = (perpLength > paraLength && perpLength > EPSILON) ? (paraLength / perpLength) : 1.0f; // recombine the two components to get the final delta From 3e2095332fdf1338fca49a0395e30ddbe6adb4ce Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 18 Aug 2014 12:50:07 -0700 Subject: [PATCH 59/62] make SkeletonRagdoll::updateMuscles() protected --- interface/src/avatar/SkeletonRagdoll.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/avatar/SkeletonRagdoll.h b/interface/src/avatar/SkeletonRagdoll.h index f9f99395ac..ae9bec9116 100644 --- a/interface/src/avatar/SkeletonRagdoll.h +++ b/interface/src/avatar/SkeletonRagdoll.h @@ -33,7 +33,9 @@ public: virtual void initPoints(); virtual void buildConstraints(); +protected: void updateMuscles(); + private: Model* _model; QVector _muscleConstraints; From 7e7978de1a83051160aee4b45606db5df418b523 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 18 Aug 2014 12:53:04 -0700 Subject: [PATCH 60/62] compute and store Ragdoll::_accumulatedMovement --- interface/src/avatar/SkeletonModel.cpp | 23 ++++++++++++---------- interface/src/avatar/SkeletonRagdoll.cpp | 5 +---- libraries/shared/src/PhysicsSimulation.cpp | 21 +++++++++++++++----- libraries/shared/src/PhysicsSimulation.h | 6 ++++-- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index ffe711b03b..536f957143 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -14,14 +14,11 @@ #include #include -#include -#include #include "Application.h" #include "Avatar.h" #include "Hand.h" #include "Menu.h" -#include "MuscleConstraint.h" #include "SkeletonModel.h" #include "SkeletonRagdoll.h" @@ -606,6 +603,7 @@ void SkeletonModel::buildShapes() { float uniformScale = extractUniformScale(_scale); const int numStates = _jointStates.size(); + float totalMass = 0.0f; for (int i = 0; i < numStates; i++) { JointState& state = _jointStates[i]; const FBXJoint& joint = state.getFBXJoint(); @@ -624,26 +622,31 @@ void SkeletonModel::buildShapes() { if (type == Shape::SPHERE_SHAPE) { shape = new VerletSphereShape(radius, &(points[i])); shape->setEntity(this); - points[i].setMass(massScale * glm::max(MIN_JOINT_MASS, DENSITY_OF_WATER * shape->getVolume())); + float mass = massScale * glm::max(MIN_JOINT_MASS, DENSITY_OF_WATER * shape->getVolume()); + points[i].setMass(mass); + totalMass += mass; } else if (type == Shape::CAPSULE_SHAPE) { assert(parentIndex != -1); shape = new VerletCapsuleShape(radius, &(points[parentIndex]), &(points[i])); shape->setEntity(this); - points[i].setMass(massScale * glm::max(MIN_JOINT_MASS, DENSITY_OF_WATER * shape->getVolume())); + float mass = massScale * glm::max(MIN_JOINT_MASS, DENSITY_OF_WATER * shape->getVolume()); + points[i].setMass(mass); + totalMass += mass; } if (parentIndex != -1) { // always disable collisions between joint and its parent if (shape) { disableCollisions(i, parentIndex); } - } else { - // give the base joint a very large mass since it doesn't actually move - // in the local-frame simulation (it defines the origin) - points[i].setMass(VERY_BIG_MASS); - } + } _shapes.push_back(shape); } + // set the mass of the root + if (numStates > 0) { + points[0].setMass(totalMass); + } + // This method moves the shapes to their default positions in Model frame. computeBoundingShape(geometry); diff --git a/interface/src/avatar/SkeletonRagdoll.cpp b/interface/src/avatar/SkeletonRagdoll.cpp index 503f38f00f..6318323990 100644 --- a/interface/src/avatar/SkeletonRagdoll.cpp +++ b/interface/src/avatar/SkeletonRagdoll.cpp @@ -70,10 +70,7 @@ void SkeletonRagdoll::buildConstraints() { for (int i = 0; i < numPoints; ++i) { const JointState& state = jointStates.at(i); int parentIndex = state.getParentIndex(); - if (parentIndex == -1) { - FixedConstraint* anchor = new FixedConstraint(&_translationInSimulationFrame, &(_points[i])); - _fixedConstraints.push_back(anchor); - } else { + if (parentIndex != -1) { DistanceConstraint* bone = new DistanceConstraint(&(_points[i]), &(_points[parentIndex])); bone->setDistance(state.getDistanceToParent()); _boneConstraints.push_back(bone); diff --git a/libraries/shared/src/PhysicsSimulation.cpp b/libraries/shared/src/PhysicsSimulation.cpp index a62b3816af..6c4901bcd5 100644 --- a/libraries/shared/src/PhysicsSimulation.cpp +++ b/libraries/shared/src/PhysicsSimulation.cpp @@ -163,10 +163,10 @@ bool PhysicsSimulation::addRagdoll(Ragdoll* doll) { } void PhysicsSimulation::removeRagdoll(Ragdoll* doll) { - int numDolls = _otherRagdolls.size(); - if (doll->_simulation != this) { + if (!doll || doll->_simulation != this) { return; } + int numDolls = _otherRagdolls.size(); for (int i = 0; i < numDolls; ++i) { if (doll == _otherRagdolls[i]) { if (i == numDolls - 1) { @@ -205,10 +205,11 @@ void PhysicsSimulation::stepForward(float deltaTime, float minError, int maxIter } } + bool collidedWithOtherRagdoll = false; int iterations = 0; float error = 0.0f; do { - computeCollisions(); + collidedWithOtherRagdoll = computeCollisions() || collidedWithOtherRagdoll; updateContacts(); resolveCollisions(); @@ -225,6 +226,14 @@ void PhysicsSimulation::stepForward(float deltaTime, float minError, int maxIter now = usecTimestampNow(); } while (_collisions.size() != 0 && (iterations < maxIterations) && (error > minError) && (now < expiry)); + // the collisions may have moved the main ragdoll from the simulation center + // so we remove this offset (potentially storing it as movement of the Ragdoll owner) + _ragdoll->removeRootOffset(collidedWithOtherRagdoll); + + // also remove any offsets from the other ragdolls + for (int i = 0; i < numDolls; ++i) { + _otherRagdolls[i]->removeRootOffset(false); + } pruneContacts(); } @@ -237,7 +246,7 @@ void PhysicsSimulation::moveRagdolls(float deltaTime) { } } -void PhysicsSimulation::computeCollisions() { +bool PhysicsSimulation::computeCollisions() { PerformanceTimer perfTimer("collide"); _collisions.clear(); @@ -258,11 +267,13 @@ void PhysicsSimulation::computeCollisions() { } // collide main ragdoll with others + bool otherCollisions = false; int numEntities = _otherEntities.size(); for (int i = 0; i < numEntities; ++i) { const QVector otherShapes = _otherEntities.at(i)->getShapes(); - ShapeCollider::collideShapesWithShapes(shapes, otherShapes, _collisions); + otherCollisions = ShapeCollider::collideShapesWithShapes(shapes, otherShapes, _collisions) || otherCollisions; } + return otherCollisions; } void PhysicsSimulation::resolveCollisions() { diff --git a/libraries/shared/src/PhysicsSimulation.h b/libraries/shared/src/PhysicsSimulation.h index 881007208b..1db56a46e2 100644 --- a/libraries/shared/src/PhysicsSimulation.h +++ b/libraries/shared/src/PhysicsSimulation.h @@ -53,9 +53,11 @@ public: protected: void moveRagdolls(float deltaTime); - void computeCollisions(); - void resolveCollisions(); + /// \return true if main ragdoll collides with other avatar + bool computeCollisions(); + + void resolveCollisions(); void enforceContacts(); void applyContactFriction(); void updateContacts(); From aa1a7307cc1219a4e965c7490af225bc5890c408 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 18 Aug 2014 12:53:34 -0700 Subject: [PATCH 61/62] use Ragdoll::_accumulatedMovement to move MyAvatar --- interface/src/avatar/MyAvatar.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index ac44e1884e..50664d33c9 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -206,12 +206,21 @@ void MyAvatar::simulate(float deltaTime) { { PerformanceTimer perfTimer("ragdoll"); - if (Menu::getInstance()->isOptionChecked(MenuOption::CollideAsRagdoll)) { + Ragdoll* ragdoll = _skeletonModel.getRagdoll(); + if (ragdoll && Menu::getInstance()->isOptionChecked(MenuOption::CollideAsRagdoll)) { const float minError = 0.00001f; const float maxIterations = 3; const quint64 maxUsec = 4000; _physicsSimulation.setTranslation(_position); _physicsSimulation.stepForward(deltaTime, minError, maxIterations, maxUsec); + + // harvest any displacement of the Ragdoll that is a result of collisions + glm::vec3 ragdollDisplacement = ragdoll->getAndClearAccumulatedMovement(); + const float MAX_RAGDOLL_DISPLACEMENT_2 = 1.0f; + float length2 = glm::length2(ragdollDisplacement); + if (length2 > EPSILON && length2 < MAX_RAGDOLL_DISPLACEMENT_2) { + setPosition(getPosition() + ragdollDisplacement); + } } else { _skeletonModel.moveShapesTowardJoints(1.0f); } From 543bf5224c118c74a571f26b7123d7c138efdac4 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 18 Aug 2014 12:54:26 -0700 Subject: [PATCH 62/62] add VerletPoint::shift() --- libraries/shared/src/VerletPoint.cpp | 5 +++++ libraries/shared/src/VerletPoint.h | 1 + 2 files changed, 6 insertions(+) diff --git a/libraries/shared/src/VerletPoint.cpp b/libraries/shared/src/VerletPoint.cpp index d2dd985587..cf9aeca149 100644 --- a/libraries/shared/src/VerletPoint.cpp +++ b/libraries/shared/src/VerletPoint.cpp @@ -39,6 +39,11 @@ void VerletPoint::move(const glm::vec3& deltaPosition, const glm::quat& deltaRot _lastPosition += deltaPosition + (deltaRotation * arm - arm); } +void VerletPoint::shift(const glm::vec3& deltaPosition) { + _position += deltaPosition; + _lastPosition += deltaPosition; +} + void VerletPoint::setMass(float mass) { const float MIN_MASS = 1.0e-6f; const float MAX_MASS = 1.0e18f; diff --git a/libraries/shared/src/VerletPoint.h b/libraries/shared/src/VerletPoint.h index 6f94656966..3c73e5eb01 100644 --- a/libraries/shared/src/VerletPoint.h +++ b/libraries/shared/src/VerletPoint.h @@ -25,6 +25,7 @@ public: void accumulateDelta(const glm::vec3& delta); void applyAccumulatedDelta(); void move(const glm::vec3& deltaPosition, const glm::quat& deltaRotation, const glm::vec3& oldPivot); + void shift(const glm::vec3& deltaPosition); void setMass(float mass); float getMass() const { return _mass; }