merge upstream/master into andrew/ragdoll

This commit is contained in:
Andrew Meadows 2014-08-04 17:37:28 -07:00
commit 39d3deee90
48 changed files with 1989 additions and 272 deletions

View file

@ -1,3 +1,4 @@
</div>
<script src='/js/jquery-2.0.3.min.js'></script>
<script src='/js/bootstrap.min.js'></script>
<script src='/js/bootstrap.min.js'></script>
<script src='/js/domain-server.js'></script>

View file

@ -8,4 +8,33 @@
<link href="/css/style.css" rel="stylesheet" media="screen">
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">domain-server</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a href="/">Nodes</a></li>
<li><a href="/settings/">Settings</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assignments <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="/assignment">New Assignment</a></li>
</ul>
</li>
</ul>
</div>
</div><!-- /.container-fluid -->
</nav>
<div class="container">

View file

@ -0,0 +1,10 @@
$(document).ready(function(){
var url = window.location;
// Will only work if string in href matches with location
$('ul.nav a[href="'+ url +'"]').parent().addClass('active');
// Will also work for relative and absolute hrefs
$('ul.nav a').filter(function() {
return this.href == url;
}).parent().addClass('active');
});

View file

@ -28,33 +28,5 @@
"default": ""
}
}
},
"voxels": {
"label": "Voxels",
"assignment-types": [2,3],
"settings": {
"voxel-wallet": {
"label": "Destination Wallet ID",
"help": "Wallet to be paid for voxel changes",
"placeholder": "00000000-0000-0000-0000-000000000000",
"default": ""
},
"per-voxel-credits": {
"type": "double",
"label": "Per Voxel Cost",
"help": "Credit cost to change each voxel",
"placeholder": "0.0",
"default": "0.0",
"input_addon": "₵"
},
"per-meter-cubed-credits": {
"type": "double",
"label": "Per Meter Cubed Cost",
"help": "Credit cost to change each cubed meter of voxel space",
"placeholder": "0.0",
"default": "0.0",
"input_addon": "₵"
}
}
}
}

View file

@ -1280,6 +1280,9 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie";
const QString ADMIN_USERS_CONFIG_KEY = "admin-users";
const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles";
const QString BASIC_AUTH_CONFIG_KEY = "basic-auth";
const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server.";
if (!_oauthProviderURL.isEmpty()
&& (_argumentVariantMap.contains(ADMIN_USERS_CONFIG_KEY) || _argumentVariantMap.contains(ADMIN_ROLES_CONFIG_KEY))) {
@ -1293,6 +1296,11 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
cookieUUID = cookieUUIDRegex.cap(1);
}
if (_argumentVariantMap.contains(BASIC_AUTH_CONFIG_KEY)) {
qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication."
<< "These cannot be combined - using OAuth for authentication.";
}
if (!cookieUUID.isNull() && _cookieSessionHash.contains(cookieUUID)) {
// pull the QJSONObject for the user with this cookie UUID
DomainServerWebSessionData sessionData = _cookieSessionHash.value(cookieUUID);
@ -1315,8 +1323,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
}
}
QString unauthenticatedRequest = "You do not have permission to access this domain-server.";
connection->respond(HTTPConnection::StatusCode401, unauthenticatedRequest.toUtf8());
connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY);
// the user does not have allowed username or role, return 401
return false;
@ -1340,6 +1347,59 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
// we don't know about this user yet, so they are not yet authenticated
return false;
}
} else if (_argumentVariantMap.contains(BASIC_AUTH_CONFIG_KEY)) {
// config file contains username and password combinations for basic auth
const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization";
// check if a username and password have been provided with the request
QString basicAuthString = connection->requestHeaders().value(BASIC_AUTH_HEADER_KEY);
if (!basicAuthString.isEmpty()) {
QStringList splitAuthString = basicAuthString.split(' ');
QString base64String = splitAuthString.size() == 2 ? splitAuthString[1] : "";
QString credentialString = QByteArray::fromBase64(base64String.toLocal8Bit());
if (!credentialString.isEmpty()) {
QStringList credentialList = credentialString.split(':');
if (credentialList.size() == 2) {
QString username = credentialList[0];
QString password = credentialList[1];
// we've pulled a username and password - now check if there is a match in our basic auth hash
QJsonObject basicAuthObject = _argumentVariantMap.value(BASIC_AUTH_CONFIG_KEY).toJsonValue().toObject();
if (basicAuthObject.contains(username)) {
const QString BASIC_AUTH_USER_PASSWORD_KEY = "password";
QJsonObject userObject = basicAuthObject.value(username).toObject();
if (userObject.contains(BASIC_AUTH_USER_PASSWORD_KEY)
&& userObject.value(BASIC_AUTH_USER_PASSWORD_KEY).toString() == password) {
// this is username / password match - let this user in
return true;
}
}
}
}
}
// basic HTTP auth being used but no username and password are present
// or the username and password are not correct
// send back a 401 and ask for basic auth
const QByteArray HTTP_AUTH_REQUEST_HEADER_KEY = "WWW-Authenticate";
static QString HTTP_AUTH_REALM_STRING = QString("Basic realm='%1 %2'")
.arg(_hostname.isEmpty() ? "localhost" : _hostname)
.arg("domain-server");
Headers basicAuthHeader;
basicAuthHeader.insert(HTTP_AUTH_REQUEST_HEADER_KEY, HTTP_AUTH_REALM_STRING.toUtf8());
connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY,
HTTPConnection::DefaultContentType, basicAuthHeader);
// not authenticated, bubble up false
return false;
} else {
// we don't have an OAuth URL + admin roles/usernames, so all users are authenticated
return true;

View file

@ -60,6 +60,413 @@ var jointList = MyAvatar.getJointNames();
var mode = 0;
var exportMenu = null;
var ExportMenu = function(opts) {
var self = this;
var windowDimensions = Controller.getViewportDimensions();
var pos = { x: windowDimensions.x / 2, y: windowDimensions.y - 100 };
this._onClose = opts.onClose || function() {};
this._position = { x: 0.0, y: 0.0, z: 0.0 };
this._scale = 1.0;
var minScale = 1;
var maxScale = 32768;
var titleWidth = 120;
var locationWidth = 100;
var scaleWidth = 144;
var exportWidth = 100;
var cancelWidth = 100;
var margin = 4;
var height = 30;
var outerHeight = height + (2 * margin);
var buttonColor = { red: 128, green: 128, blue: 128};
var SCALE_MINUS = scaleWidth * 40.0 / 100.0;
var SCALE_PLUS = scaleWidth * 63.0 / 100.0;
var fullWidth = locationWidth + scaleWidth + exportWidth + cancelWidth + (2 * margin);
var offset = fullWidth / 2;
pos.x -= offset;
var background= Overlays.addOverlay("text", {
x: pos.x,
y: pos.y,
opacity: 1,
width: fullWidth,
height: outerHeight,
backgroundColor: { red: 200, green: 200, blue: 200 },
text: "",
});
var titleText = Overlays.addOverlay("text", {
x: pos.x,
y: pos.y - height,
font: { size: 14 },
width: titleWidth,
height: height,
backgroundColor: { red: 255, green: 255, blue: 255 },
color: { red: 255, green: 255, blue: 255 },
text: "Export Models"
});
var locationButton = Overlays.addOverlay("text", {
x: pos.x + margin,
y: pos.y + margin,
width: locationWidth,
height: height,
color: { red: 255, green: 255, blue: 255 },
text: "0, 0, 0",
});
var scaleOverlay = Overlays.addOverlay("image", {
x: pos.x + margin + locationWidth,
y: pos.y + margin,
width: scaleWidth,
height: height,
subImage: { x: 0, y: 3, width: 144, height: height},
imageURL: toolIconUrl + "voxel-size-selector.svg",
alpha: 0.9,
});
var scaleViewWidth = 40;
var scaleView = Overlays.addOverlay("text", {
x: pos.x + margin + locationWidth + SCALE_MINUS,
y: pos.y + margin,
width: scaleViewWidth,
height: height,
alpha: 0.0,
color: { red: 255, green: 255, blue: 255 },
text: "1"
});
var exportButton = Overlays.addOverlay("text", {
x: pos.x + margin + locationWidth + scaleWidth,
y: pos.y + margin,
width: exportWidth,
height: height,
color: { red: 0, green: 255, blue: 255 },
text: "Export"
});
var cancelButton = Overlays.addOverlay("text", {
x: pos.x + margin + locationWidth + scaleWidth + exportWidth,
y: pos.y + margin,
width: cancelWidth,
height: height,
color: { red: 255, green: 255, blue: 255 },
text: "Cancel"
});
var voxelPreview = Overlays.addOverlay("cube", {
position: { x: 0, y: 0, z: 0},
size: this._scale,
color: { red: 255, green: 255, blue: 0},
alpha: 1,
solid: false,
visible: true,
lineWidth: 4
});
this.parsePosition = function(str) {
var parts = str.split(',');
if (parts.length == 3) {
var x = parseFloat(parts[0]);
var y = parseFloat(parts[1]);
var z = parseFloat(parts[2]);
if (isFinite(x) && isFinite(y) && isFinite(z)) {
return { x: x, y: y, z: z };
}
}
return null;
};
this.showPositionPrompt = function() {
var positionStr = self._position.x + ", " + self._position.y + ", " + self._position.z;
while (1) {
positionStr = Window.prompt("Position to export form:", positionStr);
if (positionStr == null) {
break;
}
var position = self.parsePosition(positionStr);
if (position != null) {
self.setPosition(position.x, position.y, position.z);
break;
}
Window.alert("The position you entered was invalid.");
}
};
this.setScale = function(scale) {
self._scale = Math.min(maxScale, Math.max(minScale, scale));
Overlays.editOverlay(scaleView, { text: self._scale });
Overlays.editOverlay(voxelPreview, { size: self._scale });
}
this.decreaseScale = function() {
self.setScale(self._scale /= 2);
}
this.increaseScale = function() {
self.setScale(self._scale *= 2);
}
this.exportModels = function() {
var x = self._position.x;
var y = self._position.y;
var z = self._position.z;
var s = self._scale;
var filename = "models__" + Window.location.hostname + "__" + x + "_" + y + "_" + z + "_" + s + "__.svo";
filename = Window.save("Select where to save", filename, "*.svo")
if (filename) {
var success = Clipboard.exportModels(filename, x, y, z, s);
if (!success) {
Window.alert("Export failed: no models found in selected area.");
}
}
self.close();
};
this.getPosition = function() {
return self._position;
};
this.setPosition = function(x, y, z) {
self._position = { x: x, y: y, z: z };
var positionStr = x + ", " + y + ", " + z;
Overlays.editOverlay(locationButton, { text: positionStr });
Overlays.editOverlay(voxelPreview, { position: self._position });
};
this.mouseReleaseEvent = function(event) {
var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y});
if (clickedOverlay == locationButton) {
self.showPositionPrompt();
} else if (clickedOverlay == exportButton) {
self.exportModels();
} else if (clickedOverlay == cancelButton) {
self.close();
} else if (clickedOverlay == scaleOverlay) {
var x = event.x - pos.x - margin - locationWidth;
print(x);
if (x < SCALE_MINUS) {
self.decreaseScale();
} else if (x > SCALE_PLUS) {
self.increaseScale();
}
}
};
this.close = function() {
this.cleanup();
this._onClose();
};
this.cleanup = function() {
Overlays.deleteOverlay(background);
Overlays.deleteOverlay(titleText);
Overlays.deleteOverlay(locationButton);
Overlays.deleteOverlay(exportButton);
Overlays.deleteOverlay(cancelButton);
Overlays.deleteOverlay(voxelPreview);
Overlays.deleteOverlay(scaleOverlay);
Overlays.deleteOverlay(scaleView);
};
print("CONNECTING!");
Controller.mouseReleaseEvent.connect(this.mouseReleaseEvent);
};
var ModelImporter = function(opts) {
var self = this;
var height = 30;
var margin = 4;
var outerHeight = height + (2 * margin);
var titleWidth = 120;
var cancelWidth = 100;
var fullWidth = titleWidth + cancelWidth + (2 * margin);
var localModels = Overlays.addOverlay("localmodels", {
position: { x: 1, y: 1, z: 1 },
scale: 1,
visible: false
});
var importScale = 1;
var importBoundaries = Overlays.addOverlay("cube", {
position: { x: 0, y: 0, z: 0 },
size: 1,
color: { red: 128, blue: 128, green: 128 },
lineWidth: 4,
solid: false,
visible: false
});
var pos = { x: windowDimensions.x / 2 - (fullWidth / 2), y: windowDimensions.y - 100 };
var background = Overlays.addOverlay("text", {
x: pos.x,
y: pos.y,
opacity: 1,
width: fullWidth,
height: outerHeight,
backgroundColor: { red: 200, green: 200, blue: 200 },
visible: false,
text: "",
});
var titleText = Overlays.addOverlay("text", {
x: pos.x + margin,
y: pos.y + margin,
font: { size: 14 },
width: titleWidth,
height: height,
backgroundColor: { red: 255, green: 255, blue: 255 },
color: { red: 255, green: 255, blue: 255 },
visible: false,
text: "Import Models"
});
var cancelButton = Overlays.addOverlay("text", {
x: pos.x + margin + titleWidth,
y: pos.y + margin,
width: cancelWidth,
height: height,
color: { red: 255, green: 255, blue: 255 },
visible: false,
text: "Close"
});
this._importing = false;
this.setImportVisible = function(visible) {
Overlays.editOverlay(importBoundaries, { visible: visible });
Overlays.editOverlay(localModels, { visible: visible });
Overlays.editOverlay(cancelButton, { visible: visible });
Overlays.editOverlay(titleText, { visible: visible });
Overlays.editOverlay(background, { visible: visible });
};
var importPosition = { x: 0, y: 0, z: 0 };
this.moveImport = function(position) {
importPosition = position;
Overlays.editOverlay(localModels, {
position: { x: importPosition.x, y: importPosition.y, z: importPosition.z }
});
Overlays.editOverlay(importBoundaries, {
position: { x: importPosition.x, y: importPosition.y, z: importPosition.z }
});
}
this.mouseMoveEvent = function(event) {
if (self._importing) {
var pickRay = Camera.computePickRay(event.x, event.y);
var intersection = Voxels.findRayIntersection(pickRay);
var distance = 2;// * self._scale;
if (false) {//intersection.intersects) {
var intersectionDistance = Vec3.length(Vec3.subtract(pickRay.origin, intersection.intersection));
if (intersectionDistance < distance) {
distance = intersectionDistance * 0.99;
}
}
var targetPosition = {
x: pickRay.origin.x + (pickRay.direction.x * distance),
y: pickRay.origin.y + (pickRay.direction.y * distance),
z: pickRay.origin.z + (pickRay.direction.z * distance)
};
if (targetPosition.x < 0) targetPosition.x = 0;
if (targetPosition.y < 0) targetPosition.y = 0;
if (targetPosition.z < 0) targetPosition.z = 0;
var nudgeFactor = 1;
var newPosition = {
x: Math.floor(targetPosition.x / nudgeFactor) * nudgeFactor,
y: Math.floor(targetPosition.y / nudgeFactor) * nudgeFactor,
z: Math.floor(targetPosition.z / nudgeFactor) * nudgeFactor
}
self.moveImport(newPosition);
}
}
this.mouseReleaseEvent = function(event) {
var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y});
if (clickedOverlay == cancelButton) {
self._importing = false;
self.setImportVisible(false);
}
};
// Would prefer to use {4} for the coords, but it would only capture the last digit.
var fileRegex = /__(.+)__(\d+(?:\.\d+)?)_(\d+(?:\.\d+)?)_(\d+(?:\.\d+)?)_(\d+(?:\.\d+)?)__/;
this.doImport = function() {
if (!self._importing) {
var filename = Window.browse("Select models to import", "", "*.svo")
if (filename) {
parts = fileRegex.exec(filename);
if (parts == null) {
Window.alert("The file you selected does not contain source domain or location information");
} else {
var hostname = parts[1];
var x = parts[2];
var y = parts[3];
var z = parts[4];
var s = parts[5];
importScale = s;
if (hostname != location.hostname) {
if (!Window.confirm(("These models were not originally exported from this domain. Continue?"))) {
return;
}
} else {
if (Window.confirm(("Would you like to import back to the source location?"))) {
var success = Clipboard.importModels(filename);
if (success) {
Clipboard.pasteModels(x, y, z, 1);
} else {
Window.alert("There was an error importing the model file.");
}
return;
}
}
}
var success = Clipboard.importModels(filename);
if (success) {
self._importing = true;
self.setImportVisible(true);
Overlays.editOverlay(importBoundaries, { size: s });
} else {
Window.alert("There was an error importing the model file.");
}
}
}
}
this.paste = function() {
if (self._importing) {
// self._importing = false;
// self.setImportVisible(false);
Clipboard.pasteModels(importPosition.x, importPosition.y, importPosition.z, 1);
}
}
this.cleanup = function() {
Overlays.deleteOverlay(localModels);
Overlays.deleteOverlay(importBoundaries);
Overlays.deleteOverlay(cancelButton);
Overlays.deleteOverlay(titleText);
Overlays.deleteOverlay(background);
}
Controller.mouseReleaseEvent.connect(this.mouseReleaseEvent);
Controller.mouseMoveEvent.connect(this.mouseMoveEvent);
};
var modelImporter = new ModelImporter();
function isLocked(properties) {
// special case to lock the ground plane model in hq.
if (location.hostname == "hq.highfidelity.io" &&
@ -644,6 +1051,11 @@ function checkController(deltaTime) {
var numberOfTriggers = Controller.getNumberOfTriggers();
var numberOfSpatialControls = Controller.getNumberOfSpatialControls();
var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers;
if (!isActive) {
// So that we hide the lasers bellow and keep updating the overlays position
numberOfButtons = 0;
}
// this is expected for hydras
if (numberOfButtons==12 && numberOfTriggers == 2 && controllersPerTrigger == 2) {
@ -665,11 +1077,21 @@ function checkController(deltaTime) {
moveOverlays();
}
var isActive = false;
var active;
var newModel;
var browser;
function initToolBar() {
toolBar = new ToolBar(0, 0, ToolBar.VERTICAL);
// New Model
active = toolBar.addTool({
imageURL: toolIconUrl + "models-tool.svg",
subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT },
width: toolWidth, height: toolHeight,
visible: true,
alpha: 0.9
}, true, false);
newModel = toolBar.addTool({
imageURL: toolIconUrl + "add-model-tool.svg",
subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT },
@ -787,6 +1209,11 @@ function mousePressEvent(event) {
modelSelected = false;
var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y});
if (active == toolBar.clicked(clickedOverlay)) {
isActive = !isActive;
return;
}
if (newModel == toolBar.clicked(clickedOverlay)) {
var url = Window.prompt("Model URL", modelURLs[Math.floor(Math.random() * modelURLs.length)]);
if (url == null || url == "") {
@ -822,6 +1249,11 @@ function mousePressEvent(event) {
}
} else {
// If we aren't active and didn't click on an overlay: quit
if (!isActive) {
return;
}
var pickRay = Camera.computePickRay(event.x, event.y);
Vec3.print("[Mouse] Looking at: ", pickRay.origin);
var foundIntersection = Models.findRayIntersection(pickRay);
@ -906,7 +1338,7 @@ var oldModifier = 0;
var modifier = 0;
var wasShifted = false;
function mouseMoveEvent(event) {
if (event.isAlt) {
if (event.isAlt || !isActive) {
return;
}
@ -1049,7 +1481,7 @@ function mouseMoveEvent(event) {
function mouseReleaseEvent(event) {
if (event.isAlt) {
if (event.isAlt || !isActive) {
return;
}
@ -1069,7 +1501,7 @@ function mouseReleaseEvent(event) {
var modelMenuAddedDelete = false;
function setupModelMenus() {
print("setupModelMenus()");
// add our menuitems
// adj our menuitems
Menu.addMenuItem({ menuName: "Edit", menuItemName: "Models", isSeparator: true, beforeItem: "Physics" });
Menu.addMenuItem({ menuName: "Edit", menuItemName: "Edit Properties...",
shortcutKeyEvent: { text: "`" }, afterItem: "Models" });
@ -1081,6 +1513,12 @@ function setupModelMenus() {
} else {
print("delete exists... don't add ours");
}
Menu.addMenuItem({ menuName: "Edit", menuItemName: "Paste Models", shortcutKey: "CTRL+META+V", afterItem: "Edit Properties..." });
Menu.addMenuItem({ menuName: "File", menuItemName: "Models", isSeparator: true, beforeItem: "Settings" });
Menu.addMenuItem({ menuName: "File", menuItemName: "Export Models", shortcutKey: "CTRL+META+E", afterItem: "Models" });
Menu.addMenuItem({ menuName: "File", menuItemName: "Import Models", shortcutKey: "CTRL+META+I", afterItem: "Export Models" });
}
function cleanupModelMenus() {
@ -1090,6 +1528,12 @@ function cleanupModelMenus() {
// delete our menuitems
Menu.removeMenuItem("Edit", "Delete");
}
Menu.removeMenuItem("Edit", "Paste Models");
Menu.removeSeparator("File", "Models");
Menu.removeMenuItem("File", "Export Models");
Menu.removeMenuItem("File", "Import Models");
}
function scriptEnding() {
@ -1098,6 +1542,10 @@ function scriptEnding() {
toolBar.cleanup();
cleanupModelMenus();
tooltip.cleanup();
modelImporter.cleanup();
if (exportMenu) {
exportMenu.close();
}
}
Script.scriptEnding.connect(scriptEnding);
@ -1175,6 +1623,18 @@ function handeMenuEvent(menuItem){
Models.editModel(selectedModelID, selectedModelProperties);
}
} else if (menuItem == "Paste Models") {
modelImporter.paste();
} else if (menuItem == "Export Models") {
if (!exportMenu) {
exportMenu = new ExportMenu({
onClose: function() {
exportMenu = null;
}
});
}
} else if (menuItem == "Import Models") {
modelImporter.doImport();
}
tooltip.show(false);
}
@ -1221,4 +1681,4 @@ Controller.keyReleaseEvent.connect(function(event) {
if (event.text == "BACKSPACE") {
handeMenuEvent("Delete");
}
});
});

View file

@ -0,0 +1,272 @@
// rockPaperScissorsCells.js
// examples
//
// Created by Ben Arnold on 7/16/14.
// Copyright 2014 High Fidelity, Inc.
//
// This sample script creates a voxel wall that simulates the Rock Paper Scissors cellular
// automata. http://www.gamedev.net/blog/844/entry-2249737-another-cellular-automaton-video/
// If multiple instances of this script are run, they will combine into a larger wall.
// NOTE: You must run each instance one at a time. If they all start at once there are race conditions.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
var NUMBER_OF_CELLS_EACH_DIMENSION = 48;
var NUMBER_OF_CELLS_REGION_EACH_DIMESION = 16;
var REGIONS_EACH_DIMENSION = NUMBER_OF_CELLS_EACH_DIMENSION / NUMBER_OF_CELLS_REGION_EACH_DIMESION;
var isLocal = false;
var currentCells = [];
var nextCells = [];
var cornerPosition = {x: 100, y: 0, z: 0 }
var position = {x: 0, y: 0, z: 0 };
var METER_LENGTH = 1;
var cellScale = (NUMBER_OF_CELLS_EACH_DIMENSION * METER_LENGTH) / NUMBER_OF_CELLS_EACH_DIMENSION;
var viewerPosition = {x: cornerPosition.x + (NUMBER_OF_CELLS_EACH_DIMENSION / 2) * cellScale, y: cornerPosition.y + (NUMBER_OF_CELLS_EACH_DIMENSION / 2) * cellScale, z: cornerPosition.z };
viewerPosition.z += 50;
var yaw = 0;
var orientation = Quat.fromPitchYawRollDegrees(0, yaw, 0);
//Feel free to add new cell types here. It can be more than three.
var cellTypes = [];
cellTypes[0] = { r: 255, g: 0, b: 0 };
cellTypes[1] = { r: 0, g: 255, b: 0 };
cellTypes[2] = { r: 0, g:0, b: 255 };
cellTypes[3] = { r: 0, g: 255, b: 255 };
//Check for free region for AC
var regionMarkerX = -1;
var regionMarkerY = -1;
var regionMarkerI = -1;
var regionMarkerJ = -1;
var regionMarkerColor = {r: 254, g: 0, b: 253};
function setRegionToColor(startX, startY, width, height, color) {
for (var i = startY; i < startY + height; i++) {
for (var j = startX; j < startX + width; j++) {
currentCells[i][j] = { changed: true, type: color };
// put the same value in the nextCells array for first board draw
nextCells[i][j] = { changed: true, type: color };
}
}
}
function init() {
for (var i = 0; i < REGIONS_EACH_DIMENSION; i++) {
for (var j = 0; j < REGIONS_EACH_DIMENSION; j++) {
var x = cornerPosition.x + (j) * cellScale;
var y = cornerPosition.y + (i + NUMBER_OF_CELLS_EACH_DIMENSION) * cellScale;
var z = cornerPosition.z;
var voxel = Voxels.getVoxelAt(x, y, z, cellScale);
if (voxel.x != x || voxel.y != y || voxel.z != z || voxel.s != cellScale ||
voxel.red != regionMarkerColor.r || voxel.green != regionMarkerColor.g || voxel.blue != regionMarkerColor.b) {
regionMarkerX = x;
regionMarkerY = y;
regionMarkerI = i;
regionMarkerJ = j;
i = REGIONS_EACH_DIMENSION; //force quit loop
break;
}
}
}
//Didnt find an open spot, end script
if (regionMarkerX == -1) {
Script.stop();
}
position.x = cornerPosition.x + regionMarkerJ * NUMBER_OF_CELLS_REGION_EACH_DIMESION;
position.y = cornerPosition.y + regionMarkerI * NUMBER_OF_CELLS_REGION_EACH_DIMESION;
position.z = cornerPosition.z;
Voxels.setVoxel(regionMarkerX, regionMarkerY, cornerPosition.z, cellScale, regionMarkerColor.r, regionMarkerColor.g, regionMarkerColor.b);
for (var i = 0; i < NUMBER_OF_CELLS_REGION_EACH_DIMESION; i++) {
// create the array to hold this row
currentCells[i] = [];
// create the array to hold this row in the nextCells array
nextCells[i] = [];
}
var width = NUMBER_OF_CELLS_REGION_EACH_DIMESION / 2;
setRegionToColor(0, 0, width, width, 0);
setRegionToColor(0, width, width, width, 1);
setRegionToColor(width, width, width, width, 2);
setRegionToColor(width, 0, width, width, 3);
}
function updateCells() {
var i = 0;
var j = 0;
var cell;
var y = 0;
var x = 0;
for (i = 0; i < NUMBER_OF_CELLS_REGION_EACH_DIMESION; i++) {
for (j = 0; j < NUMBER_OF_CELLS_REGION_EACH_DIMESION; j++) {
cell = currentCells[i][j];
var r = Math.floor(Math.random() * 8);
switch (r){
case 0:
y = i - 1;
x = j - 1;
break;
case 1:
y = i;
x = j-1;
break;
case 2:
y = i + 1;
x = j - 1;
break;
case 3:
y = i + 1;
x = j;
break;
case 4:
y = i + 1;
x = j + 1;
break;
case 5:
y = i;
x = j + 1;
break;
case 6:
y = i - 1;
x = j + 1;
break;
case 7:
y = i - 1;
x = j;
break;
default:
continue;
}
//check the voxel grid instead of local array when on the edge
if (x == -1 || x == NUMBER_OF_CELLS_REGION_EACH_DIMESION ||
y == -1 || y == NUMBER_OF_CELLS_REGION_EACH_DIMESION) {
var voxel = Voxels.getVoxelAt(position.x + x * cellScale, position.y + y * cellScale, position.z, cellScale);
var predatorCellType = ((cell.type + 1) % cellTypes.length);
var predatorCellColor = cellTypes[predatorCellType];
if (voxel.red == predatorCellColor.r && voxel.green == predatorCellColor.g && voxel.blue == predatorCellColor.b) {
nextCells[i][j].type = predatorCellType;
nextCells[i][j].changed = true;
}
} else {
if (currentCells[y][x].type == ((cell.type + 1) % cellTypes.length)) {
nextCells[i][j].type = currentCells[y][x].type;
nextCells[i][j].changed = true;
} else {
//indicate no update
nextCells[i][j].changed = false;
}
}
}
}
for (i = 0; i < NUMBER_OF_CELLS_REGION_EACH_DIMESION; i++) {
for (j = 0; j < NUMBER_OF_CELLS_REGION_EACH_DIMESION; j++) {
if (nextCells[i][j].changed == true) {
// there has been a change to this cell, change the value in the currentCells array
currentCells[i][j].type = nextCells[i][j].type;
currentCells[i][j].changed = true;
}
}
}
}
function sendNextCells() {
for (var i = 0; i < NUMBER_OF_CELLS_REGION_EACH_DIMESION; i++) {
for (var j = 0; j < NUMBER_OF_CELLS_REGION_EACH_DIMESION; j++) {
if (nextCells[i][j].changed == true) {
// there has been a change to the state of this cell, send it
// find the x and y position for this voxel, z = 0
var x = j * cellScale;
var y = i * cellScale;
var type = nextCells[i][j].type;
// queue a packet to add a voxel for the new cell
Voxels.setVoxel(position.x + x, position.y + y, position.z, cellScale, cellTypes[type].r, cellTypes[type].g, cellTypes[type].b);
}
}
}
}
var sentFirstBoard = false;
var voxelViewerInit = false;
var UPDATES_PER_SECOND = 6.0;
var frameIndex = 1.0;
var oldFrameIndex = 0;
var framesToWait = UPDATES_PER_SECOND;
function step(deltaTime) {
if (isLocal == false) {
if (voxelViewerInit == false) {
VoxelViewer.setPosition(viewerPosition);
VoxelViewer.setOrientation(orientation);
voxelViewerInit = true;
}
VoxelViewer.queryOctree();
}
frameIndex += deltaTime * UPDATES_PER_SECOND;
if (Math.floor(frameIndex) == oldFrameIndex) {
return;
}
oldFrameIndex++;
if (frameIndex <= framesToWait) {
return;
}
if (sentFirstBoard) {
// we've already sent the first full board, perform a step in time
updateCells();
} else {
// this will be our first board send
sentFirstBoard = true;
init();
}
if (isLocal == false) {
VoxelViewer.queryOctree();
}
sendNextCells();
}
function scriptEnding() {
Voxels.eraseVoxel(regionMarkerX, regionMarkerY, position.z, cellScale);
}
Script.scriptEnding.connect(scriptEnding);
Script.update.connect(step);
Voxels.setPacketsPerSecond(2000);
// test for local...
Menu.isOptionChecked("Voxels");
isLocal = true; // will only get here on local client

View file

@ -140,6 +140,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
_voxelImporter(NULL),
_importSucceded(false),
_sharedVoxelSystem(TREE_SCALE, DEFAULT_MAX_VOXELS_PER_SYSTEM, &_clipboard),
_modelClipboardRenderer(),
_modelClipboard(),
_wantToKillLocalVoxels(false),
_viewFrustum(),
_lastQueriedViewFrustum(),
@ -174,6 +176,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
_lastNackTime(usecTimestampNow()),
_lastSendDownstreamAudioStats(usecTimestampNow())
{
// read the ApplicationInfo.ini file for Name/Version/Domain information
QSettings applicationInfo(Application::resourcesPath() + "info/ApplicationInfo.ini", QSettings::IniFormat);
@ -1509,6 +1512,33 @@ struct SendVoxelsOperationArgs {
const unsigned char* newBaseOctCode;
};
bool Application::exportModels(const QString& filename, float x, float y, float z, float scale) {
QVector<ModelItem*> models;
_models.getTree()->findModelsInCube(AACube(glm::vec3(x / (float)TREE_SCALE, y / (float)TREE_SCALE, z / (float)TREE_SCALE), scale / (float)TREE_SCALE), models);
if (models.size() > 0) {
glm::vec3 root(x, y, z);
ModelTree exportTree;
for (int i = 0; i < models.size(); i++) {
ModelItemProperties properties;
ModelItemID id = models.at(i)->getModelItemID();
id.isKnownID = false;
properties.copyFromNewModelItem(*models.at(i));
properties.setPosition(properties.getPosition() - root);
exportTree.addModel(id, properties);
}
exportTree.writeToSVOFile(filename.toLocal8Bit().constData());
} else {
qDebug() << "No models were selected";
return false;
}
// restore the main window's active state
_window->activateWindow();
return true;
}
bool Application::sendVoxelsOperation(OctreeElement* element, void* extraData) {
VoxelTreeElement* voxel = (VoxelTreeElement*)element;
SendVoxelsOperationArgs* args = (SendVoxelsOperationArgs*)extraData;
@ -1590,6 +1620,19 @@ void Application::importVoxels() {
emit importDone();
}
bool Application::importModels(const QString& filename) {
_modelClipboard.eraseAllOctreeElements();
bool success = _modelClipboard.readFromSVOFile(filename.toLocal8Bit().constData());
if (success) {
_modelClipboard.reaverageOctreeElements();
}
return success;
}
void Application::pasteModels(float x, float y, float z) {
_modelClipboard.sendModels(&_modelEditSender, x, y, z);
}
void Application::cutVoxels(const VoxelDetail& sourceVoxel) {
copyVoxels(sourceVoxel);
deleteVoxelAt(sourceVoxel);
@ -1753,6 +1796,10 @@ void Application::init() {
_models.init();
_models.setViewFrustum(getViewFrustum());
_modelClipboardRenderer.init();
_modelClipboardRenderer.setViewFrustum(getViewFrustum());
_modelClipboardRenderer.setTree(&_modelClipboard);
_metavoxels.init();
_particleCollisionSystem.init(&_particleEditSender, _particles.getTree(), _voxels.getTree(), &_audio, &_avatarManager);
@ -3674,6 +3721,8 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript
connect(scriptEngine, SIGNAL(finished(const QString&)), this, SLOT(scriptFinished(const QString&)));
connect(scriptEngine, SIGNAL(loadScript(const QString&)), this, SLOT(loadScript(const QString&)));
scriptEngine->registerGlobalObject("Overlays", &_overlays);
QScriptValue windowValue = scriptEngine->registerGlobalObject("Window", WindowScriptingInterface::getInstance());

View file

@ -201,6 +201,8 @@ public:
bool getImportSucceded() { return _importSucceded; }
VoxelSystem* getSharedVoxelSystem() { return &_sharedVoxelSystem; }
VoxelTree* getClipboard() { return &_clipboard; }
ModelTree* getModelClipboard() { return &_modelClipboard; }
ModelTreeRenderer* getModelClipboardRenderer() { return &_modelClipboardRenderer; }
Environment* getEnvironment() { return &_environment; }
bool isMousePressed() const { return _mousePressed; }
bool isMouseHidden() const { return _mouseHidden; }
@ -227,6 +229,7 @@ public:
float getPacketsPerSecond() const { return _packetsPerSecond; }
float getBytesPerSecond() const { return _bytesPerSecond; }
const glm::vec3& getViewMatrixTranslation() const { return _viewMatrixTranslation; }
void setViewMatrixTranslation(const glm::vec3& translation) { _viewMatrixTranslation = translation; }
/// if you need to access the application settings, use lockSettings()/unlockSettings()
QSettings* lockSettings() { _settingsMutex.lock(); return _settings; }
@ -314,6 +317,10 @@ public slots:
void nodeKilled(SharedNodePointer node);
void packetSent(quint64 length);
void pasteModels(float x, float y, float z);
bool exportModels(const QString& filename, float x, float y, float z, float scale);
bool importModels(const QString& filename);
void importVoxels(); // doesn't include source voxel because it goes to clipboard
void cutVoxels(const VoxelDetail& sourceVoxel);
void copyVoxels(const VoxelDetail& sourceVoxel);
@ -462,6 +469,8 @@ private:
ParticleCollisionSystem _particleCollisionSystem;
ModelTreeRenderer _models;
ModelTreeRenderer _modelClipboardRenderer;
ModelTree _modelClipboard;
QByteArray _voxelsFilename;
bool _wantToKillLocalVoxels;

View file

@ -724,7 +724,7 @@ void Audio::handleAudioInput() {
delete[] inputAudioSamples;
}
if (_receivedAudioStream.getPacketReceived() > 0) {
if (_receivedAudioStream.getPacketsReceived() > 0) {
pushAudioToOutput();
}
}
@ -1460,9 +1460,9 @@ void Audio::renderAudioStreamStats(const AudioStreamStats& streamStats, int hori
sprintf(stringBuffer, " Packet loss | overall: %5.2f%% (%d lost), last_30s: %5.2f%% (%d lost)",
streamStats._packetStreamStats.getLostRate() * 100.0f,
streamStats._packetStreamStats._numLost,
streamStats._packetStreamStats._lost,
streamStats._packetStreamWindowStats.getLostRate() * 100.0f,
streamStats._packetStreamWindowStats._numLost);
streamStats._packetStreamWindowStats._lost);
verticalOffset += STATS_HEIGHT_PER_LINE;
drawText(horizontalOffset, verticalOffset, scale, rotation, font, stringBuffer, color);

View file

@ -108,6 +108,12 @@ int RenderVisitor::visit(MetavoxelInfo& info) {
}
void MetavoxelSystem::render() {
// update the frustum
ViewFrustum* viewFrustum = Application::getInstance()->getViewFrustum();
_frustum.set(viewFrustum->getFarTopLeft(), viewFrustum->getFarTopRight(), viewFrustum->getFarBottomLeft(),
viewFrustum->getFarBottomRight(), viewFrustum->getNearTopLeft(), viewFrustum->getNearTopRight(),
viewFrustum->getNearBottomLeft(), viewFrustum->getNearBottomRight());
RenderVisitor renderVisitor(getLOD());
guideToAugmented(renderVisitor);
}
@ -628,13 +634,31 @@ public:
SpannerRenderVisitor(const MetavoxelLOD& lod);
virtual int visit(MetavoxelInfo& info);
virtual bool visit(Spanner* spanner, const glm::vec3& clipMinimum, float clipSize);
private:
int _containmentDepth;
};
SpannerRenderVisitor::SpannerRenderVisitor(const MetavoxelLOD& lod) :
SpannerVisitor(QVector<AttributePointer>() << AttributeRegistry::getInstance()->getSpannersAttribute(),
QVector<AttributePointer>(), QVector<AttributePointer>(), QVector<AttributePointer>(),
lod, encodeOrder(Application::getInstance()->getViewFrustum()->getDirection())) {
lod, encodeOrder(Application::getInstance()->getViewFrustum()->getDirection())),
_containmentDepth(INT_MAX) {
}
int SpannerRenderVisitor::visit(MetavoxelInfo& info) {
if (_containmentDepth >= _depth) {
Frustum::IntersectionType intersection = Application::getInstance()->getMetavoxels()->getFrustum().getIntersectionType(
info.getBounds());
if (intersection == Frustum::NO_INTERSECTION) {
return STOP_RECURSION;
}
_containmentDepth = (intersection == Frustum::CONTAINS_INTERSECTION) ? _depth : INT_MAX;
}
return SpannerVisitor::visit(info);
}
bool SpannerRenderVisitor::visit(Spanner* spanner, const glm::vec3& clipMinimum, float clipSize) {
@ -652,14 +676,24 @@ public:
private:
int _order;
int _containmentDepth;
};
BufferRenderVisitor::BufferRenderVisitor(const AttributePointer& attribute, const MetavoxelLOD& lod) :
MetavoxelVisitor(QVector<AttributePointer>() << attribute, QVector<AttributePointer>(), lod),
_order(encodeOrder(Application::getInstance()->getViewFrustum()->getDirection())) {
_order(encodeOrder(Application::getInstance()->getViewFrustum()->getDirection())),
_containmentDepth(INT_MAX) {
}
int BufferRenderVisitor::visit(MetavoxelInfo& info) {
if (_containmentDepth >= _depth) {
Frustum::IntersectionType intersection = Application::getInstance()->getMetavoxels()->getFrustum().getIntersectionType(
info.getBounds());
if (intersection == Frustum::NO_INTERSECTION) {
return STOP_RECURSION;
}
_containmentDepth = (intersection == Frustum::CONTAINS_INTERSECTION) ? _depth : INT_MAX;
}
BufferDataPointer buffer = info.inputValues.at(0).getInlineValue<BufferDataPointer>();
if (buffer) {
buffer->render();

View file

@ -34,6 +34,8 @@ public:
virtual void init();
virtual MetavoxelLOD getLOD();
const Frustum& getFrustum() const { return _frustum; }
const AttributePointer& getPointBufferAttribute() { return _pointBufferAttribute; }
const AttributePointer& getHeightfieldBufferAttribute() { return _heightfieldBufferAttribute; }
@ -56,6 +58,7 @@ private:
MetavoxelLOD _lod;
QReadWriteLock _lodLock;
Frustum _frustum;
};
/// Describes contents of a point in a point buffer.

View file

@ -37,6 +37,7 @@ static QString sampleJson = "[{\"id\":1, \
static const glm::vec3 DEFAULT_HEAD_ORIGIN(0.0f, 0.0f, 0.0f);
static const float TRANSLATION_SCALE = 1.0f;
static const int NUM_BLENDSHAPE_COEFF = 30;
static const int NUM_SMOOTHING_SAMPLES = 3;
struct CaraPerson {
struct CaraPose {
@ -217,9 +218,9 @@ private:
CaraFaceTracker::CaraFaceTracker() :
_lastReceiveTimestamp(0),
_previousPitch(0.0f),
_previousYaw(0.0f),
_previousRoll(0.0f),
_pitchAverage(NUM_SMOOTHING_SAMPLES),
_yawAverage(NUM_SMOOTHING_SAMPLES),
_rollAverage(NUM_SMOOTHING_SAMPLES),
_eyeGazeLeftPitch(0.0f),
_eyeGazeLeftYaw(0.0f),
_eyeGazeRightPitch(0.0f),
@ -252,9 +253,9 @@ CaraFaceTracker::CaraFaceTracker() :
CaraFaceTracker::CaraFaceTracker(const QHostAddress& host, quint16 port) :
_lastReceiveTimestamp(0),
_previousPitch(0.0f),
_previousYaw(0.0f),
_previousRoll(0.0f),
_pitchAverage(NUM_SMOOTHING_SAMPLES),
_yawAverage(NUM_SMOOTHING_SAMPLES),
_rollAverage(NUM_SMOOTHING_SAMPLES),
_eyeGazeLeftPitch(0.0f),
_eyeGazeLeftYaw(0.0f),
_eyeGazeRightPitch(0.0f),
@ -371,6 +372,7 @@ void CaraFaceTracker::decodePacket(const QByteArray& buffer) {
CaraPerson person = CaraPacketDecoder::extractOne(buffer, &jsonError);
if(jsonError.error == QJsonParseError::NoError) {
//do some noise filtering to the head poses
//reduce the noise first by truncating to 1 dp
person.pose.roll = glm::floor(person.pose.roll * 10) / 10;
@ -386,43 +388,39 @@ void CaraFaceTracker::decodePacket(const QByteArray& buffer) {
float theta = 2 * acos(r.w);
if (theta > EPSILON) {
float rMag = glm::length(glm::vec3(r.x, r.y, r.z));
const float AVERAGE_CARA_FRAME_TIME = 0.033f;
const float AVERAGE_CARA_FRAME_TIME = 0.04f;
const float ANGULAR_VELOCITY_MIN = 1.2f;
const float YAW_STANDARD_DEV_DEG = 2.5f;
_headAngularVelocity = theta / AVERAGE_CARA_FRAME_TIME * glm::vec3(r.x, r.y, r.z) / rMag;
_pitchAverage.updateAverage(person.pose.pitch);
_rollAverage.updateAverage(person.pose.roll);
//use the angular velocity for roll and pitch, if it's below the threshold don't move
if(glm::abs(_headAngularVelocity.x) < ANGULAR_VELOCITY_MIN) {
person.pose.pitch = _previousPitch;
}
//could use the angular velocity to detemine whether to update pitch and roll to further remove the noise.
//use the angular velocity for roll and pitch, update if > THRESHOLD
//if(glm::abs(_headAngularVelocity.x) > ANGULAR_VELOCITY_MIN) {
// _pitchAverage.updateAverage(person.pose.pitch);
//}
if(glm::abs(_headAngularVelocity.z) < ANGULAR_VELOCITY_MIN) {
person.pose.roll = _previousRoll;
}
//if(glm::abs(_headAngularVelocity.z) > ANGULAR_VELOCITY_MIN) {
// _rollAverage.updateAverage(person.pose.roll);;
//}
//for yaw, the jitter is great, you can't use angular velocity because it swings too much
//use the previous and current yaw, calculate the
//abs difference and move it the difference is above the standard deviation which is around 2.5
// (this will introduce some jerks but will not encounter lag)
// < the standard deviation 2.5 deg, no move
if(glm::abs(person.pose.yaw - _previousYaw) < YAW_STANDARD_DEV_DEG) {
// > the standard deviation 2.5 deg, update the yaw smoothing average
if(glm::abs(person.pose.yaw - _yawAverage.getAverage()) > YAW_STANDARD_DEV_DEG) {
//qDebug() << "Yaw Diff: " << glm::abs(person.pose.yaw - _previousYaw);
person.pose.yaw = _previousYaw;
_yawAverage.updateAverage(person.pose.yaw);
}
//update the previous angles
_previousPitch = person.pose.pitch;
_previousYaw = person.pose.yaw;
_previousRoll = person.pose.roll;
//set the new rotation
newRotation = glm::quat(glm::vec3(DEGTORAD(person.pose.pitch), DEGTORAD(person.pose.yaw), DEGTORAD(-person.pose.roll)));
newRotation = glm::quat(glm::vec3(DEGTORAD(_pitchAverage.getAverage()), DEGTORAD(_yawAverage.getAverage()), DEGTORAD(-_rollAverage.getAverage())));
}
else {
//no change in position
newRotation = glm::quat(glm::vec3(DEGTORAD(_previousPitch), DEGTORAD(_previousYaw), DEGTORAD(-_previousRoll)));
//no change in position, use previous averages
newRotation = glm::quat(glm::vec3(DEGTORAD(_pitchAverage.getAverage()), DEGTORAD(_yawAverage.getAverage()), DEGTORAD(-_rollAverage.getAverage())));
_headAngularVelocity = glm::vec3(0,0,0);
}
@ -456,4 +454,3 @@ void CaraFaceTracker::decodePacket(const QByteArray& buffer) {
float CaraFaceTracker::getBlendshapeCoefficient(int index) const {
return (index >= 0 && index < (int)_blendshapeCoefficients.size()) ? _blendshapeCoefficients[index] : 0.0f;
}

View file

@ -14,6 +14,7 @@
#include <QUdpSocket>
#include <SimpleMovingAverage.h>
#include "FaceTracker.h"
/*!
@ -90,10 +91,10 @@ private:
//head tracking
glm::vec3 _headAngularVelocity;
//pose history
float _previousPitch;
float _previousYaw;
float _previousRoll;
//pose average
SimpleMovingAverage _pitchAverage;
SimpleMovingAverage _yawAverage;
SimpleMovingAverage _rollAverage;
// eye gaze degrees
float _eyeGazeLeftPitch;

View file

@ -100,3 +100,16 @@ void ClipboardScriptingInterface::nudgeVoxel(float x, float y, float z, float s,
Application::getInstance()->nudgeVoxelsByVector(sourceVoxel, nudgeVecInTreeSpace);
}
bool ClipboardScriptingInterface::exportModels(const QString& filename, float x, float y, float z, float s) {
return Application::getInstance()->exportModels(filename, x, y, z, s);
}
bool ClipboardScriptingInterface::importModels(const QString& filename) {
return Application::getInstance()->importModels(filename);
}
void ClipboardScriptingInterface::pasteModels(float x, float y, float z, float s) {
Application::getInstance()->pasteModels(x, y, z);
}

View file

@ -42,6 +42,10 @@ public slots:
void nudgeVoxel(const VoxelDetail& sourceVoxel, const glm::vec3& nudgeVec);
void nudgeVoxel(float x, float y, float z, float s, const glm::vec3& nudgeVec);
bool importModels(const QString& filename);
bool exportModels(const QString& filename, float x, float y, float z, float s);
void pasteModels(float x, float y, float z, float s);
};
#endif // hifi_ClipboardScriptingInterface_h

View file

@ -67,6 +67,15 @@ QScriptValue WindowScriptingInterface::browse(const QString& title, const QStrin
return retVal;
}
QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) {
QScriptValue retVal;
QMetaObject::invokeMethod(this, "showBrowse", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QScriptValue, retVal),
Q_ARG(const QString&, title), Q_ARG(const QString&, directory), Q_ARG(const QString&, nameFilter),
Q_ARG(QFileDialog::AcceptMode, QFileDialog::AcceptSave));
return retVal;
}
QScriptValue WindowScriptingInterface::s3Browse(const QString& nameFilter) {
QScriptValue retVal;
QMetaObject::invokeMethod(this, "showS3Browse", Qt::BlockingQueuedConnection,
@ -182,18 +191,26 @@ QScriptValue WindowScriptingInterface::showPrompt(const QString& message, const
/// \param const QString& directory directory to start the file browser at
/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog`
/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue`
QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QString& directory, const QString& nameFilter) {
QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QString& directory, const QString& nameFilter,
QFileDialog::AcceptMode acceptMode) {
// On OS X `directory` does not work as expected unless a file is included in the path, so we append a bogus
// filename if the directory is valid.
QString path = "";
QFileInfo fileInfo = QFileInfo(directory);
qDebug() << "File: " << directory << fileInfo.isFile();
if (fileInfo.isDir()) {
fileInfo.setFile(directory, "__HIFI_INVALID_FILE__");
path = fileInfo.filePath();
}
QFileDialog fileDialog(Application::getInstance()->getWindow(), title, path, nameFilter);
fileDialog.setFileMode(QFileDialog::ExistingFile);
fileDialog.setAcceptMode(acceptMode);
qDebug() << "Opening!";
QUrl fileUrl(directory);
if (acceptMode == QFileDialog::AcceptSave) {
fileDialog.setFileMode(QFileDialog::Directory);
fileDialog.selectFile(fileUrl.fileName());
}
if (fileDialog.exec()) {
return QScriptValue(fileDialog.selectedFiles().first());
}

View file

@ -31,6 +31,7 @@ public slots:
QScriptValue form(const QString& title, QScriptValue array);
QScriptValue prompt(const QString& message = "", const QString& defaultText = "");
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
QScriptValue s3Browse(const QString& nameFilter = "");
private slots:
@ -38,7 +39,8 @@ private slots:
QScriptValue showConfirm(const QString& message);
QScriptValue showForm(const QString& title, QScriptValue form);
QScriptValue showPrompt(const QString& message, const QString& defaultText);
QScriptValue showBrowse(const QString& title, const QString& directory, const QString& nameFilter);
QScriptValue showBrowse(const QString& title, const QString& directory, const QString& nameFilter,
QFileDialog::AcceptMode acceptMode = QFileDialog::AcceptOpen);
QScriptValue showS3Browse(const QString& nameFilter);
private:

View file

@ -26,6 +26,7 @@
#include <QPushButton>
#include <QRunnable>
#include <QScrollArea>
#include <QSpinBox>
#include <QThreadPool>
#include <QVBoxLayout>
@ -116,6 +117,7 @@ MetavoxelEditor::MetavoxelEditor() :
addTool(new ClearSpannersTool(this));
addTool(new SetSpannerTool(this));
addTool(new ImportHeightfieldTool(this));
addTool(new EraseHeightfieldTool(this));
updateAttributes();
@ -895,40 +897,68 @@ void SetSpannerTool::applyEdit(const AttributePointer& attribute, const SharedOb
spannerData->getVoxelizationGranularity(), directionImages));
}
ImportHeightfieldTool::ImportHeightfieldTool(MetavoxelEditor* editor) :
MetavoxelTool(editor, "Import Heightfield", false) {
HeightfieldTool::HeightfieldTool(MetavoxelEditor* editor, const QString& name) :
MetavoxelTool(editor, name, false) {
QWidget* widget = new QWidget();
QFormLayout* form = new QFormLayout();
widget->setLayout(form);
widget->setLayout(_form = new QFormLayout());
layout()->addWidget(widget);
form->addRow("Translation:", _translation = new Vec3Editor(widget));
form->addRow("Scale:", _scale = new QDoubleSpinBox());
_form->addRow("Translation:", _translation = new Vec3Editor(widget));
_form->addRow("Scale:", _scale = new QDoubleSpinBox());
_scale->setMinimum(-FLT_MAX);
_scale->setMaximum(FLT_MAX);
_scale->setPrefix("2^");
_scale->setValue(3.0);
form->addRow("Height:", _height = new QPushButton());
connect(_height, &QAbstractButton::clicked, this, &ImportHeightfieldTool::selectHeightFile);
form->addRow("Color:", _color = new QPushButton());
connect(_color, &QAbstractButton::clicked, this, &ImportHeightfieldTool::selectColorFile);
QPushButton* applyButton = new QPushButton("Apply");
layout()->addWidget(applyButton);
connect(applyButton, &QAbstractButton::clicked, this, &ImportHeightfieldTool::apply);
connect(applyButton, &QAbstractButton::clicked, this, &HeightfieldTool::apply);
}
bool ImportHeightfieldTool::appliesTo(const AttributePointer& attribute) const {
bool HeightfieldTool::appliesTo(const AttributePointer& attribute) const {
return attribute->inherits("HeightfieldAttribute");
}
void ImportHeightfieldTool::render() {
void HeightfieldTool::render() {
float scale = pow(2.0, _scale->value());
_translation->setSingleStep(scale);
glm::vec3 quantizedTranslation = scale * glm::floor(_translation->getValue() / scale);
_translation->setValue(quantizedTranslation);
_preview.render(quantizedTranslation, scale);
}
ImportHeightfieldTool::ImportHeightfieldTool(MetavoxelEditor* editor) :
HeightfieldTool(editor, "Import Heightfield") {
_form->addRow("Height:", _height = new QPushButton());
connect(_height, &QAbstractButton::clicked, this, &ImportHeightfieldTool::selectHeightFile);
_form->addRow("Color:", _color = new QPushButton());
connect(_color, &QAbstractButton::clicked, this, &ImportHeightfieldTool::selectColorFile);
}
void ImportHeightfieldTool::render() {
HeightfieldTool::render();
_preview.render(_translation->getValue(), _translation->getSingleStep());
}
void ImportHeightfieldTool::apply() {
float scale = _translation->getSingleStep();
foreach (const BufferDataPointer& bufferData, _preview.getBuffers()) {
HeightfieldBuffer* buffer = static_cast<HeightfieldBuffer*>(bufferData.data());
MetavoxelData data;
data.setSize(scale);
HeightfieldDataPointer heightPointer(new HeightfieldData(buffer->getHeight()));
data.setRoot(AttributeRegistry::getInstance()->getHeightfieldAttribute(), new MetavoxelNode(AttributeValue(
AttributeRegistry::getInstance()->getHeightfieldAttribute(), encodeInline(heightPointer))));
if (!buffer->getColor().isEmpty()) {
HeightfieldDataPointer colorPointer(new HeightfieldData(buffer->getColor()));
data.setRoot(AttributeRegistry::getInstance()->getHeightfieldColorAttribute(), new MetavoxelNode(AttributeValue(
AttributeRegistry::getInstance()->getHeightfieldColorAttribute(), encodeInline(colorPointer))));
}
MetavoxelEditMessage message = { QVariant::fromValue(SetDataEdit(
_translation->getValue() + buffer->getTranslation() * scale, data)) };
Application::getInstance()->getMetavoxels()->applyEdit(message, true);
}
}
void ImportHeightfieldTool::selectHeightFile() {
@ -959,26 +989,6 @@ void ImportHeightfieldTool::selectColorFile() {
updatePreview();
}
void ImportHeightfieldTool::apply() {
float scale = pow(2.0, _scale->value());
foreach (const BufferDataPointer& bufferData, _preview.getBuffers()) {
HeightfieldBuffer* buffer = static_cast<HeightfieldBuffer*>(bufferData.data());
MetavoxelData data;
data.setSize(scale);
HeightfieldDataPointer heightPointer(new HeightfieldData(buffer->getHeight()));
data.setRoot(AttributeRegistry::getInstance()->getHeightfieldAttribute(), new MetavoxelNode(AttributeValue(
AttributeRegistry::getInstance()->getHeightfieldAttribute(), encodeInline(heightPointer))));
if (!buffer->getColor().isEmpty()) {
HeightfieldDataPointer colorPointer(new HeightfieldData(buffer->getColor()));
data.setRoot(AttributeRegistry::getInstance()->getHeightfieldColorAttribute(), new MetavoxelNode(AttributeValue(
AttributeRegistry::getInstance()->getHeightfieldColorAttribute(), encodeInline(colorPointer))));
}
MetavoxelEditMessage message = { QVariant::fromValue(SetDataEdit(
_translation->getValue() + buffer->getTranslation() * scale, data)) };
Application::getInstance()->getMetavoxels()->applyEdit(message, true);
}
}
const int BLOCK_SIZE = 32;
const int BLOCK_ADVANCEMENT = BLOCK_SIZE - 1;
@ -1018,3 +1028,49 @@ void ImportHeightfieldTool::updatePreview() {
}
_preview.setBuffers(buffers);
}
EraseHeightfieldTool::EraseHeightfieldTool(MetavoxelEditor* editor) :
HeightfieldTool(editor, "Erase Heightfield") {
_form->addRow("Width:", _width = new QSpinBox());
_width->setMinimum(1);
_width->setMaximum(INT_MAX);
_form->addRow("Length:", _length = new QSpinBox());
_length->setMinimum(1);
_length->setMaximum(INT_MAX);
}
void EraseHeightfieldTool::render() {
HeightfieldTool::render();
glColor3f(1.0f, 0.0f, 0.0f);
glLineWidth(4.0f);
glPushMatrix();
glm::vec3 translation = _translation->getValue();
glTranslatef(translation.x, translation.y, translation.z);
float scale = _translation->getSingleStep();
glScalef(scale * _width->value(), scale, scale * _length->value());
glTranslatef(0.5f, 0.5f, 0.5f);
glutWireCube(1.0);
glPopMatrix();
glLineWidth(1.0f);
}
void EraseHeightfieldTool::apply() {
// clear the heightfield
float scale = _translation->getSingleStep();
BoxSetEdit edit(Box(_translation->getValue(), _translation->getValue() +
glm::vec3(_width->value() * scale, scale, _length->value() * scale)), scale,
OwnedAttributeValue(AttributeRegistry::getInstance()->getHeightfieldAttribute()));
MetavoxelEditMessage message = { QVariant::fromValue(edit) };
Application::getInstance()->getMetavoxels()->applyEdit(message, true);
// and the color
edit.value = OwnedAttributeValue(AttributeRegistry::getInstance()->getHeightfieldColorAttribute());
message.edit = QVariant::fromValue(edit);
Application::getInstance()->getMetavoxels()->applyEdit(message, true);
}

View file

@ -24,6 +24,7 @@ class QGroupBox;
class QListWidget;
class QPushButton;
class QScrollArea;
class QSpinBox;
class MetavoxelTool;
class Vec3Editor;
@ -225,30 +226,52 @@ protected:
virtual void applyEdit(const AttributePointer& attribute, const SharedObjectPointer& spanner);
};
/// Base class for heightfield tools.
class HeightfieldTool : public MetavoxelTool {
Q_OBJECT
public:
HeightfieldTool(MetavoxelEditor* editor, const QString& name);
virtual bool appliesTo(const AttributePointer& attribute) const;
virtual void render();
protected slots:
virtual void apply() = 0;
protected:
QFormLayout* _form;
Vec3Editor* _translation;
QDoubleSpinBox* _scale;
};
/// Allows importing a heightfield.
class ImportHeightfieldTool : public MetavoxelTool {
class ImportHeightfieldTool : public HeightfieldTool {
Q_OBJECT
public:
ImportHeightfieldTool(MetavoxelEditor* editor);
virtual bool appliesTo(const AttributePointer& attribute) const;
virtual void render();
protected:
virtual void apply();
private slots:
void selectHeightFile();
void selectColorFile();
void apply();
private:
void updatePreview();
Vec3Editor* _translation;
QDoubleSpinBox* _scale;
QPushButton* _height;
QPushButton* _color;
@ -258,4 +281,24 @@ private:
HeightfieldPreview _preview;
};
// Allows clearing heighfield blocks.
class EraseHeightfieldTool : public HeightfieldTool {
Q_OBJECT
public:
EraseHeightfieldTool(MetavoxelEditor* editor);
virtual void render();
protected:
virtual void apply();
private:
QSpinBox* _width;
QSpinBox* _length;
};
#endif // hifi_MetavoxelEditor_h

View file

@ -366,13 +366,12 @@ void OctreeStatsDialog::showOctreeServersOfType(int& serverCount, NodeType_t ser
QString incomingBytesString = locale.toString((uint)stats.getIncomingBytes());
QString incomingWastedBytesString = locale.toString((uint)stats.getIncomingWastedBytes());
const SequenceNumberStats& seqStats = stats.getIncomingOctreeSequenceNumberStats();
QString incomingOutOfOrderString = locale.toString((uint)seqStats.getNumOutOfOrder());
QString incomingLateString = locale.toString((uint)seqStats.getNumLate());
QString incomingUnreasonableString = locale.toString((uint)seqStats.getNumUnreasonable());
QString incomingEarlyString = locale.toString((uint)seqStats.getNumEarly());
QString incomingLikelyLostString = locale.toString((uint)seqStats.getNumLost());
QString incomingRecovered = locale.toString((uint)seqStats.getNumRecovered());
QString incomingDuplicateString = locale.toString((uint)seqStats.getNumDuplicate());
QString incomingOutOfOrderString = locale.toString((uint)seqStats.getOutOfOrder());
QString incomingLateString = locale.toString((uint)seqStats.getLate());
QString incomingUnreasonableString = locale.toString((uint)seqStats.getUnreasonable());
QString incomingEarlyString = locale.toString((uint)seqStats.getEarly());
QString incomingLikelyLostString = locale.toString((uint)seqStats.getLost());
QString incomingRecovered = locale.toString((uint)seqStats.getRecovered());
int clockSkewInMS = node->getClockSkewUsec() / (int)USECS_PER_MSEC;
QString incomingFlightTimeString = locale.toString((int)stats.getIncomingFlightTimeAverage());
@ -386,8 +385,7 @@ void OctreeStatsDialog::showOctreeServersOfType(int& serverCount, NodeType_t ser
serverDetails << "<br/>" << " Out of Order: " << qPrintable(incomingOutOfOrderString) <<
"/ Early: " << qPrintable(incomingEarlyString) <<
"/ Late: " << qPrintable(incomingLateString) <<
"/ Unreasonable: " << qPrintable(incomingUnreasonableString) <<
"/ Duplicate: " << qPrintable(incomingDuplicateString);
"/ Unreasonable: " << qPrintable(incomingUnreasonableString);
serverDetails << "<br/>" <<
" Average Flight Time: " << qPrintable(incomingFlightTimeString) << " msecs";

View file

@ -0,0 +1,40 @@
//
// LocalModelsOverlay.cpp
// interface/src/ui/overlays
//
// Created by Ryan Huffman on 07/08/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
//
#include "Application.h"
#include "LocalModelsOverlay.h"
LocalModelsOverlay::LocalModelsOverlay(ModelTreeRenderer* modelTreeRenderer) :
Volume3DOverlay(),
_modelTreeRenderer(modelTreeRenderer) {
}
LocalModelsOverlay::~LocalModelsOverlay() {
}
void LocalModelsOverlay::update(float deltatime) {
_modelTreeRenderer->update();
}
void LocalModelsOverlay::render() {
if (_visible) {
glPushMatrix(); {
Application* app = Application::getInstance();
glm::vec3 oldTranslation = app->getViewMatrixTranslation();
app->setViewMatrixTranslation(oldTranslation + _position);
_modelTreeRenderer->render();
Application::getInstance()->setViewMatrixTranslation(oldTranslation);
} glPopMatrix();
}
}

View file

@ -0,0 +1,32 @@
//
// LocalModelsOverlay.h
// interface/src/ui/overlays
//
// Created by Ryan Huffman on 07/08/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_LocalModelsOverlay_h
#define hifi_LocalModelsOverlay_h
#include "models/ModelTreeRenderer.h"
#include "Volume3DOverlay.h"
class LocalModelsOverlay : public Volume3DOverlay {
Q_OBJECT
public:
LocalModelsOverlay(ModelTreeRenderer* modelTreeRenderer);
~LocalModelsOverlay();
virtual void update(float deltatime);
virtual void render();
private:
ModelTreeRenderer *_modelTreeRenderer;
};
#endif // hifi_LocalModelsOverlay_h

View file

@ -14,6 +14,7 @@
#include "Cube3DOverlay.h"
#include "ImageOverlay.h"
#include "Line3DOverlay.h"
#include "LocalModelsOverlay.h"
#include "LocalVoxelsOverlay.h"
#include "ModelOverlay.h"
#include "Overlays.h"
@ -158,6 +159,12 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope
thisOverlay->setProperties(properties);
created = true;
is3D = true;
} else if (type == "localmodels") {
thisOverlay = new LocalModelsOverlay(Application::getInstance()->getModelClipboardRenderer());
thisOverlay->init(_parent);
thisOverlay->setProperties(properties);
created = true;
is3D = true;
} else if (type == "model") {
thisOverlay = new ModelOverlay();
thisOverlay->init(_parent);

View file

@ -212,7 +212,7 @@ SequenceNumberStats::ArrivalInfo InboundAudioStream::frameReceivedUpdateNetworkS
// discard the first few packets we receive since they usually have gaps that aren't represensative of normal jitter
const int NUM_INITIAL_PACKETS_DISCARD = 3;
quint64 now = usecTimestampNow();
if (_incomingSequenceNumberStats.getNumReceived() > NUM_INITIAL_PACKETS_DISCARD) {
if (_incomingSequenceNumberStats.getReceived() > NUM_INITIAL_PACKETS_DISCARD) {
quint64 gap = now - _lastFrameReceivedTime;
_interframeTimeGapStatsForStatsPacket.update(gap);

View file

@ -108,7 +108,7 @@ public:
int getSilentFramesDropped() const { return _silentFramesDropped; }
int getOverflowCount() const { return _ringBuffer.getOverflowCount(); }
int getPacketReceived() const { return _incomingSequenceNumberStats.getNumReceived(); }
int getPacketsReceived() const { return _incomingSequenceNumberStats.getReceived(); }
private:
void starved();

View file

@ -315,6 +315,136 @@ QDebug& operator<<(QDebug& dbg, const Box& box) {
return dbg.nospace() << "{type='Box', minimum=" << box.minimum << ", maximum=" << box.maximum << "}";
}
AxisExtents::AxisExtents(const glm::vec3& first0, const glm::vec3& first1, const glm::vec3& first2, const glm::vec3& second) :
axis(glm::cross(first2 - first1, first0 - first1)),
minimum(glm::dot(first1, axis)),
maximum(glm::dot(second, axis)) {
}
AxisExtents::AxisExtents(const glm::vec3& axis, float minimum, float maximum) :
axis(axis),
minimum(minimum),
maximum(maximum) {
}
void Frustum::set(const glm::vec3& farTopLeft, const glm::vec3& farTopRight, const glm::vec3& farBottomLeft,
const glm::vec3& farBottomRight, const glm::vec3& nearTopLeft, const glm::vec3& nearTopRight,
const glm::vec3& nearBottomLeft, const glm::vec3& nearBottomRight) {
_vertices[0] = farBottomLeft;
_vertices[1] = farBottomRight;
_vertices[2] = farTopLeft;
_vertices[3] = farTopRight;
_vertices[4] = nearBottomLeft;
_vertices[5] = nearBottomRight;
_vertices[6] = nearTopLeft;
_vertices[7] = nearTopRight;
// compute the bounds
_bounds.minimum = glm::vec3(FLT_MAX, FLT_MAX, FLT_MAX);
_bounds.maximum = glm::vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX);
for (int i = 0; i < VERTEX_COUNT; i++) {
_bounds.minimum = glm::min(_bounds.minimum, _vertices[i]);
_bounds.maximum = glm::max(_bounds.maximum, _vertices[i]);
}
// compute the extents for each side
_sideExtents[0] = AxisExtents(nearBottomLeft, nearTopLeft, nearTopRight, farBottomLeft);
_sideExtents[1] = AxisExtents(nearBottomLeft, farBottomLeft, farTopLeft, farBottomRight);
_sideExtents[2] = AxisExtents(nearBottomRight, nearTopRight, farTopRight, farBottomLeft);
_sideExtents[3] = AxisExtents(nearBottomLeft, nearBottomRight, farBottomRight, farTopLeft);
_sideExtents[4] = AxisExtents(nearTopLeft, farTopLeft, farTopRight, farBottomRight);
// the other set of extents are derived from the cross products of the frustum and box edges
glm::vec3 edges[] = { nearBottomRight - nearBottomLeft, nearTopLeft - nearBottomLeft, farBottomLeft - nearBottomLeft,
farBottomRight - nearBottomRight, farTopLeft - nearTopLeft, farTopRight - nearTopRight };
const int AXIS_COUNT = 3;
for (uint i = 0, extentIndex = 0; i < sizeof(edges) / sizeof(edges[0]); i++) {
for (int j = 0; j < AXIS_COUNT; j++) {
glm::vec3 axis;
axis[j] = 1.0f;
glm::vec3 crossProduct = glm::cross(edges[i], axis);
float minimum = FLT_MAX, maximum = -FLT_MAX;
for (int k = 0; k < VERTEX_COUNT; k++) {
float projection = glm::dot(crossProduct, _vertices[k]);
minimum = glm::min(minimum, projection);
maximum = glm::max(maximum, projection);
}
_crossProductExtents[extentIndex++] = AxisExtents(crossProduct, minimum, maximum);
}
}
}
Frustum::IntersectionType Frustum::getIntersectionType(const Box& box) const {
// first check the bounds (equivalent to checking frustum vertices against box extents)
if (!_bounds.intersects(box)) {
return NO_INTERSECTION;
}
// check box vertices against side extents
bool allInside = true;
for (int i = 0; i < SIDE_EXTENT_COUNT; i++) {
const AxisExtents& extents = _sideExtents[i];
float firstProjection = glm::dot(box.getVertex(0), extents.axis);
if (firstProjection < extents.minimum) {
allInside = false;
for (int j = 1; j < Box::VERTEX_COUNT; j++) {
if (glm::dot(box.getVertex(j), extents.axis) >= extents.minimum) {
goto sideContinue;
}
}
return NO_INTERSECTION;
} else if (firstProjection > extents.maximum) {
allInside = false;
for (int j = 1; j < Box::VERTEX_COUNT; j++) {
if (glm::dot(box.getVertex(j), extents.axis) <= extents.maximum) {
goto sideContinue;
}
}
return NO_INTERSECTION;
} else if (allInside) {
for (int j = 1; j < Box::VERTEX_COUNT; j++) {
float projection = glm::dot(box.getVertex(j), extents.axis);
if (projection < extents.minimum || projection > extents.maximum) {
allInside = false;
goto sideContinue;
}
}
}
sideContinue: ;
}
if (allInside) {
return CONTAINS_INTERSECTION;
}
// check box vertices against cross product extents
for (int i = 0; i < CROSS_PRODUCT_EXTENT_COUNT; i++) {
const AxisExtents& extents = _crossProductExtents[i];
float firstProjection = glm::dot(box.getVertex(0), extents.axis);
if (firstProjection < extents.minimum) {
for (int j = 1; j < Box::VERTEX_COUNT; j++) {
if (glm::dot(box.getVertex(j), extents.axis) >= extents.minimum) {
goto crossProductContinue;
}
}
return NO_INTERSECTION;
} else if (firstProjection > extents.maximum) {
for (int j = 1; j < Box::VERTEX_COUNT; j++) {
if (glm::dot(box.getVertex(j), extents.axis) <= extents.maximum) {
goto crossProductContinue;
}
}
return NO_INTERSECTION;
}
crossProductContinue: ;
}
return PARTIAL_INTERSECTION;
}
QMetaObjectEditor::QMetaObjectEditor(QWidget* parent) : QWidget(parent) {
QVBoxLayout* layout = new QVBoxLayout();
layout->setContentsMargins(QMargins());
@ -407,6 +537,10 @@ void BaseVec3Editor::setSingleStep(double singleStep) {
_z->setSingleStep(singleStep);
}
double BaseVec3Editor::getSingleStep() const {
return _x->singleStep();
}
QDoubleSpinBox* BaseVec3Editor::createComponentBox() {
QDoubleSpinBox* box = new QDoubleSpinBox();
box->setMinimum(-FLT_MAX);

View file

@ -65,6 +65,43 @@ Box operator*(const glm::mat4& matrix, const Box& box);
QDebug& operator<<(QDebug& out, const Box& box);
/// Represents the extents along an axis.
class AxisExtents {
public:
glm::vec3 axis;
float minimum;
float maximum;
/// Creates a set of extents given three points on the first plane and one on the second.
AxisExtents(const glm::vec3& first0, const glm::vec3& first1, const glm::vec3& first2, const glm::vec3& second);
AxisExtents(const glm::vec3& axis = glm::vec3(), float minimum = 0.0f, float maximum = 0.0f);
};
/// A simple pyramidal frustum for intersection testing.
class Frustum {
public:
void set(const glm::vec3& farTopLeft, const glm::vec3& farTopRight, const glm::vec3& farBottomLeft,
const glm::vec3& farBottomRight, const glm::vec3& nearTopLeft, const glm::vec3& nearTopRight,
const glm::vec3& nearBottomLeft, const glm::vec3& nearBottomRight);
enum IntersectionType { NO_INTERSECTION, PARTIAL_INTERSECTION, CONTAINS_INTERSECTION };
IntersectionType getIntersectionType(const Box& box) const;
private:
static const int VERTEX_COUNT = 8;
static const int SIDE_EXTENT_COUNT = 5;
static const int CROSS_PRODUCT_EXTENT_COUNT = 18;
glm::vec3 _vertices[VERTEX_COUNT];
Box _bounds;
AxisExtents _sideExtents[SIDE_EXTENT_COUNT];
AxisExtents _crossProductExtents[CROSS_PRODUCT_EXTENT_COUNT];
};
/// Editor for meta-object values.
class QMetaObjectEditor : public QWidget {
Q_OBJECT
@ -154,6 +191,7 @@ public:
BaseVec3Editor(QWidget* parent);
void setSingleStep(double singleStep);
double getSingleStep() const;
protected slots:

View file

@ -1157,6 +1157,38 @@ void ModelItemProperties::copyFromModelItem(const ModelItem& modelItem) {
_defaultSettings = false;
}
void ModelItemProperties::copyFromNewModelItem(const ModelItem& modelItem) {
_position = modelItem.getPosition() * (float) TREE_SCALE;
_color = modelItem.getXColor();
_radius = modelItem.getRadius() * (float) TREE_SCALE;
_shouldDie = modelItem.getShouldDie();
_modelURL = modelItem.getModelURL();
_modelRotation = modelItem.getModelRotation();
_animationURL = modelItem.getAnimationURL();
_animationIsPlaying = modelItem.getAnimationIsPlaying();
_animationFrameIndex = modelItem.getAnimationFrameIndex();
_animationFPS = modelItem.getAnimationFPS();
_glowLevel = modelItem.getGlowLevel();
_sittingPoints = modelItem.getSittingPoints();
_id = modelItem.getID();
_idSet = true;
_positionChanged = true;
_colorChanged = true;
_radiusChanged = true;
_shouldDieChanged = true;
_modelURLChanged = true;
_modelRotationChanged = true;
_animationURLChanged = true;
_animationIsPlayingChanged = true;
_animationFrameIndexChanged = true;
_animationFPSChanged = true;
_glowLevelChanged = true;
_defaultSettings = true;
}
QScriptValue ModelItemPropertiesToScriptValue(QScriptEngine* engine, const ModelItemProperties& properties) {
return properties.copyToScriptValue(engine);
}

View file

@ -74,6 +74,7 @@ public:
void copyToModelItem(ModelItem& modelItem) const;
void copyFromModelItem(const ModelItem& modelItem);
void copyFromNewModelItem(const ModelItem& modelItem);
const glm::vec3& getPosition() const { return _position; }
xColor getColor() const { return _color; }

View file

@ -9,6 +9,9 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "ModelEditPacketSender.h"
#include "ModelItem.h"
#include "ModelTree.h"
ModelTree::ModelTree(bool shouldReaverage) : Octree(shouldReaverage) {
@ -108,6 +111,7 @@ bool FindAndUpdateModelOperator::PostRecursion(OctreeElement* element) {
return !_found; // if we haven't yet found it, keep looking
}
// TODO: improve this to not use multiple recursions
void ModelTree::storeModel(const ModelItem& model, const SharedNodePointer& senderNode) {
// First, look for the existing model in the tree..
@ -199,6 +203,32 @@ void ModelTree::deleteModel(const ModelItemID& modelID) {
}
}
void ModelTree::sendModels(ModelEditPacketSender* packetSender, float x, float y, float z) {
SendModelsOperationArgs args;
args.packetSender = packetSender;
args.root = glm::vec3(x, y, z);
recurseTreeWithOperation(sendModelsOperation, &args);
packetSender->releaseQueuedMessages();
}
bool ModelTree::sendModelsOperation(OctreeElement* element, void* extraData) {
SendModelsOperationArgs* args = static_cast<SendModelsOperationArgs*>(extraData);
ModelTreeElement* modelTreeElement = static_cast<ModelTreeElement*>(element);
const QList<ModelItem>& modelList = modelTreeElement->getModels();
for (int i = 0; i < modelList.size(); i++) {
uint32_t creatorTokenID = ModelItem::getNextCreatorTokenID();
ModelItemID id(NEW_MODEL, creatorTokenID, false);
ModelItemProperties properties;
properties.copyFromNewModelItem(modelList.at(i));
properties.setPosition(properties.getPosition() + args->root);
args->packetSender->queueModelEditMessage(PacketTypeModelAddOrEdit, id, properties);
}
return true;
}
// scans the tree and handles mapping locally created models to know IDs.
// in the event that this tree is also viewing the scene, then we need to also
// search the tree to make sure we don't have a duplicate model from the viewing
@ -353,8 +383,28 @@ public:
QVector<ModelItem*> _foundModels;
};
void ModelTree::findModelsInCube(const AACube& cube, QVector<ModelItem*>& foundModels) {
FindModelsInCubeArgs args(cube);
lockForRead();
recurseTreeWithOperation(findInCubeOperation, &args);
unlock();
// swap the two lists of model pointers instead of copy
foundModels.swap(args._foundModels);
}
bool ModelTree::findInCubeOperation(OctreeElement* element, void* extraData) {
FindModelsInCubeArgs* args = static_cast<FindModelsInCubeArgs*>(extraData);
const AACube& elementCube = element->getAACube();
if (elementCube.touches(args->_cube)) {
ModelTreeElement* modelTreeElement = static_cast<ModelTreeElement*>(element);
modelTreeElement->getModelsInside(args->_cube, args->_foundModels);
return true;
}
return false;
}
bool ModelTree::findInCubeForUpdateOperation(OctreeElement* element, void* extraData) {
FindModelsInCubeArgs* args = static_cast< FindModelsInCubeArgs*>(extraData);
FindModelsInCubeArgs* args = static_cast<FindModelsInCubeArgs*>(extraData);
const AACube& elementCube = element->getAACube();
if (elementCube.touches(args->_cube)) {
ModelTreeElement* modelTreeElement = static_cast<ModelTreeElement*>(element);
@ -364,7 +414,7 @@ bool ModelTree::findInCubeForUpdateOperation(OctreeElement* element, void* extra
return false;
}
void ModelTree::findModelsForUpdate(const AACube& cube, QVector<ModelItem*> foundModels) {
void ModelTree::findModelsForUpdate(const AACube& cube, QVector<ModelItem*>& foundModels) {
FindModelsInCubeArgs args(cube);
lockForRead();
recurseTreeWithOperation(findInCubeForUpdateOperation, &args);

View file

@ -36,7 +36,6 @@ public:
/// Type safe version of getRoot()
ModelTreeElement* getRoot() { return static_cast<ModelTreeElement*>(_rootElement); }
// These methods will allow the OctreeServer to send your tree inbound edit packets of your
// own definition. Implement these to allow your octree based server to support editing
virtual bool getWantSVOfileVersions() const { return true; }
@ -62,12 +61,13 @@ public:
/// \param foundModels[out] vector of const ModelItem*
/// \remark Side effect: any initial contents in foundModels will be lost
void findModels(const glm::vec3& center, float radius, QVector<const ModelItem*>& foundModels);
void findModelsInCube(const AACube& cube, QVector<ModelItem*>& foundModels);
/// finds all models that touch a cube
/// \param cube the query cube
/// \param foundModels[out] vector of non-const ModelItem*
/// \remark Side effect: any initial contents in models will be lost
void findModelsForUpdate(const AACube& cube, QVector<ModelItem*> foundModels);
void findModelsForUpdate(const AACube& cube, QVector<ModelItem*>& foundModels);
void addNewlyCreatedHook(NewlyCreatedModelHook* hook);
void removeNewlyCreatedHook(NewlyCreatedModelHook* hook);
@ -83,11 +83,15 @@ public:
void setFBXService(ModelItemFBXService* service) { _fbxService = service; }
const FBXGeometry* getGeometryForModel(const ModelItem& modelItem) {
return _fbxService ? _fbxService->getGeometryForModel(modelItem) : NULL;
}
void sendModels(ModelEditPacketSender* packetSender, float x, float y, float z);
private:
static bool sendModelsOperation(OctreeElement* element, void* extraData);
static bool updateOperation(OctreeElement* element, void* extraData);
static bool findInCubeOperation(OctreeElement* element, void* extraData);
static bool findAndUpdateOperation(OctreeElement* element, void* extraData);
static bool findAndUpdateWithIDandPropertiesOperation(OctreeElement* element, void* extraData);
static bool findNearPointOperation(OctreeElement* element, void* extraData);

View file

@ -399,6 +399,19 @@ void ModelTreeElement::getModels(const glm::vec3& searchPosition, float searchRa
}
}
void ModelTreeElement::getModelsInside(const AACube& box, QVector<ModelItem*>& foundModels) {
QList<ModelItem>::iterator modelItr = _modelItems->begin();
QList<ModelItem>::iterator modelEnd = _modelItems->end();
AACube modelCube;
while(modelItr != modelEnd) {
ModelItem* model = &(*modelItr);
if (box.contains(model->getPosition())) {
foundModels.push_back(model);
}
++modelItr;
}
}
void ModelTreeElement::getModelsForUpdate(const AACube& box, QVector<ModelItem*>& foundModels) {
QList<ModelItem>::iterator modelItr = _modelItems->begin();
QList<ModelItem>::iterator modelEnd = _modelItems->end();

View file

@ -44,6 +44,12 @@ public:
bool isViewing;
};
class SendModelsOperationArgs {
public:
glm::vec3 root;
ModelEditPacketSender* packetSender;
};
class ModelTreeElement : public OctreeElement {
@ -132,6 +138,8 @@ public:
/// \param models[out] vector of non-const ModelItem*
void getModelsForUpdate(const AACube& box, QVector<ModelItem*>& foundModels);
void getModelsInside(const AACube& box, QVector<ModelItem*>& foundModels);
const ModelItem* getModelWithID(uint32_t id) const;
bool removeModelWithID(uint32_t id);

View file

@ -40,6 +40,15 @@ void ResourceCache::refresh(const QUrl& url) {
}
QSharedPointer<Resource> ResourceCache::getResource(const QUrl& url, const QUrl& fallback, bool delayLoad, void* extra) {
if (QThread::currentThread() != thread()) {
QSharedPointer<Resource> result;
QMetaObject::invokeMethod(this, "getResource", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QSharedPointer<Resource>, result), Q_ARG(const QUrl&, url), Q_ARG(const QUrl&, fallback),
Q_ARG(bool, delayLoad), Q_ARG(void*, extra));
return result;
}
if (!url.isValid() && !url.isEmpty() && fallback.isValid()) {
return getResource(fallback, QUrl(), delayLoad);
}

View file

@ -52,7 +52,7 @@ protected:
/// \param fallback a fallback URL to load if the desired one is unavailable
/// \param delayLoad if true, don't load the resource immediately; wait until load is first requested
/// \param extra extra data to pass to the creator, if appropriate
QSharedPointer<Resource> getResource(const QUrl& url, const QUrl& fallback = QUrl(),
Q_INVOKABLE QSharedPointer<Resource> getResource(const QUrl& url, const QUrl& fallback = QUrl(),
bool delayLoad = false, void* extra = NULL);
/// Creates a new resource.

View file

@ -13,19 +13,24 @@
#include <limits>
SequenceNumberStats::SequenceNumberStats(int statsHistoryLength)
: _lastReceived(std::numeric_limits<quint16>::max()),
SequenceNumberStats::SequenceNumberStats(int statsHistoryLength, bool canDetectOutOfSync)
: _lastReceivedSequence(0),
_missingSet(),
_stats(),
_lastSenderUUID(),
_statsHistory(statsHistoryLength)
_statsHistory(statsHistoryLength),
_lastUnreasonableSequence(0),
_consecutiveUnreasonableOnTime(0)
{
}
void SequenceNumberStats::reset() {
_missingSet.clear();
_stats = PacketStreamStats();
_lastSenderUUID = QUuid();
_statsHistory.clear();
_lastUnreasonableSequence = 0;
_consecutiveUnreasonableOnTime = 0;
}
static const int UINT16_RANGE = std::numeric_limits<uint16_t>::max() + 1;
@ -36,7 +41,7 @@ SequenceNumberStats::ArrivalInfo SequenceNumberStats::sequenceNumberReceived(qui
// if the sender node has changed, reset all stats
if (senderUUID != _lastSenderUUID) {
if (_stats._numReceived > 0) {
if (_stats._received > 0) {
qDebug() << "sequence number stats was reset due to new sender node";
qDebug() << "previous:" << _lastSenderUUID << "current:" << senderUUID;
reset();
@ -45,13 +50,14 @@ SequenceNumberStats::ArrivalInfo SequenceNumberStats::sequenceNumberReceived(qui
}
// determine our expected sequence number... handle rollover appropriately
quint16 expected = _stats._numReceived > 0 ? _lastReceived + (quint16)1 : incoming;
quint16 expected = _stats._received > 0 ? _lastReceivedSequence + (quint16)1 : incoming;
_stats._numReceived++;
_stats._received++;
if (incoming == expected) { // on time
arrivalInfo._status = OnTime;
_lastReceived = incoming;
_lastReceivedSequence = incoming;
_stats._expectedReceived++;
} else { // out of order
if (wantExtraDebugging) {
@ -74,14 +80,15 @@ SequenceNumberStats::ArrivalInfo SequenceNumberStats::sequenceNumberReceived(qui
} else if (absGap > MAX_REASONABLE_SEQUENCE_GAP) {
arrivalInfo._status = Unreasonable;
// ignore packet if gap is unreasonable
qDebug() << "ignoring unreasonable sequence number:" << incoming
<< "previous:" << _lastReceived;
_stats._numUnreasonable++;
qDebug() << "unreasonable sequence number:" << incoming << "previous:" << _lastReceivedSequence;
_stats._unreasonable++;
receivedUnreasonable(incoming);
return arrivalInfo;
}
// now that rollover has been corrected for (if it occurred), incomingInt and expectedInt can be
// compared to each other directly, though one of them might be negative
@ -94,16 +101,17 @@ SequenceNumberStats::ArrivalInfo SequenceNumberStats::sequenceNumberReceived(qui
qDebug() << "this packet is earlier than expected...";
qDebug() << ">>>>>>>> missing gap=" << (incomingInt - expectedInt);
}
_stats._numEarly++;
_stats._numLost += (incomingInt - expectedInt);
_lastReceived = incoming;
int skipped = incomingInt - expectedInt;
_stats._early++;
_stats._lost += skipped;
_stats._expectedReceived += (skipped + 1);
_lastReceivedSequence = incoming;
// add all sequence numbers that were skipped to the missing sequence numbers list
for (int missingInt = expectedInt; missingInt < incomingInt; missingInt++) {
_missingSet.insert((quint16)(missingInt < 0 ? missingInt + UINT16_RANGE : missingInt));
}
// prune missing sequence list if it gets too big; sequence numbers that are older than MAX_REASONABLE_SEQUENCE_GAP
// will be removed.
if (_missingSet.size() > MAX_REASONABLE_SEQUENCE_GAP) {
@ -114,32 +122,76 @@ SequenceNumberStats::ArrivalInfo SequenceNumberStats::sequenceNumberReceived(qui
qDebug() << "this packet is later than expected...";
}
_stats._numLate++;
_stats._late++;
// do not update _lastReceived; it shouldn't become smaller
// remove this from missing sequence number if it's in there
if (_missingSet.remove(incoming)) {
arrivalInfo._status = Late;
arrivalInfo._status = Recovered;
if (wantExtraDebugging) {
qDebug() << "found it in _missingSet";
}
_stats._numLost--;
_stats._numRecovered++;
_stats._lost--;
_stats._recovered++;
} else {
arrivalInfo._status = Duplicate;
// this late seq num is not in our missing set. it is possibly a duplicate, or possibly a late
// packet that should have arrived before our first received packet. we'll count these
// as unreasonable.
if (wantExtraDebugging) {
qDebug() << "sequence:" << incoming << "was NOT found in _missingSet and is probably a duplicate";
}
_stats._numDuplicate++;
arrivalInfo._status = Unreasonable;
qDebug() << "unreasonable sequence number:" << incoming << "(possible duplicate)";
_stats._unreasonable++;
receivedUnreasonable(incoming);
return arrivalInfo;
}
}
}
// if we've made it here, we received a reasonable seq number.
_consecutiveUnreasonableOnTime = 0;
return arrivalInfo;
}
void SequenceNumberStats::receivedUnreasonable(quint16 incoming) {
const int CONSECUTIVE_UNREASONABLE_ON_TIME_THRESHOLD = 8;
quint16 expected = _consecutiveUnreasonableOnTime > 0 ? _lastUnreasonableSequence + (quint16)1 : incoming;
if (incoming == expected) {
_consecutiveUnreasonableOnTime++;
_lastUnreasonableSequence = incoming;
if (_consecutiveUnreasonableOnTime >= CONSECUTIVE_UNREASONABLE_ON_TIME_THRESHOLD) {
// we've received many unreasonable seq numbers in a row, all in order. we're probably out of sync with
// the seq num sender. update our state to get back in sync with the sender.
_lastReceivedSequence = incoming;
_missingSet.clear();
_stats._received = CONSECUTIVE_UNREASONABLE_ON_TIME_THRESHOLD;
_stats._unreasonable = 0;
_stats._early = 0;
_stats._late = 0;
_stats._lost = 0;
_stats._recovered = 0;
_stats._expectedReceived = CONSECUTIVE_UNREASONABLE_ON_TIME_THRESHOLD;
_statsHistory.clear();
_consecutiveUnreasonableOnTime = 0;
qDebug() << "re-synced with sequence number sender";
}
} else {
_consecutiveUnreasonableOnTime = 0;
}
}
void SequenceNumberStats::pruneMissingSet(const bool wantExtraDebugging) {
if (wantExtraDebugging) {
qDebug() << "pruning _missingSet! size:" << _missingSet.size();
@ -148,7 +200,7 @@ void SequenceNumberStats::pruneMissingSet(const bool wantExtraDebugging) {
// some older sequence numbers may be from before a rollover point; this must be handled.
// some sequence numbers in this list may be larger than _incomingLastSequence, indicating that they were received
// before the most recent rollover.
int cutoff = (int)_lastReceived - MAX_REASONABLE_SEQUENCE_GAP;
int cutoff = (int)_lastReceivedSequence - MAX_REASONABLE_SEQUENCE_GAP;
if (cutoff >= 0) {
quint16 nonRolloverCutoff = (quint16)cutoff;
QSet<quint16>::iterator i = _missingSet.begin();
@ -159,7 +211,7 @@ void SequenceNumberStats::pruneMissingSet(const bool wantExtraDebugging) {
qDebug() << "old age cutoff:" << nonRolloverCutoff;
}
if (missing > _lastReceived || missing < nonRolloverCutoff) {
if (missing > _lastReceivedSequence || missing < nonRolloverCutoff) {
i = _missingSet.erase(i);
if (wantExtraDebugging) {
qDebug() << "pruning really old missing sequence:" << missing;
@ -178,7 +230,7 @@ void SequenceNumberStats::pruneMissingSet(const bool wantExtraDebugging) {
qDebug() << "old age cutoff:" << rolloverCutoff;
}
if (missing > _lastReceived && missing < rolloverCutoff) {
if (missing > _lastReceivedSequence && missing < rolloverCutoff) {
i = _missingSet.erase(i);
if (wantExtraDebugging) {
qDebug() << "pruning really old missing sequence:" << missing;
@ -194,7 +246,7 @@ PacketStreamStats SequenceNumberStats::getStatsForHistoryWindow() const {
const PacketStreamStats* newestStats = _statsHistory.getNewestEntry();
const PacketStreamStats* oldestStats = _statsHistory.get(_statsHistory.getNumEntries() - 1);
// this catches cases where history is length 1 or 0 (both are NULL in case of 0)
if (newestStats == oldestStats) {
return PacketStreamStats();
@ -202,13 +254,13 @@ PacketStreamStats SequenceNumberStats::getStatsForHistoryWindow() const {
// calculate difference between newest stats and oldest stats to get window stats
PacketStreamStats windowStats;
windowStats._numReceived = newestStats->_numReceived - oldestStats->_numReceived;
windowStats._numUnreasonable = newestStats->_numUnreasonable - oldestStats->_numUnreasonable;
windowStats._numEarly = newestStats->_numEarly - oldestStats->_numEarly;
windowStats._numLate = newestStats->_numLate - oldestStats->_numLate;
windowStats._numLost = newestStats->_numLost - oldestStats->_numLost;
windowStats._numRecovered = newestStats->_numRecovered - oldestStats->_numRecovered;
windowStats._numDuplicate = newestStats->_numDuplicate - oldestStats->_numDuplicate;
windowStats._received = newestStats->_received - oldestStats->_received;
windowStats._unreasonable = newestStats->_unreasonable - oldestStats->_unreasonable;
windowStats._early = newestStats->_early - oldestStats->_early;
windowStats._late = newestStats->_late - oldestStats->_late;
windowStats._lost = newestStats->_lost - oldestStats->_lost;
windowStats._recovered = newestStats->_recovered - oldestStats->_recovered;
windowStats._expectedReceived = newestStats->_expectedReceived - oldestStats->_expectedReceived;
return windowStats;
}

View file

@ -18,32 +18,28 @@
const int MAX_REASONABLE_SEQUENCE_GAP = 1000;
class PacketStreamStats {
public:
PacketStreamStats()
: _numReceived(0),
_numUnreasonable(0),
_numEarly(0),
_numLate(0),
_numLost(0),
_numRecovered(0),
_numDuplicate(0)
: _received(0),
_unreasonable(0),
_early(0),
_late(0),
_lost(0),
_recovered(0),
_expectedReceived(0)
{}
float getUnreasonableRate() const { return (float)_numUnreasonable / _numReceived; }
float getNumEaryRate() const { return (float)_numEarly / _numReceived; }
float getLateRate() const { return (float)_numLate / _numReceived; }
float getLostRate() const { return (float)_numLost / _numReceived; }
float getRecoveredRate() const { return (float)_numRecovered / _numReceived; }
float getDuplicateRate() const { return (float)_numDuplicate / _numReceived; }
float getLostRate() const { return (float)_lost / _expectedReceived; }
quint32 _numReceived;
quint32 _numUnreasonable;
quint32 _numEarly;
quint32 _numLate;
quint32 _numLost;
quint32 _numRecovered;
quint32 _numDuplicate;
quint32 _received;
quint32 _unreasonable;
quint32 _early;
quint32 _late;
quint32 _lost;
quint32 _recovered;
quint32 _expectedReceived;
};
class SequenceNumberStats {
@ -52,8 +48,7 @@ public:
OnTime,
Unreasonable,
Early,
Late, // recovered
Duplicate
Recovered,
};
class ArrivalInfo {
@ -63,27 +58,31 @@ public:
};
SequenceNumberStats(int statsHistoryLength = 0);
SequenceNumberStats(int statsHistoryLength = 0, bool canDetectOutOfSync = true);
void reset();
ArrivalInfo sequenceNumberReceived(quint16 incoming, QUuid senderUUID = QUuid(), const bool wantExtraDebugging = false);
void pruneMissingSet(const bool wantExtraDebugging = false);
void pushStatsToHistory() { _statsHistory.insert(_stats); }
quint32 getNumReceived() const { return _stats._numReceived; }
quint32 getNumUnreasonable() const { return _stats._numUnreasonable; }
quint32 getNumOutOfOrder() const { return _stats._numEarly + _stats._numLate; }
quint32 getNumEarly() const { return _stats._numEarly; }
quint32 getNumLate() const { return _stats._numLate; }
quint32 getNumLost() const { return _stats._numLost; }
quint32 getNumRecovered() const { return _stats._numRecovered; }
quint32 getNumDuplicate() const { return _stats._numDuplicate; }
quint32 getReceived() const { return _stats._received; }
quint32 getExpectedReceived() const { return _stats._expectedReceived; }
quint32 getUnreasonable() const { return _stats._unreasonable; }
quint32 getOutOfOrder() const { return _stats._early + _stats._late; }
quint32 getEarly() const { return _stats._early; }
quint32 getLate() const { return _stats._late; }
quint32 getLost() const { return _stats._lost; }
quint32 getRecovered() const { return _stats._recovered; }
const PacketStreamStats& getStats() const { return _stats; }
PacketStreamStats getStatsForHistoryWindow() const;
const QSet<quint16>& getMissingSet() const { return _missingSet; }
private:
quint16 _lastReceived;
void receivedUnreasonable(quint16 incoming);
private:
quint16 _lastReceivedSequence;
QSet<quint16> _missingSet;
PacketStreamStats _stats;
@ -91,6 +90,9 @@ private:
QUuid _lastSenderUUID;
RingBufferHistory<PacketStreamStats> _statsHistory;
quint16 _lastUnreasonableSequence;
int _consecutiveUnreasonableOnTime;
};
#endif // hifi_SequenceNumberStats_h

View file

@ -156,7 +156,7 @@ ScriptEngine::ScriptEngine(const QUrl& scriptURL,
} else {
NetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* reply = networkAccessManager.get(QNetworkRequest(url));
qDebug() << "Downloading included script at" << url;
qDebug() << "Downloading script at" << url;
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
@ -681,12 +681,12 @@ void ScriptEngine::include(const QString& includeFile) {
#endif
QFile scriptFile(fileName);
if (scriptFile.open(QFile::ReadOnly | QFile::Text)) {
qDebug() << "Loading file:" << fileName;
qDebug() << "Including file:" << fileName;
QTextStream in(&scriptFile);
includeContents = in.readAll();
} else {
qDebug() << "ERROR Loading file:" << fileName;
emit errorMessage("ERROR Loading file:" + fileName);
qDebug() << "ERROR Including file:" << fileName;
emit errorMessage("ERROR Including file:" + fileName);
}
}
@ -699,6 +699,11 @@ void ScriptEngine::include(const QString& includeFile) {
}
}
void ScriptEngine::load(const QString& loadFile) {
QUrl url = resolveInclude(loadFile);
emit loadScript(url.toString());
}
void ScriptEngine::nodeKilled(SharedNodePointer node) {
_outgoingScriptAudioSequenceNumbers.remove(node->getUUID());
}

View file

@ -102,6 +102,7 @@ public slots:
void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
void include(const QString& includeFile);
void load(const QString& loadfile);
void print(const QString& message);
void nodeKilled(SharedNodePointer node);
@ -115,6 +116,7 @@ signals:
void errorMessage(const QString& message);
void runningStateChanged();
void evaluationFinished(QScriptValue result, bool isException);
void loadScript(const QString& scriptName);
protected:
QString _scriptContents;

View file

@ -9,6 +9,7 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtCore/QFile>
#include <QtCore/QJsonDocument>
@ -68,29 +69,35 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL
int configIndex = argumentList.indexOf(CONFIG_FILE_OPTION);
QString configFilePath;
if (configIndex != -1) {
// we have a config file - try and read it
QString configFilePath = argumentList[configIndex + 1];
QFile configFile(configFilePath);
if (configFile.exists()) {
qDebug() << "Reading JSON config file at" << configFilePath;
configFile.open(QIODevice::ReadOnly);
QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll());
QJsonObject rootObject = configDocument.object();
// enumerate the keys of the configDocument object
foreach(const QString& key, rootObject.keys()) {
if (!mergedMap.contains(key)) {
// no match in existing list, add it
mergedMap.insert(key, QVariant(rootObject[key]));
}
configFilePath = argumentList[configIndex + 1];
} else {
// no config file - try to read a file at resources/config.json
configFilePath = QCoreApplication::applicationDirPath() + "/resources/config.json";
}
QFile configFile(configFilePath);
if (configFile.exists()) {
qDebug() << "Reading JSON config file at" << configFilePath;
configFile.open(QIODevice::ReadOnly);
QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll());
QJsonObject rootObject = configDocument.object();
// enumerate the keys of the configDocument object
foreach(const QString& key, rootObject.keys()) {
if (!mergedMap.contains(key)) {
// no match in existing list, add it
mergedMap.insert(key, QVariant(rootObject[key]));
}
} else {
qDebug() << "Could not find JSON config file at" << configFilePath;
}
} else {
qDebug() << "Could not find JSON config file at" << configFilePath;
}
return mergedMap;

View file

@ -80,11 +80,16 @@ void VoxelsScriptingInterface::setVoxel(float x, float y, float z, float scale,
DeleteVoxelCommand* deleteCommand = new DeleteVoxelCommand(_tree,
addVoxelDetail,
getVoxelPacketSender());
_undoStackMutex.lock();
_undoStack->beginMacro(addCommand->text());
// As QUndoStack automatically executes redo() on push, we don't need to execute the command ourselves.
_undoStack->push(deleteCommand);
_undoStack->push(addCommand);
_undoStack->endMacro();
//Unlock the mutex
_undoStackMutex.unlock();
} else {
// queue the destructive add
queueVoxelAdd(PacketTypeVoxelSetDestructive, addVoxelDetail);
@ -109,11 +114,15 @@ void VoxelsScriptingInterface::eraseVoxel(float x, float y, float z, float scale
}
if (_undoStack) {
DeleteVoxelCommand* command = new DeleteVoxelCommand(_tree,
deleteVoxelDetail,
getVoxelPacketSender());
_undoStackMutex.lock();
// As QUndoStack automatically executes redo() on push, we don't need to execute the command ourselves.
_undoStack->push(command);
_undoStackMutex.unlock();
} else {
getVoxelPacketSender()->queueVoxelEditMessages(PacketTypeVoxelErase, 1, &deleteVoxelDetail);
_tree->deleteVoxelAt(deleteVoxelDetail.x, deleteVoxelDetail.y, deleteVoxelDetail.z, deleteVoxelDetail.s);

View file

@ -102,6 +102,7 @@ private:
void queueVoxelAdd(PacketType addPacketType, VoxelDetail& addVoxelDetails);
VoxelTree* _tree;
QUndoStack* _undoStack;
QMutex _undoStackMutex;
};
#endif // hifi_VoxelsScriptingInterface_h

View file

@ -0,0 +1,33 @@
set(TARGET_NAME jitter-tests)
set(ROOT_DIR ../..)
set(MACRO_DIR ${ROOT_DIR}/cmake/macros)
# setup for find modules
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/")
#find_package(Qt5Network REQUIRED)
#find_package(Qt5Script REQUIRED)
#find_package(Qt5Widgets REQUIRED)
include(${MACRO_DIR}/SetupHifiProject.cmake)
setup_hifi_project(${TARGET_NAME} TRUE)
#include(${MACRO_DIR}/AutoMTC.cmake)
#auto_mtc(${TARGET_NAME} ${ROOT_DIR})
#qt5_use_modules(${TARGET_NAME} Network Script Widgets)
#include glm - because it's a dependency of shared utils...
include(${MACRO_DIR}/IncludeGLM.cmake)
include_glm(${TARGET_NAME} ${ROOT_DIR})
# link in the shared libraries
include(${MACRO_DIR}/LinkHifiLibrary.cmake)
link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR})
link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR})
IF (WIN32)
target_link_libraries(${TARGET_NAME} Winmm Ws2_32)
ENDIF(WIN32)

167
tests/jitter/src/main.cpp Normal file
View file

@ -0,0 +1,167 @@
//
// main.cpp
// JitterTester
//
// Created by Philip on 8/1/14.
// Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
//
#include <iostream>
#ifdef _WINDOWS
#include <winsock2.h>
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#endif
#include <stdio.h>
#include <MovingMinMaxAvg.h> // for MovingMinMaxAvg
#include <SharedUtil.h> // for usecTimestampNow
const quint64 MSEC_TO_USEC = 1000;
void runSend(const char* addressOption, int port, int gap, int size, int report);
void runReceive(const char* addressOption, int port, int gap, int size, int report);
int main(int argc, const char * argv[]) {
if (argc != 7) {
printf("usage: jitter-tests <--send|--receive> <address> <port> <gap in usecs> <packet size> <report interval in msecs>\n");
exit(1);
}
const char* typeOption = argv[1];
const char* addressOption = argv[2];
const char* portOption = argv[3];
const char* gapOption = argv[4];
const char* sizeOption = argv[5];
const char* reportOption = argv[6];
int port = atoi(portOption);
int gap = atoi(gapOption);
int size = atoi(sizeOption);
int report = atoi(reportOption);
std::cout << "type:" << typeOption << "\n";
std::cout << "address:" << addressOption << "\n";
std::cout << "port:" << port << "\n";
std::cout << "gap:" << gap << "\n";
std::cout << "size:" << size << "\n";
if (strcmp(typeOption, "--send") == 0) {
runSend(addressOption, port, gap, size, report);
} else if (strcmp(typeOption, "--receive") == 0) {
runReceive(addressOption, port, gap, size, report);
}
exit(1);
}
void runSend(const char* addressOption, int port, int gap, int size, int report) {
std::cout << "runSend...\n";
int sockfd;
struct sockaddr_in servaddr;
char* outputBuffer = new char[size];
memset(outputBuffer, 0, size);
sockfd=socket(AF_INET,SOCK_DGRAM,0);
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr=inet_addr(addressOption);
servaddr.sin_port=htons(port);
const int SAMPLES_FOR_30_SECONDS = 30 * 1000000 / gap;
std::cout << "SAMPLES_FOR_30_SECONDS:" << SAMPLES_FOR_30_SECONDS << "\n";
MovingMinMaxAvg<int> timeGaps(1, SAMPLES_FOR_30_SECONDS); // stats
quint64 last = usecTimestampNow();
quint64 lastReport = 0;
while (true) {
quint64 now = usecTimestampNow();
int actualGap = now - last;
if (actualGap >= gap) {
sendto(sockfd, outputBuffer, size, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
int gapDifferece = actualGap - gap;
timeGaps.update(gapDifferece);
last = now;
if (now - lastReport >= (report * MSEC_TO_USEC)) {
std::cout << "SEND gap Difference From Expected "
<< "min: " << timeGaps.getMin() << " usecs, "
<< "max: " << timeGaps.getMax() << " usecs, "
<< "avg: " << timeGaps.getAverage() << " usecs, "
<< "min last 30: " << timeGaps.getWindowMin() << " usecs, "
<< "max last 30: " << timeGaps.getWindowMax() << " usecs, "
<< "avg last 30: " << timeGaps.getWindowAverage() << " usecs "
<< "\n";
lastReport = now;
}
}
}
}
void runReceive(const char* addressOption, int port, int gap, int size, int report) {
std::cout << "runReceive...\n";
int sockfd,n;
struct sockaddr_in myaddr;
char* inputBuffer = new char[size];
memset(inputBuffer, 0, size);
sockfd=socket(AF_INET, SOCK_DGRAM, 0);
memset(&myaddr, 0, sizeof(myaddr));
myaddr.sin_family = AF_INET;
myaddr.sin_addr.s_addr=htonl(INADDR_ANY);
myaddr.sin_port=htons(port);
const int SAMPLES_FOR_30_SECONDS = 30 * 1000000 / gap;
std::cout << "SAMPLES_FOR_30_SECONDS:" << SAMPLES_FOR_30_SECONDS << "\n";
MovingMinMaxAvg<int> timeGaps(1, SAMPLES_FOR_30_SECONDS); // stats
if (bind(sockfd, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0) {
std::cout << "bind failed\n";
return;
}
quint64 last = 0; // first case
quint64 lastReport = 0;
while (true) {
n = recvfrom(sockfd, inputBuffer, size, 0, NULL, NULL); // we don't care about where it came from
if (last == 0) {
last = usecTimestampNow();
std::cout << "first packet received\n";
} else {
quint64 now = usecTimestampNow();
int actualGap = now - last;
int gapDifferece = actualGap - gap;
timeGaps.update(gapDifferece);
last = now;
if (now - lastReport >= (report * MSEC_TO_USEC)) {
std::cout << "RECEIVE gap Difference From Expected "
<< "min: " << timeGaps.getMin() << " usecs, "
<< "max: " << timeGaps.getMax() << " usecs, "
<< "avg: " << timeGaps.getAverage() << " usecs, "
<< "min last 30: " << timeGaps.getWindowMin() << " usecs, "
<< "max last 30: " << timeGaps.getWindowMax() << " usecs, "
<< "avg last 30: " << timeGaps.getWindowAverage() << " usecs "
<< "\n";
lastReport = now;
}
}
}
}

View file

@ -16,11 +16,11 @@
void SequenceNumberStatsTests::runAllTests() {
rolloverTest();
earlyLateTest();
duplicateTest();
pruneTest();
resyncTest();
}
const quint32 UINT16_RANGE = std::numeric_limits<quint16>::max() + 1;
@ -38,12 +38,11 @@ void SequenceNumberStatsTests::rolloverTest() {
stats.sequenceNumberReceived(seq);
seq = seq + (quint16)1;
assert(stats.getNumDuplicate() == 0);
assert(stats.getNumEarly() == 0);
assert(stats.getNumLate() == 0);
assert(stats.getNumLost() == 0);
assert(stats.getNumReceived() == i + 1);
assert(stats.getNumRecovered() == 0);
assert(stats.getEarly() == 0);
assert(stats.getLate() == 0);
assert(stats.getLost() == 0);
assert(stats.getReceived() == i + 1);
assert(stats.getRecovered() == 0);
}
stats.reset();
}
@ -69,12 +68,11 @@ void SequenceNumberStatsTests::earlyLateTest() {
seq = seq + (quint16)1;
numSent++;
assert(stats.getNumDuplicate() == 0);
assert(stats.getNumEarly() == numEarly);
assert(stats.getNumLate() == numLate);
assert(stats.getNumLost() == numLost);
assert(stats.getNumReceived() == numSent);
assert(stats.getNumRecovered() == numRecovered);
assert(stats.getEarly() == numEarly);
assert(stats.getLate() == numLate);
assert(stats.getLost() == numLost);
assert(stats.getReceived() == numSent);
assert(stats.getRecovered() == numRecovered);
}
// skip 10
@ -89,12 +87,11 @@ void SequenceNumberStatsTests::earlyLateTest() {
seq = seq + (quint16)1;
numSent++;
assert(stats.getNumDuplicate() == 0);
assert(stats.getNumEarly() == numEarly);
assert(stats.getNumLate() == numLate);
assert(stats.getNumLost() == numLost);
assert(stats.getNumReceived() == numSent);
assert(stats.getNumRecovered() == numRecovered);
assert(stats.getEarly() == numEarly);
assert(stats.getLate() == numLate);
assert(stats.getLost() == numLost);
assert(stats.getReceived() == numSent);
assert(stats.getRecovered() == numRecovered);
}
// send ones we skipped
@ -106,12 +103,11 @@ void SequenceNumberStatsTests::earlyLateTest() {
numLost--;
numRecovered++;
assert(stats.getNumDuplicate() == 0);
assert(stats.getNumEarly() == numEarly);
assert(stats.getNumLate() == numLate);
assert(stats.getNumLost() == numLost);
assert(stats.getNumReceived() == numSent);
assert(stats.getNumRecovered() == numRecovered);
assert(stats.getEarly() == numEarly);
assert(stats.getLate() == numLate);
assert(stats.getLost() == numLost);
assert(stats.getReceived() == numSent);
assert(stats.getRecovered() == numRecovered);
}
}
stats.reset();
@ -135,7 +131,7 @@ void SequenceNumberStatsTests::duplicateTest() {
quint32 numLost = 0;
for (int R = 0; R < 2; R++) {
for (int T = 0; T < 10000; T++) {
for (int T = 0; T < 5; T++) {
quint16 duplicate = seq;
@ -145,12 +141,12 @@ void SequenceNumberStatsTests::duplicateTest() {
seq = seq + (quint16)1;
numSent++;
assert(stats.getNumDuplicate() == numDuplicate);
assert(stats.getNumEarly() == numEarly);
assert(stats.getNumLate() == numLate);
assert(stats.getNumLost() == numLost);
assert(stats.getNumReceived() == numSent);
assert(stats.getNumRecovered() == 0);
assert(stats.getUnreasonable() == numDuplicate);
assert(stats.getEarly() == numEarly);
assert(stats.getLate() == numLate);
assert(stats.getLost() == numLost);
assert(stats.getReceived() == numSent);
assert(stats.getRecovered() == 0);
}
// skip 10
@ -167,12 +163,12 @@ void SequenceNumberStatsTests::duplicateTest() {
seq = seq + (quint16)1;
numSent++;
assert(stats.getNumDuplicate() == numDuplicate);
assert(stats.getNumEarly() == numEarly);
assert(stats.getNumLate() == numLate);
assert(stats.getNumLost() == numLost);
assert(stats.getNumReceived() == numSent);
assert(stats.getNumRecovered() == 0);
assert(stats.getUnreasonable() == numDuplicate);
assert(stats.getEarly() == numEarly);
assert(stats.getLate() == numLate);
assert(stats.getLost() == numLost);
assert(stats.getReceived() == numSent);
assert(stats.getRecovered() == 0);
}
// send 5 duplicates from before skip
@ -183,12 +179,12 @@ void SequenceNumberStatsTests::duplicateTest() {
numDuplicate++;
numLate++;
assert(stats.getNumDuplicate() == numDuplicate);
assert(stats.getNumEarly() == numEarly);
assert(stats.getNumLate() == numLate);
assert(stats.getNumLost() == numLost);
assert(stats.getNumReceived() == numSent);
assert(stats.getNumRecovered() == 0);
assert(stats.getUnreasonable() == numDuplicate);
assert(stats.getEarly() == numEarly);
assert(stats.getLate() == numLate);
assert(stats.getLost() == numLost);
assert(stats.getReceived() == numSent);
assert(stats.getRecovered() == 0);
}
// send 5 duplicates from after skip
@ -199,12 +195,12 @@ void SequenceNumberStatsTests::duplicateTest() {
numDuplicate++;
numLate++;
assert(stats.getNumDuplicate() == numDuplicate);
assert(stats.getNumEarly() == numEarly);
assert(stats.getNumLate() == numLate);
assert(stats.getNumLost() == numLost);
assert(stats.getNumReceived() == numSent);
assert(stats.getNumRecovered() == 0);
assert(stats.getUnreasonable() == numDuplicate);
assert(stats.getEarly() == numEarly);
assert(stats.getLate() == numLate);
assert(stats.getLost() == numLost);
assert(stats.getReceived() == numSent);
assert(stats.getRecovered() == 0);
}
}
stats.reset();
@ -278,3 +274,47 @@ void SequenceNumberStatsTests::pruneTest() {
numLost = 0;
}
}
void SequenceNumberStatsTests::resyncTest() {
SequenceNumberStats stats(0);
quint16 sequence;
sequence = 89;
stats.sequenceNumberReceived(sequence);
assert(stats.getUnreasonable() == 0);
sequence = 2990;
for (int i = 0; i < 10; i++) {
stats.sequenceNumberReceived(sequence);
sequence += (quint16)1;
}
assert(stats.getUnreasonable() == 0);
sequence = 0;
for (int R = 0; R < 7; R++) {
stats.sequenceNumberReceived(sequence);
sequence += (quint16)1;
}
assert(stats.getUnreasonable() == 7);
sequence = 6000;
for (int R = 0; R < 7; R++) {
stats.sequenceNumberReceived(sequence);
sequence += (quint16)1;
}
assert(stats.getUnreasonable() == 14);
sequence = 9000;
for (int i = 0; i < 10; i++) {
stats.sequenceNumberReceived(sequence);
sequence += (quint16)1;
}
assert(stats.getUnreasonable() == 0);
}

View file

@ -23,6 +23,7 @@ namespace SequenceNumberStatsTests {
void earlyLateTest();
void duplicateTest();
void pruneTest();
void resyncTest();
};
#endif // hifi_SequenceNumberStatsTests_h