diff --git a/BUILD.md b/BUILD.md index b8bc1cd14c..674f0d24cc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -63,7 +63,7 @@ If `libgnutls28-dev` 3.2.12 or higher is available via your package manager, it [Homebrew](http://brew.sh/) is an excellent package manager for OS X. It makes install of all hifi dependencies very simple. brew tap highfidelity/homebrew-formulas - brew install cmake glm zlib gnutls + brew install cmake glm gnutls brew install highfidelity/formulas/qt5 brew link qt5 --force brew install highfidelity/formulas/qxmpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a399e11168..b7fa55d4a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ if (WIN32) elseif (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) #SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -pedantic") #SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-unknown-pragmas") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fno-strict-aliasing") endif(WIN32) if (NOT QT_CMAKE_PREFIX_PATH) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index bbb367fdcb..5720ecaaf5 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -222,8 +222,6 @@ void Agent::run() { loop.exec(); - - // let the AvatarData and ResourceCache classes use our QNetworkAccessManager AvatarData::setNetworkAccessManager(networkManager); ResourceCache::setNetworkAccessManager(networkManager); diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 6746526d9f..663aef81a7 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -38,6 +38,9 @@ #include #include #include +#include +#include +#include #include #include @@ -479,40 +482,81 @@ void AudioMixer::run() { nodeList->linkedDataCreateCallback = attachNewBufferToNode; - // check the payload to see if we have any unattenuated zones - const QString UNATTENUATED_ZONE_REGEX_STRING = "--unattenuated-zone ([\\d.,-]+)"; - QRegExp unattenuatedZoneMatch(UNATTENUATED_ZONE_REGEX_STRING); + // setup a QNetworkAccessManager to ask the domain-server for our settings + QNetworkAccessManager *networkManager = new QNetworkAccessManager(this); - if (unattenuatedZoneMatch.indexIn(_payload) != -1) { - QString unattenuatedZoneString = unattenuatedZoneMatch.cap(1); - QStringList zoneStringList = unattenuatedZoneString.split(','); + QUrl settingsJSONURL; + settingsJSONURL.setScheme("http"); + settingsJSONURL.setHost(nodeList->getDomainHandler().getHostname()); + settingsJSONURL.setPort(DOMAIN_SERVER_HTTP_PORT); + settingsJSONURL.setPath("/settings.json"); + settingsJSONURL.setQuery(QString("type=%1").arg(_type)); + + QNetworkReply *reply = NULL; + + int failedAttempts = 0; + const int MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS = 5; + + qDebug() << "Requesting settings for assignment from domain-server at" << settingsJSONURL.toString(); + + while (!reply || reply->error() != QNetworkReply::NoError) { + reply = networkManager->get(QNetworkRequest(settingsJSONURL)); - glm::vec3 sourceCorner(zoneStringList[0].toFloat(), zoneStringList[1].toFloat(), zoneStringList[2].toFloat()); - glm::vec3 sourceDimensions(zoneStringList[3].toFloat(), zoneStringList[4].toFloat(), zoneStringList[5].toFloat()); - - glm::vec3 listenerCorner(zoneStringList[6].toFloat(), zoneStringList[7].toFloat(), zoneStringList[8].toFloat()); - glm::vec3 listenerDimensions(zoneStringList[9].toFloat(), zoneStringList[10].toFloat(), zoneStringList[11].toFloat()); + QEventLoop loop; + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - _sourceUnattenuatedZone = new AABox(sourceCorner, sourceDimensions); - _listenerUnattenuatedZone = new AABox(listenerCorner, listenerDimensions); + loop.exec(); - glm::vec3 sourceCenter = _sourceUnattenuatedZone->calcCenter(); - glm::vec3 destinationCenter = _listenerUnattenuatedZone->calcCenter(); + ++failedAttempts; - qDebug() << "There is an unattenuated zone with source center at" - << QString("%1, %2, %3").arg(sourceCenter.x).arg(sourceCenter.y).arg(sourceCenter.z); - qDebug() << "Buffers inside this zone will not be attenuated inside a box with center at" - << QString("%1, %2, %3").arg(destinationCenter.x).arg(destinationCenter.y).arg(destinationCenter.z); + if (failedAttempts == MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS) { + qDebug() << "Failed to get settings from domain-server. Bailing on assignment."; + setFinished(true); + return; + } } - - // check the payload to see if we have asked for dynamicJitterBuffer support - const QString DYNAMIC_JITTER_BUFFER_REGEX_STRING = "--dynamicJitterBuffer"; - QRegExp dynamicJitterBufferMatch(DYNAMIC_JITTER_BUFFER_REGEX_STRING); - if (dynamicJitterBufferMatch.indexIn(_payload) != -1) { - qDebug() << "Enable dynamic jitter buffers."; - _useDynamicJitterBuffers = true; - } else { - qDebug() << "Dynamic jitter buffers disabled, using old behavior."; + + QJsonObject settingsObject = QJsonDocument::fromJson(reply->readAll()).object(); + + // check the settings object to see if we have anything we can parse out + const QString AUDIO_GROUP_KEY = "audio"; + + if (settingsObject.contains(AUDIO_GROUP_KEY)) { + QJsonObject audioGroupObject = settingsObject[AUDIO_GROUP_KEY].toObject(); + + const QString UNATTENUATED_ZONE_KEY = "unattenuated-zone"; + + QString unattenuatedZoneString = audioGroupObject[UNATTENUATED_ZONE_KEY].toString(); + if (!unattenuatedZoneString.isEmpty()) { + QStringList zoneStringList = unattenuatedZoneString.split(','); + + glm::vec3 sourceCorner(zoneStringList[0].toFloat(), zoneStringList[1].toFloat(), zoneStringList[2].toFloat()); + glm::vec3 sourceDimensions(zoneStringList[3].toFloat(), zoneStringList[4].toFloat(), zoneStringList[5].toFloat()); + + glm::vec3 listenerCorner(zoneStringList[6].toFloat(), zoneStringList[7].toFloat(), zoneStringList[8].toFloat()); + glm::vec3 listenerDimensions(zoneStringList[9].toFloat(), zoneStringList[10].toFloat(), zoneStringList[11].toFloat()); + + _sourceUnattenuatedZone = new AABox(sourceCorner, sourceDimensions); + _listenerUnattenuatedZone = new AABox(listenerCorner, listenerDimensions); + + glm::vec3 sourceCenter = _sourceUnattenuatedZone->calcCenter(); + glm::vec3 destinationCenter = _listenerUnattenuatedZone->calcCenter(); + + qDebug() << "There is an unattenuated zone with source center at" + << QString("%1, %2, %3").arg(sourceCenter.x).arg(sourceCenter.y).arg(sourceCenter.z); + qDebug() << "Buffers inside this zone will not be attenuated inside a box with center at" + << QString("%1, %2, %3").arg(destinationCenter.x).arg(destinationCenter.y).arg(destinationCenter.z); + } + + // check the payload to see if we have asked for dynamicJitterBuffer support + const QString DYNAMIC_JITTER_BUFFER_JSON_KEY = "dynamic-jitter-buffer"; + bool shouldUseDynamicJitterBuffers = audioGroupObject[DYNAMIC_JITTER_BUFFER_JSON_KEY].toBool(); + if (shouldUseDynamicJitterBuffers) { + qDebug() << "Enable dynamic jitter buffers."; + _useDynamicJitterBuffers = true; + } else { + qDebug() << "Dynamic jitter buffers disabled, using old behavior."; + } } int nextFrame = 0; diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index ff33cc206b..3b60ada78b 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -1,8 +1,8 @@ -#nodes-lead { +#nodes-lead, #settings-lead { color: #66CCCC; } -#nodes-lead .lead-line { +#nodes-lead .lead-line, #settings-lead .lead-line { background-color: #66CCCC; } diff --git a/domain-server/resources/web/index.shtml b/domain-server/resources/web/index.shtml index b6ba8f67db..f0315a113f 100644 --- a/domain-server/resources/web/index.shtml +++ b/domain-server/resources/web/index.shtml @@ -18,7 +18,20 @@ - +

Queued Assignments

@@ -31,8 +44,18 @@ + + \ No newline at end of file diff --git a/domain-server/resources/web/js/form2js.min.js b/domain-server/resources/web/js/form2js.min.js new file mode 100755 index 0000000000..f1e610f7c3 --- /dev/null +++ b/domain-server/resources/web/js/form2js.min.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010 Maxim Vasiliev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author Maxim Vasiliev + * Date: 09.09.2010 + * Time: 19:02:33 + */ +(function(e,t){if(typeof define==="function"&&define.amd){define(t)}else{e.form2js=t()}})(this,function(){"use strict";function e(e,r,i,s,o,u){u=u?true:false;if(typeof i=="undefined"||i==null)i=true;if(typeof r=="undefined"||r==null)r=".";if(arguments.length<5)o=false;e=typeof e=="string"?document.getElementById(e):e;var a=[],f,l=0;if(e.constructor==Array||typeof NodeList!="undefined"&&e.constructor==NodeList){while(f=e[l++]){a=a.concat(n(f,s,o,u))}}else{a=n(e,s,o,u)}return t(a,i,r)}function t(e,t,n){var r={},i={},s,o,u,a,f,l,c,h,p,d,v,m,g;for(s=0;s1){for(u=0;u-1&&o==l.length-1){p=v.substr(0,v.indexOf("["));h+=p;if(!c[p])c[p]=[];c[p].push(f)}else if(v.indexOf("[")>-1){p=v.substr(0,v.indexOf("["));d=v.replace(/(^([a-z_]+)?\[)|(\]$)/gi,"");h+="_"+p+"_"+d;if(!i[h])i[h]={};if(p!=""&&!c[p])c[p]=[];if(o==l.length-1){if(p==""){c.push(f);i[h][d]=c[c.length-1]}else{c[p].push(f);i[h][d]=c[p][c[p].length-1]}}else{if(!i[h][d]){if(/^[0-9a-z_]+\[?/i.test(l[o+1]))c[p].push({});else c[p].push([]);i[h][d]=c[p][c[p].length-1]}}c=i[h][d]}else{h+=v;if(o0?o:r(e,t,n,s)}function r(e,t,n,r){var s=[],o=e.firstChild;while(o){s=s.concat(i(o,t,n,r));o=o.nextSibling}return s}function i(e,t,n,i){if(e.disabled&&!i)return[];var u,a,f,l=s(e,n);u=t&&t(e);if(u&&u.name){f=[u]}else if(l!=""&&e.nodeName.match(/INPUT|TEXTAREA/i)){a=o(e,i);if(null===a){f=[]}else{f=[{name:l,value:a}]}}else if(l!=""&&e.nodeName.match(/SELECT/i)){a=o(e,i);f=[{name:l.replace(/\[\]$/,""),value:a}]}else{f=r(e,t,n,i)}return f}function s(e,t){if(e.name&&e.name!="")return e.name;else if(t&&e.id&&e.id!="")return e.id;else return""}function o(e,t){if(e.disabled&&!t)return null;switch(e.nodeName){case"INPUT":case"TEXTAREA":switch(e.type.toLowerCase()){case"radio":if(e.checked&&e.value==="false")return false;case"checkbox":if(e.checked&&e.value==="true")return true;if(!e.checked&&e.value==="true")return false;if(e.checked)return e.value;break;case"button":case"reset":case"submit":case"image":return"";break;default:return e.value;break}break;case"SELECT":return u(e);break;default:break}return null}function u(e){var t=e.multiple,n=[],r,i,s;if(!t)return e.value;for(r=e.getElementsByTagName("option"),i=0,s=r.length;i"; - nodesTableBody += "" + data.uuid + ""; - nodesTableBody += "" + (data.pool ? data.pool : "") + ""; - nodesTableBody += "" + data.public.ip + ":" + data.public.port + ""; - nodesTableBody += "" + data.local.ip + ":" + data.local.port + ""; - - var uptimeSeconds = (Date.now() - data.wake_timestamp) / 1000; - nodesTableBody += "" + uptimeSeconds.toLocaleString() + ""; - - nodesTableBody += "" + (typeof data.pending_credits == 'number' ? data.pending_credits.toLocaleString() : 'N/A') + ""; - - nodesTableBody += ""; - nodesTableBody += ""; - }); - - $('#nodes-table tbody').html(nodesTableBody); + $('#nodes-table tbody').html(nodeTemplate(json)); }); $.getJSON("assignments.json", function(json){ - queuedTableBody = ""; - - $.each(json.queued, function (uuid, data) { - queuedTableBody += ""; - queuedTableBody += "" + data.type + ""; - queuedTableBody += "" + uuid + ""; - queuedTableBody += "" + (data.pool ? data.pool : "") + ""; - queuedTableBody += ""; - }); - - $('#assignments-table tbody').html(queuedTableBody); + $('#assignments-table tbody').html(queuedTemplate(json)); }); } diff --git a/domain-server/resources/web/js/underscore-1.5.0.min.js b/domain-server/resources/web/js/underscore-1.5.0.min.js new file mode 100644 index 0000000000..4db9729997 --- /dev/null +++ b/domain-server/resources/web/js/underscore-1.5.0.min.js @@ -0,0 +1,7 @@ +// Underscore.js 1.5.0 +// http://underscorejs.org +// (c) 2009-2011 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. +!function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,v=e.reduce,h=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,w=i.bind,j=function(n){return n instanceof j?n:this instanceof j?(this._wrapped=n,void 0):new j(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=j),exports._=j):n._=j,j.VERSION="1.5.0";var A=j.each=j.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(j.has(n,a)&&t.call(e,n[a],a,n)===r)return};j.map=j.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e.push(t.call(r,n,u,i))}),e)};var E="Reduce of empty array with no initial value";j.reduce=j.foldl=j.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=j.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(E);return r},j.reduceRight=j.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=j.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=j.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(E);return r},j.find=j.detect=function(n,t,r){var e;return O(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},j.filter=j.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&e.push(n)}),e)},j.reject=function(n,t,r){return j.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},j.every=j.all=function(n,t,e){t||(t=j.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var O=j.some=j.any=function(n,t,e){t||(t=j.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};j.contains=j.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:O(n,function(n){return n===t})},j.invoke=function(n,t){var r=o.call(arguments,2),e=j.isFunction(t);return j.map(n,function(n){return(e?t:n[t]).apply(n,r)})},j.pluck=function(n,t){return j.map(n,function(n){return n[t]})},j.where=function(n,t,r){return j.isEmpty(t)?r?void 0:[]:j[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},j.findWhere=function(n,t){return j.where(n,t,!0)},j.max=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.max.apply(Math,n);if(!t&&j.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>e.computed&&(e={value:n,computed:a})}),e.value},j.min=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.min.apply(Math,n);if(!t&&j.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;ae||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.indexi;){var o=i+a>>>1;r.call(e,n[o])=0})})},j.difference=function(n){var t=c.apply(e,o.call(arguments,1));return j.filter(n,function(n){return!j.contains(t,n)})},j.zip=function(){return j.unzip.apply(j,o.call(arguments))},j.unzip=function(){for(var n=j.max(j.pluck(arguments,"length").concat(0)),t=new Array(n),r=0;n>r;r++)t[r]=j.pluck(arguments,""+r);return t},j.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},j.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=j.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},j.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},j.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=new Array(e);e>u;)i[u++]=n,n+=r;return i};var M=function(){};j.bind=function(n,t){var r,e;if(w&&n.bind===w)return w.apply(n,o.call(arguments,1));if(!j.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));M.prototype=n.prototype;var u=new M;M.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},j.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},j.bindAll=function(n){var t=o.call(arguments,1);if(0===t.length)throw new Error("bindAll must be passed function names");return A(t,function(t){n[t]=j.bind(n[t],n)}),n},j.memoize=function(n,t){var r={};return t||(t=j.identity),function(){var e=t.apply(this,arguments);return j.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},j.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},j.defer=function(n){return j.delay.apply(j,[n,1].concat(o.call(arguments,1)))},j.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var c=function(){o=new Date,a=null,i=n.apply(e,u)};return function(){var l=new Date;o||r.leading!==!1||(o=l);var f=t-(l-o);return e=this,u=arguments,0>=f?(clearTimeout(a),a=null,o=l,i=n.apply(e,u)):a||r.trailing===!1||(a=setTimeout(c,f)),i}},j.debounce=function(n,t,r){var e,u=null;return function(){var i=this,a=arguments,o=function(){u=null,r||(e=n.apply(i,a))},c=r&&!u;return clearTimeout(u),u=setTimeout(o,t),c&&(e=n.apply(i,a)),e}},j.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},j.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},j.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},j.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},j.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)j.has(n,r)&&t.push(r);return t},j.values=function(n){var t=[];for(var r in n)j.has(n,r)&&t.push(n[r]);return t},j.pairs=function(n){var t=[];for(var r in n)j.has(n,r)&&t.push([r,n[r]]);return t},j.invert=function(n){var t={};for(var r in n)j.has(n,r)&&(t[n[r]]=r);return t},j.functions=j.methods=function(n){var t=[];for(var r in n)j.isFunction(n[r])&&t.push(r);return t.sort()},j.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},j.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},j.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)j.contains(r,u)||(t[u]=n[u]);return t},j.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]===void 0&&(n[r]=t[r])}),n},j.clone=function(n){return j.isObject(n)?j.isArray(n)?n.slice():j.extend({},n):n},j.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof j&&(n=n._wrapped),t instanceof j&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==String(t);case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;var a=n.constructor,o=t.constructor;if(a!==o&&!(j.isFunction(a)&&a instanceof a&&j.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c=0,f=!0;if("[object Array]"==u){if(c=n.length,f=c==t.length)for(;c--&&(f=S(n[c],t[c],r,e)););}else{for(var s in n)if(j.has(n,s)&&(c++,!(f=j.has(t,s)&&S(n[s],t[s],r,e))))break;if(f){for(s in t)if(j.has(t,s)&&!c--)break;f=!c}}return r.pop(),e.pop(),f};j.isEqual=function(n,t){return S(n,t,[],[])},j.isEmpty=function(n){if(null==n)return!0;if(j.isArray(n)||j.isString(n))return 0===n.length;for(var t in n)if(j.has(n,t))return!1;return!0},j.isElement=function(n){return!(!n||1!==n.nodeType)},j.isArray=x||function(n){return"[object Array]"==l.call(n)},j.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){j["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),j.isArguments(arguments)||(j.isArguments=function(n){return!(!n||!j.has(n,"callee"))}),"function"!=typeof/./&&(j.isFunction=function(n){return"function"==typeof n}),j.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},j.isNaN=function(n){return j.isNumber(n)&&n!=+n},j.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},j.isNull=function(n){return null===n},j.isUndefined=function(n){return n===void 0},j.has=function(n,t){return f.call(n,t)},j.noConflict=function(){return n._=t,this},j.identity=function(n){return n},j.times=function(n,t,r){for(var e=Array(Math.max(0,n)),u=0;n>u;u++)e[u]=t.call(r,u);return e},j.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var I={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};I.unescape=j.invert(I.escape);var T={escape:new RegExp("["+j.keys(I.escape).join("")+"]","g"),unescape:new RegExp("("+j.keys(I.unescape).join("|")+")","g")};j.each(["escape","unescape"],function(n){j[n]=function(t){return null==t?"":(""+t).replace(T[n],function(t){return I[n][t]})}}),j.result=function(n,t){if(null==n)return void 0;var r=n[t];return j.isFunction(r)?r.call(n):r},j.mixin=function(n){A(j.functions(n),function(t){var r=j[t]=n[t];j.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),D.call(this,r.apply(j,n))}})};var N=0;j.uniqueId=function(n){var t=++N+"";return n?n+t:t},j.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},z=/\\|'|\r|\n|\t|\u2028|\u2029/g;j.template=function(n,t,r){var e;r=j.defaults({},r,j.templateSettings);var u=new RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(z,function(n){return"\\"+B[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=new Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,j);var c=function(n){return e.call(this,n,j)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},j.chain=function(n){return j(n).chain()};var D=function(n){return this._chain?j(n).chain():n};j.mixin(j),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];j.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],D.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];j.prototype[n]=function(){return D.call(this,t.apply(this._wrapped,arguments))}}),j.extend(j.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}.call(this); +//# sourceMappingURL=underscore-min.map \ No newline at end of file diff --git a/domain-server/resources/web/settings/describe.json b/domain-server/resources/web/settings/describe.json new file mode 100644 index 0000000000..227b6bf0cd --- /dev/null +++ b/domain-server/resources/web/settings/describe.json @@ -0,0 +1,20 @@ +{ + "audio": { + "label": "Audio", + "assignment-types": [0], + "settings": { + "unattenuated-zone": { + "label": "Unattenuated Zone", + "help": "Boxes for source and listener (corner x, corner y, corner z, size x, size y, size z, corner x, corner y, corner z, size x, size y, size z)", + "placeholder": "no zone", + "default": "" + }, + "dynamic-jitter-buffer": { + "type": "checkbox", + "label": "Dynamic Jitter Buffers", + "help": "Dynamically buffer client audio based on perceived jitter in packet receipt timing", + "default": false + } + } + } +} \ No newline at end of file diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml new file mode 100644 index 0000000000..3bb669b32e --- /dev/null +++ b/domain-server/resources/web/settings/index.shtml @@ -0,0 +1,46 @@ + +

Settings

+
+ +
+ + + +
+
+ + + + + + \ No newline at end of file diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d55a9b52ca..7a2d5f4f99 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -41,7 +41,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : _oauthClientID(), _hostname(), _networkReplyUUIDMap(), - _sessionAuthenticationHash() + _sessionAuthenticationHash(), + _settingsManager() { setOrganizationName("High Fidelity"); setOrganizationDomain("highfidelity.io"); @@ -362,7 +363,7 @@ void DomainServer::createStaticAssignmentsForType(Assignment::Type type, const Q QString dashes = payloadKey.size() == 1 ? "-" : "--"; payloadStringList << QString("%1%2 %3").arg(dashes).arg(payloadKey).arg(jsonObject[payloadKey].toString()); } - + configAssignment->setPayload(payloadStringList.join(' ').toUtf8()); addStaticAssignmentToAssignmentHash(configAssignment); @@ -1162,12 +1163,13 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url } } - // didn't process the request, let the HTTPManager try and handle - return false; + // didn't process the request, let our DomainServerSettingsManager or HTTPManager handle + return _settingsManager.handleHTTPRequest(connection, url); } bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &url) { const QString URI_OAUTH = "/oauth"; + qDebug() << "HTTPS request received at" << url.toString(); if (url.path() == URI_OAUTH) { QUrlQuery codeURLQuery(url); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index b038850b3d..01f44b698e 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -24,6 +24,7 @@ #include #include +#include "DomainServerSettingsManager.h" #include "WalletTransaction.h" #include "PendingAssignedNodeData.h" @@ -110,6 +111,8 @@ private: QString _hostname; QMap _networkReplyUUIDMap; QHash _sessionAuthenticationHash; + + DomainServerSettingsManager _settingsManager; }; #endif // hifi_DomainServer_h diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp new file mode 100644 index 0000000000..d7e2e05ca8 --- /dev/null +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -0,0 +1,179 @@ +// +// DomainServerSettingsManager.cpp +// domain-server/src +// +// Created by Stephen Birarda on 2014-06-24. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "DomainServerSettingsManager.h" + +const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/web/settings/describe.json"; +const QString SETTINGS_CONFIG_FILE_RELATIVE_PATH = "/resources/config.json"; + +DomainServerSettingsManager::DomainServerSettingsManager() : + _descriptionObject(), + _settingsMap() +{ + // load the description object from the settings description + QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH); + descriptionFile.open(QIODevice::ReadOnly); + + _descriptionObject = QJsonDocument::fromJson(descriptionFile.readAll()).object(); + + // load the existing config file to get the current values + QFile configFile(QCoreApplication::applicationDirPath() + SETTINGS_CONFIG_FILE_RELATIVE_PATH); + configFile.open(QIODevice::ReadOnly); + + _settingsMap = QJsonDocument::fromJson(configFile.readAll()).toVariant().toMap(); +} + +const QString DESCRIPTION_SETTINGS_KEY = "settings"; +const QString SETTING_DEFAULT_KEY = "default"; + +bool DomainServerSettingsManager::handleHTTPRequest(HTTPConnection* connection, const QUrl &url) { + if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == "/settings.json") { + // this is a POST operation to change one or more settings + QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent()); + QJsonObject postedObject = postedDocument.object(); + + // we recurse one level deep below each group for the appropriate setting + recurseJSONObjectAndOverwriteSettings(postedObject, _settingsMap, _descriptionObject); + + // store whatever the current _settingsMap is to file + persistToFile(); + + // return success to the caller + QString jsonSuccess = "{\"status\": \"success\"}"; + connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); + + return true; + } else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == "/settings.json") { + // this is a GET operation for our settings + + // check if there is a query parameter for settings affecting a particular type of assignment + const QString SETTINGS_TYPE_QUERY_KEY = "type"; + QUrlQuery settingsQuery(url); + QString typeValue = settingsQuery.queryItemValue(SETTINGS_TYPE_QUERY_KEY); + + QJsonObject responseObject; + + if (typeValue.isEmpty()) { + // combine the description object and our current settings map + responseObject["descriptions"] = _descriptionObject; + responseObject["values"] = QJsonDocument::fromVariant(_settingsMap).object(); + } else { + // convert the string type value to a QJsonValue + QJsonValue queryType = QJsonValue(typeValue.toInt()); + + const QString AFFECTED_TYPES_JSON_KEY = "assignment-types"; + + // enumerate the groups in the description object to find which settings to pass + foreach(const QString& group, _descriptionObject.keys()) { + QJsonObject groupObject = _descriptionObject[group].toObject(); + QJsonObject groupSettingsObject = groupObject[DESCRIPTION_SETTINGS_KEY].toObject(); + + QJsonObject groupResponseObject; + + + foreach(const QString& settingKey, groupSettingsObject.keys()) { + QJsonObject settingObject = groupSettingsObject[settingKey].toObject(); + + QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray(); + if (affectedTypesArray.isEmpty()) { + affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray(); + } + + if (affectedTypesArray.contains(queryType)) { + // this is a setting we should include in the responseObject + + // we need to check if the settings map has a value for this setting + QVariant variantValue; + QVariant settingsMapGroupValue = _settingsMap.value(group); + + if (!settingsMapGroupValue.isNull()) { + variantValue = settingsMapGroupValue.toMap().value(settingKey); + } + + if (variantValue.isNull()) { + // no value for this setting, pass the default + groupResponseObject[settingKey] = settingObject[SETTING_DEFAULT_KEY]; + } else { + groupResponseObject[settingKey] = QJsonValue::fromVariant(variantValue); + } + } + } + + if (!groupResponseObject.isEmpty()) { + // set this group's object to the constructed object + responseObject[group] = groupResponseObject; + } + } + + } + + connection->respond(HTTPConnection::StatusCode200, QJsonDocument(responseObject).toJson(), "application/json"); + return true; + } + + return false; +} + +void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, + QVariantMap& settingsVariant, + QJsonObject descriptionObject) { + foreach(const QString& key, postedObject.keys()) { + + QJsonValue rootValue = postedObject[key]; + + // we don't continue if this key is not present in our descriptionObject + if (descriptionObject.contains(key)) { + if (rootValue.isString()) { + settingsVariant[key] = rootValue.toString(); + } else if (rootValue.isBool()) { + settingsVariant[key] = rootValue.toBool(); + } else if (rootValue.isObject()) { + // there's a JSON Object to explore, so attempt to recurse into it + QJsonObject nextDescriptionObject = descriptionObject[key].toObject(); + + if (nextDescriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) { + if (!settingsVariant.contains(key)) { + // we don't have a map below this key yet, so set it up now + settingsVariant[key] = QVariantMap(); + } + + recurseJSONObjectAndOverwriteSettings(rootValue.toObject(), + *reinterpret_cast(settingsVariant[key].data()), + nextDescriptionObject[DESCRIPTION_SETTINGS_KEY].toObject()); + } + } + } + } +} + +QByteArray DomainServerSettingsManager::getJSONSettingsMap() const { + return QJsonDocument::fromVariant(_settingsMap).toJson(); +} + +void DomainServerSettingsManager::persistToFile() { + QFile settingsFile(QCoreApplication::applicationDirPath() + SETTINGS_CONFIG_FILE_RELATIVE_PATH); + + if (settingsFile.open(QIODevice::WriteOnly)) { + settingsFile.write(getJSONSettingsMap()); + } else { + qCritical("Could not write to JSON settings file. Unable to persist settings."); + } +} \ No newline at end of file diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h new file mode 100644 index 0000000000..8b80cad280 --- /dev/null +++ b/domain-server/src/DomainServerSettingsManager.h @@ -0,0 +1,35 @@ +// +// DomainServerSettingsManager.h +// domain-server/src +// +// Created by Stephen Birarda on 2014-06-24. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DomainServerSettingsManager_h +#define hifi_DomainServerSettingsManager_h + +#include + +#include + +class DomainServerSettingsManager : public QObject, HTTPRequestHandler { + Q_OBJECT +public: + DomainServerSettingsManager(); + bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url); + + QByteArray getJSONSettingsMap() const; +private: + void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant, + QJsonObject descriptionObject); + void persistToFile(); + + QJsonObject _descriptionObject; + QVariantMap _settingsMap; +}; + +#endif // hifi_DomainServerSettingsManager_h \ No newline at end of file diff --git a/examples/concertCamera.js b/examples/concertCamera.js new file mode 100644 index 0000000000..03908d0b57 --- /dev/null +++ b/examples/concertCamera.js @@ -0,0 +1,72 @@ +// +// concertCamera.js +// +// Created by Philip Rosedale on June 24, 2014 +// Copyright 2014 High Fidelity, Inc. +// +// Move a camera through a series of pre-set locations by pressing number keys +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var oldMode; +var avatarPosition; + +var cameraNumber = 0; +var freeCamera = false; + +var cameraLocations = [ {x: 7971.9, y: 241.3, z: 7304.1}, {x: 7973.0, y: 241.3, z: 7304.1}, {x: 7975.5, y: 241.3, z: 7304.1}, {x: 7972.3, y: 241.3, z: 7303.3}, {x: 7971.0, y: 241.3, z: 7304.3}, {x: 7973.5, y: 240.7, z: 7302.5} ]; +var cameraLookAts = [ {x: 7971.1, y: 241.3, z: 7304.1}, {x: 7972.1, y: 241.3, z: 7304.1}, {x: 7972.1, y: 241.3, z: 7304.1}, {x: 7972.1, y: 241.3, z: 7304.1}, {x: 7972.1, y: 241.3, z: 7304.1}, {x: 7971.3, y: 241.3, z: 7304.2} ]; + +function saveCameraState() { + oldMode = Camera.getMode(); + avatarPosition = MyAvatar.position; + Camera.setModeShiftPeriod(0.0); + Camera.setMode("independent"); +} + +function restoreCameraState() { + Camera.stopLooking(); + Camera.setMode(oldMode); +} + +function update(deltaTime) { + if (freeCamera) { + var delta = Vec3.subtract(MyAvatar.position, avatarPosition); + if (Vec3.length(delta) > 0.05) { + cameraNumber = 0; + freeCamera = false; + restoreCameraState(); + } + } +} + +function keyPressEvent(event) { + + var choice = parseInt(event.text); + + if ((choice > 0) && (choice <= cameraLocations.length)) { + print("camera " + choice); + if (!freeCamera) { + saveCameraState(); + freeCamera = true; + } + Camera.setMode("independent"); + Camera.setPosition(cameraLocations[choice - 1]); + Camera.keepLookingAt(cameraLookAts[choice - 1]); + } + if (event.text == "ESC") { + cameraNumber = 0; + freeCamera = false; + restoreCameraState(); + } + if (event.text == "0") { + // Show camera location in log + var cameraLocation = Camera.getPosition(); + print(cameraLocation.x + ", " + cameraLocation.y + ", " + cameraLocation.z); + } +} + +Script.update.connect(update); +Controller.keyPressEvent.connect(keyPressEvent); diff --git a/examples/editModels.js b/examples/editModels.js index f2b76a28c3..64c203534c 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -752,7 +752,16 @@ function Tooltip() { text += "ID: " + properties.id + "\n" text += "model url: " + properties.modelURL + "\n" text += "animation url: " + properties.animationURL + "\n" - + if (properties.sittingPoints.length > 0) { + text += properties.sittingPoints.length + " sitting points: " + for (var i = 0; i < properties.sittingPoints.length; ++i) { + text += properties.sittingPoints[i].name + " " + } + } else { + text += "No sitting points" + } + + Overlays.editOverlay(this.textOverlay, { text: text }); } diff --git a/examples/inWorldTestTone.js b/examples/inWorldTestTone.js new file mode 100644 index 0000000000..e4f34d87cd --- /dev/null +++ b/examples/inWorldTestTone.js @@ -0,0 +1,38 @@ +// +// inWorldTestTone.js +// +// +// Created by Philip Rosedale on 5/29/14. +// Copyright 2014 High Fidelity, Inc. +// +// This example script plays a test tone that is useful for debugging audio dropout. 220Hz test tone played at the domain origin. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var sound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/220Sine.wav"); + +var soundPlaying = false; + +function update(deltaTime) { + if (!Audio.isInjectorPlaying(soundPlaying)) { + var options = new AudioInjectionOptions(); + options.position = { x:0, y:0, z:0 }; + options.volume = 1.0; + options.loop = true; + soundPlaying = Audio.playSound(sound, options); + print("Started sound loop"); + } +} + +function scriptEnding() { + if (Audio.isInjectorPlaying(soundPlaying)) { + Audio.stopInjector(soundPlaying); + print("Stopped sound loop"); + } +} + +Script.update.connect(update); +Script.scriptEnding.connect(scriptEnding); + diff --git a/examples/inspect.js b/examples/inspect.js index b292d5f609..a4ff405c3f 100644 --- a/examples/inspect.js +++ b/examples/inspect.js @@ -195,6 +195,8 @@ function keyReleaseEvent(event) { } } + + function mousePressEvent(event) { if (alt && !isActive) { mouseLastX = event.x; diff --git a/examples/sit.js b/examples/sit.js index d10c08c95a..056a65fbf1 100644 --- a/examples/sit.js +++ b/examples/sit.js @@ -19,7 +19,7 @@ var buttonHeight = 46; var buttonPadding = 10; var buttonPositionX = windowDimensions.x - buttonPadding - buttonWidth; -var buttonPositionY = (windowDimensions.y - buttonHeight) / 2 ; +var buttonPositionY = (windowDimensions.y - buttonHeight) / 2 - (buttonHeight + buttonPadding); var sitDownButton = Overlays.addOverlay("image", { x: buttonPositionX, y: buttonPositionY, width: buttonWidth, height: buttonHeight, @@ -38,9 +38,15 @@ var standUpButton = Overlays.addOverlay("image", { var passedTime = 0.0; var startPosition = null; +var startRotation = null; var animationLenght = 2.0; -var sitting = false; +var avatarOldPosition = { x: 0, y: 0, z: 0 }; + +var sitting = false; + +var seat = new Object(); +var hiddingSeats = false; // This is the pose we would like to end up var pose = [ @@ -49,13 +55,7 @@ var pose = [ {joint:"RightFoot", rotation: {x:30, y:15.0, z:0.0}}, {joint:"LeftUpLeg", rotation: {x:100.0, y:-15.0, z:0.0}}, {joint:"LeftLeg", rotation: {x:-130.0, y:-15.0, z:0.0}}, - {joint:"LeftFoot", rotation: {x:30, y:15.0, z:0.0}}, - - {joint:"Spine2", rotation: {x:20, y:0.0, z:0.0}}, - - {joint:"RightShoulder", rotation: {x:0.0, y:40.0, z:0.0}}, - {joint:"LeftShoulder", rotation: {x:0.0, y:-40.0, z:0.0}} - + {joint:"LeftFoot", rotation: {x:30, y:15.0, z:0.0}} ]; var startPoseAndTransition = []; @@ -89,7 +89,7 @@ var sittingDownAnimation = function(deltaTime) { } } -var standingUpAnimation = function(deltaTime){ +var standingUpAnimation = function(deltaTime) { passedTime += deltaTime; var factor = 1 - passedTime/animationLenght; @@ -103,6 +103,24 @@ var standingUpAnimation = function(deltaTime){ } } +var goToSeatAnimation = function(deltaTime) { + passedTime += deltaTime; + var factor = passedTime/animationLenght; + + if (passedTime <= animationLenght) { + var targetPosition = Vec3.sum(seat.position, { x: 0.3, y: 0.5, z: 0 }); + MyAvatar.position = Vec3.sum(Vec3.multiply(startPosition, 1 - factor), Vec3.multiply(targetPosition, factor)); + } else if (passedTime <= 2 * animationLenght) { + Quat.print("MyAvatar: ", MyAvatar.orientation); + Quat.print("Seat: ", seat.rotation); + MyAvatar.orientation = Quat.mix(startRotation, seat.rotation, factor - 1); + } else { + Script.update.disconnect(goToSeatAnimation); + sitDown(); + showIndicators(false); + } +} + function sitDown() { sitting = true; passedTime = 0.0; @@ -130,15 +148,104 @@ function standUp() { Overlays.editOverlay(sitDownButton, { visible: true }); } -Controller.mousePressEvent.connect(function(event){ +var models = new Object(); +function SeatIndicator(modelProperties, seatIndex) { + this.position = Vec3.sum(modelProperties.position, + Vec3.multiply(Vec3.multiplyQbyV(modelProperties.modelRotation, + modelProperties.sittingPoints[seatIndex].position), + modelProperties.radius)); + + this.orientation = Quat.multiply(modelProperties.modelRotation, + modelProperties.sittingPoints[seatIndex].rotation); + this.scale = MyAvatar.scale / 12; + + this.sphere = Overlays.addOverlay("sphere", { + position: this.position, + size: this.scale, + solid: true, + color: { red: 0, green: 0, blue: 255 }, + alpha: 0.3, + visible: true + }); + + this.show = function(doShow) { + Overlays.editOverlay(this.sphere, { visible: doShow }); + } + + this.update = function() { + Overlays.editOverlay(this.sphere, { + position: this.position, + size: this.scale + }); + } + + this.cleanup = function() { + Overlays.deleteOverlay(this.sphere); + } +} +Controller.mousePressEvent.connect(function(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); if (clickedOverlay == sitDownButton) { sitDown(); } else if (clickedOverlay == standUpButton) { standUp(); - } + } else { + var pickRay = Camera.computePickRay(event.x, event.y); + + var clickedOnSeat = false; + + for (index in models) { + var model = models[index]; + + for (var i = 0; i < model.properties.sittingPoints.length; ++i) { + if (raySphereIntersection(pickRay.origin, + pickRay.direction, + model.properties.sittingPoints[i].indicator.position, + model.properties.sittingPoints[i].indicator.scale / 2)) { + clickedOnSeat = true; + seat.position = model.properties.sittingPoints[i].indicator.position; + seat.rotation = model.properties.sittingPoints[i].indicator.orientation; + } + } + } + if (clickedOnSeat) { + passedTime = 0.0; + startPosition = MyAvatar.position; + startRotation = MyAvatar.orientation; + try{ Script.update.disconnect(standingUpAnimation); } catch(e){} + try{ Script.update.disconnect(sittingDownAnimation); } catch(e){} + Script.update.connect(goToSeatAnimation); + } + + + + return; + var intersection = Models.findRayIntersection(pickRay); + + if (intersection.accurate && intersection.intersects && false) { + var properties = intersection.modelProperties; + print("Intersecting with model, let's check for seats."); + + if (properties.sittingPoints.length > 0) { + print("Available seats, going to the first one: " + properties.sittingPoints[0].name); + seat.position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.modelRotation, properties.sittingPoints[0].position)); + Vec3.print("Seat position: ", seat.position); + seat.rotation = Quat.multiply(properties.modelRotation, properties.sittingPoints[0].rotation); + Quat.print("Seat rotation: ", seat.rotation); + + passedTime = 0.0; + startPosition = MyAvatar.position; + startRotation = MyAvatar.orientation; + try{ Script.update.disconnect(standingUpAnimation); } catch(e){} + try{ Script.update.disconnect(sittingDownAnimation); } catch(e){} + Script.update.connect(goToSeatAnimation); + } else { + print ("Sorry, no seats here."); + } + } + } }) function update(deltaTime){ @@ -149,7 +256,76 @@ function update(deltaTime){ var newY = (windowDimensions.y - buttonHeight) / 2 ; Overlays.editOverlay( standUpButton, {x: newX, y: newY} ); Overlays.editOverlay( sitDownButton, {x: newX, y: newY} ); - } + } + + if (MyAvatar.position.x != avatarOldPosition.x && + MyAvatar.position.y != avatarOldPosition.y && + MyAvatar.position.z != avatarOldPosition.z) { + avatarOldPosition = MyAvatar.position; + + var SEARCH_RADIUS = 5; + var foundModels = Models.findModels(MyAvatar.position, SEARCH_RADIUS); + // Let's remove indicator that got out of radius + for (model in models) { + if (Vec3.distance(models[model].properties.position, MyAvatar.position) > SEARCH_RADIUS) { + removeIndicators(models[model]); + } + } + + // Let's add indicators to new seats in radius + for (var i = 0; i < foundModels.length; ++i) { + var model = foundModels[i]; + if (typeof(models[model.id]) == "undefined") { + addIndicators(model); + } + } + + if (hiddingSeats && passedTime >= animationLenght) { + showIndicators(true); + } + } +} + +function addIndicators(modelID) { + modelID.properties = Models.getModelProperties(modelID); + if (modelID.properties.sittingPoints.length > 0) { + for (var i = 0; i < modelID.properties.sittingPoints.length; ++i) { + modelID.properties.sittingPoints[i].indicator = new SeatIndicator(modelID.properties, i); + } + + models[modelID.id] = modelID; + } else { + Models.editModel(modelID, { glowLevel: 0.0 }); + } +} + +function removeIndicators(modelID) { + for (var i = 0; i < modelID.properties.sittingPoints.length; ++i) { + modelID.properties.sittingPoints[i].indicator.cleanup(); + } + delete models[modelID.id]; +} + +function showIndicators(doShow) { + for (model in models) { + var modelID = models[model]; + for (var i = 0; i < modelID.properties.sittingPoints.length; ++i) { + modelID.properties.sittingPoints[i].indicator.show(doShow); + } + } + hiddingSeats = !doShow; +} + +function raySphereIntersection(origin, direction, center, radius) { + var A = origin; + var B = Vec3.normalize(direction); + var P = center; + + var x = Vec3.dot(Vec3.subtract(P, A), B); + var X = Vec3.sum(A, Vec3.multiply(B, x)); + var d = Vec3.length(Vec3.subtract(P, X)); + + return (x > 0 && d <= radius); } function keyPressEvent(event) { @@ -167,11 +343,15 @@ Script.update.connect(update); Controller.keyPressEvent.connect(keyPressEvent); Script.scriptEnding.connect(function() { - for (var i = 0; i < pose.length; i++){ MyAvatar.clearJointData(pose[i].joint); - } + } Overlays.deleteOverlay(sitDownButton); Overlays.deleteOverlay(standUpButton); + for (model in models){ + for (var i = 0; i < models[model].properties.sittingPoints.length; ++i) { + models[model].properties.sittingPoints[i].indicator.cleanup(); + } + } }); diff --git a/examples/squeezeHands.js b/examples/squeezeHands.js index e53dd9569c..da720734e1 100644 --- a/examples/squeezeHands.js +++ b/examples/squeezeHands.js @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var rightHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/RightHandAnim.fbx"; -var leftHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/LeftHandAnim.fbx"; +var rightHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/RightHandAnimPhilip.fbx"; +var leftHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/LeftHandAnimPhilip.fbx"; var LEFT = 0; var RIGHT = 1; diff --git a/examples/toyball.js b/examples/toyball.js index d312c1bc94..e03fd67a5d 100644 --- a/examples/toyball.js +++ b/examples/toyball.js @@ -26,14 +26,21 @@ var RIGHT_TIP = 3; var RIGHT_BUTTON_FWD = 11; var RIGHT_BUTTON_3 = 9; +var BALL_RADIUS = 0.08; +var GRAVITY_STRENGTH = 0.5; + +var HELD_COLOR = { red: 240, green: 0, blue: 0 }; +var THROWN_COLOR = { red: 128, green: 0, blue: 0 }; + var leftBallAlreadyInHand = false; var rightBallAlreadyInHand = false; var leftHandParticle; var rightHandParticle; -var throwSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/throw.raw"); +var newSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/throw.raw"); var catchSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/catch.raw"); -var targetRadius = 0.25; +var throwSound = new Sound("http://highfidelity-public.s3-us-west-1.amazonaws.com/sounds/Switches%20and%20sliders/slider%20-%20whoosh1.raw"); +var targetRadius = 1.0; var wantDebugging = false; @@ -44,31 +51,19 @@ function debugPrint(message) { } function getBallHoldPosition(whichSide) { - var normal; - var tipPosition; if (whichSide == LEFT_PALM) { - normal = Controller.getSpatialControlNormal(LEFT_PALM); - tipPosition = Controller.getSpatialControlPosition(LEFT_TIP); + position = MyAvatar.getLeftPalmPosition(); } else { - normal = Controller.getSpatialControlNormal(RIGHT_PALM); - tipPosition = Controller.getSpatialControlPosition(RIGHT_TIP); + position = MyAvatar.getRightPalmPosition(); } - var BALL_FORWARD_OFFSET = 0.08; // put the ball a bit forward of fingers - position = { x: BALL_FORWARD_OFFSET * normal.x, - y: BALL_FORWARD_OFFSET * normal.y, - z: BALL_FORWARD_OFFSET * normal.z }; - - position.x += tipPosition.x; - position.y += tipPosition.y; - position.z += tipPosition.z; - return position; } function checkControllerSide(whichSide) { var BUTTON_FWD; var BUTTON_3; + var TRIGGER; var palmPosition; var ballAlreadyInHand; var handMessage; @@ -76,18 +71,20 @@ function checkControllerSide(whichSide) { if (whichSide == LEFT_PALM) { BUTTON_FWD = LEFT_BUTTON_FWD; BUTTON_3 = LEFT_BUTTON_3; + TRIGGER = 0; palmPosition = Controller.getSpatialControlPosition(LEFT_PALM); ballAlreadyInHand = leftBallAlreadyInHand; handMessage = "LEFT"; } else { BUTTON_FWD = RIGHT_BUTTON_FWD; BUTTON_3 = RIGHT_BUTTON_3; + TRIGGER = 1; palmPosition = Controller.getSpatialControlPosition(RIGHT_PALM); ballAlreadyInHand = rightBallAlreadyInHand; handMessage = "RIGHT"; } - - var grabButtonPressed = (Controller.isButtonPressed(BUTTON_FWD) || Controller.isButtonPressed(BUTTON_3)); + + var grabButtonPressed = (Controller.isButtonPressed(BUTTON_FWD) || Controller.isButtonPressed(BUTTON_3) || (Controller.getTriggerValue(TRIGGER) > 0.5)); // If I don't currently have a ball in my hand, then try to catch closest one if (!ballAlreadyInHand && grabButtonPressed) { @@ -107,8 +104,11 @@ function checkControllerSide(whichSide) { var ballPosition = getBallHoldPosition(whichSide); var properties = { position: { x: ballPosition.x, y: ballPosition.y, - z: ballPosition.z }, - velocity : { x: 0, y: 0, z: 0}, inHand: true }; + z: ballPosition.z }, + color: HELD_COLOR, + velocity : { x: 0, y: 0, z: 0}, + lifetime : 600, + inHand: true }; Particles.editParticle(closestParticle, properties); var options = new AudioInjectionOptions(); @@ -127,7 +127,7 @@ function checkControllerSide(whichSide) { //} // If '3' is pressed, and not holding a ball, make a new one - if (Controller.isButtonPressed(BUTTON_3) && !ballAlreadyInHand) { + if (grabButtonPressed && !ballAlreadyInHand) { var ballPosition = getBallHoldPosition(whichSide); var properties = { position: { x: ballPosition.x, y: ballPosition.y, @@ -135,11 +135,11 @@ function checkControllerSide(whichSide) { velocity: { x: 0, y: 0, z: 0}, gravity: { x: 0, y: 0, z: 0}, inHand: true, - radius: 0.05, + radius: BALL_RADIUS, damping: 0.999, - color: { red: 255, green: 0, blue: 0 }, + color: HELD_COLOR, - lifetime: 10 // 10 seconds - same as default, not needed but here as an example + lifetime: 600 // 10 seconds - same as default, not needed but here as an example }; newParticle = Particles.addParticle(properties); @@ -155,7 +155,7 @@ function checkControllerSide(whichSide) { var options = new AudioInjectionOptions(); options.position = ballPosition; options.volume = 1.0; - Audio.playSound(catchSound, options); + Audio.playSound(newSound, options); return; // exit early } @@ -188,7 +188,9 @@ function checkControllerSide(whichSide) { y: tipVelocity.y * THROWN_VELOCITY_SCALING, z: tipVelocity.z * THROWN_VELOCITY_SCALING } , inHand: false, - gravity: { x: 0, y: -2, z: 0}, + color: THROWN_COLOR, + lifetime: 10, + gravity: { x: 0, y: -GRAVITY_STRENGTH, z: 0}, }; Particles.editParticle(handParticle, properties); diff --git a/interface/resources/styles/preferences.qss b/interface/resources/styles/preferences.qss index e678acd0c9..40e35c8e52 100644 --- a/interface/resources/styles/preferences.qss +++ b/interface/resources/styles/preferences.qss @@ -12,7 +12,8 @@ QLabel#advancedTuningLabel { QPushButton#buttonBrowseHead, QPushButton#buttonBrowseBody, -QPushButton#buttonBrowseLocation { +QPushButton#buttonBrowseLocation, +QPushButton#buttonBrowseScriptsLocation { background-image: url(styles/search.svg); background-repeat: no-repeat; background-position: center center; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index cfebc97c05..a032fa7ea0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -134,7 +134,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _nodeThread(new QThread(this)), _datagramProcessor(), _frameCount(0), - _fps(120.0f), + _fps(60.0f), _justStarted(true), _voxelImporter(NULL), _importSucceded(false), @@ -167,7 +167,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _nodeBoundsDisplay(this), _previousScriptLocation(), _applicationOverlay(), - _runningScriptsWidget(new RunningScriptsWidget(_window)), + _runningScriptsWidget(NULL), _runningScriptsWidgetWasVisible(false), _trayIcon(new QSystemTrayIcon(_window)), _lastNackTime(usecTimestampNow()) @@ -201,6 +201,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : // call Menu getInstance static method to set up the menu _window->setMenuBar(Menu::getInstance()); + _runningScriptsWidget = new RunningScriptsWidget(_window); + unsigned int listenPort = 0; // bind to an ephemeral port by default const char** constArgv = const_cast(argv); const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); @@ -551,7 +553,7 @@ void Application::initializeGL() { } // update before the first render - update(0.0f); + update(1.f / _fps); InfoView::showFirstTime(); } @@ -3533,10 +3535,12 @@ void Application::saveScripts() { _settings->endArray(); } -ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScriptFromEditor) { +ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScriptFromEditor, bool activateMainWindow) { QUrl scriptUrl(scriptName); const QString& scriptURLString = scriptUrl.toString(); - if(loadScriptFromEditor && _scriptEnginesHash.contains(scriptURLString) && !_scriptEnginesHash[scriptURLString]->isFinished()){ + if (_scriptEnginesHash.contains(scriptURLString) && loadScriptFromEditor + && !_scriptEnginesHash[scriptURLString]->isFinished()) { + return _scriptEnginesHash[scriptURLString]; } @@ -3549,10 +3553,11 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript if (!scriptEngine->hasScript()) { qDebug() << "Application::loadScript(), script failed to load..."; + QMessageBox::warning(getWindow(), "Error Loading Script", scriptURLString + " failed to load."); return NULL; } - _scriptEnginesHash.insert(scriptURLString, scriptEngine); + _scriptEnginesHash.insertMulti(scriptURLString, scriptEngine); _runningScriptsWidget->setRunningScripts(getRunningScripts()); } @@ -3618,7 +3623,7 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript workerThread->start(); // restore the main window's active state - if (!loadScriptFromEditor) { + if (activateMainWindow && !loadScriptFromEditor) { _window->activateWindow(); } bumpSettings(); @@ -3627,7 +3632,9 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript } void Application::scriptFinished(const QString& scriptName) { - if (_scriptEnginesHash.remove(scriptName)) { + QHash::iterator it = _scriptEnginesHash.find(scriptName); + if (it != _scriptEnginesHash.end()) { + _scriptEnginesHash.erase(it); _runningScriptsWidget->scriptStopped(scriptName); _runningScriptsWidget->setRunningScripts(getRunningScripts()); bumpSettings(); @@ -3647,8 +3654,9 @@ void Application::stopAllScripts(bool restart) { } void Application::stopScript(const QString &scriptName) { - if (_scriptEnginesHash.contains(scriptName)) { - _scriptEnginesHash.value(scriptName)->stop(); + const QString& scriptURLString = QUrl(scriptName).toString(); + if (_scriptEnginesHash.contains(scriptURLString)) { + _scriptEnginesHash.value(scriptURLString)->stop(); qDebug() << "stopping script..." << scriptName; } } diff --git a/interface/src/Application.h b/interface/src/Application.h index af0dfe9d15..56b6f673ae 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -324,7 +324,7 @@ public slots: void loadScriptURLDialog(); void toggleLogDialog(); void initAvatarAndViewFrustum(); - ScriptEngine* loadScript(const QString& fileNameString = QString(), bool loadScriptFromEditor = false); + ScriptEngine* loadScript(const QString& fileNameString = QString(), bool loadScriptFromEditor = false, bool activateMainWindow = false); void scriptFinished(const QString& scriptName); void stopAllScripts(bool restart = false); void stopScript(const QString& scriptName); diff --git a/interface/src/Camera.cpp b/interface/src/Camera.cpp index 0e33e14f32..4490b60fc9 100644 --- a/interface/src/Camera.cpp +++ b/interface/src/Camera.cpp @@ -317,8 +317,6 @@ void CameraScriptableObject::setMode(const QString& mode) { } if (currentMode != targetMode) { _camera->setMode(targetMode); - const float DEFAULT_MODE_SHIFT_PERIOD = 0.5f; // half second - _camera->setModeShiftPeriod(DEFAULT_MODE_SHIFT_PERIOD); } } diff --git a/interface/src/Camera.h b/interface/src/Camera.h index 5e189c1111..2bbbf0e751 100644 --- a/interface/src/Camera.h +++ b/interface/src/Camera.h @@ -42,9 +42,8 @@ public: void setTargetPosition(const glm::vec3& t); void setTightness(float t) { _tightness = t; } void setTargetRotation(const glm::quat& rotation); - - void setMode(CameraMode m); void setModeShiftPeriod(float r); + void setMode(CameraMode m); void setFieldOfView(float f); void setAspectRatio(float a); void setNearClip(float n); @@ -130,6 +129,7 @@ public: public slots: QString getMode() const; void setMode(const QString& mode); + void setModeShiftPeriod(float r) {_camera->setModeShiftPeriod(r); } void setPosition(const glm::vec3& value) { _camera->setTargetPosition(value);} glm::vec3 getPosition() const { return _camera->getPosition(); } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 7d4c7d109d..9357ba2004 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -109,7 +109,8 @@ Menu::Menu() : _loginAction(NULL), _preferencesDialog(NULL), _loginDialog(NULL), - _snapshotsLocation() + _snapshotsLocation(), + _scriptsLocation() { Application *appInstance = Application::getInstance(); @@ -345,6 +346,7 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, true); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, false); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::StringHair, 0, false); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Particles, 0, true); addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, Qt::SHIFT | Qt::Key_L, this, SLOT(lodTools())); @@ -607,6 +609,7 @@ void Menu::loadSettings(QSettings* settings) { _boundaryLevelAdjust = loadSetting(settings, "boundaryLevelAdjust", 0); _snapshotsLocation = settings->value("snapshotsLocation", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)).toString(); + setScriptsLocation(settings->value("scriptsLocation", QString()).toString()); settings->beginGroup("View Frustum Offset Camera"); // in case settings is corrupt or missing loadSetting() will check for NaN @@ -651,6 +654,7 @@ void Menu::saveSettings(QSettings* settings) { settings->setValue("avatarLODDistanceMultiplier", _avatarLODDistanceMultiplier); settings->setValue("boundaryLevelAdjust", _boundaryLevelAdjust); settings->setValue("snapshotsLocation", _snapshotsLocation); + settings->setValue("scriptsLocation", _scriptsLocation); settings->beginGroup("View Frustum Offset Camera"); settings->setValue("viewFrustumOffsetYaw", _viewFrustumOffset.yaw); settings->setValue("viewFrustumOffsetPitch", _viewFrustumOffset.pitch); @@ -1786,3 +1790,8 @@ QString Menu::getSnapshotsLocation() const { } return _snapshotsLocation; } + +void Menu::setScriptsLocation(const QString& scriptsLocation) { + _scriptsLocation = scriptsLocation; + emit scriptLocationChanged(scriptsLocation); +} diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 5b479773b6..7417f953c6 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -102,6 +102,9 @@ public: QString getSnapshotsLocation() const; void setSnapshotsLocation(QString snapshotsLocation) { _snapshotsLocation = snapshotsLocation; } + const QString& getScriptsLocation() const { return _scriptsLocation; } + void setScriptsLocation(const QString& scriptsLocation); + BandwidthDialog* getBandwidthDialog() const { return _bandwidthDialog; } FrustumDrawMode getFrustumDrawMode() const { return _frustumDrawMode; } ViewFrustumOffset getViewFrustumOffset() const { return _viewFrustumOffset; } @@ -156,6 +159,9 @@ public: void static goToDomain(const QString newDomain); void static goTo(QString destination); +signals: + void scriptLocationChanged(const QString& newPath); + public slots: void loginForCurrentDomain(); @@ -283,6 +289,7 @@ private: QPointer _loginDialog; QAction* _chatAction; QString _snapshotsLocation; + QString _scriptsLocation; }; namespace MenuOption { @@ -319,6 +326,7 @@ namespace MenuOption { const QString Bandwidth = "Bandwidth Display"; const QString BandwidthDetails = "Bandwidth Details"; const QString BuckyBalls = "Bucky Balls"; + const QString StringHair = "String Hair"; const QString CascadedShadows = "Cascaded"; const QString Chat = "Chat..."; const QString ChatCircling = "Chat Circling"; diff --git a/interface/src/ScriptsModel.cpp b/interface/src/ScriptsModel.cpp new file mode 100644 index 0000000000..f9ed94f3fa --- /dev/null +++ b/interface/src/ScriptsModel.cpp @@ -0,0 +1,209 @@ +// +// ScriptsModel.cpp +// interface/src +// +// Created by Ryan Huffman on 05/12/14. +// Copyright 2014 High Fidelity, Inc. +// +// S3 request code written with ModelBrowser as a reference. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include "ScriptsModel.h" +#include "Menu.h" + + +static const QString S3_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com"; +static const QString PUBLIC_URL = "http://public.highfidelity.io"; +static const QString MODELS_LOCATION = "scripts/"; + +static const QString PREFIX_PARAMETER_NAME = "prefix"; +static const QString MARKER_PARAMETER_NAME = "marker"; +static const QString IS_TRUNCATED_NAME = "IsTruncated"; +static const QString CONTAINER_NAME = "Contents"; +static const QString KEY_NAME = "Key"; + +static const int SCRIPT_PATH = Qt::UserRole; + +ScriptItem::ScriptItem(const QString& filename, const QString& fullPath) : + _filename(filename), + _fullPath(fullPath) { +}; + +ScriptsModel::ScriptsModel(QObject* parent) : + QAbstractListModel(parent), + _loadingScripts(false), + _localDirectory(), + _fsWatcher(), + _localFiles(), + _remoteFiles() { + + QString scriptPath = Menu::getInstance()->getScriptsLocation(); + + _localDirectory.setPath(scriptPath); + _localDirectory.setFilter(QDir::Files | QDir::Readable); + _localDirectory.setNameFilters(QStringList("*.js")); + + _fsWatcher.addPath(_localDirectory.absolutePath()); + connect(&_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &ScriptsModel::reloadLocalFiles); + + connect(Menu::getInstance(), &Menu::scriptLocationChanged, this, &ScriptsModel::updateScriptsLocation); + + reloadLocalFiles(); + reloadRemoteFiles(); +} + +QVariant ScriptsModel::data(const QModelIndex& index, int role) const { + const QList* files = NULL; + int row = 0; + bool isLocal = index.row() < _localFiles.size(); + if (isLocal) { + files = &_localFiles; + row = index.row(); + } else { + files = &_remoteFiles; + row = index.row() - _localFiles.size(); + } + + if (role == Qt::DisplayRole) { + return QVariant((*files)[row]->getFilename() + (isLocal ? " (local)" : "")); + } else if (role == ScriptPath) { + return QVariant((*files)[row]->getFullPath()); + } + return QVariant(); +} + +int ScriptsModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) { + return 0; + } + return _localFiles.length() + _remoteFiles.length(); +} + +void ScriptsModel::updateScriptsLocation(const QString& newPath) { + _fsWatcher.removePath(_localDirectory.absolutePath()); + _localDirectory.setPath(newPath); + _fsWatcher.addPath(_localDirectory.absolutePath()); + reloadLocalFiles(); +} + +void ScriptsModel::reloadRemoteFiles() { + if (!_loadingScripts) { + _loadingScripts = true; + while (!_remoteFiles.isEmpty()) { + delete _remoteFiles.takeFirst(); + } + requestRemoteFiles(); + } +} + +void ScriptsModel::requestRemoteFiles(QString marker) { + QUrl url(S3_URL); + QUrlQuery query; + query.addQueryItem(PREFIX_PARAMETER_NAME, MODELS_LOCATION); + if (!marker.isEmpty()) { + query.addQueryItem(MARKER_PARAMETER_NAME, marker); + } + url.setQuery(query); + + QNetworkAccessManager* accessManager = new QNetworkAccessManager(this); + connect(accessManager, SIGNAL(finished(QNetworkReply*)), SLOT(downloadFinished(QNetworkReply*))); + + QNetworkRequest request(url); + accessManager->get(request); +} + +void ScriptsModel::downloadFinished(QNetworkReply* reply) { + bool finished = true; + + if (reply->error() == QNetworkReply::NoError) { + QByteArray data = reply->readAll(); + + if (!data.isEmpty()) { + finished = parseXML(data); + } else { + qDebug() << "Error: Received no data when loading remote scripts"; + } + } + + reply->deleteLater(); + sender()->deleteLater(); + + if (finished) { + _loadingScripts = false; + } +} + +bool ScriptsModel::parseXML(QByteArray xmlFile) { + beginResetModel(); + + QXmlStreamReader xml(xmlFile); + QRegExp jsRegex(".*\\.js"); + bool truncated = false; + QString lastKey; + + while (!xml.atEnd() && !xml.hasError()) { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == IS_TRUNCATED_NAME) { + while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == IS_TRUNCATED_NAME)) { + xml.readNext(); + if (xml.text().toString() == "true") { + truncated = true; + } + } + } + + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == CONTAINER_NAME) { + while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == CONTAINER_NAME)) { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == KEY_NAME) { + xml.readNext(); + lastKey = xml.text().toString(); + if (jsRegex.exactMatch(xml.text().toString())) { + _remoteFiles.append(new ScriptItem(lastKey.mid(MODELS_LOCATION.length()), S3_URL + "/" + lastKey)); + } + } + xml.readNext(); + } + } + xml.readNext(); + } + + endResetModel(); + + // Error handling + if (xml.hasError()) { + qDebug() << "Error loading remote scripts: " << xml.errorString(); + return true; + } + + if (truncated) { + requestRemoteFiles(lastKey); + } + + // If this request was not truncated, we are done. + return !truncated; +} + +void ScriptsModel::reloadLocalFiles() { + beginResetModel(); + + while (!_localFiles.isEmpty()) { + delete _localFiles.takeFirst(); + } + + _localDirectory.refresh(); + + const QFileInfoList localFiles = _localDirectory.entryInfoList(); + for (int i = 0; i < localFiles.size(); i++) { + QFileInfo file = localFiles[i]; + _localFiles.append(new ScriptItem(file.fileName(), file.absoluteFilePath())); + } + + endResetModel(); +} diff --git a/interface/src/ScriptsModel.h b/interface/src/ScriptsModel.h new file mode 100644 index 0000000000..250c7eb9a8 --- /dev/null +++ b/interface/src/ScriptsModel.h @@ -0,0 +1,62 @@ +// +// ScriptsModel.h +// interface/src +// +// Created by Ryan Huffman on 05/12/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptsModel_h +#define hifi_ScriptsModel_h + +#include +#include +#include +#include + +class ScriptItem { +public: + ScriptItem(const QString& filename, const QString& fullPath); + + const QString& getFilename() { return _filename; }; + const QString& getFullPath() { return _fullPath; }; + +private: + QString _filename; + QString _fullPath; +}; + +class ScriptsModel : public QAbstractListModel { + Q_OBJECT +public: + ScriptsModel(QObject* parent = NULL); + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const; + + enum Role { + ScriptPath = Qt::UserRole, + }; + +protected slots: + void updateScriptsLocation(const QString& newPath); + void downloadFinished(QNetworkReply* reply); + void reloadLocalFiles(); + void reloadRemoteFiles(); + +protected: + void requestRemoteFiles(QString marker = QString()); + bool parseXML(QByteArray xmlFile); + +private: + bool _loadingScripts; + QDir _localDirectory; + QFileSystemWatcher _fsWatcher; + QList _localFiles; + QList _remoteFiles; +}; + +#endif // hifi_ScriptsModel_h diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp index 5ce1435bd6..4dbb015459 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -68,6 +68,7 @@ void printVector(glm::vec3 vec) { qDebug("%4.2f, %4.2f, %4.2f", vec.x, vec.y, vec.z); } + // Return the azimuth angle (in radians) between two points. float azimuth_to(glm::vec3 head_pos, glm::vec3 source_pos) { return atan2(head_pos.x - source_pos.x, head_pos.z - source_pos.z); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 57452f687f..9b136980f4 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -48,6 +48,10 @@ Avatar::Avatar() : _skeletonModel(this), _bodyYawDelta(0.0f), _velocity(0.0f, 0.0f, 0.0f), + _lastVelocity(0.0f, 0.0f, 0.0f), + _acceleration(0.0f, 0.0f, 0.0f), + _angularVelocity(0.0f, 0.0f, 0.0f), + _lastOrientation(), _leanScale(0.5f), _scale(1.0f), _worldUpDirection(DEFAULT_UP_DIRECTION), @@ -76,6 +80,7 @@ void Avatar::init() { _skeletonModel.init(); _initialized = true; _shouldRenderBillboard = (getLODDistance() >= BILLBOARD_LOD_DISTANCE); + initializeHair(); } glm::vec3 Avatar::getChestPosition() const { @@ -134,10 +139,15 @@ void Avatar::simulate(float deltaTime) { head->setPosition(headPosition); head->setScale(_scale); head->simulate(deltaTime, false, _shouldRenderBillboard); + + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + simulateHair(deltaTime); + } } // update position by velocity, and subtract the change added earlier for gravity _position += _velocity * deltaTime; + updateAcceleration(deltaTime); // update animation for display name fade in/out if ( _displayNameTargetAlpha != _displayNameAlpha) { @@ -157,6 +167,17 @@ void Avatar::simulate(float deltaTime) { } } +void Avatar::updateAcceleration(float deltaTime) { + // Linear Component of Acceleration + _acceleration = (_velocity - _lastVelocity) * (1.f / deltaTime); + _lastVelocity = _velocity; + // Angular Component of Acceleration + glm::quat orientation = getOrientation(); + glm::quat delta = glm::inverse(_lastOrientation) * orientation; + _angularVelocity = safeEulerAngles(delta) * (1.f / deltaTime); + _lastOrientation = getOrientation(); +} + void Avatar::setMouseRay(const glm::vec3 &origin, const glm::vec3 &direction) { _mouseRayOrigin = origin; _mouseRayDirection = direction; @@ -357,6 +378,232 @@ void Avatar::renderBody(RenderMode renderMode, float glowLevel) { getHand()->render(false, modelRenderMode); } getHead()->render(1.0f, modelRenderMode); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + renderHair(); + } +} + +// +// Constants for the Hair Simulation +// + +const float HAIR_LENGTH = 0.2f; +const float HAIR_LINK_LENGTH = HAIR_LENGTH / HAIR_LINKS; +const float HAIR_DAMPING = 0.99f; +const float HEAD_RADIUS = 0.21f; +const float CONSTRAINT_RELAXATION = 10.0f; +const glm::vec3 HAIR_GRAVITY(0.0f, -0.007f, 0.0f); +const float HAIR_ACCELERATION_COUPLING = 0.025f; +const float HAIR_ANGULAR_VELOCITY_COUPLING = 0.10f; +const float HAIR_MAX_LINEAR_ACCELERATION = 4.0f; +const float HAIR_THICKNESS = 0.015f; +const float HAIR_STIFFNESS = 0.0000f; +const glm::vec3 HAIR_COLOR1(0.98f, 0.92f, 0.843f); +const glm::vec3 HAIR_COLOR2(0.545f, 0.533f, 0.47f); +const glm::vec3 WIND_DIRECTION(0.5f, -1.0f, 0.0f); +const float MAX_WIND_STRENGTH = 0.02f; +const float FINGER_LENGTH = 0.25f; +const float FINGER_RADIUS = 0.10f; + +void Avatar::renderHair() { + // + // Render the avatar's moveable hair + // + + glm::vec3 headPosition = getHead()->getPosition(); + glPushMatrix(); + glTranslatef(headPosition.x, headPosition.y, headPosition.z); + const glm::quat& rotation = getHead()->getFinalOrientationInWorldFrame(); + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); + + glBegin(GL_QUADS); + for (int strand = 0; strand < HAIR_STRANDS; strand++) { + for (int link = 0; link < HAIR_LINKS - 1; link++) { + int vertexIndex = strand * HAIR_LINKS + link; + glColor3fv(&_hairColors[vertexIndex].x); + glNormal3fv(&_hairNormals[vertexIndex].x); + glVertex3f(_hairPosition[vertexIndex].x - _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex].y - _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex].z - _hairQuadDelta[vertexIndex].z); + glVertex3f(_hairPosition[vertexIndex].x + _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex].y + _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex].z + _hairQuadDelta[vertexIndex].z); + + glVertex3f(_hairPosition[vertexIndex + 1].x + _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex + 1].y + _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex + 1].z + _hairQuadDelta[vertexIndex].z); + glVertex3f(_hairPosition[vertexIndex + 1].x - _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex + 1].y - _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex + 1].z - _hairQuadDelta[vertexIndex].z); + } + } + glEnd(); + + glPopMatrix(); + +} + +void Avatar::simulateHair(float deltaTime) { + + deltaTime = glm::clamp(deltaTime, 0.0f, 1.0f / 30.0f); + glm::vec3 acceleration = getAcceleration(); + if (glm::length(acceleration) > HAIR_MAX_LINEAR_ACCELERATION) { + acceleration = glm::normalize(acceleration) * HAIR_MAX_LINEAR_ACCELERATION; + } + const glm::quat& rotation = getHead()->getFinalOrientationInWorldFrame(); + acceleration = acceleration * rotation; + glm::vec3 angularVelocity = getAngularVelocity() + getHead()->getAngularVelocity(); + + // Get hand positions to allow touching hair + glm::vec3 leftHandPosition, rightHandPosition; + getSkeletonModel().getLeftHandPosition(leftHandPosition); + getSkeletonModel().getRightHandPosition(rightHandPosition); + leftHandPosition -= getHead()->getPosition(); + rightHandPosition -= getHead()->getPosition(); + glm::quat leftRotation, rightRotation; + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftRotation); + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightRotation); + leftHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.0f) * glm::inverse(leftRotation); + rightHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.0f) * glm::inverse(rightRotation); + leftHandPosition = leftHandPosition * rotation; + rightHandPosition = rightHandPosition * rotation; + + float windIntensity = randFloat() * MAX_WIND_STRENGTH; + + for (int strand = 0; strand < HAIR_STRANDS; strand++) { + for (int link = 0; link < HAIR_LINKS; link++) { + int vertexIndex = strand * HAIR_LINKS + link; + if (vertexIndex % HAIR_LINKS == 0) { + // Base Joint - no integration + } else { + // + // Vertlet Integration + // + // Add velocity from last position, with damping + glm::vec3 thisPosition = _hairPosition[vertexIndex]; + glm::vec3 diff = thisPosition - _hairLastPosition[vertexIndex]; + _hairPosition[vertexIndex] += diff * HAIR_DAMPING; + // Resolve collision with head sphere + if (glm::length(_hairPosition[vertexIndex]) < HEAD_RADIUS) { + _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex]) * + (HEAD_RADIUS - glm::length(_hairPosition[vertexIndex])); + } + // Resolve collision with hands + if (glm::length(_hairPosition[vertexIndex] - leftHandPosition) < FINGER_RADIUS) { + _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex] - leftHandPosition) * + (FINGER_RADIUS - glm::length(_hairPosition[vertexIndex] - leftHandPosition)); + } + if (glm::length(_hairPosition[vertexIndex] - rightHandPosition) < FINGER_RADIUS) { + _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex] - rightHandPosition) * + (FINGER_RADIUS - glm::length(_hairPosition[vertexIndex] - rightHandPosition)); + } + + + // Add a little gravity + _hairPosition[vertexIndex] += HAIR_GRAVITY * rotation * deltaTime; + + // Add linear acceleration of the avatar body + _hairPosition[vertexIndex] -= acceleration * HAIR_ACCELERATION_COUPLING * deltaTime; + + // Add stiffness (like hair care products do) + _hairPosition[vertexIndex] += (_hairOriginalPosition[vertexIndex] - _hairPosition[vertexIndex]) + * powf(1.f - link / HAIR_LINKS, 2.f) * HAIR_STIFFNESS; + + // Add some wind + glm::vec3 wind = WIND_DIRECTION * windIntensity; + _hairPosition[vertexIndex] += wind * deltaTime; + + const float ANGULAR_VELOCITY_MIN = 0.001f; + // Add angular acceleration of the avatar body + if (glm::length(angularVelocity) > ANGULAR_VELOCITY_MIN) { + glm::vec3 yawVector = _hairPosition[vertexIndex]; + yawVector.y = 0.f; + if (glm::length(yawVector) > EPSILON) { + float radius = glm::length(yawVector); + yawVector = glm::normalize(yawVector); + float angle = atan2f(yawVector.x, -yawVector.z) + PI; + glm::vec3 delta = glm::vec3(-1.f, 0.f, 0.f) * glm::angleAxis(angle, glm::vec3(0, 1, 0)); + _hairPosition[vertexIndex] -= delta * radius * angularVelocity.y * HAIR_ANGULAR_VELOCITY_COUPLING * deltaTime; + } + glm::vec3 pitchVector = _hairPosition[vertexIndex]; + pitchVector.x = 0.f; + if (glm::length(pitchVector) > EPSILON) { + float radius = glm::length(pitchVector); + pitchVector = glm::normalize(pitchVector); + float angle = atan2f(pitchVector.y, -pitchVector.z) + PI; + glm::vec3 delta = glm::vec3(0.0f, 1.0f, 0.f) * glm::angleAxis(angle, glm::vec3(1, 0, 0)); + _hairPosition[vertexIndex] -= delta * radius * angularVelocity.x * HAIR_ANGULAR_VELOCITY_COUPLING * deltaTime; + } + glm::vec3 rollVector = _hairPosition[vertexIndex]; + rollVector.z = 0.f; + if (glm::length(rollVector) > EPSILON) { + float radius = glm::length(rollVector); + pitchVector = glm::normalize(rollVector); + float angle = atan2f(rollVector.x, rollVector.y) + PI; + glm::vec3 delta = glm::vec3(-1.0f, 0.0f, 0.f) * glm::angleAxis(angle, glm::vec3(0, 0, 1)); + _hairPosition[vertexIndex] -= delta * radius * angularVelocity.z * HAIR_ANGULAR_VELOCITY_COUPLING * deltaTime; + } + } + + // Iterate length constraints to other links + for (int link = 0; link < HAIR_MAX_CONSTRAINTS; link++) { + if (_hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + link] > -1) { + // If there is a constraint, try to enforce it + glm::vec3 vectorBetween = _hairPosition[_hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + link]] - _hairPosition[vertexIndex]; + _hairPosition[vertexIndex] += glm::normalize(vectorBetween) * (glm::length(vectorBetween) - HAIR_LINK_LENGTH) * CONSTRAINT_RELAXATION * deltaTime; + } + } + // Store start position for next vertlet pass + _hairLastPosition[vertexIndex] = thisPosition; + } + } + } +} + +void Avatar::initializeHair() { + const float FACE_WIDTH = PI / 4.0f; + glm::vec3 thisVertex; + for (int strand = 0; strand < HAIR_STRANDS; strand++) { + float strandAngle = randFloat() * PI; + float azimuth = FACE_WIDTH / 2.0f + (randFloat() * (2.0 * PI - FACE_WIDTH)); + float elevation = PI_OVER_TWO - (randFloat() * 0.75 * PI); + glm::vec3 thisStrand(sinf(azimuth) * cosf(elevation), sinf(elevation), -cosf(azimuth) * cosf(elevation)); + thisStrand *= HEAD_RADIUS; + + for (int link = 0; link < HAIR_LINKS; link++) { + int vertexIndex = strand * HAIR_LINKS + link; + // Clear constraints + for (int link2 = 0; link2 < HAIR_MAX_CONSTRAINTS; link2++) { + _hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + link2] = -1; + } + if (vertexIndex % HAIR_LINKS == 0) { + // start of strand + thisVertex = thisStrand; + } else { + thisVertex+= glm::normalize(thisStrand) * HAIR_LINK_LENGTH; + // Set constraints to vertex before and maybe vertex after in strand + _hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS] = vertexIndex - 1; + if (link < (HAIR_LINKS - 1)) { + _hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + 1] = vertexIndex + 1; + } + } + _hairPosition[vertexIndex] = thisVertex; + _hairLastPosition[vertexIndex] = _hairPosition[vertexIndex]; + _hairOriginalPosition[vertexIndex] = _hairPosition[vertexIndex]; + + _hairQuadDelta[vertexIndex] = glm::vec3(cos(strandAngle) * HAIR_THICKNESS, 0.f, sin(strandAngle) * HAIR_THICKNESS); + _hairQuadDelta[vertexIndex] *= 1.f - (link / HAIR_LINKS); + _hairNormals[vertexIndex] = glm::normalize(randVector()); + if (randFloat() < elevation / PI_OVER_TWO) { + _hairColors[vertexIndex] = HAIR_COLOR1 * ((float)(link + 1) / (float)HAIR_LINKS); + } else { + _hairColors[vertexIndex] = HAIR_COLOR2 * ((float)(link + 1) / (float)HAIR_LINKS); + } + + } + } + qDebug() << "Initialize Hair"; } bool Avatar::shouldRenderHead(const glm::vec3& cameraPosition, RenderMode renderMode) const { diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index a0e41d7122..f20db1019d 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -32,6 +32,10 @@ static const float RESCALING_TOLERANCE = .02f; extern const float CHAT_MESSAGE_SCALE; extern const float CHAT_MESSAGE_HEIGHT; +const int HAIR_STRANDS = 150; // Number of strands of hair +const int HAIR_LINKS = 10; // Number of links in a hair strand +const int HAIR_MAX_CONSTRAINTS = 2; // Hair verlet is connected to at most how many others + enum DriveKeys { FWD = 0, BACK, @@ -145,6 +149,9 @@ public: Q_INVOKABLE glm::quat getJointCombinedRotation(int index) const; Q_INVOKABLE glm::quat getJointCombinedRotation(const QString& name) const; + glm::vec3 getAcceleration() const { return _acceleration; } + glm::vec3 getAngularVelocity() const { return _angularVelocity; } + public slots: void updateCollisionGroups(); @@ -156,6 +163,10 @@ protected: QVector _attachmentModels; float _bodyYawDelta; glm::vec3 _velocity; + glm::vec3 _lastVelocity; + glm::vec3 _acceleration; + glm::vec3 _angularVelocity; + glm::quat _lastOrientation; float _leanScale; float _scale; glm::vec3 _worldUpDirection; @@ -172,6 +183,7 @@ protected: glm::vec3 getBodyFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } glm::quat computeRotationFromBodyToWorldUp(float proportion = 1.0f) const; void setScale(float scale); + void updateAcceleration(float deltaTime); float getSkeletonHeight() const; float getHeadHeight() const; @@ -187,6 +199,18 @@ protected: virtual void renderAttachments(RenderMode renderMode); virtual void updateJointMappings(); + + glm::vec3 _hairPosition[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairOriginalPosition[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairLastPosition[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairQuadDelta[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairNormals[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairColors[HAIR_STRANDS * HAIR_LINKS]; + int _hairIsMoveable[HAIR_STRANDS * HAIR_LINKS]; + int _hairConstraints[HAIR_STRANDS * HAIR_LINKS * 2]; // Hair can link to two others + void renderHair(); + void simulateHair(float deltaTime); + void initializeHair(); private: @@ -198,6 +222,7 @@ private: void renderBillboard(); float getBillboardSize() const; + }; #endif // hifi_Avatar_h diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index a90a65b5ed..7367f64d73 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -194,6 +194,13 @@ void MyAvatar::simulate(float deltaTime) { head->setScale(_scale); head->simulate(deltaTime, true); } + + { + PerformanceTimer perfTimer("MyAvatar::simulate/hair Simulate"); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + simulateHair(deltaTime); + } + } { PerformanceTimer perfTimer("MyAvatar::simulate/ragdoll"); @@ -368,6 +375,7 @@ void MyAvatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { if (!_shouldRender) { return; // exit early } + Avatar::render(cameraPosition, renderMode); // don't display IK constraints in shadow mode @@ -420,6 +428,25 @@ void MyAvatar::renderHeadMouse(int screenWidth, int screenHeight) const { } } +const glm::vec3 HAND_TO_PALM_OFFSET(0.0f, 0.12f, 0.08f); + +glm::vec3 MyAvatar::getLeftPalmPosition() { + glm::vec3 leftHandPosition; + getSkeletonModel().getLeftHandPosition(leftHandPosition); + glm::quat leftRotation; + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftRotation); + leftHandPosition += HAND_TO_PALM_OFFSET * glm::inverse(leftRotation); + return leftHandPosition; +} +glm::vec3 MyAvatar::getRightPalmPosition() { + glm::vec3 rightHandPosition; + getSkeletonModel().getRightHandPosition(rightHandPosition); + glm::quat rightRotation; + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightRotation); + rightHandPosition += HAND_TO_PALM_OFFSET * glm::inverse(rightRotation); + return rightHandPosition; +} + void MyAvatar::setLocalGravity(glm::vec3 gravity) { _motionBehaviors |= AVATAR_MOTION_OBEY_LOCAL_GRAVITY; // Environmental and Local gravities are incompatible. Since Local is being set here @@ -831,6 +858,9 @@ void MyAvatar::renderBody(RenderMode renderMode, float glowLevel) { // Render head so long as the camera isn't inside it if (shouldRenderHead(Application::getInstance()->getCamera()->getPosition(), renderMode)) { getHead()->render(1.0f, modelRenderMode); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + renderHair(); + } } getHand()->render(true, modelRenderMode); } @@ -881,11 +911,19 @@ void MyAvatar::updateOrientation(float deltaTime) { float yaw, pitch, roll; OculusManager::getEulerAngles(yaw, pitch, roll); // ... so they need to be converted to degrees before we do math... - + yaw *= DEGREES_PER_RADIAN; + pitch *= DEGREES_PER_RADIAN; + roll *= DEGREES_PER_RADIAN; + + // Record the angular velocity Head* head = getHead(); - head->setBaseYaw(yaw * DEGREES_PER_RADIAN); - head->setBasePitch(pitch * DEGREES_PER_RADIAN); - head->setBaseRoll(roll * DEGREES_PER_RADIAN); + glm::vec3 angularVelocity(yaw - head->getBaseYaw(), pitch - head->getBasePitch(), roll - head->getBaseRoll()); + head->setAngularVelocity(angularVelocity); + + head->setBaseYaw(yaw); + head->setBasePitch(pitch); + head->setBaseRoll(roll); + } // update the euler angles @@ -974,6 +1012,7 @@ void MyAvatar::updatePosition(float deltaTime) { } else { _position += _velocity * deltaTime; } + updateAcceleration(deltaTime); } // update moving flag based on speed diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e7023b45a1..0ee76c6b45 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -139,7 +139,10 @@ public slots: void setThrust(glm::vec3 newThrust) { _thrust = newThrust; } void updateMotionBehaviorsFromMenu(); - + + glm::vec3 getLeftPalmPosition(); + glm::vec3 getRightPalmPosition(); + signals: void transformChanged(); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index fdb3ce03d8..3b09a1a2ba 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -39,7 +39,6 @@ void SkeletonModel::setJointStates(QVector states) { } clearShapes(); - clearRagdollConstraintsAndPoints(); if (_enableShapes) { buildShapes(); } @@ -505,8 +504,7 @@ void SkeletonModel::renderRagdoll() { // virtual void SkeletonModel::initRagdollPoints() { - assert(_ragdollPoints.size() == 0); - assert(_ragdollConstraints.size() == 0); + clearRagdollConstraintsAndPoints(); // one point for each joint int numJoints = _jointStates.size(); @@ -637,12 +635,15 @@ void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { // compute the default transforms and slam the ragdoll positions accordingly // (which puts the shapes where we want them) - transforms[0] = _jointStates[0].getTransform(); - _ragdollPoints[0]._position = extractTranslation(transforms[0]); - _ragdollPoints[0]._lastPosition = _ragdollPoints[0]._position; - for (int i = 1; i < numJoints; i++) { + for (int i = 0; i < numJoints; i++) { const FBXJoint& joint = geometry.joints.at(i); int parentIndex = joint.parentIndex; + if (parentIndex == -1) { + transforms[i] = _jointStates[i].getTransform(); + _ragdollPoints[i]._position = extractTranslation(transforms[i]); + _ragdollPoints[i]._lastPosition = _ragdollPoints[i]._position; + continue; + } assert(parentIndex != -1); glm::quat modifiedRotation = joint.preRotation * joint.rotation * joint.postRotation; diff --git a/interface/src/location/LocationManager.cpp b/interface/src/location/LocationManager.cpp index 1d783cc8e7..32172d6e38 100644 --- a/interface/src/location/LocationManager.cpp +++ b/interface/src/location/LocationManager.cpp @@ -15,7 +15,7 @@ #include "LocationManager.h" const QString GET_USER_ADDRESS = "/api/v1/users/%1/address"; -const QString GET_PLACE_ADDRESS = "/api/v1/places/%1/address"; +const QString GET_PLACE_ADDRESS = "/api/v1/places/%1"; const QString GET_ADDRESSES = "/api/v1/addresses/%1"; const QString POST_PLACE_CREATE = "/api/v1/places/"; diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 9be556cf62..77e8986297 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -39,9 +39,10 @@ inline float min(float a, float b) { ApplicationOverlay::ApplicationOverlay() : _framebufferObject(NULL), _textureFov(DEFAULT_OCULUS_UI_ANGULAR_SIZE * RADIANS_PER_DEGREE), - _crosshairTexture(0), _alpha(1.0f), - _active(true) { + _active(true), + _crosshairTexture(0) +{ memset(_reticleActive, 0, sizeof(_reticleActive)); memset(_magActive, 0, sizeof(_reticleActive)); @@ -375,7 +376,7 @@ void ApplicationOverlay::renderControllerPointers() { //then disable it. const int MAX_BUTTON_PRESS_TIME = 250 * MSECS_TO_USECS; - if (usecTimestampNow() - pressedTime[index] < MAX_BUTTON_PRESS_TIME) { + if (usecTimestampNow() < pressedTime[index] + MAX_BUTTON_PRESS_TIME) { _magActive[index] = !stateWhenPressed[index]; } } diff --git a/interface/src/ui/ChatWindow.cpp b/interface/src/ui/ChatWindow.cpp index a7d2c212d2..23bf2eafc4 100644 --- a/interface/src/ui/ChatWindow.cpp +++ b/interface/src/ui/ChatWindow.cpp @@ -181,7 +181,7 @@ void ChatWindow::addTimeStamp() { QLabel* timeLabel = new QLabel(timeString); timeLabel->setStyleSheet("color: #333333;" "background-color: white;" - "font-size: 14pt;" + "font-size: 14px;" "padding: 4px;"); timeLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); timeLabel->setAlignment(Qt::AlignLeft); @@ -287,7 +287,7 @@ void ChatWindow::participantsChanged() { "padding-bottom: 2px;" "padding-left: 2px;" "border: 1px solid palette(shadow);" - "font-size: 14pt;" + "font-size: 14px;" "font-weight: bold"); userLabel->setProperty("user", participantName); userLabel->setCursor(Qt::PointingHandCursor); @@ -323,7 +323,7 @@ void ChatWindow::messageReceived(const QXmppMessage& message) { "padding-right: 20px;" "margin: 0px;" "color: #333333;" - "font-size: 14pt;" + "font-size: 14px;" "background-color: rgba(0, 0, 0, 0%);" "border: 0; }" "QMenu{ border: 2px outset gray; }"); diff --git a/interface/src/ui/MetavoxelEditor.cpp b/interface/src/ui/MetavoxelEditor.cpp index a42c84470b..c1deb20111 100644 --- a/interface/src/ui/MetavoxelEditor.cpp +++ b/interface/src/ui/MetavoxelEditor.cpp @@ -771,7 +771,7 @@ int VoxelizationVisitor::visit(MetavoxelInfo& info) { } return DEFAULT_ORDER; } - QRgb closestColor; + QRgb closestColor = QRgb(); float closestDistance = FLT_MAX; for (unsigned int i = 0; i < sizeof(DIRECTION_ROTATIONS) / sizeof(DIRECTION_ROTATIONS[0]); i++) { glm::vec3 rotated = DIRECTION_ROTATIONS[i] * center; diff --git a/interface/src/ui/ModelsBrowser.cpp b/interface/src/ui/ModelsBrowser.cpp index 4296a096a0..203c54d97a 100644 --- a/interface/src/ui/ModelsBrowser.cpp +++ b/interface/src/ui/ModelsBrowser.cpp @@ -89,6 +89,12 @@ ModelsBrowser::ModelsBrowser(ModelType modelsType, QWidget* parent) : _view.setEditTriggers(QAbstractItemView::NoEditTriggers); _view.setRootIsDecorated(false); _view.setModel(_handler->getModel()); + _view.blockSignals(true); + + // Initialize the search bar + _searchBar = new QLineEdit; + _searchBar->setDisabled(true); + connect(_handler, SIGNAL(doneDownloading()), SLOT(enableSearchBar())); } void ModelsBrowser::applyFilter(const QString &filter) { @@ -130,6 +136,11 @@ void ModelsBrowser::resizeView() { } } +void ModelsBrowser::enableSearchBar() { + _view.blockSignals(false); + _searchBar->setEnabled(true); +} + void ModelsBrowser::browse() { QDialog dialog; dialog.setWindowTitle("Browse models"); @@ -138,12 +149,10 @@ void ModelsBrowser::browse() { QGridLayout* layout = new QGridLayout(&dialog); dialog.setLayout(layout); - QLineEdit* searchBar = new QLineEdit(&dialog); - layout->addWidget(searchBar, 0, 0); - + layout->addWidget(_searchBar, 0, 0); layout->addWidget(&_view, 1, 0); dialog.connect(&_view, SIGNAL(doubleClicked(const QModelIndex&)), SLOT(accept())); - connect(searchBar, SIGNAL(textChanged(const QString&)), SLOT(applyFilter(const QString&))); + connect(_searchBar, SIGNAL(textChanged(const QString&)), SLOT(applyFilter(const QString&))); QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); layout->addWidget(buttons, 2, 0); diff --git a/interface/src/ui/ModelsBrowser.h b/interface/src/ui/ModelsBrowser.h index ff273a45bc..3e832c9dbe 100644 --- a/interface/src/ui/ModelsBrowser.h +++ b/interface/src/ui/ModelsBrowser.h @@ -74,9 +74,11 @@ public slots: private slots: void applyFilter(const QString& filter); void resizeView(); + void enableSearchBar(); private: ModelHandler* _handler; + QLineEdit* _searchBar; QTreeView _view; }; diff --git a/interface/src/ui/OAuthWebViewHandler.cpp b/interface/src/ui/OAuthWebViewHandler.cpp index 5b4431bd0f..8ec415584d 100644 --- a/interface/src/ui/OAuthWebViewHandler.cpp +++ b/interface/src/ui/OAuthWebViewHandler.cpp @@ -109,6 +109,8 @@ void OAuthWebViewHandler::displayWebviewForAuthorizationURL(const QUrl& authoriz connect(_activeWebView->page()->networkAccessManager(), &QNetworkAccessManager::sslErrors, this, &OAuthWebViewHandler::handleSSLErrors); + connect(_activeWebView->page()->networkAccessManager(), &QNetworkAccessManager::finished, + this, &OAuthWebViewHandler::handleReplyFinished); connect(_activeWebView.data(), &QWebView::loadFinished, this, &OAuthWebViewHandler::handleLoadFinished); // connect to the destroyed signal so after the web view closes we can start a timer @@ -132,6 +134,14 @@ void OAuthWebViewHandler::handleLoadFinished(bool success) { NodeList::getInstance()->setSessionUUID(QUuid(authQuery.queryItemValue(AUTH_STATE_QUERY_KEY))); _activeWebView->close(); + _activeWebView = NULL; + } +} + +void OAuthWebViewHandler::handleReplyFinished(QNetworkReply* reply) { + if (_activeWebView && reply->error() != QNetworkReply::NoError) { + qDebug() << "Error loading" << reply->url() << "-" << reply->errorString(); + _activeWebView->close(); } } @@ -148,6 +158,7 @@ void OAuthWebViewHandler::handleURLChanged(const QUrl& url) { _activeWebView->show(); } else if (url.toString() == DEFAULT_NODE_AUTH_URL.toString() + "/login") { // this is a login request - we're going to close the webview and signal the AccountManager that we need a login + qDebug() << "data-server replied with login request. Signalling that login is required to proceed with OAuth."; _activeWebView->close(); AccountManager::getInstance().checkAndSignalForAccessToken(); } diff --git a/interface/src/ui/OAuthWebViewHandler.h b/interface/src/ui/OAuthWebViewHandler.h index 8f0c01c90d..1a95f17dfd 100644 --- a/interface/src/ui/OAuthWebViewHandler.h +++ b/interface/src/ui/OAuthWebViewHandler.h @@ -31,6 +31,7 @@ public slots: private slots: void handleSSLErrors(QNetworkReply* networkReply, const QList& errorList); void handleLoadFinished(bool success); + void handleReplyFinished(QNetworkReply* reply); void handleWebViewDestroyed(QObject* destroyedObject); void handleURLChanged(const QUrl& url); private: diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 5e6c6984eb..55d3360884 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -29,6 +29,7 @@ PreferencesDialog::PreferencesDialog(QWidget* parent, Qt::WindowFlags flags) : F connect(ui.buttonBrowseHead, &QPushButton::clicked, this, &PreferencesDialog::openHeadModelBrowser); connect(ui.buttonBrowseBody, &QPushButton::clicked, this, &PreferencesDialog::openBodyModelBrowser); connect(ui.buttonBrowseLocation, &QPushButton::clicked, this, &PreferencesDialog::openSnapshotLocationBrowser); + connect(ui.buttonBrowseScriptsLocation, &QPushButton::clicked, this, &PreferencesDialog::openScriptsLocationBrowser); connect(ui.buttonReloadDefaultScripts, &QPushButton::clicked, Application::getInstance(), &Application::loadDefaultScripts); } @@ -72,13 +73,32 @@ void PreferencesDialog::openBodyModelBrowser() { void PreferencesDialog::openSnapshotLocationBrowser() { setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); + show(); + QString dir = QFileDialog::getExistingDirectory(this, tr("Snapshots Location"), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (!dir.isNull() && !dir.isEmpty()) { ui.snapshotLocationEdit->setText(dir); } + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + show(); +} + +void PreferencesDialog::openScriptsLocationBrowser() { + setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); + show(); + + QString dir = QFileDialog::getExistingDirectory(this, tr("Scripts Location"), + ui.scriptsLocationEdit->text(), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!dir.isNull() && !dir.isEmpty()) { + ui.scriptsLocationEdit->setText(dir); + } + + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + show(); } void PreferencesDialog::resizeEvent(QResizeEvent *resizeEvent) { @@ -118,6 +138,8 @@ void PreferencesDialog::loadPreferences() { ui.snapshotLocationEdit->setText(menuInstance->getSnapshotsLocation()); + ui.scriptsLocationEdit->setText(menuInstance->getScriptsLocation()); + ui.pupilDilationSlider->setValue(myAvatar->getHead()->getPupilDilation() * ui.pupilDilationSlider->maximum()); @@ -180,6 +202,10 @@ void PreferencesDialog::savePreferences() { Menu::getInstance()->setSnapshotsLocation(ui.snapshotLocationEdit->text()); } + if (!ui.scriptsLocationEdit->text().isEmpty() && QDir(ui.scriptsLocationEdit->text()).exists()) { + Menu::getInstance()->setScriptsLocation(ui.scriptsLocationEdit->text()); + } + myAvatar->getHead()->setPupilDilation(ui.pupilDilationSlider->value() / (float)ui.pupilDilationSlider->maximum()); myAvatar->setLeanScale(ui.leanScaleSpin->value()); myAvatar->setClampedTargetScale(ui.avatarScaleSpin->value()); diff --git a/interface/src/ui/PreferencesDialog.h b/interface/src/ui/PreferencesDialog.h index c52986cc5c..0573304eda 100644 --- a/interface/src/ui/PreferencesDialog.h +++ b/interface/src/ui/PreferencesDialog.h @@ -42,6 +42,7 @@ private slots: void setHeadUrl(QString modelUrl); void setSkeletonUrl(QString modelUrl); void openSnapshotLocationBrowser(); + void openScriptsLocationBrowser(); }; diff --git a/interface/src/ui/RunningScriptsWidget.cpp b/interface/src/ui/RunningScriptsWidget.cpp index 9bef454829..8a7ebcbfd4 100644 --- a/interface/src/ui/RunningScriptsWidget.cpp +++ b/interface/src/ui/RunningScriptsWidget.cpp @@ -3,6 +3,7 @@ // interface/src/ui // // Created by Mohammed Nafees on 03/28/2014. +// Updated by Ryan Huffman on 05/13/2014. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -12,18 +13,27 @@ #include "ui_runningScriptsWidget.h" #include "RunningScriptsWidget.h" +#include #include #include #include #include #include "Application.h" +#include "Menu.h" +#include "ScriptsModel.h" + RunningScriptsWidget::RunningScriptsWidget(QWidget* parent) : FramelessDialog(parent, 0, POSITION_LEFT), - ui(new Ui::RunningScriptsWidget) { + ui(new Ui::RunningScriptsWidget), + _signalMapper(this), + _proxyModel(this), + _scriptsModel(this) { ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose, false); + setAllowResize(false); ui->hideWidgetButton->setIcon(QIcon(Application::resourcesPath() + "images/close.svg")); @@ -31,17 +41,24 @@ RunningScriptsWidget::RunningScriptsWidget(QWidget* parent) : ui->stopAllButton->setIcon(QIcon(Application::resourcesPath() + "images/stop.svg")); ui->loadScriptButton->setIcon(QIcon(Application::resourcesPath() + "images/plus-white.svg")); - _runningScriptsTable = new ScriptsTableWidget(ui->runningScriptsTableWidget); - _runningScriptsTable->setColumnCount(2); - _runningScriptsTable->setColumnWidth(0, 245); - _runningScriptsTable->setColumnWidth(1, 22); - connect(_runningScriptsTable, &QTableWidget::cellClicked, this, &RunningScriptsWidget::stopScript); + ui->recentlyLoadedScriptsArea->hide(); + + ui->filterLineEdit->installEventFilter(this); + + connect(&_proxyModel, &QSortFilterProxyModel::modelReset, + this, &RunningScriptsWidget::selectFirstInList); + + _proxyModel.setSourceModel(&_scriptsModel); + _proxyModel.sort(0, Qt::AscendingOrder); + _proxyModel.setDynamicSortFilter(true); + ui->scriptListView->setModel(&_proxyModel); + + connect(ui->filterLineEdit, &QLineEdit::textChanged, this, &RunningScriptsWidget::updateFileFilter); + connect(ui->scriptListView, &QListView::doubleClicked, this, &RunningScriptsWidget::loadScriptFromList); _recentlyLoadedScriptsTable = new ScriptsTableWidget(ui->recentlyLoadedScriptsTableWidget); _recentlyLoadedScriptsTable->setColumnCount(1); _recentlyLoadedScriptsTable->setColumnWidth(0, 265); - connect(_recentlyLoadedScriptsTable, &QTableWidget::cellClicked, - this, &RunningScriptsWidget::loadScript); connect(ui->hideWidgetButton, &QPushButton::clicked, Application::getInstance(), &Application::toggleRunningScriptsWidget); @@ -51,59 +68,121 @@ RunningScriptsWidget::RunningScriptsWidget(QWidget* parent) : this, &RunningScriptsWidget::allScriptsStopped); connect(ui->loadScriptButton, &QPushButton::clicked, Application::getInstance(), &Application::loadDialog); + connect(&_signalMapper, SIGNAL(mapped(QString)), Application::getInstance(), SLOT(stopScript(const QString&))); } RunningScriptsWidget::~RunningScriptsWidget() { delete ui; } +void RunningScriptsWidget::updateFileFilter(const QString& filter) { + QRegExp regex("^.*" + QRegExp::escape(filter) + ".*$", Qt::CaseInsensitive); + _proxyModel.setFilterRegExp(regex); + selectFirstInList(); +} + +void RunningScriptsWidget::loadScriptFromList(const QModelIndex& index) { + QVariant scriptFile = _proxyModel.data(index, ScriptsModel::ScriptPath); + Application::getInstance()->loadScript(scriptFile.toString(), false, false); +} + +void RunningScriptsWidget::loadSelectedScript() { + QModelIndex selectedIndex = ui->scriptListView->currentIndex(); + if (selectedIndex.isValid()) { + loadScriptFromList(selectedIndex); + } +} + void RunningScriptsWidget::setBoundary(const QRect& rect) { _boundary = rect; } void RunningScriptsWidget::setRunningScripts(const QStringList& list) { - _runningScriptsTable->setRowCount(list.size()); + setUpdatesEnabled(false); + QLayoutItem* widget; + while ((widget = ui->scrollAreaWidgetContents->layout()->takeAt(0)) != NULL) { + delete widget->widget(); + delete widget; + } + const int CLOSE_ICON_HEIGHT = 12; + for (int i = 0; i < list.size(); i++) { + QWidget* row = new QWidget(ui->scrollAreaWidgetContents); + row->setLayout(new QHBoxLayout(row)); + + QUrl url = QUrl(list.at(i)); + QLabel* name = new QLabel(url.fileName(), row); + QPushButton* closeButton = new QPushButton(row); + closeButton->setFlat(true); + closeButton->setIcon( + QIcon(QPixmap(Application::resourcesPath() + "images/kill-script.svg").scaledToHeight(CLOSE_ICON_HEIGHT))); + closeButton->setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred)); + closeButton->setStyleSheet("border: 0;"); + closeButton->setCursor(Qt::PointingHandCursor); + + connect(closeButton, SIGNAL(clicked()), &_signalMapper, SLOT(map())); + _signalMapper.setMapping(closeButton, url.toString()); + + row->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); + + row->layout()->setContentsMargins(4, 4, 4, 4); + row->layout()->setSpacing(0); + + row->layout()->addWidget(name); + row->layout()->addWidget(closeButton); + + row->setToolTip(url.toString()); + + QFrame* line = new QFrame(row); + line->setFrameShape(QFrame::HLine); + line->setStyleSheet("color: #E1E1E1; margin-left: 6px; margin-right: 6px;"); + + ui->scrollAreaWidgetContents->layout()->addWidget(row); + ui->scrollAreaWidgetContents->layout()->addWidget(line); + } + ui->noRunningScriptsLabel->setVisible(list.isEmpty()); - ui->currentlyRunningLabel->setVisible(!list.isEmpty()); - ui->runningScriptsTableWidget->setVisible(!list.isEmpty()); ui->reloadAllButton->setVisible(!list.isEmpty()); ui->stopAllButton->setVisible(!list.isEmpty()); - const int CLOSE_ICON_HEIGHT = 12; + ui->scrollAreaWidgetContents->updateGeometry(); + setUpdatesEnabled(true); + Application::processEvents(); + repaint(); +} - for (int i = 0; i < list.size(); ++i) { - QTableWidgetItem *scriptName = new QTableWidgetItem; - scriptName->setText(QFileInfo(list.at(i)).fileName()); - scriptName->setToolTip(list.at(i)); - scriptName->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); - QTableWidgetItem *closeIcon = new QTableWidgetItem; - closeIcon->setIcon(QIcon(QPixmap(Application::resourcesPath() + "images/kill-script.svg").scaledToHeight(CLOSE_ICON_HEIGHT))); - - _runningScriptsTable->setItem(i, 0, scriptName); - _runningScriptsTable->setItem(i, 1, closeIcon); +void RunningScriptsWidget::showEvent(QShowEvent* event) { + if (!event->spontaneous()) { + ui->filterLineEdit->setFocus(); } - const int RUNNING_SCRIPTS_TABLE_LEFT_MARGIN = 12; - const int RECENTLY_LOADED_TOP_MARGIN = 61; - const int RECENTLY_LOADED_LABEL_TOP_MARGIN = 19; + FramelessDialog::showEvent(event); +} - int y = ui->runningScriptsTableWidget->y() + RUNNING_SCRIPTS_TABLE_LEFT_MARGIN; - for (int i = 0; i < _runningScriptsTable->rowCount(); ++i) { - y += _runningScriptsTable->rowHeight(i); +void RunningScriptsWidget::selectFirstInList() { + if (_proxyModel.rowCount() > 0) { + ui->scriptListView->setCurrentIndex(_proxyModel.index(0, 0)); + } +} + +bool RunningScriptsWidget::eventFilter(QObject* sender, QEvent* event) { + if (sender == ui->filterLineEdit) { + if (event->type() != QEvent::KeyPress) { + return false; + } + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { + QModelIndex selectedIndex = ui->scriptListView->currentIndex(); + if (selectedIndex.isValid()) { + loadScriptFromList(selectedIndex); + } + event->accept(); + return true; + } + return false; } - ui->runningScriptsTableWidget->resize(ui->runningScriptsTableWidget->width(), y - RUNNING_SCRIPTS_TABLE_LEFT_MARGIN); - _runningScriptsTable->resize(_runningScriptsTable->width(), y - RUNNING_SCRIPTS_TABLE_LEFT_MARGIN); - ui->reloadAllButton->move(ui->reloadAllButton->x(), y); - ui->stopAllButton->move(ui->stopAllButton->x(), y); - ui->recentlyLoadedLabel->move(ui->recentlyLoadedLabel->x(), - ui->stopAllButton->y() + ui->stopAllButton->height() + RECENTLY_LOADED_TOP_MARGIN); - ui->recentlyLoadedScriptsTableWidget->move(ui->recentlyLoadedScriptsTableWidget->x(), - ui->recentlyLoadedLabel->y() + RECENTLY_LOADED_LABEL_TOP_MARGIN); - - - createRecentlyLoadedScriptsTable(); + return FramelessDialog::eventFilter(sender, event); } void RunningScriptsWidget::keyPressEvent(QKeyEvent *keyEvent) { @@ -114,79 +193,10 @@ void RunningScriptsWidget::keyPressEvent(QKeyEvent *keyEvent) { } } -void RunningScriptsWidget::paintEvent(QPaintEvent* event) { - QPainter painter(this); - painter.setPen(QColor::fromRgb(225, 225, 225)); // #e1e1e1 - - if (ui->currentlyRunningLabel->isVisible()) { - // line below the 'Currently Running' label - painter.drawLine(36, ui->currentlyRunningLabel->y() + ui->currentlyRunningLabel->height(), - 300, ui->currentlyRunningLabel->y() + ui->currentlyRunningLabel->height()); - } - - if (ui->recentlyLoadedLabel->isVisible()) { - // line below the 'Recently loaded' label - painter.drawLine(36, ui->recentlyLoadedLabel->y() + ui->recentlyLoadedLabel->height(), - 300, ui->recentlyLoadedLabel->y() + ui->recentlyLoadedLabel->height()); - } - - painter.end(); -} - void RunningScriptsWidget::scriptStopped(const QString& scriptName) { - _recentlyLoadedScripts.prepend(scriptName); -} - -void RunningScriptsWidget::stopScript(int row, int column) { - if (column == 1) { // make sure the user has clicked on the close icon - _lastStoppedScript = _runningScriptsTable->item(row, 0)->toolTip(); - emit stopScriptName(_runningScriptsTable->item(row, 0)->toolTip()); - } -} - -void RunningScriptsWidget::loadScript(int row, int column) { - Application::getInstance()->loadScript(_recentlyLoadedScriptsTable->item(row, column)->toolTip()); + // _recentlyLoadedScripts.prepend(scriptName); } void RunningScriptsWidget::allScriptsStopped() { Application::getInstance()->stopAllScripts(); } - -void RunningScriptsWidget::createRecentlyLoadedScriptsTable() { - if (!_recentlyLoadedScripts.contains(_lastStoppedScript) && !_lastStoppedScript.isEmpty()) { - _recentlyLoadedScripts.prepend(_lastStoppedScript); - _lastStoppedScript = ""; - } - - for (int i = 0; i < _recentlyLoadedScripts.size(); ++i) { - if (Application::getInstance()->getRunningScripts().contains(_recentlyLoadedScripts.at(i))) { - _recentlyLoadedScripts.removeOne(_recentlyLoadedScripts.at(i)); - } - } - - ui->recentlyLoadedLabel->setVisible(!_recentlyLoadedScripts.isEmpty()); - ui->recentlyLoadedScriptsTableWidget->setVisible(!_recentlyLoadedScripts.isEmpty()); - ui->recentlyLoadedInstruction->setVisible(!_recentlyLoadedScripts.isEmpty()); - - int limit = _recentlyLoadedScripts.size() > 9 ? 9 : _recentlyLoadedScripts.size(); - _recentlyLoadedScriptsTable->setRowCount(limit); - for (int i = 0; i < limit; i++) { - QTableWidgetItem *scriptName = new QTableWidgetItem; - scriptName->setText(QString::number(i + 1) + ". " + QFileInfo(_recentlyLoadedScripts.at(i)).fileName()); - scriptName->setToolTip(_recentlyLoadedScripts.at(i)); - scriptName->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); - - _recentlyLoadedScriptsTable->setItem(i, 0, scriptName); - } - - int y = ui->recentlyLoadedScriptsTableWidget->y() + 15; - for (int i = 0; i < _recentlyLoadedScriptsTable->rowCount(); ++i) { - y += _recentlyLoadedScriptsTable->rowHeight(i); - } - - ui->recentlyLoadedInstruction->setGeometry(36, y, - ui->recentlyLoadedInstruction->width(), - ui->recentlyLoadedInstruction->height()); - - repaint(); -} diff --git a/interface/src/ui/RunningScriptsWidget.h b/interface/src/ui/RunningScriptsWidget.h index 14a1f4a58e..6810aca487 100644 --- a/interface/src/ui/RunningScriptsWidget.h +++ b/interface/src/ui/RunningScriptsWidget.h @@ -3,6 +3,7 @@ // interface/src/ui // // Created by Mohammed Nafees on 03/28/2014. +// Updated by Ryan Huffman on 05/13/2014. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -12,6 +13,11 @@ #ifndef hifi_RunningScriptsWidget_h #define hifi_RunningScriptsWidget_h +#include +#include +#include + +#include "ScriptsModel.h" #include "FramelessDialog.h" #include "ScriptsTableWidget.h" @@ -32,27 +38,31 @@ signals: void stopScriptName(const QString& name); protected: + virtual bool eventFilter(QObject* sender, QEvent* event); + virtual void keyPressEvent(QKeyEvent* event); - virtual void paintEvent(QPaintEvent* event); + virtual void showEvent(QShowEvent* event); public slots: void scriptStopped(const QString& scriptName); void setBoundary(const QRect& rect); private slots: - void stopScript(int row, int column); - void loadScript(int row, int column); void allScriptsStopped(); + void updateFileFilter(const QString& filter); + void loadScriptFromList(const QModelIndex& index); + void loadSelectedScript(); + void selectFirstInList(); private: Ui::RunningScriptsWidget* ui; - ScriptsTableWidget* _runningScriptsTable; + QSignalMapper _signalMapper; + QSortFilterProxyModel _proxyModel; + ScriptsModel _scriptsModel; ScriptsTableWidget* _recentlyLoadedScriptsTable; QStringList _recentlyLoadedScripts; QString _lastStoppedScript; QRect _boundary; - - void createRecentlyLoadedScriptsTable(); }; #endif // hifi_RunningScriptsWidget_h diff --git a/interface/src/ui/ScriptsTableWidget.cpp b/interface/src/ui/ScriptsTableWidget.cpp index 95acca052c..7b4f9e6b1f 100644 --- a/interface/src/ui/ScriptsTableWidget.cpp +++ b/interface/src/ui/ScriptsTableWidget.cpp @@ -23,7 +23,7 @@ ScriptsTableWidget::ScriptsTableWidget(QWidget* parent) : setShowGrid(false); setSelectionMode(QAbstractItemView::NoSelection); setEditTriggers(QAbstractItemView::NoEditTriggers); - setStyleSheet("QTableWidget { background: transparent; color: #333333; } QToolTip { color: #000000; background: #f9f6e4; padding: 2px; }"); + setStyleSheet("QTableWidget { border: none; background: transparent; color: #333333; } QToolTip { color: #000000; background: #f9f6e4; padding: 2px; }"); setToolTipDuration(200); setWordWrap(true); setGeometry(0, 0, parent->width(), parent->height()); diff --git a/interface/ui/preferencesDialog.ui b/interface/ui/preferencesDialog.ui index f00d7c4788..95678bf6f8 100644 --- a/interface/ui/preferencesDialog.ui +++ b/interface/ui/preferencesDialog.ui @@ -154,9 +154,9 @@ color: #0e7077 0 - -1002 - 477 - 1386 + 0 + 600 + 1091 @@ -645,6 +645,112 @@ color: #0e7077 + + + + + 0 + 0 + + + + + 0 + 30 + + + + + Arial + 16 + + + + color: #0e7077 + + + Load scripts from this directory: + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + 0 + + + snapshotLocationEdit + + + + + + + + + + 0 + 0 + + + + + Arial + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + 30 + 30 + + + + + + + + + + + 30 + 30 + + + + + + @@ -660,7 +766,7 @@ color: #0e7077 background: #0e7077; color: #fff; border-radius: 4px; -font: bold 14pt; +font: bold 14px; padding: 10px;margin-top:10px @@ -796,7 +902,7 @@ padding: 10px;margin-top:10px - + 0 @@ -831,7 +937,7 @@ padding: 10px;margin-top:10px - + Arial diff --git a/interface/ui/runningScriptsWidget.ui b/interface/ui/runningScriptsWidget.ui index 6cb23f4c89..71efe6970a 100644 --- a/interface/ui/runningScriptsWidget.ui +++ b/interface/ui/runningScriptsWidget.ui @@ -6,8 +6,8 @@ 0 0 - 323 - 894 + 324 + 971 @@ -21,240 +21,623 @@ QWidget { background: #f7f7f7; } - - - - 37 - 29 - 251 - 20 - + + + 20 - - color: #0e7077; -font-size: 20pt; + + 20 + + + 20 + + + 20 + + + + + + 0 + + + 0 + + + 0 + + + + + color: #0e7077; +font-size: 20px; background: transparent; - - - <html><head/><body><p><span style=" font-size:18pt;">Running Scripts</span></p></body></html> - - - 0 - - - -1 - - - - - - 36 - 110 - 270 - 20 - - - - color: #0e7077; -font: bold 14pt; + + + <html><head/><body><p><span style=" font-size:18px;">Running Scripts</span></p></body></html> + + + 0 + + + -1 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + PointingHandCursor + + + border: 0 + + + + + + + 16 + 16 + + + + true + + + + + + + + + + + 0 + 1 + + + + + 0 + 141 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Helvetica,Arial,sans-serif + 16 + 75 + false + true + + + + color: #0e7077; +font: bold 16px; background: transparent; - - - <html><head/><body><p><span style=" font-weight:600;">Currently running</span></p></body></html> - - - - - - 36 - 270 - 111 - 35 - - - - PointingHandCursor - - - false - - - background: #0e7077; + + + <html><head/><body><p><span style=" font-weight:600;">Currently running</span></p></body></html> + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 0 + 8 + + + + + + + + + 0 + 0 + + + + + 24 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 111 + 35 + + + + PointingHandCursor + + + false + + + background: #0e7077; color: #fff; border-radius: 4px; -font: bold 14pt; +font: bold 14px; padding-top: 3px; - - - Reload all - - - - - - 160 - 270 - 93 - 35 - - - - PointingHandCursor - - - background: #0e7077; + + + Reload all + + + + + + + + 0 + 0 + + + + + 111 + 35 + + + + PointingHandCursor + + + background: #0e7077; color: #fff; border-radius: 4px; -font: bold 14pt; +font: bold 14px; padding-top: 3px; - - - Stop all - - - - - - 36 - 320 - 265 - 20 - - - - color: #0e7077; -font: bold 14pt; - - - <html><head/><body><p><span style=" font-weight:600;">Recently loaded</span></p></body></html> - - - - - - 36 - 630 - 211 - 41 - - - - color: #95a5a6; -font-size: 14pt; - - - (click a script to load and run it) - - - true - - - - - - 285 - 29 - 16 - 16 - - - - PointingHandCursor - - - - - - - 16 - 16 - - - - true - - - - - - 36 - 110 - 271 - 51 - - - - font: 14pt; - - - There are no scripts currently running. - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - 30 - 340 - 272 - 280 - - - - background: transparent; -font-size: 14pt; - - - - - - 30 - 128 - 272 - 161 - - - - background: transparent; -font-size: 14pt; - - - - - - 36 - 70 - 111 - 35 - - - - PointingHandCursor - - - background: #0e7077; + + + Stop all + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + + + font: 14px; + + + There are no scripts currently running. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + Helvetica,Arial,sans-serif + 14 + + + + Qt::LeftToRight + + + margin: 0; + + + QFrame::NoFrame + + + 0 + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + 0 + 0 + 269 + 16 + + + + + 0 + 0 + + + + font-size: 14px; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + true + + + + 0 + 100 + + + + + 16777215 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + color: #0e7077; +font: bold 16px; + + + <html><head/><body><p><span style=" font-weight:600;">Recently loaded</span></p></body></html> + + + + + + + font: 14px; + + + There are no recently loaded scripts. + + + + + + + + 0 + 1 + + + + + 284 + 0 + + + + background: transparent; +font-size: 14px; + + runningScriptsList + + + + + + + + + + 0 + 2 + + + + + 0 + 300 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 15 + + + + + color: #0e7077; +font: bold 16px; + + + Scripts + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 111 + 35 + + + + PointingHandCursor + + + background: #0e7077; color: #fff; border-radius: 4px; -font: bold 14pt; +font: bold 14px; padding-top: 3px; - - - Load script - - - widgetTitle - currentlyRunningLabel - recentlyLoadedLabel - recentlyLoadedInstruction - hideWidgetButton - recentlyLoadedScriptsTableWidget - runningScriptsTableWidget - noRunningScriptsLabel - reloadAllButton - stopAllButton - loadScriptButton + + + Load script + + + + + + + + + + border: 1px solid rgb(128, 128, 128); +border-radius: 2px; +padding: 4px; +background-color: white; + + + + + + filter + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 6 + + + + + + + + QListView { + border: 1px solid rgb(128, 128, 128); + border-radius: 2px; +} +QListView::item { + padding-top: 2px; + padding-bottom: 2px; +} + + + QFrame::Box + + + QFrame::Plain + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAsNeeded + + + + + + + diff --git a/interface/ui/shareSnapshot.ui b/interface/ui/shareSnapshot.ui index df7fc4939f..19e0772f13 100644 --- a/interface/ui/shareSnapshot.ui +++ b/interface/ui/shareSnapshot.ui @@ -277,7 +277,7 @@ padding-left:20px; <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Helvetica'; font-size:14pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'Helvetica'; font-size:14px; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> diff --git a/libraries/audio/src/AudioRingBuffer.cpp b/libraries/audio/src/AudioRingBuffer.cpp index 10aa6f47cf..07b48fe88d 100644 --- a/libraries/audio/src/AudioRingBuffer.cpp +++ b/libraries/audio/src/AudioRingBuffer.cpp @@ -126,7 +126,6 @@ qint64 AudioRingBuffer::writeSamples(const int16_t* source, qint64 maxSamples) { qint64 AudioRingBuffer::writeData(const char* data, qint64 maxSize) { // make sure we have enough bytes left for this to be the right amount of audio // otherwise we should not copy that data, and leave the buffer pointers where they are - int samplesToCopy = std::min((quint64)(maxSize / sizeof(int16_t)), (quint64)_sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); diff --git a/libraries/audio/src/PositionalAudioRingBuffer.cpp b/libraries/audio/src/PositionalAudioRingBuffer.cpp index e11d73358c..546ed97fe2 100644 --- a/libraries/audio/src/PositionalAudioRingBuffer.cpp +++ b/libraries/audio/src/PositionalAudioRingBuffer.cpp @@ -212,7 +212,7 @@ bool PositionalAudioRingBuffer::shouldBeAddedToMix() { } return false; - } else if (samplesAvailable() < samplesPerFrame) { + } else if (samplesAvailable() < (unsigned int)samplesPerFrame) { // if the buffer doesn't have a full frame of samples to take for mixing, it is starved _isStarved = true; diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 33b7fca0ef..56fb566d6a 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1897,7 +1897,20 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) } geometry.attachments.append(attachment); } - + + // Add sitting points + QVariantHash sittingPoints = mapping.value("sit").toHash(); + for (QVariantHash::const_iterator it = sittingPoints.constBegin(); it != sittingPoints.constEnd(); it++) { + SittingPoint sittingPoint; + sittingPoint.name = it.key(); + + QVariantList properties = it->toList(); + sittingPoint.position = parseVec3(properties.at(0).toString()); + sittingPoint.rotation = glm::quat(glm::radians(parseVec3(properties.at(1).toString()))); + + geometry.sittingPoints.append(sittingPoint); + } + return geometry; } diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 4c93f3dc5e..c336252574 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -182,6 +182,14 @@ public: glm::vec3 scale; }; +/// A point where an avatar can sit +class SittingPoint { +public: + QString name; + glm::vec3 position; // relative postion + glm::quat rotation; // relative orientation +}; + /// A set of meshes extracted from an FBX document. class FBXGeometry { public: @@ -209,6 +217,8 @@ public: glm::vec3 palmDirection; + QVector sittingPoints; + glm::vec3 neckPivot; Extents bindExtents; diff --git a/libraries/metavoxels/src/DatagramSequencer.cpp b/libraries/metavoxels/src/DatagramSequencer.cpp index f1f60e4d87..a75a4bf95b 100644 --- a/libraries/metavoxels/src/DatagramSequencer.cpp +++ b/libraries/metavoxels/src/DatagramSequencer.cpp @@ -23,6 +23,9 @@ const int MAX_DATAGRAM_SIZE = MAX_PACKET_SIZE; const int DEFAULT_MAX_PACKET_SIZE = 3000; +// the default slow-start threshold, which will be lowered quickly when we first encounter packet loss +const float DEFAULT_SLOW_START_THRESHOLD = 1000.0f; + DatagramSequencer::DatagramSequencer(const QByteArray& datagramHeader, QObject* parent) : QObject(parent), _outgoingPacketStream(&_outgoingPacketData, QIODevice::WriteOnly), @@ -37,7 +40,12 @@ DatagramSequencer::DatagramSequencer(const QByteArray& datagramHeader, QObject* _incomingPacketStream(&_incomingPacketData, QIODevice::ReadOnly), _inputStream(_incomingPacketStream), _receivedHighPriorityMessages(0), - _maxPacketSize(DEFAULT_MAX_PACKET_SIZE) { + _maxPacketSize(DEFAULT_MAX_PACKET_SIZE), + _packetsPerGroup(1.0f), + _packetsToWrite(0.0f), + _slowStartThreshold(DEFAULT_SLOW_START_THRESHOLD), + _packetRateIncreasePacketNumber(0), + _packetRateDecreasePacketNumber(0) { _outgoingPacketStream.setByteOrder(QDataStream::LittleEndian); _incomingDatagramStream.setByteOrder(QDataStream::LittleEndian); @@ -71,6 +79,33 @@ ReliableChannel* DatagramSequencer::getReliableInputChannel(int index) { return channel; } +int DatagramSequencer::startPacketGroup(int desiredPackets) { + // figure out how much data we have enqueued and increase the number of packets desired + int totalAvailable = 0; + foreach (ReliableChannel* channel, _reliableOutputChannels) { + totalAvailable += channel->getBytesAvailable(); + } + desiredPackets += (totalAvailable / _maxPacketSize); + + // increment our packet counter and subtract/return the integer portion + _packetsToWrite += _packetsPerGroup; + int wholePackets = (int)_packetsToWrite; + _packetsToWrite -= wholePackets; + wholePackets = qMin(wholePackets, desiredPackets); + + // if we don't want to send any more, push out the rate increase number past the group + if (desiredPackets <= _packetsPerGroup) { + _packetRateIncreasePacketNumber = _outgoingPacketNumber + wholePackets + 1; + } + + // likewise, if we're only sending one packet, don't let its loss cause rate decrease + if (wholePackets == 1) { + _packetRateDecreasePacketNumber = _outgoingPacketNumber + 2; + } + + return wholePackets; +} + Bitstream& DatagramSequencer::startPacket() { // start with the list of acknowledgements _outgoingPacketStream << (quint32)_receiveRecords.size(); @@ -172,7 +207,10 @@ void DatagramSequencer::receivedDatagram(const QByteArray& datagram) { if (index < 0 || index >= _sendRecords.size()) { continue; } - QList::iterator it = _sendRecords.begin() + index; + QList::iterator it = _sendRecords.begin(); + for (int i = 0; i < index; i++) { + sendRecordLost(*it++); + } sendRecordAcknowledged(*it); emit sendAcknowledged(index); _sendRecords.erase(_sendRecords.begin(), it + 1); @@ -253,6 +291,28 @@ void DatagramSequencer::sendRecordAcknowledged(const SendRecord& record) { foreach (const ChannelSpan& span, record.spans) { getReliableOutputChannel(span.channel)->spanAcknowledged(span); } + + // increase the packet rate with every ack until we pass the slow start threshold; then, every round trip + if (record.packetNumber >= _packetRateIncreasePacketNumber) { + if (_packetsPerGroup >= _slowStartThreshold) { + _packetRateIncreasePacketNumber = _outgoingPacketNumber + 1; + } + _packetsPerGroup += 1.0f; + } +} + +void DatagramSequencer::sendRecordLost(const SendRecord& record) { + // notify the channels of their lost spans + foreach (const ChannelSpan& span, record.spans) { + getReliableOutputChannel(span.channel)->spanLost(record.packetNumber, _outgoingPacketNumber + 1); + } + + // halve the rate and remember as threshold + if (record.packetNumber >= _packetRateDecreasePacketNumber) { + _packetsPerGroup = qMax(_packetsPerGroup * 0.5f, 1.0f); + _slowStartThreshold = _packetsPerGroup; + _packetRateDecreasePacketNumber = _outgoingPacketNumber + 1; + } } void DatagramSequencer::appendReliableData(int bytes, QVector& spans) { @@ -520,7 +580,9 @@ int SpanList::set(int offset, int length) { // look for an intersection within the list int position = 0; - for (QList::iterator it = _spans.begin(); it != _spans.end(); it++) { + for (int i = 0; i < _spans.size(); i++) { + QList::iterator it = _spans.begin() + i; + // if we intersect the unset portion, contract it position += it->unset; if (offset <= position) { @@ -530,16 +592,20 @@ int SpanList::set(int offset, int length) { // if we continue into the set portion, expand it and consume following spans int extra = offset + length - position; if (extra >= 0) { - int amount = setSpans(it + 1, extra); - it->set += amount; - _totalSet += amount; - + extra -= it->set; + it->set += remove; + _totalSet += remove; + if (extra > 0) { + int amount = setSpans(it + 1, extra); + _spans[i].set += amount; + _totalSet += amount; + } // otherwise, insert a new span } else { - Span span = { it->unset, length + extra }; - _spans.insert(it, span); + Span span = { it->unset, length }; it->unset = -extra; - _totalSet += span.set; + _spans.insert(it, span); + _totalSet += length; } return 0; } @@ -548,9 +614,11 @@ int SpanList::set(int offset, int length) { position += it->set; if (offset <= position) { int extra = offset + length - position; - int amount = setSpans(it + 1, extra); - it->set += amount; - _totalSet += amount; + if (extra > 0) { + int amount = setSpans(it + 1, extra); + _spans[i].set += amount; + _totalSet += amount; + } return 0; } } @@ -619,6 +687,7 @@ ReliableChannel::ReliableChannel(DatagramSequencer* sequencer, int index, bool o _priority(1.0f), _offset(0), _writePosition(0), + _writePositionResetPacketNumber(0), _messagesEnabled(true) { _buffer.open(output ? QIODevice::WriteOnly : QIODevice::ReadOnly); @@ -629,67 +698,76 @@ ReliableChannel::ReliableChannel(DatagramSequencer* sequencer, int index, bool o } void ReliableChannel::writeData(QDataStream& out, int bytes, QVector& spans) { - // find out how many spans we want to write - int spanCount = 0; - int remainingBytes = bytes; - bool first = true; - while (remainingBytes > 0) { - int position = 0; - foreach (const SpanList::Span& span, _acknowledged.getSpans()) { - if (remainingBytes <= 0) { - break; - } - spanCount++; - remainingBytes -= getBytesToWrite(first, qMin(remainingBytes, span.unset)); - position += (span.unset + span.set); - } - int leftover = _buffer.pos() - position; - if (remainingBytes > 0 && leftover > 0) { - spanCount++; - remainingBytes -= getBytesToWrite(first, qMin(remainingBytes, leftover)); - } + if (bytes == 0) { + out << (quint32)0; + return; } - - // write the count and the spans - out << (quint32)spanCount; - remainingBytes = bytes; - first = true; - while (remainingBytes > 0) { + _writePosition %= _buffer.pos(); + while (bytes > 0) { int position = 0; - foreach (const SpanList::Span& span, _acknowledged.getSpans()) { - if (remainingBytes <= 0) { - break; + for (int i = 0; i < _acknowledged.getSpans().size(); i++) { + const SpanList::Span& span = _acknowledged.getSpans().at(i); + position += span.unset; + if (_writePosition < position) { + int start = qMax(position - span.unset, _writePosition); + int length = qMin(bytes, position - start); + writeSpan(out, start, length, spans); + writeFullSpans(out, bytes - length, i + 1, position + span.set, spans); + out << (quint32)0; + return; } - remainingBytes -= writeSpan(out, first, position, qMin(remainingBytes, span.unset), spans); - position += (span.unset + span.set); + position += span.set; } int leftover = _buffer.pos() - position; - if (remainingBytes > 0 && leftover > 0) { - remainingBytes -= writeSpan(out, first, position, qMin(remainingBytes, leftover), spans); + position = _buffer.pos(); + + if (_writePosition < position && leftover > 0) { + int start = qMax(position - leftover, _writePosition); + int length = qMin(bytes, position - start); + writeSpan(out, start, length, spans); + writeFullSpans(out, bytes - length, 0, 0, spans); + out << (quint32)0; + return; + } + _writePosition = 0; + } +} + +void ReliableChannel::writeFullSpans(QDataStream& out, int bytes, int startingIndex, int position, + QVector& spans) { + int expandedSize = _acknowledged.getSpans().size() + 1; + for (int i = 0; i < expandedSize; i++) { + if (bytes == 0) { + return; + } + int index = (startingIndex + i) % expandedSize; + if (index == _acknowledged.getSpans().size()) { + int leftover = _buffer.pos() - position; + if (leftover > 0) { + int length = qMin(leftover, bytes); + writeSpan(out, position, length, spans); + bytes -= length; + } + position = 0; + + } else { + const SpanList::Span& span = _acknowledged.getSpans().at(index); + int length = qMin(span.unset, bytes); + writeSpan(out, position, length, spans); + bytes -= length; + position += (span.unset + span.set); } } } -int ReliableChannel::getBytesToWrite(bool& first, int length) const { - if (first) { - first = false; - return length - (_writePosition % length); - } - return length; -} - -int ReliableChannel::writeSpan(QDataStream& out, bool& first, int position, int length, QVector& spans) { - if (first) { - first = false; - position = _writePosition % length; - length -= position; - _writePosition += length; - } +int ReliableChannel::writeSpan(QDataStream& out, int position, int length, QVector& spans) { DatagramSequencer::ChannelSpan span = { _index, _offset + position, length }; spans.append(span); - out << (quint32)span.offset; out << (quint32)length; + out << (quint32)span.offset; _buffer.writeToStream(position, length, out); + _writePosition = position + length; + return length; } @@ -700,17 +778,28 @@ void ReliableChannel::spanAcknowledged(const DatagramSequencer::ChannelSpan& spa _buffer.seek(_buffer.size()); _offset += advancement; - _writePosition = qMax(_writePosition - advancement, 0); - } + _writePosition = qMax(_writePosition - advancement, 0); + } +} + +void ReliableChannel::spanLost(int packetNumber, int nextOutgoingPacketNumber) { + // reset the write position up to once each round trip time + if (packetNumber >= _writePositionResetPacketNumber) { + _writePosition = 0; + _writePositionResetPacketNumber = nextOutgoingPacketNumber; + } } void ReliableChannel::readData(QDataStream& in) { - quint32 segments; - in >> segments; bool readSome = false; - for (quint32 i = 0; i < segments; i++) { - quint32 offset, size; - in >> offset >> size; + forever { + quint32 size; + in >> size; + if (size == 0) { + break; + } + quint32 offset; + in >> offset; int position = offset - _offset; int end = position + size; diff --git a/libraries/metavoxels/src/DatagramSequencer.h b/libraries/metavoxels/src/DatagramSequencer.h index 5ac88556f0..9a4cd1334b 100644 --- a/libraries/metavoxels/src/DatagramSequencer.h +++ b/libraries/metavoxels/src/DatagramSequencer.h @@ -99,6 +99,11 @@ public: /// Returns the intput channel at the specified index, creating it if necessary. ReliableChannel* getReliableInputChannel(int index = 0); + /// Starts a packet group. + /// \param desiredPackets the number of packets we'd like to write in the group + /// \return the number of packets to write in the group + int startPacketGroup(int desiredPackets = 1); + /// Starts a new packet for transmission. /// \return a reference to the Bitstream to use for writing to the packet Bitstream& startPacket(); @@ -165,6 +170,9 @@ private: /// Notes that the described send was acknowledged by the other party. void sendRecordAcknowledged(const SendRecord& record); + /// Notes that the described send was lost in transit. + void sendRecordLost(const SendRecord& record); + /// Appends some reliable data to the outgoing packet. void appendReliableData(int bytes, QVector& spans); @@ -200,6 +208,12 @@ private: int _maxPacketSize; + float _packetsPerGroup; + float _packetsToWrite; + float _slowStartThreshold; + int _packetRateIncreasePacketNumber; + int _packetRateDecreasePacketNumber; + QHash _reliableOutputChannels; QHash _reliableInputChannels; }; @@ -343,10 +357,12 @@ private: ReliableChannel(DatagramSequencer* sequencer, int index, bool output); void writeData(QDataStream& out, int bytes, QVector& spans); - int getBytesToWrite(bool& first, int length) const; - int writeSpan(QDataStream& out, bool& first, int position, int length, QVector& spans); + void writeFullSpans(QDataStream& out, int bytes, int startingIndex, int position, + QVector& spans); + int writeSpan(QDataStream& out, int position, int length, QVector& spans); void spanAcknowledged(const DatagramSequencer::ChannelSpan& span); + void spanLost(int packetNumber, int nextOutgoingPacketNumber); void readData(QDataStream& in); @@ -359,6 +375,7 @@ private: int _offset; int _writePosition; + int _writePositionResetPacketNumber; SpanList _acknowledged; bool _messagesEnabled; }; diff --git a/libraries/metavoxels/src/MetavoxelData.cpp b/libraries/metavoxels/src/MetavoxelData.cpp index 3e70e0f09b..2d61ede796 100644 --- a/libraries/metavoxels/src/MetavoxelData.cpp +++ b/libraries/metavoxels/src/MetavoxelData.cpp @@ -1094,14 +1094,14 @@ const int ORDER_ELEMENT_MASK = (1 << ORDER_ELEMENT_BITS) - 1; int MetavoxelVisitor::encodeRandomOrder() { // see http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_.22inside-out.22_algorithm - int order; + int order = 0; int randomValues = rand(); for (int i = 0, iShift = 0; i < MetavoxelNode::CHILD_COUNT; i++, iShift += ORDER_ELEMENT_BITS) { int j = (randomValues >> iShift) % (i + 1); int jShift = j * ORDER_ELEMENT_BITS; if (j != i) { int jValue = (order >> jShift) & ORDER_ELEMENT_MASK; - order = (order & ~(ORDER_ELEMENT_MASK << iShift)) | (jValue << iShift); + order |= (jValue << iShift); } order = (order & ~(ORDER_ELEMENT_MASK << jShift)) | (i << jShift); } diff --git a/libraries/models/src/ModelItem.cpp b/libraries/models/src/ModelItem.cpp index b6f4fe6c1d..8c061102a0 100644 --- a/libraries/models/src/ModelItem.cpp +++ b/libraries/models/src/ModelItem.cpp @@ -886,7 +886,19 @@ QScriptValue ModelItemProperties::copyToScriptValue(QScriptEngine* engine) const properties.setProperty("shouldDie", _shouldDie); properties.setProperty("modelURL", _modelURL); - + + + QScriptValue sittingPoints = engine->newObject(); + for (int i = 0; i < _sittingPoints.size(); ++i) { + QScriptValue sittingPoint = engine->newObject(); + sittingPoint.setProperty("name", _sittingPoints[i].name); + sittingPoint.setProperty("position", vec3toScriptValue(engine, _sittingPoints[i].position)); + sittingPoint.setProperty("rotation", quatToScriptValue(engine, _sittingPoints[i].rotation)); + sittingPoints.setProperty(i, sittingPoint); + } + sittingPoints.setProperty("length", _sittingPoints.size()); + properties.setProperty("sittingPoints", sittingPoints); + QScriptValue modelRotation = quatToScriptValue(engine, _modelRotation); properties.setProperty("modelRotation", modelRotation); @@ -971,7 +983,7 @@ void ModelItemProperties::copyFromScriptValue(const QScriptValue &object) { _modelURLChanged = true; } } - + QScriptValue modelRotation = object.property("modelRotation"); if (modelRotation.isValid()) { QScriptValue x = modelRotation.property("x"); @@ -1125,6 +1137,7 @@ void ModelItemProperties::copyFromModelItem(const ModelItem& modelItem) { _animationFrameIndex = modelItem.getAnimationFrameIndex(); _animationFPS = modelItem.getAnimationFPS(); _glowLevel = modelItem.getGlowLevel(); + _sittingPoints = modelItem.getSittingPoints(); _id = modelItem.getID(); _idSet = true; diff --git a/libraries/models/src/ModelItem.h b/libraries/models/src/ModelItem.h index 9a558f2ef4..43aaca48a0 100644 --- a/libraries/models/src/ModelItem.h +++ b/libraries/models/src/ModelItem.h @@ -12,9 +12,10 @@ #ifndef hifi_ModelItem_h #define hifi_ModelItem_h -#include #include +#include + #include #include @@ -22,6 +23,8 @@ #include #include #include +#include + class ModelItem; class ModelEditPacketSender; @@ -122,7 +125,8 @@ private: float _animationFrameIndex; float _animationFPS; float _glowLevel; - + QVector _sittingPoints; + uint32_t _id; bool _idSet; quint64 _lastEdited; @@ -211,6 +215,7 @@ public: bool hasAnimation() const { return !_animationURL.isEmpty(); } const QString& getAnimationURL() const { return _animationURL; } float getGlowLevel() const { return _glowLevel; } + QVector getSittingPoints() const { return _sittingPoints; } ModelItemID getModelItemID() const { return ModelItemID(getID(), getCreatorTokenID(), getID() != UNKNOWN_MODEL_ID); } ModelItemProperties getProperties() const; @@ -254,6 +259,7 @@ public: void setAnimationIsPlaying(bool value) { _animationIsPlaying = value; } void setAnimationFPS(float value) { _animationFPS = value; } void setGlowLevel(float glowLevel) { _glowLevel = glowLevel; } + void setSittingPoints(QVector sittingPoints) { _sittingPoints = sittingPoints; } void setProperties(const ModelItemProperties& properties); @@ -300,6 +306,8 @@ protected: QString _modelURL; glm::quat _modelRotation; + QVector _sittingPoints; + float _glowLevel; uint32_t _creatorTokenID; diff --git a/libraries/models/src/ModelTree.cpp b/libraries/models/src/ModelTree.cpp index 466d4c5273..763f0a969e 100644 --- a/libraries/models/src/ModelTree.cpp +++ b/libraries/models/src/ModelTree.cpp @@ -117,7 +117,7 @@ void ModelTree::storeModel(const ModelItem& model, const SharedNodePointer& send // if we didn't find it in the tree, then store it... if (!theOperator.wasFound()) { AACube modelCube = model.getAACube(); - ModelTreeElement* element = (ModelTreeElement*)getOrCreateChildElementContaining(model.getAACube()); + ModelTreeElement* element = static_cast(getOrCreateChildElementContaining(model.getAACube())); element->storeModel(model); // In the case where we stored it, we also need to mark the entire "path" down to the model as diff --git a/libraries/models/src/ModelTreeElement.cpp b/libraries/models/src/ModelTreeElement.cpp index 75b9670d0f..960d1dd4cb 100644 --- a/libraries/models/src/ModelTreeElement.cpp +++ b/libraries/models/src/ModelTreeElement.cpp @@ -330,6 +330,9 @@ bool ModelTreeElement::updateModel(const ModelItemID& modelID, const ModelItemPr } if (found) { thisModel.setProperties(properties); + if (_myTree->getGeometryForModel(thisModel)) { + thisModel.setSittingPoints(_myTree->getGeometryForModel(thisModel)->sittingPoints); + } markWithChangedTime(); // mark our element as changed.. const bool wantDebug = false; if (wantDebug) { diff --git a/libraries/models/src/ModelsScriptingInterface.cpp b/libraries/models/src/ModelsScriptingInterface.cpp index 7e08571fe5..bac1213071 100644 --- a/libraries/models/src/ModelsScriptingInterface.cpp +++ b/libraries/models/src/ModelsScriptingInterface.cpp @@ -160,10 +160,8 @@ ModelItemID ModelsScriptingInterface::findClosestModel(const glm::vec3& center, QVector ModelsScriptingInterface::findModels(const glm::vec3& center, float radius) const { QVector result; if (_modelTree) { - _modelTree->lockForRead(); QVector models; _modelTree->findModels(center/(float)TREE_SCALE, radius/(float)TREE_SCALE, models); - _modelTree->unlock(); foreach (const ModelItem* model, models) { ModelItemID thisModelItemID(model->getID(), UNKNOWN_MODEL_TOKEN, true); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 34f22af234..916d7ffb19 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -314,8 +314,9 @@ void ScriptEngine::evaluate() { if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); - qDebug() << "Uncaught exception at line" << line << ":" << result.toString(); - emit errorMessage("Uncaught exception at line" + QString::number(line) + ":" + result.toString()); + qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << result.toString(); + emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + result.toString()); + _engine.clearExceptions(); } } @@ -324,7 +325,7 @@ QScriptValue ScriptEngine::evaluate(const QString& program, const QString& fileN bool hasUncaughtException = _engine.hasUncaughtException(); if (hasUncaughtException) { int line = _engine.uncaughtExceptionLineNumber(); - qDebug() << "Uncaught exception at line" << line << ": " << result.toString(); + qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ": " << result.toString(); } emit evaluationFinished(result, hasUncaughtException); _engine.clearExceptions(); @@ -353,9 +354,9 @@ void ScriptEngine::run() { QScriptValue result = _engine.evaluate(_scriptContents); if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); - - qDebug() << "Uncaught exception at line" << line << ":" << result.toString(); - emit errorMessage("Uncaught exception at line" + QString::number(line) + ":" + result.toString()); + qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << result.toString(); + emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + result.toString()); + _engine.clearExceptions(); } QElapsedTimer startTime; @@ -511,8 +512,9 @@ void ScriptEngine::run() { if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); - qDebug() << "Uncaught exception at line" << line << ":" << _engine.uncaughtException().toString(); - emit errorMessage("Uncaught exception at line" + QString::number(line) + ":" + _engine.uncaughtException().toString()); + qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << _engine.uncaughtException().toString(); + emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + _engine.uncaughtException().toString()); + _engine.clearExceptions(); } } emit scriptEnding(); @@ -672,6 +674,7 @@ void ScriptEngine::include(const QString& includeFile) { int line = _engine.uncaughtExceptionLineNumber(); qDebug() << "Uncaught exception at (" << includeFile << ") line" << line << ":" << result.toString(); emit errorMessage("Uncaught exception at (" + includeFile + ") line" + QString::number(line) + ":" + result.toString()); + _engine.clearExceptions(); } } diff --git a/tests/metavoxels/src/MetavoxelTests.cpp b/tests/metavoxels/src/MetavoxelTests.cpp index 287d3a648c..68aaf7ec70 100644 --- a/tests/metavoxels/src/MetavoxelTests.cpp +++ b/tests/metavoxels/src/MetavoxelTests.cpp @@ -28,12 +28,127 @@ MetavoxelTests::MetavoxelTests(int& argc, char** argv) : QCoreApplication(argc, argv) { } +static bool testSpanList() { + SpanList list; + + if (list.getTotalSet() != 0 || !list.getSpans().isEmpty()) { + qDebug() << "Failed empty state test."; + return true; + } + + if (list.set(-5, 15) != 10 || list.getTotalSet() != 0 || !list.getSpans().isEmpty()) { + qDebug() << "Failed initial front set."; + return true; + } + + if (list.set(5, 15) != 0 || list.getTotalSet() != 15 || list.getSpans().size() != 1 || + list.getSpans().at(0).unset != 5 || list.getSpans().at(0).set != 15) { + qDebug() << "Failed initial middle set."; + return true; + } + + if (list.set(25, 5) != 0 || list.getTotalSet() != 20 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 5 || list.getSpans().at(0).set != 15 || + list.getSpans().at(1).unset != 5 || list.getSpans().at(1).set != 5) { + qDebug() << "Failed initial end set."; + return true; + } + + if (list.set(1, 3) != 0 || list.getTotalSet() != 23 || list.getSpans().size() != 3 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 15 || + list.getSpans().at(2).unset != 5 || list.getSpans().at(2).set != 5) { + qDebug() << "Failed second front set."; + return true; + } + SpanList threeSet = list; + + if (list.set(20, 5) != 0 || list.getTotalSet() != 28 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 25) { + qDebug() << "Failed minimal join last two."; + return true; + } + + list = threeSet; + if (list.set(5, 25) != 0 || list.getTotalSet() != 28 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 25) { + qDebug() << "Failed maximal join last two."; + return true; + } + + list = threeSet; + if (list.set(10, 18) != 0 || list.getTotalSet() != 28 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 25) { + qDebug() << "Failed middle join last two."; + return true; + } + + list = threeSet; + if (list.set(10, 18) != 0 || list.getTotalSet() != 28 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 25) { + qDebug() << "Failed middle join last two."; + return true; + } + + list = threeSet; + if (list.set(2, 26) != 0 || list.getTotalSet() != 29 || list.getSpans().size() != 1 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 29) { + qDebug() << "Failed middle join three."; + return true; + } + + list = threeSet; + if (list.set(0, 2) != 4 || list.getTotalSet() != 20 || list.getSpans().size() != 2 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 15 || + list.getSpans().at(1).unset != 5 || list.getSpans().at(1).set != 5) { + qDebug() << "Failed front advance."; + return true; + } + + list = threeSet; + if (list.set(-10, 15) != 20 || list.getTotalSet() != 5 || list.getSpans().size() != 1 || + list.getSpans().at(0).unset != 5 || list.getSpans().at(0).set != 5) { + qDebug() << "Failed middle advance."; + return true; + } + + list = threeSet; + if (list.set(-10, 38) != 30 || list.getTotalSet() != 0 || list.getSpans().size() != 0) { + qDebug() << "Failed end advance."; + return true; + } + + list = threeSet; + if (list.set(-10, 100) != 90 || list.getTotalSet() != 0 || list.getSpans().size() != 0) { + qDebug() << "Failed clobber advance."; + return true; + } + + list = threeSet; + if (list.set(21, 3) != 0 || list.getTotalSet() != 26 || list.getSpans().size() != 4 || + list.getSpans().at(0).unset != 1 || list.getSpans().at(0).set != 3 || + list.getSpans().at(1).unset != 1 || list.getSpans().at(1).set != 15 || + list.getSpans().at(2).unset != 1 || list.getSpans().at(2).set != 3 || + list.getSpans().at(3).unset != 1 || list.getSpans().at(3).set != 5) { + qDebug() << "Failed adding fourth."; + return true; + } + + return false; +} + static int datagramsSent = 0; static int datagramsReceived = 0; static int bytesSent = 0; static int bytesReceived = 0; static int maxDatagramsPerPacket = 0; static int maxBytesPerPacket = 0; +static int groupsSent = 0; +static int maxPacketsPerGroup = 0; static int highPriorityMessagesSent = 0; static int highPriorityMessagesReceived = 0; static int unreliableMessagesSent = 0; @@ -332,10 +447,19 @@ bool MetavoxelTests::run() { QStringList arguments = this->arguments(); int test = (arguments.size() > 1) ? arguments.at(1).toInt() : 0; + if (test == 0 || test == 1) { + qDebug() << "Running SpanList test..."; + qDebug(); + + if (testSpanList()) { + return true; + } + } + QByteArray datagramHeader("testheader"); const int SIMULATION_ITERATIONS = 10000; - if (test == 0 || test == 1) { - qDebug() << "Running transmission tests..."; + if (test == 0 || test == 2) { + qDebug() << "Running transmission test..."; qDebug(); // create two endpoints with the same header @@ -364,8 +488,40 @@ bool MetavoxelTests::run() { qDebug(); } - if (test == 0 || test == 2) { - qDebug() << "Running serialization tests..."; + if (test == 0 || test == 3) { + qDebug() << "Running congestion control test..."; + qDebug(); + + // clear the stats + streamedBytesSent = streamedBytesReceived = datagramsSent = bytesSent = 0; + datagramsReceived = bytesReceived = maxDatagramsPerPacket = maxBytesPerPacket = 0; + + // create two endpoints with the same header + Endpoint alice(datagramHeader, Endpoint::CONGESTION_MODE), bob(datagramHeader, Endpoint::CONGESTION_MODE); + + alice.setOther(&bob); + bob.setOther(&alice); + + // perform a large number of simulation iterations + for (int i = 0; i < SIMULATION_ITERATIONS; i++) { + if (alice.simulate(i) || bob.simulate(i)) { + return true; + } + } + + qDebug() << "Sent" << streamedBytesSent << "streamed bytes, received" << streamedBytesReceived; + qDebug() << "Sent" << datagramsSent << "datagrams in" << groupsSent << "groups with" << bytesSent << + "bytes, received" << datagramsReceived << "with" << bytesReceived << "bytes"; + qDebug() << "Max" << maxDatagramsPerPacket << "datagrams," << maxBytesPerPacket << "bytes per packet"; + qDebug() << "Max" << maxPacketsPerGroup << "packets per group"; + qDebug() << "Average" << (bytesReceived / datagramsReceived) << "bytes per datagram," << + (datagramsSent / groupsSent) << "datagrams per group"; + qDebug() << "Speed:" << (bytesReceived / SIMULATION_ITERATIONS) << "bytes per iteration"; + qDebug() << "Efficiency:" << ((float)streamedBytesReceived / bytesReceived); + } + + if (test == 0 || test == 4) { + qDebug() << "Running serialization test..."; qDebug(); if (testSerialization(Bitstream::HASH_METADATA) || testSerialization(Bitstream::FULL_METADATA)) { @@ -373,8 +529,8 @@ bool MetavoxelTests::run() { } } - if (test == 0 || test == 3) { - qDebug() << "Running metavoxel data tests..."; + if (test == 0 || test == 5) { + qDebug() << "Running metavoxel data test..."; qDebug(); // clear the stats @@ -498,9 +654,22 @@ Endpoint::Endpoint(const QByteArray& datagramHeader, Mode mode) : ReliableChannel* output = _sequencer->getReliableOutputChannel(1); output->setPriority(0.25f); output->setMessagesEnabled(false); - const int MIN_STREAM_BYTES = 100000; - const int MAX_STREAM_BYTES = 200000; - QByteArray bytes = createRandomBytes(MIN_STREAM_BYTES, MAX_STREAM_BYTES); + QByteArray bytes; + if (mode == CONGESTION_MODE) { + const int HUGE_STREAM_BYTES = 60 * 1024 * 1024; + bytes = createRandomBytes(HUGE_STREAM_BYTES, HUGE_STREAM_BYTES); + + // initialize the pipeline + for (int i = 0; i < 10; i++) { + _pipeline.append(ByteArrayVector()); + } + _remainingPipelineCapacity = 100 * 1024; + + } else { + const int MIN_STREAM_BYTES = 100000; + const int MAX_STREAM_BYTES = 200000; + bytes = createRandomBytes(MIN_STREAM_BYTES, MAX_STREAM_BYTES); + } _dataStreamed.append(bytes); output->getBuffer().write(bytes); streamedBytesSent += bytes.size(); @@ -633,10 +802,9 @@ int MutateVisitor::visit(MetavoxelInfo& info) { bool Endpoint::simulate(int iterationNumber) { // update/send our delayed datagrams - for (QList >::iterator it = _delayedDatagrams.begin(); it != _delayedDatagrams.end(); ) { + for (QList::iterator it = _delayedDatagrams.begin(); it != _delayedDatagrams.end(); ) { if (it->second-- == 1) { - _other->_sequencer->receivedDatagram(it->first); - datagramsReceived++; + _other->receiveDatagram(it->first); it = _delayedDatagrams.erase(it); } else { @@ -646,7 +814,37 @@ bool Endpoint::simulate(int iterationNumber) { int oldDatagramsSent = datagramsSent; int oldBytesSent = bytesSent; - if (_mode == METAVOXEL_CLIENT_MODE) { + if (_mode == CONGESTION_MODE) { + // cycle our pipeline + ByteArrayVector datagrams = _pipeline.takeLast(); + _pipeline.prepend(ByteArrayVector()); + foreach (const QByteArray& datagram, datagrams) { + _sequencer->receivedDatagram(datagram); + datagramsReceived++; + bytesReceived += datagram.size(); + _remainingPipelineCapacity += datagram.size(); + } + int packetCount = _sequencer->startPacketGroup(); + groupsSent++; + maxPacketsPerGroup = qMax(maxPacketsPerGroup, packetCount); + for (int i = 0; i < packetCount; i++) { + oldDatagramsSent = datagramsSent; + oldBytesSent = bytesSent; + + Bitstream& out = _sequencer->startPacket(); + out << QVariant(); + _sequencer->endPacket(); + + maxDatagramsPerPacket = qMax(maxDatagramsPerPacket, datagramsSent - oldDatagramsSent); + maxBytesPerPacket = qMax(maxBytesPerPacket, bytesSent - oldBytesSent); + + // record the send + SendRecord record = { _sequencer->getOutgoingPacketNumber() }; + _sendRecords.append(record); + } + return false; + + } else if (_mode == METAVOXEL_CLIENT_MODE) { Bitstream& out = _sequencer->startPacket(); ClientStateMessage state = { _lod }; @@ -748,29 +946,28 @@ void Endpoint::sendDatagram(const QByteArray& datagram) { // some datagrams are dropped const float DROP_PROBABILITY = 0.1f; - if (randFloat() < DROP_PROBABILITY) { + float probabilityMultiplier = (_mode == CONGESTION_MODE) ? 0.01f : 1.0f; + if (randFloat() < DROP_PROBABILITY * probabilityMultiplier) { return; } // some are received out of order const float REORDER_PROBABILITY = 0.1f; - if (randFloat() < REORDER_PROBABILITY) { + if (randFloat() < REORDER_PROBABILITY * probabilityMultiplier) { const int MIN_DELAY = 1; const int MAX_DELAY = 5; // have to copy the datagram; the one we're passed is a reference to a shared buffer - _delayedDatagrams.append(QPair(QByteArray(datagram.constData(), datagram.size()), + _delayedDatagrams.append(ByteArrayIntPair(QByteArray(datagram.constData(), datagram.size()), randIntInRange(MIN_DELAY, MAX_DELAY))); // and some are duplicated const float DUPLICATE_PROBABILITY = 0.01f; - if (randFloat() > DUPLICATE_PROBABILITY) { + if (randFloat() > DUPLICATE_PROBABILITY * probabilityMultiplier) { return; } } - _other->_sequencer->receivedDatagram(datagram); - datagramsReceived++; - bytesReceived += datagram.size(); + _other->receiveDatagram(datagram); } void Endpoint::handleHighPriorityMessage(const QVariant& message) { @@ -788,6 +985,15 @@ void Endpoint::handleHighPriorityMessage(const QVariant& message) { } void Endpoint::readMessage(Bitstream& in) { + if (_mode == CONGESTION_MODE) { + QVariant message; + in >> message; + + // record the receipt + ReceiveRecord record = { _sequencer->getIncomingPacketNumber() }; + _receiveRecords.append(record); + return; + } if (_mode == METAVOXEL_CLIENT_MODE) { QVariant message; in >> message; @@ -887,6 +1093,20 @@ void Endpoint::clearReceiveRecordsBefore(int index) { _receiveRecords.erase(_receiveRecords.begin(), _receiveRecords.begin() + index + 1); } +void Endpoint::receiveDatagram(const QByteArray& datagram) { + if (_mode == CONGESTION_MODE) { + if (datagram.size() <= _remainingPipelineCapacity) { + // have to copy the datagram; the one we're passed is a reference to a shared buffer + _pipeline[0].append(QByteArray(datagram.constData(), datagram.size())); + _remainingPipelineCapacity -= datagram.size(); + } + } else { + _sequencer->receivedDatagram(datagram); + datagramsReceived++; + bytesReceived += datagram.size(); + } +} + void Endpoint::handleMessage(const QVariant& message, Bitstream& in) { int userType = message.userType(); if (userType == ClientStateMessage::Type) { diff --git a/tests/metavoxels/src/MetavoxelTests.h b/tests/metavoxels/src/MetavoxelTests.h index c340d78963..f9a314dcd7 100644 --- a/tests/metavoxels/src/MetavoxelTests.h +++ b/tests/metavoxels/src/MetavoxelTests.h @@ -40,7 +40,7 @@ class Endpoint : public QObject { public: - enum Mode { BASIC_PEER_MODE, METAVOXEL_SERVER_MODE, METAVOXEL_CLIENT_MODE }; + enum Mode { BASIC_PEER_MODE, CONGESTION_MODE, METAVOXEL_SERVER_MODE, METAVOXEL_CLIENT_MODE }; Endpoint(const QByteArray& datagramHeader, Mode mode = BASIC_PEER_MODE); @@ -63,6 +63,8 @@ private slots: private: + void receiveDatagram(const QByteArray& datagram); + void handleMessage(const QVariant& message, Bitstream& in); class SendRecord { @@ -96,7 +98,14 @@ private: SharedObjectPointer _sphere; Endpoint* _other; - QList > _delayedDatagrams; + + typedef QPair ByteArrayIntPair; + QList _delayedDatagrams; + + typedef QVector ByteArrayVector; + QList _pipeline; + int _remainingPipelineCapacity; + float _highPriorityMessagesToSend; QVariantList _highPriorityMessagesSent; QList _unreliableMessagesSent; diff --git a/tests/octree/CMakeLists.txt b/tests/octree/CMakeLists.txt index 1697064ff4..9c5e031d74 100644 --- a/tests/octree/CMakeLists.txt +++ b/tests/octree/CMakeLists.txt @@ -30,15 +30,20 @@ include_glm(${TARGET_NAME} ${ROOT_DIR}) # link in the shared libraries include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(animation ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(audio ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(fbx ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) link_hifi_library(models ${TARGET_NAME} ${ROOT_DIR}) link_hifi_library(octree ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(audio ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(animation ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(fbx ${TARGET_NAME} ${ROOT_DIR}) link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) IF (WIN32) + # add a definition for ssize_t so that windows doesn't bail + add_definitions(-Dssize_t=long) + #target_link_libraries(${TARGET_NAME} Winmm Ws2_32) + target_link_libraries(${TARGET_NAME} wsock32.lib) ENDIF(WIN32) + diff --git a/tests/octree/src/ModelTests.cpp b/tests/octree/src/ModelTests.cpp index 63653dac69..2cca4b43f6 100644 --- a/tests/octree/src/ModelTests.cpp +++ b/tests/octree/src/ModelTests.cpp @@ -14,20 +14,17 @@ #include -#if 0 +#include #include #include #include -#include #include #include #include -#endif #include "ModelTests.h" void ModelTests::modelTreeTests(bool verbose) { -#if 0 int testsTaken = 0; int testsPassed = 0; int testsFailed = 0; @@ -258,7 +255,6 @@ void ModelTests::modelTreeTests(bool verbose) { if (verbose) { qDebug() << "******************************************************************************************"; } -#endif } diff --git a/tests/physics/src/ShapeColliderTests.cpp b/tests/physics/src/ShapeColliderTests.cpp index 4d3c90b905..bde29ea588 100644 --- a/tests/physics/src/ShapeColliderTests.cpp +++ b/tests/physics/src/ShapeColliderTests.cpp @@ -1183,7 +1183,7 @@ void ShapeColliderTests::rayMissesCapsule() { void ShapeColliderTests::rayHitsPlane() { // make a simple plane - float planeDistanceFromOrigin = 3.579; + float planeDistanceFromOrigin = 3.579f; glm::vec3 planePosition(0.0f, planeDistanceFromOrigin, 0.0f); PlaneShape plane; plane.setTranslation(planePosition); @@ -1228,7 +1228,7 @@ void ShapeColliderTests::rayHitsPlane() { void ShapeColliderTests::rayMissesPlane() { // make a simple plane - float planeDistanceFromOrigin = 3.579; + float planeDistanceFromOrigin = 3.579f; glm::vec3 planePosition(0.0f, planeDistanceFromOrigin, 0.0f); PlaneShape plane; plane.setTranslation(planePosition);