diff --git a/examples/swissArmyJetpack.js b/examples/swissArmyJetpack.js
index 9bb5bea267..fb4dc1dc04 100644
--- a/examples/swissArmyJetpack.js
+++ b/examples/swissArmyJetpack.js
@@ -12,18 +12,63 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
-var numberOfButtons = 3;
+// misc global constants
+var NUMBER_OF_COLLISION_BUTTONS = 3;
+var NUMBER_OF_BUTTONS = 4;
+var DOWN = { x: 0.0, y: -1.0, z: 0.0 };
+var MAX_VOXEL_SCAN_DISTANCE = 30.0;
-var enabledColors = new Array();
-enabledColors[0] = { red: 255, green: 0, blue: 0};
-enabledColors[1] = { red: 0, green: 255, blue: 0};
-enabledColors[2] = { red: 0, green: 0, blue: 255};
+// behavior transition thresholds
+var MIN_FLYING_SPEED = 3.0;
+var MIN_COLLISIONLESS_SPEED = 5.0;
+var MAX_WALKING_SPEED = 30.0;
+var MAX_COLLIDABLE_SPEED = 35.0;
-var disabledColors = new Array();
-disabledColors[0] = { red: 90, green: 75, blue: 75};
-disabledColors[1] = { red: 75, green: 90, blue: 75};
-disabledColors[2] = { red: 75, green: 90, blue: 90};
+// button URL and geometry/UI tuning
+var BUTTON_IMAGE_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/testing-swatches.svg";
+var DISABLED_OFFSET_Y = 12;
+var ENABLED_OFFSET_Y = 55 + 12;
+var UI_BUFFER = 1;
+var OFFSET_X = UI_BUFFER;
+var OFFSET_Y = 200;
+var BUTTON_WIDTH = 30;
+var BUTTON_HEIGHT = 30;
+var TEXT_OFFSET_X = OFFSET_X + BUTTON_WIDTH + UI_BUFFER;
+var TEXT_HEIGHT = BUTTON_HEIGHT;
+var TEXT_WIDTH = 210;
+var MSEC_PER_SECOND = 1000;
+var RAYCAST_EXPIRY_PERIOD = MSEC_PER_SECOND / 16;
+var COLLISION_EXPIRY_PERIOD = 2 * MSEC_PER_SECOND;
+var GRAVITY_ON_EXPIRY_PERIOD = MSEC_PER_SECOND / 2;
+var GRAVITY_OFF_EXPIRY_PERIOD = MSEC_PER_SECOND / 8;
+
+var dater = new Date();
+var raycastExpiry = dater.getTime() + RAYCAST_EXPIRY_PERIOD;
+var gravityOnExpiry = dater.getTime() + GRAVITY_ON_EXPIRY_PERIOD;
+var gravityOffExpiry = dater.getTime() + GRAVITY_OFF_EXPIRY_PERIOD;
+var collisionOnExpiry = dater.getTime() + COLLISION_EXPIRY_PERIOD;
+
+// avatar state
+var velocity = { x: 0.0, y: 0.0, z: 0.0 };
+var standing = false;
+
+// speedometer globals
+var speed = 0.0;
+var lastPosition = MyAvatar.position;
+var speedometer = Overlays.addOverlay("text", {
+ x: OFFSET_X,
+ y: OFFSET_Y - BUTTON_HEIGHT,
+ width: BUTTON_WIDTH + UI_BUFFER + TEXT_WIDTH,
+ height: TEXT_HEIGHT,
+ color: { red: 0, green: 0, blue: 0 },
+ textColor: { red: 255, green: 0, blue: 0},
+ topMargin: 4,
+ leftMargin: 4,
+ text: "Speed: 0.0"
+ });
+
+// collision group buttons
var buttons = new Array();
var labels = new Array();
@@ -31,40 +76,43 @@ var labelContents = new Array();
labelContents[0] = "Collide with Avatars";
labelContents[1] = "Collide with Voxels";
labelContents[2] = "Collide with Particles";
+labelContents[3] = "Use local gravity";
var groupBits = 0;
+var enabledColors = new Array();
+enabledColors[0] = { red: 255, green: 0, blue: 0};
+enabledColors[1] = { red: 0, green: 255, blue: 0};
+enabledColors[2] = { red: 0, green: 0, blue: 255};
+enabledColors[3] = { red: 255, green: 255, blue: 0};
+
+var disabledColors = new Array();
+disabledColors[0] = { red: 90, green: 75, blue: 75};
+disabledColors[1] = { red: 75, green: 90, blue: 75};
+disabledColors[2] = { red: 75, green: 75, blue: 90};
+disabledColors[3] = { red: 90, green: 90, blue: 75};
+
var buttonStates = new Array();
-var disabledOffsetT = 0;
-var enabledOffsetT = 55;
-
-var buttonX = 50;
-var buttonY = 200;
-var buttonWidth = 30;
-var buttonHeight = 54;
-var textX = buttonX + buttonWidth + 10;
-
-for (i = 0; i < numberOfButtons; i++) {
+for (i = 0; i < NUMBER_OF_BUTTONS; i++) {
var offsetS = 12
- var offsetT = disabledOffsetT;
+ var offsetT = DISABLED_OFFSET_Y;
buttons[i] = Overlays.addOverlay("image", {
- //x: buttonX + (buttonWidth * i),
- x: buttonX,
- y: buttonY + (buttonHeight * i),
- width: buttonWidth,
- height: buttonHeight,
- subImage: { x: offsetS, y: offsetT, width: buttonWidth, height: buttonHeight },
- imageURL: "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/testing-swatches.svg",
+ x: OFFSET_X,
+ y: OFFSET_Y + (BUTTON_HEIGHT * i),
+ width: BUTTON_WIDTH,
+ height: BUTTON_HEIGHT,
+ subImage: { x: offsetS, y: offsetT, width: BUTTON_WIDTH, height: BUTTON_HEIGHT },
+ imageURL: BUTTON_IMAGE_URL,
color: disabledColors[i],
alpha: 1,
});
labels[i] = Overlays.addOverlay("text", {
- x: textX,
- y: buttonY + (buttonHeight * i) + 12,
- width: 150,
- height: 50,
+ x: TEXT_OFFSET_X,
+ y: OFFSET_Y + (BUTTON_HEIGHT * i),
+ width: TEXT_WIDTH,
+ height: TEXT_HEIGHT,
color: { red: 0, green: 0, blue: 0},
textColor: { red: 255, green: 0, blue: 0},
topMargin: 4,
@@ -75,12 +123,14 @@ for (i = 0; i < numberOfButtons; i++) {
buttonStates[i] = false;
}
+
+// functions
+
function updateButton(i, enabled) {
- var offsetY = disabledOffsetT;
+ var offsetY = DISABLED_OFFSET_Y;
var buttonColor = disabledColors[i];
- groupBits
if (enabled) {
- offsetY = enabledOffsetT;
+ offsetY = ENABLED_OFFSET_Y;
buttonColor = enabledColors[i];
if (i == 0) {
groupBits |= COLLISION_GROUP_AVATARS;
@@ -98,24 +148,46 @@ function updateButton(i, enabled) {
groupBits &= ~COLLISION_GROUP_PARTICLES;
}
}
- MyAvatar.collisionGroups = groupBits;
+ if (groupBits != MyAvatar.collisionGroups) {
+ MyAvatar.collisionGroups = groupBits;
+ }
Overlays.editOverlay(buttons[i], { subImage: { y: offsetY } } );
Overlays.editOverlay(buttons[i], { color: buttonColor } );
buttonStates[i] = enabled;
}
+
// When our script shuts down, we should clean up all of our overlays
function scriptEnding() {
- for (i = 0; i < numberOfButtons; i++) {
- print("adebug deleting overlay " + i);
+ for (i = 0; i < NUMBER_OF_BUTTONS; i++) {
Overlays.deleteOverlay(buttons[i]);
Overlays.deleteOverlay(labels[i]);
}
+ Overlays.deleteOverlay(speedometer);
}
Script.scriptEnding.connect(scriptEnding);
+function updateSpeedometerDisplay() {
+ Overlays.editOverlay(speedometer, { text: "Speed: " + speed.toFixed(2) });
+}
+Script.setInterval(updateSpeedometerDisplay, 100);
+
+function disableArtificialGravity() {
+ MyAvatar.motionBehaviors = MyAvatar.motionBehaviors & ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY;
+ updateButton(3, false);
+}
+
+function enableArtificialGravity() {
+ // NOTE: setting the gravity automatically sets the AVATAR_MOTION_OBEY_LOCAL_GRAVITY behavior bit.
+ MyAvatar.gravity = DOWN;
+ updateButton(3, true);
+ // also enable collisions with voxels
+ groupBits |= COLLISION_GROUP_VOXELS;
+ updateButton(1, groupBits & COLLISION_GROUP_VOXELS);
+}
+
// Our update() function is called at approximately 60fps, and we will use it to animate our various overlays
function update(deltaTime) {
if (groupBits != MyAvatar.collisionGroups) {
@@ -124,6 +196,83 @@ function update(deltaTime) {
updateButton(1, groupBits & COLLISION_GROUP_VOXELS);
updateButton(2, groupBits & COLLISION_GROUP_PARTICLES);
}
+
+ // measure speed
+ var distance = Vec3.distance(MyAvatar.position, lastPosition);
+ speed = 0.8 * speed + 0.2 * distance / deltaTime;
+ lastPosition = MyAvatar.position;
+
+ dater = new Date();
+ var now = dater.getTime();
+
+ // transition gravity
+ if (raycastExpiry < now) {
+ // scan for landing platform
+ ray = { origin: MyAvatar.position, direction: DOWN };
+ var intersection = Voxels.findRayIntersection(ray);
+ // NOTE: it is possible for intersection.intersects to be false when it should be true
+ // (perhaps the raycast failed to lock the octree thread?). To workaround this problem
+ // we only transition on repeated failures.
+
+ if (intersection.intersects) {
+ // compute distance to voxel
+ var v = intersection.voxel;
+ var maxCorner = Vec3.sum({ x: v.x, y: v.y, z: v.z }, {x: v.s, y: v.s, z: v.s });
+ var distance = lastPosition.y - maxCorner.y;
+
+ if (distance < MAX_VOXEL_SCAN_DISTANCE) {
+ if (speed < MIN_FLYING_SPEED &&
+ gravityOnExpiry < now &&
+ !(MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY)) {
+ enableArtificialGravity();
+ }
+ if (speed < MAX_WALKING_SPEED) {
+ gravityOffExpiry = now + GRAVITY_OFF_EXPIRY_PERIOD;
+ } else if (gravityOffExpiry < now && MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY) {
+ disableArtificialGravity();
+ }
+ } else {
+ // distance too far
+ if (gravityOffExpiry < now && MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY) {
+ disableArtificialGravity();
+ }
+ gravityOnExpiry = now + GRAVITY_ON_EXPIRY_PERIOD;
+ }
+ } else {
+ // no intersection
+ if (gravityOffExpiry < now && MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY) {
+ disableArtificialGravity();
+ }
+ gravityOnExpiry = now + GRAVITY_ON_EXPIRY_PERIOD;
+ }
+ }
+ if (speed > MAX_WALKING_SPEED && gravityOffExpiry < now) {
+ if (MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY) {
+ // turn off gravity
+ MyAvatar.motionBehaviors = MyAvatar.motionBehaviors & ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY;
+ updateButton(3, false);
+ }
+ gravityOnExpiry = now + GRAVITY_ON_EXPIRY_PERIOD;
+ }
+
+ // transition collidability with voxels
+ if (speed < MIN_COLLISIONLESS_SPEED) {
+ if (collisionOnExpiry < now && !(MyAvatar.collisionGroups & COLLISION_GROUP_VOXELS)) {
+ // TODO: check to make sure not already colliding
+ // enable collision with voxels
+ groupBits |= COLLISION_GROUP_VOXELS;
+ updateButton(1, groupBits & COLLISION_GROUP_VOXELS);
+ }
+ } else {
+ collisionOnExpiry = now + COLLISION_EXPIRY_PERIOD;
+ }
+ if (speed > MAX_COLLIDABLE_SPEED) {
+ if (MyAvatar.collisionGroups & COLLISION_GROUP_VOXELS) {
+ // disable collisions with voxels
+ groupBits &= ~COLLISION_GROUP_VOXELS;
+ updateButton(1, groupBits & COLLISION_GROUP_VOXELS);
+ }
+ }
}
Script.update.connect(update);
@@ -131,7 +280,7 @@ Script.update.connect(update);
// we also handle click detection in our mousePressEvent()
function mousePressEvent(event) {
var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y});
- for (i = 0; i < numberOfButtons; i++) {
+ for (i = 0; i < NUMBER_OF_COLLISION_BUTTONS; i++) {
if (clickedOverlay == buttons[i]) {
var enabled = !(buttonStates[i]);
updateButton(i, enabled);
diff --git a/interface/resources/images/hifi-logo.svg b/interface/resources/images/hifi-logo.svg
new file mode 100644
index 0000000000..e5d66d8f18
--- /dev/null
+++ b/interface/resources/images/hifi-logo.svg
@@ -0,0 +1,58 @@
+
+
+
+
diff --git a/interface/resources/images/login.svg b/interface/resources/images/login.svg
new file mode 100644
index 0000000000..f45097b8e3
--- /dev/null
+++ b/interface/resources/images/login.svg
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/interface/resources/styles/log_dialog.qss b/interface/resources/styles/log_dialog.qss
index b7387c1ee7..330356e750 100644
--- a/interface/resources/styles/log_dialog.qss
+++ b/interface/resources/styles/log_dialog.qss
@@ -33,6 +33,7 @@ QPushButton#searchButton {
}
QPushButton#revealLogButton {
+ font-family: Helvetica, Arial, sans-serif;
background: url(styles/txt-file.svg);
background-repeat: none;
background-position: left center;
@@ -50,9 +51,9 @@ QCheckBox {
}
QCheckBox::indicator:unchecked {
- image: url(:/styles/unchecked.svg);
+ image: url(styles/unchecked.svg);
}
QCheckBox::indicator:checked {
- image: url(:/styles/checked.svg);
+ image: url(styles/checked.svg);
}
\ No newline at end of file
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 97b5c05f25..7ee4fdc90d 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -316,6 +316,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
Particle::setVoxelEditPacketSender(&_voxelEditSender);
Particle::setParticleEditPacketSender(&_particleEditSender);
+ // when -url in command line, teleport to location
+ urlGoTo(argc, constArgv);
+
// For now we're going to set the PPS for outbound packets to be super high, this is
// probably not the right long term solution. But for now, we're going to do this to
// allow you to move a particle around in your hand
@@ -352,8 +355,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
QMutexLocker locker(&_settingsMutex);
_previousScriptLocation = _settings->value("LastScriptLocation", QVariant("")).toString();
}
- //When -url in command line, teleport to location
- urlGoTo(argc, constArgv);
}
Application::~Application() {
@@ -857,7 +858,7 @@ void Application::keyPressEvent(QKeyEvent* event) {
case Qt::Key_G:
if (isShifted) {
- Menu::getInstance()->triggerOption(MenuOption::ObeyGravity);
+ Menu::getInstance()->triggerOption(MenuOption::ObeyEnvironmentalGravity);
}
break;
@@ -3576,34 +3577,33 @@ void Application::takeSnapshot() {
void Application::urlGoTo(int argc, const char * constArgv[]) {
//Gets the url (hifi://domain/destination/orientation)
QString customUrl = getCmdOption(argc, constArgv, "-url");
-
- if (customUrl.startsWith("hifi://")) {
+ if(customUrl.startsWith(CUSTOM_URL_SCHEME + "//")) {
QStringList urlParts = customUrl.remove(0, CUSTOM_URL_SCHEME.length() + 2).split('/', QString::SkipEmptyParts);
- if (urlParts.count() > 1) {
+ if (urlParts.count() == 1) {
+ // location coordinates or place name
+ QString domain = urlParts[0];
+ Menu::goToDomain(domain);
+ } else if (urlParts.count() > 1) {
// if url has 2 or more parts, the first one is domain name
QString domain = urlParts[0];
-
+
// second part is either a destination coordinate or
// a place name
QString destination = urlParts[1];
-
+
// any third part is an avatar orientation.
QString orientation = urlParts.count() > 2 ? urlParts[2] : QString();
-
+
Menu::goToDomain(domain);
-
+
// goto either @user, #place, or x-xx,y-yy,z-zz
// style co-ordinate.
Menu::goTo(destination);
-
+
if (!orientation.isEmpty()) {
// location orientation
Menu::goToOrientation(orientation);
}
- } else if (urlParts.count() == 1) {
- // location coordinates or place name
- QString destination = urlParts[0];
- Menu::goTo(destination);
- }
+ }
}
}
diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp
index 44117df55c..59da23dc88 100644
--- a/interface/src/Menu.cpp
+++ b/interface/src/Menu.cpp
@@ -40,6 +40,7 @@
#include "ui/InfoView.h"
#include "ui/MetavoxelEditor.h"
#include "ui/ModelsBrowser.h"
+#include "ui/LoginDialog.h"
Menu* Menu::_instance = NULL;
@@ -188,8 +189,8 @@ Menu::Menu() :
addDisabledActionAndSeparator(editMenu, "Physics");
QObject* avatar = appInstance->getAvatar();
- addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::ObeyGravity, Qt::SHIFT | Qt::Key_G, true,
- avatar, SLOT(updateMotionBehaviorFlags()));
+ addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::ObeyEnvironmentalGravity, Qt::SHIFT | Qt::Key_G, true,
+ avatar, SLOT(updateMotionBehaviors()));
addAvatarCollisionSubMenu(editMenu);
@@ -817,38 +818,9 @@ const int QLINE_MINIMUM_WIDTH = 400;
const float DIALOG_RATIO_OF_WINDOW = 0.30f;
void Menu::loginForCurrentDomain() {
- QDialog loginDialog(Application::getInstance()->getWindow());
- loginDialog.setWindowTitle("Login");
-
- QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom);
- loginDialog.setLayout(layout);
- loginDialog.setWindowFlags(Qt::Sheet);
-
- QFormLayout* form = new QFormLayout();
- layout->addLayout(form, 1);
-
- QLineEdit* loginLineEdit = new QLineEdit();
- loginLineEdit->setMinimumWidth(QLINE_MINIMUM_WIDTH);
- form->addRow("Login:", loginLineEdit);
-
- QLineEdit* passwordLineEdit = new QLineEdit();
- passwordLineEdit->setMinimumWidth(QLINE_MINIMUM_WIDTH);
- passwordLineEdit->setEchoMode(QLineEdit::Password);
- form->addRow("Password:", passwordLineEdit);
-
- QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
- loginDialog.connect(buttons, SIGNAL(accepted()), SLOT(accept()));
- loginDialog.connect(buttons, SIGNAL(rejected()), SLOT(reject()));
- layout->addWidget(buttons);
-
- int dialogReturn = loginDialog.exec();
-
- if (dialogReturn == QDialog::Accepted && !loginLineEdit->text().isEmpty() && !passwordLineEdit->text().isEmpty()) {
- // attempt to get an access token given this username and password
- AccountManager::getInstance().requestAccessToken(loginLineEdit->text(), passwordLineEdit->text());
- }
-
- sendFakeEnterEvent();
+ LoginDialog* loginDialog = new LoginDialog(Application::getInstance()->getWindow());
+ loginDialog->show();
+ loginDialog->resizeAndPosition(false);
}
void Menu::editPreferences() {
@@ -931,7 +903,12 @@ void Menu::goTo() {
if (desiredDestination.startsWith(CUSTOM_URL_SCHEME + "//")) {
QStringList urlParts = desiredDestination.remove(0, CUSTOM_URL_SCHEME.length() + 2).split('/', QString::SkipEmptyParts);
- if (urlParts.count() > 1) {
+ if (urlParts.count() == 1) {
+ // location coordinates or place name
+ QString domain = urlParts[0];
+ goToDomain(domain);
+ }
+ else if (urlParts.count() > 1) {
// if url has 2 or more parts, the first one is domain name
QString domain = urlParts[0];
@@ -952,12 +929,7 @@ void Menu::goTo() {
// location orientation
goToOrientation(orientation);
}
- } else if (urlParts.count() == 1) {
- // location coordinates or place name
- QString destination = urlParts[0];
- goTo(destination);
}
-
} else {
goToUser(gotoDialog.textValue());
}
diff --git a/interface/src/Menu.h b/interface/src/Menu.h
index bc70f8f83f..bbd3aa13ae 100644
--- a/interface/src/Menu.h
+++ b/interface/src/Menu.h
@@ -315,7 +315,7 @@ namespace MenuOption {
const QString GoTo = "Go To...";
const QString GoToDomain = "Go To Domain...";
const QString GoToLocation = "Go To Location...";
- const QString ObeyGravity = "Obey Gravity";
+ const QString ObeyEnvironmentalGravity = "Obey Environmental Gravity";
const QString HandsCollideWithSelf = "Collide With Self";
const QString HeadMouse = "Head Mouse";
const QString IncreaseAvatarSize = "Increase Avatar Size";
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index 26dbc09d5b..40e350dcb7 100644
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -125,10 +125,8 @@ void MyAvatar::update(float deltaTime) {
head->setAudioLoudness(audio->getLastInputLoudness());
head->setAudioAverageLoudness(audio->getAudioAverageInputLoudness());
- if (_motionBehaviors & AVATAR_MOTION_OBEY_GRAVITY) {
+ if (_motionBehaviors & AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY) {
setGravity(Application::getInstance()->getEnvironment()->getGravity(getPosition()));
- } else {
- setGravity(glm::vec3(0.0f, 0.0f, 0.0f));
}
simulate(deltaTime);
@@ -463,6 +461,27 @@ void MyAvatar::renderHeadMouse() const {
*/
}
+void MyAvatar::setLocalGravity(glm::vec3 gravity) {
+ _motionBehaviors |= AVATAR_MOTION_OBEY_LOCAL_GRAVITY;
+ // Environmental and Local gravities are incompatible. Since Local is being set here
+ // the environmental setting must be removed.
+ _motionBehaviors &= ~AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY;
+ setGravity(gravity);
+}
+
+void MyAvatar::setGravity(const glm::vec3& gravity) {
+ _gravity = gravity;
+ getHead()->setGravity(_gravity);
+
+ // use the gravity to determine the new world up direction, if possible
+ float gravityLength = glm::length(gravity);
+ if (gravityLength > EPSILON) {
+ _worldUpDirection = _gravity / -gravityLength;
+ } else {
+ _worldUpDirection = DEFAULT_UP_DIRECTION;
+ }
+}
+
void MyAvatar::saveData(QSettings* settings) {
settings->beginGroup("Avatar");
@@ -1046,19 +1065,6 @@ void MyAvatar::maybeUpdateBillboard() {
sendBillboardPacket();
}
-void MyAvatar::setGravity(glm::vec3 gravity) {
- _gravity = gravity;
- getHead()->setGravity(_gravity);
-
- // use the gravity to determine the new world up direction, if possible
- float gravityLength = glm::length(gravity);
- if (gravityLength > EPSILON) {
- _worldUpDirection = _gravity / -gravityLength;
- } else {
- _worldUpDirection = DEFAULT_UP_DIRECTION;
- }
-}
-
void MyAvatar::goHome() {
qDebug("Going Home!");
setPosition(START_LOCATION);
@@ -1147,8 +1153,13 @@ void MyAvatar::goToLocationFromResponse(const QJsonObject& jsonObject) {
void MyAvatar::updateMotionBehaviors() {
_motionBehaviors = 0;
- if (Menu::getInstance()->isOptionChecked(MenuOption::ObeyGravity)) {
- _motionBehaviors |= AVATAR_MOTION_OBEY_GRAVITY;
+ if (Menu::getInstance()->isOptionChecked(MenuOption::ObeyEnvironmentalGravity)) {
+ _motionBehaviors |= AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY;
+ // Environmental and Local gravities are incompatible. Environmental setting trumps local.
+ _motionBehaviors &= ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY;
+ }
+ if (! (_motionBehaviors & (AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY | AVATAR_MOTION_OBEY_LOCAL_GRAVITY))) {
+ setGravity(glm::vec3(0.0f));
}
}
@@ -1164,7 +1175,14 @@ void MyAvatar::setCollisionGroups(quint32 collisionGroups) {
void MyAvatar::setMotionBehaviors(quint32 flags) {
_motionBehaviors = flags;
Menu* menu = Menu::getInstance();
- menu->setIsOptionChecked(MenuOption::ObeyGravity, (bool)(_motionBehaviors & AVATAR_MOTION_OBEY_GRAVITY));
+ menu->setIsOptionChecked(MenuOption::ObeyEnvironmentalGravity, (bool)(_motionBehaviors & AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY));
+ // Environmental and Local gravities are incompatible. Environmental setting trumps local.
+ if (_motionBehaviors & AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY) {
+ _motionBehaviors &= ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY;
+ setGravity(Application::getInstance()->getEnvironment()->getGravity(getPosition()));
+ } else if (! (_motionBehaviors & (AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY | AVATAR_MOTION_OBEY_LOCAL_GRAVITY))) {
+ setGravity(glm::vec3(0.0f));
+ }
}
void MyAvatar::applyCollision(const glm::vec3& contactPoint, const glm::vec3& penetration) {
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
index 9b6b13568f..a5312b0016 100644
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -25,12 +25,11 @@ enum AvatarHandState
NUM_HAND_STATES
};
-const quint32 AVATAR_MOTION_OBEY_GRAVITY = 1U << 0;
-
class MyAvatar : public Avatar {
Q_OBJECT
Q_PROPERTY(bool shouldRenderLocally READ getShouldRenderLocally WRITE setShouldRenderLocally)
Q_PROPERTY(quint32 motionBehaviors READ getMotionBehaviors WRITE setMotionBehaviors)
+ Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setLocalGravity)
public:
MyAvatar();
@@ -52,8 +51,7 @@ public:
void setMousePressed(bool mousePressed) { _mousePressed = mousePressed; }
void setVelocity(const glm::vec3 velocity) { _velocity = velocity; }
void setLeanScale(float scale) { _leanScale = scale; }
- void setGravity(glm::vec3 gravity);
- void setMoveTarget(const glm::vec3 moveTarget);
+ void setLocalGravity(glm::vec3 gravity);
void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; }
// getters
@@ -123,6 +121,7 @@ private:
bool _shouldJump;
float _driveKeys[MAX_DRIVE_KEYS];
glm::vec3 _gravity;
+ glm::vec3 _environmentGravity;
float _distanceToNearestAvatar; // How close is the nearest avatar?
// motion stuff
@@ -151,6 +150,7 @@ private:
void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency);
void updateChatCircle(float deltaTime);
void maybeUpdateBillboard();
+ void setGravity(const glm::vec3& gravity);
};
#endif // hifi_MyAvatar_h
diff --git a/interface/src/ui/FramelessDialog.cpp b/interface/src/ui/FramelessDialog.cpp
index 4919e99db6..71c58078d2 100644
--- a/interface/src/ui/FramelessDialog.cpp
+++ b/interface/src/ui/FramelessDialog.cpp
@@ -19,7 +19,8 @@ FramelessDialog::FramelessDialog(QWidget *parent, Qt::WindowFlags flags, Positio
_isResizing(false),
_resizeInitialWidth(0),
_selfHidden(false),
- _position(position) {
+ _position(position),
+ _hideOnBlur(true) {
setAttribute(Qt::WA_DeleteOnClose);
@@ -43,7 +44,7 @@ bool FramelessDialog::eventFilter(QObject* sender, QEvent* event) {
}
break;
case QEvent::WindowStateChange:
- if (parentWidget()->isMinimized()) {
+ if (_hideOnBlur && parentWidget()->isMinimized()) {
if (isVisible()) {
_selfHidden = true;
setHidden(true);
@@ -55,7 +56,7 @@ bool FramelessDialog::eventFilter(QObject* sender, QEvent* event) {
break;
case QEvent::ApplicationDeactivate:
// hide on minimize and focus lost
- if (isVisible()) {
+ if (_hideOnBlur && isVisible()) {
_selfHidden = true;
setHidden(true);
}
@@ -84,18 +85,23 @@ void FramelessDialog::setStyleSheetFile(const QString& fileName) {
void FramelessDialog::showEvent(QShowEvent* event) {
resizeAndPosition();
+ QDialog::showEvent(event);
}
void FramelessDialog::resizeAndPosition(bool resizeParent) {
- // keep full app height
- setFixedHeight(parentWidget()->size().height());
+ // keep full app height or width depending on position
+ if (_position == POSITION_LEFT || _position == POSITION_RIGHT) {
+ setFixedHeight(parentWidget()->size().height());
+ } else {
+ setFixedWidth(parentWidget()->size().width());
+ }
// resize parrent if width is smaller than this dialog
if (resizeParent && parentWidget()->size().width() < size().width()) {
parentWidget()->resize(size().width(), parentWidget()->size().height());
}
- if (_position == POSITION_LEFT) {
+ if (_position == POSITION_LEFT || _position == POSITION_TOP) {
// move to upper left corner
move(parentWidget()->geometry().topLeft());
} else if (_position == POSITION_RIGHT) {
@@ -104,16 +110,26 @@ void FramelessDialog::resizeAndPosition(bool resizeParent) {
pos.setX(pos.x() - size().width());
move(pos);
}
+ repaint();
}
void FramelessDialog::mousePressEvent(QMouseEvent* mouseEvent) {
if (mouseEvent->button() == Qt::LeftButton) {
- bool hitLeft = _position == POSITION_LEFT && abs(mouseEvent->pos().x() - size().width()) < RESIZE_HANDLE_WIDTH;
- bool hitRight = _position == POSITION_RIGHT && mouseEvent->pos().x() < RESIZE_HANDLE_WIDTH;
- if (hitLeft || hitRight) {
- _isResizing = true;
- _resizeInitialWidth = size().width();
- QApplication::setOverrideCursor(Qt::SizeHorCursor);
+ if (_position == POSITION_LEFT || _position == POSITION_RIGHT) {
+ bool hitLeft = (_position == POSITION_LEFT) && (abs(mouseEvent->pos().x() - size().width()) < RESIZE_HANDLE_WIDTH);
+ bool hitRight = (_position == POSITION_RIGHT) && (mouseEvent->pos().x() < RESIZE_HANDLE_WIDTH);
+ if (hitLeft || hitRight) {
+ _isResizing = true;
+ _resizeInitialWidth = size().width();
+ QApplication::setOverrideCursor(Qt::SizeHorCursor);
+ }
+ } else {
+ bool hitTop = (_position == POSITION_TOP) && (abs(mouseEvent->pos().y() - size().height()) < RESIZE_HANDLE_WIDTH);
+ if (hitTop) {
+ _isResizing = true;
+ _resizeInitialWidth = size().height();
+ QApplication::setOverrideCursor(Qt::SizeHorCursor);
+ }
}
}
}
@@ -133,6 +149,8 @@ void FramelessDialog::mouseMoveEvent(QMouseEvent* mouseEvent) {
resizeAndPosition();
_resizeInitialWidth = size().width();
setUpdatesEnabled(true);
+ } else if (_position == POSITION_TOP) {
+ resize(size().width(), mouseEvent->pos().y());
}
}
}
diff --git a/interface/src/ui/FramelessDialog.h b/interface/src/ui/FramelessDialog.h
index 828602a5db..2265f3c9e4 100644
--- a/interface/src/ui/FramelessDialog.h
+++ b/interface/src/ui/FramelessDialog.h
@@ -17,12 +17,15 @@
class FramelessDialog : public QDialog {
Q_OBJECT
-
-public:
- enum Position { POSITION_LEFT, POSITION_RIGHT };
- FramelessDialog(QWidget* parent = 0, Qt::WindowFlags flags = 0, Position position = POSITION_LEFT);
+public:
+ enum Position { POSITION_LEFT, POSITION_RIGHT, POSITION_TOP };
+
+ FramelessDialog(QWidget* parent, Qt::WindowFlags flags = 0, Position position = POSITION_LEFT);
void setStyleSheetFile(const QString& fileName);
+ void setHideOnBlur(bool hideOnBlur) { _hideOnBlur = hideOnBlur; };
+ bool getHideOnBlur() { return _hideOnBlur; };
+ void resizeAndPosition(bool resizeParent = true);
protected:
virtual void mouseMoveEvent(QMouseEvent* mouseEvent);
@@ -33,12 +36,11 @@ protected:
bool eventFilter(QObject* sender, QEvent* event);
private:
- void resizeAndPosition(bool resizeParent = true);
-
bool _isResizing;
int _resizeInitialWidth;
bool _selfHidden; ///< true when the dialog itself because of a window event (deactivation or minimization)
Position _position;
+ bool _hideOnBlur;
};
diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp
new file mode 100644
index 0000000000..993f355c47
--- /dev/null
+++ b/interface/src/ui/LoginDialog.cpp
@@ -0,0 +1,91 @@
+//
+// LoginDialog.cpp
+// interface/src/ui
+//
+// Created by Ryan Huffman on 4/23/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
+#include
+#include
+
+#include "Application.h"
+#include "Menu.h"
+#include "AccountManager.h"
+#include "ui_loginDialog.h"
+
+#include "LoginDialog.h"
+
+const QString FORGOT_PASSWORD_URL = "https://data-web.highfidelity.io/password/new";
+
+LoginDialog::LoginDialog(QWidget* parent) :
+ FramelessDialog(parent, 0, FramelessDialog::POSITION_TOP),
+ _ui(new Ui::LoginDialog) {
+
+ _ui->setupUi(this);
+ _ui->errorLabel->hide();
+ _ui->emailLineEdit->setFocus();
+ _ui->logoLabel->setPixmap(QPixmap(Application::resourcesPath() + "images/hifi-logo.svg"));
+ _ui->loginButton->setIcon(QIcon(Application::resourcesPath() + "images/login.svg"));
+ _ui->infoLabel->setVisible(false);
+ _ui->errorLabel->setVisible(false);
+
+ setModal(true);
+ setHideOnBlur(false);
+
+ connect(&AccountManager::getInstance(), &AccountManager::loginComplete,
+ this, &LoginDialog::handleLoginCompleted);
+ connect(&AccountManager::getInstance(), &AccountManager::loginFailed,
+ this, &LoginDialog::handleLoginFailed);
+ connect(_ui->loginButton, &QPushButton::clicked,
+ this, &LoginDialog::handleLoginClicked);
+ connect(_ui->closeButton, &QPushButton::clicked,
+ this, &LoginDialog::close);
+};
+
+LoginDialog::~LoginDialog() {
+ delete _ui;
+};
+
+void LoginDialog::handleLoginCompleted(const QUrl& authURL) {
+ _ui->infoLabel->setVisible(false);
+ _ui->errorLabel->setVisible(false);
+ close();
+};
+
+void LoginDialog::handleLoginFailed() {
+ _ui->infoLabel->setVisible(false);
+ _ui->errorLabel->setVisible(true);
+
+ _ui->errorLabel->show();
+ _ui->loginArea->setDisabled(false);
+
+ // Move focus to password and select the entire line
+ _ui->passwordLineEdit->setFocus();
+ _ui->passwordLineEdit->setSelection(0, _ui->emailLineEdit->maxLength());
+};
+
+void LoginDialog::handleLoginClicked() {
+ // If the email or password inputs are empty, move focus to them, otherwise attempt to login.
+ if (_ui->emailLineEdit->text().isEmpty()) {
+ _ui->emailLineEdit->setFocus();
+ } else if (_ui->passwordLineEdit->text().isEmpty()) {
+ _ui->passwordLineEdit->setFocus();
+ } else {
+ _ui->infoLabel->setVisible(true);
+ _ui->errorLabel->setVisible(false);
+
+ _ui->loginArea->setDisabled(true);
+ AccountManager::getInstance().requestAccessToken(_ui->emailLineEdit->text(), _ui->passwordLineEdit->text());
+ }
+};
+
+void LoginDialog::moveEvent(QMoveEvent* event) {
+ // Modal dialogs seemed to get repositioned automatically. Combat this by moving the window if needed.
+ resizeAndPosition();
+};
diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h
new file mode 100644
index 0000000000..b4aad06614
--- /dev/null
+++ b/interface/src/ui/LoginDialog.h
@@ -0,0 +1,42 @@
+//
+// LoginDialog.h
+// interface/src/ui
+//
+// Created by Ryan Huffman on 4/23/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_LoginDialog_h
+#define hifi_LoginDialog_h
+
+#include
+#include "FramelessDialog.h"
+
+namespace Ui {
+ class LoginDialog;
+}
+
+class LoginDialog : public FramelessDialog {
+ Q_OBJECT
+
+public:
+ LoginDialog(QWidget* parent);
+ ~LoginDialog();
+
+public slots:
+ void handleLoginClicked();
+ void handleLoginCompleted(const QUrl& authURL);
+ void handleLoginFailed();
+
+protected:
+ void moveEvent(QMoveEvent* event);
+
+private:
+ Ui::LoginDialog* _ui;
+
+};
+
+#endif // hifi_LoginDialog_h
diff --git a/interface/ui/loginDialog.ui b/interface/ui/loginDialog.ui
new file mode 100644
index 0000000000..d54b3c833f
--- /dev/null
+++ b/interface/ui/loginDialog.ui
@@ -0,0 +1,491 @@
+
+
+ LoginDialog
+
+
+
+ 0
+ 0
+ 1003
+ 130
+
+
+
+
+ 0
+ 130
+
+
+
+ Dialog
+
+
+ font-family: Helvetica, Arial, sans-serif;
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 30
+
+
+
+
+ 5
+
+
+ 0
+
+
+ 10
+
+
+ 10
+
+
+ 0
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 825
+ 0
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ Helvetica,Arial,sans-serif
+ 17
+
+
+
+ Authenticating...
+
+
+
+ -
+
+
+ true
+
+
+
+ Helvetica,Arial,sans-serif
+ 17
+
+
+
+ color: #992800;
+
+
+ <style type="text/css">
+ a { text-decoration: none; color: #267077;}
+</style>
+Invalid username or password. <a href="https://data-web.highfidelity.io/password/new">Recover?</a>
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16
+ 16
+
+
+
+
+ 16
+ 16
+
+
+
+ SplitHCursor
+
+
+
+
+
+
+
+
+
+ ../resources/images/close.svg../resources/images/close.svg
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 102
+
+
+
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 30
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 12
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ ../resources/images/hifi-logo.png
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 931
+ 60
+
+
+
+ QLineEdit {
+ padding: 10px;
+border-width: 1px; border-style: solid; border-radius: 3px; border-color: #aaa;
+}
+
+
+
+ 12
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 300
+ 54
+
+
+
+
+ Helvetica,Arial,sans-serif
+ 20
+
+
+
+ padding-top: 14px;
+
+
+
+
+
+ Username or Email
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 300
+ 54
+
+
+
+
+ Helvetica,Arial,sans-serif
+ 20
+
+
+
+ padding-top: 14px;
+
+
+
+
+
+ QLineEdit::Password
+
+
+ Password
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 141
+ 54
+
+
+
+
+ Helvetica,Arial,sans-serif
+ 20
+
+
+
+ PointingHandCursor
+
+
+
+background: #0e7077;
+color: #e7eeee;
+border-radius: 4px; padding-top: 1px;
+
+
+ Login
+
+
+
+ ../resources/images/login.svg../resources/images/login.svg
+
+
+
+ 32
+ 32
+
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+
+ Helvetica,Arial,sans-serif
+ 17
+
+
+
+ <style type="text/css">
+ a { text-decoration: none; color: #267077;}
+</style>
+<a href="https://data-web.highfidelity.io/password/new">Recover password?</a>
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h
index 250ae07b01..be47aed1ba 100755
--- a/libraries/avatars/src/AvatarData.h
+++ b/libraries/avatars/src/AvatarData.h
@@ -50,6 +50,10 @@ typedef unsigned long long quint64;
#include "HeadData.h"
#include "HandData.h"
+// avatar motion behaviors
+const quint32 AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY = 1U << 0;
+const quint32 AVATAR_MOTION_OBEY_LOCAL_GRAVITY = 1U << 1;
+
// First bitset
const int KEY_STATE_START_BIT = 0; // 1st and 2nd bits
const int HAND_STATE_START_BIT = 2; // 3rd and 4th bits
diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp
index 278923026d..547768ec48 100644
--- a/libraries/networking/src/AccountManager.cpp
+++ b/libraries/networking/src/AccountManager.cpp
@@ -337,6 +337,7 @@ void AccountManager::requestFinished() {
} else {
// TODO: error handling
qDebug() << "Error in response for password grant -" << rootObject["error_description"].toString();
+ emit loginFailed();
}
}
diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h
index cb76786f4e..8df75195cf 100644
--- a/libraries/networking/src/AccountManager.h
+++ b/libraries/networking/src/AccountManager.h
@@ -71,6 +71,7 @@ signals:
void usernameChanged(const QString& username);
void accessTokenChanged();
void loginComplete(const QUrl& authURL);
+ void loginFailed();
void logoutComplete();
private slots:
void processReply();
diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp
index ed05658538..b8b755e099 100644
--- a/libraries/script-engine/src/ScriptEngine.cpp
+++ b/libraries/script-engine/src/ScriptEngine.cpp
@@ -239,6 +239,9 @@ void ScriptEngine::init() {
globalObject.setProperty("COLLISION_GROUP_VOXELS", _engine.newVariant(QVariant(COLLISION_GROUP_VOXELS)));
globalObject.setProperty("COLLISION_GROUP_PARTICLES", _engine.newVariant(QVariant(COLLISION_GROUP_PARTICLES)));
+ globalObject.setProperty("AVATAR_MOTION_OBEY_LOCAL_GRAVITY", _engine.newVariant(QVariant(AVATAR_MOTION_OBEY_LOCAL_GRAVITY)));
+ globalObject.setProperty("AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY", _engine.newVariant(QVariant(AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY)));
+
// let the VoxelPacketSender know how frequently we plan to call it
_voxelsScriptingInterface.getVoxelPacketSender()->setProcessCallIntervalHint(SCRIPT_DATA_CALLBACK_USECS);
_particlesScriptingInterface.getParticlePacketSender()->setProcessCallIntervalHint(SCRIPT_DATA_CALLBACK_USECS);
diff --git a/libraries/script-engine/src/Vec3.cpp b/libraries/script-engine/src/Vec3.cpp
index bab529df1f..badc980913 100644
--- a/libraries/script-engine/src/Vec3.cpp
+++ b/libraries/script-engine/src/Vec3.cpp
@@ -36,6 +36,10 @@ float Vec3::length(const glm::vec3& v) {
return glm::length(v);
}
+float Vec3::distance(const glm::vec3& v1, const glm::vec3& v2) {
+ return glm::distance(v1, v2);
+}
+
glm::vec3 Vec3::normalize(const glm::vec3& v) {
return glm::normalize(v);
}
diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h
index b30e274d89..e401cd71bd 100644
--- a/libraries/script-engine/src/Vec3.h
+++ b/libraries/script-engine/src/Vec3.h
@@ -31,6 +31,7 @@ public slots:
glm::vec3 sum(const glm::vec3& v1, const glm::vec3& v2);
glm::vec3 subtract(const glm::vec3& v1, const glm::vec3& v2);
float length(const glm::vec3& v);
+ float distance(const glm::vec3& v1, const glm::vec3& v2);
glm::vec3 normalize(const glm::vec3& v);
void print(const QString& lable, const glm::vec3& v);
};