From 8fc04776f9859b048965509a033358a6ad5a4684 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 21 Jun 2018 10:59:07 -0700 Subject: [PATCH 001/182] starting to implement the step detection code in MyAvatar.cpp --- interface/src/avatar/MyAvatar.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index d57905ee33..7c683de68f 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3325,11 +3325,15 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat activate(Vertical); } } else { + // this is where we put the code for the stepping. + // we do not have hmd lean enabled and we are looking for a step via our criteria. + if (!isActive(Rotation) && getForceActivateRotation()) { activate(Rotation); setForceActivateRotation(false); } - if (!isActive(Horizontal) && getForceActivateHorizontal()) { + if (!isActive(Horizontal) && (getForceActivateHorizontal() || + !withinTheBaseOfSupport() { activate(Horizontal); setForceActivateHorizontal(false); } From 3027f8242cdd6f14843bcb1d1a7d0e7df53d5cf8 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 21 Jun 2018 17:23:03 -0700 Subject: [PATCH 002/182] added the conditional for leaving the base of support to pre physics update in MyAvatar.cpp --- interface/src/avatar/MyAvatar.cpp | 35 +++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 7c683de68f..c1898c43e8 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3105,6 +3105,29 @@ glm::mat4 MyAvatar::deriveBodyUsingCgModel() const { return worldToSensorMat * avatarToWorldMat * avatarHipsMat; } +static bool isInsideLine(glm::vec3 a, glm::vec3 b, glm::vec3 c) { + return (((b.x - a.x)*(c.z - a.z) - (b.z - a.z)*(c.x - a.x)) > 0); +} + +static bool withinBaseOfSupport(glm::vec3 position) { + float userScale = 1.0f; + + const float DEFAULT_LATERAL = 1.10f; + const float DEFAULT_ANTERIOR = 1.04f; + const float DEFAULT_POSTERIOR = 1.06f; + + glm::vec3 frontLeft(-DEFAULT_LATERAL, 0.0f, -DEFAULT_ANTERIOR); + glm::vec3 frontRight(DEFAULT_LATERAL, 0.0f, -DEFAULT_ANTERIOR); + glm::vec3 backLeft(-DEFAULT_LATERAL, 0.0f, DEFAULT_POSTERIOR); + glm::vec3 backRight(DEFAULT_LATERAL, 0.0f, DEFAULT_POSTERIOR); + + bool withinFrontBase = isInsideLine(userScale * frontLeft, userScale * frontRight, position); + bool withinBackBase = isInsideLine(userScale * backRight, userScale * backLeft, position); + bool withinLateralBase = (isInsideLine(userScale * frontRight, userScale * backRight, position) && + isInsideLine(userScale * backLeft, userScale * frontLeft, position)); + return (withinFrontBase && withinBackBase && withinLateralBase); +} + float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -3327,13 +3350,21 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } else { // this is where we put the code for the stepping. // we do not have hmd lean enabled and we are looking for a step via our criteria. - + qCDebug(interfaceapp) << "hmd lean is off"; if (!isActive(Rotation) && getForceActivateRotation()) { activate(Rotation); setForceActivateRotation(false); } + glm::vec3 temp = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation(); + qCDebug(interfaceapp) << temp; + qCDebug(interfaceapp) << "zero within base " << withinBaseOfSupport(glm::vec3(0.0f,0.0f,0.0f)); + qCDebug(interfaceapp) << "10 meters within base " << withinBaseOfSupport(glm::vec3(1.0f, 0.0f, 0.0f)); + qCDebug(interfaceapp) << "head within base " << withinBaseOfSupport(temp); + qCDebug(interfaceapp) << "force activate horizontal " << getForceActivateHorizontal(); + qCDebug(interfaceapp) << "is active horizontal " << isActive(Horizontal); if (!isActive(Horizontal) && (getForceActivateHorizontal() || - !withinTheBaseOfSupport() { + !withinBaseOfSupport(temp))) { + qCDebug(interfaceapp) << "----------------------------------------over the base of support"; activate(Horizontal); setForceActivateHorizontal(false); } From 8cf59783d1be7b502b5760bb7ebd17d455021560 Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 27 Jun 2018 09:20:46 -0700 Subject: [PATCH 003/182] removed white space --- interface/src/avatar/MyAvatar.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0768cc321e..5766322bad 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3124,7 +3124,7 @@ static bool withinBaseOfSupport(glm::vec3 position) { bool withinFrontBase = isInsideLine(userScale * frontLeft, userScale * frontRight, position); bool withinBackBase = isInsideLine(userScale * backRight, userScale * backLeft, position); - bool withinLateralBase = (isInsideLine(userScale * frontRight, userScale * backRight, position) && + bool withinLateralBase = (isInsideLine(userScale * frontRight, userScale * backRight, position) && isInsideLine(userScale * backLeft, userScale * frontLeft, position)); return (withinFrontBase && withinBackBase && withinLateralBase); } @@ -3363,7 +3363,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat qCDebug(interfaceapp) << "head within base " << withinBaseOfSupport(temp); qCDebug(interfaceapp) << "force activate horizontal " << getForceActivateHorizontal(); qCDebug(interfaceapp) << "is active horizontal " << isActive(Horizontal); - if (!isActive(Horizontal) && (getForceActivateHorizontal() || + if (!isActive(Horizontal) && (getForceActivateHorizontal() || !withinBaseOfSupport(temp))) { qCDebug(interfaceapp) << "----------------------------------------over the base of support"; activate(Horizontal); From 3893b7e339fb38b61544f72eb6581f3b70ab915c Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 27 Jun 2018 10:37:54 -0700 Subject: [PATCH 004/182] added the head angular velocity check --- interface/src/avatar/MyAvatar.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5766322bad..2b1e48e752 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3129,6 +3129,14 @@ static bool withinBaseOfSupport(glm::vec3 position) { return (withinFrontBase && withinBackBase && withinLateralBase); } +static bool headAngularVelocityBelowThreshold(glm::vec3 angularVelocity) { + const float ANGULAR_VELOCITY_THRESHOLD = 0.3f; + float xzPlaneAngularVelocity = glm::vec3(angularVelocity.x, 0.0f, angularVelocity.z).length(); + bool isBelowThreshold = xzPlaneAngularVelocity < ANGULAR_VELOCITY_THRESHOLD; + qCDebug(interfaceapp) << "head velocity below threshold is: " << isBelowThreshold; + return isBelowThreshold; +} + float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -3356,15 +3364,12 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat activate(Rotation); setForceActivateRotation(false); } - glm::vec3 temp = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation(); - qCDebug(interfaceapp) << temp; - qCDebug(interfaceapp) << "zero within base " << withinBaseOfSupport(glm::vec3(0.0f,0.0f,0.0f)); - qCDebug(interfaceapp) << "10 meters within base " << withinBaseOfSupport(glm::vec3(1.0f, 0.0f, 0.0f)); - qCDebug(interfaceapp) << "head within base " << withinBaseOfSupport(temp); - qCDebug(interfaceapp) << "force activate horizontal " << getForceActivateHorizontal(); - qCDebug(interfaceapp) << "is active horizontal " << isActive(Horizontal); + + headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()); + if (!isActive(Horizontal) && (getForceActivateHorizontal() || - !withinBaseOfSupport(temp))) { + (!withinBaseOfSupport(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation()) && + headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity())))) { qCDebug(interfaceapp) << "----------------------------------------over the base of support"; activate(Horizontal); setForceActivateHorizontal(false); From 7b49ae49508141ccd0129f64bf394555aca2ec47 Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 27 Jun 2018 14:23:24 -0700 Subject: [PATCH 005/182] added the within threshold of height function and to do: the mode computation --- interface/src/avatar/MyAvatar.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 2b1e48e752..1d6034fd11 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3131,12 +3131,19 @@ static bool withinBaseOfSupport(glm::vec3 position) { static bool headAngularVelocityBelowThreshold(glm::vec3 angularVelocity) { const float ANGULAR_VELOCITY_THRESHOLD = 0.3f; - float xzPlaneAngularVelocity = glm::vec3(angularVelocity.x, 0.0f, angularVelocity.z).length(); - bool isBelowThreshold = xzPlaneAngularVelocity < ANGULAR_VELOCITY_THRESHOLD; - qCDebug(interfaceapp) << "head velocity below threshold is: " << isBelowThreshold; + glm::vec3 xzPlaneAngularVelocity(angularVelocity.x, 0.0f, angularVelocity.z); + float magnitudeAngularVelocity = glm::length(xzPlaneAngularVelocity); + bool isBelowThreshold = (magnitudeAngularVelocity < ANGULAR_VELOCITY_THRESHOLD); + qCDebug(interfaceapp) << "magnitude " << magnitudeAngularVelocity << "head velocity below threshold is: " << isBelowThreshold; + qCDebug(interfaceapp) << "ang vel values x: " << angularVelocity.x << " y: " << angularVelocity.y << " z: " << angularVelocity.z; return isBelowThreshold; } +static bool withinThresholdOfStandingHeightMode(float diffFromMode) { + const float MODE_HEIGHT_THRESHOLD = 0.3f; + return (diffFromMode < MODE_HEIGHT_THRESHOLD); +} + float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -3369,7 +3376,8 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat if (!isActive(Horizontal) && (getForceActivateHorizontal() || (!withinBaseOfSupport(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation()) && - headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity())))) { + headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()))) && + withinThresholdOfStandingHeightMode(0.01f)) { qCDebug(interfaceapp) << "----------------------------------------over the base of support"; activate(Horizontal); setForceActivateHorizontal(false); From 61a935dbb50b96adb8bb084921aef3c1bf838eb9 Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 27 Jun 2018 18:02:11 -0700 Subject: [PATCH 006/182] added the first pass at mode computation --- interface/src/avatar/MyAvatar.cpp | 524 +++++++++++++++--------------- interface/src/avatar/MyAvatar.h | 8 + 2 files changed, 271 insertions(+), 261 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1d6034fd11..f33bd41f16 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -69,11 +69,11 @@ using namespace std; const float DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES = 30.0f; const float YAW_SPEED_DEFAULT = 100.0f; // degrees/sec -const float PITCH_SPEED_DEFAULT = 75.0f; // degrees/sec +const float PITCH_SPEED_DEFAULT = 75.0f; // degrees/sec -const float MAX_BOOST_SPEED = 0.5f * DEFAULT_AVATAR_MAX_WALKING_SPEED; // action motor gets additive boost below this speed +const float MAX_BOOST_SPEED = 0.5f * DEFAULT_AVATAR_MAX_WALKING_SPEED; // action motor gets additive boost below this speed const float MIN_AVATAR_SPEED = 0.05f; -const float MIN_AVATAR_SPEED_SQUARED = MIN_AVATAR_SPEED * MIN_AVATAR_SPEED; // speed is set to zero below this +const float MIN_AVATAR_SPEED_SQUARED = MIN_AVATAR_SPEED * MIN_AVATAR_SPEED; // speed is set to zero below this float MIN_SCRIPTED_MOTOR_TIMESCALE = 0.005f; float DEFAULT_SCRIPTED_MOTOR_TIMESCALE = 1.0e6f; @@ -82,7 +82,8 @@ const int SCRIPTED_MOTOR_AVATAR_FRAME = 1; const int SCRIPTED_MOTOR_WORLD_FRAME = 2; const int SCRIPTED_MOTOR_SIMPLE_MODE = 0; const int SCRIPTED_MOTOR_DYNAMIC_MODE = 1; -const QString& DEFAULT_AVATAR_COLLISION_SOUND_URL = "https://hifi-public.s3.amazonaws.com/sounds/Collisions-otherorganic/Body_Hits_Impact.wav"; +const QString& DEFAULT_AVATAR_COLLISION_SOUND_URL = + "https://hifi-public.s3.amazonaws.com/sounds/Collisions-otherorganic/Body_Hits_Impact.wav"; const float MyAvatar::ZOOM_MIN = 0.5f; const float MyAvatar::ZOOM_MAX = 25.0f; @@ -90,33 +91,15 @@ const float MyAvatar::ZOOM_DEFAULT = 1.5f; const float MIN_SCALE_CHANGED_DELTA = 0.001f; MyAvatar::MyAvatar(QThread* thread) : - Avatar(thread), - _yawSpeed(YAW_SPEED_DEFAULT), - _pitchSpeed(PITCH_SPEED_DEFAULT), - _scriptedMotorTimescale(DEFAULT_SCRIPTED_MOTOR_TIMESCALE), - _scriptedMotorFrame(SCRIPTED_MOTOR_CAMERA_FRAME), - _scriptedMotorMode(SCRIPTED_MOTOR_SIMPLE_MODE), - _motionBehaviors(AVATAR_MOTION_DEFAULTS), - _characterController(this), - _eyeContactTarget(LEFT_EYE), - _realWorldFieldOfView("realWorldFieldOfView", - DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), + Avatar(thread), _yawSpeed(YAW_SPEED_DEFAULT), _pitchSpeed(PITCH_SPEED_DEFAULT), + _scriptedMotorTimescale(DEFAULT_SCRIPTED_MOTOR_TIMESCALE), _scriptedMotorFrame(SCRIPTED_MOTOR_CAMERA_FRAME), + _scriptedMotorMode(SCRIPTED_MOTOR_SIMPLE_MODE), _motionBehaviors(AVATAR_MOTION_DEFAULTS), _characterController(this), + _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false), - _smoothOrientationTimer(std::numeric_limits::max()), - _smoothOrientationInitial(), - _smoothOrientationTarget(), - _hmdSensorMatrix(), - _hmdSensorOrientation(), - _hmdSensorPosition(), - _bodySensorMatrix(), - _goToPending(false), - _goToPosition(), - _goToOrientation(), - _prevShouldDrawHead(true), - _audioListenerMode(FROM_HEAD), - _hmdAtRestDetector(glm::vec3(0), glm::quat()) -{ - + _smoothOrientationTimer(std::numeric_limits::max()), _smoothOrientationInitial(), _smoothOrientationTarget(), + _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), _bodySensorMatrix(), _goToPending(false), + _goToPosition(), _goToOrientation(), _prevShouldDrawHead(true), _audioListenerMode(FROM_HEAD), + _hmdAtRestDetector(glm::vec3(0), glm::quat()) { // give the pointer to our head to inherited _headData variable from AvatarData _headData = new MyHead(this); @@ -145,11 +128,11 @@ MyAvatar::MyAvatar(QThread* thread) : clearDriveKeys(); // Necessary to select the correct slot - using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); + using SlotType = void (MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); // connect to AddressManager signal for location jumps - connect(DependencyManager::get().data(), &AddressManager::locationChangeRequired, - this, static_cast(&MyAvatar::goToLocation)); + connect(DependencyManager::get().data(), &AddressManager::locationChangeRequired, this, + static_cast(&MyAvatar::goToLocation)); // handle scale constraints imposed on us by the domain-server auto& domainHandler = DependencyManager::get()->getDomainHandler(); @@ -211,7 +194,6 @@ MyAvatar::MyAvatar(QThread* thread) : if (recordingInterface->getPlayerUseSkeletonModel() && dummyAvatar.getSkeletonModelURL().isValid() && (dummyAvatar.getSkeletonModelURL() != getSkeletonModelURL())) { - setSkeletonModelURL(dummyAvatar.getSkeletonModelURL()); } @@ -258,7 +240,8 @@ void MyAvatar::setDominantHand(const QString& hand) { } void MyAvatar::registerMetaTypes(ScriptEnginePointer engine) { - QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); + QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, + QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); engine->globalObject().setProperty("MyAvatar", value); QScriptValue driveKeys = engine->newObject(); @@ -334,7 +317,7 @@ void MyAvatar::centerBody() { } // derive the desired body orientation from the current hmd orientation, before the sensor reset. - auto newBodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. + auto newBodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. // transform this body into world space auto worldBodyMatrix = _sensorToWorldMatrix * newBodySensorMatrix; @@ -371,7 +354,6 @@ void MyAvatar::clearIKJointLimitHistory() { } void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { - assert(QThread::currentThread() == thread()); // Reset dynamic state. @@ -380,14 +362,14 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { if (andReload) { _skeletonModel->reset(); } - if (andHead) { // which drives camera in desktop + if (andHead) { // which drives camera in desktop getHead()->reset(); } setThrust(glm::vec3(0.0f)); if (andRecenter) { // derive the desired body orientation from the *old* hmd orientation, before the sensor reset. - auto newBodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. + auto newBodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. // transform this body into world space auto worldBodyMatrix = _sensorToWorldMatrix * newBodySensorMatrix; @@ -412,9 +394,8 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { } void MyAvatar::update(float deltaTime) { - // update moving average of HMD facing in xz plane. - const float HMD_FACING_TIMESCALE = 4.0f; // very slow average + const float HMD_FACING_TIMESCALE = 4.0f; // very slow average float tau = deltaTime / HMD_FACING_TIMESCALE; _headControllerFacingMovingAverage = lerp(_headControllerFacingMovingAverage, _headControllerFacing, tau); @@ -426,8 +407,11 @@ void MyAvatar::update(float deltaTime) { #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); - glm::vec3 worldFacingAverage = transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); - glm::vec3 worldFacing = transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacing.x, 0.0f, _headControllerFacing.y)); + glm::vec3 worldFacingAverage = + transformVectorFast(getSensorToWorldMatrix(), + glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); + glm::vec3 worldFacing = + transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacing.x, 0.0f, _headControllerFacing.y)); DebugDraw::getInstance().drawRay(worldHeadPos, worldHeadPos + worldFacing, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)); DebugDraw::getInstance().drawRay(worldHeadPos, worldHeadPos + worldFacingAverage, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f)); #endif @@ -445,12 +429,12 @@ void MyAvatar::update(float deltaTime) { emit positionGoneTo(); // Run safety tests as soon as we can after goToLocation, or clear if we're not colliding. _physicsSafetyPending = getCollisionsEnabled(); - _characterController.recomputeFlying(); // In case we've gone to into the sky. + _characterController.recomputeFlying(); // In case we've gone to into the sky. } if (_physicsSafetyPending && qApp->isPhysicsEnabled() && _characterController.isEnabledAndReady()) { // When needed and ready, arrange to check and fix. _physicsSafetyPending = false; - safeLanding(_goToPosition); // no-op if already safe + safeLanding(_goToPosition); // no-op if already safe } Head* head = getHead(); @@ -464,12 +448,13 @@ void MyAvatar::update(float deltaTime) { setAudioLoudness(audio->getLastInputLoudness()); setAudioAverageLoudness(audio->getAudioAverageInputLoudness()); - glm::vec3 halfBoundingBoxDimensions(_characterController.getCapsuleRadius(), _characterController.getCapsuleHalfHeight(), _characterController.getCapsuleRadius()); + glm::vec3 halfBoundingBoxDimensions(_characterController.getCapsuleRadius(), _characterController.getCapsuleHalfHeight(), + _characterController.getCapsuleRadius()); // This might not be right! Isn't the capsule local offset in avatar space? -HRS 5/26/17 halfBoundingBoxDimensions += _characterController.getCapsuleLocalOffset(); QMetaObject::invokeMethod(audio.data(), "setAvatarBoundingBoxParameters", - Q_ARG(glm::vec3, (getWorldPosition() - halfBoundingBoxDimensions)), - Q_ARG(glm::vec3, (halfBoundingBoxDimensions*2.0f))); + Q_ARG(glm::vec3, (getWorldPosition() - halfBoundingBoxDimensions)), + Q_ARG(glm::vec3, (halfBoundingBoxDimensions * 2.0f))); if (getIdentityDataChanged()) { sendIdentityPacket(); @@ -481,23 +466,20 @@ void MyAvatar::update(float deltaTime) { currentEnergy -= getAccelerationEnergy(); currentEnergy -= getAudioEnergy(); - if(didTeleport()) { + if (didTeleport()) { currentEnergy = 0.0f; } - currentEnergy = max(0.0f, min(currentEnergy,1.0f)); + currentEnergy = max(0.0f, min(currentEnergy, 1.0f)); emit energyChanged(currentEnergy); updateEyeContactTarget(deltaTime); } void MyAvatar::updateEyeContactTarget(float deltaTime) { - _eyeContactTargetTimer -= deltaTime; if (_eyeContactTargetTimer < 0.0f) { - const float CHANCE_OF_CHANGING_TARGET = 0.01f; if (randFloat() < CHANCE_OF_CHANGING_TARGET) { - float const FIFTY_FIFTY_CHANCE = 0.5f; float const EYE_TO_MOUTH_CHANCE = 0.25f; switch (_eyeContactTarget) { @@ -550,9 +532,7 @@ void MyAvatar::simulate(float deltaTime) { bool isChildOfHead = headBoneSet.find(object->getParentJointIndex()) != headBoneSet.end(); if (isChildOfHead) { updateChildCauterization(object); - object->forEachDescendant([&](SpatiallyNestablePointer descendant) { - updateChildCauterization(descendant); - }); + object->forEachDescendant([&](SpatiallyNestablePointer descendant) { updateChildCauterization(descendant); }); } }); _cauterizationNeedsUpdate = false; @@ -593,7 +573,7 @@ void MyAvatar::simulate(float deltaTime) { if (!_skeletonModel->hasSkeleton()) { // All the simulation that can be done has been done - getHead()->setPosition(getWorldPosition()); // so audio-position isn't 0,0,0 + getHead()->setPosition(getWorldPosition()); // so audio-position isn't 0,0,0 return; } @@ -672,8 +652,9 @@ void MyAvatar::simulate(float deltaTime) { EntityItemProperties descendantProperties; descendantProperties.setQueryAACube(descendant->getQueryAACube()); descendantProperties.setLastEdited(now); - packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entityDescendant->getID(), descendantProperties); - entityDescendant->setLastBroadcast(now); // for debug/physics status icons + packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, + entityDescendant->getID(), descendantProperties); + entityDescendant->setLastBroadcast(now); // for debug/physics status icons } }); } @@ -703,8 +684,7 @@ void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) { _hmdSensorMatrix = hmdSensorMatrix; auto newHmdSensorPosition = extractTranslation(hmdSensorMatrix); - if (newHmdSensorPosition != getHMDSensorPosition() && - glm::length(newHmdSensorPosition) > MAX_HMD_ORIGIN_DISTANCE) { + if (newHmdSensorPosition != getHMDSensorPosition() && glm::length(newHmdSensorPosition) > MAX_HMD_ORIGIN_DISTANCE) { qWarning() << "Invalid HMD sensor position " << newHmdSensorPosition; // Ignore unreasonable HMD sensor data return; @@ -736,11 +716,11 @@ void MyAvatar::updateJointFromController(controller::Action poseKey, ThreadSafeV // update sensor to world matrix from current body position and hmd sensor. // This is so the correct camera can be used for rendering. void MyAvatar::updateSensorToWorldMatrix() { - // update the sensor mat so that the body position will end up in the desired // position when driven from the head. float sensorToWorldScale = getEyeHeight() / getUserEyeHeight(); - glm::mat4 desiredMat = createMatFromScaleQuatAndPos(glm::vec3(sensorToWorldScale), getWorldOrientation(), getWorldPosition()); + glm::mat4 desiredMat = + createMatFromScaleQuatAndPos(glm::vec3(sensorToWorldScale), getWorldOrientation(), getWorldPosition()); _sensorToWorldMatrix = desiredMat * glm::inverse(_bodySensorMatrix); bool hasSensorToWorldScaleChanged = false; @@ -758,11 +738,10 @@ void MyAvatar::updateSensorToWorldMatrix() { _sensorToWorldMatrixCache.set(_sensorToWorldMatrix); updateJointFromController(controller::Action::LEFT_HAND, _controllerLeftHandMatrixCache); updateJointFromController(controller::Action::RIGHT_HAND, _controllerRightHandMatrixCache); - + if (hasSensorToWorldScaleChanged) { emit sensorToWorldScaleChanged(sensorToWorldScale); } - } // Update avatar head rotation with sensor data @@ -787,8 +766,7 @@ void MyAvatar::updateFromTrackers(float deltaTime) { const float TRACKER_YAW_TURN_SENSITIVITY = 0.5f; const float TRACKER_MIN_YAW_TURN = 15.0f; const float TRACKER_MAX_YAW_TURN = 50.0f; - if ( (fabs(estimatedRotation.y) > TRACKER_MIN_YAW_TURN) && - (fabs(estimatedRotation.y) < TRACKER_MAX_YAW_TURN) ) { + if ((fabs(estimatedRotation.y) > TRACKER_MIN_YAW_TURN) && (fabs(estimatedRotation.y) < TRACKER_MAX_YAW_TURN)) { if (estimatedRotation.y > 0.0f) { _bodyYawDelta += (estimatedRotation.y - TRACKER_MIN_YAW_TURN) * TRACKER_YAW_TURN_SENSITIVITY; } else { @@ -803,7 +781,6 @@ void MyAvatar::updateFromTrackers(float deltaTime) { // their head only 30 degrees or so, this may correspond to a 90 degree field of view. // Note that roll is magnified by a constant because it is not related to field of view. - Head* head = getHead(); if (inHmd || playing) { head->setDeltaPitch(estimatedRotation.x); @@ -866,8 +843,8 @@ controller::Pose MyAvatar::getRightHandTipPose() const { } glm::vec3 MyAvatar::worldToJointPoint(const glm::vec3& position, const int jointIndex) const { - glm::vec3 jointPos = getWorldPosition();//default value if no or invalid joint specified - glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified + glm::vec3 jointPos = getWorldPosition(); //default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified if (jointIndex != -1) { if (_skeletonModel->getJointPositionInWorldFrame(jointIndex, jointPos)) { _skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot); @@ -882,7 +859,7 @@ glm::vec3 MyAvatar::worldToJointPoint(const glm::vec3& position, const int joint } glm::vec3 MyAvatar::worldToJointDirection(const glm::vec3& worldDir, const int jointIndex) const { - glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { qWarning() << "Invalid joint index specified: " << jointIndex; } @@ -892,7 +869,7 @@ glm::vec3 MyAvatar::worldToJointDirection(const glm::vec3& worldDir, const int j } glm::quat MyAvatar::worldToJointRotation(const glm::quat& worldRot, const int jointIndex) const { - glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { qWarning() << "Invalid joint index specified: " << jointIndex; } @@ -901,8 +878,8 @@ glm::quat MyAvatar::worldToJointRotation(const glm::quat& worldRot, const int jo } glm::vec3 MyAvatar::jointToWorldPoint(const glm::vec3& jointSpacePos, const int jointIndex) const { - glm::vec3 jointPos = getWorldPosition();//default value if no or invalid joint specified - glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified + glm::vec3 jointPos = getWorldPosition(); //default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified if (jointIndex != -1) { if (_skeletonModel->getJointPositionInWorldFrame(jointIndex, jointPos)) { @@ -919,7 +896,7 @@ glm::vec3 MyAvatar::jointToWorldPoint(const glm::vec3& jointSpacePos, const int } glm::vec3 MyAvatar::jointToWorldDirection(const glm::vec3& jointSpaceDir, const int jointIndex) const { - glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { qWarning() << "Invalid joint index specified: " << jointIndex; } @@ -928,7 +905,7 @@ glm::vec3 MyAvatar::jointToWorldDirection(const glm::vec3& jointSpaceDir, const } glm::quat MyAvatar::jointToWorldRotation(const glm::quat& jointSpaceRot, const int jointIndex) const { - glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { qWarning() << "Invalid joint index specified: " << jointIndex; } @@ -940,15 +917,15 @@ glm::quat MyAvatar::jointToWorldRotation(const glm::quat& jointSpaceRot, const i void MyAvatar::render(RenderArgs* renderArgs) { // don't render if we've been asked to disable local rendering if (!_shouldRender) { - return; // exit early + return; // exit early } Avatar::render(renderArgs); } void MyAvatar::overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "overrideAnimation", Q_ARG(const QString&, url), Q_ARG(float, fps), - Q_ARG(bool, loop), Q_ARG(float, firstFrame), Q_ARG(float, lastFrame)); + QMetaObject::invokeMethod(this, "overrideAnimation", Q_ARG(const QString&, url), Q_ARG(float, fps), Q_ARG(bool, loop), + Q_ARG(float, firstFrame), Q_ARG(float, lastFrame)); return; } _skeletonModel->getRig().overrideAnimation(url, fps, loop, firstFrame, lastFrame); @@ -971,8 +948,12 @@ QStringList MyAvatar::getAnimationRoles() { return _skeletonModel->getRig().getAnimationRoles(); } -void MyAvatar::overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, - float firstFrame, float lastFrame) { +void MyAvatar::overrideRoleAnimation(const QString& role, + const QString& url, + float fps, + bool loop, + float firstFrame, + float lastFrame) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "overrideRoleAnimation", Q_ARG(const QString&, role), Q_ARG(const QString&, url), Q_ARG(float, fps), Q_ARG(bool, loop), Q_ARG(float, firstFrame), Q_ARG(float, lastFrame)); @@ -992,11 +973,10 @@ void MyAvatar::restoreRoleAnimation(const QString& role) { void MyAvatar::saveAvatarUrl() { Settings settings; settings.beginGroup("Avatar"); - if (qApp->getSaveAvatarOverrideUrl() || !qApp->getAvatarOverrideUrl().isValid() ) { - settings.setValue("fullAvatarURL", - _fullAvatarURLFromPreferences == AvatarData::defaultFullAvatarModelUrl() ? - "" : - _fullAvatarURLFromPreferences.toString()); + if (qApp->getSaveAvatarOverrideUrl() || !qApp->getAvatarOverrideUrl().isValid()) { + settings.setValue("fullAvatarURL", _fullAvatarURLFromPreferences == AvatarData::defaultFullAvatarModelUrl() + ? "" + : _fullAvatarURLFromPreferences.toString()); } settings.endGroup(); } @@ -1016,11 +996,10 @@ void MyAvatar::saveData() { // only save the fullAvatarURL if it has not been overwritten on command line // (so the overrideURL is not valid), or it was overridden _and_ we specified // --replaceAvatarURL (so _saveAvatarOverrideUrl is true) - if (qApp->getSaveAvatarOverrideUrl() || !qApp->getAvatarOverrideUrl().isValid() ) { - settings.setValue("fullAvatarURL", - _fullAvatarURLFromPreferences == AvatarData::defaultFullAvatarModelUrl() ? - "" : - _fullAvatarURLFromPreferences.toString()); + if (qApp->getSaveAvatarOverrideUrl() || !qApp->getAvatarOverrideUrl().isValid()) { + settings.setValue("fullAvatarURL", _fullAvatarURLFromPreferences == AvatarData::defaultFullAvatarModelUrl() + ? "" + : _fullAvatarURLFromPreferences.toString()); } settings.setValue("fullAvatarModelName", _fullAvatarModelName); @@ -1229,7 +1208,7 @@ void MyAvatar::loadData() { settings.endGroup(); setEnableMeshVisible(Menu::getInstance()->isOptionChecked(MenuOption::MeshVisible)); - _follow.setToggleHipsFollowing (Menu::getInstance()->isOptionChecked(MenuOption::ToggleHipsFollowing)); + _follow.setToggleHipsFollowing(Menu::getInstance()->isOptionChecked(MenuOption::ToggleHipsFollowing)); setEnableDebugDrawBaseOfSupport(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawBaseOfSupport)); setEnableDebugDrawDefaultPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawDefaultPose)); setEnableDebugDrawAnimPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawAnimPose)); @@ -1297,7 +1276,7 @@ AttachmentData MyAvatar::loadAttachmentData(const QUrl& modelURL, const QString& int MyAvatar::parseDataFromBuffer(const QByteArray& buffer) { qCDebug(interfaceapp) << "Error: ignoring update packet for MyAvatar" - << " packetLength = " << buffer.size(); + << " packetLength = " << buffer.size(); // this packet is just bad, so we pretend that we unpacked it ALL return buffer.size(); } @@ -1332,18 +1311,17 @@ void MyAvatar::updateLookAtTargetAvatar() { bool isCurrentTarget = avatar->getIsLookAtTarget(); float distanceTo = glm::length(avatar->getHead()->getEyePosition() - cameraPosition); avatar->setIsLookAtTarget(false); - if (!avatar->isMyAvatar() && avatar->isInitialized() && - (distanceTo < GREATEST_LOOKING_AT_DISTANCE * getModelScale())) { + if (!avatar->isMyAvatar() && avatar->isInitialized() && (distanceTo < GREATEST_LOOKING_AT_DISTANCE * getModelScale())) { float radius = glm::length(avatar->getHead()->getEyePosition() - avatar->getHead()->getRightEyePosition()); - float angleTo = coneSphereAngle(getHead()->getEyePosition(), lookForward, avatar->getHead()->getEyePosition(), radius); + float angleTo = + coneSphereAngle(getHead()->getEyePosition(), lookForward, avatar->getHead()->getEyePosition(), radius); if (angleTo < (smallestAngleTo * (isCurrentTarget ? KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR : 1.0f))) { _lookAtTargetAvatar = avatarPointer; _targetAvatarPosition = avatarPointer->getWorldPosition(); } if (_lookAtSnappingEnabled && avatar->getLookAtSnappingEnabled() && isLookingAtMe(avatar)) { - // Alter their gaze to look directly at my camera; this looks more natural than looking at my avatar's face. - glm::vec3 lookAtPosition = avatar->getHead()->getLookAtPosition(); // A position, in world space, on my avatar. + glm::vec3 lookAtPosition = avatar->getHead()->getLookAtPosition(); // A position, in world space, on my avatar. // The camera isn't at the point midway between the avatar eyes. (Even without an HMD, the head can be offset a bit.) // Let's get everything to world space: @@ -1354,12 +1332,12 @@ void MyAvatar::updateLookAtTargetAvatar() { // (We will be adding that offset to the camera position, after making some other adjustments.) glm::vec3 gazeOffset = lookAtPosition - getHead()->getEyePosition(); - ViewFrustum viewFrustum; - qApp->copyViewFrustum(viewFrustum); + ViewFrustum viewFrustum; + qApp->copyViewFrustum(viewFrustum); - glm::vec3 viewPosition = viewFrustum.getPosition(); + glm::vec3 viewPosition = viewFrustum.getPosition(); #if DEBUG_ALWAYS_LOOKAT_EYES_NOT_CAMERA - viewPosition = (avatarLeftEye + avatarRightEye) / 2.0f; + viewPosition = (avatarLeftEye + avatarRightEye) / 2.0f; #endif // scale gazeOffset by IPD, if wearing an HMD. if (qApp->isHMDMode()) { @@ -1426,7 +1404,7 @@ void MyAvatar::setJointRotations(const QVector& jointRotations) { void MyAvatar::setJointData(int index, const glm::quat& rotation, const glm::vec3& translation) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setJointData", Q_ARG(int, index), Q_ARG(const glm::quat&, rotation), - Q_ARG(const glm::vec3&, translation)); + Q_ARG(const glm::vec3&, translation)); return; } // HACK: ATM only JS scripts call setJointData() on MyAvatar so we hardcode the priority @@ -1462,7 +1440,7 @@ void MyAvatar::clearJointData(int index) { void MyAvatar::setJointData(const QString& name, const glm::quat& rotation, const glm::vec3& translation) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setJointData", Q_ARG(QString, name), Q_ARG(const glm::quat&, rotation), - Q_ARG(const glm::vec3&, translation)); + Q_ARG(const glm::vec3&, translation)); return; } writeLockWithNamedJointIndex(name, [&](int index) { @@ -1498,9 +1476,7 @@ void MyAvatar::clearJointData(const QString& name) { QMetaObject::invokeMethod(this, "clearJointData", Q_ARG(QString, name)); return; } - writeLockWithNamedJointIndex(name, [&](int index) { - _skeletonModel->getRig().clearJointAnimationPriority(index); - }); + writeLockWithNamedJointIndex(name, [&](int index) { _skeletonModel->getRig().clearJointAnimationPriority(index); }); } void MyAvatar::clearJointsData() { @@ -1523,24 +1499,25 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _cauterizationNeedsUpdate = true; std::shared_ptr skeletonConnection = std::make_shared(); - *skeletonConnection = QObject::connect(_skeletonModel.get(), &SkeletonModel::skeletonLoaded, [this, skeletonModelChangeCount, skeletonConnection]() { - if (skeletonModelChangeCount == _skeletonModelChangeCount) { + *skeletonConnection = QObject::connect(_skeletonModel.get(), &SkeletonModel::skeletonLoaded, + [this, skeletonModelChangeCount, skeletonConnection]() { + if (skeletonModelChangeCount == _skeletonModelChangeCount) { + if (_fullAvatarModelName.isEmpty()) { + // Store the FST file name into preferences + const auto& mapping = _skeletonModel->getGeometry()->getMapping(); + if (mapping.value("name").isValid()) { + _fullAvatarModelName = mapping.value("name").toString(); + } + } - if (_fullAvatarModelName.isEmpty()) { - // Store the FST file name into preferences - const auto& mapping = _skeletonModel->getGeometry()->getMapping(); - if (mapping.value("name").isValid()) { - _fullAvatarModelName = mapping.value("name").toString(); - } - } - - initHeadBones(); - _skeletonModel->setCauterizeBoneSet(_headBoneSet); - _fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl(); - initAnimGraph(); - } - QObject::disconnect(*skeletonConnection); - }); + initHeadBones(); + _skeletonModel->setCauterizeBoneSet(_headBoneSet); + _fstAnimGraphOverrideUrl = + _skeletonModel->getGeometry()->getAnimGraphOverrideUrl(); + initAnimGraph(); + } + QObject::disconnect(*skeletonConnection); + }); saveAvatarUrl(); emit skeletonChanged(); emit skeletonModelURLChanged(); @@ -1577,7 +1554,6 @@ QVariantList MyAvatar::getAvatarEntitiesVariant() { return avatarEntitiesData; } - void MyAvatar::resetFullAvatarURL() { auto lastAvatarURL = getFullAvatarURLFromPreferences(); auto lastAvatarName = getFullAvatarModelName(); @@ -1586,11 +1562,8 @@ void MyAvatar::resetFullAvatarURL() { } void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName) { - if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "useFullAvatarURL", - Q_ARG(const QUrl&, fullAvatarURL), - Q_ARG(const QString&, modelName)); + BLOCKING_INVOKE_METHOD(this, "useFullAvatarURL", Q_ARG(const QUrl&, fullAvatarURL), Q_ARG(const QString&, modelName)); return; } @@ -1610,8 +1583,7 @@ void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelN void MyAvatar::setAttachmentData(const QVector& attachmentData) { if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "setAttachmentData", - Q_ARG(const QVector, attachmentData)); + BLOCKING_INVOKE_METHOD(this, "setAttachmentData", Q_ARG(const QVector, attachmentData)); return; } Avatar::setAttachmentData(attachmentData); @@ -1656,7 +1628,7 @@ controller::Pose MyAvatar::getControllerPoseInSensorFrame(controller::Action act if (iter != _controllerPoseMap.end()) { return iter->second; } else { - return controller::Pose(); // invalid pose + return controller::Pose(); // invalid pose } } @@ -1665,7 +1637,7 @@ controller::Pose MyAvatar::getControllerPoseInWorldFrame(controller::Action acti if (pose.valid) { return pose.transform(getSensorToWorldMatrix()); } else { - return controller::Pose(); // invalid pose + return controller::Pose(); // invalid pose } } @@ -1675,7 +1647,7 @@ controller::Pose MyAvatar::getControllerPoseInAvatarFrame(controller::Action act glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getWorldOrientation(), getWorldPosition())); return pose.transform(invAvatarMatrix); } else { - return controller::Pose(); // invalid pose + return controller::Pose(); // invalid pose } } @@ -1691,7 +1663,7 @@ void MyAvatar::updateMotors() { float verticalMotorTimescale; if (_characterController.getState() == CharacterController::State::Hover || - _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { horizontalMotorTimescale = FLYING_MOTOR_TIMESCALE; verticalMotorTimescale = FLYING_MOTOR_TIMESCALE; } else { @@ -1701,7 +1673,7 @@ void MyAvatar::updateMotors() { if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { if (_characterController.getState() == CharacterController::State::Hover || - _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { motorRotation = getMyHead()->getHeadOrientation(); } else { // non-hovering = walking: follow camera twist about vertical but not lift @@ -1709,14 +1681,15 @@ void MyAvatar::updateMotors() { // however, we need to perform the decomposition in the avatar-frame // using the local UP axis and then transform back into world-frame glm::quat orientation = getWorldOrientation(); - glm::quat headOrientation = glm::inverse(orientation) * getMyHead()->getHeadOrientation(); // avatar-frame + glm::quat headOrientation = glm::inverse(orientation) * getMyHead()->getHeadOrientation(); // avatar-frame glm::quat liftRotation; swingTwistDecomposition(headOrientation, Vectors::UNIT_Y, liftRotation, motorRotation); motorRotation = orientation * motorRotation; } if (_isPushing || _isBraking || !_isBeingPushed) { - _characterController.addMotor(_actionMotorVelocity, motorRotation, horizontalMotorTimescale, verticalMotorTimescale); + _characterController.addMotor(_actionMotorVelocity, motorRotation, horizontalMotorTimescale, + verticalMotorTimescale); } else { // _isBeingPushed must be true --> disable action motor by giving it a long timescale, // otherwise it's attempt to "stand in in place" could defeat scripted motor/thrusts @@ -1736,7 +1709,8 @@ void MyAvatar::updateMotors() { _characterController.addMotor(_scriptedMotorVelocity, motorRotation, _scriptedMotorTimescale); } else { // dynamic mode - _characterController.addMotor(_scriptedMotorVelocity, motorRotation, horizontalMotorTimescale, verticalMotorTimescale); + _characterController.addMotor(_scriptedMotorVelocity, motorRotation, horizontalMotorTimescale, + verticalMotorTimescale); } } @@ -1841,8 +1815,7 @@ void MyAvatar::setScriptedMotorVelocity(const glm::vec3& velocity) { void MyAvatar::setScriptedMotorTimescale(float timescale) { // we clamp the timescale on the large side (instead of just the low side) to prevent // obnoxiously large values from introducing NaN into avatar's velocity - _scriptedMotorTimescale = glm::clamp(timescale, MIN_SCRIPTED_MOTOR_TIMESCALE, - DEFAULT_SCRIPTED_MOTOR_TIMESCALE); + _scriptedMotorTimescale = glm::clamp(timescale, MIN_SCRIPTED_MOTOR_TIMESCALE, DEFAULT_SCRIPTED_MOTOR_TIMESCALE); } void MyAvatar::setScriptedMotorFrame(QString frame) { @@ -1883,10 +1856,14 @@ SharedSoundPointer MyAvatar::getCollisionSound() { return _collisionSound; } -void MyAvatar::attach(const QString& modelURL, const QString& jointName, - const glm::vec3& translation, const glm::quat& rotation, - float scale, bool isSoft, - bool allowDuplicates, bool useSaved) { +void MyAvatar::attach(const QString& modelURL, + const QString& jointName, + const glm::vec3& translation, + const glm::quat& rotation, + float scale, + bool isSoft, + bool allowDuplicates, + bool useSaved) { if (QThread::currentThread() != thread()) { Avatar::attach(modelURL, jointName, translation, rotation, scale, isSoft, allowDuplicates, useSaved); return; @@ -1894,10 +1871,8 @@ void MyAvatar::attach(const QString& modelURL, const QString& jointName, if (useSaved) { AttachmentData attachment = loadAttachmentData(modelURL, jointName); if (attachment.isValid()) { - Avatar::attach(modelURL, attachment.jointName, - attachment.translation, attachment.rotation, - attachment.scale, attachment.isSoft, - allowDuplicates, useSaved); + Avatar::attach(modelURL, attachment.jointName, attachment.translation, attachment.rotation, attachment.scale, + attachment.isSoft, allowDuplicates, useSaved); return; } } @@ -1950,7 +1925,6 @@ QUrl MyAvatar::getAnimGraphUrl() const { } void MyAvatar::setAnimGraphUrl(const QUrl& url) { - if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setAnimGraphUrl", Q_ARG(QUrl, url)); return; @@ -1960,7 +1934,7 @@ void MyAvatar::setAnimGraphUrl(const QUrl& url) { return; } destroyAnimGraph(); - _skeletonModel->reset(); // Why is this necessary? Without this, we crash in the next render. + _skeletonModel->reset(); // Why is this necessary? Without this, we crash in the next render. _currentAnimGraphUrl.set(url); _skeletonModel->getRig().initAnimGraph(url); @@ -1987,18 +1961,16 @@ void MyAvatar::destroyAnimGraph() { } void MyAvatar::animGraphLoaded() { - _bodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. - updateSensorToWorldMatrix(); // Uses updated position/orientation and _bodySensorMatrix changes + _bodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. + updateSensorToWorldMatrix(); // Uses updated position/orientation and _bodySensorMatrix changes _isAnimatingScale = true; _cauterizationNeedsUpdate = true; disconnect(&(_skeletonModel->getRig()), SIGNAL(onLoadComplete()), this, SLOT(animGraphLoaded())); } void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { - Avatar::postUpdate(deltaTime, scene); if (_enableDebugDrawDefaultPose || _enableDebugDrawAnimPose) { - auto animSkeleton = _skeletonModel->getRig().getAnimSkeleton(); // the rig is in the skeletonModel frame @@ -2006,7 +1978,8 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { if (_enableDebugDrawDefaultPose && animSkeleton) { glm::vec4 gray(0.2f, 0.2f, 0.2f, 0.2f); - AnimDebugDraw::getInstance().addAbsolutePoses("myAvatarDefaultPoses", animSkeleton, _skeletonModel->getRig().getAbsoluteDefaultPoses(), xform, gray); + AnimDebugDraw::getInstance().addAbsolutePoses("myAvatarDefaultPoses", animSkeleton, + _skeletonModel->getRig().getAbsoluteDefaultPoses(), xform, gray); } if (_enableDebugDrawAnimPose && animSkeleton) { @@ -2027,13 +2000,15 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { auto rightHandPose = getControllerPoseInWorldFrame(controller::Action::RIGHT_HAND); if (leftHandPose.isValid()) { - DebugDraw::getInstance().addMarker("leftHandController", leftHandPose.getRotation(), leftHandPose.getTranslation(), glm::vec4(1)); + DebugDraw::getInstance().addMarker("leftHandController", leftHandPose.getRotation(), leftHandPose.getTranslation(), + glm::vec4(1)); } else { DebugDraw::getInstance().removeMarker("leftHandController"); } if (rightHandPose.isValid()) { - DebugDraw::getInstance().addMarker("rightHandController", rightHandPose.getRotation(), rightHandPose.getTranslation(), glm::vec4(1)); + DebugDraw::getInstance().addMarker("rightHandController", rightHandPose.getRotation(), + rightHandPose.getTranslation(), glm::vec4(1)); } else { DebugDraw::getInstance().removeMarker("rightHandController"); } @@ -2050,14 +2025,9 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { AnimPose rigToWorldPose(glm::vec3(1.0f), getWorldOrientation() * Quaternions::Y_180, getWorldPosition()); const int NUM_DEBUG_COLORS = 8; const glm::vec4 DEBUG_COLORS[NUM_DEBUG_COLORS] = { - glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), - glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), - glm::vec4(0.0f, 1.0f, 0.0f, 1.0f), - glm::vec4(0.25f, 0.25f, 1.0f, 1.0f), - glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), - glm::vec4(0.25f, 1.0f, 1.0f, 1.0f), - glm::vec4(1.0f, 0.25f, 1.0f, 1.0f), - glm::vec4(1.0f, 0.65f, 0.0f, 1.0f) // Orange you glad I added this color? + glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), glm::vec4(0.0f, 1.0f, 0.0f, 1.0f), + glm::vec4(0.25f, 0.25f, 1.0f, 1.0f), glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), glm::vec4(0.25f, 1.0f, 1.0f, 1.0f), + glm::vec4(1.0f, 0.25f, 1.0f, 1.0f), glm::vec4(1.0f, 0.65f, 0.0f, 1.0f) // Orange you glad I added this color? }; if (_skeletonModel && _skeletonModel->isLoaded()) { @@ -2079,7 +2049,6 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { } void MyAvatar::preDisplaySide(const RenderArgs* renderArgs) { - // toggle using the cauterizedBones depending on where the camera is and the rendering pass type. const bool shouldDrawHead = shouldRenderHead(renderArgs); if (shouldDrawHead != _prevShouldDrawHead) { @@ -2144,7 +2113,6 @@ void MyAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) } void MyAvatar::updateOrientation(float deltaTime) { - // Smoothly rotate body with arrow keys float targetSpeed = getDriveKey(YAW) * _yawSpeed; if (targetSpeed != 0.0f) { @@ -2171,7 +2139,6 @@ void MyAvatar::updateOrientation(float deltaTime) { float totalBodyYaw = _bodyYawDelta * deltaTime; - // Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll // get an instantaneous 15 degree turn. If you keep holding the key down you'll get another // snap turn every half second. @@ -2182,8 +2149,8 @@ void MyAvatar::updateOrientation(float deltaTime) { } // Use head/HMD roll to turn while flying, but not when standing still. - if (qApp->isHMDMode() && getCharacterController()->getState() == CharacterController::State::Hover && _hmdRollControlEnabled && hasDriveInput()) { - + if (qApp->isHMDMode() && getCharacterController()->getState() == CharacterController::State::Hover && + _hmdRollControlEnabled && hasDriveInput()) { // Turn with head roll. const float MIN_CONTROL_SPEED = 2.0f * getSensorToWorldScale(); // meters / sec const glm::vec3 characterForward = getWorldOrientation() * Vectors::UNIT_NEG_Z; @@ -2191,7 +2158,6 @@ void MyAvatar::updateOrientation(float deltaTime) { // only enable roll-turns if we are moving forward or backward at greater then MIN_CONTROL_SPEED if (fabsf(forwardSpeed) >= MIN_CONTROL_SPEED) { - float direction = forwardSpeed > 0.0f ? 1.0f : -1.0f; float rollAngle = glm::degrees(asinf(glm::dot(IDENTITY_UP, _hmdSensorOrientation * IDENTITY_RIGHT))); float rollSign = rollAngle < 0.0f ? -1.0f : 1.0f; @@ -2245,8 +2211,8 @@ void MyAvatar::updateOrientation(float deltaTime) { void MyAvatar::updateActionMotor(float deltaTime) { bool thrustIsPushing = (glm::length2(_thrust) > EPSILON); - bool scriptedMotorIsPushing = (_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED) - && _scriptedMotorTimescale < MAX_CHARACTER_MOTOR_TIMESCALE; + bool scriptedMotorIsPushing = + (_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED) && _scriptedMotorTimescale < MAX_CHARACTER_MOTOR_TIMESCALE; _isBeingPushed = thrustIsPushing || scriptedMotorIsPushing; if (_isPushing || _isBeingPushed) { // we don't want the motor to brake if a script is pushing the avatar around @@ -2266,7 +2232,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { glm::vec3 direction = forward + right; if (state == CharacterController::State::Hover || - _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP; direction += up; } @@ -2287,7 +2253,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { float motorSpeed = glm::length(_actionMotorVelocity); float finalMaxMotorSpeed = getSensorToWorldScale() * DEFAULT_AVATAR_MAX_FLYING_SPEED * _walkSpeedScalar; - float speedGrowthTimescale = 2.0f; + float speedGrowthTimescale = 2.0f; float speedIncreaseFactor = 1.8f * _walkSpeedScalar; motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale, 0.0f, 1.0f) * speedIncreaseFactor; const float maxBoostSpeed = getSensorToWorldScale() * MAX_BOOST_SPEED; @@ -2304,7 +2270,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { _actionMotorVelocity = motorSpeed * direction; } else { // we're interacting with a floor --> simple horizontal speed and exponential decay - _actionMotorVelocity = getSensorToWorldScale() * (_walkSpeed.get() * _walkSpeedScalar) * direction; + _actionMotorVelocity = getSensorToWorldScale() * (_walkSpeed.get() * _walkSpeedScalar) * direction; } float previousBoomLength = _boomLength; @@ -2326,7 +2292,7 @@ void MyAvatar::updatePosition(float deltaTime) { vec3 velocity = getWorldVelocity(); float sensorToWorldScale = getSensorToWorldScale(); float sensorToWorldScale2 = sensorToWorldScale * sensorToWorldScale; - const float MOVING_SPEED_THRESHOLD_SQUARED = 0.0001f; // 0.01 m/s + const float MOVING_SPEED_THRESHOLD_SQUARED = 0.0001f; // 0.01 m/s if (!_characterController.isEnabledAndReady()) { // _characterController is not in physics simulation but it can still compute its target velocity updateMotors(); @@ -2354,12 +2320,17 @@ void MyAvatar::updatePosition(float deltaTime) { } } -void MyAvatar::updateCollisionSound(const glm::vec3 &penetration, float deltaTime, float frequency) { +void MyAvatar::updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency) { // COLLISION SOUND API in Audio has been removed } -bool findAvatarAvatarPenetration(const glm::vec3 positionA, float radiusA, float heightA, - const glm::vec3 positionB, float radiusB, float heightB, glm::vec3& penetration) { +bool findAvatarAvatarPenetration(const glm::vec3 positionA, + float radiusA, + float heightA, + const glm::vec3 positionB, + float radiusB, + float heightB, + glm::vec3& penetration) { glm::vec3 positionBA = positionB - positionA; float xzDistance = sqrt(positionBA.x * positionBA.x + positionBA.z * positionBA.z); if (xzDistance < (radiusA + radiusB)) { @@ -2531,9 +2502,9 @@ void MyAvatar::goToLocation(const QVariant& propertiesVar) { } void MyAvatar::goToLocation(const glm::vec3& newPosition, - bool hasOrientation, const glm::quat& newOrientation, + bool hasOrientation, + const glm::quat& newOrientation, bool shouldFaceLocation) { - // Most cases of going to a place or user go through this now. Some possible improvements to think about in the future: // - It would be nice if this used the same teleport steps and smoothing as in the teleport.js script, as long as it // still worked if the target is in the air. @@ -2547,15 +2518,15 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, // compute the position (e.g., so that if I'm on stage, going to me would compute an available seat in the audience rather than // being in my face on-stage). Note that this could work for going to an entity as well as to a person. - qCDebug(interfaceapp).nospace() << "MyAvatar goToLocation - moving to " << newPosition.x << ", " - << newPosition.y << ", " << newPosition.z; + qCDebug(interfaceapp).nospace() << "MyAvatar goToLocation - moving to " << newPosition.x << ", " << newPosition.y << ", " + << newPosition.z; _goToPending = true; _goToPosition = newPosition; _goToOrientation = getWorldOrientation(); if (hasOrientation) { - qCDebug(interfaceapp).nospace() << "MyAvatar goToLocation - new orientation is " - << newOrientation.x << ", " << newOrientation.y << ", " << newOrientation.z << ", " << newOrientation.w; + qCDebug(interfaceapp).nospace() << "MyAvatar goToLocation - new orientation is " << newOrientation.x << ", " + << newOrientation.y << ", " << newOrientation.z << ", " << newOrientation.w; // orient the user to face the target glm::quat quatOrientation = cancelOutRollAndPitch(newOrientation); @@ -2574,7 +2545,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, emit transformChanged(); } -void MyAvatar::goToLocationAndEnableCollisions(const glm::vec3& position) { // See use case in safeLanding. +void MyAvatar::goToLocationAndEnableCollisions(const glm::vec3& position) { // See use case in safeLanding. goToLocation(position); QMetaObject::invokeMethod(this, "setCollisionsEnabled", Qt::QueuedConnection, Q_ARG(bool, true)); } @@ -2600,29 +2571,29 @@ bool MyAvatar::safeLanding(const glm::vec3& position) { } if (!getCollisionsEnabled()) { goToLocation(better); // recurses on next update - } else { // If you try to go while stuck, physics will keep you stuck. + } else { // If you try to go while stuck, physics will keep you stuck. setCollisionsEnabled(false); // Don't goToLocation just yet. Yield so that physics can act on the above. - QMetaObject::invokeMethod(this, "goToLocationAndEnableCollisions", Qt::QueuedConnection, // The equivalent of javascript nextTick - Q_ARG(glm::vec3, better)); - } - return true; + QMetaObject::invokeMethod(this, "goToLocationAndEnableCollisions", + Qt::QueuedConnection, // The equivalent of javascript nextTick + Q_ARG(glm::vec3, better)); + } + return true; } // If position is not reliably safe from being stuck by physics, answer true and place a candidate better position in betterPositionOut. bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& betterPositionOut) { - // We begin with utilities and tests. The Algorithm in four parts is below. // NOTE: we use estimated avatar height here instead of the bullet capsule halfHeight, because // the domain avatar height limiting might not have taken effect yet on the actual bullet shape. auto halfHeight = 0.5f * getHeight(); if (halfHeight == 0) { - return false; // zero height avatar + return false; // zero height avatar } auto entityTree = DependencyManager::get()->getTree(); if (!entityTree) { - return false; // no entity tree + return false; // no entity tree } // More utilities. const auto capsuleCenter = positionIn; @@ -2634,7 +2605,8 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette betterPositionOut = upperIntersection + (up * halfHeight); return true; }; - auto findIntersection = [&](const glm::vec3& startPointIn, const glm::vec3& directionIn, glm::vec3& intersectionOut, EntityItemID& entityIdOut, glm::vec3& normalOut) { + auto findIntersection = [&](const glm::vec3& startPointIn, const glm::vec3& directionIn, glm::vec3& intersectionOut, + EntityItemID& entityIdOut, glm::vec3& normalOut) { OctreeElementPointer element; float distance; BoxFace face; @@ -2644,12 +2616,13 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette // See https://highfidelity.fogbugz.com/f/cases/5003/findRayIntersection-has-option-to-use-collidableOnly-but-doesn-t-actually-use-colliders const bool collidableOnly = true; const bool precisionPicking = true; - const auto lockType = Octree::Lock; // Should we refactor to take a lock just once? + const auto lockType = Octree::Lock; // Should we refactor to take a lock just once? bool* accurateResult = NULL; QVariantMap extraInfo; - EntityItemID entityID = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, collidableOnly, precisionPicking, - element, distance, face, normalOut, extraInfo, lockType, accurateResult); + EntityItemID entityID = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, + collidableOnly, precisionPicking, element, distance, face, + normalOut, extraInfo, lockType, accurateResult); if (entityID.isNull()) { return false; } @@ -2664,12 +2637,12 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette // We currently believe that physics will reliably push us out if our feet are embedded, // as long as our capsule center is out and there's room above us. Here we have those // conditions, so no need to check our feet below. - return false; // nothing above + return false; // nothing above } if (!findIntersection(capsuleCenter, down, lowerIntersection, lowerId, lowerNormal)) { // Our head may be embedded, but our center is out and there's room below. See corresponding comment above. - return false; // nothing below + return false; // nothing below } // See if we have room between entities above and below, but that we are not contained. @@ -2677,7 +2650,8 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette // I.e., we are in a clearing between two objects. if (isDown(upperNormal) && isUp(lowerNormal)) { auto spaceBetween = glm::distance(upperIntersection, lowerIntersection); - const float halfHeightFactor = 2.25f; // Until case 5003 is fixed (and maybe after?), we need a fudge factor. Also account for content modelers not being precise. + const float halfHeightFactor = + 2.25f; // Until case 5003 is fixed (and maybe after?), we need a fudge factor. Also account for content modelers not being precise. if (spaceBetween > (halfHeightFactor * halfHeight)) { // There is room for us to fit in that clearing. If there wasn't, physics would oscilate us between the objects above and below. // We're now going to iterate upwards through successive upperIntersections, testing to see if we're contained within the top surface of some entity. @@ -2689,7 +2663,7 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette ignore.push_back(upperId); if (!findIntersection(upperIntersection, up, upperIntersection, upperId, upperNormal)) { // We're not inside an entity, and from the nested tests, we have room between what is above and below. So position is good! - return false; // enough room + return false; // enough room } if (isUp(upperNormal)) { // This new intersection is the top surface of an entity that we have not yet seen, which means we're contained within it. @@ -2704,19 +2678,18 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette } } - include.push_back(upperId); // We're now looking for the intersection from above onto this entity. + include.push_back(upperId); // We're now looking for the intersection from above onto this entity. const float big = (float)TREE_SCALE; const auto skyHigh = up * big; auto fromAbove = capsuleCenter + skyHigh; if (!findIntersection(fromAbove, down, upperIntersection, upperId, upperNormal)) { - return false; // Unable to find a landing + return false; // Unable to find a landing } // Our arbitrary rule is to always go up. There's no need to look down or sideways for a "closer" safe candidate. return mustMove(); } void MyAvatar::updateMotionBehaviorFromMenu() { - if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "updateMotionBehaviorFromMenu"); return; @@ -2766,7 +2739,6 @@ float MyAvatar::getAvatarScale() { } void MyAvatar::setAvatarScale(float val) { - if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setAvatarScale", Q_ARG(float, val)); return; @@ -2776,7 +2748,6 @@ void MyAvatar::setAvatarScale(float val) { } void MyAvatar::setCollisionsEnabled(bool enabled) { - if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled)); return; @@ -2902,7 +2873,7 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { // AJT: TODO: can remove this Y_180, if we remove the higher level one. glm::vec3 headToNeck = headOrientation * Quaternions::Y_180 * (localNeck - localHead); - glm::vec3 neckToRoot = headOrientationYawOnly * Quaternions::Y_180 * -localNeck; + glm::vec3 neckToRoot = headOrientationYawOnly * Quaternions::Y_180 * -localNeck; float invSensorToWorldScale = getUserEyeHeight() / getEyeHeight(); glm::vec3 bodyPos = headPosition + invSensorToWorldScale * (headToNeck + neckToRoot); @@ -2963,7 +2934,7 @@ glm::vec3 MyAvatar::computeCounterBalance() const { QString name; float weight; glm::vec3 position; - JointMass() {}; + JointMass(){}; JointMass(QString n, float w, glm::vec3 p) { name = n; weight = w; @@ -2983,12 +2954,14 @@ glm::vec3 MyAvatar::computeCounterBalance() const { tposeHead = getAbsoluteDefaultJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgHeadMass.name)); } if (_skeletonModel->getRig().indexOfJoint(cgLeftHandMass.name) != -1) { - cgLeftHandMass.position = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgLeftHandMass.name)); + cgLeftHandMass.position = + getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgLeftHandMass.name)); } else { cgLeftHandMass.position = DEFAULT_AVATAR_LEFTHAND_POS; } if (_skeletonModel->getRig().indexOfJoint(cgRightHandMass.name) != -1) { - cgRightHandMass.position = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgRightHandMass.name)); + cgRightHandMass.position = + getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgRightHandMass.name)); } else { cgRightHandMass.position = DEFAULT_AVATAR_RIGHTHAND_POS; } @@ -2997,7 +2970,8 @@ glm::vec3 MyAvatar::computeCounterBalance() const { } // find the current center of gravity position based on head and hand moments - glm::vec3 sumOfMoments = (cgHeadMass.weight * cgHeadMass.position) + (cgLeftHandMass.weight * cgLeftHandMass.position) + (cgRightHandMass.weight * cgRightHandMass.position); + glm::vec3 sumOfMoments = (cgHeadMass.weight * cgHeadMass.position) + (cgLeftHandMass.weight * cgLeftHandMass.position) + + (cgRightHandMass.weight * cgRightHandMass.position); float totalMass = cgHeadMass.weight + cgLeftHandMass.weight + cgRightHandMass.weight; glm::vec3 currentCg = (1.0f / totalMass) * sumOfMoments; @@ -3037,7 +3011,6 @@ glm::vec3 MyAvatar::computeCounterBalance() const { // headOrientation, headPosition and hipsPosition are in avatar space // returns the matrix of the hips in Avatar space static glm::mat4 computeNewHipsMatrix(glm::quat headOrientation, glm::vec3 headPosition, glm::vec3 hipsPosition) { - glm::quat bodyOrientation = computeBodyFacingFromHead(headOrientation, Vectors::UNIT_Y); const float MIX_RATIO = 0.3f; @@ -3047,10 +3020,7 @@ static glm::mat4 computeNewHipsMatrix(glm::quat headOrientation, glm::vec3 headP glm::vec3 spineVec = headPosition - hipsPosition; glm::vec3 u, v, w; generateBasisVectors(glm::normalize(spineVec), hipsFacing, u, v, w); - return glm::mat4(glm::vec4(w, 0.0f), - glm::vec4(u, 0.0f), - glm::vec4(v, 0.0f), - glm::vec4(hipsPosition, 1.0f)); + return glm::mat4(glm::vec4(w, 0.0f), glm::vec4(u, 0.0f), glm::vec4(v, 0.0f), glm::vec4(hipsPosition, 1.0f)); } static void drawBaseOfSupport(float baseOfSupportScale, float footLocal, glm::mat4 avatarToWorld) { @@ -3091,7 +3061,8 @@ glm::mat4 MyAvatar::deriveBodyUsingCgModel() const { if (_enableDebugDrawBaseOfSupport) { float scaleBaseOfSupport = getUserEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT; - glm::vec3 rightFootPositionLocal = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint("RightFoot")); + glm::vec3 rightFootPositionLocal = + getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint("RightFoot")); drawBaseOfSupport(scaleBaseOfSupport, rightFootPositionLocal.y, avatarToWorldMat); } @@ -3099,7 +3070,8 @@ glm::mat4 MyAvatar::deriveBodyUsingCgModel() const { const glm::vec3 cgHipsPosition = computeCounterBalance(); // find the new hips rotation using the new head-hips axis as the up axis - glm::mat4 avatarHipsMat = computeNewHipsMatrix(glmExtractRotation(avatarHeadMat), extractTranslation(avatarHeadMat), cgHipsPosition); + glm::mat4 avatarHipsMat = + computeNewHipsMatrix(glmExtractRotation(avatarHeadMat), extractTranslation(avatarHeadMat), cgHipsPosition); // convert hips from avatar to sensor space // The Y_180 is to convert from z forward to -z forward. @@ -3107,7 +3079,7 @@ glm::mat4 MyAvatar::deriveBodyUsingCgModel() const { } static bool isInsideLine(glm::vec3 a, glm::vec3 b, glm::vec3 c) { - return (((b.x - a.x)*(c.z - a.z) - (b.z - a.z)*(c.x - a.x)) > 0); + return (((b.x - a.x) * (c.z - a.z) - (b.z - a.z) * (c.x - a.x)) > 0); } static bool withinBaseOfSupport(glm::vec3 position) { @@ -3125,7 +3097,7 @@ static bool withinBaseOfSupport(glm::vec3 position) { bool withinFrontBase = isInsideLine(userScale * frontLeft, userScale * frontRight, position); bool withinBackBase = isInsideLine(userScale * backRight, userScale * backLeft, position); bool withinLateralBase = (isInsideLine(userScale * frontRight, userScale * backRight, position) && - isInsideLine(userScale * backLeft, userScale * frontLeft, position)); + isInsideLine(userScale * backLeft, userScale * frontLeft, position)); return (withinFrontBase && withinBackBase && withinLateralBase); } @@ -3134,16 +3106,46 @@ static bool headAngularVelocityBelowThreshold(glm::vec3 angularVelocity) { glm::vec3 xzPlaneAngularVelocity(angularVelocity.x, 0.0f, angularVelocity.z); float magnitudeAngularVelocity = glm::length(xzPlaneAngularVelocity); bool isBelowThreshold = (magnitudeAngularVelocity < ANGULAR_VELOCITY_THRESHOLD); - qCDebug(interfaceapp) << "magnitude " << magnitudeAngularVelocity << "head velocity below threshold is: " << isBelowThreshold; - qCDebug(interfaceapp) << "ang vel values x: " << angularVelocity.x << " y: " << angularVelocity.y << " z: " << angularVelocity.z; + qCDebug(interfaceapp) << "magnitude " << magnitudeAngularVelocity + << "head velocity below threshold is: " << isBelowThreshold; + qCDebug(interfaceapp) << "ang vel values x: " << angularVelocity.x << " y: " << angularVelocity.y + << " z: " << angularVelocity.z; return isBelowThreshold; } - -static bool withinThresholdOfStandingHeightMode(float diffFromMode) { +/* +bool MyAvatar::withinThresholdOfStandingHeightMode(float newReading) { + const float CENTIMETERS_PER_METER = 100.0f; + const float MODE_CORRECTION_FACTOR = 0.02f; const float MODE_HEIGHT_THRESHOLD = 0.3f; - return (diffFromMode < MODE_HEIGHT_THRESHOLD); -} + //first add the number to the mode array + for (int i = 0; i < (SIZE_OF_MODE_ARRAY - 1); i++) { + _heightModeArray[i] = _heightModeArray[i + 1]; + } + _heightModeArray[SIZE_OF_MODE_ARRAY - 1] = (int)(newReading * CENTIMETERS_PER_METER); + + int greatestFrequency = 0; + int mode = 0; + std::map freq; + for (int j = 0; j < SIZE_OF_MODE_ARRAY; j++) { + freq[_heightModeArray[j]] += 1; + if ((freq[_heightModeArray[j]] > greatestFrequency) || + ((freq[_heightModeArray[j]] == SIZE_OF_MODE_ARRAY) && (_heightModeArray[j] > _currentMode))) { + greatestFrequency = freq[_heightModeArray[j]]; + mode = _heightModeArray[j]; + } + } + if (mode > _currentMode) { + return mode; + } else { + if (!_resetMode && qApp->isHMDMode()) { + _resetMode = true; + return (newReading - MODE_CORRECTION_FACTOR); + } + } + //return (diffFromMode < MODE_HEIGHT_THRESHOLD); +} +*/ float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -3250,12 +3252,10 @@ void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& d driveKeys = static_cast(object.toUInt16()); } - void MyAvatar::lateUpdatePalms() { Avatar::updatePalms(); } - static const float FOLLOW_TIME = 0.5f; MyAvatar::FollowHelper::FollowHelper() { @@ -3309,13 +3309,17 @@ void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) { } } -bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { - const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees +bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, + const glm::mat4& desiredBodyMatrix, + const glm::mat4& currentBodyMatrix) const { + const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix); return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; } -bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { +bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, + const glm::mat4& desiredBodyMatrix, + const glm::mat4& currentBodyMatrix) const { // -z axis of currentBodyMatrix in world space. glm::vec3 forward = glm::normalize(glm::vec3(-currentBodyMatrix[0][2], -currentBodyMatrix[1][2], -currentBodyMatrix[2][2])); // x axis of currentBodyMatrix in world space. @@ -3329,7 +3333,6 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, const float MAX_FORWARD_LEAN = 0.15f; const float MAX_BACKWARD_LEAN = 0.1f; - if (forwardLeanAmount > 0 && forwardLeanAmount > MAX_FORWARD_LEAN) { return true; } else if (forwardLeanAmount < 0 && forwardLeanAmount < -MAX_BACKWARD_LEAN) { @@ -3337,10 +3340,11 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, } return fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; - } -bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { +bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, + const glm::mat4& desiredBodyMatrix, + const glm::mat4& currentBodyMatrix) const { const float CYLINDER_TOP = 0.1f; const float CYLINDER_BOTTOM = -1.5f; @@ -3349,11 +3353,11 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, co return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); } -void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, - const glm::mat4& currentBodyMatrix, bool hasDriveInput) { - - if (myAvatar.getHMDLeanRecenterEnabled() && - qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { +void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, + const glm::mat4& desiredBodyMatrix, + const glm::mat4& currentBodyMatrix, + bool hasDriveInput) { + if (myAvatar.getHMDLeanRecenterEnabled() && qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); } @@ -3375,9 +3379,9 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()); if (!isActive(Horizontal) && (getForceActivateHorizontal() || - (!withinBaseOfSupport(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation()) && - headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()))) && - withinThresholdOfStandingHeightMode(0.01f)) { + (!withinBaseOfSupport(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation()) && + headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity())))){ //&& + //withinThresholdOfStandingHeightMode(0.01f)))) { qCDebug(interfaceapp) << "----------------------------------------over the base of support"; activate(Horizontal); setForceActivateHorizontal(false); @@ -3395,7 +3399,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat glm::quat currentHipsLocal = myAvatar.getAbsoluteJointRotationInObjectFrame(myAvatar.getJointIndex("Hips")); const glm::quat hipsinWorldSpace = followWorldPose.rot() * (Quaternions::Y_180 * (currentHipsLocal)); - const glm::vec3 avatarUpWorld = glm::normalize(followWorldPose.rot()*(Vectors::UP)); + const glm::vec3 avatarUpWorld = glm::normalize(followWorldPose.rot() * (Vectors::UP)); glm::quat resultingSwingInWorld; glm::quat resultingTwistInWorld; swingTwistDecomposition(hipsinWorldSpace, avatarUpWorld, resultingSwingInWorld, resultingTwistInWorld); @@ -3404,8 +3408,8 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat followWorldPose.scale() = glm::vec3(1.0f); if (isActive(Rotation)) { - //use the hmd reading for the hips follow - followWorldPose.rot() = glmExtractRotation(desiredWorldMatrix); + //use the hmd reading for the hips follow + followWorldPose.rot() = glmExtractRotation(desiredWorldMatrix); } if (isActive(Horizontal)) { glm::vec3 desiredTranslation = extractTranslation(desiredWorldMatrix); @@ -3433,7 +3437,8 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, co glm::mat4 worldToSensorMatrix = glm::inverse(sensorToWorldMatrix); glm::vec3 sensorLinearDisplacement = transformVectorFast(worldToSensorMatrix, worldLinearDisplacement); - glm::quat sensorAngularDisplacement = glmExtractRotation(worldToSensorMatrix) * worldAngularDisplacement * glmExtractRotation(sensorToWorldMatrix); + glm::quat sensorAngularDisplacement = + glmExtractRotation(worldToSensorMatrix) * worldAngularDisplacement * glmExtractRotation(sensorToWorldMatrix); glm::mat4 newBodyMat = createMatFromQuatAndPos(sensorAngularDisplacement * glmExtractRotation(currentBodyMatrix), sensorLinearDisplacement + extractTranslation(currentBodyMatrix)); @@ -3496,7 +3501,8 @@ bool MyAvatar::didTeleport() { } bool MyAvatar::hasDriveInput() const { - return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f; + return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || + fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f; } void MyAvatar::setAway(bool value) { @@ -3512,7 +3518,6 @@ void MyAvatar::setAway(bool value) { // Specificly, if we are rendering using a third person camera. We would like to render the hand controllers in front of the camera, // not in front of the avatar. glm::mat4 MyAvatar::computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const { - // Fetch the current camera transform. glm::mat4 cameraWorldMatrix = qApp->getCamera().getTransform(); if (qApp->getCamera().getMode() == CAMERA_MODE_MIRROR) { @@ -3539,7 +3544,7 @@ glm::mat4 MyAvatar::computeCameraRelativeHandControllerMatrix(const glm::mat4& c glm::quat MyAvatar::getAbsoluteJointRotationInObjectFrame(int index) const { if (index < 0) { - index += numeric_limits::max() + 1; // 65536 + index += numeric_limits::max() + 1; // 65536 } switch (index) { @@ -3568,15 +3573,13 @@ glm::quat MyAvatar::getAbsoluteJointRotationInObjectFrame(int index) const { glm::mat4 invAvatarMat = avatarTransform.getInverseMatrix(); return glmExtractRotation(invAvatarMat * qApp->getCamera().getTransform()); } - default: { - return Avatar::getAbsoluteJointRotationInObjectFrame(index); - } + default: { return Avatar::getAbsoluteJointRotationInObjectFrame(index); } } } glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { if (index < 0) { - index += numeric_limits::max() + 1; // 65536 + index += numeric_limits::max() + 1; // 65536 } switch (index) { @@ -3605,9 +3608,7 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { glm::mat4 invAvatarMat = avatarTransform.getInverseMatrix(); return extractTranslation(invAvatarMat * qApp->getCamera().getTransform()); } - default: { - return Avatar::getAbsoluteJointTranslationInObjectFrame(index); - } + default: { return Avatar::getAbsoluteJointTranslationInObjectFrame(index); } } } @@ -3616,7 +3617,9 @@ glm::mat4 MyAvatar::getCenterEyeCalibrationMat() const { int rightEyeIndex = _skeletonModel->getRig().indexOfJoint("RightEye"); int leftEyeIndex = _skeletonModel->getRig().indexOfJoint("LeftEye"); if (rightEyeIndex >= 0 && leftEyeIndex >= 0) { - auto centerEyePos = (getAbsoluteDefaultJointTranslationInObjectFrame(rightEyeIndex) + getAbsoluteDefaultJointTranslationInObjectFrame(leftEyeIndex)) * 0.5f; + auto centerEyePos = (getAbsoluteDefaultJointTranslationInObjectFrame(rightEyeIndex) + + getAbsoluteDefaultJointTranslationInObjectFrame(leftEyeIndex)) * + 0.5f; auto centerEyeRot = Quaternions::Y_180; return createMatFromQuatAndPos(centerEyeRot, centerEyePos / getSensorToWorldScale()); } else { @@ -3684,7 +3687,6 @@ glm::mat4 MyAvatar::getRightFootCalibrationMat() const { } } - glm::mat4 MyAvatar::getRightArmCalibrationMat() const { int rightArmIndex = _skeletonModel->getRig().indexOfJoint("RightArm"); if (rightArmIndex >= 0) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 1a6feb142a..abe957f8cd 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -49,6 +49,8 @@ enum AudioListenerMode { CUSTOM }; +const int SIZE_OF_MODE_ARRAY = 50; + Q_DECLARE_METATYPE(AudioListenerMode); class MyAvatar : public Avatar { @@ -1025,6 +1027,8 @@ public: bool isReadyForPhysics() const; + bool withinThresholdOfStandingHeightMode(float newReading); + public slots: /**jsdoc @@ -1631,6 +1635,10 @@ private: bool _shouldLoadScripts { false }; bool _haveReceivedHeightLimitsFromDomain = { false }; + int _heightModeArray[SIZE_OF_MODE_ARRAY]; + int _currentMode = 0; + bool _resetMode = false; + }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); From 621ca7d2b4f44371cfa0b0cf47b443bec9926bc2 Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 27 Jun 2018 18:46:24 -0700 Subject: [PATCH 007/182] more work on the mode function --- interface/src/avatar/MyAvatar.cpp | 16 +++++++++++----- interface/src/avatar/MyAvatar.h | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index f33bd41f16..06b788a002 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3112,11 +3112,17 @@ static bool headAngularVelocityBelowThreshold(glm::vec3 angularVelocity) { << " z: " << angularVelocity.z; return isBelowThreshold; } + /* -bool MyAvatar::withinThresholdOfStandingHeightMode(float newReading) { +bool MyAvatar::isWithinThresholdHeightMode(float newMode, float newReading) { + const float MODE_HEIGHT_THRESHOLD = 0.3f; + return newMode < +} +*/ +float MyAvatar::computeStandingHeightMode(float newReading) { const float CENTIMETERS_PER_METER = 100.0f; const float MODE_CORRECTION_FACTOR = 0.02f; - const float MODE_HEIGHT_THRESHOLD = 0.3f; + //first add the number to the mode array for (int i = 0; i < (SIZE_OF_MODE_ARRAY - 1); i++) { @@ -3143,9 +3149,9 @@ bool MyAvatar::withinThresholdOfStandingHeightMode(float newReading) { return (newReading - MODE_CORRECTION_FACTOR); } } - //return (diffFromMode < MODE_HEIGHT_THRESHOLD); + return _currentMode; } -*/ + float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -3377,7 +3383,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, } headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()); - + float temp = myAvatar.computeStandingHeightMode(0.01f); if (!isActive(Horizontal) && (getForceActivateHorizontal() || (!withinBaseOfSupport(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation()) && headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity())))){ //&& diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index abe957f8cd..dddc229171 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -49,8 +49,6 @@ enum AudioListenerMode { CUSTOM }; -const int SIZE_OF_MODE_ARRAY = 50; - Q_DECLARE_METATYPE(AudioListenerMode); class MyAvatar : public Avatar { @@ -1027,7 +1025,8 @@ public: bool isReadyForPhysics() const; - bool withinThresholdOfStandingHeightMode(float newReading); + float computeStandingHeightMode(float newReading); + //bool isWithinThresholdHeightMode(float newReading); public slots: @@ -1634,6 +1633,7 @@ private: // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; + static const int SIZE_OF_MODE_ARRAY = 50; bool _haveReceivedHeightLimitsFromDomain = { false }; int _heightModeArray[SIZE_OF_MODE_ARRAY]; int _currentMode = 0; From 2307c8e24adbad15353b00666febea3335526379 Mon Sep 17 00:00:00 2001 From: Angus Antley Date: Thu, 28 Jun 2018 15:40:42 +0100 Subject: [PATCH 008/182] more work on the mode threshold --- interface/src/avatar/MyAvatar.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 06b788a002..a8246a24e2 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3113,12 +3113,12 @@ static bool headAngularVelocityBelowThreshold(glm::vec3 angularVelocity) { return isBelowThreshold; } -/* -bool MyAvatar::isWithinThresholdHeightMode(float newMode, float newReading) { - const float MODE_HEIGHT_THRESHOLD = 0.3f; - return newMode < + +static bool isWithinThresholdHeightMode(float newReading, float newMode) { + const float MODE_HEIGHT_THRESHOLD = -0.02f; + return (newReading - newMode) > MODE_HEIGHT_THRESHOLD; } -*/ + float MyAvatar::computeStandingHeightMode(float newReading) { const float CENTIMETERS_PER_METER = 100.0f; const float MODE_CORRECTION_FACTOR = 0.02f; @@ -3386,8 +3386,8 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, float temp = myAvatar.computeStandingHeightMode(0.01f); if (!isActive(Horizontal) && (getForceActivateHorizontal() || (!withinBaseOfSupport(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation()) && - headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity())))){ //&& - //withinThresholdOfStandingHeightMode(0.01f)))) { + headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()) && + isWithinThresholdHeightMode(myAvatar.computeStandingHeightMode(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y), myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y)))) { qCDebug(interfaceapp) << "----------------------------------------over the base of support"; activate(Horizontal); setForceActivateHorizontal(false); From 7fe9365a76f59968c519e911ab814ec592e55d2e Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 28 Jun 2018 19:12:03 +0200 Subject: [PATCH 009/182] create app - native windows --- .../qml/hifi/tablet/EditEntityList.qml | 15 + .../resources/qml/hifi/tablet/EditTabView.qml | 63 ++-- .../resources/qml/hifi/tablet/EditTools.qml | 58 ++++ .../qml/hifi/tablet/EditToolsTabView.qml | 328 ++++++++++++++++++ .../resources/qml/hifi/tablet/EntityList.qml | 5 + .../qml/hifi/tablet/NewMaterialDialog.qml | 16 +- .../qml/hifi/tablet/NewMaterialWindow.qml | 20 ++ .../qml/hifi/tablet/NewModelDialog.qml | 16 +- .../qml/hifi/tablet/NewModelWindow.qml | 20 ++ scripts/system/edit.js | 307 +++++++++------- scripts/system/libraries/entityList.js | 101 ++++-- scripts/system/libraries/gridTool.js | 14 +- scripts/system/modules/createWindow.js | 150 ++++++++ .../particle_explorer/particleExplorerTool.js | 21 +- 14 files changed, 918 insertions(+), 216 deletions(-) create mode 100644 interface/resources/qml/hifi/tablet/EditEntityList.qml create mode 100644 interface/resources/qml/hifi/tablet/EditTools.qml create mode 100644 interface/resources/qml/hifi/tablet/EditToolsTabView.qml create mode 100644 interface/resources/qml/hifi/tablet/EntityList.qml create mode 100644 interface/resources/qml/hifi/tablet/NewMaterialWindow.qml create mode 100644 interface/resources/qml/hifi/tablet/NewModelWindow.qml create mode 100644 scripts/system/modules/createWindow.js diff --git a/interface/resources/qml/hifi/tablet/EditEntityList.qml b/interface/resources/qml/hifi/tablet/EditEntityList.qml new file mode 100644 index 0000000000..d484885103 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EditEntityList.qml @@ -0,0 +1,15 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtWebChannel 1.0 +import "../../controls" +import "../toolbars" +import QtGraphicalEffects 1.0 +import "../../controls-uit" as HifiControls +import "../../styles-uit" + + +WebView { + id: entityListToolWebView + url: Paths.defaultScripts + "/system/html/entityList.html" + enabled: true +} diff --git a/interface/resources/qml/hifi/tablet/EditTabView.qml b/interface/resources/qml/hifi/tablet/EditTabView.qml index 9a7958f95c..4ac8755570 100644 --- a/interface/resources/qml/hifi/tablet/EditTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditTabView.qml @@ -9,7 +9,6 @@ import "../../styles-uit" TabBar { id: editTabView - // anchors.fill: parent width: parent.width contentWidth: parent.width padding: 0 @@ -34,7 +33,7 @@ TabBar { width: parent.width clip: true - contentHeight: createEntitiesFlow.height + importButton.height + assetServerButton.height + + contentHeight: createEntitiesFlow.height + importButton.height + assetServerButton.height + header.anchors.topMargin + createEntitiesFlow.anchors.topMargin + assetServerButton.anchors.topMargin + importButton.anchors.topMargin + header.paintedHeight @@ -77,8 +76,9 @@ TabBar { text: "MODEL" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newModelButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newModelButton" } + }); editTabView.currentIndex = 2 } } @@ -88,8 +88,9 @@ TabBar { text: "CUBE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newCubeButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newCubeButton" } + }); editTabView.currentIndex = 2 } } @@ -99,8 +100,9 @@ TabBar { text: "SPHERE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newSphereButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newSphereButton" } + }); editTabView.currentIndex = 2 } } @@ -110,8 +112,9 @@ TabBar { text: "LIGHT" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newLightButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newLightButton" } + }); editTabView.currentIndex = 2 } } @@ -121,8 +124,9 @@ TabBar { text: "TEXT" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newTextButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newTextButton" } + }); editTabView.currentIndex = 2 } } @@ -132,8 +136,9 @@ TabBar { text: "IMAGE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newImageButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newImageButton" } + }); editTabView.currentIndex = 2 } } @@ -143,8 +148,9 @@ TabBar { text: "WEB" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newWebButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newWebButton" } + }); editTabView.currentIndex = 2 } } @@ -154,8 +160,9 @@ TabBar { text: "ZONE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newZoneButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newZoneButton" } + }); editTabView.currentIndex = 2 } } @@ -165,8 +172,9 @@ TabBar { text: "PARTICLE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newParticleButton" } + }); editTabView.currentIndex = 4 } } @@ -176,8 +184,9 @@ TabBar { text: "MATERIAL" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newMaterialButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newMaterialButton" } + }); editTabView.currentIndex = 2 } } @@ -196,8 +205,9 @@ TabBar { anchors.topMargin: 35 onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "openAssetBrowserButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "openAssetBrowserButton" } + }); } } @@ -214,8 +224,9 @@ TabBar { anchors.topMargin: 20 onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "importEntitiesButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "importEntitiesButton" } + }); } } } diff --git a/interface/resources/qml/hifi/tablet/EditTools.qml b/interface/resources/qml/hifi/tablet/EditTools.qml new file mode 100644 index 0000000000..f989038c16 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EditTools.qml @@ -0,0 +1,58 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.3 + +// FIXME pretty non-DRY code, should figure out a way to optionally hide one tab from the tab view, keep in sync with Edit.qml +StackView { + id: editRoot + objectName: "stack" + + signal sendToScript(var message); + + topPadding: 40 + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + + anchors.fill: parent + + property var itemProperties: {"y": editRoot.topPadding, + "width": editRoot.availableWidth, + "height": editRoot.availableHeight } + Component.onCompleted: { + tab.currentIndex = 0 + } + + background: Rectangle { + color: "#404040" //default background color + EditToolsTabView { + id: tab + anchors.fill: parent + currentIndex: -1 + onCurrentIndexChanged: { + editRoot.replace(null, tab.itemAt(currentIndex).visualItem, + itemProperties, + StackView.Immediate) + } + } + } + + function pushSource(path) { + editRoot.push(Qt.resolvedUrl("../../" + path), itemProperties, + StackView.Immediate); + editRoot.currentItem.sendToScript.connect(editRoot.sendToScript); + } + + function popSource() { + editRoot.pop(StackView.Immediate); + } + + // Passes script messages to the item on the top of the stack + function fromScript(message) { + var currentItem = editRoot.currentItem; + if (currentItem && currentItem.fromScript) { + currentItem.fromScript(message); + } else if (tab.fromScript) { + tab.fromScript(message); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/EditToolsTabView.qml b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml new file mode 100644 index 0000000000..00084b8ca9 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml @@ -0,0 +1,328 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtWebChannel 1.0 +import "../../controls" +import "../toolbars" +import QtGraphicalEffects 1.0 +import "../../controls-uit" as HifiControls +import "../../styles-uit" + +TabBar { + id: editTabView + width: parent.width + contentWidth: parent.width + padding: 0 + spacing: 0 + + readonly property QtObject tabIndex: QtObject { + readonly property int create: 0 + readonly property int properties: 1 + readonly property int grid: 2 + readonly property int particle: 3 + } + + readonly property HifiConstants hifi: HifiConstants {} + + EditTabButton { + title: "CREATE" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + + Rectangle { + color: "#404040" + id: container + + Flickable { + height: parent.height + width: parent.width + clip: true + + contentHeight: createEntitiesFlow.height + importButton.height + assetServerButton.height + + header.anchors.topMargin + createEntitiesFlow.anchors.topMargin + + assetServerButton.anchors.topMargin + importButton.anchors.topMargin + + header.paintedHeight + + contentWidth: width + + ScrollBar.vertical : ScrollBar { + visible: parent.contentHeight > parent.height + width: 20 + background: Rectangle { + color: hifi.colors.tableScrollBackgroundDark + } + } + + Text { + id: header + color: "#ffffff" + text: "Choose an Entity Type to Create:" + font.pixelSize: 14 + font.bold: true + anchors.top: parent.top + anchors.topMargin: 28 + anchors.left: parent.left + anchors.leftMargin: 28 + } + + Flow { + id: createEntitiesFlow + spacing: 35 + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: parent.top + anchors.topMargin: 70 + + + NewEntityButton { + icon: "icons/create-icons/94-model-01.svg" + text: "MODEL" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newModelButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/21-cube-01.svg" + text: "CUBE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newCubeButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/22-sphere-01.svg" + text: "SPHERE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newSphereButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/24-light-01.svg" + text: "LIGHT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newLightButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/20-text-01.svg" + text: "TEXT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newTextButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/image.svg" + text: "IMAGE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newImageButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/25-web-1-01.svg" + text: "WEB" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newWebButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/23-zone-01.svg" + text: "ZONE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newZoneButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/90-particles-01.svg" + text: "PARTICLE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newParticleButton" } + }); + editTabView.currentIndex = tabIndex.particle + } + } + + NewEntityButton { + icon: "icons/create-icons/126-material-01.svg" + text: "MATERIAL" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newMaterialButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + } + + HifiControls.Button { + id: assetServerButton + text: "Open This Domain's Asset Server" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: createEntitiesFlow.bottom + anchors.topMargin: 35 + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "openAssetBrowserButton" } + }); + } + } + + HifiControls.Button { + id: importButton + text: "Import Entities (.json)" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: assetServerButton.bottom + anchors.topMargin: 20 + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "importEntitiesButton" } + }); + } + } + } + } // Flickable + } + } + + EditTabButton { + title: "PROPERTIES" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + WebView { + id: entityPropertiesWebView + url: Paths.defaultScripts + "/system/html/entityProperties.html" + enabled: true + } + } + } + + EditTabButton { + title: "GRID" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + WebView { + id: gridControlsWebView + url: Paths.defaultScripts + "/system/html/gridControls.html" + enabled: true + } + } + } + + EditTabButton { + title: "P" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + WebView { + id: particleExplorerWebView + url: Paths.defaultScripts + "/system/particle_explorer/particleExplorer.html" + enabled: true + } + } + } + + function fromScript(message) { + switch (message.method) { + case 'selectTab': + selectTab(message.params.id); + break; + default: + console.warn('Unrecognized message:', JSON.stringify(message)); + } + } + + // Changes the current tab based on tab index or title as input + function selectTab(id) { + if (typeof id === 'number') { + if (id >= tabIndex.create && id <= tabIndex.particle) { + editTabView.currentIndex = id; + } else { + console.warn('Attempt to switch to invalid tab:', id); + } + } else if (typeof id === 'string'){ + switch (id.toLowerCase()) { + case 'create': + editTabView.currentIndex = tabIndex.create; + break; + case 'properties': + editTabView.currentIndex = tabIndex.properties; + break; + case 'grid': + editTabView.currentIndex = tabIndex.grid; + break; + case 'particle': + editTabView.currentIndex = tabIndex.particle; + break; + default: + console.warn('Attempt to switch to invalid tab:', id); + } + } else { + console.warn('Attempt to switch tabs with invalid input:', JSON.stringify(id)); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/EntityList.qml b/interface/resources/qml/hifi/tablet/EntityList.qml new file mode 100644 index 0000000000..f4b47c19bb --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EntityList.qml @@ -0,0 +1,5 @@ +WebView { + id: entityListToolWebView + url: Paths.defaultScripts + "/system/html/entityList.html" + enabled: true +} diff --git a/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml b/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml index 6df97e67b0..526a42f8e2 100644 --- a/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml @@ -29,12 +29,16 @@ Rectangle { property bool keyboardRasied: false function errorMessageBox(message) { - return desktop.messageBox({ - icon: hifi.icons.warning, - defaultButton: OriginalDialogs.StandardButton.Ok, - title: "Error", - text: message - }); + try { + return desktop.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } catch(e) { + Window.alert(message); + } } Item { diff --git a/interface/resources/qml/hifi/tablet/NewMaterialWindow.qml b/interface/resources/qml/hifi/tablet/NewMaterialWindow.qml new file mode 100644 index 0000000000..def816c36e --- /dev/null +++ b/interface/resources/qml/hifi/tablet/NewMaterialWindow.qml @@ -0,0 +1,20 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +StackView { + id: stackView + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + anchors.topMargin: 40 + + signal sendToScript(var message); + + NewMaterialDialog { + id: dialog + anchors.fill: parent + Component.onCompleted:{ + dialog.sendToScript.connect(stackView.sendToScript); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/NewModelDialog.qml b/interface/resources/qml/hifi/tablet/NewModelDialog.qml index 8f6718e1f3..10b844c987 100644 --- a/interface/resources/qml/hifi/tablet/NewModelDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewModelDialog.qml @@ -29,12 +29,16 @@ Rectangle { property bool keyboardRasied: false function errorMessageBox(message) { - return desktop.messageBox({ - icon: hifi.icons.warning, - defaultButton: OriginalDialogs.StandardButton.Ok, - title: "Error", - text: message - }); + try { + return desktop.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } catch(e) { + Window.alert(message); + } } Item { diff --git a/interface/resources/qml/hifi/tablet/NewModelWindow.qml b/interface/resources/qml/hifi/tablet/NewModelWindow.qml new file mode 100644 index 0000000000..616a44ab7a --- /dev/null +++ b/interface/resources/qml/hifi/tablet/NewModelWindow.qml @@ -0,0 +1,20 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +StackView { + id: stackView + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + anchors.topMargin: 40 + + signal sendToScript(var message); + + NewModelDialog { + id: dialog + anchors.fill: parent + Component.onCompleted:{ + dialog.sendToScript.connect(stackView.sendToScript); + } + } +} diff --git a/scripts/system/edit.js b/scripts/system/edit.js index e9c7a49378..da68151fbe 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -10,17 +10,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, Overlays, OverlayWebWindow, UserActivityLogger, - Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */ +/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, + Overlays, OverlayWebWindow, UserActivityLogger, Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, + progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool, OverlaySystemWindow */ (function() { // BEGIN LOCAL_SCOPE "use strict"; -var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; var EDIT_TOGGLE_BUTTON = "com.highfidelity.interface.system.editButton"; -var SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system"; -var EDIT_TOOLBAR = "com.highfidelity.interface.toolbar.edit"; Script.include([ "libraries/stringHelpers.js", @@ -36,13 +34,43 @@ Script.include([ "libraries/entityIconOverlayManager.js" ]); +var CreateWindow = Script.require('./modules/createWindow.js'); + +var TITLE_OFFSET = 60; +var ENTITY_LIST_WIDTH = 470; +var MAX_DEFAULT_ENTITY_LIST_HEIGHT = 942; + +var createToolsWindow = new CreateWindow( + Script.resourcesPath() + "qml/hifi/tablet/EditTools.qml", + 'Create Tools', + 'com.highfidelity.create.createToolsWindow', + function () { + var windowHeight = Window.innerHeight - TITLE_OFFSET; + if (windowHeight > MAX_DEFAULT_ENTITY_LIST_HEIGHT) { + windowHeight = MAX_DEFAULT_ENTITY_LIST_HEIGHT; + } + return { + size: { + x: ENTITY_LIST_WIDTH, + y: windowHeight + }, + position: { + x: Window.x + Window.innerWidth - ENTITY_LIST_WIDTH, + y: Window.y + TITLE_OFFSET + } + } + }, + false +); + var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; var PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); var POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); var SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); -entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { + +var entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); if (properties.type === 'Light') { return { @@ -59,7 +87,8 @@ var cameraManager = new CameraManager(); var grid = new Grid(); var gridTool = new GridTool({ - horizontalGrid: grid + horizontalGrid: grid, + createToolsWindow: createToolsWindow }); gridTool.setVisible(false); @@ -207,7 +236,7 @@ function hideMarketplace() { // } function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { - // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original + // Adjust the position such that the bounding box (registration, dimensions and orientation) lies behind the original // position in the given direction. var CORNERS = [ { x: 0, y: 0, z: 0 }, @@ -232,7 +261,6 @@ function adjustPositionPerBoundingBox(position, direction, registration, dimensi return position; } -var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); var GRABBABLE_ENTITIES_MENU_CATEGORY = "Edit"; // Handles any edit mode updates required when domains have switched @@ -260,6 +288,7 @@ var toolBar = (function () { toolBar, activeButton = null, systemToolbar = null, + dialogWindow = null, tablet = null; function createNewEntity(properties) { @@ -356,6 +385,13 @@ var toolBar = (function () { return entityID; } + function closeExistingDialogWindow() { + if (dialogWindow) { + dialogWindow.close(); + dialogWindow = null; + } + } + function cleanup() { that.setActive(false); if (tablet) { @@ -438,7 +474,7 @@ var toolBar = (function () { if (materialURL.startsWith("materialData")) { materialData = JSON.stringify({ "materials": {} - }) + }); } var DEFAULT_LAYERED_MATERIAL_PRIORITY = 1; @@ -458,15 +494,23 @@ var toolBar = (function () { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); tablet.popFromStack(); switch (message.method) { - case "newModelDialogAdd": - handleNewModelDialogResult(message.params); - break; - case "newEntityButtonClicked": - buttonHandlers[message.params.buttonName](); - break; - case "newMaterialDialogAdd": - handleNewMaterialDialogResult(message.params); - break; + case "newModelDialogAdd": + handleNewModelDialogResult(message.params); + closeExistingDialogWindow(); + break; + case "newModelDialogCancel": + closeExistingDialogWindow(); + break; + case "newEntityButtonClicked": + buttonHandlers[message.params.buttonName](); + break; + case "newMaterialDialogAdd": + handleNewMaterialDialogResult(message.params); + closeExistingDialogWindow(); + break; + case "newMaterialDialogCancel": + closeExistingDialogWindow(); + break; } } @@ -527,11 +571,13 @@ var toolBar = (function () { }); createButton = activeButton; tablet.screenChanged.connect(function (type, url) { - if (isActive && (type !== "QML" || url !== "hifi/tablet/Edit.qml")) { - that.setActive(false) + var isGoingToHomescreenOnDesktop = (!HMD.active && url === 'hifi/tablet/TabletHome.qml'); + if (isActive && (type !== "QML" || url !== "hifi/tablet/Edit.qml") && !isGoingToHomescreenOnDesktop) { + that.setActive(false); } }); tablet.fromQml.connect(fromQml); + createToolsWindow.fromQml.addListener(fromQml); createButton.clicked.connect(function() { if ( ! (Entities.canRez() || Entities.canRezTmp() || Entities.canRezCertified() || Entities.canRezTmpCertified()) ) { @@ -550,12 +596,26 @@ var toolBar = (function () { addButton("openAssetBrowserButton", function() { Window.showAssetServer(); }); + function createNewEntityDialogButtonCallback(entityType) { + return function() { + if (HMD.active) { + // tablet version of new-model dialog + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.pushOntoStack("hifi/tablet/New" + entityType + "Dialog.qml"); + } else { + closeExistingDialogWindow(); + dialogWindow = Desktop.createWindow("qml/hifi/tablet/New" + entityType + "Window.qml", { + title: "New " + entityType + " Entity", + flags: Desktop.AlwaysOnTop | Desktop.ForceNative, + size: { x: 500, y: 300 }, + visible: true + }); + dialogWindow.fromQml.connect(fromQml); + } + }; + }; - addButton("newModelButton", function () { - // tablet version of new-model dialog - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.pushOntoStack("hifi/tablet/NewModelDialog.qml"); - }); + addButton("newModelButton", createNewEntityDialogButtonCallback("Model")); addButton("newCubeButton", function () { createNewEntity({ @@ -716,11 +776,7 @@ var toolBar = (function () { }); }); - addButton("newMaterialButton", function () { - // tablet version of new material dialog - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.pushOntoStack("hifi/tablet/NewMaterialDialog.qml"); - }); + addButton("newMaterialButton", createNewEntityDialogButtonCallback("Material")); that.setActive(false); } @@ -743,6 +799,8 @@ var toolBar = (function () { Controller.captureEntityClickEvents(); } else { Controller.releaseEntityClickEvents(); + + closeExistingDialogWindow(); } if (active === isActive) { return; @@ -769,7 +827,12 @@ var toolBar = (function () { selectionDisplay.triggerMapping.disable(); tablet.landscape = false; } else { - tablet.loadQMLSource("hifi/tablet/Edit.qml", true); + if (HMD.active) { + tablet.loadQMLSource("hifi/tablet/Edit.qml", true); + } else { + // make other apps inactive while in desktop mode + tablet.gotoHomeScreen(); + } UserActivityLogger.enabledEdit(); entityListTool.setVisible(true); gridTool.setVisible(true); @@ -790,17 +853,6 @@ var toolBar = (function () { return that; })(); - -function isLocked(properties) { - // special case to lock the ground plane model in hq. - if (location.hostname === "hq.highfidelity.io" && - properties.modelURL === HIFI_PUBLIC_BUCKET + "ozan/Terrain_Reduce_forAlpha.fbx") { - return true; - } - return false; -} - - var selectedEntityID; var orientation; var intersection; @@ -1047,68 +1099,62 @@ function mouseClickEvent(event) { return; } properties = Entities.getEntityProperties(foundEntity); - if (isLocked(properties)) { - if (wantDebug) { - print("Model locked " + properties.id); + var halfDiagonal = Vec3.length(properties.dimensions) / 2.0; + + if (wantDebug) { + print("Checking properties: " + properties.id + " " + " - Half Diagonal:" + halfDiagonal); + } + // P P - Model + // /| A - Palm + // / | d B - unit vector toward tip + // / | X - base of the perpendicular line + // A---X----->B d - distance fom axis + // x x - distance from A + // + // |X-A| = (P-A).B + // X === A + ((P-A).B)B + // d = |P-X| + + var A = pickRay.origin; + var B = Vec3.normalize(pickRay.direction); + var P = properties.position; + + var x = Vec3.dot(Vec3.subtract(P, A), B); + + var angularSize = 2 * Math.atan(halfDiagonal / Vec3.distance(Camera.getPosition(), properties.position)) * + 180 / Math.PI; + + var sizeOK = (allowLargeModels || angularSize < MAX_ANGULAR_SIZE) && + (allowSmallModels || angularSize > MIN_ANGULAR_SIZE); + + if (0 < x && sizeOK) { + selectedEntityID = foundEntity; + orientation = MyAvatar.orientation; + intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation)); + + if (event.isShifted) { + particleExplorerTool.destroyWebView(); + } + if (properties.type !== "ParticleEffect") { + particleExplorerTool.destroyWebView(); + } + + if (!event.isShifted) { + selectionManager.setSelections([foundEntity]); + } else { + selectionManager.addEntity(foundEntity, true); } - } else { - var halfDiagonal = Vec3.length(properties.dimensions) / 2.0; if (wantDebug) { - print("Checking properties: " + properties.id + " " + " - Half Diagonal:" + halfDiagonal); + print("Model selected: " + foundEntity); } - // P P - Model - // /| A - Palm - // / | d B - unit vector toward tip - // / | X - base of the perpendicular line - // A---X----->B d - distance fom axis - // x x - distance from A - // - // |X-A| = (P-A).B - // X === A + ((P-A).B)B - // d = |P-X| + selectionDisplay.select(selectedEntityID, event); - var A = pickRay.origin; - var B = Vec3.normalize(pickRay.direction); - var P = properties.position; - - var x = Vec3.dot(Vec3.subtract(P, A), B); - - var angularSize = 2 * Math.atan(halfDiagonal / Vec3.distance(Camera.getPosition(), properties.position)) * - 180 / Math.PI; - - var sizeOK = (allowLargeModels || angularSize < MAX_ANGULAR_SIZE) && - (allowSmallModels || angularSize > MIN_ANGULAR_SIZE); - - if (0 < x && sizeOK) { - selectedEntityID = foundEntity; - orientation = MyAvatar.orientation; - intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation)); - - if (event.isShifted) { - particleExplorerTool.destroyWebView(); - } - if (properties.type !== "ParticleEffect") { - particleExplorerTool.destroyWebView(); - } - - if (!event.isShifted) { - selectionManager.setSelections([foundEntity]); - } else { - selectionManager.addEntity(foundEntity, true); - } - - if (wantDebug) { - print("Model selected: " + foundEntity); - } - selectionDisplay.select(selectedEntityID, event); - - if (Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)) { - cameraManager.enable(); - cameraManager.focus(selectionManager.worldPosition, - selectionManager.worldDimensions, - Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); - } + if (Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)) { + cameraManager.enable(); + cameraManager.focus(selectionManager.worldPosition, + selectionManager.worldDimensions, + Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); } } } else if (event.isRightButton) { @@ -1368,11 +1414,7 @@ function selectAllEtitiesInCurrentSelectionBox(keepIfTouching) { var localPosition = Vec3.multiplyQbyV(Quat.inverse(selectionManager.localRotation), Vec3.subtract(position, selectionManager.localPosition)); - return insideBox({ - x: 0, - y: 0, - z: 0 - }, selectionManager.localDimensions, localPosition); + return insideBox(Vec3.ZERO, selectionManager.localDimensions, localPosition); }; } for (var i = 0; i < entities.length; ++i) { @@ -1476,7 +1518,7 @@ function parentSelectedEntities() { return; } var parentCheck = false; - var lastEntityId = selectedEntities[selectedEntities.length-1]; + var lastEntityId = selectedEntities[selectedEntities.length - 1]; selectedEntities.forEach(function (id, index) { if (lastEntityId !== id) { var parentId = Entities.getEntityProperties(id, ["parentID"]).parentID; @@ -1489,7 +1531,7 @@ function parentSelectedEntities() { if (parentCheck) { Window.notify("Entities parented"); - }else { + } else { Window.notify("Entities are already parented to last"); } } else { @@ -1902,8 +1944,6 @@ function pushCommandForSelections(createdEntityData, deletedEntityData) { UndoStack.pushCommand(applyEntityProperties, undoData, applyEntityProperties, redoData); } -var ENTITY_PROPERTIES_URL = Script.resolvePath('html/entityProperties.html'); - var ServerScriptStatusMonitor = function(entityID, statusCallback) { var self = this; @@ -1947,13 +1987,14 @@ var PropertiesTool = function (opts) { var currentSelectedEntityID = null; var statusMonitor = null; - webView.setVisible(visible); - that.setVisible = function (newVisible) { visible = newVisible; - webView.setVisible(visible); + webView.setVisible(HMD.active && visible); + createToolsWindow.setVisible(!HMD.active && visible); }; + that.setVisible(false); + function updateScriptStatus(info) { info.type = "server_script_status"; webView.emitScriptEvent(JSON.stringify(info)); @@ -1982,7 +2023,7 @@ var PropertiesTool = function (opts) { statusMonitor = null; } currentSelectedEntityID = null; - } else if (currentSelectedEntityID != selectionManager.selections[0]) { + } else if (currentSelectedEntityID !== selectionManager.selections[0]) { if (statusMonitor !== null) { statusMonitor.stop(); } @@ -2008,11 +2049,14 @@ var PropertiesTool = function (opts) { selections.push(entity); } data.selections = selections; + webView.emitScriptEvent(JSON.stringify(data)); + createToolsWindow.emitScriptEvent(JSON.stringify(data)); } selectionManager.addEventListener(updateSelections); - webView.webEventReceived.connect(function (data) { + + var onWebEventReceived = function(data) { try { data = JSON.parse(data); } @@ -2034,16 +2078,8 @@ var PropertiesTool = function (opts) { } else if (data.properties) { if (data.properties.dynamic === false) { // this object is leaving dynamic, so we zero its velocities - data.properties.velocity = { - x: 0, - y: 0, - z: 0 - }; - data.properties.angularVelocity = { - x: 0, - y: 0, - z: 0 - }; + data.properties.velocity = Vec3.ZERO; + data.properties.angularVelocity = Vec3.ZERO; } if (data.properties.rotation !== undefined) { var rotation = data.properties.rotation; @@ -2171,7 +2207,11 @@ var PropertiesTool = function (opts) { } else if (data.type === "propertiesPageReady") { updateSelections(true); } - }); + }; + + createToolsWindow.webEventReceived.addListener(this, onWebEventReceived); + + webView.webEventReceived.connect(onWebEventReceived); return that; }; @@ -2186,6 +2226,8 @@ var PopupMenu = function () { var overlays = []; var overlayInfo = {}; + var visible = false; + var upColor = { red: 0, green: 0, @@ -2303,8 +2345,6 @@ var PopupMenu = function () { } }; - var visible = false; - self.setVisible = function (newVisible) { if (newVisible !== visible) { visible = newVisible; @@ -2358,7 +2398,7 @@ propertyMenu.onSelectMenuItem = function (name) { var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); -var particleExplorerTool = new ParticleExplorerTool(); +var particleExplorerTool = new ParticleExplorerTool(createToolsWindow); var selectedParticleEntityID = null; function selectParticleEntity(entityID) { @@ -2376,11 +2416,16 @@ function selectParticleEntity(entityID) { particleExplorerTool.setActiveParticleProperties(properties); // Switch to particle explorer - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.sendToQml({method: 'selectTab', params: {id: 'particle'}}); + var selectTabMethod = { method: 'selectTab', params: { id: 'particle' } }; + if (HMD.active) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.sendToQml(selectTabMethod); + } else { + createToolsWindow.sendToQml(selectTabMethod); + } } -entityListTool.webView.webEventReceived.connect(function (data) { +entityListTool.webView.webEventReceived.connect(function(data) { try { data = JSON.parse(data); } catch(e) { diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 3fda7588df..76ab3ef8f2 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -11,27 +11,64 @@ /* global EntityListTool, Tablet, selectionManager, Entities, Camera, MyAvatar, Vec3, Menu, Messages, cameraManager, MENU_EASE_ON_FOCUS, deleteSelectedEntities, toggleSelectedEntitiesLocked, toggleSelectedEntitiesVisible */ -EntityListTool = function(opts) { +EntityListTool = function() { var that = {}; + var CreateWindow = Script.require('../modules/createWindow.js'); + + var TITLE_OFFSET = 60; + var CREATE_TOOLS_WIDTH = 495; + var MAX_DEFAULT_CREATE_TOOLS_HEIGHT = 778; + var entityListWindow = new CreateWindow( + Script.resourcesPath() + "qml/hifi/tablet/EditEntityList.qml", + 'Entity List', + 'com.highfidelity.create.entityListWindow', + function () { + var windowHeight = Window.innerHeight - TITLE_OFFSET; + if (windowHeight > MAX_DEFAULT_CREATE_TOOLS_HEIGHT) { + windowHeight = MAX_DEFAULT_CREATE_TOOLS_HEIGHT; + } + return { + size: { + x: CREATE_TOOLS_WIDTH, + y: windowHeight + }, + position: { + x: Window.x, + y: Window.y + TITLE_OFFSET + } + }; + }, + false + ); + var webView = null; webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - webView.setVisible = function(value) {}; + webView.setVisible = function (value) { }; var filterInView = false; var searchRadius = 100; var visible = false; - webView.setVisible(visible); - that.webView = webView; that.setVisible = function(newVisible) { visible = newVisible; - webView.setVisible(visible); + webView.setVisible(HMD.active && visible); + entityListWindow.setVisible(!HMD.active && visible); }; + that.setVisible(false); + + function emitJSONScriptEvent(data) { + var dataString = JSON.stringify(data); + webView.emitScriptEvent(dataString); + if (entityListWindow.window) { + entityListWindow.window.emitScriptEvent(dataString); + } + } + that.toggleVisible = function() { that.setVisible(!visible); }; @@ -43,18 +80,16 @@ EntityListTool = function(opts) { selectedIDs.push(selectionManager.selections[i]); } - var data = { + emitJSONScriptEvent({ type: 'selectionUpdate', - selectedIDs: selectedIDs, - }; - webView.emitScriptEvent(JSON.stringify(data)); + selectedIDs: selectedIDs + }); }); - that.clearEntityList = function () { - var data = { + that.clearEntityList = function() { + emitJSONScriptEvent({ type: 'clearEntityList' - }; - webView.emitScriptEvent(JSON.stringify(data)); + }); }; that.removeEntities = function (deletedIDs, selectedIDs) { @@ -87,9 +122,9 @@ EntityListTool = function(opts) { if (!filterInView || Vec3.distance(properties.position, cameraPosition) <= searchRadius) { var url = ""; - if (properties.type == "Model") { + if (properties.type === "Model") { url = properties.modelURL; - } else if (properties.type == "Material") { + } else if (properties.type === "Material") { url = properties.materialURL; } entities.push({ @@ -103,7 +138,7 @@ EntityListTool = function(opts) { texturesCount: valueIfDefined(properties.renderInfo.texturesCount), texturesSize: valueIfDefined(properties.renderInfo.texturesSize), hasTransparent: valueIfDefined(properties.renderInfo.hasTransparent), - isBaked: properties.type == "Model" ? url.toLowerCase().endsWith(".baked.fbx") : false, + isBaked: properties.type === "Model" ? url.toLowerCase().endsWith(".baked.fbx") : false, drawCalls: valueIfDefined(properties.renderInfo.drawCalls), hasScript: properties.script !== "" }); @@ -115,12 +150,11 @@ EntityListTool = function(opts) { selectedIDs.push(selectionManager.selections[j]); } - var data = { + emitJSONScriptEvent({ type: "update", entities: entities, selectedIDs: selectedIDs, - }; - webView.emitScriptEvent(JSON.stringify(data)); + }); }; function onFileSaveChanged(filename) { @@ -133,15 +167,15 @@ EntityListTool = function(opts) { } } - webView.webEventReceived.connect(function(data) { + var onWebEventReceived = function(data) { try { data = JSON.parse(data); } catch(e) { - print("entityList.js: Error parsing JSON: " + e.name + " data " + data) + print("entityList.js: Error parsing JSON: " + e.name + " data " + data); return; } - if (data.type == "selectionUpdate") { + if (data.type === "selectionUpdate") { var ids = data.entityIds; var entityIDs = []; for (var i = 0; i < ids.length; i++) { @@ -154,20 +188,20 @@ EntityListTool = function(opts) { selectionManager.worldDimensions, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); } - } else if (data.type == "refresh") { + } else if (data.type === "refresh") { that.sendUpdate(); - } else if (data.type == "teleport") { + } else if (data.type === "teleport") { if (selectionManager.hasSelection()) { MyAvatar.position = selectionManager.worldPosition; } - } else if (data.type == "export") { + } else if (data.type === "export") { if (!selectionManager.hasSelection()) { Window.notifyEditError("No entities have been selected."); } else { Window.saveFileChanged.connect(onFileSaveChanged); Window.saveAsync("Select Where to Save", "", "*.json"); } - } else if (data.type == "pal") { + } else if (data.type === "pal") { var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates. selectionManager.selections.forEach(function (id) { var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy; @@ -184,24 +218,21 @@ EntityListTool = function(opts) { // No need to subscribe if we're just sending. Messages.sendMessage('com.highfidelity.pal', JSON.stringify({method: 'select', params: [dedupped, true, false]}), 'local'); } - } else if (data.type == "delete") { + } else if (data.type === "delete") { deleteSelectedEntities(); - } else if (data.type == "toggleLocked") { + } else if (data.type === "toggleLocked") { toggleSelectedEntitiesLocked(); - } else if (data.type == "toggleVisible") { + } else if (data.type === "toggleVisible") { toggleSelectedEntitiesVisible(); } else if (data.type === "filterInView") { filterInView = data.filterInView === true; } else if (data.type === "radius") { searchRadius = data.radius; } - }); + }; - // webView.visibleChanged.connect(function () { - // if (webView.visible) { - // that.sendUpdate(); - // } - // }); + webView.webEventReceived.connect(onWebEventReceived); + entityListWindow.webEventReceived.addListener(onWebEventReceived); return that; }; diff --git a/scripts/system/libraries/gridTool.js b/scripts/system/libraries/gridTool.js index 3be6ac0b00..690b4eb4b9 100644 --- a/scripts/system/libraries/gridTool.js +++ b/scripts/system/libraries/gridTool.js @@ -240,6 +240,7 @@ GridTool = function(opts) { var horizontalGrid = opts.horizontalGrid; var verticalGrid = opts.verticalGrid; + var createToolsWindow = opts.createToolsWindow; var listeners = []; var webView = null; @@ -247,13 +248,15 @@ GridTool = function(opts) { webView.setVisible = function(value) { }; horizontalGrid.addListener(function(data) { - webView.emitScriptEvent(JSON.stringify(data)); + var dataString = JSON.stringify(data); + webView.emitScriptEvent(dataString); + createToolsWindow.emitScriptEvent(dataString); if (selectionDisplay) { selectionDisplay.updateHandles(); } }); - webView.webEventReceived.connect(function(data) { + var webEventReceived = function(data) { try { data = JSON.parse(data); } catch (e) { @@ -282,14 +285,17 @@ GridTool = function(opts) { grid.setPosition(newPosition); } } - }); + }; + + webView.webEventReceived.connect(webEventReceived); + createToolsWindow.webEventReceived.addListener(webEventReceived); that.addListener = function(callback) { listeners.push(callback); }; that.setVisible = function(visible) { - webView.setVisible(visible); + webView.setVisible(HMD.active && visible); }; return that; diff --git a/scripts/system/modules/createWindow.js b/scripts/system/modules/createWindow.js new file mode 100644 index 0000000000..bf6231ddda --- /dev/null +++ b/scripts/system/modules/createWindow.js @@ -0,0 +1,150 @@ +"use strict"; + +// createWindow.js +// +// Created by Thijs Wenker on 6/1/18 +// +// Copyright 2018 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 +// + +var getWindowRect = function(settingsKey, defaultRect) { + var windowRect = Settings.getValue(settingsKey, defaultRect); + return windowRect; +}; + +var setWindowRect = function(settingsKey, position, size) { + Settings.setValue(settingsKey, { + position: position, + size: size + }); +}; + +var CallableEvent = (function() { + function CallableEvent() { + this.callbacks = []; + } + + CallableEvent.prototype = { + callbacks: null, + call: function () { + var callArguments = arguments; + this.callbacks.forEach(function(callbackObject) { + try { + callbackObject.callback.apply(callbackObject.context ? callbackObject.context : this, callArguments); + } catch (e) { + console.error('Call to CallableEvent callback failed!'); + } + }); + }, + addListener: function(contextOrCallback, callback) { + if (callback) { + this.callbacks.push({ + context: contextOrCallback, + callback: callback + }); + } else { + this.callbacks.push({ + callback: contextOrCallback + }); + } + }, + removeListener: function(callback) { + var foundIndex = -1; + this.callbacks.forEach(function (callbackObject, index) { + if (callbackObject.callback === callback) { + foundIndex = index; + } + }); + + if (foundIndex !== -1) { + this.callbacks.splice(foundIndex, 1); + } + } + }; + + return CallableEvent; +})(); + +module.exports = (function() { + function CreateWindow(qmlPath, title, settingsKey, defaultRect, createOnStartup) { + this.qmlPath = qmlPath; + this.title = title; + this.settingsKey = settingsKey; + this.defaultRect = defaultRect; + this.webEventReceived = new CallableEvent(); + this.fromQml = new CallableEvent(); + if (createOnStartup) { + this.createWindow(); + } + } + + CreateWindow.prototype = { + window: null, + createWindow: function() { + var defaultRect = this.defaultRect; + if (typeof this.defaultRect === "function") { + defaultRect = this.defaultRect(); + } + + var windowRect = getWindowRect(this.settingsKey, defaultRect); + this.window = Desktop.createWindow(this.qmlPath, { + title: this.title, + flags: Desktop.AlwaysOnTop | Desktop.ForceNative, + size: windowRect.size, + visible: true, + position: windowRect.position + }); + + var windowRectChanged = function () { + if (this.window.visible) { + setWindowRect(this.settingsKey, this.window.position, this.window.size); + } + }; + + this.window.sizeChanged.connect(this, windowRectChanged); + this.window.positionChanged.connect(this, windowRectChanged); + + this.window.webEventReceived.connect(this, function (data) { + this.webEventReceived.call(data); + }); + + this.window.fromQml.connect(this, function (data) { + this.fromQml.call(data); + }); + + Script.scriptEnding.connect(this, function() { + this.window.close(); + }); + }, + setVisible: function(visible) { + if (visible && !this.window) { + this.createWindow(); + } + + if (this.window) { + if (visible) { + this.window.show(); + } else { + this.window.visible = false; + } + } + }, + emitScriptEvent: function(data) { + if (this.window) { + this.window.emitScriptEvent(data); + } + }, + sendToQml: function(data) { + if (this.window) { + this.window.sendToQml(data); + } + }, + webEventReceived: null, + fromQml: null + }; + + return CreateWindow; +})(); diff --git a/scripts/system/particle_explorer/particleExplorerTool.js b/scripts/system/particle_explorer/particleExplorerTool.js index a1f06fda35..4e3b5cdad1 100644 --- a/scripts/system/particle_explorer/particleExplorerTool.js +++ b/scripts/system/particle_explorer/particleExplorerTool.js @@ -9,12 +9,12 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global window, alert, ParticleExplorerTool, EventBridge, dat, listenForSettingsUpdates,createVec3Folder,createQuatFolder,writeVec3ToInterface,writeDataToInterface*/ +/* global ParticleExplorerTool */ var PARTICLE_EXPLORER_HTML_URL = Script.resolvePath('particleExplorer.html'); -ParticleExplorerTool = function() { +ParticleExplorerTool = function (createToolsWindow) { var that = {}; that.activeParticleEntity = 0; that.activeParticleProperties = {}; @@ -23,8 +23,15 @@ ParticleExplorerTool = function() { that.webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); that.webView.setVisible = function(value) {}; that.webView.webEventReceived.connect(that.webEventReceived); + createToolsWindow.webEventReceived.addListener(this, that.webEventReceived); }; + function emitScriptEvent(data) { + var messageData = JSON.stringify(data); + that.webView.emitScriptEvent(messageData); + createToolsWindow.emitScriptEvent(messageData); + } + that.destroyWebView = function() { if (!that.webView) { return; @@ -32,17 +39,16 @@ ParticleExplorerTool = function() { that.activeParticleEntity = 0; that.activeParticleProperties = {}; - var messageData = { + emitScriptEvent({ messageType: "particle_close" - }; - that.webView.emitScriptEvent(JSON.stringify(messageData)); + }); }; function sendActiveParticleProperties() { - that.webView.emitScriptEvent(JSON.stringify({ + emitScriptEvent({ messageType: "particle_settings", currentProperties: that.activeParticleProperties - })); + }); } that.webEventReceived = function(message) { @@ -58,7 +64,6 @@ ParticleExplorerTool = function() { that.activeParticleProperties[key] = data.updatedSettings[key]; } } - var optionalProps = ["alphaStart", "alphaFinish", "radiusStart", "radiusFinish", "colorStart", "colorFinish"]; var fallbackProps = ["alpha", "particleRadius", "color"]; var entityProps = Entities.getEntityProperties(that.activeParticleProperties, optionalProps); From 3e6d54e83cf8e857c3144bc285d1cb41df03d0f1 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 28 Jun 2018 11:56:46 -0700 Subject: [PATCH 010/182] added all the parts for the step conditional to do: failsafe --- interface/src/avatar/MyAvatar.cpp | 53 ++++++++++++++++++++++++++++++- interface/src/avatar/MyAvatar.h | 2 ++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index a8246a24e2..5e2816b511 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3152,6 +3152,51 @@ float MyAvatar::computeStandingHeightMode(float newReading) { return _currentMode; } +static bool handDirectionMatchesHeadDirection(controller::Pose leftHand, controller::Pose rightHand, controller::Pose head) { + const float HANDS_VELOCITY_DIRECTION_THRESHOLD = 0.4f; + const float VELOCITY_EPSILON = 0.02f; + leftHand.velocity.y = 0.0f; + rightHand.velocity.y = 0.0f; + head.velocity.y = 0.0f; + float handDotHeadLeft = glm::dot(glm::normalize(leftHand.getVelocity()), glm::normalize(head.getVelocity())); + float handDotHeadRight = glm::dot(glm::normalize(rightHand.getVelocity()), glm::normalize(head.getVelocity())); + + return ((!leftHand.isValid() || ((handDotHeadLeft > HANDS_VELOCITY_DIRECTION_THRESHOLD) && (glm::length(leftHand.getVelocity()) > VELOCITY_EPSILON))) && + (!rightHand.isValid() || ((handDotHeadRight > HANDS_VELOCITY_DIRECTION_THRESHOLD) && (glm::length(rightHand.getVelocity()) > VELOCITY_EPSILON)))); +} + +static bool handAngularVelocityBelowThreshold(controller::Pose leftHand, controller::Pose rightHand) { + const float HANDS_ANGULAR_VELOCITY_THRESHOLD = 0.4f; + leftHand.angularVelocity.y = 0.0f; + rightHand.angularVelocity.y = 0.0f; + float leftHandXZAngularVelocity = glm::length(leftHand.getAngularVelocity()); + float rightHandXZAngularVelocity = glm::length(rightHand.getAngularVelocity()); + + return ((!leftHand.isValid() || (leftHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD)) && + (!rightHand.isValid() || (rightHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD))); +} + +static bool headVelocityGreaterThanThreshold(glm::vec3 headVelocity) { + const float VELOCITY_EPSILON = 0.02f; + const float HEAD_VELOCITY_THRESHOLD = 0.14f; + float headVelocityMagnitude = glm::length(headVelocity); + return headVelocityMagnitude > HEAD_VELOCITY_THRESHOLD; +} + +bool MyAvatar::isHeadLevel(controller::Pose head) { + const float AVERAGING_RATE = 0.03f; + const float HEAD_PITCH_TOLERANCE = 7.0f; + const float HEAD_ROLL_TOLERANCE = 7.0f; + + _averageHeadRotation = slerp(_averageHeadRotation, head.getRotation(), AVERAGING_RATE); + glm::vec3 averageHeadEulers = glm::degrees(safeEulerAngles(_averageHeadRotation)); + glm::vec3 currentHeadEulers = glm::degrees(safeEulerAngles(head.getRotation())); + glm::vec3 diffFromAverageEulers = averageHeadEulers - currentHeadEulers; + + return ((fabs(diffFromAverageEulers.x) < HEAD_PITCH_TOLERANCE) && (fabs(diffFromAverageEulers.z) < HEAD_ROLL_TOLERANCE)); + +} + float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -3387,7 +3432,13 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, if (!isActive(Horizontal) && (getForceActivateHorizontal() || (!withinBaseOfSupport(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation()) && headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()) && - isWithinThresholdHeightMode(myAvatar.computeStandingHeightMode(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y), myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y)))) { + isWithinThresholdHeightMode(myAvatar.computeStandingHeightMode(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y), myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y) && + handDirectionMatchesHeadDirection(myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && + handAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND)) && + headVelocityGreaterThanThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getVelocity()) && + myAvatar.isHeadLevel(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD))))) { + + qCDebug(interfaceapp) << "----------------------------------------over the base of support"; activate(Horizontal); setForceActivateHorizontal(false); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index dddc229171..1257e62548 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1026,6 +1026,7 @@ public: bool isReadyForPhysics() const; float computeStandingHeightMode(float newReading); + bool isHeadLevel(controller::Pose head); //bool isWithinThresholdHeightMode(float newReading); public slots: @@ -1638,6 +1639,7 @@ private: int _heightModeArray[SIZE_OF_MODE_ARRAY]; int _currentMode = 0; bool _resetMode = false; + glm::quat _averageHeadRotation = glm::quat(0.0f,0.0f,0.0f,0.0f); }; From 15a17ae32a6a2d5280be16a3c6be380fdb19e4e2 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 28 Jun 2018 18:36:13 -0700 Subject: [PATCH 011/182] finished the step detection, need to finish the failsafe --- interface/src/avatar/MyAvatar.cpp | 193 +++++++++++++++++++----------- interface/src/avatar/MyAvatar.h | 4 +- 2 files changed, 124 insertions(+), 73 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5e2816b511..95e5ca970a 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3082,104 +3082,147 @@ static bool isInsideLine(glm::vec3 a, glm::vec3 b, glm::vec3 c) { return (((b.x - a.x) * (c.z - a.z) - (b.z - a.z) * (c.x - a.x)) > 0); } -static bool withinBaseOfSupport(glm::vec3 position) { +static bool withinBaseOfSupport(controller::Pose head) { float userScale = 1.0f; - const float DEFAULT_LATERAL = 1.10f; - const float DEFAULT_ANTERIOR = 1.04f; - const float DEFAULT_POSTERIOR = 1.06f; + const float DEFAULT_LATERAL = 0.10f; + const float DEFAULT_ANTERIOR = 0.04f; + const float DEFAULT_POSTERIOR = 0.06f; glm::vec3 frontLeft(-DEFAULT_LATERAL, 0.0f, -DEFAULT_ANTERIOR); glm::vec3 frontRight(DEFAULT_LATERAL, 0.0f, -DEFAULT_ANTERIOR); glm::vec3 backLeft(-DEFAULT_LATERAL, 0.0f, DEFAULT_POSTERIOR); glm::vec3 backRight(DEFAULT_LATERAL, 0.0f, DEFAULT_POSTERIOR); - bool withinFrontBase = isInsideLine(userScale * frontLeft, userScale * frontRight, position); - bool withinBackBase = isInsideLine(userScale * backRight, userScale * backLeft, position); - bool withinLateralBase = (isInsideLine(userScale * frontRight, userScale * backRight, position) && - isInsideLine(userScale * backLeft, userScale * frontLeft, position)); - return (withinFrontBase && withinBackBase && withinLateralBase); + bool isWithinSupport = false; + if (head.isValid()) { + bool withinFrontBase = isInsideLine(userScale * frontLeft, userScale * frontRight, head.getTranslation()); + bool withinBackBase = isInsideLine(userScale * backRight, userScale * backLeft, head.getTranslation()); + bool withinLateralBase = (isInsideLine(userScale * frontRight, userScale * backRight, head.getTranslation()) && + isInsideLine(userScale * backLeft, userScale * frontLeft, head.getTranslation())); + isWithinSupport = (withinFrontBase && withinBackBase && withinLateralBase); + } + qCDebug(interfaceapp) << "within base of support " << isWithinSupport; + return isWithinSupport; } -static bool headAngularVelocityBelowThreshold(glm::vec3 angularVelocity) { +static bool headAngularVelocityBelowThreshold(controller::Pose head) { const float ANGULAR_VELOCITY_THRESHOLD = 0.3f; - glm::vec3 xzPlaneAngularVelocity(angularVelocity.x, 0.0f, angularVelocity.z); + glm::vec3 xzPlaneAngularVelocity(0.0f, 0.0f, 0.0f); + if (head.isValid()) { + xzPlaneAngularVelocity.x = head.getAngularVelocity().x; + xzPlaneAngularVelocity.z = head.getAngularVelocity().z; + } float magnitudeAngularVelocity = glm::length(xzPlaneAngularVelocity); bool isBelowThreshold = (magnitudeAngularVelocity < ANGULAR_VELOCITY_THRESHOLD); - qCDebug(interfaceapp) << "magnitude " << magnitudeAngularVelocity - << "head velocity below threshold is: " << isBelowThreshold; - qCDebug(interfaceapp) << "ang vel values x: " << angularVelocity.x << " y: " << angularVelocity.y - << " z: " << angularVelocity.z; + + qCDebug(interfaceapp) << "head angular velocity " << isBelowThreshold; return isBelowThreshold; } -static bool isWithinThresholdHeightMode(float newReading, float newMode) { +static bool isWithinThresholdHeightMode(controller::Pose head, float newMode) { + const float MODE_HEIGHT_THRESHOLD = -0.02f; - return (newReading - newMode) > MODE_HEIGHT_THRESHOLD; + bool isWithinThreshold = true; + if (head.isValid()) { + isWithinThreshold = (head.getTranslation().y - newMode) > MODE_HEIGHT_THRESHOLD; + } + qCDebug(interfaceapp) << "height threshold " << isWithinThreshold; + return isWithinThreshold; } -float MyAvatar::computeStandingHeightMode(float newReading) { +float MyAvatar::computeStandingHeightMode(controller::Pose head) { const float CENTIMETERS_PER_METER = 100.0f; const float MODE_CORRECTION_FACTOR = 0.02f; + //qCDebug(interfaceapp) << "new reading is " << newReading << " as an integer " << (int)(newReading * CENTIMETERS_PER_METER); + if (head.isValid()) { + float newReading = head.getTranslation().y; + //first add the number to the mode array + for (int i = 0; i < (SIZE_OF_MODE_ARRAY - 1); i++) { + _heightModeArray[i] = _heightModeArray[i + 1]; + } + _heightModeArray[SIZE_OF_MODE_ARRAY - 1] = (int)(newReading * CENTIMETERS_PER_METER); - //first add the number to the mode array - for (int i = 0; i < (SIZE_OF_MODE_ARRAY - 1); i++) { - _heightModeArray[i] = _heightModeArray[i + 1]; - } - _heightModeArray[SIZE_OF_MODE_ARRAY - 1] = (int)(newReading * CENTIMETERS_PER_METER); - - int greatestFrequency = 0; - int mode = 0; - std::map freq; - for (int j = 0; j < SIZE_OF_MODE_ARRAY; j++) { - freq[_heightModeArray[j]] += 1; - if ((freq[_heightModeArray[j]] > greatestFrequency) || - ((freq[_heightModeArray[j]] == SIZE_OF_MODE_ARRAY) && (_heightModeArray[j] > _currentMode))) { - greatestFrequency = freq[_heightModeArray[j]]; - mode = _heightModeArray[j]; - } - } - if (mode > _currentMode) { - return mode; - } else { - if (!_resetMode && qApp->isHMDMode()) { - _resetMode = true; - return (newReading - MODE_CORRECTION_FACTOR); + int greatestFrequency = 0; + int mode = 0; + std::map freq; + for (int j = 0; j < SIZE_OF_MODE_ARRAY; j++) { + freq[_heightModeArray[j]] += 1; + if ((freq[_heightModeArray[j]] > greatestFrequency) || + ((freq[_heightModeArray[j]] == SIZE_OF_MODE_ARRAY) && (_heightModeArray[j] > mode))) { + greatestFrequency = freq[_heightModeArray[j]]; + mode = _heightModeArray[j]; + } + } + float modeInMeters = ((float)mode) / CENTIMETERS_PER_METER; + if (modeInMeters > _currentMode) { + qCDebug(interfaceapp) << "new mode value set"; + _currentMode = modeInMeters; + } + else { + if (!_resetMode && qApp->isHMDMode()) { + _resetMode = true; + qCDebug(interfaceapp) << "reset mode value occurred"; + _currentMode = (newReading - MODE_CORRECTION_FACTOR); + } } } + //qCDebug(interfaceapp) << "_current mode is " << _currentMode; return _currentMode; } static bool handDirectionMatchesHeadDirection(controller::Pose leftHand, controller::Pose rightHand, controller::Pose head) { + const float HANDS_VELOCITY_DIRECTION_THRESHOLD = 0.4f; const float VELOCITY_EPSILON = 0.02f; - leftHand.velocity.y = 0.0f; - rightHand.velocity.y = 0.0f; - head.velocity.y = 0.0f; - float handDotHeadLeft = glm::dot(glm::normalize(leftHand.getVelocity()), glm::normalize(head.getVelocity())); - float handDotHeadRight = glm::dot(glm::normalize(rightHand.getVelocity()), glm::normalize(head.getVelocity())); + bool leftHandDirectionMatchesHead = true; + bool rightHandDirectionMatchesHead = true; + if (leftHand.isValid() && head.isValid()) { + leftHand.velocity.y = 0.0f; + float handDotHeadLeft = glm::dot(glm::normalize(leftHand.getVelocity()), glm::normalize(head.getVelocity())); + leftHandDirectionMatchesHead = ((handDotHeadLeft > HANDS_VELOCITY_DIRECTION_THRESHOLD) && (glm::length(leftHand.getVelocity()) > VELOCITY_EPSILON)); + qCDebug(interfaceapp) << "hand dot head left " << handDotHeadLeft; + } + if (rightHand.isValid() && head.isValid()) { + rightHand.velocity.y = 0.0f; + float handDotHeadRight = glm::dot(glm::normalize(rightHand.getVelocity()), glm::normalize(head.getVelocity())); + rightHandDirectionMatchesHead = ((handDotHeadRight > HANDS_VELOCITY_DIRECTION_THRESHOLD) && (glm::length(rightHand.getVelocity()) > VELOCITY_EPSILON)); + } + + qCDebug(interfaceapp) << "left right hand velocity "<< (leftHandDirectionMatchesHead && rightHandDirectionMatchesHead); + - return ((!leftHand.isValid() || ((handDotHeadLeft > HANDS_VELOCITY_DIRECTION_THRESHOLD) && (glm::length(leftHand.getVelocity()) > VELOCITY_EPSILON))) && - (!rightHand.isValid() || ((handDotHeadRight > HANDS_VELOCITY_DIRECTION_THRESHOLD) && (glm::length(rightHand.getVelocity()) > VELOCITY_EPSILON)))); + return leftHandDirectionMatchesHead && rightHandDirectionMatchesHead; } static bool handAngularVelocityBelowThreshold(controller::Pose leftHand, controller::Pose rightHand) { const float HANDS_ANGULAR_VELOCITY_THRESHOLD = 0.4f; - leftHand.angularVelocity.y = 0.0f; - rightHand.angularVelocity.y = 0.0f; - float leftHandXZAngularVelocity = glm::length(leftHand.getAngularVelocity()); - float rightHandXZAngularVelocity = glm::length(rightHand.getAngularVelocity()); - - return ((!leftHand.isValid() || (leftHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD)) && - (!rightHand.isValid() || (rightHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD))); + float leftHandXZAngularVelocity = 0.0f; + float rightHandXZAngularVelocity = 0.0f; + if (leftHand.isValid()) { + leftHand.angularVelocity.y = 0.0f; + leftHandXZAngularVelocity = glm::length(leftHand.getAngularVelocity()); + } + if (rightHand.isValid()) { + rightHand.angularVelocity.y = 0.0f; + rightHandXZAngularVelocity = glm::length(rightHand.getAngularVelocity()); + } + qCDebug(interfaceapp) << " hands angular velocity left " << (leftHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD) << " and right " << (rightHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD); + return ((leftHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD) && + (rightHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD)); } -static bool headVelocityGreaterThanThreshold(glm::vec3 headVelocity) { +static bool headVelocityGreaterThanThreshold(controller::Pose head) { const float VELOCITY_EPSILON = 0.02f; const float HEAD_VELOCITY_THRESHOLD = 0.14f; - float headVelocityMagnitude = glm::length(headVelocity); + float headVelocityMagnitude = 0.0f; + if (head.isValid()) { + //qCDebug(interfaceapp) << " head velocity " << head.getVelocity(); + headVelocityMagnitude = glm::length(head.getVelocity()); + } + qCDebug(interfaceapp) << " head velocity " << (headVelocityMagnitude > HEAD_VELOCITY_THRESHOLD); return headVelocityMagnitude > HEAD_VELOCITY_THRESHOLD; } @@ -3187,14 +3230,17 @@ bool MyAvatar::isHeadLevel(controller::Pose head) { const float AVERAGING_RATE = 0.03f; const float HEAD_PITCH_TOLERANCE = 7.0f; const float HEAD_ROLL_TOLERANCE = 7.0f; + glm::vec3 diffFromAverageEulers(0.0f, 0.0f, 0.0f); - _averageHeadRotation = slerp(_averageHeadRotation, head.getRotation(), AVERAGING_RATE); - glm::vec3 averageHeadEulers = glm::degrees(safeEulerAngles(_averageHeadRotation)); - glm::vec3 currentHeadEulers = glm::degrees(safeEulerAngles(head.getRotation())); - glm::vec3 diffFromAverageEulers = averageHeadEulers - currentHeadEulers; + if (head.isValid()) { + _averageHeadRotation = slerp(_averageHeadRotation, head.getRotation(), AVERAGING_RATE); + glm::vec3 averageHeadEulers = glm::degrees(safeEulerAngles(_averageHeadRotation)); + glm::vec3 currentHeadEulers = glm::degrees(safeEulerAngles(head.getRotation())); + diffFromAverageEulers = averageHeadEulers - currentHeadEulers; + } + qCDebug(interfaceapp) << " diff from average eulers x " << (fabs(diffFromAverageEulers.x) < HEAD_PITCH_TOLERANCE) << " and z " << (fabs(diffFromAverageEulers.z) < HEAD_ROLL_TOLERANCE); return ((fabs(diffFromAverageEulers.x) < HEAD_PITCH_TOLERANCE) && (fabs(diffFromAverageEulers.z) < HEAD_ROLL_TOLERANCE)); - } float MyAvatar::getUserHeight() const { @@ -3421,25 +3467,30 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, } else { // this is where we put the code for the stepping. // we do not have hmd lean enabled and we are looking for a step via our criteria. - qCDebug(interfaceapp) << "hmd lean is off"; + //qCDebug(interfaceapp) << "hmd lean is off"; if (!isActive(Rotation) && getForceActivateRotation()) { activate(Rotation); setForceActivateRotation(false); } - headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()); - float temp = myAvatar.computeStandingHeightMode(0.01f); + //compute the mode each frame + float theMode = myAvatar.computeStandingHeightMode(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)); + + //qCDebug(interfaceapp) << " y value head " << headPositionYAvatarFrame; + //headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()); + //float temp = myAvatar.computeStandingHeightMode(0.01f); if (!isActive(Horizontal) && (getForceActivateHorizontal() || - (!withinBaseOfSupport(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation()) && - headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()) && - isWithinThresholdHeightMode(myAvatar.computeStandingHeightMode(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y), myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y) && + (!withinBaseOfSupport(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && + headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && + isWithinThresholdHeightMode(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD), theMode) && handDirectionMatchesHeadDirection(myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && handAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND)) && - headVelocityGreaterThanThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getVelocity()) && - myAvatar.isHeadLevel(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD))))) { + headVelocityGreaterThanThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && + myAvatar.isHeadLevel(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) + ))) { - qCDebug(interfaceapp) << "----------------------------------------over the base of support"; + qCDebug(interfaceapp) << "----------------------------------------take a step--------------------------------------"; activate(Horizontal); setForceActivateHorizontal(false); } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 1257e62548..6e2daab611 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1025,7 +1025,7 @@ public: bool isReadyForPhysics() const; - float computeStandingHeightMode(float newReading); + float computeStandingHeightMode(controller::Pose head); bool isHeadLevel(controller::Pose head); //bool isWithinThresholdHeightMode(float newReading); @@ -1637,7 +1637,7 @@ private: static const int SIZE_OF_MODE_ARRAY = 50; bool _haveReceivedHeightLimitsFromDomain = { false }; int _heightModeArray[SIZE_OF_MODE_ARRAY]; - int _currentMode = 0; + float _currentMode = 0; bool _resetMode = false; glm::quat _averageHeadRotation = glm::quat(0.0f,0.0f,0.0f,0.0f); From d8474f1ec408668b77ad958b2ff33fb27b94a4e5 Mon Sep 17 00:00:00 2001 From: Angus Antley Date: Fri, 29 Jun 2018 05:44:24 +0100 Subject: [PATCH 012/182] added rotate html file --- scripts/developer/rotateRecenterApp.html | 171 +++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 scripts/developer/rotateRecenterApp.html diff --git a/scripts/developer/rotateRecenterApp.html b/scripts/developer/rotateRecenterApp.html new file mode 100644 index 0000000000..1ccb54e4a3 --- /dev/null +++ b/scripts/developer/rotateRecenterApp.html @@ -0,0 +1,171 @@ + + + + + Rotate App + + + + + + + + +
+
Rotate App
+
+
+
+
+ + +
+
+
+ + +
+
+ +
+
+ + + + + + From c9c222a2532fc7a397ebf18004265a91a5a535b7 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 29 Jun 2018 11:33:42 -0700 Subject: [PATCH 013/182] added the failsafe code to the stepping --- interface/src/avatar/MyAvatar.cpp | 37 +++++++++++++++++++------------ 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 95e5ca970a..ee1b775ea3 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3102,7 +3102,7 @@ static bool withinBaseOfSupport(controller::Pose head) { isInsideLine(userScale * backLeft, userScale * frontLeft, head.getTranslation())); isWithinSupport = (withinFrontBase && withinBackBase && withinLateralBase); } - qCDebug(interfaceapp) << "within base of support " << isWithinSupport; + //qCDebug(interfaceapp) << "within base of support " << isWithinSupport; return isWithinSupport; } @@ -3116,7 +3116,7 @@ static bool headAngularVelocityBelowThreshold(controller::Pose head) { float magnitudeAngularVelocity = glm::length(xzPlaneAngularVelocity); bool isBelowThreshold = (magnitudeAngularVelocity < ANGULAR_VELOCITY_THRESHOLD); - qCDebug(interfaceapp) << "head angular velocity " << isBelowThreshold; + //qCDebug(interfaceapp) << "head angular velocity " << isBelowThreshold; return isBelowThreshold; } @@ -3128,7 +3128,7 @@ static bool isWithinThresholdHeightMode(controller::Pose head, float newMode) { if (head.isValid()) { isWithinThreshold = (head.getTranslation().y - newMode) > MODE_HEIGHT_THRESHOLD; } - qCDebug(interfaceapp) << "height threshold " << isWithinThreshold; + //qCDebug(interfaceapp) << "height threshold " << isWithinThreshold; return isWithinThreshold; } @@ -3143,7 +3143,7 @@ float MyAvatar::computeStandingHeightMode(controller::Pose head) { for (int i = 0; i < (SIZE_OF_MODE_ARRAY - 1); i++) { _heightModeArray[i] = _heightModeArray[i + 1]; } - _heightModeArray[SIZE_OF_MODE_ARRAY - 1] = (int)(newReading * CENTIMETERS_PER_METER); + _heightModeArray[SIZE_OF_MODE_ARRAY - 1] = glm::floor(newReading * CENTIMETERS_PER_METER); int greatestFrequency = 0; int mode = 0; @@ -3165,7 +3165,8 @@ float MyAvatar::computeStandingHeightMode(controller::Pose head) { if (!_resetMode && qApp->isHMDMode()) { _resetMode = true; qCDebug(interfaceapp) << "reset mode value occurred"; - _currentMode = (newReading - MODE_CORRECTION_FACTOR); + float modeInCentimeters = glm::floor((newReading - MODE_CORRECTION_FACTOR)*CENTIMETERS_PER_METER); + _currentMode = modeInCentimeters/CENTIMETERS_PER_METER; } } } @@ -3183,7 +3184,7 @@ static bool handDirectionMatchesHeadDirection(controller::Pose leftHand, control leftHand.velocity.y = 0.0f; float handDotHeadLeft = glm::dot(glm::normalize(leftHand.getVelocity()), glm::normalize(head.getVelocity())); leftHandDirectionMatchesHead = ((handDotHeadLeft > HANDS_VELOCITY_DIRECTION_THRESHOLD) && (glm::length(leftHand.getVelocity()) > VELOCITY_EPSILON)); - qCDebug(interfaceapp) << "hand dot head left " << handDotHeadLeft; + //qCDebug(interfaceapp) << "hand dot head left " << handDotHeadLeft; } if (rightHand.isValid() && head.isValid()) { rightHand.velocity.y = 0.0f; @@ -3191,7 +3192,7 @@ static bool handDirectionMatchesHeadDirection(controller::Pose leftHand, control rightHandDirectionMatchesHead = ((handDotHeadRight > HANDS_VELOCITY_DIRECTION_THRESHOLD) && (glm::length(rightHand.getVelocity()) > VELOCITY_EPSILON)); } - qCDebug(interfaceapp) << "left right hand velocity "<< (leftHandDirectionMatchesHead && rightHandDirectionMatchesHead); + //qCDebug(interfaceapp) << "left right hand velocity "<< (leftHandDirectionMatchesHead && rightHandDirectionMatchesHead); return leftHandDirectionMatchesHead && rightHandDirectionMatchesHead; @@ -3209,7 +3210,7 @@ static bool handAngularVelocityBelowThreshold(controller::Pose leftHand, control rightHand.angularVelocity.y = 0.0f; rightHandXZAngularVelocity = glm::length(rightHand.getAngularVelocity()); } - qCDebug(interfaceapp) << " hands angular velocity left " << (leftHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD) << " and right " << (rightHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD); + //qCDebug(interfaceapp) << " hands angular velocity left " << (leftHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD) << " and right " << (rightHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD); return ((leftHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD) && (rightHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD)); } @@ -3222,7 +3223,7 @@ static bool headVelocityGreaterThanThreshold(controller::Pose head) { //qCDebug(interfaceapp) << " head velocity " << head.getVelocity(); headVelocityMagnitude = glm::length(head.getVelocity()); } - qCDebug(interfaceapp) << " head velocity " << (headVelocityMagnitude > HEAD_VELOCITY_THRESHOLD); + //qCDebug(interfaceapp) << " head velocity " << (headVelocityMagnitude > HEAD_VELOCITY_THRESHOLD); return headVelocityMagnitude > HEAD_VELOCITY_THRESHOLD; } @@ -3238,7 +3239,7 @@ bool MyAvatar::isHeadLevel(controller::Pose head) { glm::vec3 currentHeadEulers = glm::degrees(safeEulerAngles(head.getRotation())); diffFromAverageEulers = averageHeadEulers - currentHeadEulers; } - qCDebug(interfaceapp) << " diff from average eulers x " << (fabs(diffFromAverageEulers.x) < HEAD_PITCH_TOLERANCE) << " and z " << (fabs(diffFromAverageEulers.z) < HEAD_ROLL_TOLERANCE); + //qCDebug(interfaceapp) << " diff from average eulers x " << (fabs(diffFromAverageEulers.x) < HEAD_PITCH_TOLERANCE) << " and z " << (fabs(diffFromAverageEulers.z) < HEAD_ROLL_TOLERANCE); return ((fabs(diffFromAverageEulers.x) < HEAD_PITCH_TOLERANCE) && (fabs(diffFromAverageEulers.z) < HEAD_ROLL_TOLERANCE)); } @@ -3475,6 +3476,9 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, //compute the mode each frame float theMode = myAvatar.computeStandingHeightMode(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)); + //compute the average length of the spine given the current mode + glm::vec3 defaultHipsPos = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); + float anatomicalHeadToHipsDistance = fabs(theMode - defaultHipsPos.y); //qCDebug(interfaceapp) << " y value head " << headPositionYAvatarFrame; //headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()); @@ -3486,13 +3490,18 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, handDirectionMatchesHeadDirection(myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && handAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND)) && headVelocityGreaterThanThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && - myAvatar.isHeadLevel(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) - ))) { - - + myAvatar.isHeadLevel(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD))))) { qCDebug(interfaceapp) << "----------------------------------------take a step--------------------------------------"; activate(Horizontal); setForceActivateHorizontal(false); + } else { + const float SPINE_STRETCH_LIMIT = 0.07f; + const float FAILSAFE_TIMEOUT = 2.5f; + if (!isActive(Horizontal) && + (glm::length(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation() - defaultHipsPos) > (anatomicalHeadToHipsDistance + SPINE_STRETCH_LIMIT))) { + myAvatar._resetMode = false; + activate(Horizontal); + } } if (!isActive(Vertical) && getForceActivateVertical()) { activate(Vertical); From 44d9edec3cdf2cfc4cd900b57c9c8b5abf3eb4ca Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 29 Jun 2018 14:42:23 -0700 Subject: [PATCH 014/182] added the properties for filter length and rotation threshold to MyAvatar class This will help us test the right combo for head based turning --- interface/src/avatar/MyAvatar.cpp | 14 ++++++++++++-- interface/src/avatar/MyAvatar.h | 8 ++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index ee1b775ea3..198d2e6f3b 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -395,7 +395,8 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. - const float HMD_FACING_TIMESCALE = 4.0f; // very slow average + const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); //4.0f; // very slow average + qCDebug(interfaceapp) << "rotation recenter value is " << HMD_FACING_TIMESCALE; float tau = deltaTime / HMD_FACING_TIMESCALE; _headControllerFacingMovingAverage = lerp(_headControllerFacingMovingAverage, _headControllerFacing, tau); @@ -2112,6 +2113,14 @@ void MyAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) _headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement); } +void MyAvatar::setRotationRecenterFilterLength(float length) { + _rotationRecenterFilterLength = length; +} + +void MyAvatar::setRotationThreshold(float angleRadians) { + _rotationThreshold = angleRadians; +} + void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys float targetSpeed = getDriveKey(YAW) * _yawSpeed; @@ -3410,7 +3419,8 @@ void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) { bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { - const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees + qCDebug(interfaceapp) << "rotation threshold is " << myAvatar.getRotationThreshold(); + const float FOLLOW_ROTATION_THRESHOLD = cosf(myAvatar.getRotationThreshold()); //cosf(PI / 6.0f); // 30 degrees glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix); return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 6e2daab611..ee7028cfe1 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -195,6 +195,8 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool hasProceduralBlinkFaceMovement READ getHasProceduralBlinkFaceMovement WRITE setHasProceduralBlinkFaceMovement) Q_PROPERTY(bool hasProceduralEyeFaceMovement READ getHasProceduralEyeFaceMovement WRITE setHasProceduralEyeFaceMovement) Q_PROPERTY(bool hasAudioEnabledFaceMovement READ getHasAudioEnabledFaceMovement WRITE setHasAudioEnabledFaceMovement) + Q_PROPERTY(float rotationRecenterFilterLength READ getRotationRecenterFilterLength WRITE setRotationRecenterFilterLength) + Q_PROPERTY(float rotationThreshold READ getRotationThreshold WRITE setRotationThreshold) //TODO: make gravity feature work Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setGravity) Q_PROPERTY(glm::vec3 leftHandPosition READ getLeftHandPosition) @@ -1400,6 +1402,10 @@ private: bool getHasProceduralEyeFaceMovement() const override { return _headData->getHasProceduralEyeFaceMovement(); } void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement); bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } + void setRotationRecenterFilterLength(float length); + float getRotationRecenterFilterLength() const { return _rotationRecenterFilterLength; } + void setRotationThreshold(float angleRadians); + float getRotationThreshold() const { return _rotationThreshold; } bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; @@ -1509,6 +1515,8 @@ private: float _hmdRollControlDeadZone { ROLL_CONTROL_DEAD_ZONE_DEFAULT }; float _hmdRollControlRate { ROLL_CONTROL_RATE_DEFAULT }; std::atomic _hasScriptedBlendShapes { false }; + std::atomic _rotationRecenterFilterLength { 4.0f }; + std::atomic _rotationThreshold { 0.5235f }; // 30 degrees in radians // working copy -- see AvatarData for thread-safe _sensorToWorldMatrixCache, used for outward facing access glm::mat4 _sensorToWorldMatrix { glm::mat4() }; From bc1d2fa87b1c720b6261bd576ab7b962f8832337 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 29 Jun 2018 14:59:34 -0700 Subject: [PATCH 015/182] added the rotate app for testing the head driven turning --- scripts/developer/rotateApp.js | 277 +++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 scripts/developer/rotateApp.js diff --git a/scripts/developer/rotateApp.js b/scripts/developer/rotateApp.js new file mode 100644 index 0000000000..111530cf78 --- /dev/null +++ b/scripts/developer/rotateApp.js @@ -0,0 +1,277 @@ +/* global Script, Vec3, MyAvatar, Tablet, Messages, Quat, +DebugDraw, Mat4, Entities, Xform, Controller, Camera, console, document*/ + +Script.registerValue("ROTATEAPP", true); + +var TABLET_BUTTON_NAME = "ROTATE"; +var CHANGE_OF_BASIS_ROTATION = { x: 0, y: 1, z: 0, w: 0 }; +var HEAD_TURN_THRESHOLD = 25.0; +var LOADING_DELAY = 500; +var AVERAGING_RATE = 0.03; + +var activated = false; +var documentLoaded = false; +var headPoseAverageOrientation = { x: 0, y: 0, z: 0, w: 1 }; +var hipToLeftHandAverage = 0.0; // { x: 0, y: 0, z: 0, w: 1 }; +var hipToRightHandAverage = 0.0; // { x: 0, y: 0, z: 0, w: 1 }; +var averageAzimuth = 0.0; +var hipsPositionRigSpace = { x: 0, y: 0, z: 0 }; +var spine2PositionRigSpace = { x: 0, y: 0, z: 0 }; +var hipsRotationRigSpace = { x: 0, y: 0, z: 0, w: 1 }; +var spine2RotationRigSpace = { x: 0, y: 0, z: 0, w: 1 }; +var spine2Rotation = { x: 0, y: 0, z: 0, w: 1 }; + +var ikTypes = { + RotationAndPosition: 0, + RotationOnly: 1, + HmdHead: 2, + HipsRelativeRotationAndPosition: 3, + Spline: 4, + Unknown: 5 +}; + + +var ANIM_VARS = [ + //"headType", + "spine2Type", + //"hipsType", + "spine2Position", + "spine2Rotation", + //"hipsPosition", + //"hipsRotation" +]; + +var handlerId = MyAvatar.addAnimationStateHandler(function (props) { + //print("in callback"); + //print("props spine2 pos: " + props.spine2Position.x + " " + props.spine2Position.y + " " + props.spine2Position.z); + //print("props hip pos: " + props.hipsPosition.x + " " + props.hipsPosition.y + " " + props.hipsPosition.z); + var result = {}; + //{x:0,y:0,z:0} + //result.headType = ikTypes.HmdHead; + //result.hipsType = ikTypes.RotationAndPosition; + //result.hipsPosition = hipsPositionRigSpace; // { x: 0, y: 0, z: 0 }; + //result.hipsRotation = hipsRotationRigSpace;//{ x: 0, y: 0, z: 0, w: 1 }; // + result.spine2Type = ikTypes.Spline; + result.spine2Position = spine2PositionRigSpace; // { x: 0, y: 1.3, z: 0 }; + result.spine2Rotation = spine2Rotation; + + return result; +}, ANIM_VARS); + +// define state readings constructor +function StateReading(headPose, rhandPose, lhandPose, diffFromAverageEulers) { + this.headPose = headPose; + this.rhandPose = rhandPose; + this.lhandPose = lhandPose; + this.diffFromAverageEulers = diffFromAverageEulers; +} + +// define current state readings object for holding tracker readings and current differences from averages +var currentStateReadings = new StateReading(Controller.getPoseValue(Controller.Standard.Head), + Controller.getPoseValue(Controller.Standard.RightHand), + Controller.getPoseValue(Controller.Standard.LeftHand), + { x: 0, y: 0, z: 0 }); + +// declare the checkbox constructor +function AppCheckbox(type,id,eventType,isChecked) { + this.type = type; + this.id = id; + this.eventType = eventType; + this.data = {value: isChecked}; +} + +// declare the html slider constructor +function AppProperty(name, type, eventType, signalType, setFunction, initValue, convertToThreshold, convertToSlider, signalOn) { + this.name = name; + this.type = type; + this.eventType = eventType; + this.signalType = signalType; + this.setValue = setFunction; + this.value = initValue; + this.get = function () { + return this.value; + }; + this.convertToThreshold = convertToThreshold; + this.convertToSlider = convertToSlider; +} + +var HTML_URL = Script.resolvePath("file:///c:/dev/high fidelity/hifi/scripts/developer/stepAppExtra.html"); +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + +function manageClick() { + if (activated) { + tablet.gotoHomeScreen(); + } else { + tablet.gotoWebScreen(HTML_URL); + } +} + +var tabletButton = tablet.addButton({ + text: TABLET_BUTTON_NAME, + icon: Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/foot.svg"), + activeIcon: Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/foot.svg") +}); + +function onKeyPress(event) { + if (event.text === "'") { + // when the sensors are reset, then reset the mode. + } +} +/* +function onWebEventReceived(msg) { + var message = JSON.parse(msg); + print(" we have a message from html dialog " + message.type); + propArray.forEach(function (prop) { + if (prop.eventType === message.type) { + prop.setValue(prop.convertToThreshold(message.data.value)); + print("message from " + prop.name); + // break; + } + }); + checkBoxArray.forEach(function(cbox) { + if (cbox.eventType === message.type) { + cbox.data.value = message.data.value; + // break; + } + }); +} + +function initAppForm() { + print("step app is loaded: " + documentLoaded); + propArray.forEach(function (prop) { + print(prop.name); + tablet.emitScriptEvent(JSON.stringify({ + "type": "trigger", + "id": prop.signalType, + "data": { "value": "green" } + })); + tablet.emitScriptEvent(JSON.stringify({ + "type": "slider", + "id": prop.name, + "data": { "value": prop.convertToSlider(prop.value) } + })); + }); + checkBoxArray.forEach(function(cbox) { + tablet.emitScriptEvent(JSON.stringify({ + "type": "checkboxtick", + "id": cbox.id, + "data": { value: cbox.data.value } + })); + }); + +} + + +function onScreenChanged(type, url) { + print("Screen changed"); + if (type === "Web" && url === HTML_URL) { + if (!activated) { + // hook up to event bridge + tablet.webEventReceived.connect(onWebEventReceived); + print("after connect web event"); + MyAvatar.hmdLeanRecenterEnabled = false; + Script.setTimeout(initAppForm, LOADING_DELAY); + } + activated = true; + } else { + if (activated) { + // disconnect from event bridge + tablet.webEventReceived.disconnect(onWebEventReceived); + } + activated = false; + } +} +*/ +function update(dt) { + + // Update head information + currentStateReadings.headPose = Controller.getPoseValue(Controller.Standard.Head); + currentStateReadings.rhandPose = Controller.getPoseValue(Controller.Standard.RightHand); + currentStateReadings.lhandPose = Controller.getPoseValue(Controller.Standard.LeftHand); + + // get the position of the hips and set them for the anim vars callback. + var spine2PositionAvatarSpace = MyAvatar.getAbsoluteJointTranslationInObjectFrame(MyAvatar.getJointIndex("Spine2")); + var spine2RotationAvatarSpace = MyAvatar.getAbsoluteJointRotationInObjectFrame(MyAvatar.getJointIndex("Spine2")); + var hipsPositionAvatarSpace = MyAvatar.getAbsoluteJointTranslationInObjectFrame(MyAvatar.getJointIndex("Hips")); + var hipsRotationAvatarSpace = MyAvatar.getAbsoluteJointRotationInObjectFrame(MyAvatar.getJointIndex("Hips")); + hipsPositionRigSpace = Vec3.multiplyQbyV(Quat.inverse(CHANGE_OF_BASIS_ROTATION), hipsPositionAvatarSpace); + spine2PositionRigSpace = Vec3.multiplyQbyV(Quat.inverse(CHANGE_OF_BASIS_ROTATION), spine2PositionAvatarSpace); + hipsRotationRigSpace = Quat.multiply(CHANGE_OF_BASIS_ROTATION, hipsRotationAvatarSpace); + spine2RotationRigSpace = Quat.multiply(CHANGE_OF_BASIS_ROTATION, spine2RotationAvatarSpace); + //print("hip position rig space" + hipsPositionAvatarSpace.x + " " + hipsPositionAvatarSpace.y + " " + hipsPositionAvatarSpace.z); + //print("hip rotation rig space x: " + hipsRotationRigSpace.x + " y: " + hipsRotationRigSpace.y + " z: " + hipsRotationRigSpace.z + " w: " + hipsRotationRigSpace.w); + // print("spine2 rotation rig space x: " + spine2RotationRigSpace.x + " y: " + spine2RotationRigSpace.y + " z: " + spine2RotationRigSpace.z + " w: " + spine2RotationRigSpace.w); + //print("spine2 position rig space" + spine2PositionRigSpace.x + " " + spine2PositionRigSpace.y + " " + spine2PositionRigSpace.z); + + var headPoseRigSpace = Quat.multiply(CHANGE_OF_BASIS_ROTATION, currentStateReadings.headPose.rotation); + headPoseAverageOrientation = Quat.slerp(headPoseAverageOrientation, headPoseRigSpace, AVERAGING_RATE); + var headPoseAverageEulers = Quat.safeEulerAngles(headPoseAverageOrientation); + + + if (((headPoseAverageEulers.y > HEAD_TURN_THRESHOLD) && (averageAzimuth > HEAD_TURN_THRESHOLD)) + || ((headPoseAverageEulers.y < -HEAD_TURN_THRESHOLD) && (averageAzimuth < -HEAD_TURN_THRESHOLD))) { + print("azimuth " + averageAzimuth); + print("headposeAverageEulers " + headPoseAverageEulers.y); + // Turn feet + print("rotate feet") + MyAvatar.triggerRotationRecenter(); + headPoseAverageOrientation = { x: 0, y: 0, z: 0, w: 1 }; + } + + // and the hand to hip vector to determine when to change head rotation. + // leftHandOrientation = Quat.multiply(MyAvatar.orientation, currentStateReadings.lhandPose.rotation); + // rightHandOrientation = Quat.multiply(MyAvatar.orientation, currentStateReadings.rhandPose.rotation); + var leftHandPositionRigSpace = Vec3.multiplyQbyV(Quat.inverse(CHANGE_OF_BASIS_ROTATION), currentStateReadings.lhandPose.translation); + var rightHandPositionRigSpace = Vec3.multiplyQbyV(Quat.inverse(CHANGE_OF_BASIS_ROTATION), currentStateReadings.rhandPose.translation); + + // Update angle from hips to hand, to be used for turning + var hipToLeftHandAngle = Vec3.orientedAngle({ x: 0, y: 0, z: 1 }, { x: leftHandPositionRigSpace.x, y: 0, z: leftHandPositionRigSpace.z }, { x: 0, y: 1, z: 0 }); + var hipToRightHandAngle = Vec3.orientedAngle({ x: 0, y: 0, z: 1 }, { x: rightHandPositionRigSpace.x, y: 0, z: rightHandPositionRigSpace.z }, { x: 0, y: 1, z: 0 }); + var hipToLeftHand = Quat.lookAtSimple({ x: 0, y: 0, z: 0 }, { x: leftHandPositionRigSpace.x, y: 0, z: leftHandPositionRigSpace.z }); + var hipToRightHand = Quat.lookAtSimple({ x: 0, y: 0, z: 0 }, { x: rightHandPositionRigSpace.x, y: 0, z: rightHandPositionRigSpace.z }); + if (currentStateReadings.lhandPose.valid) { + hipToLeftHandAverage = hipToLeftHandAngle;// hipToLeftHandAverage * (1 - AVERAGING_RATE) + hipToLeftHandAngle * (AVERAGING_RATE); + } + if (currentStateReadings.rhandPose.valid) { + hipToRightHandAverage = hipToRightHandAngle;// hipToRightHandAverage * (1 - AVERAGING_RATE) + hipToRightHandAngle * (AVERAGING_RATE); + } + var leftRightMidpoint = (hipToLeftHandAverage + hipToRightHandAverage) / 2.0; + var FILTER_FACTOR = 0.01; + averageAzimuth = leftRightMidpoint; // * (FILTER_FACTOR) + averageAzimuth * (1 - FILTER_FACTOR); + spine2Rotation = Quat.angleAxis(averageAzimuth, { x: 0, y: 1, z: 0 }); + // print("spine 2 orientation x: " + spine2Rotation.x + " y: " + spine2Rotation.y + " z: " + spine2Rotation.z + " w: " + spine2Rotation.w); + // print("average azimuth " + averageAzimuth); + // print("hip to left hand angle " + hipToLeftHandAngle); + // print("hip to right hand angle " + hipToRightHandAngle); + // print("left right midpoint " + leftRightMidpoint); + // print("left hand position " + leftHandPositionRigSpace.x + " " + leftHandPositionRigSpace.y + " " + leftHandPositionRigSpace.z); + // print("right hand position " + rightHandPositionRigSpace.x + " " + rightHandPositionRigSpace.y + " " + rightHandPositionRigSpace.z); + // print("hip to left hand average " + hipToLeftHandAverage.x + " " + hipToLeftHandAverage.y + " " + hipToLeftHandAverage.z + " " + hipToLeftHandAverage.w); + +} + +function shutdownTabletApp() { + tablet.removeButton(tabletButton); + if (activated) { + // tablet.webEventReceived.disconnect(onWebEventReceived); + tablet.gotoHomeScreen(); + } + // tablet.screenChanged.disconnect(onScreenChanged); +} + +tabletButton.clicked.connect(manageClick); +// tablet.screenChanged.connect(onScreenChanged); + +Script.update.connect(update); + +Controller.keyPressEvent.connect(onKeyPress); + +Script.scriptEnding.connect(function () { + if (handlerId) { + print("removing animation state handler"); + handlerId = MyAvatar.removeAnimationStateHandler(handlerId); + } + MyAvatar.hmdLeanRecenterEnabled = true; + Script.update.disconnect(update); + shutdownTabletApp(); +}); \ No newline at end of file From 301658c71a7cd903bbef0d28f4b4e7a49f92f359 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 29 Jun 2018 15:50:08 -0700 Subject: [PATCH 016/182] getting the rotate app nearly ready to use --- scripts/developer/rotateApp.js | 105 +++++++++------------------------ 1 file changed, 28 insertions(+), 77 deletions(-) diff --git a/scripts/developer/rotateApp.js b/scripts/developer/rotateApp.js index 111530cf78..3b12ed8789 100644 --- a/scripts/developer/rotateApp.js +++ b/scripts/developer/rotateApp.js @@ -5,7 +5,8 @@ Script.registerValue("ROTATEAPP", true); var TABLET_BUTTON_NAME = "ROTATE"; var CHANGE_OF_BASIS_ROTATION = { x: 0, y: 1, z: 0, w: 0 }; -var HEAD_TURN_THRESHOLD = 25.0; +var HEAD_TURN_THRESHOLD = .5333; +var HEAD_TURN_FILTER_LENGTH = 4.0; var LOADING_DELAY = 500; var AVERAGING_RATE = 0.03; @@ -30,7 +31,7 @@ var ikTypes = { Unknown: 5 }; - +/* var ANIM_VARS = [ //"headType", "spine2Type", @@ -57,7 +58,7 @@ var handlerId = MyAvatar.addAnimationStateHandler(function (props) { return result; }, ANIM_VARS); - +*/ // define state readings constructor function StateReading(headPose, rhandPose, lhandPose, diffFromAverageEulers) { this.headPose = headPose; @@ -95,9 +96,25 @@ function AppProperty(name, type, eventType, signalType, setFunction, initValue, this.convertToSlider = convertToSlider; } -var HTML_URL = Script.resolvePath("file:///c:/dev/high fidelity/hifi/scripts/developer/stepAppExtra.html"); +var HTML_URL = Script.resolvePath("file:///c:/dev/hifi_fork/hifi/scripts/developer/rotateApp.html"); var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +// define the sliders +var filterLengthProperty = new AppProperty("#filterLength-slider", "slider", "onFilterLengthSlider", "filterSignal", + setAnteriorDistance, DEFAULT_ANTERIOR, function (num) { + return convertToMeters(num); + }, function (num) { + return convertToCentimeters(num); + },true); +var angleThresholdProperty = new AppProperty("#angleThreshold-slider", "slider", "onAngleThresholdSlider", "angleSignal", + setPosteriorDistance, DEFAULT_POSTERIOR, function (num) { + return convertToMeters(num); + }, function (num) { + return convertToCentimeters(num); + }, true); + + + function manageClick() { if (activated) { tablet.gotoHomeScreen(); @@ -117,7 +134,7 @@ function onKeyPress(event) { // when the sensors are reset, then reset the mode. } } -/* + function onWebEventReceived(msg) { var message = JSON.parse(msg); print(" we have a message from html dialog " + message.type); @@ -140,17 +157,13 @@ function initAppForm() { print("step app is loaded: " + documentLoaded); propArray.forEach(function (prop) { print(prop.name); - tablet.emitScriptEvent(JSON.stringify({ - "type": "trigger", - "id": prop.signalType, - "data": { "value": "green" } - })); tablet.emitScriptEvent(JSON.stringify({ "type": "slider", "id": prop.name, "data": { "value": prop.convertToSlider(prop.value) } })); }); + /* checkBoxArray.forEach(function(cbox) { tablet.emitScriptEvent(JSON.stringify({ "type": "checkboxtick", @@ -158,6 +171,7 @@ function initAppForm() { "data": { value: cbox.data.value } })); }); + */ } @@ -184,83 +198,20 @@ function onScreenChanged(type, url) { */ function update(dt) { - // Update head information - currentStateReadings.headPose = Controller.getPoseValue(Controller.Standard.Head); - currentStateReadings.rhandPose = Controller.getPoseValue(Controller.Standard.RightHand); - currentStateReadings.lhandPose = Controller.getPoseValue(Controller.Standard.LeftHand); - - // get the position of the hips and set them for the anim vars callback. - var spine2PositionAvatarSpace = MyAvatar.getAbsoluteJointTranslationInObjectFrame(MyAvatar.getJointIndex("Spine2")); - var spine2RotationAvatarSpace = MyAvatar.getAbsoluteJointRotationInObjectFrame(MyAvatar.getJointIndex("Spine2")); - var hipsPositionAvatarSpace = MyAvatar.getAbsoluteJointTranslationInObjectFrame(MyAvatar.getJointIndex("Hips")); - var hipsRotationAvatarSpace = MyAvatar.getAbsoluteJointRotationInObjectFrame(MyAvatar.getJointIndex("Hips")); - hipsPositionRigSpace = Vec3.multiplyQbyV(Quat.inverse(CHANGE_OF_BASIS_ROTATION), hipsPositionAvatarSpace); - spine2PositionRigSpace = Vec3.multiplyQbyV(Quat.inverse(CHANGE_OF_BASIS_ROTATION), spine2PositionAvatarSpace); - hipsRotationRigSpace = Quat.multiply(CHANGE_OF_BASIS_ROTATION, hipsRotationAvatarSpace); - spine2RotationRigSpace = Quat.multiply(CHANGE_OF_BASIS_ROTATION, spine2RotationAvatarSpace); - //print("hip position rig space" + hipsPositionAvatarSpace.x + " " + hipsPositionAvatarSpace.y + " " + hipsPositionAvatarSpace.z); - //print("hip rotation rig space x: " + hipsRotationRigSpace.x + " y: " + hipsRotationRigSpace.y + " z: " + hipsRotationRigSpace.z + " w: " + hipsRotationRigSpace.w); - // print("spine2 rotation rig space x: " + spine2RotationRigSpace.x + " y: " + spine2RotationRigSpace.y + " z: " + spine2RotationRigSpace.z + " w: " + spine2RotationRigSpace.w); - //print("spine2 position rig space" + spine2PositionRigSpace.x + " " + spine2PositionRigSpace.y + " " + spine2PositionRigSpace.z); - - var headPoseRigSpace = Quat.multiply(CHANGE_OF_BASIS_ROTATION, currentStateReadings.headPose.rotation); - headPoseAverageOrientation = Quat.slerp(headPoseAverageOrientation, headPoseRigSpace, AVERAGING_RATE); - var headPoseAverageEulers = Quat.safeEulerAngles(headPoseAverageOrientation); - - - if (((headPoseAverageEulers.y > HEAD_TURN_THRESHOLD) && (averageAzimuth > HEAD_TURN_THRESHOLD)) - || ((headPoseAverageEulers.y < -HEAD_TURN_THRESHOLD) && (averageAzimuth < -HEAD_TURN_THRESHOLD))) { - print("azimuth " + averageAzimuth); - print("headposeAverageEulers " + headPoseAverageEulers.y); - // Turn feet - print("rotate feet") - MyAvatar.triggerRotationRecenter(); - headPoseAverageOrientation = { x: 0, y: 0, z: 0, w: 1 }; - } - - // and the hand to hip vector to determine when to change head rotation. - // leftHandOrientation = Quat.multiply(MyAvatar.orientation, currentStateReadings.lhandPose.rotation); - // rightHandOrientation = Quat.multiply(MyAvatar.orientation, currentStateReadings.rhandPose.rotation); - var leftHandPositionRigSpace = Vec3.multiplyQbyV(Quat.inverse(CHANGE_OF_BASIS_ROTATION), currentStateReadings.lhandPose.translation); - var rightHandPositionRigSpace = Vec3.multiplyQbyV(Quat.inverse(CHANGE_OF_BASIS_ROTATION), currentStateReadings.rhandPose.translation); - - // Update angle from hips to hand, to be used for turning - var hipToLeftHandAngle = Vec3.orientedAngle({ x: 0, y: 0, z: 1 }, { x: leftHandPositionRigSpace.x, y: 0, z: leftHandPositionRigSpace.z }, { x: 0, y: 1, z: 0 }); - var hipToRightHandAngle = Vec3.orientedAngle({ x: 0, y: 0, z: 1 }, { x: rightHandPositionRigSpace.x, y: 0, z: rightHandPositionRigSpace.z }, { x: 0, y: 1, z: 0 }); - var hipToLeftHand = Quat.lookAtSimple({ x: 0, y: 0, z: 0 }, { x: leftHandPositionRigSpace.x, y: 0, z: leftHandPositionRigSpace.z }); - var hipToRightHand = Quat.lookAtSimple({ x: 0, y: 0, z: 0 }, { x: rightHandPositionRigSpace.x, y: 0, z: rightHandPositionRigSpace.z }); - if (currentStateReadings.lhandPose.valid) { - hipToLeftHandAverage = hipToLeftHandAngle;// hipToLeftHandAverage * (1 - AVERAGING_RATE) + hipToLeftHandAngle * (AVERAGING_RATE); - } - if (currentStateReadings.rhandPose.valid) { - hipToRightHandAverage = hipToRightHandAngle;// hipToRightHandAverage * (1 - AVERAGING_RATE) + hipToRightHandAngle * (AVERAGING_RATE); - } - var leftRightMidpoint = (hipToLeftHandAverage + hipToRightHandAverage) / 2.0; - var FILTER_FACTOR = 0.01; - averageAzimuth = leftRightMidpoint; // * (FILTER_FACTOR) + averageAzimuth * (1 - FILTER_FACTOR); - spine2Rotation = Quat.angleAxis(averageAzimuth, { x: 0, y: 1, z: 0 }); - // print("spine 2 orientation x: " + spine2Rotation.x + " y: " + spine2Rotation.y + " z: " + spine2Rotation.z + " w: " + spine2Rotation.w); - // print("average azimuth " + averageAzimuth); - // print("hip to left hand angle " + hipToLeftHandAngle); - // print("hip to right hand angle " + hipToRightHandAngle); - // print("left right midpoint " + leftRightMidpoint); - // print("left hand position " + leftHandPositionRigSpace.x + " " + leftHandPositionRigSpace.y + " " + leftHandPositionRigSpace.z); - // print("right hand position " + rightHandPositionRigSpace.x + " " + rightHandPositionRigSpace.y + " " + rightHandPositionRigSpace.z); - // print("hip to left hand average " + hipToLeftHandAverage.x + " " + hipToLeftHandAverage.y + " " + hipToLeftHandAverage.z + " " + hipToLeftHandAverage.w); } function shutdownTabletApp() { tablet.removeButton(tabletButton); if (activated) { - // tablet.webEventReceived.disconnect(onWebEventReceived); + tablet.webEventReceived.disconnect(onWebEventReceived); tablet.gotoHomeScreen(); } - // tablet.screenChanged.disconnect(onScreenChanged); + tablet.screenChanged.disconnect(onScreenChanged); } tabletButton.clicked.connect(manageClick); -// tablet.screenChanged.connect(onScreenChanged); +tablet.screenChanged.connect(onScreenChanged); Script.update.connect(update); @@ -269,7 +220,7 @@ Controller.keyPressEvent.connect(onKeyPress); Script.scriptEnding.connect(function () { if (handlerId) { print("removing animation state handler"); - handlerId = MyAvatar.removeAnimationStateHandler(handlerId); + // handlerId = MyAvatar.removeAnimationStateHandler(handlerId); } MyAvatar.hmdLeanRecenterEnabled = true; Script.update.disconnect(update); From 096d0d2827a3e126a0e3d96687f3c158b4d0e015 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 2 Jul 2018 15:43:57 -0700 Subject: [PATCH 017/182] reorganized the step detection code, creating a shouldActivateHorizontalCG that calls all the step detection conditionals. The average rotation and the mode are now calculated in MyAvatar::update --- interface/src/avatar/MyAvatar.cpp | 160 ++++++++-------- interface/src/avatar/MyAvatar.h | 25 ++- libraries/shared/src/AvatarConstants.h | 11 ++ scripts/developer/rotateApp.js | 228 ----------------------- scripts/developer/rotateRecenterApp.html | 28 +-- 5 files changed, 117 insertions(+), 335 deletions(-) delete mode 100644 scripts/developer/rotateApp.js diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 198d2e6f3b..388bae6fdb 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -396,7 +396,7 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); //4.0f; // very slow average - qCDebug(interfaceapp) << "rotation recenter value is " << HMD_FACING_TIMESCALE; + //qCDebug(interfaceapp) << "rotation recenter value is " << HMD_FACING_TIMESCALE; float tau = deltaTime / HMD_FACING_TIMESCALE; _headControllerFacingMovingAverage = lerp(_headControllerFacingMovingAverage, _headControllerFacing, tau); @@ -404,6 +404,8 @@ void MyAvatar::update(float deltaTime) { _rotationChanged = usecTimestampNow(); _smoothOrientationTimer += deltaTime; } + setStandingHeightMode(computeStandingHeightMode(getControllerPoseInAvatarFrame(controller::Action::HEAD))); + setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); @@ -3094,14 +3096,10 @@ static bool isInsideLine(glm::vec3 a, glm::vec3 b, glm::vec3 c) { static bool withinBaseOfSupport(controller::Pose head) { float userScale = 1.0f; - const float DEFAULT_LATERAL = 0.10f; - const float DEFAULT_ANTERIOR = 0.04f; - const float DEFAULT_POSTERIOR = 0.06f; - - glm::vec3 frontLeft(-DEFAULT_LATERAL, 0.0f, -DEFAULT_ANTERIOR); - glm::vec3 frontRight(DEFAULT_LATERAL, 0.0f, -DEFAULT_ANTERIOR); - glm::vec3 backLeft(-DEFAULT_LATERAL, 0.0f, DEFAULT_POSTERIOR); - glm::vec3 backRight(DEFAULT_LATERAL, 0.0f, DEFAULT_POSTERIOR); + glm::vec3 frontLeft(-DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, -DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD); + glm::vec3 frontRight(DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, -DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD); + glm::vec3 backLeft(-DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD); + glm::vec3 backRight(DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD); bool isWithinSupport = false; if (head.isValid()) { @@ -3111,40 +3109,38 @@ static bool withinBaseOfSupport(controller::Pose head) { isInsideLine(userScale * backLeft, userScale * frontLeft, head.getTranslation())); isWithinSupport = (withinFrontBase && withinBackBase && withinLateralBase); } - //qCDebug(interfaceapp) << "within base of support " << isWithinSupport; + qCDebug(interfaceapp) << "within base of support " << isWithinSupport; return isWithinSupport; } static bool headAngularVelocityBelowThreshold(controller::Pose head) { - const float ANGULAR_VELOCITY_THRESHOLD = 0.3f; glm::vec3 xzPlaneAngularVelocity(0.0f, 0.0f, 0.0f); if (head.isValid()) { xzPlaneAngularVelocity.x = head.getAngularVelocity().x; xzPlaneAngularVelocity.z = head.getAngularVelocity().z; } float magnitudeAngularVelocity = glm::length(xzPlaneAngularVelocity); - bool isBelowThreshold = (magnitudeAngularVelocity < ANGULAR_VELOCITY_THRESHOLD); + bool isBelowThreshold = (magnitudeAngularVelocity < DEFAULT_AVATAR_HEAD_ANGULAR_VELOCITY_STEPPING_THRESHOLD); - //qCDebug(interfaceapp) << "head angular velocity " << isBelowThreshold; + qCDebug(interfaceapp) << "head angular velocity " << isBelowThreshold; return isBelowThreshold; } static bool isWithinThresholdHeightMode(controller::Pose head, float newMode) { - - const float MODE_HEIGHT_THRESHOLD = -0.02f; bool isWithinThreshold = true; if (head.isValid()) { - isWithinThreshold = (head.getTranslation().y - newMode) > MODE_HEIGHT_THRESHOLD; + isWithinThreshold = (head.getTranslation().y - newMode) > DEFAULT_AVATAR_MODE_HEIGHT_STEPPING_THRESHOLD; } - //qCDebug(interfaceapp) << "height threshold " << isWithinThreshold; + qCDebug(interfaceapp) << "height threshold " << isWithinThreshold; return isWithinThreshold; } float MyAvatar::computeStandingHeightMode(controller::Pose head) { const float CENTIMETERS_PER_METER = 100.0f; const float MODE_CORRECTION_FACTOR = 0.02f; - + // init mode in meters to the current mode + float modeInMeters = getStandingHeightMode(); //qCDebug(interfaceapp) << "new reading is " << newReading << " as an integer " << (int)(newReading * CENTIMETERS_PER_METER); if (head.isValid()) { float newReading = head.getTranslation().y; @@ -3165,50 +3161,46 @@ float MyAvatar::computeStandingHeightMode(controller::Pose head) { mode = _heightModeArray[j]; } } - float modeInMeters = ((float)mode) / CENTIMETERS_PER_METER; - if (modeInMeters > _currentMode) { - qCDebug(interfaceapp) << "new mode value set"; - _currentMode = modeInMeters; - } - else { - if (!_resetMode && qApp->isHMDMode()) { - _resetMode = true; + modeInMeters = ((float)mode) / CENTIMETERS_PER_METER; + if (!(modeInMeters > getStandingHeightMode())) { + // if not greater check for a reset + if (getResetMode() && qApp->isHMDMode()) { + setResetMode(false); qCDebug(interfaceapp) << "reset mode value occurred"; - float modeInCentimeters = glm::floor((newReading - MODE_CORRECTION_FACTOR)*CENTIMETERS_PER_METER); - _currentMode = modeInCentimeters/CENTIMETERS_PER_METER; + float resetModeInCentimeters = glm::floor((newReading - MODE_CORRECTION_FACTOR)*CENTIMETERS_PER_METER); + modeInMeters = (resetModeInCentimeters / CENTIMETERS_PER_METER); + } else { + // if not greater and no reset, keep the mode as it is + modeInMeters = getStandingHeightMode(); } + } else { + qCDebug(interfaceapp) << "new mode value set" << modeInMeters; } } //qCDebug(interfaceapp) << "_current mode is " << _currentMode; - return _currentMode; + return modeInMeters; } static bool handDirectionMatchesHeadDirection(controller::Pose leftHand, controller::Pose rightHand, controller::Pose head) { - - const float HANDS_VELOCITY_DIRECTION_THRESHOLD = 0.4f; const float VELOCITY_EPSILON = 0.02f; bool leftHandDirectionMatchesHead = true; bool rightHandDirectionMatchesHead = true; if (leftHand.isValid() && head.isValid()) { leftHand.velocity.y = 0.0f; float handDotHeadLeft = glm::dot(glm::normalize(leftHand.getVelocity()), glm::normalize(head.getVelocity())); - leftHandDirectionMatchesHead = ((handDotHeadLeft > HANDS_VELOCITY_DIRECTION_THRESHOLD) && (glm::length(leftHand.getVelocity()) > VELOCITY_EPSILON)); + leftHandDirectionMatchesHead = ((handDotHeadLeft > DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD) && (glm::length(leftHand.getVelocity()) > VELOCITY_EPSILON)); //qCDebug(interfaceapp) << "hand dot head left " << handDotHeadLeft; } if (rightHand.isValid() && head.isValid()) { rightHand.velocity.y = 0.0f; float handDotHeadRight = glm::dot(glm::normalize(rightHand.getVelocity()), glm::normalize(head.getVelocity())); - rightHandDirectionMatchesHead = ((handDotHeadRight > HANDS_VELOCITY_DIRECTION_THRESHOLD) && (glm::length(rightHand.getVelocity()) > VELOCITY_EPSILON)); + rightHandDirectionMatchesHead = ((handDotHeadRight > DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD) && (glm::length(rightHand.getVelocity()) > VELOCITY_EPSILON)); } - - //qCDebug(interfaceapp) << "left right hand velocity "<< (leftHandDirectionMatchesHead && rightHandDirectionMatchesHead); - - + qCDebug(interfaceapp) << "left right hand velocity "<< (leftHandDirectionMatchesHead && rightHandDirectionMatchesHead); return leftHandDirectionMatchesHead && rightHandDirectionMatchesHead; } static bool handAngularVelocityBelowThreshold(controller::Pose leftHand, controller::Pose rightHand) { - const float HANDS_ANGULAR_VELOCITY_THRESHOLD = 0.4f; float leftHandXZAngularVelocity = 0.0f; float rightHandXZAngularVelocity = 0.0f; if (leftHand.isValid()) { @@ -3219,38 +3211,35 @@ static bool handAngularVelocityBelowThreshold(controller::Pose leftHand, control rightHand.angularVelocity.y = 0.0f; rightHandXZAngularVelocity = glm::length(rightHand.getAngularVelocity()); } - //qCDebug(interfaceapp) << " hands angular velocity left " << (leftHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD) << " and right " << (rightHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD); - return ((leftHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD) && - (rightHandXZAngularVelocity < HANDS_ANGULAR_VELOCITY_THRESHOLD)); + qCDebug(interfaceapp) << " hands angular velocity left " << (leftHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD) << " and right " << (rightHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD); + return ((leftHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD) && + (rightHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD)); } static bool headVelocityGreaterThanThreshold(controller::Pose head) { - const float VELOCITY_EPSILON = 0.02f; - const float HEAD_VELOCITY_THRESHOLD = 0.14f; float headVelocityMagnitude = 0.0f; if (head.isValid()) { //qCDebug(interfaceapp) << " head velocity " << head.getVelocity(); headVelocityMagnitude = glm::length(head.getVelocity()); } - //qCDebug(interfaceapp) << " head velocity " << (headVelocityMagnitude > HEAD_VELOCITY_THRESHOLD); - return headVelocityMagnitude > HEAD_VELOCITY_THRESHOLD; + qCDebug(interfaceapp) << " head velocity " << (headVelocityMagnitude > DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD); + return headVelocityMagnitude > DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD; } -bool MyAvatar::isHeadLevel(controller::Pose head) { +glm::quat MyAvatar::computeAverageHeadRotation(controller::Pose head) { const float AVERAGING_RATE = 0.03f; - const float HEAD_PITCH_TOLERANCE = 7.0f; - const float HEAD_ROLL_TOLERANCE = 7.0f; - glm::vec3 diffFromAverageEulers(0.0f, 0.0f, 0.0f); + return slerp(_averageHeadRotation, head.getRotation(), AVERAGING_RATE); +} +static bool isHeadLevel(controller::Pose head, glm::quat averageHeadRotation) { + glm::vec3 diffFromAverageEulers(0.0f, 0.0f, 0.0f); if (head.isValid()) { - _averageHeadRotation = slerp(_averageHeadRotation, head.getRotation(), AVERAGING_RATE); - glm::vec3 averageHeadEulers = glm::degrees(safeEulerAngles(_averageHeadRotation)); + glm::vec3 averageHeadEulers = glm::degrees(safeEulerAngles(averageHeadRotation)); glm::vec3 currentHeadEulers = glm::degrees(safeEulerAngles(head.getRotation())); diffFromAverageEulers = averageHeadEulers - currentHeadEulers; } - //qCDebug(interfaceapp) << " diff from average eulers x " << (fabs(diffFromAverageEulers.x) < HEAD_PITCH_TOLERANCE) << " and z " << (fabs(diffFromAverageEulers.z) < HEAD_ROLL_TOLERANCE); - - return ((fabs(diffFromAverageEulers.x) < HEAD_PITCH_TOLERANCE) && (fabs(diffFromAverageEulers.z) < HEAD_ROLL_TOLERANCE)); + qCDebug(interfaceapp) << " diff from average eulers x " << (fabs(diffFromAverageEulers.x) < DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE) << " and z " << (fabs(diffFromAverageEulers.z) < DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE); + return ((fabs(diffFromAverageEulers.x) < DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE) && (fabs(diffFromAverageEulers.z) < DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE)); } float MyAvatar::getUserHeight() const { @@ -3419,7 +3408,7 @@ void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) { bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { - qCDebug(interfaceapp) << "rotation threshold is " << myAvatar.getRotationThreshold(); + //qCDebug(interfaceapp) << "rotation threshold is " << myAvatar.getRotationThreshold(); const float FOLLOW_ROTATION_THRESHOLD = cosf(myAvatar.getRotationThreshold()); //cosf(PI / 6.0f); // 30 degrees glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix); return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; @@ -3450,6 +3439,37 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, return fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; } +bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) const { + + // get the current readings + controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD); + controller::Pose currentLeftHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND); + controller::Pose currentRightHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND); + + bool stepDetected = false; + if (!withinBaseOfSupport(currentHeadPose) && + headAngularVelocityBelowThreshold(currentHeadPose) && + isWithinThresholdHeightMode(currentHeadPose, myAvatar.getStandingHeightMode()) && + handDirectionMatchesHeadDirection(currentLeftHandPose, currentRightHandPose, currentHeadPose) && + handAngularVelocityBelowThreshold(currentLeftHandPose, currentRightHandPose) && + headVelocityGreaterThanThreshold(currentHeadPose) && + isHeadLevel(currentHeadPose, myAvatar.getAverageHeadRotation())) { + // a step is detected + stepDetected = true; + } else { + glm::vec3 defaultHipsPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); + glm::vec3 defaultHeadPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head")); + glm::vec3 currentHeadPosition = currentHeadPose.getTranslation(); + float anatomicalHeadToHipsDistance = glm::length(defaultHeadPosition - defaultHipsPosition); + if (!isActive(Horizontal) && + (glm::length(currentHeadPosition - defaultHipsPosition) > (anatomicalHeadToHipsDistance + DEFAULT_AVATAR_SPINE_STRETCH_LIMIT))) { + myAvatar.setResetMode(true); + stepDetected = true; + } + } + return stepDetected; +} + bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { @@ -3476,42 +3496,14 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, activate(Vertical); } } else { - // this is where we put the code for the stepping. - // we do not have hmd lean enabled and we are looking for a step via our criteria. - //qCDebug(interfaceapp) << "hmd lean is off"; if (!isActive(Rotation) && getForceActivateRotation()) { activate(Rotation); setForceActivateRotation(false); } - - //compute the mode each frame - float theMode = myAvatar.computeStandingHeightMode(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)); - //compute the average length of the spine given the current mode - glm::vec3 defaultHipsPos = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); - float anatomicalHeadToHipsDistance = fabs(theMode - defaultHipsPos.y); - - //qCDebug(interfaceapp) << " y value head " << headPositionYAvatarFrame; - //headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getAngularVelocity()); - //float temp = myAvatar.computeStandingHeightMode(0.01f); - if (!isActive(Horizontal) && (getForceActivateHorizontal() || - (!withinBaseOfSupport(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && - headAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && - isWithinThresholdHeightMode(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD), theMode) && - handDirectionMatchesHeadDirection(myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && - handAngularVelocityBelowThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND), myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND)) && - headVelocityGreaterThanThreshold(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD)) && - myAvatar.isHeadLevel(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD))))) { + if (!isActive(Horizontal) && (getForceActivateHorizontal() || shouldActivateHorizontalCG(myAvatar))) { qCDebug(interfaceapp) << "----------------------------------------take a step--------------------------------------"; activate(Horizontal); setForceActivateHorizontal(false); - } else { - const float SPINE_STRETCH_LIMIT = 0.07f; - const float FAILSAFE_TIMEOUT = 2.5f; - if (!isActive(Horizontal) && - (glm::length(myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation() - defaultHipsPos) > (anatomicalHeadToHipsDistance + SPINE_STRETCH_LIMIT))) { - myAvatar._resetMode = false; - activate(Horizontal); - } } if (!isActive(Vertical) && getForceActivateVertical()) { activate(Vertical); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index ee7028cfe1..6c1efce126 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -892,6 +892,12 @@ public: virtual void rebuildCollisionShape() override; const glm::vec2& getHeadControllerFacingMovingAverage() const { return _headControllerFacingMovingAverage; } + const float getStandingHeightMode() const { return _standingHeightMode; } + void setStandingHeightMode(float newMode) { _standingHeightMode = newMode; } + const glm::quat getAverageHeadRotation() const { return _averageHeadRotation; } + void setAverageHeadRotation(glm::quat rotation) { _averageHeadRotation = rotation; } + bool getResetMode() const { return _resetMode; } + void setResetMode(bool hasBeenReset) { _resetMode = hasBeenReset; } void setControllerPoseInSensorFrame(controller::Action action, const controller::Pose& pose); controller::Pose getControllerPoseInSensorFrame(controller::Action action) const; @@ -1028,8 +1034,7 @@ public: bool isReadyForPhysics() const; float computeStandingHeightMode(controller::Pose head); - bool isHeadLevel(controller::Pose head); - //bool isWithinThresholdHeightMode(float newReading); + glm::quat computeAverageHeadRotation(controller::Pose head); public slots: @@ -1528,6 +1533,12 @@ private: // cache head controller pose in sensor space glm::vec2 _headControllerFacing; // facing vector in xz plane (sensor space) glm::vec2 _headControllerFacingMovingAverage { 0.0f, 0.0f }; // facing vector in xz plane (sensor space) + glm::quat _averageHeadRotation { 0.0f, 0.0f, 0.0f, 1.0f }; + + static const int SIZE_OF_MODE_ARRAY { 50 }; + int _heightModeArray[SIZE_OF_MODE_ARRAY]; + float _standingHeightMode { 0.0f }; + bool _resetMode { true }; // cache of the current body position and orientation of the avatar's body, // in sensor space. @@ -1555,6 +1566,7 @@ private: bool shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; + bool shouldActivateHorizontalCG(MyAvatar& myAvatar) const; void prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& bodySensorMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput); glm::mat4 postPhysicsUpdate(const MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix); bool getForceActivateRotation() const; @@ -1605,7 +1617,7 @@ private: mutable std::mutex _controllerPoseMapMutex; bool _centerOfGravityModelEnabled { true }; - bool _hmdLeanRecenterEnabled { true }; + bool _hmdLeanRecenterEnabled { false }; bool _sprint { false }; AnimPose _prePhysicsRoomPose; @@ -1642,12 +1654,7 @@ private: // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; - static const int SIZE_OF_MODE_ARRAY = 50; - bool _haveReceivedHeightLimitsFromDomain = { false }; - int _heightModeArray[SIZE_OF_MODE_ARRAY]; - float _currentMode = 0; - bool _resetMode = false; - glm::quat _averageHeadRotation = glm::quat(0.0f,0.0f,0.0f,0.0f); + bool _haveReceivedHeightLimitsFromDomain { false }; }; diff --git a/libraries/shared/src/AvatarConstants.h b/libraries/shared/src/AvatarConstants.h index 58cbff6669..bce71a03ff 100644 --- a/libraries/shared/src/AvatarConstants.h +++ b/libraries/shared/src/AvatarConstants.h @@ -24,6 +24,17 @@ const float DEFAULT_AVATAR_SUPPORT_BASE_LEFT = -0.25f; const float DEFAULT_AVATAR_SUPPORT_BASE_RIGHT = 0.25f; const float DEFAULT_AVATAR_SUPPORT_BASE_FRONT = -0.20f; const float DEFAULT_AVATAR_SUPPORT_BASE_BACK = 0.10f; +const float DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD = 0.10f; +const float DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD = 0.04f; +const float DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD = 0.06f; +const float DEFAULT_AVATAR_HEAD_ANGULAR_VELOCITY_STEPPING_THRESHOLD = 0.3f; +const float DEFAULT_AVATAR_MODE_HEIGHT_STEPPING_THRESHOLD = -0.02f; +const float DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD = 0.4f; +const float DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD = 3.3f; +const float DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD = 0.14f; +const float DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE = 7.0f; +const float DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE = 7.0f; +const float DEFAULT_AVATAR_SPINE_STRETCH_LIMIT = 0.07f; const float DEFAULT_AVATAR_FORWARD_DAMPENING_FACTOR = 0.5f; const float DEFAULT_AVATAR_LATERAL_DAMPENING_FACTOR = 2.0f; const float DEFAULT_AVATAR_HIPS_MASS = 40.0f; diff --git a/scripts/developer/rotateApp.js b/scripts/developer/rotateApp.js deleted file mode 100644 index 3b12ed8789..0000000000 --- a/scripts/developer/rotateApp.js +++ /dev/null @@ -1,228 +0,0 @@ -/* global Script, Vec3, MyAvatar, Tablet, Messages, Quat, -DebugDraw, Mat4, Entities, Xform, Controller, Camera, console, document*/ - -Script.registerValue("ROTATEAPP", true); - -var TABLET_BUTTON_NAME = "ROTATE"; -var CHANGE_OF_BASIS_ROTATION = { x: 0, y: 1, z: 0, w: 0 }; -var HEAD_TURN_THRESHOLD = .5333; -var HEAD_TURN_FILTER_LENGTH = 4.0; -var LOADING_DELAY = 500; -var AVERAGING_RATE = 0.03; - -var activated = false; -var documentLoaded = false; -var headPoseAverageOrientation = { x: 0, y: 0, z: 0, w: 1 }; -var hipToLeftHandAverage = 0.0; // { x: 0, y: 0, z: 0, w: 1 }; -var hipToRightHandAverage = 0.0; // { x: 0, y: 0, z: 0, w: 1 }; -var averageAzimuth = 0.0; -var hipsPositionRigSpace = { x: 0, y: 0, z: 0 }; -var spine2PositionRigSpace = { x: 0, y: 0, z: 0 }; -var hipsRotationRigSpace = { x: 0, y: 0, z: 0, w: 1 }; -var spine2RotationRigSpace = { x: 0, y: 0, z: 0, w: 1 }; -var spine2Rotation = { x: 0, y: 0, z: 0, w: 1 }; - -var ikTypes = { - RotationAndPosition: 0, - RotationOnly: 1, - HmdHead: 2, - HipsRelativeRotationAndPosition: 3, - Spline: 4, - Unknown: 5 -}; - -/* -var ANIM_VARS = [ - //"headType", - "spine2Type", - //"hipsType", - "spine2Position", - "spine2Rotation", - //"hipsPosition", - //"hipsRotation" -]; - -var handlerId = MyAvatar.addAnimationStateHandler(function (props) { - //print("in callback"); - //print("props spine2 pos: " + props.spine2Position.x + " " + props.spine2Position.y + " " + props.spine2Position.z); - //print("props hip pos: " + props.hipsPosition.x + " " + props.hipsPosition.y + " " + props.hipsPosition.z); - var result = {}; - //{x:0,y:0,z:0} - //result.headType = ikTypes.HmdHead; - //result.hipsType = ikTypes.RotationAndPosition; - //result.hipsPosition = hipsPositionRigSpace; // { x: 0, y: 0, z: 0 }; - //result.hipsRotation = hipsRotationRigSpace;//{ x: 0, y: 0, z: 0, w: 1 }; // - result.spine2Type = ikTypes.Spline; - result.spine2Position = spine2PositionRigSpace; // { x: 0, y: 1.3, z: 0 }; - result.spine2Rotation = spine2Rotation; - - return result; -}, ANIM_VARS); -*/ -// define state readings constructor -function StateReading(headPose, rhandPose, lhandPose, diffFromAverageEulers) { - this.headPose = headPose; - this.rhandPose = rhandPose; - this.lhandPose = lhandPose; - this.diffFromAverageEulers = diffFromAverageEulers; -} - -// define current state readings object for holding tracker readings and current differences from averages -var currentStateReadings = new StateReading(Controller.getPoseValue(Controller.Standard.Head), - Controller.getPoseValue(Controller.Standard.RightHand), - Controller.getPoseValue(Controller.Standard.LeftHand), - { x: 0, y: 0, z: 0 }); - -// declare the checkbox constructor -function AppCheckbox(type,id,eventType,isChecked) { - this.type = type; - this.id = id; - this.eventType = eventType; - this.data = {value: isChecked}; -} - -// declare the html slider constructor -function AppProperty(name, type, eventType, signalType, setFunction, initValue, convertToThreshold, convertToSlider, signalOn) { - this.name = name; - this.type = type; - this.eventType = eventType; - this.signalType = signalType; - this.setValue = setFunction; - this.value = initValue; - this.get = function () { - return this.value; - }; - this.convertToThreshold = convertToThreshold; - this.convertToSlider = convertToSlider; -} - -var HTML_URL = Script.resolvePath("file:///c:/dev/hifi_fork/hifi/scripts/developer/rotateApp.html"); -var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - -// define the sliders -var filterLengthProperty = new AppProperty("#filterLength-slider", "slider", "onFilterLengthSlider", "filterSignal", - setAnteriorDistance, DEFAULT_ANTERIOR, function (num) { - return convertToMeters(num); - }, function (num) { - return convertToCentimeters(num); - },true); -var angleThresholdProperty = new AppProperty("#angleThreshold-slider", "slider", "onAngleThresholdSlider", "angleSignal", - setPosteriorDistance, DEFAULT_POSTERIOR, function (num) { - return convertToMeters(num); - }, function (num) { - return convertToCentimeters(num); - }, true); - - - -function manageClick() { - if (activated) { - tablet.gotoHomeScreen(); - } else { - tablet.gotoWebScreen(HTML_URL); - } -} - -var tabletButton = tablet.addButton({ - text: TABLET_BUTTON_NAME, - icon: Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/foot.svg"), - activeIcon: Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/foot.svg") -}); - -function onKeyPress(event) { - if (event.text === "'") { - // when the sensors are reset, then reset the mode. - } -} - -function onWebEventReceived(msg) { - var message = JSON.parse(msg); - print(" we have a message from html dialog " + message.type); - propArray.forEach(function (prop) { - if (prop.eventType === message.type) { - prop.setValue(prop.convertToThreshold(message.data.value)); - print("message from " + prop.name); - // break; - } - }); - checkBoxArray.forEach(function(cbox) { - if (cbox.eventType === message.type) { - cbox.data.value = message.data.value; - // break; - } - }); -} - -function initAppForm() { - print("step app is loaded: " + documentLoaded); - propArray.forEach(function (prop) { - print(prop.name); - tablet.emitScriptEvent(JSON.stringify({ - "type": "slider", - "id": prop.name, - "data": { "value": prop.convertToSlider(prop.value) } - })); - }); - /* - checkBoxArray.forEach(function(cbox) { - tablet.emitScriptEvent(JSON.stringify({ - "type": "checkboxtick", - "id": cbox.id, - "data": { value: cbox.data.value } - })); - }); - */ - -} - - -function onScreenChanged(type, url) { - print("Screen changed"); - if (type === "Web" && url === HTML_URL) { - if (!activated) { - // hook up to event bridge - tablet.webEventReceived.connect(onWebEventReceived); - print("after connect web event"); - MyAvatar.hmdLeanRecenterEnabled = false; - Script.setTimeout(initAppForm, LOADING_DELAY); - } - activated = true; - } else { - if (activated) { - // disconnect from event bridge - tablet.webEventReceived.disconnect(onWebEventReceived); - } - activated = false; - } -} -*/ -function update(dt) { - - -} - -function shutdownTabletApp() { - tablet.removeButton(tabletButton); - if (activated) { - tablet.webEventReceived.disconnect(onWebEventReceived); - tablet.gotoHomeScreen(); - } - tablet.screenChanged.disconnect(onScreenChanged); -} - -tabletButton.clicked.connect(manageClick); -tablet.screenChanged.connect(onScreenChanged); - -Script.update.connect(update); - -Controller.keyPressEvent.connect(onKeyPress); - -Script.scriptEnding.connect(function () { - if (handlerId) { - print("removing animation state handler"); - // handlerId = MyAvatar.removeAnimationStateHandler(handlerId); - } - MyAvatar.hmdLeanRecenterEnabled = true; - Script.update.disconnect(update); - shutdownTabletApp(); -}); \ No newline at end of file diff --git a/scripts/developer/rotateRecenterApp.html b/scripts/developer/rotateRecenterApp.html index 1ccb54e4a3..f50a2f5b0d 100644 --- a/scripts/developer/rotateRecenterApp.html +++ b/scripts/developer/rotateRecenterApp.html @@ -51,7 +51,7 @@
Date: Mon, 2 Jul 2018 15:46:58 -0700 Subject: [PATCH 018/182] added tweaks to the avatar animation json that speed up the transitions to better match the new step detection --- interface/resources/avatar/avatar-animation.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index ee2b916d1e..44d294f767 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -566,7 +566,7 @@ }, { "id": "idleToWalkFwd", - "interpTarget": 3, + "interpTarget": 10, "interpDuration": 3, "transitions": [ { "var": "idleToWalkFwdOnDone", "state": "walkFwd" }, @@ -603,8 +603,8 @@ }, { "id": "walkBwd", - "interpTarget": 6, - "interpDuration": 6, + "interpTarget": 8, + "interpDuration": 2, "transitions": [ { "var": "isNotMoving", "state": "idle" }, { "var": "isMovingForward", "state": "walkFwd" }, @@ -621,8 +621,8 @@ }, { "id": "strafeRight", - "interpTarget": 6, - "interpDuration": 6, + "interpTarget": 20, + "interpDuration": 1, "transitions": [ { "var": "isNotMoving", "state": "idle" }, { "var": "isMovingForward", "state": "walkFwd" }, @@ -639,8 +639,8 @@ }, { "id": "strafeLeft", - "interpTarget": 6, - "interpDuration": 6, + "interpTarget": 20, + "interpDuration": 1, "transitions": [ { "var": "isNotMoving", "state": "idle" }, { "var": "isMovingForward", "state": "walkFwd" }, From 47110d080fc6a8f60cb5b0c37e4cac6be7e3aeff Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 2 Jul 2018 17:06:16 -0700 Subject: [PATCH 019/182] removed clang induced changes to MyAvatar.cpp --- interface/src/avatar/MyAvatar.cpp | 484 ++++++++++++----------- scripts/developer/rotateRecenterApp.html | 171 -------- 2 files changed, 248 insertions(+), 407 deletions(-) delete mode 100644 scripts/developer/rotateRecenterApp.html diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index be9a557219..5c879f195a 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -69,11 +69,11 @@ using namespace std; const float DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES = 30.0f; const float YAW_SPEED_DEFAULT = 100.0f; // degrees/sec -const float PITCH_SPEED_DEFAULT = 75.0f; // degrees/sec +const float PITCH_SPEED_DEFAULT = 75.0f; // degrees/sec -const float MAX_BOOST_SPEED = 0.5f * DEFAULT_AVATAR_MAX_WALKING_SPEED; // action motor gets additive boost below this speed +const float MAX_BOOST_SPEED = 0.5f * DEFAULT_AVATAR_MAX_WALKING_SPEED; // action motor gets additive boost below this speed const float MIN_AVATAR_SPEED = 0.05f; -const float MIN_AVATAR_SPEED_SQUARED = MIN_AVATAR_SPEED * MIN_AVATAR_SPEED; // speed is set to zero below this +const float MIN_AVATAR_SPEED_SQUARED = MIN_AVATAR_SPEED * MIN_AVATAR_SPEED; // speed is set to zero below this float MIN_SCRIPTED_MOTOR_TIMESCALE = 0.005f; float DEFAULT_SCRIPTED_MOTOR_TIMESCALE = 1.0e6f; @@ -82,8 +82,7 @@ const int SCRIPTED_MOTOR_AVATAR_FRAME = 1; const int SCRIPTED_MOTOR_WORLD_FRAME = 2; const int SCRIPTED_MOTOR_SIMPLE_MODE = 0; const int SCRIPTED_MOTOR_DYNAMIC_MODE = 1; -const QString& DEFAULT_AVATAR_COLLISION_SOUND_URL = - "https://hifi-public.s3.amazonaws.com/sounds/Collisions-otherorganic/Body_Hits_Impact.wav"; +const QString& DEFAULT_AVATAR_COLLISION_SOUND_URL = "https://hifi-public.s3.amazonaws.com/sounds/Collisions-otherorganic/Body_Hits_Impact.wav"; const float MyAvatar::ZOOM_MIN = 0.5f; const float MyAvatar::ZOOM_MAX = 25.0f; @@ -91,15 +90,33 @@ const float MyAvatar::ZOOM_DEFAULT = 1.5f; const float MIN_SCALE_CHANGED_DELTA = 0.001f; MyAvatar::MyAvatar(QThread* thread) : - Avatar(thread), _yawSpeed(YAW_SPEED_DEFAULT), _pitchSpeed(PITCH_SPEED_DEFAULT), - _scriptedMotorTimescale(DEFAULT_SCRIPTED_MOTOR_TIMESCALE), _scriptedMotorFrame(SCRIPTED_MOTOR_CAMERA_FRAME), - _scriptedMotorMode(SCRIPTED_MOTOR_SIMPLE_MODE), _motionBehaviors(AVATAR_MOTION_DEFAULTS), _characterController(this), - _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), + Avatar(thread), + _yawSpeed(YAW_SPEED_DEFAULT), + _pitchSpeed(PITCH_SPEED_DEFAULT), + _scriptedMotorTimescale(DEFAULT_SCRIPTED_MOTOR_TIMESCALE), + _scriptedMotorFrame(SCRIPTED_MOTOR_CAMERA_FRAME), + _scriptedMotorMode(SCRIPTED_MOTOR_SIMPLE_MODE), + _motionBehaviors(AVATAR_MOTION_DEFAULTS), + _characterController(this), + _eyeContactTarget(LEFT_EYE), + _realWorldFieldOfView("realWorldFieldOfView", + DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false), - _smoothOrientationTimer(std::numeric_limits::max()), _smoothOrientationInitial(), _smoothOrientationTarget(), - _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), _bodySensorMatrix(), _goToPending(false), - _goToPosition(), _goToOrientation(), _prevShouldDrawHead(true), _audioListenerMode(FROM_HEAD), - _hmdAtRestDetector(glm::vec3(0), glm::quat()) { + _smoothOrientationTimer(std::numeric_limits::max()), + _smoothOrientationInitial(), + _smoothOrientationTarget(), + _hmdSensorMatrix(), + _hmdSensorOrientation(), + _hmdSensorPosition(), + _bodySensorMatrix(), + _goToPending(false), + _goToPosition(), + _goToOrientation(), + _prevShouldDrawHead(true), + _audioListenerMode(FROM_HEAD), + _hmdAtRestDetector(glm::vec3(0), glm::quat()) +{ + // give the pointer to our head to inherited _headData variable from AvatarData _headData = new MyHead(this); @@ -128,11 +145,11 @@ MyAvatar::MyAvatar(QThread* thread) : clearDriveKeys(); // Necessary to select the correct slot - using SlotType = void (MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); + using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); // connect to AddressManager signal for location jumps - connect(DependencyManager::get().data(), &AddressManager::locationChangeRequired, this, - static_cast(&MyAvatar::goToLocation)); + connect(DependencyManager::get().data(), &AddressManager::locationChangeRequired, + this, static_cast(&MyAvatar::goToLocation)); // handle scale constraints imposed on us by the domain-server auto& domainHandler = DependencyManager::get()->getDomainHandler(); @@ -194,6 +211,7 @@ MyAvatar::MyAvatar(QThread* thread) : if (recordingInterface->getPlayerUseSkeletonModel() && dummyAvatar.getSkeletonModelURL().isValid() && (dummyAvatar.getSkeletonModelURL() != getSkeletonModelURL())) { + setSkeletonModelURL(dummyAvatar.getSkeletonModelURL()); } @@ -240,8 +258,7 @@ void MyAvatar::setDominantHand(const QString& hand) { } void MyAvatar::registerMetaTypes(ScriptEnginePointer engine) { - QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, - QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); + QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); engine->globalObject().setProperty("MyAvatar", value); QScriptValue driveKeys = engine->newObject(); @@ -317,7 +334,7 @@ void MyAvatar::centerBody() { } // derive the desired body orientation from the current hmd orientation, before the sensor reset. - auto newBodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. + auto newBodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. // transform this body into world space auto worldBodyMatrix = _sensorToWorldMatrix * newBodySensorMatrix; @@ -354,6 +371,7 @@ void MyAvatar::clearIKJointLimitHistory() { } void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { + assert(QThread::currentThread() == thread()); // Reset dynamic state. @@ -362,14 +380,14 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { if (andReload) { _skeletonModel->reset(); } - if (andHead) { // which drives camera in desktop + if (andHead) { // which drives camera in desktop getHead()->reset(); } setThrust(glm::vec3(0.0f)); if (andRecenter) { // derive the desired body orientation from the *old* hmd orientation, before the sensor reset. - auto newBodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. + auto newBodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. // transform this body into world space auto worldBodyMatrix = _sensorToWorldMatrix * newBodySensorMatrix; @@ -394,9 +412,9 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { } void MyAvatar::update(float deltaTime) { + // update moving average of HMD facing in xz plane. - const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); //4.0f; // very slow average - //qCDebug(interfaceapp) << "rotation recenter value is " << HMD_FACING_TIMESCALE; + const float HMD_FACING_TIMESCALE = 4.0f; // very slow average float tau = deltaTime / HMD_FACING_TIMESCALE; _headControllerFacingMovingAverage = lerp(_headControllerFacingMovingAverage, _headControllerFacing, tau); @@ -404,17 +422,12 @@ void MyAvatar::update(float deltaTime) { _rotationChanged = usecTimestampNow(); _smoothOrientationTimer += deltaTime; } - setStandingHeightMode(computeStandingHeightMode(getControllerPoseInAvatarFrame(controller::Action::HEAD))); - setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); - glm::vec3 worldFacingAverage = - transformVectorFast(getSensorToWorldMatrix(), - glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); - glm::vec3 worldFacing = - transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacing.x, 0.0f, _headControllerFacing.y)); + glm::vec3 worldFacingAverage = transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); + glm::vec3 worldFacing = transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacing.x, 0.0f, _headControllerFacing.y)); DebugDraw::getInstance().drawRay(worldHeadPos, worldHeadPos + worldFacing, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)); DebugDraw::getInstance().drawRay(worldHeadPos, worldHeadPos + worldFacingAverage, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f)); #endif @@ -432,12 +445,12 @@ void MyAvatar::update(float deltaTime) { emit positionGoneTo(); // Run safety tests as soon as we can after goToLocation, or clear if we're not colliding. _physicsSafetyPending = getCollisionsEnabled(); - _characterController.recomputeFlying(); // In case we've gone to into the sky. + _characterController.recomputeFlying(); // In case we've gone to into the sky. } if (_physicsSafetyPending && qApp->isPhysicsEnabled() && _characterController.isEnabledAndReady()) { // When needed and ready, arrange to check and fix. _physicsSafetyPending = false; - safeLanding(_goToPosition); // no-op if already safe + safeLanding(_goToPosition); // no-op if already safe } Head* head = getHead(); @@ -451,13 +464,12 @@ void MyAvatar::update(float deltaTime) { setAudioLoudness(audio->getLastInputLoudness()); setAudioAverageLoudness(audio->getAudioAverageInputLoudness()); - glm::vec3 halfBoundingBoxDimensions(_characterController.getCapsuleRadius(), _characterController.getCapsuleHalfHeight(), - _characterController.getCapsuleRadius()); + glm::vec3 halfBoundingBoxDimensions(_characterController.getCapsuleRadius(), _characterController.getCapsuleHalfHeight(), _characterController.getCapsuleRadius()); // This might not be right! Isn't the capsule local offset in avatar space? -HRS 5/26/17 halfBoundingBoxDimensions += _characterController.getCapsuleLocalOffset(); QMetaObject::invokeMethod(audio.data(), "setAvatarBoundingBoxParameters", - Q_ARG(glm::vec3, (getWorldPosition() - halfBoundingBoxDimensions)), - Q_ARG(glm::vec3, (halfBoundingBoxDimensions * 2.0f))); + Q_ARG(glm::vec3, (getWorldPosition() - halfBoundingBoxDimensions)), + Q_ARG(glm::vec3, (halfBoundingBoxDimensions*2.0f))); if (getIdentityDataChanged()) { sendIdentityPacket(); @@ -469,20 +481,23 @@ void MyAvatar::update(float deltaTime) { currentEnergy -= getAccelerationEnergy(); currentEnergy -= getAudioEnergy(); - if (didTeleport()) { + if(didTeleport()) { currentEnergy = 0.0f; } - currentEnergy = max(0.0f, min(currentEnergy, 1.0f)); + currentEnergy = max(0.0f, min(currentEnergy,1.0f)); emit energyChanged(currentEnergy); updateEyeContactTarget(deltaTime); } void MyAvatar::updateEyeContactTarget(float deltaTime) { + _eyeContactTargetTimer -= deltaTime; if (_eyeContactTargetTimer < 0.0f) { + const float CHANCE_OF_CHANGING_TARGET = 0.01f; if (randFloat() < CHANCE_OF_CHANGING_TARGET) { + float const FIFTY_FIFTY_CHANCE = 0.5f; float const EYE_TO_MOUTH_CHANCE = 0.25f; switch (_eyeContactTarget) { @@ -607,7 +622,7 @@ void MyAvatar::simulate(float deltaTime) { if (!_skeletonModel->hasSkeleton()) { // All the simulation that can be done has been done - getHead()->setPosition(getWorldPosition()); // so audio-position isn't 0,0,0 + getHead()->setPosition(getWorldPosition()); // so audio-position isn't 0,0,0 return; } @@ -686,9 +701,8 @@ void MyAvatar::simulate(float deltaTime) { EntityItemProperties descendantProperties; descendantProperties.setQueryAACube(descendant->getQueryAACube()); descendantProperties.setLastEdited(now); - packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, - entityDescendant->getID(), descendantProperties); - entityDescendant->setLastBroadcast(now); // for debug/physics status icons + packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entityDescendant->getID(), descendantProperties); + entityDescendant->setLastBroadcast(now); // for debug/physics status icons } }); } @@ -719,7 +733,8 @@ void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) { _hmdSensorMatrix = hmdSensorMatrix; auto newHmdSensorPosition = extractTranslation(hmdSensorMatrix); - if (newHmdSensorPosition != getHMDSensorPosition() && glm::length(newHmdSensorPosition) > MAX_HMD_ORIGIN_DISTANCE) { + if (newHmdSensorPosition != getHMDSensorPosition() && + glm::length(newHmdSensorPosition) > MAX_HMD_ORIGIN_DISTANCE) { qWarning() << "Invalid HMD sensor position " << newHmdSensorPosition; // Ignore unreasonable HMD sensor data return; @@ -751,11 +766,11 @@ void MyAvatar::updateJointFromController(controller::Action poseKey, ThreadSafeV // update sensor to world matrix from current body position and hmd sensor. // This is so the correct camera can be used for rendering. void MyAvatar::updateSensorToWorldMatrix() { + // update the sensor mat so that the body position will end up in the desired // position when driven from the head. float sensorToWorldScale = getEyeHeight() / getUserEyeHeight(); - glm::mat4 desiredMat = - createMatFromScaleQuatAndPos(glm::vec3(sensorToWorldScale), getWorldOrientation(), getWorldPosition()); + glm::mat4 desiredMat = createMatFromScaleQuatAndPos(glm::vec3(sensorToWorldScale), getWorldOrientation(), getWorldPosition()); _sensorToWorldMatrix = desiredMat * glm::inverse(_bodySensorMatrix); bool hasSensorToWorldScaleChanged = false; @@ -773,10 +788,11 @@ void MyAvatar::updateSensorToWorldMatrix() { _sensorToWorldMatrixCache.set(_sensorToWorldMatrix); updateJointFromController(controller::Action::LEFT_HAND, _controllerLeftHandMatrixCache); updateJointFromController(controller::Action::RIGHT_HAND, _controllerRightHandMatrixCache); - + if (hasSensorToWorldScaleChanged) { emit sensorToWorldScaleChanged(sensorToWorldScale); } + } // Update avatar head rotation with sensor data @@ -801,7 +817,8 @@ void MyAvatar::updateFromTrackers(float deltaTime) { const float TRACKER_YAW_TURN_SENSITIVITY = 0.5f; const float TRACKER_MIN_YAW_TURN = 15.0f; const float TRACKER_MAX_YAW_TURN = 50.0f; - if ((fabs(estimatedRotation.y) > TRACKER_MIN_YAW_TURN) && (fabs(estimatedRotation.y) < TRACKER_MAX_YAW_TURN)) { + if ( (fabs(estimatedRotation.y) > TRACKER_MIN_YAW_TURN) && + (fabs(estimatedRotation.y) < TRACKER_MAX_YAW_TURN) ) { if (estimatedRotation.y > 0.0f) { _bodyYawDelta += (estimatedRotation.y - TRACKER_MIN_YAW_TURN) * TRACKER_YAW_TURN_SENSITIVITY; } else { @@ -816,6 +833,7 @@ void MyAvatar::updateFromTrackers(float deltaTime) { // their head only 30 degrees or so, this may correspond to a 90 degree field of view. // Note that roll is magnified by a constant because it is not related to field of view. + Head* head = getHead(); if (inHmd || playing) { head->setDeltaPitch(estimatedRotation.x); @@ -878,8 +896,8 @@ controller::Pose MyAvatar::getRightHandTipPose() const { } glm::vec3 MyAvatar::worldToJointPoint(const glm::vec3& position, const int jointIndex) const { - glm::vec3 jointPos = getWorldPosition(); //default value if no or invalid joint specified - glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified + glm::vec3 jointPos = getWorldPosition();//default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified if (jointIndex != -1) { if (_skeletonModel->getJointPositionInWorldFrame(jointIndex, jointPos)) { _skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot); @@ -894,7 +912,7 @@ glm::vec3 MyAvatar::worldToJointPoint(const glm::vec3& position, const int joint } glm::vec3 MyAvatar::worldToJointDirection(const glm::vec3& worldDir, const int jointIndex) const { - glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { qWarning() << "Invalid joint index specified: " << jointIndex; } @@ -904,7 +922,7 @@ glm::vec3 MyAvatar::worldToJointDirection(const glm::vec3& worldDir, const int j } glm::quat MyAvatar::worldToJointRotation(const glm::quat& worldRot, const int jointIndex) const { - glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { qWarning() << "Invalid joint index specified: " << jointIndex; } @@ -913,8 +931,8 @@ glm::quat MyAvatar::worldToJointRotation(const glm::quat& worldRot, const int jo } glm::vec3 MyAvatar::jointToWorldPoint(const glm::vec3& jointSpacePos, const int jointIndex) const { - glm::vec3 jointPos = getWorldPosition(); //default value if no or invalid joint specified - glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified + glm::vec3 jointPos = getWorldPosition();//default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified if (jointIndex != -1) { if (_skeletonModel->getJointPositionInWorldFrame(jointIndex, jointPos)) { @@ -931,7 +949,7 @@ glm::vec3 MyAvatar::jointToWorldPoint(const glm::vec3& jointSpacePos, const int } glm::vec3 MyAvatar::jointToWorldDirection(const glm::vec3& jointSpaceDir, const int jointIndex) const { - glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { qWarning() << "Invalid joint index specified: " << jointIndex; } @@ -940,7 +958,7 @@ glm::vec3 MyAvatar::jointToWorldDirection(const glm::vec3& jointSpaceDir, const } glm::quat MyAvatar::jointToWorldRotation(const glm::quat& jointSpaceRot, const int jointIndex) const { - glm::quat jointRot = getWorldOrientation(); //default value if no or invalid joint specified + glm::quat jointRot = getWorldOrientation();//default value if no or invalid joint specified if ((jointIndex != -1) && (!_skeletonModel->getJointRotationInWorldFrame(jointIndex, jointRot))) { qWarning() << "Invalid joint index specified: " << jointIndex; } @@ -952,15 +970,15 @@ glm::quat MyAvatar::jointToWorldRotation(const glm::quat& jointSpaceRot, const i void MyAvatar::render(RenderArgs* renderArgs) { // don't render if we've been asked to disable local rendering if (!_shouldRender) { - return; // exit early + return; // exit early } Avatar::render(renderArgs); } void MyAvatar::overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "overrideAnimation", Q_ARG(const QString&, url), Q_ARG(float, fps), Q_ARG(bool, loop), - Q_ARG(float, firstFrame), Q_ARG(float, lastFrame)); + QMetaObject::invokeMethod(this, "overrideAnimation", Q_ARG(const QString&, url), Q_ARG(float, fps), + Q_ARG(bool, loop), Q_ARG(float, firstFrame), Q_ARG(float, lastFrame)); return; } _skeletonModel->getRig().overrideAnimation(url, fps, loop, firstFrame, lastFrame); @@ -983,12 +1001,8 @@ QStringList MyAvatar::getAnimationRoles() { return _skeletonModel->getRig().getAnimationRoles(); } -void MyAvatar::overrideRoleAnimation(const QString& role, - const QString& url, - float fps, - bool loop, - float firstFrame, - float lastFrame) { +void MyAvatar::overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, + float firstFrame, float lastFrame) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "overrideRoleAnimation", Q_ARG(const QString&, role), Q_ARG(const QString&, url), Q_ARG(float, fps), Q_ARG(bool, loop), Q_ARG(float, firstFrame), Q_ARG(float, lastFrame)); @@ -1008,10 +1022,11 @@ void MyAvatar::restoreRoleAnimation(const QString& role) { void MyAvatar::saveAvatarUrl() { Settings settings; settings.beginGroup("Avatar"); - if (qApp->getSaveAvatarOverrideUrl() || !qApp->getAvatarOverrideUrl().isValid()) { - settings.setValue("fullAvatarURL", _fullAvatarURLFromPreferences == AvatarData::defaultFullAvatarModelUrl() - ? "" - : _fullAvatarURLFromPreferences.toString()); + if (qApp->getSaveAvatarOverrideUrl() || !qApp->getAvatarOverrideUrl().isValid() ) { + settings.setValue("fullAvatarURL", + _fullAvatarURLFromPreferences == AvatarData::defaultFullAvatarModelUrl() ? + "" : + _fullAvatarURLFromPreferences.toString()); } settings.endGroup(); } @@ -1031,10 +1046,11 @@ void MyAvatar::saveData() { // only save the fullAvatarURL if it has not been overwritten on command line // (so the overrideURL is not valid), or it was overridden _and_ we specified // --replaceAvatarURL (so _saveAvatarOverrideUrl is true) - if (qApp->getSaveAvatarOverrideUrl() || !qApp->getAvatarOverrideUrl().isValid()) { - settings.setValue("fullAvatarURL", _fullAvatarURLFromPreferences == AvatarData::defaultFullAvatarModelUrl() - ? "" - : _fullAvatarURLFromPreferences.toString()); + if (qApp->getSaveAvatarOverrideUrl() || !qApp->getAvatarOverrideUrl().isValid() ) { + settings.setValue("fullAvatarURL", + _fullAvatarURLFromPreferences == AvatarData::defaultFullAvatarModelUrl() ? + "" : + _fullAvatarURLFromPreferences.toString()); } settings.setValue("fullAvatarModelName", _fullAvatarModelName); @@ -1242,7 +1258,7 @@ void MyAvatar::loadData() { settings.endGroup(); setEnableMeshVisible(Menu::getInstance()->isOptionChecked(MenuOption::MeshVisible)); - _follow.setToggleHipsFollowing(Menu::getInstance()->isOptionChecked(MenuOption::ToggleHipsFollowing)); + _follow.setToggleHipsFollowing (Menu::getInstance()->isOptionChecked(MenuOption::ToggleHipsFollowing)); setEnableDebugDrawBaseOfSupport(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawBaseOfSupport)); setEnableDebugDrawDefaultPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawDefaultPose)); setEnableDebugDrawAnimPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawAnimPose)); @@ -1310,7 +1326,7 @@ AttachmentData MyAvatar::loadAttachmentData(const QUrl& modelURL, const QString& int MyAvatar::parseDataFromBuffer(const QByteArray& buffer) { qCDebug(interfaceapp) << "Error: ignoring update packet for MyAvatar" - << " packetLength = " << buffer.size(); + << " packetLength = " << buffer.size(); // this packet is just bad, so we pretend that we unpacked it ALL return buffer.size(); } @@ -1345,17 +1361,18 @@ void MyAvatar::updateLookAtTargetAvatar() { bool isCurrentTarget = avatar->getIsLookAtTarget(); float distanceTo = glm::length(avatar->getHead()->getEyePosition() - cameraPosition); avatar->setIsLookAtTarget(false); - if (!avatar->isMyAvatar() && avatar->isInitialized() && (distanceTo < GREATEST_LOOKING_AT_DISTANCE * getModelScale())) { + if (!avatar->isMyAvatar() && avatar->isInitialized() && + (distanceTo < GREATEST_LOOKING_AT_DISTANCE * getModelScale())) { float radius = glm::length(avatar->getHead()->getEyePosition() - avatar->getHead()->getRightEyePosition()); - float angleTo = - coneSphereAngle(getHead()->getEyePosition(), lookForward, avatar->getHead()->getEyePosition(), radius); + float angleTo = coneSphereAngle(getHead()->getEyePosition(), lookForward, avatar->getHead()->getEyePosition(), radius); if (angleTo < (smallestAngleTo * (isCurrentTarget ? KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR : 1.0f))) { _lookAtTargetAvatar = avatarPointer; _targetAvatarPosition = avatarPointer->getWorldPosition(); } if (_lookAtSnappingEnabled && avatar->getLookAtSnappingEnabled() && isLookingAtMe(avatar)) { + // Alter their gaze to look directly at my camera; this looks more natural than looking at my avatar's face. - glm::vec3 lookAtPosition = avatar->getHead()->getLookAtPosition(); // A position, in world space, on my avatar. + glm::vec3 lookAtPosition = avatar->getHead()->getLookAtPosition(); // A position, in world space, on my avatar. // The camera isn't at the point midway between the avatar eyes. (Even without an HMD, the head can be offset a bit.) // Let's get everything to world space: @@ -1366,12 +1383,12 @@ void MyAvatar::updateLookAtTargetAvatar() { // (We will be adding that offset to the camera position, after making some other adjustments.) glm::vec3 gazeOffset = lookAtPosition - getHead()->getEyePosition(); - ViewFrustum viewFrustum; - qApp->copyViewFrustum(viewFrustum); + ViewFrustum viewFrustum; + qApp->copyViewFrustum(viewFrustum); - glm::vec3 viewPosition = viewFrustum.getPosition(); + glm::vec3 viewPosition = viewFrustum.getPosition(); #if DEBUG_ALWAYS_LOOKAT_EYES_NOT_CAMERA - viewPosition = (avatarLeftEye + avatarRightEye) / 2.0f; + viewPosition = (avatarLeftEye + avatarRightEye) / 2.0f; #endif // scale gazeOffset by IPD, if wearing an HMD. if (qApp->isHMDMode()) { @@ -1438,7 +1455,7 @@ void MyAvatar::setJointRotations(const QVector& jointRotations) { void MyAvatar::setJointData(int index, const glm::quat& rotation, const glm::vec3& translation) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setJointData", Q_ARG(int, index), Q_ARG(const glm::quat&, rotation), - Q_ARG(const glm::vec3&, translation)); + Q_ARG(const glm::vec3&, translation)); return; } // HACK: ATM only JS scripts call setJointData() on MyAvatar so we hardcode the priority @@ -1474,7 +1491,7 @@ void MyAvatar::clearJointData(int index) { void MyAvatar::setJointData(const QString& name, const glm::quat& rotation, const glm::vec3& translation) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setJointData", Q_ARG(QString, name), Q_ARG(const glm::quat&, rotation), - Q_ARG(const glm::vec3&, translation)); + Q_ARG(const glm::vec3&, translation)); return; } writeLockWithNamedJointIndex(name, [&](int index) { @@ -1510,7 +1527,9 @@ void MyAvatar::clearJointData(const QString& name) { QMetaObject::invokeMethod(this, "clearJointData", Q_ARG(QString, name)); return; } - writeLockWithNamedJointIndex(name, [&](int index) { _skeletonModel->getRig().clearJointAnimationPriority(index); }); + writeLockWithNamedJointIndex(name, [&](int index) { + _skeletonModel->getRig().clearJointAnimationPriority(index); + }); } void MyAvatar::clearJointsData() { @@ -1533,25 +1552,24 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _cauterizationNeedsUpdate = true; std::shared_ptr skeletonConnection = std::make_shared(); - *skeletonConnection = QObject::connect(_skeletonModel.get(), &SkeletonModel::skeletonLoaded, - [this, skeletonModelChangeCount, skeletonConnection]() { - if (skeletonModelChangeCount == _skeletonModelChangeCount) { - if (_fullAvatarModelName.isEmpty()) { - // Store the FST file name into preferences - const auto& mapping = _skeletonModel->getGeometry()->getMapping(); - if (mapping.value("name").isValid()) { - _fullAvatarModelName = mapping.value("name").toString(); - } - } + *skeletonConnection = QObject::connect(_skeletonModel.get(), &SkeletonModel::skeletonLoaded, [this, skeletonModelChangeCount, skeletonConnection]() { + if (skeletonModelChangeCount == _skeletonModelChangeCount) { - initHeadBones(); - _skeletonModel->setCauterizeBoneSet(_headBoneSet); - _fstAnimGraphOverrideUrl = - _skeletonModel->getGeometry()->getAnimGraphOverrideUrl(); - initAnimGraph(); - } - QObject::disconnect(*skeletonConnection); - }); + if (_fullAvatarModelName.isEmpty()) { + // Store the FST file name into preferences + const auto& mapping = _skeletonModel->getGeometry()->getMapping(); + if (mapping.value("name").isValid()) { + _fullAvatarModelName = mapping.value("name").toString(); + } + } + + initHeadBones(); + _skeletonModel->setCauterizeBoneSet(_headBoneSet); + _fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl(); + initAnimGraph(); + } + QObject::disconnect(*skeletonConnection); + }); saveAvatarUrl(); emit skeletonChanged(); emit skeletonModelURLChanged(); @@ -1588,6 +1606,7 @@ QVariantList MyAvatar::getAvatarEntitiesVariant() { return avatarEntitiesData; } + void MyAvatar::resetFullAvatarURL() { auto lastAvatarURL = getFullAvatarURLFromPreferences(); auto lastAvatarName = getFullAvatarModelName(); @@ -1596,8 +1615,11 @@ void MyAvatar::resetFullAvatarURL() { } void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName) { + if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "useFullAvatarURL", Q_ARG(const QUrl&, fullAvatarURL), Q_ARG(const QString&, modelName)); + BLOCKING_INVOKE_METHOD(this, "useFullAvatarURL", + Q_ARG(const QUrl&, fullAvatarURL), + Q_ARG(const QString&, modelName)); return; } @@ -1617,7 +1639,8 @@ void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelN void MyAvatar::setAttachmentData(const QVector& attachmentData) { if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "setAttachmentData", Q_ARG(const QVector, attachmentData)); + BLOCKING_INVOKE_METHOD(this, "setAttachmentData", + Q_ARG(const QVector, attachmentData)); return; } Avatar::setAttachmentData(attachmentData); @@ -1662,7 +1685,7 @@ controller::Pose MyAvatar::getControllerPoseInSensorFrame(controller::Action act if (iter != _controllerPoseMap.end()) { return iter->second; } else { - return controller::Pose(); // invalid pose + return controller::Pose(); // invalid pose } } @@ -1671,7 +1694,7 @@ controller::Pose MyAvatar::getControllerPoseInWorldFrame(controller::Action acti if (pose.valid) { return pose.transform(getSensorToWorldMatrix()); } else { - return controller::Pose(); // invalid pose + return controller::Pose(); // invalid pose } } @@ -1681,7 +1704,7 @@ controller::Pose MyAvatar::getControllerPoseInAvatarFrame(controller::Action act glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getWorldOrientation(), getWorldPosition())); return pose.transform(invAvatarMatrix); } else { - return controller::Pose(); // invalid pose + return controller::Pose(); // invalid pose } } @@ -1697,7 +1720,7 @@ void MyAvatar::updateMotors() { float verticalMotorTimescale; if (_characterController.getState() == CharacterController::State::Hover || - _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { horizontalMotorTimescale = FLYING_MOTOR_TIMESCALE; verticalMotorTimescale = FLYING_MOTOR_TIMESCALE; } else { @@ -1707,7 +1730,7 @@ void MyAvatar::updateMotors() { if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { if (_characterController.getState() == CharacterController::State::Hover || - _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { motorRotation = getMyHead()->getHeadOrientation(); } else { // non-hovering = walking: follow camera twist about vertical but not lift @@ -1715,15 +1738,14 @@ void MyAvatar::updateMotors() { // however, we need to perform the decomposition in the avatar-frame // using the local UP axis and then transform back into world-frame glm::quat orientation = getWorldOrientation(); - glm::quat headOrientation = glm::inverse(orientation) * getMyHead()->getHeadOrientation(); // avatar-frame + glm::quat headOrientation = glm::inverse(orientation) * getMyHead()->getHeadOrientation(); // avatar-frame glm::quat liftRotation; swingTwistDecomposition(headOrientation, Vectors::UNIT_Y, liftRotation, motorRotation); motorRotation = orientation * motorRotation; } if (_isPushing || _isBraking || !_isBeingPushed) { - _characterController.addMotor(_actionMotorVelocity, motorRotation, horizontalMotorTimescale, - verticalMotorTimescale); + _characterController.addMotor(_actionMotorVelocity, motorRotation, horizontalMotorTimescale, verticalMotorTimescale); } else { // _isBeingPushed must be true --> disable action motor by giving it a long timescale, // otherwise it's attempt to "stand in in place" could defeat scripted motor/thrusts @@ -1743,8 +1765,7 @@ void MyAvatar::updateMotors() { _characterController.addMotor(_scriptedMotorVelocity, motorRotation, _scriptedMotorTimescale); } else { // dynamic mode - _characterController.addMotor(_scriptedMotorVelocity, motorRotation, horizontalMotorTimescale, - verticalMotorTimescale); + _characterController.addMotor(_scriptedMotorVelocity, motorRotation, horizontalMotorTimescale, verticalMotorTimescale); } } @@ -1849,7 +1870,8 @@ void MyAvatar::setScriptedMotorVelocity(const glm::vec3& velocity) { void MyAvatar::setScriptedMotorTimescale(float timescale) { // we clamp the timescale on the large side (instead of just the low side) to prevent // obnoxiously large values from introducing NaN into avatar's velocity - _scriptedMotorTimescale = glm::clamp(timescale, MIN_SCRIPTED_MOTOR_TIMESCALE, DEFAULT_SCRIPTED_MOTOR_TIMESCALE); + _scriptedMotorTimescale = glm::clamp(timescale, MIN_SCRIPTED_MOTOR_TIMESCALE, + DEFAULT_SCRIPTED_MOTOR_TIMESCALE); } void MyAvatar::setScriptedMotorFrame(QString frame) { @@ -1890,14 +1912,10 @@ SharedSoundPointer MyAvatar::getCollisionSound() { return _collisionSound; } -void MyAvatar::attach(const QString& modelURL, - const QString& jointName, - const glm::vec3& translation, - const glm::quat& rotation, - float scale, - bool isSoft, - bool allowDuplicates, - bool useSaved) { +void MyAvatar::attach(const QString& modelURL, const QString& jointName, + const glm::vec3& translation, const glm::quat& rotation, + float scale, bool isSoft, + bool allowDuplicates, bool useSaved) { if (QThread::currentThread() != thread()) { Avatar::attach(modelURL, jointName, translation, rotation, scale, isSoft, allowDuplicates, useSaved); return; @@ -1905,8 +1923,10 @@ void MyAvatar::attach(const QString& modelURL, if (useSaved) { AttachmentData attachment = loadAttachmentData(modelURL, jointName); if (attachment.isValid()) { - Avatar::attach(modelURL, attachment.jointName, attachment.translation, attachment.rotation, attachment.scale, - attachment.isSoft, allowDuplicates, useSaved); + Avatar::attach(modelURL, attachment.jointName, + attachment.translation, attachment.rotation, + attachment.scale, attachment.isSoft, + allowDuplicates, useSaved); return; } } @@ -1959,6 +1979,7 @@ QUrl MyAvatar::getAnimGraphUrl() const { } void MyAvatar::setAnimGraphUrl(const QUrl& url) { + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setAnimGraphUrl", Q_ARG(QUrl, url)); return; @@ -1968,7 +1989,7 @@ void MyAvatar::setAnimGraphUrl(const QUrl& url) { return; } destroyAnimGraph(); - _skeletonModel->reset(); // Why is this necessary? Without this, we crash in the next render. + _skeletonModel->reset(); // Why is this necessary? Without this, we crash in the next render. _currentAnimGraphUrl.set(url); _skeletonModel->getRig().initAnimGraph(url); @@ -1995,16 +2016,18 @@ void MyAvatar::destroyAnimGraph() { } void MyAvatar::animGraphLoaded() { - _bodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. - updateSensorToWorldMatrix(); // Uses updated position/orientation and _bodySensorMatrix changes + _bodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation.. + updateSensorToWorldMatrix(); // Uses updated position/orientation and _bodySensorMatrix changes _isAnimatingScale = true; _cauterizationNeedsUpdate = true; disconnect(&(_skeletonModel->getRig()), SIGNAL(onLoadComplete()), this, SLOT(animGraphLoaded())); } void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { + Avatar::postUpdate(deltaTime, scene); if (_enableDebugDrawDefaultPose || _enableDebugDrawAnimPose) { + auto animSkeleton = _skeletonModel->getRig().getAnimSkeleton(); // the rig is in the skeletonModel frame @@ -2012,8 +2035,7 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { if (_enableDebugDrawDefaultPose && animSkeleton) { glm::vec4 gray(0.2f, 0.2f, 0.2f, 0.2f); - AnimDebugDraw::getInstance().addAbsolutePoses("myAvatarDefaultPoses", animSkeleton, - _skeletonModel->getRig().getAbsoluteDefaultPoses(), xform, gray); + AnimDebugDraw::getInstance().addAbsolutePoses("myAvatarDefaultPoses", animSkeleton, _skeletonModel->getRig().getAbsoluteDefaultPoses(), xform, gray); } if (_enableDebugDrawAnimPose && animSkeleton) { @@ -2034,15 +2056,13 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { auto rightHandPose = getControllerPoseInWorldFrame(controller::Action::RIGHT_HAND); if (leftHandPose.isValid()) { - DebugDraw::getInstance().addMarker("leftHandController", leftHandPose.getRotation(), leftHandPose.getTranslation(), - glm::vec4(1)); + DebugDraw::getInstance().addMarker("leftHandController", leftHandPose.getRotation(), leftHandPose.getTranslation(), glm::vec4(1)); } else { DebugDraw::getInstance().removeMarker("leftHandController"); } if (rightHandPose.isValid()) { - DebugDraw::getInstance().addMarker("rightHandController", rightHandPose.getRotation(), - rightHandPose.getTranslation(), glm::vec4(1)); + DebugDraw::getInstance().addMarker("rightHandController", rightHandPose.getRotation(), rightHandPose.getTranslation(), glm::vec4(1)); } else { DebugDraw::getInstance().removeMarker("rightHandController"); } @@ -2059,9 +2079,14 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { AnimPose rigToWorldPose(glm::vec3(1.0f), getWorldOrientation() * Quaternions::Y_180, getWorldPosition()); const int NUM_DEBUG_COLORS = 8; const glm::vec4 DEBUG_COLORS[NUM_DEBUG_COLORS] = { - glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), glm::vec4(0.0f, 1.0f, 0.0f, 1.0f), - glm::vec4(0.25f, 0.25f, 1.0f, 1.0f), glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), glm::vec4(0.25f, 1.0f, 1.0f, 1.0f), - glm::vec4(1.0f, 0.25f, 1.0f, 1.0f), glm::vec4(1.0f, 0.65f, 0.0f, 1.0f) // Orange you glad I added this color? + glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), + glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), + glm::vec4(0.0f, 1.0f, 0.0f, 1.0f), + glm::vec4(0.25f, 0.25f, 1.0f, 1.0f), + glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), + glm::vec4(0.25f, 1.0f, 1.0f, 1.0f), + glm::vec4(1.0f, 0.25f, 1.0f, 1.0f), + glm::vec4(1.0f, 0.65f, 0.0f, 1.0f) // Orange you glad I added this color? }; if (_skeletonModel && _skeletonModel->isLoaded()) { @@ -2083,6 +2108,7 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { } void MyAvatar::preDisplaySide(const RenderArgs* renderArgs) { + // toggle using the cauterizedBones depending on where the camera is and the rendering pass type. const bool shouldDrawHead = shouldRenderHead(renderArgs); if (shouldDrawHead != _prevShouldDrawHead) { @@ -2146,15 +2172,8 @@ void MyAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) _headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement); } -void MyAvatar::setRotationRecenterFilterLength(float length) { - _rotationRecenterFilterLength = length; -} - -void MyAvatar::setRotationThreshold(float angleRadians) { - _rotationThreshold = angleRadians; -} - void MyAvatar::updateOrientation(float deltaTime) { + // Smoothly rotate body with arrow keys float targetSpeed = getDriveKey(YAW) * _yawSpeed; if (targetSpeed != 0.0f) { @@ -2181,6 +2200,7 @@ void MyAvatar::updateOrientation(float deltaTime) { float totalBodyYaw = _bodyYawDelta * deltaTime; + // Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll // get an instantaneous 15 degree turn. If you keep holding the key down you'll get another // snap turn every half second. @@ -2191,8 +2211,8 @@ void MyAvatar::updateOrientation(float deltaTime) { } // Use head/HMD roll to turn while flying, but not when standing still. - if (qApp->isHMDMode() && getCharacterController()->getState() == CharacterController::State::Hover && - _hmdRollControlEnabled && hasDriveInput()) { + if (qApp->isHMDMode() && getCharacterController()->getState() == CharacterController::State::Hover && _hmdRollControlEnabled && hasDriveInput()) { + // Turn with head roll. const float MIN_CONTROL_SPEED = 2.0f * getSensorToWorldScale(); // meters / sec const glm::vec3 characterForward = getWorldOrientation() * Vectors::UNIT_NEG_Z; @@ -2200,6 +2220,7 @@ void MyAvatar::updateOrientation(float deltaTime) { // only enable roll-turns if we are moving forward or backward at greater then MIN_CONTROL_SPEED if (fabsf(forwardSpeed) >= MIN_CONTROL_SPEED) { + float direction = forwardSpeed > 0.0f ? 1.0f : -1.0f; float rollAngle = glm::degrees(asinf(glm::dot(IDENTITY_UP, _hmdSensorOrientation * IDENTITY_RIGHT))); float rollSign = rollAngle < 0.0f ? -1.0f : 1.0f; @@ -2253,8 +2274,8 @@ void MyAvatar::updateOrientation(float deltaTime) { void MyAvatar::updateActionMotor(float deltaTime) { bool thrustIsPushing = (glm::length2(_thrust) > EPSILON); - bool scriptedMotorIsPushing = - (_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED) && _scriptedMotorTimescale < MAX_CHARACTER_MOTOR_TIMESCALE; + bool scriptedMotorIsPushing = (_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED) + && _scriptedMotorTimescale < MAX_CHARACTER_MOTOR_TIMESCALE; _isBeingPushed = thrustIsPushing || scriptedMotorIsPushing; if (_isPushing || _isBeingPushed) { // we don't want the motor to brake if a script is pushing the avatar around @@ -2274,7 +2295,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { glm::vec3 direction = forward + right; if (state == CharacterController::State::Hover || - _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP; direction += up; } @@ -2295,7 +2316,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { float motorSpeed = glm::length(_actionMotorVelocity); float finalMaxMotorSpeed = getSensorToWorldScale() * DEFAULT_AVATAR_MAX_FLYING_SPEED * _walkSpeedScalar; - float speedGrowthTimescale = 2.0f; + float speedGrowthTimescale = 2.0f; float speedIncreaseFactor = 1.8f * _walkSpeedScalar; motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale, 0.0f, 1.0f) * speedIncreaseFactor; const float maxBoostSpeed = getSensorToWorldScale() * MAX_BOOST_SPEED; @@ -2312,7 +2333,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { _actionMotorVelocity = motorSpeed * direction; } else { // we're interacting with a floor --> simple horizontal speed and exponential decay - _actionMotorVelocity = getSensorToWorldScale() * (_walkSpeed.get() * _walkSpeedScalar) * direction; + _actionMotorVelocity = getSensorToWorldScale() * (_walkSpeed.get() * _walkSpeedScalar) * direction; } float previousBoomLength = _boomLength; @@ -2334,7 +2355,7 @@ void MyAvatar::updatePosition(float deltaTime) { vec3 velocity = getWorldVelocity(); float sensorToWorldScale = getSensorToWorldScale(); float sensorToWorldScale2 = sensorToWorldScale * sensorToWorldScale; - const float MOVING_SPEED_THRESHOLD_SQUARED = 0.0001f; // 0.01 m/s + const float MOVING_SPEED_THRESHOLD_SQUARED = 0.0001f; // 0.01 m/s if (!_characterController.isEnabledAndReady()) { // _characterController is not in physics simulation but it can still compute its target velocity updateMotors(); @@ -2362,17 +2383,12 @@ void MyAvatar::updatePosition(float deltaTime) { } } -void MyAvatar::updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency) { +void MyAvatar::updateCollisionSound(const glm::vec3 &penetration, float deltaTime, float frequency) { // COLLISION SOUND API in Audio has been removed } -bool findAvatarAvatarPenetration(const glm::vec3 positionA, - float radiusA, - float heightA, - const glm::vec3 positionB, - float radiusB, - float heightB, - glm::vec3& penetration) { +bool findAvatarAvatarPenetration(const glm::vec3 positionA, float radiusA, float heightA, + const glm::vec3 positionB, float radiusB, float heightB, glm::vec3& penetration) { glm::vec3 positionBA = positionB - positionA; float xzDistance = sqrt(positionBA.x * positionBA.x + positionBA.z * positionBA.z); if (xzDistance < (radiusA + radiusB)) { @@ -2544,9 +2560,9 @@ void MyAvatar::goToLocation(const QVariant& propertiesVar) { } void MyAvatar::goToLocation(const glm::vec3& newPosition, - bool hasOrientation, - const glm::quat& newOrientation, + bool hasOrientation, const glm::quat& newOrientation, bool shouldFaceLocation) { + // Most cases of going to a place or user go through this now. Some possible improvements to think about in the future: // - It would be nice if this used the same teleport steps and smoothing as in the teleport.js script, as long as it // still worked if the target is in the air. @@ -2560,15 +2576,15 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, // compute the position (e.g., so that if I'm on stage, going to me would compute an available seat in the audience rather than // being in my face on-stage). Note that this could work for going to an entity as well as to a person. - qCDebug(interfaceapp).nospace() << "MyAvatar goToLocation - moving to " << newPosition.x << ", " << newPosition.y << ", " - << newPosition.z; + qCDebug(interfaceapp).nospace() << "MyAvatar goToLocation - moving to " << newPosition.x << ", " + << newPosition.y << ", " << newPosition.z; _goToPending = true; _goToPosition = newPosition; _goToOrientation = getWorldOrientation(); if (hasOrientation) { - qCDebug(interfaceapp).nospace() << "MyAvatar goToLocation - new orientation is " << newOrientation.x << ", " - << newOrientation.y << ", " << newOrientation.z << ", " << newOrientation.w; + qCDebug(interfaceapp).nospace() << "MyAvatar goToLocation - new orientation is " + << newOrientation.x << ", " << newOrientation.y << ", " << newOrientation.z << ", " << newOrientation.w; // orient the user to face the target glm::quat quatOrientation = cancelOutRollAndPitch(newOrientation); @@ -2587,7 +2603,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, emit transformChanged(); } -void MyAvatar::goToLocationAndEnableCollisions(const glm::vec3& position) { // See use case in safeLanding. +void MyAvatar::goToLocationAndEnableCollisions(const glm::vec3& position) { // See use case in safeLanding. goToLocation(position); QMetaObject::invokeMethod(this, "setCollisionsEnabled", Qt::QueuedConnection, Q_ARG(bool, true)); } @@ -2613,29 +2629,29 @@ bool MyAvatar::safeLanding(const glm::vec3& position) { } if (!getCollisionsEnabled()) { goToLocation(better); // recurses on next update - } else { // If you try to go while stuck, physics will keep you stuck. + } else { // If you try to go while stuck, physics will keep you stuck. setCollisionsEnabled(false); // Don't goToLocation just yet. Yield so that physics can act on the above. - QMetaObject::invokeMethod(this, "goToLocationAndEnableCollisions", - Qt::QueuedConnection, // The equivalent of javascript nextTick - Q_ARG(glm::vec3, better)); - } - return true; + QMetaObject::invokeMethod(this, "goToLocationAndEnableCollisions", Qt::QueuedConnection, // The equivalent of javascript nextTick + Q_ARG(glm::vec3, better)); + } + return true; } // If position is not reliably safe from being stuck by physics, answer true and place a candidate better position in betterPositionOut. bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& betterPositionOut) { + // We begin with utilities and tests. The Algorithm in four parts is below. // NOTE: we use estimated avatar height here instead of the bullet capsule halfHeight, because // the domain avatar height limiting might not have taken effect yet on the actual bullet shape. auto halfHeight = 0.5f * getHeight(); if (halfHeight == 0) { - return false; // zero height avatar + return false; // zero height avatar } auto entityTree = DependencyManager::get()->getTree(); if (!entityTree) { - return false; // no entity tree + return false; // no entity tree } // More utilities. const auto capsuleCenter = positionIn; @@ -2647,8 +2663,7 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette betterPositionOut = upperIntersection + (up * halfHeight); return true; }; - auto findIntersection = [&](const glm::vec3& startPointIn, const glm::vec3& directionIn, glm::vec3& intersectionOut, - EntityItemID& entityIdOut, glm::vec3& normalOut) { + auto findIntersection = [&](const glm::vec3& startPointIn, const glm::vec3& directionIn, glm::vec3& intersectionOut, EntityItemID& entityIdOut, glm::vec3& normalOut) { OctreeElementPointer element; float distance; BoxFace face; @@ -2658,13 +2673,12 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette // See https://highfidelity.fogbugz.com/f/cases/5003/findRayIntersection-has-option-to-use-collidableOnly-but-doesn-t-actually-use-colliders const bool collidableOnly = true; const bool precisionPicking = true; - const auto lockType = Octree::Lock; // Should we refactor to take a lock just once? + const auto lockType = Octree::Lock; // Should we refactor to take a lock just once? bool* accurateResult = NULL; QVariantMap extraInfo; - EntityItemID entityID = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, - collidableOnly, precisionPicking, element, distance, face, - normalOut, extraInfo, lockType, accurateResult); + EntityItemID entityID = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, collidableOnly, precisionPicking, + element, distance, face, normalOut, extraInfo, lockType, accurateResult); if (entityID.isNull()) { return false; } @@ -2679,12 +2693,12 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette // We currently believe that physics will reliably push us out if our feet are embedded, // as long as our capsule center is out and there's room above us. Here we have those // conditions, so no need to check our feet below. - return false; // nothing above + return false; // nothing above } if (!findIntersection(capsuleCenter, down, lowerIntersection, lowerId, lowerNormal)) { // Our head may be embedded, but our center is out and there's room below. See corresponding comment above. - return false; // nothing below + return false; // nothing below } // See if we have room between entities above and below, but that we are not contained. @@ -2692,8 +2706,7 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette // I.e., we are in a clearing between two objects. if (isDown(upperNormal) && isUp(lowerNormal)) { auto spaceBetween = glm::distance(upperIntersection, lowerIntersection); - const float halfHeightFactor = - 2.25f; // Until case 5003 is fixed (and maybe after?), we need a fudge factor. Also account for content modelers not being precise. + const float halfHeightFactor = 2.25f; // Until case 5003 is fixed (and maybe after?), we need a fudge factor. Also account for content modelers not being precise. if (spaceBetween > (halfHeightFactor * halfHeight)) { // There is room for us to fit in that clearing. If there wasn't, physics would oscilate us between the objects above and below. // We're now going to iterate upwards through successive upperIntersections, testing to see if we're contained within the top surface of some entity. @@ -2705,7 +2718,7 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette ignore.push_back(upperId); if (!findIntersection(upperIntersection, up, upperIntersection, upperId, upperNormal)) { // We're not inside an entity, and from the nested tests, we have room between what is above and below. So position is good! - return false; // enough room + return false; // enough room } if (isUp(upperNormal)) { // This new intersection is the top surface of an entity that we have not yet seen, which means we're contained within it. @@ -2720,18 +2733,19 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette } } - include.push_back(upperId); // We're now looking for the intersection from above onto this entity. + include.push_back(upperId); // We're now looking for the intersection from above onto this entity. const float big = (float)TREE_SCALE; const auto skyHigh = up * big; auto fromAbove = capsuleCenter + skyHigh; if (!findIntersection(fromAbove, down, upperIntersection, upperId, upperNormal)) { - return false; // Unable to find a landing + return false; // Unable to find a landing } // Our arbitrary rule is to always go up. There's no need to look down or sideways for a "closer" safe candidate. return mustMove(); } void MyAvatar::updateMotionBehaviorFromMenu() { + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "updateMotionBehaviorFromMenu"); return; @@ -2781,6 +2795,7 @@ float MyAvatar::getAvatarScale() { } void MyAvatar::setAvatarScale(float val) { + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setAvatarScale", Q_ARG(float, val)); return; @@ -2790,6 +2805,7 @@ void MyAvatar::setAvatarScale(float val) { } void MyAvatar::setCollisionsEnabled(bool enabled) { + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled)); return; @@ -2915,7 +2931,7 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { // AJT: TODO: can remove this Y_180, if we remove the higher level one. glm::vec3 headToNeck = headOrientation * Quaternions::Y_180 * (localNeck - localHead); - glm::vec3 neckToRoot = headOrientationYawOnly * Quaternions::Y_180 * -localNeck; + glm::vec3 neckToRoot = headOrientationYawOnly * Quaternions::Y_180 * -localNeck; float invSensorToWorldScale = getUserEyeHeight() / getEyeHeight(); glm::vec3 bodyPos = headPosition + invSensorToWorldScale * (headToNeck + neckToRoot); @@ -2976,7 +2992,7 @@ glm::vec3 MyAvatar::computeCounterBalance() const { QString name; float weight; glm::vec3 position; - JointMass(){}; + JointMass() {}; JointMass(QString n, float w, glm::vec3 p) { name = n; weight = w; @@ -2996,14 +3012,12 @@ glm::vec3 MyAvatar::computeCounterBalance() const { tposeHead = getAbsoluteDefaultJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgHeadMass.name)); } if (_skeletonModel->getRig().indexOfJoint(cgLeftHandMass.name) != -1) { - cgLeftHandMass.position = - getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgLeftHandMass.name)); + cgLeftHandMass.position = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgLeftHandMass.name)); } else { cgLeftHandMass.position = DEFAULT_AVATAR_LEFTHAND_POS; } if (_skeletonModel->getRig().indexOfJoint(cgRightHandMass.name) != -1) { - cgRightHandMass.position = - getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgRightHandMass.name)); + cgRightHandMass.position = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgRightHandMass.name)); } else { cgRightHandMass.position = DEFAULT_AVATAR_RIGHTHAND_POS; } @@ -3012,8 +3026,7 @@ glm::vec3 MyAvatar::computeCounterBalance() const { } // find the current center of gravity position based on head and hand moments - glm::vec3 sumOfMoments = (cgHeadMass.weight * cgHeadMass.position) + (cgLeftHandMass.weight * cgLeftHandMass.position) + - (cgRightHandMass.weight * cgRightHandMass.position); + glm::vec3 sumOfMoments = (cgHeadMass.weight * cgHeadMass.position) + (cgLeftHandMass.weight * cgLeftHandMass.position) + (cgRightHandMass.weight * cgRightHandMass.position); float totalMass = cgHeadMass.weight + cgLeftHandMass.weight + cgRightHandMass.weight; glm::vec3 currentCg = (1.0f / totalMass) * sumOfMoments; @@ -3053,6 +3066,7 @@ glm::vec3 MyAvatar::computeCounterBalance() const { // headOrientation, headPosition and hipsPosition are in avatar space // returns the matrix of the hips in Avatar space static glm::mat4 computeNewHipsMatrix(glm::quat headOrientation, glm::vec3 headPosition, glm::vec3 hipsPosition) { + glm::quat bodyOrientation = computeBodyFacingFromHead(headOrientation, Vectors::UNIT_Y); const float MIX_RATIO = 0.3f; @@ -3062,7 +3076,10 @@ static glm::mat4 computeNewHipsMatrix(glm::quat headOrientation, glm::vec3 headP glm::vec3 spineVec = headPosition - hipsPosition; glm::vec3 u, v, w; generateBasisVectors(glm::normalize(spineVec), hipsFacing, u, v, w); - return glm::mat4(glm::vec4(w, 0.0f), glm::vec4(u, 0.0f), glm::vec4(v, 0.0f), glm::vec4(hipsPosition, 1.0f)); + return glm::mat4(glm::vec4(w, 0.0f), + glm::vec4(u, 0.0f), + glm::vec4(v, 0.0f), + glm::vec4(hipsPosition, 1.0f)); } static void drawBaseOfSupport(float baseOfSupportScale, float footLocal, glm::mat4 avatarToWorld) { @@ -3103,8 +3120,7 @@ glm::mat4 MyAvatar::deriveBodyUsingCgModel() const { if (_enableDebugDrawBaseOfSupport) { float scaleBaseOfSupport = getUserEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT; - glm::vec3 rightFootPositionLocal = - getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint("RightFoot")); + glm::vec3 rightFootPositionLocal = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint("RightFoot")); drawBaseOfSupport(scaleBaseOfSupport, rightFootPositionLocal.y, avatarToWorldMat); } @@ -3112,8 +3128,7 @@ glm::mat4 MyAvatar::deriveBodyUsingCgModel() const { const glm::vec3 cgHipsPosition = computeCounterBalance(); // find the new hips rotation using the new head-hips axis as the up axis - glm::mat4 avatarHipsMat = - computeNewHipsMatrix(glmExtractRotation(avatarHeadMat), extractTranslation(avatarHeadMat), cgHipsPosition); + glm::mat4 avatarHipsMat = computeNewHipsMatrix(glmExtractRotation(avatarHeadMat), extractTranslation(avatarHeadMat), cgHipsPosition); // convert hips from avatar to sensor space // The Y_180 is to convert from z forward to -z forward. @@ -3221,7 +3236,7 @@ static bool handDirectionMatchesHeadDirection(controller::Pose leftHand, control float handDotHeadLeft = glm::dot(glm::normalize(leftHand.getVelocity()), glm::normalize(head.getVelocity())); leftHandDirectionMatchesHead = ((handDotHeadLeft > DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD) && (glm::length(leftHand.getVelocity()) > VELOCITY_EPSILON)); //qCDebug(interfaceapp) << "hand dot head left " << handDotHeadLeft; - } + } if (rightHand.isValid() && head.isValid()) { rightHand.velocity.y = 0.0f; float handDotHeadRight = glm::dot(glm::normalize(rightHand.getVelocity()), glm::normalize(head.getVelocity())); @@ -3272,7 +3287,6 @@ static bool isHeadLevel(controller::Pose head, glm::quat averageHeadRotation) { qCDebug(interfaceapp) << " diff from average eulers x " << (fabs(diffFromAverageEulers.x) < DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE) << " and z " << (fabs(diffFromAverageEulers.z) < DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE); return ((fabs(diffFromAverageEulers.x) < DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE) && (fabs(diffFromAverageEulers.z) < DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE)); } - float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -3379,10 +3393,12 @@ void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& d driveKeys = static_cast(object.toUInt16()); } + void MyAvatar::lateUpdatePalms() { Avatar::updatePalms(); } + static const float FOLLOW_TIME = 0.5f; MyAvatar::FollowHelper::FollowHelper() { @@ -3436,18 +3452,13 @@ void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) { } } -bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, - const glm::mat4& desiredBodyMatrix, - const glm::mat4& currentBodyMatrix) const { - //qCDebug(interfaceapp) << "rotation threshold is " << myAvatar.getRotationThreshold(); - const float FOLLOW_ROTATION_THRESHOLD = cosf(myAvatar.getRotationThreshold()); //cosf(PI / 6.0f); // 30 degrees +bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { + const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix); return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; } -bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, - const glm::mat4& desiredBodyMatrix, - const glm::mat4& currentBodyMatrix) const { +bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { // -z axis of currentBodyMatrix in world space. glm::vec3 forward = glm::normalize(glm::vec3(-currentBodyMatrix[0][2], -currentBodyMatrix[1][2], -currentBodyMatrix[2][2])); // x axis of currentBodyMatrix in world space. @@ -3461,6 +3472,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, const float MAX_FORWARD_LEAN = 0.15f; const float MAX_BACKWARD_LEAN = 0.1f; + if (forwardLeanAmount > 0 && forwardLeanAmount > MAX_FORWARD_LEAN) { return true; } else if (forwardLeanAmount < 0 && forwardLeanAmount < -MAX_BACKWARD_LEAN) { @@ -3501,9 +3513,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons return stepDetected; } -bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, - const glm::mat4& desiredBodyMatrix, - const glm::mat4& currentBodyMatrix) const { +bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { const float CYLINDER_TOP = 0.1f; const float CYLINDER_BOTTOM = -1.5f; @@ -3512,11 +3522,11 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); } -void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, - const glm::mat4& desiredBodyMatrix, - const glm::mat4& currentBodyMatrix, - bool hasDriveInput) { - if (myAvatar.getHMDLeanRecenterEnabled() && qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { +void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, + const glm::mat4& currentBodyMatrix, bool hasDriveInput) { + + if (myAvatar.getHMDLeanRecenterEnabled() && + qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); } @@ -3532,7 +3542,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, setForceActivateRotation(false); } if (!isActive(Horizontal) && (getForceActivateHorizontal() || shouldActivateHorizontalCG(myAvatar))) { - qCDebug(interfaceapp) << "----------------------------------------take a step--------------------------------------"; + qCDebug(interfaceapp) << "----------------------------------------take a step--------------------------------------"; activate(Horizontal); setForceActivateHorizontal(false); } @@ -3549,7 +3559,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, glm::quat currentHipsLocal = myAvatar.getAbsoluteJointRotationInObjectFrame(myAvatar.getJointIndex("Hips")); const glm::quat hipsinWorldSpace = followWorldPose.rot() * (Quaternions::Y_180 * (currentHipsLocal)); - const glm::vec3 avatarUpWorld = glm::normalize(followWorldPose.rot() * (Vectors::UP)); + const glm::vec3 avatarUpWorld = glm::normalize(followWorldPose.rot()*(Vectors::UP)); glm::quat resultingSwingInWorld; glm::quat resultingTwistInWorld; swingTwistDecomposition(hipsinWorldSpace, avatarUpWorld, resultingSwingInWorld, resultingTwistInWorld); @@ -3558,8 +3568,8 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, followWorldPose.scale() = glm::vec3(1.0f); if (isActive(Rotation)) { - //use the hmd reading for the hips follow - followWorldPose.rot() = glmExtractRotation(desiredWorldMatrix); + //use the hmd reading for the hips follow + followWorldPose.rot() = glmExtractRotation(desiredWorldMatrix); } if (isActive(Horizontal)) { glm::vec3 desiredTranslation = extractTranslation(desiredWorldMatrix); @@ -3587,8 +3597,7 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, co glm::mat4 worldToSensorMatrix = glm::inverse(sensorToWorldMatrix); glm::vec3 sensorLinearDisplacement = transformVectorFast(worldToSensorMatrix, worldLinearDisplacement); - glm::quat sensorAngularDisplacement = - glmExtractRotation(worldToSensorMatrix) * worldAngularDisplacement * glmExtractRotation(sensorToWorldMatrix); + glm::quat sensorAngularDisplacement = glmExtractRotation(worldToSensorMatrix) * worldAngularDisplacement * glmExtractRotation(sensorToWorldMatrix); glm::mat4 newBodyMat = createMatFromQuatAndPos(sensorAngularDisplacement * glmExtractRotation(currentBodyMatrix), sensorLinearDisplacement + extractTranslation(currentBodyMatrix)); @@ -3651,8 +3660,7 @@ bool MyAvatar::didTeleport() { } bool MyAvatar::hasDriveInput() const { - return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || - fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f; + return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f; } void MyAvatar::setAway(bool value) { @@ -3668,6 +3676,7 @@ void MyAvatar::setAway(bool value) { // Specificly, if we are rendering using a third person camera. We would like to render the hand controllers in front of the camera, // not in front of the avatar. glm::mat4 MyAvatar::computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const { + // Fetch the current camera transform. glm::mat4 cameraWorldMatrix = qApp->getCamera().getTransform(); if (qApp->getCamera().getMode() == CAMERA_MODE_MIRROR) { @@ -3694,7 +3703,7 @@ glm::mat4 MyAvatar::computeCameraRelativeHandControllerMatrix(const glm::mat4& c glm::quat MyAvatar::getAbsoluteJointRotationInObjectFrame(int index) const { if (index < 0) { - index += numeric_limits::max() + 1; // 65536 + index += numeric_limits::max() + 1; // 65536 } switch (index) { @@ -3723,13 +3732,15 @@ glm::quat MyAvatar::getAbsoluteJointRotationInObjectFrame(int index) const { glm::mat4 invAvatarMat = avatarTransform.getInverseMatrix(); return glmExtractRotation(invAvatarMat * qApp->getCamera().getTransform()); } - default: { return Avatar::getAbsoluteJointRotationInObjectFrame(index); } + default: { + return Avatar::getAbsoluteJointRotationInObjectFrame(index); + } } } glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { if (index < 0) { - index += numeric_limits::max() + 1; // 65536 + index += numeric_limits::max() + 1; // 65536 } switch (index) { @@ -3758,7 +3769,9 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { glm::mat4 invAvatarMat = avatarTransform.getInverseMatrix(); return extractTranslation(invAvatarMat * qApp->getCamera().getTransform()); } - default: { return Avatar::getAbsoluteJointTranslationInObjectFrame(index); } + default: { + return Avatar::getAbsoluteJointTranslationInObjectFrame(index); + } } } @@ -3767,9 +3780,7 @@ glm::mat4 MyAvatar::getCenterEyeCalibrationMat() const { int rightEyeIndex = _skeletonModel->getRig().indexOfJoint("RightEye"); int leftEyeIndex = _skeletonModel->getRig().indexOfJoint("LeftEye"); if (rightEyeIndex >= 0 && leftEyeIndex >= 0) { - auto centerEyePos = (getAbsoluteDefaultJointTranslationInObjectFrame(rightEyeIndex) + - getAbsoluteDefaultJointTranslationInObjectFrame(leftEyeIndex)) * - 0.5f; + auto centerEyePos = (getAbsoluteDefaultJointTranslationInObjectFrame(rightEyeIndex) + getAbsoluteDefaultJointTranslationInObjectFrame(leftEyeIndex)) * 0.5f; auto centerEyeRot = Quaternions::Y_180; return createMatFromQuatAndPos(centerEyeRot, centerEyePos / getSensorToWorldScale()); } else { @@ -3837,6 +3848,7 @@ glm::mat4 MyAvatar::getRightFootCalibrationMat() const { } } + glm::mat4 MyAvatar::getRightArmCalibrationMat() const { int rightArmIndex = _skeletonModel->getRig().indexOfJoint("RightArm"); if (rightArmIndex >= 0) { diff --git a/scripts/developer/rotateRecenterApp.html b/scripts/developer/rotateRecenterApp.html deleted file mode 100644 index f50a2f5b0d..0000000000 --- a/scripts/developer/rotateRecenterApp.html +++ /dev/null @@ -1,171 +0,0 @@ - - - - - Rotate App - - - - - - - - -
-
Rotate App
-
-
-
-
- - -
-
-
- - -
-
- -
-
- - - - - - From f216316b558bc8599a45229de792d97df430d759 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 2 Jul 2018 18:19:31 -0700 Subject: [PATCH 020/182] removed some comments and whitespace --- interface/src/avatar/MyAvatar.cpp | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5c879f195a..bc71d12cf0 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -414,7 +414,8 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. - const float HMD_FACING_TIMESCALE = 4.0f; // very slow average + const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); //4.0f; // very slow average + //qCDebug(interfaceapp) << "rotation recenter value is " << HMD_FACING_TIMESCALE; float tau = deltaTime / HMD_FACING_TIMESCALE; _headControllerFacingMovingAverage = lerp(_headControllerFacingMovingAverage, _headControllerFacing, tau); @@ -422,6 +423,8 @@ void MyAvatar::update(float deltaTime) { _rotationChanged = usecTimestampNow(); _smoothOrientationTimer += deltaTime; } + setStandingHeightMode(computeStandingHeightMode(getControllerPoseInAvatarFrame(controller::Action::HEAD))); + setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); @@ -2172,6 +2175,14 @@ void MyAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) _headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement); } +void MyAvatar::setRotationRecenterFilterLength(float length) { + _rotationRecenterFilterLength = length; +} + +void MyAvatar::setRotationThreshold(float angleRadians) { + _rotationThreshold = angleRadians; +} + void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys @@ -3155,7 +3166,6 @@ static bool withinBaseOfSupport(controller::Pose head) { isInsideLine(userScale * backLeft, userScale * frontLeft, head.getTranslation())); isWithinSupport = (withinFrontBase && withinBackBase && withinLateralBase); } - qCDebug(interfaceapp) << "within base of support " << isWithinSupport; return isWithinSupport; } @@ -3168,17 +3178,14 @@ static bool headAngularVelocityBelowThreshold(controller::Pose head) { float magnitudeAngularVelocity = glm::length(xzPlaneAngularVelocity); bool isBelowThreshold = (magnitudeAngularVelocity < DEFAULT_AVATAR_HEAD_ANGULAR_VELOCITY_STEPPING_THRESHOLD); - qCDebug(interfaceapp) << "head angular velocity " << isBelowThreshold; return isBelowThreshold; } - static bool isWithinThresholdHeightMode(controller::Pose head, float newMode) { bool isWithinThreshold = true; if (head.isValid()) { isWithinThreshold = (head.getTranslation().y - newMode) > DEFAULT_AVATAR_MODE_HEIGHT_STEPPING_THRESHOLD; } - qCDebug(interfaceapp) << "height threshold " << isWithinThreshold; return isWithinThreshold; } @@ -3187,10 +3194,9 @@ float MyAvatar::computeStandingHeightMode(controller::Pose head) { const float MODE_CORRECTION_FACTOR = 0.02f; // init mode in meters to the current mode float modeInMeters = getStandingHeightMode(); - //qCDebug(interfaceapp) << "new reading is " << newReading << " as an integer " << (int)(newReading * CENTIMETERS_PER_METER); if (head.isValid()) { float newReading = head.getTranslation().y; - //first add the number to the mode array + // first add the number to the mode array for (int i = 0; i < (SIZE_OF_MODE_ARRAY - 1); i++) { _heightModeArray[i] = _heightModeArray[i + 1]; } @@ -3212,18 +3218,14 @@ float MyAvatar::computeStandingHeightMode(controller::Pose head) { // if not greater check for a reset if (getResetMode() && qApp->isHMDMode()) { setResetMode(false); - qCDebug(interfaceapp) << "reset mode value occurred"; float resetModeInCentimeters = glm::floor((newReading - MODE_CORRECTION_FACTOR)*CENTIMETERS_PER_METER); modeInMeters = (resetModeInCentimeters / CENTIMETERS_PER_METER); } else { // if not greater and no reset, keep the mode as it is modeInMeters = getStandingHeightMode(); } - } else { - qCDebug(interfaceapp) << "new mode value set" << modeInMeters; } } - //qCDebug(interfaceapp) << "_current mode is " << _currentMode; return modeInMeters; } @@ -3235,14 +3237,12 @@ static bool handDirectionMatchesHeadDirection(controller::Pose leftHand, control leftHand.velocity.y = 0.0f; float handDotHeadLeft = glm::dot(glm::normalize(leftHand.getVelocity()), glm::normalize(head.getVelocity())); leftHandDirectionMatchesHead = ((handDotHeadLeft > DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD) && (glm::length(leftHand.getVelocity()) > VELOCITY_EPSILON)); - //qCDebug(interfaceapp) << "hand dot head left " << handDotHeadLeft; } if (rightHand.isValid() && head.isValid()) { rightHand.velocity.y = 0.0f; float handDotHeadRight = glm::dot(glm::normalize(rightHand.getVelocity()), glm::normalize(head.getVelocity())); rightHandDirectionMatchesHead = ((handDotHeadRight > DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD) && (glm::length(rightHand.getVelocity()) > VELOCITY_EPSILON)); } - qCDebug(interfaceapp) << "left right hand velocity "<< (leftHandDirectionMatchesHead && rightHandDirectionMatchesHead); return leftHandDirectionMatchesHead && rightHandDirectionMatchesHead; } @@ -3257,7 +3257,6 @@ static bool handAngularVelocityBelowThreshold(controller::Pose leftHand, control rightHand.angularVelocity.y = 0.0f; rightHandXZAngularVelocity = glm::length(rightHand.getAngularVelocity()); } - qCDebug(interfaceapp) << " hands angular velocity left " << (leftHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD) << " and right " << (rightHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD); return ((leftHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD) && (rightHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD)); } @@ -3265,10 +3264,8 @@ static bool handAngularVelocityBelowThreshold(controller::Pose leftHand, control static bool headVelocityGreaterThanThreshold(controller::Pose head) { float headVelocityMagnitude = 0.0f; if (head.isValid()) { - //qCDebug(interfaceapp) << " head velocity " << head.getVelocity(); headVelocityMagnitude = glm::length(head.getVelocity()); } - qCDebug(interfaceapp) << " head velocity " << (headVelocityMagnitude > DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD); return headVelocityMagnitude > DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD; } @@ -3284,7 +3281,6 @@ static bool isHeadLevel(controller::Pose head, glm::quat averageHeadRotation) { glm::vec3 currentHeadEulers = glm::degrees(safeEulerAngles(head.getRotation())); diffFromAverageEulers = averageHeadEulers - currentHeadEulers; } - qCDebug(interfaceapp) << " diff from average eulers x " << (fabs(diffFromAverageEulers.x) < DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE) << " and z " << (fabs(diffFromAverageEulers.z) < DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE); return ((fabs(diffFromAverageEulers.x) < DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE) && (fabs(diffFromAverageEulers.z) < DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE)); } float MyAvatar::getUserHeight() const { @@ -3542,7 +3538,6 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat setForceActivateRotation(false); } if (!isActive(Horizontal) && (getForceActivateHorizontal() || shouldActivateHorizontalCG(myAvatar))) { - qCDebug(interfaceapp) << "----------------------------------------take a step--------------------------------------"; activate(Horizontal); setForceActivateHorizontal(false); } From 7ed03c2f779da6f56e5703301d4b9c433cc9c94b Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 3 Jul 2018 17:26:18 +0200 Subject: [PATCH 021/182] - native window presentation mode update - de-activate create on displayMode change. --- scripts/system/edit.js | 10 +++++++++- scripts/system/modules/createWindow.js | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index da68151fbe..014ab87c0f 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -545,6 +545,13 @@ var toolBar = (function () { checkEditPermissionsAndUpdate(); }); + HMD.displayModeChanged.connect(function () { + if (isActive) { + tablet.gotoHomeScreen(); + } + that.setActive(false); + }); + Entities.canAdjustLocksChanged.connect(function (canAdjustLocks) { if (isActive && !canAdjustLocks) { that.setActive(false); @@ -606,7 +613,8 @@ var toolBar = (function () { closeExistingDialogWindow(); dialogWindow = Desktop.createWindow("qml/hifi/tablet/New" + entityType + "Window.qml", { title: "New " + entityType + " Entity", - flags: Desktop.AlwaysOnTop | Desktop.ForceNative, + flags: Desktop.ALWAYS_ON_TOP, + presentationMode: Desktop.PresentationMode.NATIVE, size: { x: 500, y: 300 }, visible: true }); diff --git a/scripts/system/modules/createWindow.js b/scripts/system/modules/createWindow.js index bf6231ddda..ad5ddb8e1f 100644 --- a/scripts/system/modules/createWindow.js +++ b/scripts/system/modules/createWindow.js @@ -92,7 +92,8 @@ module.exports = (function() { var windowRect = getWindowRect(this.settingsKey, defaultRect); this.window = Desktop.createWindow(this.qmlPath, { title: this.title, - flags: Desktop.AlwaysOnTop | Desktop.ForceNative, + flags: Desktop.ALWAYS_ON_TOP, + presentationMode: Desktop.PresentationMode.NATIVE, size: windowRect.size, visible: true, position: windowRect.position From d5fb094d8ada859d890004e6c5cccd1eadb27da0 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 3 Jul 2018 18:49:23 +0200 Subject: [PATCH 022/182] enable Desktop.CLOSE_BUTTON_HIDES flag for create app native windows --- scripts/system/edit.js | 2 +- scripts/system/modules/createWindow.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index df59d695c1..9361609607 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -613,7 +613,7 @@ var toolBar = (function () { closeExistingDialogWindow(); dialogWindow = Desktop.createWindow("qml/hifi/tablet/New" + entityType + "Window.qml", { title: "New " + entityType + " Entity", - flags: Desktop.ALWAYS_ON_TOP, + flags: Desktop.ALWAYS_ON_TOP | Desktop.CLOSE_BUTTON_HIDES, presentationMode: Desktop.PresentationMode.NATIVE, size: { x: 500, y: 300 }, visible: true diff --git a/scripts/system/modules/createWindow.js b/scripts/system/modules/createWindow.js index ad5ddb8e1f..185991d2ef 100644 --- a/scripts/system/modules/createWindow.js +++ b/scripts/system/modules/createWindow.js @@ -92,7 +92,7 @@ module.exports = (function() { var windowRect = getWindowRect(this.settingsKey, defaultRect); this.window = Desktop.createWindow(this.qmlPath, { title: this.title, - flags: Desktop.ALWAYS_ON_TOP, + flags: Desktop.ALWAYS_ON_TOP | Desktop.CLOSE_BUTTON_HIDES, presentationMode: Desktop.PresentationMode.NATIVE, size: windowRect.size, visible: true, From 9e5763ea5c9fd2c9421c8dfed57caec0182f4c10 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 3 Jul 2018 11:46:59 -0700 Subject: [PATCH 023/182] adding the recentering back into the non hmd lean code --- interface/src/avatar/MyAvatar.cpp | 4 ++-- interface/src/avatar/MyAvatar.h | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index bc71d12cf0..e39c3dd607 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3533,7 +3533,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat activate(Vertical); } } else { - if (!isActive(Rotation) && getForceActivateRotation()) { + if (!isActive(Rotation) && (getForceActivateRotation() || shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix))) { activate(Rotation); setForceActivateRotation(false); } @@ -3541,7 +3541,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat activate(Horizontal); setForceActivateHorizontal(false); } - if (!isActive(Vertical) && getForceActivateVertical()) { + if (!isActive(Vertical) && (getForceActivateVertical() || shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix))) { activate(Vertical); setForceActivateVertical(false); } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index ec2b2d9661..68476bfe2a 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1646,7 +1646,6 @@ private: bool _shouldLoadScripts { false }; bool _haveReceivedHeightLimitsFromDomain { false }; - }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); From ed4d0f2577107e4282926156ef5a5453dff6fbc4 Mon Sep 17 00:00:00 2001 From: David Back Date: Tue, 3 Jul 2018 12:39:41 -0700 Subject: [PATCH 024/182] early-out if event undefined --- .../system/libraries/entitySelectionTool.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index cd3c9fe418..9dee29ba78 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1654,6 +1654,10 @@ SelectionDisplay = (function() { return (origin.y - intersection.y) / Vec3.distance(origin, intersection); }, onMove: function(event) { + if (event.x === undefined || event.y === undefined) { + return; + } + var wantDebug = false; var pickRay = generalComputePickRay(event.x, event.y); @@ -1811,6 +1815,10 @@ SelectionDisplay = (function() { pushCommandForSelections(duplicatedEntityIDs); }, onMove: function(event) { + if (event.x === undefined || event.y === undefined) { + return; + } + var pickRay = generalComputePickRay(event.x, event.y); // Use previousPickRay if new pickRay will cause resulting rayPlaneIntersection values to wrap around @@ -2060,7 +2068,11 @@ SelectionDisplay = (function() { pushCommandForSelections(); }; - var onMove = function(event) { + var onMove = function(event) + if (event.x === undefined || event.y === undefined) { + return; + } + var proportional = (spaceMode === SPACE_WORLD) || directionEnum === STRETCH_DIRECTION.ALL; var position, rotation; @@ -2393,6 +2405,10 @@ SelectionDisplay = (function() { // EARLY EXIT return; } + + if (event.x === undefined || event.y === undefined) { + return; + } var wantDebug = false; if (wantDebug) { From 2af0f05cb4b09d3c530d7c323d93f9e5ccc62ec0 Mon Sep 17 00:00:00 2001 From: David Back Date: Tue, 3 Jul 2018 12:40:19 -0700 Subject: [PATCH 025/182] tabs --- .../system/libraries/entitySelectionTool.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 9dee29ba78..273ecb6fe5 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1654,10 +1654,10 @@ SelectionDisplay = (function() { return (origin.y - intersection.y) / Vec3.distance(origin, intersection); }, onMove: function(event) { - if (event.x === undefined || event.y === undefined) { - return; - } - + if (event.x === undefined || event.y === undefined) { + return; + } + var wantDebug = false; var pickRay = generalComputePickRay(event.x, event.y); @@ -1815,9 +1815,9 @@ SelectionDisplay = (function() { pushCommandForSelections(duplicatedEntityIDs); }, onMove: function(event) { - if (event.x === undefined || event.y === undefined) { - return; - } + if (event.x === undefined || event.y === undefined) { + return; + } var pickRay = generalComputePickRay(event.x, event.y); @@ -2069,10 +2069,10 @@ SelectionDisplay = (function() { }; var onMove = function(event) - if (event.x === undefined || event.y === undefined) { - return; - } - + if (event.x === undefined || event.y === undefined) { + return; + } + var proportional = (spaceMode === SPACE_WORLD) || directionEnum === STRETCH_DIRECTION.ALL; var position, rotation; @@ -2405,10 +2405,10 @@ SelectionDisplay = (function() { // EARLY EXIT return; } - - if (event.x === undefined || event.y === undefined) { - return; - } + + if (event.x === undefined || event.y === undefined) { + return; + } var wantDebug = false; if (wantDebug) { From afbe64ec11d15a4366b0aa52f9d62fe57515f8ca Mon Sep 17 00:00:00 2001 From: David Back Date: Tue, 3 Jul 2018 13:32:11 -0700 Subject: [PATCH 026/182] missing bracket --- scripts/system/libraries/entitySelectionTool.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 273ecb6fe5..a84afbe322 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -2068,7 +2068,7 @@ SelectionDisplay = (function() { pushCommandForSelections(); }; - var onMove = function(event) + var onMove = function(event) { if (event.x === undefined || event.y === undefined) { return; } From 4995b57712e3d746b7492c31a3b9108df1452426 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 3 Jul 2018 14:31:36 -0700 Subject: [PATCH 027/182] removed print statements and fixed rotation threshold update code --- interface/src/avatar/MyAvatar.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index f01ec83330..ac85645f78 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -414,8 +414,8 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. - const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); //4.0f; // very slow average - //qCDebug(interfaceapp) << "rotation recenter value is " << HMD_FACING_TIMESCALE; + const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); + float tau = deltaTime / HMD_FACING_TIMESCALE; _headControllerFacingMovingAverage = lerp(_headControllerFacingMovingAverage, _headControllerFacing, tau); @@ -2181,6 +2181,7 @@ void MyAvatar::setRotationRecenterFilterLength(float length) { void MyAvatar::setRotationThreshold(float angleRadians) { _rotationThreshold = angleRadians; + qCDebug(interfaceapp) << "setting the rotation threshold " << _rotationThreshold; } void MyAvatar::updateOrientation(float deltaTime) { @@ -3449,7 +3450,7 @@ void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) { } bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { - const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees + const float FOLLOW_ROTATION_THRESHOLD = cosf(myAvatar.getRotationThreshold()); glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix); return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; } @@ -3533,15 +3534,15 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat activate(Vertical); } } else { - if (!isActive(Rotation) && (getForceActivateRotation() || shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix))) { + if (!isActive(Rotation) && (getForceActivateRotation() || shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); setForceActivateRotation(false); } - if (!isActive(Horizontal) && (getForceActivateHorizontal() || shouldActivateHorizontalCG(myAvatar))) { + if (!isActive(Horizontal) && (getForceActivateHorizontal() || shouldActivateHorizontalCG(myAvatar) || hasDriveInput)) { activate(Horizontal); setForceActivateHorizontal(false); } - if (!isActive(Vertical) && (getForceActivateVertical() || shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix))) { + if (!isActive(Vertical) && (getForceActivateVertical() || shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Vertical); setForceActivateVertical(false); } From 6460c9e8612f8b369b611702fcc629be2ff98f69 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 3 Jul 2018 17:11:19 -0700 Subject: [PATCH 028/182] fixed case for 0.0 as the filter length --- interface/src/avatar/MyAvatar.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index ac85645f78..397e0fd73a 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -89,6 +89,8 @@ const float MyAvatar::ZOOM_MAX = 25.0f; const float MyAvatar::ZOOM_DEFAULT = 1.5f; const float MIN_SCALE_CHANGED_DELTA = 0.001f; +//#define DEBUG_DRAW_HMD_MOVING_AVERAGE + MyAvatar::MyAvatar(QThread* thread) : Avatar(thread), _yawSpeed(YAW_SPEED_DEFAULT), @@ -414,7 +416,7 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. - const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); + const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); float tau = deltaTime / HMD_FACING_TIMESCALE; _headControllerFacingMovingAverage = lerp(_headControllerFacingMovingAverage, _headControllerFacing, tau); @@ -2176,12 +2178,15 @@ void MyAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) } void MyAvatar::setRotationRecenterFilterLength(float length) { - _rotationRecenterFilterLength = length; + if (length > 0.01f) { + _rotationRecenterFilterLength = length; + } else { + _rotationRecenterFilterLength = 0.01f; + } } void MyAvatar::setRotationThreshold(float angleRadians) { _rotationThreshold = angleRadians; - qCDebug(interfaceapp) << "setting the rotation threshold " << _rotationThreshold; } void MyAvatar::updateOrientation(float deltaTime) { From b00d07b02989586431d3090bc39b2b24f94e8fef Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 5 Jul 2018 10:50:08 +1200 Subject: [PATCH 029/182] ESLint --- .../system/libraries/entitySelectionTool.js | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index d30de1045f..0ad9fae2ab 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -13,7 +13,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global SelectionManager, grid, rayPlaneIntersection, rayPlaneIntersection2, pushCommandForSelections, +/* global SelectionManager, SelectionDisplay, grid, rayPlaneIntersection, rayPlaneIntersection2, pushCommandForSelections, getMainTabletIDs, getControllerWorldLocation */ var SPACE_LOCAL = "local"; @@ -72,7 +72,9 @@ SelectionManager = (function() { subscribeToUpdateMessages(); - var COLOR_ORANGE_HIGHLIGHT = { red: 255, green: 99, blue: 9 } + // disabling this for now as it is causing rendering issues with the other handle overlays + /* + var COLOR_ORANGE_HIGHLIGHT = { red: 255, green: 99, blue: 9 }; var editHandleOutlineStyle = { outlineUnoccludedColor: COLOR_ORANGE_HIGHLIGHT, outlineOccludedColor: COLOR_ORANGE_HIGHLIGHT, @@ -85,8 +87,8 @@ SelectionManager = (function() { outlineWidth: 3, isOutlineSmooth: true }; - // disabling this for now as it is causing rendering issues with the other handle overlays - //Selection.enableListHighlight(HIGHLIGHT_LIST_NAME, editHandleOutlineStyle); + Selection.enableListHighlight(HIGHLIGHT_LIST_NAME, editHandleOutlineStyle); + */ that.savedProperties = {}; that.selections = []; @@ -362,8 +364,6 @@ SelectionDisplay = (function() { var spaceMode = SPACE_LOCAL; var overlayNames = []; - var lastCameraPosition = Camera.getPosition(); - var lastCameraOrientation = Camera.getOrientation(); var lastControllerPoses = [ getControllerWorldLocation(Controller.Standard.LeftHand, true), getControllerWorldLocation(Controller.Standard.RightHand, true) @@ -383,8 +383,6 @@ SelectionDisplay = (function() { var ctrlPressed = false; - var replaceCollisionsAfterStretch = false; - var handlePropertiesTranslateArrowCones = { shape: "Cone", solid: true, @@ -1019,9 +1017,6 @@ SelectionDisplay = (function() { that.select = function(entityID, event) { var properties = Entities.getEntityProperties(SelectionManager.selections[0]); - lastCameraPosition = Camera.getPosition(); - lastCameraOrientation = Camera.getOrientation(); - if (event !== false) { var wantDebug = false; if (wantDebug) { @@ -1265,8 +1260,8 @@ SelectionDisplay = (function() { var scaleRTFCubeToCamera = getDistanceToCamera(scaleRTFCubePosition); var scaleCubeToCamera = Math.min(scaleLBNCubeToCamera, scaleRBNCubeToCamera, scaleLBFCubeToCamera, - scaleRBFCubeToCamera, scaleLTNCubeToCamera, scaleRTNCubeToCamera, - scaleLTFCubeToCamera, scaleRTFCubeToCamera); + scaleRBFCubeToCamera, scaleLTNCubeToCamera, scaleRTNCubeToCamera, + scaleLTFCubeToCamera, scaleRTFCubeToCamera); var scaleCubeDimension = scaleCubeToCamera * SCALE_CUBE_CAMERA_DISTANCE_MULTIPLE; var scaleCubeDimensions = { x: scaleCubeDimension, y: scaleCubeDimension, z: scaleCubeDimension }; @@ -1619,8 +1614,7 @@ SelectionDisplay = (function() { translateXZTool.pickPlanePosition = pickResult.intersection; translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, - SelectionManager.worldDimensions.y), - SelectionManager.worldDimensions.z); + SelectionManager.worldDimensions.y), SelectionManager.worldDimensions.z); translateXZTool.startingDistance = Vec3.distance(pickRay.origin, SelectionManager.position); translateXZTool.startingElevation = translateXZTool.elevation(pickRay.origin, translateXZTool.pickPlanePosition); if (wantDebug) { @@ -2136,8 +2130,8 @@ SelectionDisplay = (function() { newDimensions = Vec3.sum(initialDimensions, changeInDimensions); } - var minimumDimension = directionEnum === STRETCH_DIRECTION.ALL ? STRETCH_ALL_MINIMUM_DIMENSION : - STRETCH_MINIMUM_DIMENSION; + var minimumDimension = directionEnum === + STRETCH_DIRECTION.ALL ? STRETCH_ALL_MINIMUM_DIMENSION : STRETCH_MINIMUM_DIMENSION; if (newDimensions.x < minimumDimension) { newDimensions.x = minimumDimension; changeInDimensions.x = minimumDimension - initialDimensions.x; @@ -2231,8 +2225,7 @@ SelectionDisplay = (function() { selectedHandle = handleScaleRTFCube; } offset = Vec3.multiply(directionVector, NEGATE_VECTOR); - var tool = makeStretchTool(mode, STRETCH_DIRECTION.ALL, directionVector, - directionVector, offset, null, selectedHandle); + var tool = makeStretchTool(mode, STRETCH_DIRECTION.ALL, directionVector, directionVector, offset, null, selectedHandle); return addHandleTool(overlay, tool); } @@ -2363,7 +2356,7 @@ SelectionDisplay = (function() { var rotationCenterToZero = Vec3.subtract(rotationZero, rotationCenter); var rotationCenterToZeroLength = Vec3.length(rotationCenterToZero); rotationDegreesPosition = Vec3.sum(rotationCenter, Vec3.multiply(Vec3.normalize(rotationCenterToZero), - rotationCenterToZeroLength * ROTATE_DISPLAY_DISTANCE_MULTIPLIER)); + rotationCenterToZeroLength * ROTATE_DISPLAY_DISTANCE_MULTIPLIER)); updateRotationDegreesOverlay(0, rotationDegreesPosition); if (wantDebug) { From 94c097fc39ec8333f5c99c6d5c98987de0b45477 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 5 Jul 2018 11:42:37 +1200 Subject: [PATCH 030/182] Fix entity y value jumping when translate with grid snapping enabled --- scripts/system/libraries/entitySelectionTool.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 0ad9fae2ab..bad69c279b 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1830,7 +1830,8 @@ SelectionDisplay = (function() { var dotVector = Vec3.dot(vector, projectionVector); vector = Vec3.multiply(dotVector, projectionVector); - vector = grid.snapToGrid(vector); + var gridOrigin = grid.getOrigin(); + vector = Vec3.subtract(grid.snapToGrid(Vec3.sum(vector, gridOrigin)), gridOrigin); var wantDebug = false; if (wantDebug) { From 9709599c1a5f58afc4dfd24b778825e54f2eaa8b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 5 Jul 2018 11:47:34 +1200 Subject: [PATCH 031/182] Fix translation handle not following mouse at some view angles --- scripts/system/libraries/entitySelectionTool.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index bad69c279b..12a54b8779 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1768,16 +1768,18 @@ SelectionDisplay = (function() { addHandleTool(overlay, { mode: mode, onBegin: function(event, pickRay, pickResult) { + var axisVector; if (direction === TRANSLATE_DIRECTION.X) { - pickNormal = { x: 0, y: 1, z: 1 }; + axisVector = { x: 1, y: 0, z: 0 }; } else if (direction === TRANSLATE_DIRECTION.Y) { - pickNormal = { x: 1, y: 0, z: 1 }; + axisVector = { x: 0, y: 1, z: 0 }; } else if (direction === TRANSLATE_DIRECTION.Z) { - pickNormal = { x: 1, y: 1, z: 0 }; + axisVector = { x: 0, y: 0, z: 1 }; } var rotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation; - pickNormal = Vec3.multiplyQbyV(rotation, pickNormal); + axisVector = Vec3.multiplyQbyV(rotation, axisVector); + pickNormal = Vec3.cross(Vec3.cross(pickRay.direction, axisVector), axisVector); lastPick = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, pickNormal); From a642324a11e0aed12b2fcf8e1c3ae18418c84d19 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 5 Jul 2018 11:58:48 +1200 Subject: [PATCH 032/182] Fix oscillating positions as translate when snapping to grid --- scripts/system/libraries/entitySelectionTool.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 12a54b8779..4a972e9d53 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1763,6 +1763,7 @@ SelectionDisplay = (function() { function addHandleTranslateTool(overlay, mode, direction) { var pickNormal = null; var lastPick = null; + var initialPosition = null; var projectionVector = null; var previousPickRay = null; addHandleTool(overlay, { @@ -1782,6 +1783,7 @@ SelectionDisplay = (function() { pickNormal = Vec3.cross(Vec3.cross(pickRay.direction, axisVector), axisVector); lastPick = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, pickNormal); + initialPosition = SelectionManager.worldPosition; SelectionManager.saveProperties(); that.resetPreviousHandleColor(); @@ -1816,7 +1818,7 @@ SelectionDisplay = (function() { pickRay = previousPickRay; } - var newIntersection = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, pickNormal); + var newIntersection = rayPlaneIntersection(pickRay, initialPosition, pickNormal); var vector = Vec3.subtract(newIntersection, lastPick); if (direction === TRANSLATE_DIRECTION.X) { From 574bfd177db2bdcc027261573f63854a98b15fc5 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 5 Jul 2018 17:57:00 +0200 Subject: [PATCH 033/182] Create app (desktop mode): - Fix insta-close on switching from other app to create app. - Fix new model and particle dialogs. --- scripts/system/edit.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 9361609607..104ffc2e7f 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -578,7 +578,7 @@ var toolBar = (function () { }); createButton = activeButton; tablet.screenChanged.connect(function (type, url) { - var isGoingToHomescreenOnDesktop = (!HMD.active && url === 'hifi/tablet/TabletHome.qml'); + var isGoingToHomescreenOnDesktop = (!HMD.active && (url === 'hifi/tablet/TabletHome.qml' || url === '')); if (isActive && (type !== "QML" || url !== "hifi/tablet/Edit.qml") && !isGoingToHomescreenOnDesktop) { that.setActive(false); } @@ -611,7 +611,8 @@ var toolBar = (function () { tablet.pushOntoStack("hifi/tablet/New" + entityType + "Dialog.qml"); } else { closeExistingDialogWindow(); - dialogWindow = Desktop.createWindow("qml/hifi/tablet/New" + entityType + "Window.qml", { + var qmlPath = Script.resourcesPath() + "qml/hifi/tablet/New" + entityType + "Window.qml"; + dialogWindow = Desktop.createWindow(qmlPath, { title: "New " + entityType + " Entity", flags: Desktop.ALWAYS_ON_TOP | Desktop.CLOSE_BUTTON_HIDES, presentationMode: Desktop.PresentationMode.NATIVE, From 0ebc7fc29e5adad91ee54cb24d15837f10e773fa Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 5 Jul 2018 13:41:06 -0700 Subject: [PATCH 034/182] fixed the mode computation so that it can keep track of all long term height frequencies. This makes it unnecessary to reset the mode, because it will revert back to the highest frequency --- interface/src/avatar/MyAvatar.cpp | 43 ++++++++++++------------------- interface/src/avatar/MyAvatar.h | 4 +-- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 397e0fd73a..2ae88117a7 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3198,37 +3198,26 @@ static bool isWithinThresholdHeightMode(controller::Pose head, float newMode) { float MyAvatar::computeStandingHeightMode(controller::Pose head) { const float CENTIMETERS_PER_METER = 100.0f; const float MODE_CORRECTION_FACTOR = 0.02f; + const int MAX_INT = 2147483647; // init mode in meters to the current mode float modeInMeters = getStandingHeightMode(); if (head.isValid()) { float newReading = head.getTranslation().y; - // first add the number to the mode array - for (int i = 0; i < (SIZE_OF_MODE_ARRAY - 1); i++) { - _heightModeArray[i] = _heightModeArray[i + 1]; - } - _heightModeArray[SIZE_OF_MODE_ARRAY - 1] = glm::floor(newReading * CENTIMETERS_PER_METER); - - int greatestFrequency = 0; - int mode = 0; - std::map freq; - for (int j = 0; j < SIZE_OF_MODE_ARRAY; j++) { - freq[_heightModeArray[j]] += 1; - if ((freq[_heightModeArray[j]] > greatestFrequency) || - ((freq[_heightModeArray[j]] == SIZE_OF_MODE_ARRAY) && (_heightModeArray[j] > mode))) { - greatestFrequency = freq[_heightModeArray[j]]; - mode = _heightModeArray[j]; - } - } - modeInMeters = ((float)mode) / CENTIMETERS_PER_METER; - if (!(modeInMeters > getStandingHeightMode())) { - // if not greater check for a reset - if (getResetMode() && qApp->isHMDMode()) { - setResetMode(false); - float resetModeInCentimeters = glm::floor((newReading - MODE_CORRECTION_FACTOR)*CENTIMETERS_PER_METER); - modeInMeters = (resetModeInCentimeters / CENTIMETERS_PER_METER); - } else { - // if not greater and no reset, keep the mode as it is - modeInMeters = getStandingHeightMode(); + int newReadingInCentimeters = glm::floor(newReading * CENTIMETERS_PER_METER); + _heightFrequencyMap[newReadingInCentimeters] += 1; + if (_heightFrequencyMap[newReadingInCentimeters] > _greatestFrequency) { + _greatestFrequency = _heightFrequencyMap[newReadingInCentimeters]; + modeInMeters = ((float)newReadingInCentimeters) / CENTIMETERS_PER_METER; + // we need to deal with possible overflow error here + if (_greatestFrequency > (MAX_INT/2)) { + for (auto & heightValue : _heightFrequencyMap) { + if (heightValue.second == _greatestFrequency) { + heightValue.second = 100; + } else { + heightValue.second = 0; + } + } + _greatestFrequency = 100; } } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 68476bfe2a..0a65a34789 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1525,10 +1525,10 @@ private: glm::vec2 _headControllerFacingMovingAverage { 0.0f, 0.0f }; // facing vector in xz plane (sensor space) glm::quat _averageHeadRotation { 0.0f, 0.0f, 0.0f, 1.0f }; - static const int SIZE_OF_MODE_ARRAY { 50 }; - int _heightModeArray[SIZE_OF_MODE_ARRAY]; float _standingHeightMode { 0.0f }; bool _resetMode { true }; + std::map _heightFrequencyMap; + long int _greatestFrequency { 0 }; // cache of the current body position and orientation of the avatar's body, // in sensor space. From b1801c689401a7b7f49effdce96dd9baa0f37be2 Mon Sep 17 00:00:00 2001 From: cmpt376edits <40580877+cmpt376edits@users.noreply.github.com> Date: Thu, 5 Jul 2018 13:44:38 -0700 Subject: [PATCH 035/182] Improve punctuation on README.md --- README.md | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e0bbed3105..363064964a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ High Fidelity (hifi) is an early-stage technology lab experimenting with Virtual Worlds and VR. -In this repository you'll find the source to many of the components in our -alpha-stage virtual world. The project embraces distributed development -and if you'd like to help, we'll pay you -- find out more at [Worklist.net](https://worklist.net). +This repository contains the source to many of the components in our +alpha-stage virtual world. The project embraces distributed development. +If you'd like to help, we'll pay you -- find out more at [Worklist.net](https://worklist.net). If you find a small bug and have a fix, pull requests are welcome. If you'd like to get paid for your work, make sure you report the bug via a job on [Worklist.net](https://worklist.net). @@ -32,9 +32,10 @@ Running Interface When you launch interface, you will automatically connect to our default domain: "root.highfidelity.io". If you don't see anything, make sure your preferences are pointing to -root.highfidelity.io (set your domain via Cmnd+D/Cntrl+D), if you still have no luck it's possible our servers are -simply down; if you're experiencing a major bug, let us know by adding an issue to this repository. -Make sure to include details about your computer and how to reproduce the bug. +root.highfidelity.io (set your domain via Cmnd+D/Cntrl+D). If you still have no luck, +it's possible our servers are down. If you're experiencing a major bug, let us know by +adding an issue to this repository. Include details about your computer and how to +reproduce the bug in your issue. To move around in-world, use the arrow keys (and Shift + up/down to fly up or down) or W A S D, and E or C to fly up/down. All of the other possible options @@ -48,7 +49,8 @@ you to run the full stack of the virtual world. In order to set up your own virtual world, you need to set up and run your own local "domain". -The domain-server gives a number different types of assignments to the assignment-client for different features: audio, avatars, voxels, particles, meta-voxels and models. +The domain-server gives a number different types of assignments to the assignment-client +for different features: audio, avatars, voxels, particles, meta-voxels and models. Follow the instructions in the [build guide](BUILD.md) to build the various components. @@ -56,7 +58,8 @@ From the domain-server build directory, launch a domain-server. ./domain-server -Then, run an assignment-client. The assignment-client uses localhost as its assignment-server and talks to it on port 40102 (the default domain-server port). +Then, run an assignment-client. The assignment-client uses localhost as its assignment-server +and talks to it on port 40102 (the default domain-server port). In a new Terminal window, run: @@ -64,13 +67,20 @@ In a new Terminal window, run: Any target can be terminated with Ctrl-C (SIGINT) in the associated Terminal window. -This assignment-client will grab one assignment from the domain-server. You can tell the assignment-client what type you want it to be with the `-t` option. You can also run an assignment-client that forks off *n* assignment-clients with the `-n` option. The `-min` and `-max` options allow you to set a range of required assignment-clients, this allows you to have flexibility in the number of assignment-clients that are running. See `--help` for more options. +This assignment-client will grab one assignment from the domain-server. You can tell the +assignment-client what type you want it to be with the `-t` option. You can also run an +assignment-client that forks off *n* assignment-clients with the `-n` option. The `-min` +and `-max` options allow you to set a range of required assignment-clients. This allows +you to have flexibility in the number of assignment-clients that are running. +See `--help` for more options. ./assignment-client --min 6 --max 20 -To test things out you'll want to run the Interface client. +To test things out, you'll need to run the Interface client. -To access your local domain in Interface, open your Preferences -- on OS X this is available in the Interface menu, on Linux you'll find it in the File menu. Enter "localhost" in the "Domain server" field. +To access your local domain in Interface, open your Preferences. On OS X, this is available +in the Interface menu. On Linux, you'll find it in the File menu. Enter "localhost" in the +"Domain server" field. -If everything worked you should see that you are connected to at least one server. +If everything worked, you should see that you are connected to at least one server. Nice work! From 1a18d3305e48f7db620418c692a7fdc9563397d1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 6 Jul 2018 17:03:30 +1200 Subject: [PATCH 036/182] Code review --- scripts/system/libraries/entitySelectionTool.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 4a972e9d53..eacf5869c8 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -383,6 +383,8 @@ SelectionDisplay = (function() { var ctrlPressed = false; + that.replaceCollisionsAfterStretch = false; + var handlePropertiesTranslateArrowCones = { shape: "Cone", solid: true, @@ -1260,8 +1262,8 @@ SelectionDisplay = (function() { var scaleRTFCubeToCamera = getDistanceToCamera(scaleRTFCubePosition); var scaleCubeToCamera = Math.min(scaleLBNCubeToCamera, scaleRBNCubeToCamera, scaleLBFCubeToCamera, - scaleRBFCubeToCamera, scaleLTNCubeToCamera, scaleRTNCubeToCamera, - scaleLTFCubeToCamera, scaleRTFCubeToCamera); + scaleRBFCubeToCamera, scaleLTNCubeToCamera, scaleRTNCubeToCamera, + scaleLTFCubeToCamera, scaleRTFCubeToCamera); var scaleCubeDimension = scaleCubeToCamera * SCALE_CUBE_CAMERA_DISTANCE_MULTIPLE; var scaleCubeDimensions = { x: scaleCubeDimension, y: scaleCubeDimension, z: scaleCubeDimension }; @@ -1614,7 +1616,8 @@ SelectionDisplay = (function() { translateXZTool.pickPlanePosition = pickResult.intersection; translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, - SelectionManager.worldDimensions.y), SelectionManager.worldDimensions.z); + SelectionManager.worldDimensions.y), + SelectionManager.worldDimensions.z); translateXZTool.startingDistance = Vec3.distance(pickRay.origin, SelectionManager.position); translateXZTool.startingElevation = translateXZTool.elevation(pickRay.origin, translateXZTool.pickPlanePosition); if (wantDebug) { @@ -2361,7 +2364,7 @@ SelectionDisplay = (function() { var rotationCenterToZero = Vec3.subtract(rotationZero, rotationCenter); var rotationCenterToZeroLength = Vec3.length(rotationCenterToZero); rotationDegreesPosition = Vec3.sum(rotationCenter, Vec3.multiply(Vec3.normalize(rotationCenterToZero), - rotationCenterToZeroLength * ROTATE_DISPLAY_DISTANCE_MULTIPLIER)); + rotationCenterToZeroLength * ROTATE_DISPLAY_DISTANCE_MULTIPLIER)); updateRotationDegreesOverlay(0, rotationDegreesPosition); if (wantDebug) { From 42afd3132579009902433cb0c2526564571c248e Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 6 Jul 2018 19:48:35 +0200 Subject: [PATCH 037/182] default window width fixes --- scripts/system/edit.js | 6 +++--- scripts/system/libraries/entityList.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 104ffc2e7f..464ccf2995 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -37,7 +37,7 @@ Script.include([ var CreateWindow = Script.require('./modules/createWindow.js'); var TITLE_OFFSET = 60; -var ENTITY_LIST_WIDTH = 470; +var CREATE_TOOLS_WIDTH = 490; var MAX_DEFAULT_ENTITY_LIST_HEIGHT = 942; var createToolsWindow = new CreateWindow( @@ -51,11 +51,11 @@ var createToolsWindow = new CreateWindow( } return { size: { - x: ENTITY_LIST_WIDTH, + x: CREATE_TOOLS_WIDTH, y: windowHeight }, position: { - x: Window.x + Window.innerWidth - ENTITY_LIST_WIDTH, + x: Window.x + Window.innerWidth - CREATE_TOOLS_WIDTH, y: Window.y + TITLE_OFFSET } } diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 539751b12f..1edcc82b0d 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -17,7 +17,7 @@ EntityListTool = function() { var CreateWindow = Script.require('../modules/createWindow.js'); var TITLE_OFFSET = 60; - var CREATE_TOOLS_WIDTH = 495; + var ENTITY_LIST_WIDTH = 495; var MAX_DEFAULT_CREATE_TOOLS_HEIGHT = 778; var entityListWindow = new CreateWindow( Script.resourcesPath() + "qml/hifi/tablet/EditEntityList.qml", @@ -30,7 +30,7 @@ EntityListTool = function() { } return { size: { - x: CREATE_TOOLS_WIDTH, + x: ENTITY_LIST_WIDTH, y: windowHeight }, position: { From 10bce7ea8d538f152087c022bbef3c96e3150ea0 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Tue, 24 Apr 2018 16:25:22 -0700 Subject: [PATCH 038/182] adding some functionality to avatar bookmarks --- interface/src/AvatarBookmarks.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 7b47ea8af7..f2ff88c974 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -39,6 +39,9 @@ public slots: * @function AvatarBookmarks.addBookMark */ void addBookmark(); + void addBookmark(QString bookmarkName) {} + void removeBookmark(QString bookmark) {} + QVariantMap getBookmarks() { return _bookmarks; } protected: void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) override; From 1ddfc8739696ff4bb489ca3505f671993d9006eb Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Thu, 19 Apr 2018 11:59:26 +0300 Subject: [PATCH 039/182] wip on avatarapp --- .../resources/images/FavoriteIconActive.svg | 9 + .../resources/images/FavoriteIconInActive.svg | 8 + ...e76946cc-c272-4adf-9bb6-02cde0a4b57d-1.png | Bin 0 -> 8851 bytes ...e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png | Bin 0 -> 7560 bytes ...e76946cc-c272-4adf-9bb6-02cde0a4b57d-3.png | Bin 0 -> 9019 bytes ...e76946cc-c272-4adf-9bb6-02cde0a4b57d-4.png | Bin 0 -> 8758 bytes ...e76946cc-c272-4adf-9bb6-02cde0a4b57d-5.png | Bin 0 -> 16128 bytes ...p-e76946cc-c272-4adf-9bb6-02cde0a4b57d.png | Bin 0 -> 8257 bytes ...e-77312e4b-6f48-4eb4-87e2-50444d8e56d1.png | Bin 0 -> 105721 bytes .../images/samples/hifi-place-get-avatars.png | Bin 0 -> 84340 bytes .../qml/controls-uit/RadioButton.qml | 16 +- .../resources/qml/controls-uit/SpinBox.qml | 7 +- interface/resources/qml/hifi/AvatarApp.qml | 759 +++++++++++++++ .../qml/hifi/avatarapp/AdjustWearables.qml | 183 ++++ .../qml/hifi/avatarapp/AvatarAppHeader.qml | 57 ++ .../qml/hifi/avatarapp/AvatarAppStyle.qml | 29 + .../qml/hifi/avatarapp/AvatarThumbnail.qml | 30 + .../avatarapp/AvatarWearablesIndicator.qml | 40 + .../qml/hifi/avatarapp/AvatarsModel.qml | 48 + .../qml/hifi/avatarapp/BlueButton.qml | 13 + .../hifi/avatarapp/CreateFavoriteDialog.qml | 136 +++ .../qml/hifi/avatarapp/DialogButtons.qml | 41 + .../qml/hifi/avatarapp/InputTextStyle4.qml | 23 + .../qml/hifi/avatarapp/MessageBox.qml | 177 ++++ .../resources/qml/hifi/avatarapp/Settings.qml | 304 ++++++ .../qml/hifi/avatarapp/ShadowImage.qml | 26 + .../qml/hifi/avatarapp/ShadowRectangle.qml | 24 + .../qml/hifi/avatarapp/SquareLabel.qml | 21 + .../qml/hifi/avatarapp/TextStyle1.qml | 6 + .../qml/hifi/avatarapp/TextStyle10.qml | 6 + .../qml/hifi/avatarapp/TextStyle11.qml | 6 + .../qml/hifi/avatarapp/TextStyle2.qml | 6 + .../qml/hifi/avatarapp/TextStyle3.qml | 6 + .../qml/hifi/avatarapp/TextStyle4.qml | 6 + .../qml/hifi/avatarapp/TextStyle5.qml | 6 + .../qml/hifi/avatarapp/TextStyle6.qml | 6 + .../qml/hifi/avatarapp/TextStyle7.qml | 7 + .../qml/hifi/avatarapp/TextStyle8.qml | 6 + .../qml/hifi/avatarapp/TextStyle9.qml | 7 + .../resources/qml/hifi/avatarapp/Vector3.qml | 47 + .../qml/hifi/avatarapp/WhiteButton.qml | 13 + scripts/defaultScripts.js | 1 + scripts/system/avatarapp.js | 910 ++++++++++++++++++ 43 files changed, 2981 insertions(+), 9 deletions(-) create mode 100644 interface/resources/images/FavoriteIconActive.svg create mode 100644 interface/resources/images/FavoriteIconInActive.svg create mode 100644 interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-1.png create mode 100644 interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png create mode 100644 interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-3.png create mode 100644 interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-4.png create mode 100644 interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-5.png create mode 100644 interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d.png create mode 100644 interface/resources/images/samples/hifi-place-77312e4b-6f48-4eb4-87e2-50444d8e56d1.png create mode 100644 interface/resources/images/samples/hifi-place-get-avatars.png create mode 100644 interface/resources/qml/hifi/AvatarApp.qml create mode 100644 interface/resources/qml/hifi/avatarapp/AdjustWearables.qml create mode 100644 interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml create mode 100644 interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml create mode 100644 interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml create mode 100644 interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml create mode 100644 interface/resources/qml/hifi/avatarapp/AvatarsModel.qml create mode 100644 interface/resources/qml/hifi/avatarapp/BlueButton.qml create mode 100644 interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml create mode 100644 interface/resources/qml/hifi/avatarapp/DialogButtons.qml create mode 100644 interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml create mode 100644 interface/resources/qml/hifi/avatarapp/MessageBox.qml create mode 100644 interface/resources/qml/hifi/avatarapp/Settings.qml create mode 100644 interface/resources/qml/hifi/avatarapp/ShadowImage.qml create mode 100644 interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml create mode 100644 interface/resources/qml/hifi/avatarapp/SquareLabel.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle1.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle10.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle11.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle2.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle3.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle4.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle5.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle6.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle7.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle8.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TextStyle9.qml create mode 100644 interface/resources/qml/hifi/avatarapp/Vector3.qml create mode 100644 interface/resources/qml/hifi/avatarapp/WhiteButton.qml create mode 100644 scripts/system/avatarapp.js diff --git a/interface/resources/images/FavoriteIconActive.svg b/interface/resources/images/FavoriteIconActive.svg new file mode 100644 index 0000000000..5f03217d27 --- /dev/null +++ b/interface/resources/images/FavoriteIconActive.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/interface/resources/images/FavoriteIconInActive.svg b/interface/resources/images/FavoriteIconInActive.svg new file mode 100644 index 0000000000..7cca31ac66 --- /dev/null +++ b/interface/resources/images/FavoriteIconInActive.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-1.png b/interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-1.png new file mode 100644 index 0000000000000000000000000000000000000000..15d177190ae9605b9c5fff6379627de2e7d01e79 GIT binary patch literal 8851 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 literal 0 HcmV?d00001 diff --git a/interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-4.png b/interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-4.png new file mode 100644 index 0000000000000000000000000000000000000000..2afd2657c4c3bf453d17f71add968d09f709dce5 GIT binary patch literal 8758 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 gM@~07*qoM6N<$f|T;fAOHXW literal 0 HcmV?d00001 diff --git a/interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-5.png b/interface/resources/images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-5.png new file mode 100644 index 0000000000000000000000000000000000000000..52df23ff7953e58a4ef0fe3a7f20d50677b8d15e GIT binary patch literal 16128 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 S(f0QM0000s0Pj%k8nuIw8d@h&Pzo7r+X}Xm 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 literal 0 HcmV?d00001 diff --git a/interface/resources/images/samples/hifi-place-77312e4b-6f48-4eb4-87e2-50444d8e56d1.png b/interface/resources/images/samples/hifi-place-77312e4b-6f48-4eb4-87e2-50444d8e56d1.png new file mode 100644 index 0000000000000000000000000000000000000000..d39546a90e5ee0072710edb4d9674746b5aa7180 GIT binary patch literal 105721 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

CiPU6-Uq!6y2dsYaHmF4 zr=`TEtLtZ0MdHzEWbmR1uJ`b0X7gjbQ+MrowaTO82A&-st82>U{%aVCg!n zzMb+p>1}WZIc}0I*GlM!BdlAi1XCqMD$@$B^7H@3AYJEZ`>l=XGkCm(8}a(=xg6g+ zm5a-_h_tQM|DH}JmZQZfSZqhGO$9`iA+I@L=sh>c9<^ME4JD)8F0W<%8WsQ?-ltE0 zB2VxCh5kKb#|jFc1TUM*oAVAVj)$(=+rXpHey`w-;iv(dnT~A~Aa9(9ffkYDBi4vgt*&bMHPb(!)W;Njn)j)fIwDhJQ-MGSL%Bc+)sG}6u5*D z*lBZA>x|qekIkg}+l^*gYk=!DkmneGr&1YFsaiic<$@fR%MR&P5{8B0mH&O0pb6y6~S4pe7@PJ(V#9uf zsz%Wh7%Am!ZF*z$99lzuoI74+9=OXa2s*S@=9(rnQr4+bMRYXNG61-~xx88Cvf8Ny z1ADKQNQ0m??qhVgSKc#Yaf5@BVU}4V+{_xB>Ie%l9$LEqIe@7=V5jbQ*(kj-%O_f4 zVvSi+C;R+nc35)j|n@?f`p)G@cj-bSJ zdIaVBy}8P8ku{Z==>>Ern?l{Yz@JHU!=+%g2^%#iUxx$sD=6HXT~ybY{E}MD;6qtb zbg0&&%ea|V5nN-F{_*)njtg^DuOakrfO58Q50}JgnT_onx+}{mS|t$y-KnPWT(sI! z1^79h@L6fvnwR2<397cIA{W_MR}^gzRMlQ#lBq?t0rS=leTu_!1+MLHzP^+nKLWoB zp5`}KtPly{aB}$qoXu!5(-Du~;GUNZ9O3ydHDquWOI^zwC{K3%; zK*Yyk1+<6*OdwR3>V|IXsiH`d`RZ2rAg){!v~nGaR3X4n&k9xUgDiSAfuS36QZ`5@ z*9d=AZ!B$ug!nMGxy>vSBgqXV!0yL9>^WVqfz6N58Ph}C-)N(N0GD{8 zYq^yL++NLl9p=h|t+qRFAZu;!i`?4GGh)o=-V!!xA6B@jGzQ{xvgxX3o|2+nS8J=y zjn=Vypfbd)j#}I8r{Xb+=fu1AFoo(GNcVDegJ(61%}y~($j}03`-T-8b*^-bLA+eO zKc}uiSq=0ay?MC>AE}E#p%LM0AJM{4X`^#%W{%v&b7mo{%Ztgqq>U%$q^rP^-W*Z2^n^j!j(OWtv3EhK<@=cygdo+Z;uWs5jcq-9`e|+DEam zXdQ94$AFaz}>xm#m*f|u@+PrxXGOcpc+_9 z;!@737&UBIHNu+LOmD|$473KgW-{r}1|Y5A4s$rqY>?@py#Wcnf?JVIfM_VFOqd4O z#?q}a4(yO=aActC#*qRMoM3rG`ZTriVEOKI%_5>6d>8#GbBd7l!&o{-I zAT}*8_A6Px*=aLW@>1lz53!~XPZmHo*LqkBx@-s_?HZuwYOacANO=d)U{L~@A=cho z`dXpwvUI{8p`PY-u`HtSSF@AO+>x9Y|yuqLF^0Ma)@E&F8LmV6)UrI`sbUkv0i6ykjb zj)o`l&tG2v*$)B0qRy5frSQ>%BY1yXD#6r^xce7+rD760Zm}C-rZhkJ|J5f?KMvbaDLb*DM~k#9fIlJQdO{8pkY1_T{i?Qn)*oY zA3T8|Eb+UUCRx{&mulq(U0HI&BL2{f;W8i^bvglbh80|cP<{_rYz0CT0=b2CTEor0 z*>9AnUYAihE=?mr&}K{5v@-HPW|eYaA>}S572d9itw-szZkfjMVpr%eAoiN0TlEMV z`aiJ$8T=47AlTHFIJ8?eBwLm?V%ohU($6@WeREh>J zYhXS4pw(4L8UF*`rL^)cb{S^U`Sm_(Y6Jy?frVQ(3_zghM-Z|PVhJQ9oF$DcwPWb6z#e6s`UdnjVVCD^sbeYm90B|ms{lw;v$YY; z(}WE>Oa462awQ9f<(elKuiiuGw<p>cWUY7P6pb3pCSLP1xo$+SEI_oL z8HFmu6+FMMs!XfEh`6m`FTI1t00c7}ih%`eu-CI8sGFH_r4dMhNzpvhm#ZaEV$hah z%N32rTJ9ZXaNoBGa-FOGx!$nT4fRObmCdDqh_r`3m56m6mOW#t9~QCv9=b94P!k-t z`&;lN7kEG1!g2$MoGDkQiM?!lLy60+AE4%BvHnrak}{!m08PE!47A1i!GtxfkL2p= zLO%ZFnattx@;XEzIvge#u}v~A*OIhGBWBEFJ7nK7@HvlJ;XwWt2Y`Ie3ipp#2?+bO zTH7t|ZH4!;qJT(2t*PeH84@%hFgH~<({quEtZR4Gcq7p(2osUr!`TF%JJ94%@=X=< z!VZsR`}mO}hx(?Gzy0+;%4fKjqv=#`p5D`DmyD`iA{cc3{gr%%dl-PDx;o9YDkB3D zA>!LCSMr#RXW^c%)Xk<5z`Ek_z1YDWVJr0gR)v)51fSfSVGRce;+)8R@OZPS1vQw5 zc@IeY@!TW7xb}T0hHtOd8lhB#6%7UO5OCPe97^iyW~1abgDo7sL+XS{(pxCzs?H1@ z)zO=_in=nE25LS0cJKg5jBAxiFC&2w-1%Z5-@Uz(Uw`uoYdqDIBMSPD!Sjsf4BWsq zzP*)IW4$2fyl+^0x}tlg+gzM$%`lrftZ~p62d7#V$NONb_6-O5U|laa;HvTa$u!e~ zenzneBcMzio(!Cp)vKOp`>xUEoem)X)5oXs7k~DJC2Zr_%e~$C4U9BV6w5~0Cz9^z z<&yLSIJ#Y?oMy>9%W9yByzJJp-;*Clbb7m21xjz_;INS6W5{ixZB#%PG#UW9ylCYA z``2H|n{tfL0yuz-olX~U=SCViW^{Z3n0PcBXx`#sHq*1q#uJN4p~#SEYFF7=$?6HN zF_(Y%{!AV{IgvlV2QP`~LEF+gxL{w_?9&I3z`%j#l2(?8%*21@mBuR97FnubE1yN| zZQO`Z_dDinh&DRa!T=pGV>(4UE_tAm0UA9=xpfl1#{LXIwM?*|EP*&>3*H89dNh=W^I8E0V+>4hT9aN_0d2otVr{nCJhv!T^7JYEtPNX&tsu8=;lT_* zi$ELXJ41q2eIKQtE~w%eJheo+KuII9ZDMMuS@S&9jX67i3-|S%BF5!(2Zbp)v zs&dPDrXwp$%cJr*w~7`s?LkIzR?|bEFe)tS5qPpGr@Y~QzP{Z88QtppG*ySGg_#8{ zLgoz_dXaIf-}S*!nh{ZJ<%f3BWlQ+Ofx4$*PbU+H1jgVWUtefjcG3hHAj1MmCC5P= zpWc%jCM{Db)8EZOkRt%WcJTSdC8Ttm1UHCTE$_Ca0Tf4;iG+QPuFRhi~)Hc$Xu*NNOL z*D9CltLK*t@?X~>Vcek42!ajZDY%PzD3Mhw>+~C0fyZ`RbWrOEuj^?BKM*i++ z-$*eV%Bz>}<;VL=ZQPk1Ph`B9$+xerAYKQWYVsfd`K4Sob9~NThKT&;7w_f2oy;}) zh;DOpvy#ulN}hZ=1fN^TuMwm;Is902?ic{cAZM=;?I*p!{(yAR2T+b2Juru-GrY%X zXAAt!x?Wqn8_JFe^|h5^YOZW_CCEU)mQ5RKokF+# zXP<*Zq$@v?6}+o4=IEvGY9KpkknLGvQK7iZDxwWaz2HbQkY_ol>)d-pyPhW*mPd0y`5(XE>VcrbmkErZ(ORGrMK$Tu*=`ooHfD&?Pyq&%x zWyVZ0Mx_UepmJ-oqmgROBil9F=kD8A7jgl9^WflGepFd~HjMN12tvy$g$WxB;4q&<5nu1ELb?Iva0L-Ngs_+83QB7v)%`*)<`cND z6S;T|IoVr-hz4#un+9mz^!Cb9w-^wBGFxG-_CTHUQ>^j*p?1fP7os_2TET$zj8Ojn z`1Q5?{Wt9F(I^dZ0oVTUXsY$i_wji{7=RxiRZ99_%Yiojd~lp;#U*}YSpr}L9fPZUIK;m(Hg5fo1?zj<{o|9)|( zC6DV>tyI7o%4m&%3l);4p9P!vc*|MDKzi#nK*4UOF7N@zW8ymBpV2+u%QZf4v)aoX zN^k)=9I{l65%DBJgoz6)a@tsrI$JYf{ft*>T<7hOBjVNhL!~} zQgjLfH5lp;^*QK>L7q8R8I+>gTlCf2Ol3TJ$b`-ay* z7(c<^j%c7u>p5D-i3L=ch>z=Bldr9V22Ly1A_wJUg1NP|^$D~bK?|36ZQDclUlDI} zBt~v(9bQmnb+f+I8~OCvkL1<6Z#30~b%c#Zb=huwq%eA~zGtCfaXwI1d%Gl#ADrZR zKWynVpU&kO6x?hwGvUju)>y|jYEE5LG@S#Y??7_1o=Wr8GE>rY(So8mMySa>FcEd2 zE-(43lIb%{DE0v&8Wk3cJZC-ANU!v;lxsp)etLufZAX_)X-&l#~r%`($$~kG1Pa@Dw1cn9;1hL1R7q zINFfU!~jfT)3ul=Mbyl-x`C_pw{LIccNe8RURXP#5iIg01o<08LYv)K(@1V%!N-G2 z0~<`_CBmaXG39|}Ou=iW@1M-o6=OE71u0D_pHg0{tWW%a)#|I_}JHjQYMJHbByOvzLLY;rF?Zcm0z9DR0;i?pDcB6zxr|kWiXZT z)C?^-%?DV!F^uxjVkn zIM!4Y9e%`oN}^3heXBwPS`1h+_jQT2ySiDk53S}k*4ArYJ9Uqmp>mn%_wTq_b7R5n-dmlm0wOcZq~TFlJF)|3F{Hmw1v@>pW?OB5bvmF7gz zC1TDN2gB&Fc}0Oj%aF`B9cWIbv{E>SF>k?7(w zh*vRGj0YIM7ub4s_-$lTmpfH>-EBPa8R1Ow|LhevGC} zwHCUuTsEe$Xc7`R-^`l1>d>UYND27m@4v(I&wywuO;W#wVq(tX9(?G__t;NP!+3xg z>~$sEB~JYss6F570oF0*nV~NYd~rM|jVVwr%O__r8i)(Slex7$F08s$lXKaK07{xN z1nHC2`c|ulCI=Ha+F8nPfAEET^e0c{&C3;(>`IOwJ%Qw0$@Aa8mtTCflW)FzBme3zKav;UU&{H- zd-(}?WO=@p^Q$Wu1}4R`-{BbRPnm(s&h-Z}+~TlLpqLJx90KKGeaE@h$Fl?TmI=?) zjIzK}9&5GM@1QvT)6Y4`omEXW_HQVUo}J*Fu9Ze1MMO`B)hh(@h1#%OY+8jSVAJ@q z8%)$O0*$t;(li@SfYq_5(Re-9Ij=|&)S?}`iE`;Zuu%s`(r#JuNzM{Hwj#pXrU=df zxb7bxYfZB>MqzP^73oLLnx=wXN4q|2&M8MVNBV+N8`TJ!;&HI*JAo7_Mo1ZS^X;gD>id?fy zg0+F-X0dj$uAJEn z-jF}DjZkCtjsx8cOJdGc8Cr6)hA`t;Z7R#?qVi&*X*@$MS_+D&*kxF29;pmqXiBZk z6%{tR+aY?_7i|_B#Pe#@JsNr{leQxQy7^}a_|IS7>KZ*dSb!AI@mq1xKjJ5O_n0AH3(H}9+n9=0|6B~qU;*0uh?z0EA$O$1*X(cX0Cyj zw}`r5-8M2W*m?O%TUz1dV%-T1E3ahPS%ra5vttm2Td_yv@&dt<-+Z-1gtk;;8}q3~ zh_%ZRUN>@rh}~>TeAdPJnbbg>4HPgN2n@4IgBZJt_bQu52+zm>;N=W>swgXIK4mX*B0bN%}J3wiwUBl+uJypUVaBOl$H z%76Oh3;F9`ej}gXKaxWr%nZhXwbS&h?jMcir}x3@AwZ`CzIe9i(vHUR!+UVwkEZGw zRgJZ;vM4$^N@gm+xG{AF@-qc5|D(rnyTKP0pk&5mtms8KO5=I5+HD3!@E#qf#~{>R zzPseyXOjWe@NUaWjVax6jvuAF3kE~E*0(&zjHH*fB2`8AY$V)RR!X5Al|@r$NWmTW z9B9)?jW(klFw35vU$hBx0Y-H^$*jv1^F`Sq-2oo;&U7@OmBdm`)RL>464HDj zM?7NiHSPVT{H=h0(s5u(nB#2q1m_!cfUA9MrmaWDsL28Ld8D0i^AAdUB& z)Yd)OUKLfL$(JLG=9a=J>k!`7q}DDqM~DCuUNEx84QI{k zl2I{0C=S&Amk&=t{6-x+YKFsVJy&dH1YuZdW&2QTX!o#sTfBFy^>T&g)D_V-mw=Uh z4#~NU4@O$Hr`SC6a8Q$)6JwN|p)?$z2Nfh2@T@DB%8Ex~b;alf-%Pv)DI)-00= z%CV-~vC#$r6mGg^9F;g{0sPPmF0DHY@gF`pmS4X{%saP^K(VwGm%$_`%|VaiBOFM1 zIFV1Vt`W%Ut1Iw^>?(7;k{^9Omd}5Bf;|B0esCb$Ukv5q#a5%|q)#gCff*HHa^uaV z3!3Q7^q0>b1Jyz4e|I&PtM5zY+t_S?)GHk}2D~zDIvaJKN(NajbUb9EHPgRGWc%~a z7#x~ny&LNsGlf!RzUn^Ld^RghV=N$39+^?pAr17N^}U%s%LF_)-8fFFTQln%-#I-^2zn>vVgGOc z{ipKpPY%@cDH>1ekER^bu%%2p#^>_=_vi8@f=_H9TQiD1AL#EFP`Vc!a3t*NGmr=1 zM2XfX1VWr`%;3_yYjB%u^?0-cKV`kP>33{sI|4%g?|=NU?9TBz7@mrXm~=>EMF2L| z!H^@n9$EYTPWPl>t#$(n!TTDRu3>{?f{z-ASUce`F+FEZvra5F-Id!2gTj&`gAwJthuzhk_Bd56(*fT@QwWM)2YSzqYDsIa6tpiJ$K(XZK-dWms-A0>Va+o!9 z^FBM6!!@ep?-3RL6kOjYfL33@-8tiUFdW1q{JhiG7K)_nXeljh8+D6kB3fYdu3@Ws zHrYI!X|Fvh2~9BP_*W9wg^5Y2Lx46OWEv`0r1zG}o%q`!5z<(Z#gq+mnMan_n(uc! zpJ{un>vwPqueI`AK8E5gZ+D8OhjjMwUY3zD?cx2q8#%^atT`DiSdFMss9Em&ZbnbL zqw7E;PA|jtc1t*h;OX!z%o<c7J-?|^-8X; zEaj^T#%M0dRo6o`q}BpU6zQ%Y$Mw+YkHI9$!O=iz8%E#n9Y54AC-fEycDpIBRVf@E z9mwG!0zQ`)>gpaJAIe8hA7Q<>I&*~)SJvy(uucY6cf4v?bAjssu^zG-X1}tY-I?{C zv z3raM*+ADa1cHObg<7~7O3Rm137z`H2 zh+a;|R}?;iLjf2|`l8VqEMMdq*pT~VY80zutQUhp%rvzu^E64lh$;lD*Gm~RS_&mL zQA(pO^-#m_R44(F7)F9vF2jC+eEjp}OQv#y-#f(tDz%JzkK2z&kBl;^&c9tAH%%#xf!sAcPBYSrwCu$8a$kbxlfE9SjUL z(LJm8#tjzUYc7V$wMW#!+69~a@EJn%R^m3~NSiULht{ae%m}wm_o{C7K$G8_B6#zF zL^61uox90|qf;#S>%aNAj98^Y5|+`k5o;-@I`NJXuYsc14V3*7haLP(VM)0|onH)P zxwjd=95Klr)XK}{R(-Bsf|dr_z;NDU?dc4KR=8M~4(k6W$0NDMwV%Oi|MnVuErk7x zqlK)po#xRosdvo8-4PW`_ja(t5;kK$6n4(Yo+x8ivF3+VcUqO@T?$P`WFQt%Nan!~S_J z$<+F>QV+rHyIXm|N+ASvrVAL{5?t#-1r0K{f=;P)LLAWmQ9-VJYN74GSfTRb5JbpH45%@VYRZ~{I*hs zSt!%7SH;R^1gpxJi!8G$4^}#iMh9|z^IX3A>O1-Lho8y}sB|(P>ZsZ;zk-JW1^Cl1 zekect2Lxb%zBu1`#royfFEu#9)DkJIb2BNZsHsS$(k8q88tZ3~*j!VnM5EqK6B6&G zN|?<_y}Vz4MPta?F7_fM;Z1oZT{7Z00vL)l4D<;M*x?v1Et}PHCUk98Pv%?IVlC(> z=R@l`NZy!(+1RNltJO%cU4HF2Lv@gQk9upl8~h;F@PXZ5GMmk;!AvqXMbiap7?cCT zV{5$By`EH6;7nzr8LpEKYPaBH5hXpbv15k?;~ z>)xtO6<`&I8X!f441UZopUCmiOb+MwWDQhQ=eOWWhFSr_T(tnQpOeHi@`!_VI=c@m zMlPe08y4W>0fV-6Vu5z0f13Z!F$$Gm^k2Yy-T={%SPo zrACyGsNxXD_tzt~PlDTb^G<#MLwO1%_V5&L^0h+Z2wzPzdE~IyP*}<6XZz}o!{g1zqU;M*c1UD(Z-VXkN_}eC;Ddo`>2ds?ancmKR-OeK>7z zFd5pQuIOz|iN+i2B%`Tw!OBC`4}_r}%q%c^g|k?$EiaJhcay5D@>)G8Mv{dFbWAD$ zl*G>Jfupz4s;%3HdX}vBCf`rQx!gwUP|ovLTfb1N9E~>MQcU?tw5G9zkBQ|Mi!Io2 zMH8_#IXh@BF6HdOy@j4vZ7fW*&-1~+3Jl=A*$8LWMrafo1=hJ2u}hlv(il~kA*F&e zo=Rg0(~1{^74$c%w%InHe8DYTSXiE$V6C?c=9VqA!2!EH#{qbR`c`ek7%Ds)u{R(V zl_g@#btCtcA<{;77Q(|ay8?X5F1Ln}EY%~?JJcR)LoLxEVi-1KondghV-p8!K$?dU zl;Kzp@_-Ml9h0NhjaEi@swL#pu?sJgsn^~tB3f~XCQ|B9Au)GGpPTYS1HkSVO|65k z>(c70>&%Ml2i)N3xmrxK8Pa8#L*dP2536+c>PqWiM^GXoL^J>7WTDr*zS+uezrJDH zOIeN}WZ;MXn@~3UbRt)Z4tr~fOL;DJDD)Z+KU|3gg;l-R?%faVI>b4dewxiFrE`7q(3#{*S z4};7cEf`x?9p&Dnpk$r=-a1XQOA9+8v#}~s-(EAR3mbR9?%xo`@0VBd_Tm}=!G*kd zf1z0g%*iBgtCf=2Lma$LD~&ZdkbrV75A zk?m4b)U>@--YAW+_u6f(-Vh%wrIkDu3!o9Y0oOVM;9!71$XTdb`eSW}z+5pNr3 zV>dJX?9rg{GC@0~%6&Fe_POda214ofa!v|M^=N3wTXK#ua}6yCfJ(6O2BJ~g4?NjT9+Wo<2fEMPx#SvpBdX`{lV>6Q+agfCxv$k!}#(;LQ~d1xsOA z%p8UuC1xSE6`|+QD65S5a(n!4Yl)xJvHJE*fUGq#1&letz28h#C`u^UfauZ)+!Ggl zyH&?gzGclRPSaGUrg82;VHE-hle433X&4&l27!*S7n`eWh1zu;y+x0i1#_OGmDe%( z78W;fD3h0fv5mfNShQvl%ul4H=HTujtlbm{SUEvh%zt&i0GT}J>6a6T;G$UrG)=U% z*cB}sBc=nU0vySooHpuuK0n*buU=it4G!QU*!^fGKMDWJXRtPwX{Nq1-E1wer+FW+ z`G_e3tlvj)rzj|#E&CmO;d3ah$p8wP5;8#Kl|zn(Hm*{;N>jnI$DfEct3Bk%N9@7d z_j|cQ;DZ)942Ymh4NNU@NT#s1B-%xL6PC&uuMlv_;0rRrSQ}buqjs!Am~e=yOSUb z$^lT>ArQ?_(|bhoC#O^x!(8iWsi>brW?rp8@I%r61*nx9xR~F)6S;>^p5G$4_VxF2 z`hE>1UCC8gN(HU(`G_s+tqm86{yJj<)=*tDDjud~u=~q3{(czt+H7}?!+OCCFXzxL z<;?QjCagZ0jpffW@E|*rR<$8#LNfqTP^*FFu$wYHhf+MCW4MhtAG&{npTr^9;11#3UE_Joft=2rx zwZxMNHwF1QZELMWK#I4BwkoQEp&g<>n|C%;Y|+$XKY)AHs>(Z&2+lJdK=aFAy^upj z%@{3YVe@#Vlh*67yUM zcv?Z`?-8_QgS~DnU1eJY1YkI8we+Bac<3ELgv+zn5Zs~W#EmqP#ng|=(jpKkw02ln zd{|`msT(2+%Nht_BoPPX{~wHf7oa5+1Y-R#f< zmu8GW(Uj>Enyx}c3;v1oTG%!n2V@IsVG382Q};M8giPl^`I({30oG<0qOW1_Wr;Pu z!ojac94WAqheKxckHPO$IzVRxWyc!U>z%cwIzS*tgGp{}NntSYo$ETX=#g6GE%P;j z68X0|f*M46^c;4}TdYT+#r4Y5GJhFCkCUd?nvN zxo#>bE*y|CI9c|ecnakC_4PH(#!&8$GkFMPdv6KU^XuQrbT^WFkeRQrXTQWAu7URd z_3;cY_)ZP|vb20RjtgVMfGuA8+uK6U!S+0YywYI)a?SLhN_IS~!P>|(P^QD8IA96N zt#Z^e%Z6eOcVk_P3QHGZj^uKErC9@W_8tTkK+0u}YjI*D|Gko>mW^Gl;2EtO&7I?3 zu)RJbxY0W!kxR6Os@~wVGz7+IYy@an3=SX%D>-Kv8;X;>Fx|>Uv6mk|%jJ^?Ov>l1 z7~bzhzP%Rt{Tj4FZXMy*%}k)zKnP|=^l@wd3(O5AmK42&G z%z74RQ&^@H19X&Z2Ua#Vdl`mvKA*}0oatt>0y3@I&P;hK_=st~iue>e^C(gU90A0L zhFF`MinX^^|JTzi2pc(^y7ppcO`T@);`*)37N1%754Ad_1}M0Jm6Mh%#o9)5U73tL zB7X(q^ZofdSd~(1OL;-gV9gjA)zkJ5KH*TL4d=13(FaR5{^bwIl*CH;Cm(wt8v|K zH7a-wzHI`YX#)3=4P6VLdPU_wpk4%L$@{0UjIxc)qP0XPNA&dBT*9=_)Qt*UyD5ayOk{9>i**YFqOw&Zsq>>OZom^KsmxleDe3;(C*zx^T|f0ub}iUhjRH0 z!JxrPiZ6c-MfEX^&}#|fb0|}m0*>V=WMZB#>Ut-a5V9q+ zAgmCAkwIo4%L2-5qot6ACYBF44ub2RnW0Zf|4o^iS{QelA%dC@;UPdNeSVb5lZ8$G zWBHtR;sJuL5R`hivL*=!N0mIAWEw4gJUo;f5BtBqzLI&E%AeeW7XbNx1)05pypDM< zGqHYy)E6tZDXVp0R1-}}lcHmD5_(mIqBu**BPYnl7vwj`xE?(OwsgxIYp`W$e^yz> zwsN`_-XJFT5>2hz$*nR?Ga_~HvAx}c9qww0bp_T|HJp3}()!sC@5}RVuJzz^(we4V z)RpBDiiN<*fzcvrAvt|mW=$&FB-SVnsxb3B_{=%jDs)b|ck_)67th730+UOhhl-@ItJCEG-bDQeOgbyu_hA z0W`k_!nG-0Rsq9c%B$<8cFbVvK-Bz0>n)=VE4eQ0yulV>t5te0!zs)_($G z_TPSdB#SXH`=}9Q&`nMnT@o81sj<%5s5Mb{sEDPyJxpA#SqBS6Ivrd8#mp`DcDq-1orUpRRvBIWE2%ag z$@&N!F*L#Wypi)i9VjpPFET`dKZncvaVa0KVMOsvr(a~+xZ>&8*!dqv`SL0FpWhqV zfAMEabrnX2?_ z+~#+9=AS&A%NVpz*uK`bUjyvRLOX*4{c7?w?wy<%=ipAFdRG4%d;b}1+nQa6VRMC@ z_t__(o9}%&cE9dMHyc0$4I+aSAqEpvk!8~=ik4Kavg8tfSa!*@WST|$hiO_QEm5Xb z6zQT#8x|A5On?m#K%;Ydp%bjR_23o_aIYJ9kCs$Mfs4xn!L7Gu_*t%zdc7emjaHW_PDx6( zO-BpaZ!WQBdw7;Jj(r-(P21Y9JP27*j^Z zdwI#`}QII!op zHgUL*Z5#LTVcAnfkps(&w-D0V-6i`E0cnb)piyfG9-$VZ=rltsPMMxQtLA%!##69*m}V-bv9cU$`MAvF3| zzC_t_#QDp$AT<^FxEyC#i58V%q9QhxTBHq7j;S8xvXkH|%4~rx{EYL$bxE$tm}!Bt zlH9&BI>#mDYezGt*n;I{qLH7)%#}SFlu)N65x{ohX39*ZYQs`Dbzw-i6cgQN9=Uop zGod=4dJ#leDfwOcbxJK*LZG-V5rmyMD%>xS^60NnNW~_VaW+BS2aAE4H){wQ1=93K z2(Yi6PVL1@2ohrDXJ{zkl*GyR9yap>oYE~EEZ26M2sSjB24pZC+1F1d_U$8zKnz4Z zxUt=kKm^r5T8Or5vWm z7MP{fXq<^}xeH)r9ruspT@*ps23$$;XW}L$F+pZ{OJ?ysk;O-VChFaV6jUTMKrU$H zjUxuaPCe0Row|I|w98fUXwpTdMtFUuzGYpFCBl|>-g(#FzP*olK%U^4jZZq(J9yca z47$Cg?Y&zUJhNDv+jDOv2vUe7$S^*32W8C!INp=kr0vMAeI2W;TiVw*BRl*cwwosi zuG@HqW4z~kX2(=PN06-no`qEYG!f$lZ`+aqDMOf{M@%5B=!M? zGm!OitZ;uwzGTxnh@j+&Qg!~-2e};hwrGQyG?BqY8jECdv!L`9OI8FrVVk)N#aa+K zCe~l0R9?;1XPR}2BR6e?W@7(ztizeT3EEzyjlu;$;v&fJ$1A8W4j$A#cTBUT!j+sQ z-5&W6KN}+DO69o0?;h3c6W7-4!!~)~ThyedT0Ko8uJa~zjxo9Obg$+E5ojOHA z6LIAH5TuhG=W*v&r_ubvDHU9d9pE*bOXjnefDS)DGFmnbSZa4sIWEox6PDr}HrG22FS-CyoR`!q(nfkXcqjlN5=00Fp@>rky_Ei2sf_9f4WX}n zfP=0(v5x`Myw>zXfk8G2^f^LC4cvnz0{NFt@n=*N&!i>;^vN`~&Fu|4#`nDm?Ef0p z{_)unGSZfP1FCLS!ExKQ3Q1LNml&8|$)?FGpJJa~9)508$tz!-=$pNiM4>ooMV zx-NALbdIn=p1{()jvMm?Nv=4dfHzAfXvs0A6ON<-Sf-%ntY#;;=115#6V518>W8Op zEfB2Hbm6_B8ydwI_@P<>-)s_$7o`MQXbay@{-OB{>U>t{Frtz*<3_Y_ z43Ii*Tq_-%$lU1k4<_!!E@z#kTFh*>t<1d4M)E;v)BHIi+~5Q9Q*b6>-LF{U$N(2*s%1*rsboK9phA6Jj!k7C18!0N(ArHE}`t5^P1Z= zZvtIM5Jg9`m{NUaq7B?jZ{r?3Kq;AM0^=BE`{(e@Gi;tR#}R~%XpDQdzE-zAG(VWp zy!vn;pOvS!IyOfpc!GQJV2H!y5S>umubsAAoQ_!r$FBa&o* zNKzvfd0s-nwVDXR88GsG?BW9~p_v7a*~w%^N^(7|Q)8c=U_7olfc_6X0Z=uql%|e8Xi?UO7 zjnR73BdS#C!`>w+on-1I4xBNyJaNdL?GNk`@ZNi2W_K{HH+YSwQ*VTax?^9{J{Y-Z3NXOXS73g zOH()V=_-${u|^+H4vsle>+bFB1dGKKod>f}lm_x^q$!M-uW#A*m9~BHn{#=6uo`BX znusJ^gjCaw zarHlen|lTC@eqOcE^g!?r*p6>DK}5ze{p)QNzkTYg9W*10IlcLG%IZjH*i1|W(x-H zUcUh|OWi_H<$d7+SR+<7_Zp6UhS5UVAZVa0FR0ZN?f_T1BpFL%eH3(PU))Cbu+s$Q zG!x0Y#OI-QAp`s-vY79?-mqO{)^F^e+It9SoZz4bOAV#$N3Zs5ce`a@x;wTzM`sE` zwar+HC|m$P^hC{W;5@)dM^!c4zpl37$z_&GK$HoiF zuX-ZUfL6O}o5+ASH+K*mffe3=D|oTgB`rY1EMkDNEnu}EbDfcmH-8OSQD!ec-9tm+ z1AFCTySDT4H7k3&RyHr$!$W}W$f#fW_|wW3794N;U71w4$0KAjubd$pX#o1bIsh~= zxpQRuzx1~4ZNF{zIpxI41J8>y;P-)Vz0U3_CCxb3Oo|J-xkGgAC$?!1k@0QDGvUE~ z+{iOh0Ft(SFda*(3^kw#^dH&UQbu)eG}R0}JG%SGZrAWy&+Ks8BGULMG3+S#{gTE- zH^6VAtYe^K?XEhAJUeDBcz7RKqX=+K>PyUCdW3v%U{OSs)O=EDc>gvKsHyl&T#+t) zHM{`@3n)TCayqA3QYo!_vZK_5_-F3pZ8hu>jx0{`na03cNBFF7;Pvi=EVj-D2Km2f zsb*N48CAiKFOnm`B(I}M^Uio`qnyo3W}m4r(lg-U3+hqQ9~#Fj2Ld=pTxe`}DKjuO zD>C^WIp0q`iqfaPcpsR5bUV}|M8+uU&K&Q9Qd>5!vG}x%2Mxv}9NcPpaTKrSYDE%w zR`Sz85;m~eVCrOP8JYxBlQN{nWl(&soaRamUpMT%dtB2{2;@76-y z94>_Ad}euTf{ikh$RwY1-dwYEN;2`n-MvUvsS|r}$t`jYc~cmS0hh@$j^I2`$^%@_ zZ{Rw*csLJ$8O%sWAO!^1+QQFl(&QT5(Fb^teR50@)SB3YT`c4o4XdN6ZQw%Jk@awS z9Rf3%n-hw;;p$~w0tb-{8%v#{{=tI-oH>m>7M&>brcUYC%SHt^482y$z$+wKZn=z9 znrz~<+HTh6;gC@RVObGIsXBwqcB2g(4y7Ir3TpT*H9FQ1H}eY0x}Sb#!@j*9*_R%8 zsm_aIca9R|uUuvsykTGcpl`454^iTt+V6O73-5`nDt1~%QcVV!2S0VmaIZJ8@uoO^ zPZu){naf6?bYELeILu?sDB>AJz{wievzZ`(N(r1@$k~KlxrS32Vq#=HWM~5&G@N&4 z+1p0O+wH6?2x@GLnIDbBPgm3 zfxosy9ZCvjXcJ@ykpeCfW&$ACD8T4WAK8_g*m86PJUg~&t8eYk?~7xIW5`DnNWSL) z9X{{-i<<4R>sl~-MI)+%jwdkI-CIwg3AJGn-uv%;?(>!+VE5`I>O?x=II_-ahYH|_ zz}|-F;2zGnmZ!qtM7*Gq3qEI;Q#c5ua|&)K+a`ZAw-aRM$AEW4-w2HhmLi&lBNc|K zJHj%!W)WQQjHxhvigJHSAzHldb!4f(?eZ>~J#+i!{YN%LU}p1>O+W^zyq1K_FLB*X z3u6$GkHJ>@e45HfR?9t+vWbZI7-1m|xM$QHT>5}T9JqXVW}|7WtdWe3@q~wBu7jUa z{XDeSn=N~PIu>3zU2AF|joEz^zMja(W=2OkbD?F?WO667YhRUab$W(o%N&i4?G&YX4{InbqM$+?2me|NskXYR zMSv2WIuoav#4Q)&xplT=lOjnf8JlxNEy|U`&gWwpqR)$-Mt#y&k9KDklfk`oNhNlf z`2F&jMY%9X2|}fpwe}hUV%NGn{c?H`7viHG4aWAh1~wCd#To$AEr73UIH@jU5p=PU zq>6hf=KL!ILZrzPVj_u1#CX4&m#}B_%*$Y2>$)aJ)3Ve>DHc(eJ!`d;H)1-Kqi?Yg+6xr7B44dv!+ez_* zRCp1a91+daC2?l1l}K5w*zFiLaL?8e^do%c-K(iR@l@O1y#K!4KW?G1)sdn#A(c@< zcQzcdgm@N(Feajz-xwTA?T3orS?ry8W`s4$@FVNzR17(gXDBz<69n94Bs$CqCBDo% z#AWPmKB`zK6-6&&y0_twIGb^5rjB(UDce6r;2=)BnY-fi*Y4gHH@A&MV;cw1;Q)IM z4V~5+GW@lUWY3Ds{hrHUTE=Xm3Ls=`(C{lmR>u*!^+sJyoDPM0)yMY?R9+ukAoLtT za&lQ%p30KRs5s>+g;+qGNUwtIO^_l3z)z9^+yPVouqT~-qyGr$D1{=iP7hB&;V z@lw!Fv{tY??6!QF#1cZu7bL05sA7|Q`9`iB<&w^ZA0Vm%+i7ihSD2lh<=7TD$@p{m z+<(uko(FARUU;54ky+jnh~{SEoPlLI#fHP>waC_bb@>e!F@;x|LC$5w(ndh&Aa%Wg z#=yp6i~zV0_?zV3^jIYBH=6^yp)?X7BzbNW6lHggo(>$hW9h0EI!g*H#4Yld0U8gm z=!PuA3m?|zjmjkR9fbk^AT`Di6?;QPBRoZ-6SVSPAs4Pi%7iEksTxE{Tz5$z5@SWo zcHfd^5TGEPMU5K53krMC<&||yAs>=Rxa;` zB?t8bcC!#gDRrv^F0Rr=C{y7irh$G!BJx2lUBr?AbeHm3^QAD^(UcyyXm6oxo#TPk z8!fBjxlkOzg=Rg2AZ9qkA;lBEnK(YoO;_0yOv#m;`<$}oHu0l7y`~x}B=c%cpSL!^ z*`jOAIIQ1ttIrYE*6E?9Q<5g9LHMxHRm7oN(tb}Eqq73&nwN7fovuivSm5^&0wyOa zSaKC1+w$Q6f1WMrS+kg~3m>K=e1dy>g2VhhV7INK$c~T=7BX!*As!t>$ zx+ZxN7q%z^JC}(Dn|D-UCZNA=F3-kNw@Zz|X=!e}8ik36DDYe}HHNs4>ZR*2&ZXa% zi|k80mu2p}pwpU*^Nz3w$HOz=t2kKd9o#FY9&)7Gg|rEfu&QvzjZgQ`wx52qCQm8S|nUi?qHR02|%f zv-Ne{zs+qc`+f0#2|vj*FThOjf``)$}kt{f$!7HtcAY7tB z0*r(%Z46?|g_bwVJkBsGaN+wl@LAWfz~;gX3p>UGV8G`*BoB!|?6Ev77-5gp6{IaC z0>;`pGE}zfaS9TM@2#aWwI<8IES#6OAVd*xY+06CU(mnxfb6+Ysxy$MY%u?OJ(gEikYx2yNVj%q8u3eSZY5!$ok0A z7lN=5IJpkzZj0KK!3!Dmb? zMS07^n$@AlCK@{LbFLkg`b!;}uRI4V7jW2%AO9ETt5f zTb4_YJY}+~4iuLPZ^TI5Ow=`JuXw;rB-TI%3MaUdnrorz&-gwjRXKYDS9`r{mR%)uOYa=m z18nB&Yio8=pP_}HNSkN9JvQ8rI@ICeP13O zo^BH#K_dxZb8NfFIw+5HU#$L8{vlGw;igA+)Wqv-qFa0U^0wVMc!Z8Qd48NOs0lwy zM6O23V}V8>ujE9SkU!oa(keh9bl#b%azv3-6B1h4K^Gn}SC-VQmlC7q>7V0XH>iu& zsA+N1xQ7x~gLzFFh1c5n?87A*LuNOaSz}{PkQME9Q=er{-PJ4C>jSe)D>8Z{i78$X z`Gwpe0|sx-Uza$ZqLB5*fk-cvBA?~t6aq8G8S%wN8|5#aA^nA|rt+Dl=mP{GDj#pJ z?>G%>IWjuOY+(WP+M48d@6IFNWLzt?>)GUuh1E)mZ?*2F>sez8nk#V|lb}oSgWR(yiX8OT zx|U)y9H3mPW1(tl0VniYwi^gt7F+NAEPHG365 zbHdHd#ZSX)GSQKKvuvT88lBBr^~^rtr^_7UqL5^b7K+r@!y??=01n1epm58zc&fLH#3CC!J?qgIsIh1*Qtnn)ZXro$3ru#(^$`mI)%OGyZxeH?xLMB$FN*2Sq-3sJv!5{zLqaLfXD*7$l{Kt z-u-t*gqODR)LxbzSZjVDoecW6u>lOJQP)g&pBA2$$}V+UqwISs87#`Mm-3~VBl9k> zCQ`dLFPz^gm8VQuAQiWHQOuw?we#={%N9S=%$wF857A(#NjGxw{zG;D=%CC70jXZ% zFC7h}h#R3%kf7VPL=c=#G95t7!kHl}Ae(bMNegP$5L6LVm_*WAvrx8?Dq~c$fG`Yx zqD4Kvh5xJ$$TWS$2HV!aYnd@3a2d}Zt@QwV!hKqK3@5O zdjOP=5X<4j>3jWIENUAo4>oZa22`M*UIkF9#T?a9O6)!WQm9%^5og2ejTeF z5@u=EQj3(?P%t5%mkLs8BiHO=M7u*OloD^-#trUv9WMzZI#ovFUD#Y3i=_$|WxUE0 zO{GpGwtM0mki=(Xc=}R`U>V@Ga}ae)l@MoEa*zIS-`0b8Z4nRoh@b0=;8yVOSUUuR}ICg znu;oVStKuQ-7865B?x#}XEpk%0KUNcvPqQJBTZ^>6fs`W(yBp!&j`KZLUTddQic3y zNPeE@GbzMeY5-To9vCbq@`D~DQyQ|_MQRC$4RDxuvA8eaI+2kDxlh1w9?(WWWvl zB9IH>IJ4ACnFdZ7#)AoPL((+1TKLdR>v6*$kh&sCj;VeBPk#(p zs>4V+%#!n+cs>3k3CB52uyGUFjiQVXCo>|J76_~m;z4Sa8Zposw`PuDqo{>G*ue+W%t-rq^$)w0CyhOODys5?nB$?kf2W)uDU%K zC-!W2fY;n|Nq2lts{W5d7gPj(=GqRvpAu_{^~Z%qF>Q{ulTXzIpm+2vAK?&ViHr<_ z_l}1lw%D(IaAa-5F4$C``0X#+@B5J#?ew*MJpQGfOj*LSq--bwkd*@NFWClzNo3#n z`poVE+8K;mZimT%eyxqd2Lu9~B8(vn=~sH3{Cm#SW;*bE{&fVroQ;}8G!92tPnYU# z5%~Maf<~o_8F*JGumnOnDEbn9ex{DATg~RkV(B40#PweP$PN3%vrpilIJDEVxg7vE zeS~V@WH1x?{A@PYflex#aAAs@Qh7P{Y)M`#OP|u<;ZLY)Mp0C9LeqtKgybypUs=LP zfPtAh>1)W=PpG@Y#v=CMd#pr=&c+T4WT^UTlt8*^wrgolTXN=V|7+Qx>ahN;fe|F70frNpEh9n>EM7VfjY` z7~1Ml&Z$`ev)R&vA(d^IF_gjR zsp6EDp`*unSik^H&V=G1oT2noM-tfqn-a}-7tdj7Z@>2@N}id7BF3TIq>#jJud9*G z9)f$h*0Rk`-N~=Zr9^jAiUd%P5*bggLI0VC6jmsO^}#8yMI4M&E-$i8RKO$2f2rF| zo*J;d2A-8 zpcW7*HmsyjPF$EuL5XuG2QnWgc8hh4Oy}C=p51zW+b+-1I840h$Pvro2muhy!^4v^ zH6PcvI?D3Xxbo1(mGQ)(3iraqXyAQkLV>6zYDpo!t2b;|4e>x{0IEfvtuB$EOpyf4 z8$M$A2$?oNf1m3I!L+sD%s$rSF&dCZC~eo*Z4KA4e(R2a^mL3EaR6L< z0l2M$#A+h7qIC^f;3b=-D0rlLak_*O{|wS3c{AbSB-6&_j(Q8R6u5E7?AC2(tD%5B zqDaNGW)Jq4z@||-Zl)6XNXsC^$OT>odrCdVxD1`6%5N4Ap>2{Y*xJ~< z19He39^^ju$xmnzef5`qQ%2ev)aNu!=Ki0f@jxBd6c4;bo)~b5lgShxaDa_Kx<=~V zI(308oTjmaAKB?jY7-l@Vtc*5VLviDuph%>`dYMR8yx*Is&eG^U~p_l%dxJvgU)2T zxh+R6vdX_zl5{;2uCT`D1KMVP?f?CYwu#TNQ*Y^E)+GOp-v?uJ0<0WBr%wbk1uwN? z$^sJd#KKA3mXY1j!j)D?!Jr*L2MwHS&)iZuH{+O4E!APOKo*=Kn_e*M1y;8NMmie| zt-l|++&qaA^+XSGxrUr)8=1{6f+>P+iTg-AGeJ36@1X&aB8xbS_0UIW6kQnUq)uv0 z4f;_4Le5;YM#QEkZYxY**t1hGil8R0#-Q46rK{ zUd+$*j1g>bPc{(veg+S7?d)wk7?12zDGolAAyY{!qI5y9yLlBDB7#2_h+V;2nx4%h zYsRXAlKg6BrqsfVk#+RZSuSlib7}S|4$&s=$$Kx9tdU5z=g^S>$((gFOttsrx{6l{@Rr;p5LD6H>Z7kzT+YO z9)~XWAt^qMaxR)xLAnVtuqO7%+D^xQ`sL4xs`3Z{^bBkE7(uE}uIhMUgXt96YbN}g zjZyMli#D1&_+z)f&`97q{j_nQH#b?{t|PE*T6=p-2k;Q9aE2z?**!L@W>S-(Kk(^b zV9icTg9k&b2MU)k=ti+qut>ilouYx}G$ZcRbN^WO*!&ukSQ$2OAiNFapdJ?};v4*_Jak^zz%9A)GjA+ws{ro4&r@5W+iPa{>n$1x#f8j}$)5M)F%ye6kxqV9&3l%0*3 z1=U2+uxWyg)>M;V54Z(EbNN4@lzfQng=RHew2d$iO^m2I;R&4MHY0c}RJQWQk~b-3 znaD$n2Ep;&l$t+#bZAG+*jgx)!sU^E6fl-g3J)9{|U!pYF>p&q`TqTYUB%2GJH0lin_!TxY=sk<()m-V5Xmw zGnr`2%JHzk9W1=NXTe0BSz=sK5+r$Et|l2Oz}`W7U!0;T1VwE&4wxw%A{%`gLGWeV zpS5YCQe0Jnn?*IM>dz?InJ>7pW!6Ytgj$(jDR{qHrL7h&}`vn+qjk< ztzB_}+g6HrzuRfbN4QRTNt>zbO6D0yG|vbxkN^$5`N0{=tgK}ZAJM~O>bgk?(j+UN z0#6>zbD4(|j$md=Y^{Ned^uz0)lvyr2Dx~CR&GFfTf|Ao0Btqx;bO!g8#EtTW1Y`? zJ0wm;C@%6RFrhcE`F<-awno5RWmQCtOePaG$CcyEXWWOyQYPa(bc7+Mj@bp*1zi`j z!eT-0CrbN=BfvEGP$8g`jBNDMdRJL@A8YS}heM0TW4mz^&jt(Xjk6B^ZOyi?Y}(CR zrI-&SrxVf`ji+{ob;}rV0G$4;KeY#3)UP+Xdc9owG&T=V%i&g~qtv{**Kn zHx8$4P&Rd$bq%F+ffDG&gWlgiw${l5TdrTWtc3+djEpo14t^&ti**}|=pTLMUEox& z*i%<=c%U?F_E7R|cBO;?5CIv;sm*@bJ^Q6MW z2*l5Y;gO5ABq5dkUEE{RmCAagE@%(Icsd%QJSVAsE_Lq+Ylq`z$|N|Rhikk(yeB9p76YdP+87xa6}Rq_B=XOizX_)5h;l*9!4B@BXONL zvFvQoln?xFrD74ueY+x<7H|-MOyr-XGRyJ46=wofYbss_T z7$9}MC#n(;f6h@;Nyi0B0s}A%AS@~Q#l|A{nGP*rqsRc2U#Fg2l;jFV?p0!i>%nk3 z>|_+H!c>_~qc?ho00Xauu}mf!6j~r@pBB7yQb>@UP9@%u*fyaePA|EvR4G1_QvK3` zkEaDORabP7f;q0?=yYOJ%GCp5JUZ?x6FEi-TcZP3=&X`27Kgb+VkP`p;tU*+<`qwH z(r$0I>}mX(+yPGa01cIfkI}I!$F5Kj&7|a9GiTrDfyKd0vI#&nL)Z0fEUx+e_v}`N z4|UeJuhI#Pr8XYatz>L_^D}!Ni|(XGRg}8C6$)%bVwg02KgPy+pIIFPJg)aXN-s{C zY|tGIKeN46w<{d-#`{yrd>5SFm69XHb{f{ zIn_OYVGVI3G|LIFmj>+-%a}_&`=kGdf6E?x=}mj%wY$R3+Vxx#VL5P!sQuBT3Ecf=!h#&-=~IH8KjN6{6=Ca78epakl3yC-l!2+ccmHxla{hE4$+To2ib2Y z4y+2^jTw1vQ>mM9q>BEyr0>)|`5dx_uI0mta4M>Q6z@E;KGqkr+7t`9Ji4^~rr~v&tj`Sze?Y`k|mY*g+#=4;kJt+OVS`4*b!+ z9pE5;c`LOi8;*6e(2;JE9w3##Yxr!j^@(kb3VY|+XMA}eQHqUIU=%tX%zFqnZHGjq zF&PDTG)$m{;Q$K8(x{6RAC7#G5VAz7R??%UbT&g05V=@dp1D;XBSn}c7K?=(L8{S* zzQqW!h_ojv)vM@cGm@%3DY5sO^e+$l6LhTCtVFWWZ4riuU8AeWe2hVWIL|x^JQfa+ zGiqu^g7bKVxA959qg?j-TGJ|PlZv?=g%8c<1hWh&Oea20qnGE&i`Qc2at)hh*=+(0 zMdxdj;gms&3?QGgsya0U02exqnTb;Qmh-3!b#W8By}o1R*|BXQUwOHh+3O7e>?pPS zSQz_gksslN12k;OU~IdWt{@P$)M;ZT(?G^}GGQr%n~#&R$?_K&L_g>4Ygh|Sh(;Zq zaAYzKyzitKT29MMME-9dKGHbe1m*r^EbP;Iq%yBTAy;9Ulo^YKY0B~sz;ez(cha!v z^+joXa<=&0I8>M+@1vaW&Ih)|ROArvO{pnX^KI();qX~QHZYAnyTmOLwr#;u zGI@At@ue+Wa769WC2L&Ww)ul2>us;gc6E#cxVF_o*KuyMFQVi9spqUfS(m@RZ|UB; zjUOCX@!*7J6w3Npo-g=}3KtI}G-!^F2Z9Z{ zXx#MBY?_3ti%5l`xk>oCyEKg-g#ic~CuZ z^NMg!$1E$!G+SZ5kr(91aPj&mV~;0*Sehew3BNU@7)ir!rAt{NkZCv*`^RZpiNuDZ zKN;$^;!eR@VXL1n9#^UBzwD8zy=51xmn8ry!P;h=FztekAbwdRIId^0?+ zp$@>BOxp6LWAn_ARi#E2Wc%`9iR8v+?ykt$_ofRe^Eyi(b0r6zm?3~v^f1PDM8OMO z>lw=UNP2|f^yjA4?4B+XMQCbX=O-RojgWGkkZ_Lgym5p z8^F|qX$ZLFZ;eOj=#h+^v77|%MG`Q{s|YkFD4Dvgb#*hda-y*u1!dewc8(O3qhRU- z%;EMNOK1MTp2F$dMppQK-mz0OOeR_1hU)a;Br7wCI^=l2DH;-`EEA17`2v*5j=xlp zUgMwZn6#OV9b4``A&V6)Ax5!Fh?0)f6a0E_*;Gcx0+!fBL{D~{ zYSmr=%AT%=NY&V^4T2)yIdrTyK%{Hy3}qTy7lOPY)lWE2&UY$5JR2iQryn_F!c zi}Az>(sO*C$%vYbiJBN0Uoav^>XL94ZZ`6?n+mz52*ezrB0e}Js2{qQ8d9gkkX!}5 z`gqB1XYa}`g|J=xt3V$_m3x{E$kt1yQH}XS1FLh zrA~c?p0B-U*}WrQ*feo!*EK`jzP=;x8kNaxJkgtkwxql0F79kulg;O6e5|y`5Syuw= z?cwv^z(YRaT12y|9dkAwB?vjl=or{=ERsx;&xMmxXH#CV^ zy!iA@HN7P16`tFQwrH!;PL-Mjb?F|5*StH5?Di~0*SMu^n!WVWw*8LZ`vdmNzxeBR|1ETkHbW1So0nNk;hCy}A=y3j z*%elRb7ZS&w+9>6>z~*yD$@*3?F(2=&4M1YrH{o>Z}70MPC~6J^$PbNZqi_#xsJ%m z&Vggf8sl^3GTB3L+PiEu;Gp^F9gESS+iTlD<4KOi7S%$_Q4CKo|gBo_>@EK)VT}@E4 zu@i!58R*@DOxP*qq6(rJ1rGx1G}db318N-=LF$}`=0;21oah80?Cpm-Turzj7C8y6 zEMew&KAQ+g2=>#1BP+*KJ+C3LT`c<6*+8NJEH&o$kV&HR8R0&4HqkKzY?Ftwxb$Qh z8DQ!of8yB{65uFNic%hof%oumq(i8;(ZRi)*Z_Dp-30k>2dBq2Kn77vbJ;tRTX@n( zg8Y(&Y$+ZawJXJAW6&dqGzt2e7G|^Qco@0x!WuF-?g5sm9b{s)+$}1a#N%sz zALt*<5y>;)+tmTokO5Yrxp9d-a6F7cBSkZ0T_N+1s{Q6)3u!e{2xE#Qrl~^)5Vr&x zj<8(6JTQ^Uf99FGX8%j`EQyNO-uQFuSv0BACG)IODp&TY`cGN4X15EUGY;fqj|+i4 zBn6*1h?1}qjC)I%jJSYY!cmb)cQdl!AcwL)$hgH!cCdDCp|RLbOASh7h2yA%;<7x9 z$MUbjq>+4!$sPg5nFDHC!@Rg}=FN_Z^eMFwag!Db0NLWqu59k01BHclGO*ikd|<<)vBl76ahC5Xo~) zce1$)PE2PAn)vU0S~|r*8gZpIHv<3z1n)e9Cxt_sIJktj>OcuRm!OQp1*AFVIFwLS znPFL0D@4Nf(Z5LDwi3aPehz-PdvK&d4udXZVi6R(*$XhxZudO!4v zEw{V2eDl6_fcs7MPb>mXv$*}pIxpO^*&BD2KP{2z6)21I(}69HN6HqtMtLyxzkOTx zwa!l9aEf&^5rrs61{aUe5nj5y87mQz4Ps4O5GyQ&L2+#+lG0x&W8i@_CAah_6P2sh zs!RBSpRorRga-gKSWbmc#|XHa_&aPyVzIm(Br@mmLK9APH8@P-7R0A`Xf_m}(CwyJ zY3>sIG?ZrrG-UHZ0S6EZvLqVSFA*G0mmKk%sf$cLqq@ymP@^U%m9R&>=K7S(uu9*1 z^5xJfsuX@)2Ck1f_pJfeDy;*sch2U7hT4*~Pm`9`;X;B4Z7E2L5ELe2)ZLB(g{Q0% z9S@QmLlmZOK|4`F-$gl#s?8l(1e}C!VQoCQ-LONxA0QSENem}TVV2aYOypWP_Y6|_ zUoKl*F%=@`rji_8t2G2!zOn6Xs*LDvY=6u+~-W?Br{Z69x`i5C1GDzDSO6D2H6ES zZPB1CJj(P9l&#~jUAwVnYn`4-`_t1CysiWPG-Riddy9H(r0D=Nj1V-~;E9vECSRoJ z;?DQM9<(7`wWfwjrr@XyO>kW;7{R+*E`j74b?N|852(c4tx4S_M{u3twf@%6|AsyH z(iOY<%ng*=g;@Iyl}z5fU5aWVNloEOlq3>aT^XKF3&cS%f=i&;aPd>lUUtx}X0SZm zr^gG0Xj5e)Y3g_nGb(n@nGtk(FL7^^iu#ee_XyvM%0V0zT;jAR0#jVswB&Sb4Kzve zIkQ3h<>htje*Z^o`o&kR{e91)0d{1AU;KhKKJv7S|F+Q3z?z|y`|RC^w)5Ff*z%o+ z$S}J)*kS}s;)n|X<;xEaB)LXFoy25b1L2Wa6tJ3m*^nH~X(Oq6W9{LdCXGm3Sambm zq{D$X3IO^aBoleL4q^ID%HxugmAt{-6xrJ!JXpz+ z@!ENB7@Md|$l(B5>Zak~U^U25&NE7}(KHK(*X0R0kDNv;DhcJ(15NDZO}U~<6st)H zZWxMI_SR$T9Mv@}-#}38;Df}+EFJ2a@SyC_TBjzmbX)}G>QD|@=^~b~unErJqHEb( zw+*DWwLxE}8Cm|s2+l@*oEUA5y)l|g|B+${RA5ehAiSbSY8OmPYy;ZaH5hy%P3#@vW+umqLlLpitm`+fW7-~NiFqnB*`#wATX3=!~q0QuK& zAGtxM^r*-km&gO2qqC_9$|SLmXptP9SJFyJsfDxm#FqW^AN&z}^IP}r3%~NZP5Vo2 zOl^2}decOd1!6|L!Z|k98M2ypmK>W*m3>HCl9|9id)-+FSS#bV?nzj)I7L3hsT!2F zIStxhz3Ui_%r7+gUB66FEDKeQs zJ=f1rqfW{$xW1cDY#5LeWqHD;m_X1N3K=IE2%C*qBa##wOiG6_IQ>B+gZ{ov68nj{ z;UKPA6jXI;?r)7>$FVmCx{O8mxd9I92{7Oj1d*FKI7%Hl)znEvd-BSShaQdV9>^@eM`xhrt zY9T@5`d&wc)kvr638b-?-tzxe* z>s@4oiAau&%Io;^Y}(hxUeNS|xLjIS87TkIm9Cz5a9JRGPbt(I{z)^HaAmunJ;KYwfnWOC0)U#3vF#^$2!{?yp7G$A@dzdt6zvi0rgV zd%Cr6x2HWjp67P(!F#ry3HLz`mp%sDbt zk}|@SeQ2jyLw1)nbp~fzj6AU><1<`m9sfmm+G=Ycd|-k%`S@MjGzwkGl9q<-0>^5y zQ0_}0dQfSpNUx4$R+F_U;cDvU(Kz6TpSWdL3uNc~+l6c7Ff*CVGuL{Y zFNJYYnOM+5nD~eS$S&&~9#F(Y<5F|mon~aHu`UV&XW%3B$gV{6PY|; zN@ATt6Ewi-qnrH;ePCt7T+OF-Y-l>NUB>Hnan0KZ9%}=x0o;RwePMOWcD;%S70-Dxo5{6|$KUe0(&r ziocssl1;RcSVE|d%NHtHPw*U>fi3Aw*G^r2wyGX3eCfaqL5b8F;$`GOj+YZRA}1_V zLx68~Y&IO?jBh!Vk+dvzP0Rw5#3^1$11K|kklOY7phtwd~jcTq$nH8zRkUd(-`%Q_xTc!KsStZqFNC?OP(k_59_j+BszP)Y_ z9vxv*0hq;y&ycyLQ69k5!ulv}rc+=)Xyxx{+$fe(aSt1l$|ZbOP0oMx>UJ63zgny2 zq|<`ZLsQEEbaPho$(xt$5B|{~waH;_zxGS7+9Q0=DQ8US#6>KPz(2c5b!4cxrV=Ik z7@O+R{vo=9bEC=U3ANHtjy8>5$w>B^%XmYncMIi33z$kDVE$weX+47Jqgh?|bdve~ zs8hbBO^Yz&;jD}^p36Fqd|O|{J6T5Z|qK@q9VwGHDSk|4q}6@u`-H7v+8Am;HqZ?B*nz?yxybLT-}DqyeHhK^ioAo+F(UM59k#4=Lx*e5*gGf?lCg5 zw-EGZi8rFi*?JZMvzz-YcDaUifcF~AM|Lyrp(EUoK4FV|O9ahzvic_EP@<#yU?g2U zQfXS5ivdurl+7VhO-cmL<+~qf@i#d!!Hva6174$#AUmngtW$T<12)1^I=cZ*NXR+F zO&=TrL*41vrKd018}GhjA3U7e#-+JLlY-Tbju7p>KOJ)vP<%v}wzq~}EK)VUA z_1IVB1S(Z!QI#Q{2jK9Za4p_=ef{=*5!@35UsLl4c;%FR{zN}p!~P}yzJ|a@Qzd@3 zDH5QQ(<56RuL-&ys2lJG>-S)A-w{{E|ttZW}m3w$-ykcHjzc_cIe!v2DNp6 z=2td!n$2%M<-}m2AFi_NXQrFBSLdDZG@eA4pR#|Hc$ZEpyrgQBelR2<<8~ajSe@2o z0I+LOrDLrRp)7s2jm-a<@VxMPUrAg9wHnZ`zlKfUI0y}{GL$0mYBBP2Q7%9G#E%Cd z$qH{RS2T$8Lg#$ZGC5h^^3R==IP&9`my=ol~C77oP(rdD4n zSj50Gkt^9cX(CvZG>jtVhNicTW}Muq*)cPswH=#~rZOs#eRf^0tS~_%-1r@l8Z&K~ z&5X^Cr5a&fDxV-zqJJ`}O1r%_uvuK^)SC&zGr%mduT#omrR1UNVdtI=o168V4d%9v zY<3O5p1$#Nxb9s?ZbC5;VjV`opG)tltSWX~ zO}9S{o0vDB9yzghJLPW^yCx>f^rFnI5tf>qTG4J`z~Kq7Rmd3f=Z3rpmA-eJx5HFV!RZ# z=(O6_13X3cLWa07WAf8S>5GJidsNbI(Rp*P2o1nkkeiDYmywI0NOCLO*WOxUAMI`H z-nWp<#P1JrSaO&Ez-&aNk21WNa64C)E|*RfDRcjDb%K?|CHiCA>2>Vt<|bQ0qtl~oAj{qRBlt(S>c;t45Xqk;N)zPe7}cfqn(gg1xNu~G2NTRX@A0hp4fQU=m@^{_wuv>UZmK)LW}L+T$r_ILVGoVLU2+pDNr`L>iF zT0Fi=Y{hGcKD>CsSMB__)9Podf2Pzs0{i*Tf9Qp(*Q&QJY?ak9y?UR=!(sTj^m{Mv z)yLq7J|8VIK@!BJRY(Uos`e1LN6=%F<}!lUuJA$6h@=jK%%eX;bEF4ct+%Oe>3n!9 zh=AmD;z5g`=taIG=f~;XhU(hRh#%FO;z?3IYZ{>*KvmMl+MQgzs(IV&WNiDVbNl-r zJhZ(|WIuvy;rN*w)Vn$k#bFufD5Tq1E9~y02lmFBI5diu9i0rZR(EZDI>Iv@+6Zfn zPzY7jr_#e)C`&AY-$j!7B!f2!7NTQhdChs5%wUnpafFT{5X@GhfP1i6GGpl(#UkD> zoWia~7Aq_2=23BcPSQPjbSTxEoC9_QT#c&GrMU3${BbTwd|w7>JS9Ee_$2h8;7SHT z8vJfFo|wCk%Id``F6sH2tOO-0oG^OqEj^${K_u3*z$l2dVBgfc@&#UB+eUQ>*s9sq z95@f8*<@gY!5Lr=1ghoI4~LL(*3=4jx>MxyPTD(Jh&2>PCR-v&y+VjXDX+;9ek-S~ z5T_1t-$!hiNIZcccp1!Z%%ywWZ?>Yf_#}wN>2Q~iPbU_m^t!s&vm-22O1B>&P*H63 z3?VQCIdFnMsLk>3gh;N zw$(v!>2PvpE_;ADG~H~~>0C+%j?)}euO_a`45~xV72to*?I-qA>)W zGZP(RdULk#!wXseD!l;DS&`;Qh5_?9I3 ziiOz-Jhi2t7&Ky`P!ew5c+8V7mOM3Q@Q#NQ%g85--JNW>;NbPZuB~tCfuyUO;~iG- zNX7v^%|)%@2~4Rda<^tQRBFa{b$d;Gy_)T4@Yzoxlu}rb+zF(kTmo*>={BVXyp%$& z2}j}$WpJI5ib1s!QgTGi$#g^G#X_Gmj$1(~XSH6wYwY;fU%&NNwCdJpqAqliaYqjv zk0WQSg?rzX32!d4@=`z^DGI5$b;P0gEEvERky{#=x$pp+3StAqUb6CVPGvxqi(tJd zT){?^uf)6Tgc**`&eU07>-KCHc;$})_+Ku^>Nc_?A1z}YzI^NiP)GrzW}x$7OLgSe zu}OEfO4~+b=u>%W&)(RwyXZpSr+oZ~pFCB!vj~TM6gZoFQI5f}(sI}&6a7XGm;9+D zSV}DN^T`(Pm+A0OTuJ@sIVt7Iy$@tZy8hg?^P*ss7CV?DK|Taax_?8@a^ z!FvXs>EQ{2-q@z&5%%s-5CHE*R7-ry#-y(4D(p!VN`A*tb@WB$c?KGVP+0olM;Q)> zD0l*-isPDHMIQtAztvFU(_s5372&M`KgzmMSC2lGG6<^{use=-Og}YpU7l+sm>9p30znNk*hT!m73i7UHm)h z%q?_U7w6?e^sl4eztmUh^wc(jz&4GbILSv*&d>>Olun*!gCZ>|jgwdurFrRwLJR;b z7o$LtaJ8CPU3-hFf&P$x>u^oAnTc9ml2z%|W!M`ZDAt7WrOW}XYZ z*KApHJeR60nyjg-`M0E}u%zGGcJJT>4ZEQ&pX?y;%+-9G;ByTcoE1mLX!IbCf|^Ve zAl4*|fwD}S`%Hf7QxxZ=V$g37>#hiVz*R#b5KbbgrlqUERfMXHl_~{nIXzMN>y|EG z^VnmlUY}i3{vt$G;ervThw2<0NTdLydsGC4p%4B1_xjRRmKG+@0=JNBAj8l@bIyaq za)f9U&RSM)c*U7g>PKAm5CCJP$y8;Yh^`RFHSwq{oWdOTr~30AQptQs`T3^pZ0uP% zJXEx;0}Q7>EzcVY7S3nr_@LHbVk2y{x_TWS8}m~~R1pgXPgU7W_1`&l#>SR4a%2!q z8v_tJ86vBTS~jKn&S)T&ikRZ4^64$T#3t{H8=@XdT$A)ZlARa6GBvrN5+HMwW^vP^j5rKV$JO^XReHdO}WS= z)VLvK!X3)4RmNGmN~4xg;^cdMOP&Z+mrV_|6HyJ^d2nJkuk47MHm!A$?VV{heVJFC z%}6*jd9E~OXTH%QL6@q^8G_;*<@$EbzJ*=-@XbS9(}sdP;TcZJNPw4_7HeBFz=N-H zwpi$TYl&xec_5;Tuee->%+E*7bS%UcnubeBP)P+ylR(~ZlM+0ecTweof<{xfsibAT zta=6C9m?QD-xBwCPQhL_1c<+(;~b&M$eH0b4c5Ep(R`D z!(#9$O2QmvfqS&fMm8Px0Wpk3=~_(53tX!3loY>3fwC1TgN%d3o0?FOH`wCPv0ecK zANr_e_$&Rlbl$U7r8x0FeC9QDoja&vXIOXpbMu6cHTFW5hq;FMouFMPm~f~_r?Eg( z;(HT2o!4zEuiK5y#D=q_ot`cA!fMZqGc5!fE!TkKO_0XVDOSnTkOoG0{Z6|bR3$R^ zyN!Zm5w3cM4$wSveKBUB%jFdBG1p?HOnXC<3J7=v)u}oWMtX7?^wxC4*umyWpKI_s z(`KPxFJ+dlU)~dzIOva1Dh;JJQC13}|N85H_*cL@1*%IsCD%f&2eH=NeCmj$u`oV@ z-#H!AoIISNd)pxov)MoGL#<`lW* zhC8xnZeFz!jeoOsTMkEFQpWLWv{Az^I623hDJB<{MJ%y0YsgsZOIV*@eRym?y|^L< zg0}3;yQxxYme0k^a>}rbf3nAL?iJnJGh(>DH2V4NH9v;)a@C2$`=`bxNnO z#qs&(XMxC)onH)=gaFiR)ZyR+0)dy(*j;Kv zF!ccHyha5WS5WJF_7ot{JIApU)?fIa|)A((s#635oii`BD~}RA{IFu^LFq z6jLvCOAF$%-bi$cUdZ}HFt4l(N?U1#UHJ0z)+sWQ72|WQQE9u41EZ~j=!i;3lesK_ zH7MmuxK4Or>Q1oNN1;n;$i>2OGBr68bVwgA+BSPIvs)VgvU_#w?%^alNpO=h8CGA~ zY@zuO1J`Sd!a{x=LGmdc!g%58$(%K9*1g^*FwPDTZ#vZ=rn5{LA~zFDk8GX^=w!ss z=D8%?^Q@}({ozMpHnfnxacM3bzzm}9PG2;Fp|#+AKr*3q2>|!77{>Uof%K$!yIm^|Xt*-6Dbw(Gi zS1)&=v;AFzdbz@sR$ZV~Bk1vK`mM3i@LE=`YLMz3mD|U`l`RNhM3O}FYJz`H5zLu9 zg)_~>wz00d99wI&Pz3~b0_~KdD2M@s?qhU*!RKv)^E>I)3@;{ep;Hn|$QHmK-lwVz z*4YzJKW#_Y8x0=Li>XY&iZX2Y+_e;WjZ=rrl# z%%LNenXEeUpw!}9yDq*Ul~EXTWK&9*rgZS$$rw(Y<_t1pvBWB(IEYy|Rcxt1;Y~`D zkImA{S6wg;>SjTJv&O;E13%L+-AU98xO(+p!nLQ7M-^G$MdTjkh zc0q4l|KTX8(|abiKqGTGK10ql6V*;4Bdm%`I^-ef#dUX6tRidTF^Cl!Rdo0ao%~%2 zE0yI&>>R`@EJuNpm`H2z@pY-gHu%3#?W)y6>Dz2cIAu7^?G*QceS@T!0-NT<2v173 z?8yftqu!a)DnkGi_&|1E%?s94E)%F8;zWGokQ9)XJ>6Z}m92*LIkSsc{_OQz8araU zo7r%))5aT5&6O8Q?{+OI#nf>yS&Gu0g0q>ysRr_{B(hL?hezGJrE}Gse60&|UZWo! zelwz)tr$*%kxD5q)3=yY^F#R~97|)2KChQIdO_nb{D3xtmsK}9UAuy8;B|oO1ChI3 zV1N(h7P84(`1kkGWxg|++Qq=JhM@YftsT3IP4vNJa{hRt^9NP^hwszE1-!JrWdj^Q zZx2Qn0?K!bLg$|?w8SoaUlsfU@MM9DcmX7@Ftrtwd@Xy*}@=g-AF464IS3dwRAj`mn2dv2kK`!1h~ zHZ&olBkV8#VGp8ZXh1}(i4tF93Ova2CjCR3^^cJNjf44?cXVB#!#_)@F1(laW!TNa z2rcky%RJ}-M&a{Es`h-jg(ELqk8;7X`uQpm@E|CnG^&a=_9&elIVE*EEu?pVlK|Oj z?zC*O7g(#82vFn;E^`N@nUto%Acl)xqL~OTT&P7h)mU2*HEfQudV*p&BKXyf#yaJYSs*ZsLO4>;yx4HW( zu&o^hL08IUYsNK~P=gOk$Ks^%A&MVF@tZUX<#wV}n=(rYI(?KLqU_fgDe?aCiL~g> zgG&)`-O_&Q)-C(9KmKF(6IZWyQ8757@u|@+$})nMadq0avkTl@)sF9<9KS z3xP7a@OM%8RhExsmHtxa*<1BvR#sjiX6M1Q`gP&&%J{Lst6*dozAjuVxA#Iqp`ih? z40Gx<1&VwsMbc6Dx6QRR%R6g!GM!<)O_4D)MPrDntmtA!SY1+hZ}-V|QhE}fUzhHB zSjP;gWFcSy*qwtj;LVwMiiynC<6xgzxF81uc)>BwNb|c?*x=l;8w(`1O(kr0RA(lQ zyCMiCXzGf3e2J6d^P&tS`breQR}rBCNl8h8#oW0qaf~hv(sW*GSilLfu50d8$L=!e zVvV$>axyhn(UA%@AFBx6X^02s>wtk`LYG5vhilqLMeO8j?zxP{_vT7J@=7jUSi@f< zRrAyV2+z`Wc`UopB%PlnP-e6wkF#tq7lHU4vbQo!rd1jRQ7C&E z6sepe**#6u6uQ1dsr^%}(#l?4uhq zDZ<>??xN!}vj@YuUESGGD!kL`Nm8Vc?nQD*vpUC!1r7MR&g*Ku43I|0V*ob1DaKwD-R%1IQ`0~`g3&QvlJ`J zfbR$Hs@(Zj;WYJ_Ql4jYb|G+9C1>RC>ep19mu1n#fD?XiuFub+w^uC|71l+YV2;NN4!FRALNC;I=)Mpqj0$L0227ioT_V&l|5uxymdQ z1WTQ7Y=qGS>jWA1_Rf~poSG;s%d>#fQ4yO=18%mtm(@Is7bvA#2-z=hO=NFMVS{qn zQ1uc8D^}epN>SnW#57YiL^6jwlvx&%`_e>4?4xP?1WXDt$*?674~fHCQm`ugV3vq& z;mY9r#lTIxu|q3RqJ61rZE`kNx}HU}>a6lzmdM08co5gxq8IV~=*6+9O|V9XvP&gv zDPJURD5ap!j7dFS`J8a{j*UoKEiT4=%f=^GPERR9;C#?Hc+nK+v7=glKIh`=i;RKF zsgNY+>fC2ylNVmHR~27TI<m_h=six zDLrN4)Flk^fxNC%wR#ddWETtn(iK`Ya1y)1u@)rI0~Kd8#Y(jRzB3M>?J( zaI`3$51{+#bY>%fznAu|AzQ@4qsj*sIb-PF*0$X{Jl2C=P%#%x5WNp)TGN_vJB{o` zehP>ln!45@PdrT%sC;mxTa^r}IOZpfPr{UbVp|+KO*G^QQu48%#!e6qyDdDNH~T|7 z0jT@J=9YEvcVpfbT-*0;ZRvFW`8U69zxSDE>?2#72!bO$?CnO=KC-)O565GB7eTEk zeVj~2O=+TgwGihkKCkLSoYVa7g+jO#N*)VVU?An(GleIr52?&8%T~`oU2o|n`mFY9 z?vI;LSX{0QA}_+h;owPN;PV9b_Tq|~!2nbE0wr!*1mItbUDxc-;v)LQFCLb}x0;cc zi$xe0T<}_%^-CnA07#AOuhp}oNrA2*HWJO3!aa)YJg9%SukV=LDMQwDp>$Sr!Qz$q z74>4W*dYQo;F!I+qJQqqgwh30L-D78GLf%sAoCQinmw-VTDM^1rm((}7+*)V#67#Y zcH>5>Oh~9?>hr8c*z9Z$k`ki(!IGUyE>uB^+#tz|o!DigBxC0|nSw*0h%lh!qL8H6 zPp(6#nPQF3=@u15*w-HT<*KMEuci?@&Ocl1jVtyN=1XR~g$%w(E^5oV z03$E&tlRNqDc%)3$k(>kBri1_w`_ZJ-S+!uw%+YnHW*OpUYp=#Ft*$G?xB;rfs0R+ zEl{q?q9#s)nZyy<7Y zb!|@p>-xhlzGy#s{f4Bw-a0+DKmFye*f)-kJs@0IA{NTJbv)?LUb$g^@c9?)rJWrq z82{~e-?snywO8$I>80LovuQu~^t1N+pMBak(S$ld2lOxD-~ZkR@7w9pP3wR3+7_7d&7l8G9 zXSag|^CQ=;*}size{OpV2VI81|EB%_-hRg(0m@i)=A!e!ZRS}MCHX0`q36H@0C$+~ zdOFbXx^-Z3BV-K&lqoR|$67>5^`=v&!t)fS6c%#1D%H=ws(?^svEC>vR}F=WC3__e zr%k2QFjgSQa|<3Qp5ZQZ_}4sykhm{N{z!)qwH!~!gnq8tM|%^|ebCkxL4OgtXVq&} zoo)IeYp$At1r$Aq9qf-{R|H~!7KVzZP%3FFFT3L~!!AME$UE*VE0aJO;%8=qu`UlE zlQYgcjWayOltCB!R=?RuX zUE$9qzR+c^h`nVstrEL;4W~+^aLx`omB8`76Kv|)G}qKvAMzE12vB+5#e>EcgU zL_s~2-+1^Cf$mAWy1ro#4v(ae@&i||+wDQ$zIkwDcaU;_-`*wL!QUSuP`!i_YY*k! zKe>BH_x0J{rv2AH^HazsHxcws>;vEuA45?4yTAAM*?;g)|Ec|r+wX;ke*Q%!{LelA zwEdrd_=jzOI<~jY2DXmw>3{O+@3*JcH|;<9jbGOV{+S>75&PLEpR%tV9@_g@q*oDq z|J;xLn0@Tk*X&Px`3vZ>F75e^4f`VqazA|KiZ=A!(ZF6pX8T|M*pJ)ae&=2L#3gJB z1jS7pH2=Z(eFA}gX@BBNUqp78*dKZE1^eIrw(qxNoc_1_BWoe3{U3ku2krYWU$Ot_ zSAIpGcXizt;6!#kshu}h`R1iF^&iU)vLlXo0slLlr8dF=n4qj|a zUa1pUnD94d9P9)a^bT%N0UC)yx^xNpg@gM=&AY$Ms%1TqN)=E+kmWe_`{t5>O2ud( zeinxY7OiKODsxGKc2VJa;_h{#45jW6=ySd(&3Uh|*7uJl3J?u6Zm1#2E^HPQ*sBdM z(GMgt*LGWWb+>09zqMmS1oimpwndk=?7^M;cK^=I-h6$+scl>@kVZgMv$IDwIJ}2$ z?=WO*?wwr=x4V$!e(DXw$TPt6^{kMj=jYj%XSAa6wpbVck|u*&;t6MN8>Mmnql<%D{!PDGGXgep8&P;hwGKIplV}r-c)IF=*U9k0AC% zWQEV5^t}fB?iauHjz*b&<=$QULod8ww*bZtfpfmJy`%g0m7^n--~Y}_FWQs6HT%CH z!}|Xt=nQcmpFrmOSI|`X_h0#_{rZCkp||011g~kY)3E>LE3Y819Ez3^?YLWMn{w^E-}si^8hZ&!1XG_28vf0;Kk=z z`Pp22XhG)AucmMqr3jr?&H%Gqt@AJ?PtKfv!gZElT`o8&f*)oX&$)MrKtv2?rKI8* z$Wvr&Lu|$w8iDC@Oe=6_t>o2O;~H~*qB7XRm6(J>hXa=ZRvLm)qTsqxSt`qrnUbSh zxJ_gdf|>AeKMeR0Yhf07G@aYu|K+dPdarJmF7Mfs*LP4stx2Smg9H-G4dSgNapw!y zYNXdmw;s!EXluP=TQA?TA=Ybz&+ zd9h~BgR)Cl)1=}QL3zl^bKLnNQ$jSDPy(~LNdZ*;;SOm5sXb=lI`$8~tAwPtEBKl@}DV4v%Q7Xv;R z7e%cj1+Js~JvvQ9=A<$Ec$Nw!Jv%wJ+jmKRy@ZF+)FNjad<|J>g8(mYECaJ@iG@|e z;u3KnmkX^umgsSy6I77sDk2B& z;D3Zp@u$&2J_IiK3WDJ0?%o5|GLi4d&pr3Ny@m{sLGj8)&#snE;QZUj0RPt?|CoK^ z$|ZXb8D_K^H;U{tmoM7}GL`>50^mPJkYpx%3UK#7{Iy@P9uA=cy!K!GsUNr3@tS}7 z3!k?Smj2mT#{Q`<{HFcCe&%QFfAY*z_PIMB2$MWUN%mJ>f7QN%pvsT-Yxq2U0OY@R z|AGCjJ9m{i^%3m8h4TDKWW2;bKa0<^iH-kfzVj{fG8bH=nehd-hrT z3*Y#LZ%oN~c-Kx%nlO8Q226B=8>Q31uQls00d?#@1Y|PPC?x}>L^UI~t8|w`FH1eZ z6`_Ze2O@42>746WUdRqt&7gC1L?e1dfI#?csn5y`Ep64sSEFPWXUVB>Lt`m&7wL<` zpJmoX$*u^0*8~)E=VW4MZz4dnd&raaESa83aeNZV3`ElQK7ND z=QZGdCoZN!A(FK2K_yJCRk1+fLN1c->)~_TRbcb?SQI{cT!hkBCSHY8e5%7Kr3^mS z1F>&fFKEgKne}8sXGjLWi5s|czeYYa;+b43%3c~f#>k6HG*4)&VCZ096xL6)QqI>p z2>~)Cv%R7f2%HtaDV!XUgXE<|D{Cr;bIA?P&3#Q1fxfW}q>)4iDXJkB*7Ot)deN|J zn~`nRapI$MH6mr1BSipQsr*~8bBl+_Q@MxJ=+hs2(SG&Uzk~(Qm2CYwO20KMz;C^O zq>aUWLDDFT^7U>PF?DRsPD?n$0Hwq`cMffLr)?YPP}iB};scT!)N8pEf0hRw3kLe4 zkV_lQkj{^W51kS;Eytq%CW6gR-+aQ>@StAV*+$oU2<*h2&E7pbvpXnpKf1kbUqirp z9+~2w`TAG&T0MM!8|B|;fvbIT?{a8@IL6b!=V{~Tui(VKT^>EQf%outbdx(`$63{+ zpRPkZs}afo2A(UOu6T%ptfCT_?!zH)%wyc!tDN9TeDl*1bHLA82BnqU4Uy(RR4}pFi+4`USp&xPp-n>)G7vy~)3=jp`rHjPuid@b>Fhry5#tk$P)_Zn$ zh((Rks)_PZUM8`h-m+;5+(wN?l@7ulgcfP|74QWYP zHG_yd)6)Yv3^VZF$?B**C4gO~(OWZ_~ z{LEU3%N2q1RsY;Skdu3uTV-R%wA+3Z=TS<`EG)0Pd+ zuy-^1{0@-~-bUkhq^wsY<}wO;0v5uFh0H79@64__=MngzQGh0x{c_nRnK$3~b6(2( zv*tafntQELcjvpv-)9QgjgZ)oe$rTuu&djYwF;vyfX-pFZK6m*&wK))Zqfb zpKqgOypPwq*(mK6POY78(*}9XrfPpRgeK3i5VL4%^L3QD>rMNeKl(}FeLZ{ajaQW& zE{cXao>wnj!Ry92k#Fk+pAIJ?a?gbQ~iG)+-`Rw4$Gc za(S7bzkn_{w+fxHDi~C?ec|c9COE2HD8RW2Iu1^20bQWo`?-oBIcEm>n%y3bt)iz| zpxg`=!IO5Skyu@?cipL7HwJ!_;<=noX7WMq)liC&%0R3+DlhQ+)UB`zsIC}6)uANl zVwLutpH*?l2BQn7S4E^QLtra{nhbF?AF)Kkr`giIcl*{{o|q92yjKvt|Q@4uABySmv%?T@wo-_YxiN zJ1KmkNaJ1HXQj(Q@fz7JmF{17j=5-E;jx#l$(aXk9lQ2Tb!5E~6W@@}v+m8mDXXwY zBfGahuy4MeqB+?_Ane!^H@A=}?pPNvNQ>9KJhNl$m15EdM%!?bdg7QFn==%Hh%0aE z_?)xpP(ZE`TWODms)D_C`q%X!_!&|8+m{Xw>?hE5{Yi9xUq`^ai&C8rq>l$nicKFW{oj7$+jeI< zTVY#IB3s?XLSUohTC)*0um^w#2s*^Ao}`%&gCzeRtL)j0b^BwV_?Z1SzyaSrJ3)qd z-7dF*u?_p)2*US0g>v>5vgBXAdsiH}GQLpiYe7~-ZRaSiI^ywpbm3>dgT@aR-Zv2N zfBv1@nmOeQb?|*JZ0{n#%|d7{6~Lo_jPnp}kvejEYDcIP50R~|;{o;Rw4%i3BCec- z>^5>TuHde?Jnw^4@jYO*MNvN17^(6+KeVF|7%v6z|qP8K9ZLs>*i>w1%EQD+w1rsm)9b@ zOb-ptLlQ@v*F>Uj0v}@Fm<&cfdLO`RE{2z1cv?3o=WH?-6`j3Wt**?N2-LQKy~&{- zokn(+89ZqwqX)rKj+`|#Gl;EmzL_bvw8kPQsruwyxOvrHx&Xl6!A3qnW=V<(1tH!% zJ#m6<;pOEU=+1K5=0|Vdu)p)}dv>~XtfP^T{KYsJ{O*DDf-6q+X(-ls*Hl5xz1{7uz zn|qqxVFtKB=}3QQmh71juGk?Ly>A%d3St=NYQ=tSu`seQq8G07;ykg1fEj$`&of>A zv8c?9Ocg~_j%BV#87E70i6d1tY|Kpgng}RVJ7)PuLPUu*_U&)JZLQ4>teLsJ`sLez zNK-X4@>wQT7HV)(mW_g`(*-?7GR?-PjZ}7|QE=j87984@+J-rg6XUUWy2v zIj+edtz{WI&8u{7Cp)t-F|s5Hc&X2?>p+RsnhN`{bSsHOtsg6c`ZWEmEeJH6&Q5Ix ze0UuhVWZY^9w>bZiiJ~40vw^cnFsEn1WFUnvY5#x-jJxPa&(^$?hie$sL-D^b%N`e zVE+!WmqxRRy5v%V4#Aw2TE!QPR~_BRF7`E6MR|FSPnOkEF&~+$;$(Dj95z%kD-L7i zkc@fKR+SC@=G>4RI_7whQ|qQ57x-p;l%{5SJAuCGaoC(g%G5}uXRD$8vL z4GzF__TI8-%eB&Wfn{u??3g6<{wzeaZczRnnc%E+?2p6lycsRv6LTz1nsk33Ha-K` zcrn3dCCk1EkbTq2CUTW{Uv6NH6akejbmo-ANM-}0|Eo3{hq)y%xzxDav~c(O3h%nG ztIWwjg&iTleCg3c`*#sw{szk0!|~+eQ?YmYLwmb_2HbBCs6IR{n2a{hB?) z4DM5(vOoONN9+U{)K%bl{Q57DQNDu=_-eCheqIR#vPUQ%|IF9EYX7Ite8&F4@BjVi z68CKr_{lah#=r9V>-PCa`^p@C{;jv{-+uW;bcvt9&rEFxr+62Q6LJuL_5OV=$SCl+ zs!t}Yt*ioW=-7GaagUbpx!yz*<&UFt{KtRb2kdYDp5KFp*F^S~J4ln*wf;+J67hsO zuhyNO%j|59GMG~!Z9JD+c_@h~VMKUt%MkRk!h+1Y&y34|$^yV1`=;kV`erLufzLu? zEdWDi7Xo=T*KJ|RS2MQ$fqi;pv$tuj^@gZGX${v|_Z<@-Ege z4({4t__Oa=jzi|*gOPQBsWHfp1`FWexpkzV9_v8ptf2vcQn;WiT^5UC!{%EJdxy$2 z#AmB!5IK~~!pljo9wW{7x;vvEDp}$IozI4i6iqsq&(vp;KIY12ByoQMk=8}n zwpAdHs`D{q1=UL=AA9lT|E%KC2pc5lCQbsKq&O!smTp|a`ABhy1f(w{WSXx|?O5K! z%t2+yqqe*$;}h;-{`cd|%RUWrDDNh43-`SDN0E!YircXdqWw&dq@j=&#>mhhi zz@dhdtC`fi)WLNfosF=78@2;%jvzI&#fK-S^4#6s+E#Wtz(bkfXKC$sgsgFd>o`WH zwg`dfyMEz&j&L~8@8Q3F_Z|BdUNc|)n-84#@w$h2T@sql=F1RP3}y7({@K0zXjqIj zt-;Oz8p@A9|CO)XUw!Ls1z|2|I*EP#@BsgIB5dVVl;Gb&7RW$OuGsz2$o{KueAE8? zH@_k32}Am?<8|LbhSs4FChq@xfDZoD7rtPB?d`X8kcdhka&2nAjLeZ3pPmsG?m7Z0 zv&DD&eXEX7&SM{-L?La5oWfU+5A7@a2jUEJA)Vs${la_i+MoK;7wuI5@2ioQ5Qw4> z;976JYiqc-%!qH)W}@@BfT*(rNM$Yad?kcgDXQd^;ASP}8*pk(!sL$aF{{hNzn2x> zCmq2sAr#hPQGHKPKN1xN8~yUtJzL-EU}NH5;F+i#=FEAm2t)rlnsjxoGli2lvz^_< zPKUrj(cRv=(!qfo*{r{`HJWopVXDJif=i>GR!YQ|2QEJ~8+6PBB_5I!{9h;_$j+Ak z9y{H|Cxpt?eyiXBW@owltMkw_l1P{oP^5+(oYll7R4`233pjE(Cl8t8s(Or_h+PYl zIj7^L?X7nKMWt#qcH4B1Y$Ic)YI{d^m;7%V&9W9C9CmUQ{7TPGS<+6G;SJ>od5SgF z2ShU*jwD#Xa+kr6c&mxJ;-iYIj4uROPyIp=^e}FfYEbp>(S=N}f>NT3Do!CgjDv$> zRV}DAj-nu7VXFf}vF{K3uHRk8kxNpkov&5t4^Qc3?1SAJon|>q-G7v>8vS8}<_C*& zd4;H(2HZ_D;%H9rBq$a`tK(;Q(vTa-(o}=-HFmZZ_S{yYM18FDuBMV{KF)27jXT5Q z+(iJ30p@ZPiCn7%7Z$BHOPBtqBs&A-0$r#9ZZ@%uUT00p#T<_R^4s6GO$5X1m#+hl zn%X=2$Myih=EYlA?Ty=y>;VGuJHPk$+IVv-EWV4F;CY7O4FFd?v5=0ha&q%7tdxW+ z{4G2XjsK|l10bJrq9zh3&H?^12`8ZDNDIfws`rnbeZtb7-B^~UBYa*h?vR0d`ANG^ zxA0_E!fZo}zj%C6JT@C`p_>-U$FeGk!#cDJ$N)1*zIG>dO2iofuhQ)NQlr%Z^saku zFZ}vHvuBS7_I}y0AKzL6`%Fdmb&x$02aaR1u4>|!k!59=0Et(f)YUY>1yKur`7Cx+ z<*y0~u2$pkp?euV8%tP~!yFlGruOX1=<=g6FvnpN1AC=UiQ}e)7zoaV^>t4=RZJrz zG~8alGqbnvW~fGRAe}DcRq_KbY^u{M7@t8mcYh0sx>Z?d;Q?Qh_L-BfIonEOdo@K$ z-+SIyaFJoB5&J`^tzZ`~N~>p@lwPN}>tDQ5W^sB>1%HH2Xq*IL2IsiMH78#4wRtGN ze|Tyy-nfJ$r0(`CZ%#J&?&)kYvvYv|TB z`TT^CJWmnGsT!pVr>a#!vJ`EgaH(GrrYVb6C&N3nfU`wXkDy$4lAzH+Gi`d5kbH_0 zx14i~EwUj+!~3c2G-g;-z*F#ZO9s@s8;56k-NpwK>-5$vBNe7z7uSgwTc??}-jANH zE?Ly;w&Wd4-8yoX*u8t|>J>`@&x#+jjZ(SSLP^xxz|S1I!BlmFs~f(m@jNQ545!2B zjem14%(N0BD5G)}OfHtzz6hN*GQ`3eRRJl;hF9;odJDac-&@{G_F#{h!G%q4WrcSI zVFA3)kk2KQ;45*13lZ(2lvZ{9Wgb3HbRO}llC1i?`hPaJw9rF0IRt(k2Blv`CDKrR ziQ&iWr`Ew57-_*dh;QNh0%)$TZTL)4^>5{h0<${BFZ2e*O^t1(BWc0%vk0VMepj({ z=`|Y1%ha1V8oH&-@W>-28)1=6t(*GPlYDv=DBPY=R;6XBrbkNiZ@@#t$pD{$_+LYn zO43^3u$@@F>r+(Tg?4dYQn5@*D3M&viu@dgX{4!Cs+WRtyaVdvp5EE?QL`v8G<2%y z^UB)ONq8%2P)d$mEeW(B)AehxL6CY9rd7;eTLIRyb^e>%WkfcfPW8Z;EfTMrQ!8E{5?P!SBMq zMbUZitUA!upEV927Py$mrE3iMG;N`iI|^107D{dYF4g>ESv5}_tS-XzNRqfWi1f!o zR26+tOwL>*b%{u&WdL0>Cz)~@L?+X7b;BAdXMUFwY?TB)(;1XqEICG*OXsj_b#oUu zWT#`rcqDoJ34ShS*)pFh!#pOOg3PdnQm%(zWRx>dQ)Wmr#ZtMwe?*EfF-*1vRqKfWRix{H4!%x&&;y~W&i>}{l9Zr zOhI2j0YfSxc9A(Q6i@`}BS041`~_7_d?uQ90&(WbM@jB$fiPfJN#f^;GM+YlJu>$s zk!V8@i>Mk4)qR%ECj}W|3v;mXD#J0Wx~|n6b+kG|S5r~u8MYt?d`JGEf6-?MG_qHlnxgl2M1nKHH45O=hc9n;A$P`y=t-5A@ z59iCd&f;_KXZ5_R|9&Sn`1n^DDvJOTt{QA{RuQJlko`Gq!#E@r84H-oO5#RpB(7ef zhY?;}bDF91uc--`2i{_Z(eMoyXrM$0JQ7=oyfei-Yp(JwG+A(w9hTL=2@Cb_)JZ< zkBLap>{?NRBFbwvoBKop8Scijg9YM1EEq|Z5yb1=ZMIS|voh@EEhJP;t`8ME1D?eL}LzbGR@Vr?UCi1ta zR)ASHc6Y6&K!AqYJeKsAXkejPBPT+Y=5;OfHrCh@4TR+^cJ-Ge&`y-B5leTnvS;^k zkZCHVd_qx>j4o-!!Vx&jPUPKLCX5TecafCPeO@@ZFzUs!JWjkBw5k@&o0t{|*2`=W zNW|`;!{!U|$s)*eQQdv1%x>-(bm@(RM4Fll4-UMi47LoaCNa*Svh5_}D*L~v0T>76 zm;hbJ9>{d{p%J^%q70|0#_q~#lcP zgDm4K^%#xxl|IoTxwAB22sP?}>2iZQOnH9cGSu|-;-fi@sT%@x-#RPpgTdG?ujS&-MI>DVe`q1oYo)pE z5t}TiP!rk7pk}?bj^vCAwD>!Xn!1<77Ds6+MGS#F)dX0Zt+(sKIo8pI-r9)m z7}rz}iXWw2%m)8e`ubki6a5Q(|8E%;|HZy81kQ8Q_Q;98Y-lWVm7v;8v1dU1iWLl< zXkQU%*@ca&4IcU5Ram7cF8<;HqqJ29=%19Yk~j%DNKI8G+lrg6O^$Vo0Nf%>!y}o~ zM_3CnW!K{^S0TZg<$u$q1OBt%eZd*oD|?&v)D}erVtaZ#vn!WcwzXT6??_xyNTDuF zxSEz=fYg9W=4F_yBXge9AkTDkO@yfkqqx}+xX!erNJuKeM5 zA=lqY5}z}a%+6vLKb009HvQN5P-YXIeLd+ zHk#X&UIR^n+y=-t4`+!DhBT}JaLp_arS-g=i?DjRmue1^{<6hl1W+G=k7^vXhMG0i z{BpvRKyo|P?poD<{|W}ef5pGN>eqT;7m%eX&67A&Tecil(a0rzt(E=N%t^iEz14h% zG+A|OJUa}(G7J3G(Cw%;Qdwb&QN?d9SLi}5brU4wbEGO!MdsUgAK4a4zpbsNyo8%g ztmm#t5r$Ii#sgBMTx4+gRae61GsT_psoi>VQ<{A9Vj&JJ%UP1YrB)OS)5}1T&O?^E zT>9{Vlx64(LP9pzHnKO4+hr1DHR0_`8H0trQgy>u8@)KE6czcZyIF*c+@Y*u6(HE; z@C>c$ka~cN14gYNNb{$1hI>`lYslR%3I~fCLyp>cCT8LOSE$>?z*8MdZgEkB46vds zRSl2mLTk5b+IU9i$zau7SuLsWQUZUka=5yNYB5&#LeD0w4~i}^Bc3 zhPuAEDeU?#S=L%rqT~UxszzkA%oP+X_18Gf9aoEeL?}aMrdpdFWV%_A3J}biB!^vV zTZHDsVm1ixh482A&(+l(10#8t^aYj?$O^9_;XGKh)KH-!%3h;Hv!rX2Np6n@H0f?g zHi}e;Zmp?#%xU3j*?g#Uy5jYZ=f^?e1b}k0?F)>?*oDtS`ngeo3jYIwVEH{TxPR%d zf6>?bAN&3PIq$S80jsJM>j8(&q8MXk&`6uqqtxDeaAa$(y6tp35-N2ghtS+1PP4+H z{Xnf`poBTu`E-KRi9^$X;ZYe6gGF4)WJNwLQAl1?L10O$1m3d`DCKl|YWIh^=q;Pw zu5GTfQP#9tg9h*T)ug<DzJvK~G5s*vo`c}s8u9r{->Wl@o zcU#4d>Z?jBJ*zKQ7_zd+6$BE#H_TyQ%=oG@_`_M^#cZ_gVdSjQg^0UW4&fo?@tRnDTDNnKXAwSiq!~D9YLnv>_|O1N05s` z%3E*L{Xvs3yI6#Amcr^dmT zceUW7XcbiDe;WwX`h8kugY?-5I232znRW(QrYQw;$wyXl6Ik%2wK7~4boG((Zk6R#%&eu;~o_F=5KW1JEype?Vc>z+q^YHoQx?>D;Q ziQw4;5S_PjdEQPD?8mdrCBAt8Oh)#}hws|Q+Z20EL~9tM+?nOXAyaEKeb$jdGK(dj zDP>)nO;-d|s_e2!I9b%}tp^9Tjk3M4h8>TF)&}M@p3U5cmb3(p22perSm+#oPn@QU zOhklPW*xZxCYnZ_R!dz~byo3em)2UgUZdJeq~?`yjl#iRzMikEn#3jEeQ;{GkNW`t zt`?aG_NOmxS)<|I@JNjbSLazxSLkP=6-nNAR~dkBS?t)o!!u;sx&8j%|06iW){&t{ z)G44edkOeZta=91QM&S1qF4*C!9v4Ay zj{xO|7JU`msYc^YY~-4{(1!w4LQB_3J9%4?FDOXyFcHufNp43`*K_N$frR%cCzFZLF@jrjB zFJDVxv@h{ayw=Dndn|((MOb^x1}l}?H~^IFU`f)t=HA2dQsqWu*LFqhOjdtZ*F~E( zQcz?7EJUiN7e&i8zv7un0yZRSI!)a2j|q34yL3>-?pWMHe00&(w3bQPmr5}%>s6Pg zAUj>Raa2w5Ym7iRnx@*+^<-gJ*O1vQM<~U(IoQpkW?gEn4jVZr&alWS7)kNfF0#nk zB3Cx?>BIX7b^!9p`$qS#-ol0+j_p#Tg}?V3*=w)k`;lEDsAb4LIbubVCJJAs(ZWs< zSRWk%Tz~7f{rE?3*xl3EzIksSL7)!soG)A#$-BF?A*^YN#kYU=p*7dL)+Bcf!C1kJ z0tD?2vgx+_xh!VlwSE5!PuNGVbYzfCqB#l7b&6R;PUDbSB<|5M%B;Wt=G&IN^**|w ziJN)TDu+rx_}))Ge*+DnjtWr^euM-jU*TDZOs~Q_8#9V z$C4Duol~dapdpZ0R2UwF4jUw1T}2ki;NBH=&2hH z7#J_|SUuo==+OHfzyEH$!c~?WIkm6)zFd?T|MyBti@0gCfdoV#tEB0Jw?pZ;iuC#; z$2OQT$%nWaOh*G^v6zd}pe=?|oy5wjvs?rr$r7aq7g`ee{3Mr1u9;qGcvX+lIHJej ziDQM6k&4X{evJHQENMycLnQ&%70Y)l)_b_dvwkjJKIUrUc~hP1I)1K|&XJvDv>VWZ zUPGYla6u!(I_}T3dDc5EZN@RW&;xuPPD7B=z-gD=wL(Tt1lXtf+(wf#TVp_<(QdS6 z8=bb@Kb_kH+=IzzpzQ4GX3wr~Z`q@f+53YbGKroIxj^c{WGC`WYJ@;HBYzTL{H3R! zRrY;$dSpd^V4G__l=KZdo6IF$K0|P6p^5a3+xx)PYG?u_xb7wbOp3sqSPM;uwN4G) zagvjf&14H1pSx*OEFp>{@!-K{L?z;OyQR#~b?O4$fk#qQZHCYoss>QG5OQyZ#q)*l z5qd@+zh#Z_Z9AB}V9O(P-0LHYzVQHf?jtKvHs-j-kY|QJt?5|2Me|BEBx+tqZhDu4M`n_-Vay1>K61a3u zWOVTsT9vhhQaMD*%$&kTb?ayaO?a7IW$5S4mh*s9p+4a+>>LVT;Vh43Z{Ll|awt_Xrs>4^k@Iu=1Zum~D- z3XG+v=p-1X6-O|iJKspM;V6k!-X;MRkG&ZY`w%l$fKwama+tzOY7uO)(u>qs`AX5#MlxfLELQ>3TvxRb)9!1nds7 zso%q+1-`$3 zc8V;er4Bg*B&EOJa3aO@e7FGCImA>M0Ks{tU{&(!0QVpe$XK#k`{~k)1$DiXXGjq zvLH8AZ`NHfk|k|U0%%K{BBzcZJH9k!s`3$(O;zInsOh z7H}a2K^YWhl|*~ff!Wze3LvDtP@0KHG(L`+Ts?{`P^&FW2Jj9~kwr}aLLafoQ%j_= zwu^<`MTQk4{cm*f-}=njc-`S}h!erFx+cXO@Dkm!j!jUrhX}rd(a5ec%UfHQ((PSb z?>k4h4jcwtNc_HM))RX$7}4H7@3pvT z>)3W@B*!vyL)MfsJLzp&8(7)^-RxrKa^g`wwT-n6VS}t+X}(?IEW-4xbm2^t>P5d) zR$d}Vab9>vEYFRN63^6DNX zdBa8w<*fp8sHv=6kz=c1=kx{(WnZ?+s&ZO%;xoK>9nm&w+j8E=;nG2qY=WwwZ&WZ$ zr*#_*k-fGUv{7aBr#5c`*UKls>k=KFEo6)dnm7#}*7d?B`Pe4K+-_{m?7hPo7c8!6 zVR1%m+yrarZp1+ur_8MyV=O|+C0-(>;z$*X9Q)&elyHwZCuSAQqS~O`36J-A!9~G%xO`sxHt z9pUpPb9-rL!;a=NyL&dUJ0~-FGC#T9vb)0*dw_EL?M7c75v>O0|3@Ny)6|L+T4R)G z9DHWkmh5LXuk&*t@YkCN&P{7GW9c+x_MJ1stEV9YsK$a^!>w9W+!*9|F(DT=i$L`} z<@`&@9V@_83cNhkW|cff83YKlnF8sybV`JGB2Ak-T1i($Dg*gUNgkd~XLkS5Bl~~+ zng6HtC=W(l3TZWGP@Tx`T)-}}ubuTRl=@gVI4p}r88~i1PtYp0qN>EHI*b(tXsZ&- z_gW|&@tH4g_3Xu4+qzyVA|H>>>{|!e;3p%Aptd)G2X?rYT6Q=}fhEUMRV$`kr#MQ9 zi7&%Q8%H|<*VI}QHJA!&3*sZRI)y#FgT#HwY=Zdm(yv*#siaT?r_T=u&Gv@tCN;bI zJQX?oJbQ^k;3!4O;T*o?AR;H7uQD8B+=A+=XA}G5!2P4Z2LH>_T{JZHQC!T_<*Z8W z3(9aYiK(|5*bk}p?kNsuDmhQ^9Lu$atOZ-#9!TbL{0r;L z;TQMMPG$X9RP8S3J-O{+x{}MGNThe+{NI;LFC}??@OxFh>ESpVJ(uWIMWZoce)2{Y z_P5jnEi|%%tSD{xh!Zy<&6H98 zUCt!O6&LQCOgvA+(RnVv5jMRS=txWxY9u4@pv*aDmWmSn1mEA}=b2Az3xTjr)tk~Y z+-=~02e_WU_3e?}T-&t0UV;*91Ls56?jBA=58B&Bphm!XbT-D#Mp=bmJ!9&E2eZtA z)KVC83b8~?{{kiI93^o)=b?b?1OIsRV5tXL#|hR&q~2I-3SMx3lmJykj{hX9Mdpi1 znG>@l&Us?@GBB&Lkr$fC@nz_zw)kI`hwfrfAE{1tl{cv`#A<>g59ciY)5HPv6F>Ax zG#|F?AO7kW?9F#Su)(bF3oJjg4g$EG-2`#qAVRR0s~R@aT%~yI8#Su%|I#^jA<)H9 zHG&md1@ZYN^v~+l5kA*jmphs_J)2GJ-u*kaKRU#rft?d5-Ixj2p6z%{iGgVRb~iSu|B9g;akSh@^j)xZNWq zv$BO_nn|Ftc-OJ?24d`=TiL9K33ojxJaES0xyEvE%vZrblLnznn6WF%!#>flft#1> zSr-293`&DBf_J;FfI$*=5&3Me$jV4a2BWb8+Y@L`<>f-MX)gHSMu=sS9 z7}1{MXr*qI22I22Nbx&d%}cvIsdv~-eF;&lBCj}kRdUw!g&$I1bBx2=@jjd_&QqYk z$oW05Izf4;sZGJcd0jPg&xED|B|S}Pzz$@H#DZV@0r4MCX_jbq1%pK^BJ0If-yQK|n!pWtp1-Z$s8^b(YYj&rlLeB(A8= zA5O?e3v`W30OmlwOO*bXnjL!{87_Ly_S#WmhiJYW_m}qXyx0=R{|H&#$hJa zPxDx&OdhNv&ld3Kjb2awwR3!KWgjUCaD@&rUL+@RoFu#u?zlkKOj=Q0$ZV_zgq~Lj zZfQ^>t_}$=!>ZZ!^U_hHWpZmPL`Ay1m8dc}lgv{CaKf%VwNdC`Z%>={=*?0xWCyJS z1nmubVZCMh^ReK8J-`xA?ycKLu58(*%RLdbsepa^;fejm*I&1zvwO%~aiEPm7Pscs zY2HT@?vk>*MOKs3S+2?`3g+weRasm~LD1GA3o=fcwdY*1zQGSi(R`HYEXCoI2yRi8 znx4tc5HC*8Mkw1`dL33K-k>ZczCc>+{iA)`*xW<~_%A&lZAa z=8|S(6;gKr13nkZw&m_|5(Nk%4}qx+DjH=7Eb5A1$PhiJFN3L->qX{O*;>6uD6uPS zkhG;*FjbF(-%6Ca8cvnn@c`Ez2>ZD=b=2G%%;F$ zB0J8;fp=J#--$v}@-EU@*t_@d+O^$n+uPVssi`U2sFKktSLQqCr*T+RVQ4&8BLkRM z?u%fV1T~MshXyJeNOw1@GRhF_C~3x^9S$h>nMgI4J;27kefN<)|J>8|>F@t-cK8o} z$;MM(bVbl{B*r_(VYG!zdmBw7BvLYO?*fw~Rf%MJ$Mk~`0H5hH3m0(Cky~{wql&xe zJw*O2U_1oC%vuO&PoPtN>)9LH1aloVHI?Plg2p{)fS|!eubIKaV|%I_Yj%A;nAkBg z*jJBF?e#ZfVY1V-Z|~l_V|R|&7)$LVx4KA-QoDP9WIy`-FWZeLZsOo>+Sk8&$KHSc z0Ba>zkiE3k4*2G|YKt~HnKpC3w>33SIFsx_lOuDp`BHq-)6>4odEwq=)%aNH{wMt2 z2mAYWb#GfnNz8sIqeYiFe%lg8o>On@U8q}_8{#?H&jwt#7-!54Mpx7m~C{9W4mfHNfD7W#WXNjk~ zE_TS_bC$yMl3CD9m?0bZbh3+p^Dt#zB5+j7u#R1o3JNr5(!*oc0St&1gbL{Ra7{LJ z>NT5=#gjpYxG9Rmn6lqI?Z_+3D96vTr6xS|-tkZ>A(v4eJ+;1RHI$VU!pxV*5P&H? zf$S$nCh^YwnSC2&*Hr%VXtH#B(%tKJIx@J8r4-CltrR1l*H@#}BeIX8Xd@^itA6;% z*67KL&&O^t%YN#(Wtufz+UCYOu(fW$W}@=|Vdcu&@Zwh#$|QC#+mUBlT(S#X138cl zJq=U0Uh5_Da-b}#MO+bObCvOYsx92VHm;lh<&1U{Yvo}-vVZieU$k$(^R~)ccB7m4 zcM{ww)ucufo@f88Z+mO0I|uZ3)_rNeGKnE`vt#J`LJo>@g=T~R=3s+~}V z&zJ_BbBb>6c!;hovb$vgT&u>Zk;aNFOgd2)2O3%7ZcSz|T&wa8H0|ljln% z=-qD3n=*hYuH;!t#+ z$%UYsNln~i1aUprL3B>BOaGO>|yKbsO_cnn6+>@!aD~5ZcxCOBli{Fc>?0mW!I-t zI8DN9WI5Mr>fUEzU_L~F5F{>?!ggK~dpFSMYO_#o*BAQ0a>k=6RTc`iKyC{Gwp=Y* z{vR{t4KzDAy+U#~X-0|U;DOUpK?v9P)(1xjK6ATy`HKC%fAdFeh)wnSYi}Z$oS>10 z5*3F})a$C7Dc>*=Pcw-P(3-T~>FIUm2>55Cp?3&N-yCJY${Z^U<7EC%fn`wWG#?|& z+LA^nFg^Sa4^o03akRkN#B*3g!ldVfUx2zU@R{qiH4)5NZ8wrNTSu^LuQ`Z5qbD%m z4~PB;X#=I<|Hd!BVMoYxADyDJJS8O4u(SR`!IqWC2tks8L^yX^SE;!WgoLIH$4t?9 z*xu>c4V2z|L^-qSU{1#7-w^LHI6FdV-%+;AwW+ycY+BN)mW!n@S{@qnnQo4cl?Ah< z!DPuMKf2&U%PC{W0FwA#k}`G7?&?BKFPS>w*C1rDt86S&1t4uI}04r>{aQaJSZ5hS!YPD6PmOlG^-X!r>Qz=}(42PlUPa*rS^d~J8 z$Aw2Ve@{`Ama8td2gyv;i70--P9U%_CH=R+vq>|}?fM1e#RYl-aSZd)n3M*Wj+fGa zUoTCVbfLVFx%*ip-T>ff=cTzj#CxW}6K1Zr=K`oyK;*0y5sGN6l#6qjWwT_<^uWtN z{>>|9zE_Y^xQR<=)JwmPX|DD%vPUwnRerNOT+{2)UxXzwSvFBK@zWNOmJV_R2-r+$ zqXWp-yWc-^*&W2Pt=5|E$=Pr$QCOBIDc+Z|T7hTe^>%E2<=bX7^Rnxt6GY??3?cK?`p*RZfT zEB(g7vCM@&{_K+~KgnrJBhS7ZBz6@N9$fTr-t9K65xWp4yNm+d^HO-~Ca}2YuJ-KU zbY|mOX(y-0_M5-?4ZCsevi;}}f7b5r-?eYQ_O7y$M7FH{{0T@Vx-SdecS2j7oo#eb z;^AUWhZB`)>U0NUzGKQxzOQbiTWC7WF`DB0iJOT&;soeS7-%ZRAZaO-S&)EhC@Unq zLZ(72$$9q$84nW=7`msKy0z=Iwskj6v_r-GI{8wgI zW9%nZZ*zSAf(^#l%Vqgx#OL;!n@tboQPBT$S=6P#fr9d2eBRg#T&AbEZIb0GHtm)jNl5ugVsyMo8r$5)&Q?oI+bQ-$JP!AQ>0R zh?JCY@8{u@ZWzAUz$pvF|Awb*ZT0PXw&yAFVRZIUhue(N3k+UsutFI}^Z4KyqO(zgdw>SKgbCU-!e zGqkanGU}Y-J!j+0Iw<>@T^!NsGj~(3ARpjTZMjxtnnjXN!Z8 zPl*jp0btKYkbSefOg%sm-;Clg9%gRjlxZJ$SC#=NpY*K+#>+CFzP#8ecJ%Pj-hcaj zG}Le%%}8!=oY3h{=GqgIqz?^)`C^XsWN5HZb!mi`PBlj^r5xS^`3ZB(GuIkVh{MJX zNihT0YQW(<$vj(HYt#emE!gpG~?EaUDSAcAglvVJuawT%$W>*$=lU<-uId#Ry=3J=O zLa?h6K9${(XNk@;j6ysplmHcF)nF+ej|XMBViZ)}>L`evsPR;|J+KQX6fRmp&BoAY zD1!j>QlBaKk5JmLD11{WHO(BDt~^EJz`rSi8iRN6a5AvVNKBBYM6Dibbe7LaC~H%t zn+hH@RF=m>Xrid8LHw>R%_G)g~sn#Evg+s zE2?2oNKu+su|z62*Qu49ceU^uLtu=@C@;DQe*BC5(}AvYw(t?cX1yZ}g(U|u&PJ3- zYfI*yW&`}^jzQ@I=GJQ<*9w{-oW1LkM%4$Q+C(oUekLm&JCS;bQT$-T zF0Z4rKN*SlN!tWIa~C!B*O#X=!DU=;8x4;xvcj8t9h*FH$&LXVJUp7nS&V_1<4LV% zYCcKimTv$D-9a|EgQSC_nC|g{4cLtB&i!M1 z@6M53Lb==^Q^1@Bz-PnG?eyNhMJIE+g2sppgkoR&n$yR{o{bN*D7xJ>w=vLWH?u0E zCWjWU)oLV8mZl&Iv*eWYA7n+qFJ~lcCrvf}h`Y@fPOU1#!Q;W}oN!!}tHm0AB`fBF zAjPr_lRV*oHZO!L`b(udSeTvjMHS5a-#zaO2VsZ@QGnS>$$$BLwU{ULIilZl=DsKt zSI!rHC@Gr^bMQ1cTPm4L_45=*;q+U|MN(G2rxwOo!T@~*gr1McxNk$mlsavi`-Y~v zX&k517#0^-o@rjSa(+@*cNu1j2~KDEYPedSBw-U1OXK$`olTvm$bgj56N1VuVIDRU z_#3$adN_d`04)W`$2GhjPH(^ArE7*ErRRxDQA>Ws!;}^Bv&r9cSuuY=3RH0#LbJ;NrBM{d(QGx-DY~5%mITy}50Q9S5rEr^ z>LJzXTZRm8^$fymvQrjjWJ1oaat%C+V4u9Ocb; z&Gjxy;Pnpf&&UeWTB4d{@wtYx6{(pBLn4s@sjnx6GH`w#DtmXE>vnx>7q45`ozpQ& zHy4=5!v~2f+_75m=5p{XJ!!&4W#mTF)rq*MeQq}3Nuj)|s3&ULAV8e-m-g$g9f}~{ zLFV4VwXJn#@~Cdr6K^Eo3Yran-IP=is`(TFz9`))lMoK=T2(|+l!6cm*7JB=pq!mb z}iy00)$bmfyN5Y2K9X+$IaD1z?*SwlIxI}kA6>U8?fnVVJ5pw{RH8f_{P4vD*VMPViaq_cbvTVyEXZ z7KCAoB=W?9Z^3)S*O?VQHN&YZWtx%mhU+?{B2lC$9msf=a!gLjyrrj#3~EwcwR7rB z9K1%@z+g}!L{Ry?Uf7Dt@GQ(y=Ao5dtG9h*h%kNOr4e;t2>7#nN7GK~A z(YU5OAAXLb^HV|hSvN1FL@ev#FoV1l_;FrVQi_Wm=RgRRqbxHV+{`gDkOskQlz==P zW`k23jQhY>0Ad%MGhNyc0BRc;*%-}`yHrD2?*>CD;3wox;^$aiw>h2@d`Dge0>n+Rh?g@x{V5;$5FkHDS38rnQO;cIrO zHL;_4;+lH+-^pNPH@CNSP}E~DJ$TJ)%$^7;6Nu;L;=vXt!Ko{%&T$cwQ)StuI=hC3 zM1hUE5baXAzYYny5B89GG-`3+k~xmcgq}DqH!A1AeFP?6f!wJ?RUJD`qKaHz9SjzH zA_8z9IU!k@DT3x1%J;Z%GO=J7d>hM3Du=BBXl>WnF~)($Za1=kwRR^+w{fUG4X{EX z=)_f$a|GFmJvcqHCBE_g@yJeq3y+3V-T$544YrAtg87X zJ8Nx`ndL|!d^PYS**C}M=ICC%(G(sjXe##3FxSwP-F57_rcXa4ejfxGkkpO;UD?~R z?Mu6Of0V_74tzqL5uWRvl%*%0xF!QA`W^F&;n?Ir`ZURckQMjO9Ko;I@r*O* zBKoYhH>|4p7iO}W-!9MTRb}A&>6sCFNqjP>;uBg3n%UgnlO5(L3$_5c3(u;Ir;=3S zgB-->X?9;m9vlTbkg*Yx3SMS|gBSIhp4nhL6T^ayU1g)nsPR2D=c2Zo4F%)jWGtLe zK`;>pGWSd>a&_q}pQ=Pn()2=`JkrBm1PjP?CGK#pqQTb+w@chU3+e88$2s#(Y1IMF z+-pKzsU&1X#CABE6vx5gK6dLz1jsmO&S|tSa*@djs1@adb7^^@GMa4sB~K&{&{_Ghtsrv-(k%16&p8=Gghis;g8@ju> zjZNlleG5TbU~k}#x$miGhst^Z51_KvTzP(^6ss&R9MJJObH7|=e;yEh*3*rw zn!LX_uo#$j*l;3$lK>&EiN-qQrgc(BPb0CXJ(D1=$?|j-yBfH^W+;ES7*&o=-7PRj zAcK%@r~)XU07aZ{t`W*axF{#@rY4le6b32$n#(}_V21p{lh=T=HZ_^h zu93f&*c2Jc64`nK*Kza8rfqC}+B$LO5E0ix>OrzZaqjYQ${fLltRQLZ=rRCI<*W|Q$4ftY{RbIIKUA@|+2 zz=J7D$56!Ki!QdY9gQPU&n}Z{d-LwDdvS(|hQhNjO-axROYh-B=JM(_m-QAYM?9M_ z_eYCQ9>!S~##~CBqBRB6QXQha%8TS1u<$M@muDFo5Rs3Cm3bwvqE24&*(?{^nBl5o%K9@-jAz!~%O=!@_sV?kt5oF$367D0ToG}BOh4MCb zqGl5T_!ODLMz4+2x2<@`)SVBMBA#c+8aQT0c%ck4y$XCqH4yF#tTMRKUGuf+190L* z+Gh+V|A2!9uQ+NjJr@y<&)L>;S20?x`mFX;)4jE5V2GB2gf zqeClk|6T@_wN_-WelQfRq(hB0tPA4T8p%Ydz1G{b-}9574!CSC>XULiPW^Csl214m#@WGo8sgzd6g^3fo;my@%}o>IgjS{8sO08S*}6{gTdqwpJq(9LYA18gwCkMZNBKwr zUrmqk2^YS0;;OtV6G2L%Hj(Q~Dhvl9uvz6zE@G@#lA^=S{d-x()Px&n6jd#PqHUy_ zn?XmylXR%Bg^WP-gVe$P!cTHyMq%7DblBoR1M!AR)ghMDP?QAOeh+qKF$+QqP65!# z!a?uIS3s#TvoIsbL}|GQK_zsm&BwQt3CSxp71HKBxk!f8JS~Nt@_RTq9`H!bycCXa z&Z$Ce)Sat~@Xa;6q@BQMjD=rVv(?abvRh7m0r4D`4}COL+Ii&J97Oy2059C=dU+B0<>8X^X#L0Gea5c92c+JUl%a zi6@y-ix`X-dnPBza( zn4gz*d;j2+TU(!HfP--`8E~@fzw7E+d*nEhV^(+1%+Acd_z;DYd}kT3Vc4(%|Nmd% zPc~o}KFB_uPNYcgV_yBLaz;c}@9HEFa587zz}(`D{e9v`7+j0Wx0^~Jxf8p-1r`l;Wa zWp=^6pzNMp+7ePTWJCy(sJw!mAo^ReP25ND5J96k7c~}F#P>1-Cc4^}UX(SwW}oG( zUV_D-nayfwAUnP^7gJ;hj zN>H3!=&E4D@&>PDHi)n1IWOCdqJ8Kkxj|L8LklTFb4-=J?F{`oZVmuLeDxx&wNxk7|g3nHc zXDg?TGM)Uy3MDw&<=8m09unuP%mE3sB5cAPXJYrQWfKR$IIxf2ukkcoY}*&wsAms! zutvf-(W}ifH-z9lxf_cjqi!wQAvb9%6Hrm(`1(&8tD$9_cT0V1q)UQLWQ16xcGLmmbE<~dCLKJSbYa3fYEg5J9;juc;m~cH}UNI_<{&y=bT+wI(!>hZ%f_O|^q?MeeQDY_1 zNOWc!T;`C*n==%IzR@4lV0W!}Oh04hE%nP}%?Z0h&}}=3%O`cH2<{fmP?`KeB8won z%7zOvL{$e0cE1ne2x* z_r5UC*7`!aZr0pPUJx=P05yuCJPF%O!6VgsYu$7_jAmYE6~xbBynqD^IPK{7YXol( z?-AMd=i_K!4`(&!>Oh|)^8;b276C1qBvx_JS3$1=m{uLK3+Yf5sIR$Wc}XLq#8%K1 zM^n%P4t2mX*WOsU^DgS8KX{qm^1dPnj3efb6y9yN3Yu^hfwK}n|3sG{QnO4!n|4`& zt5X@1>#?D~E8(zG?cwX1hw!j>EEnqvVFumM9Wts$(lC|oSKLb{&#WFFy~FR>{`&e> z$q20J*!{-MKu15bjKq#OIhOs?)4%|=qSJCmE-LoIk+A{vp&e(oRQk`%Zf0iXM4oM@ z&%X*cDZEbJ@0rbSI8#Rh-f>^Ze*`qOk|9gGH)f!177=Sc4;xk;MJHA52H;{qB z!ARgiqB5moO^k{uoNH;u0N(-+#D_CpZN<#|fSEpnDw#OKqnyUNj=!v!(=_HKVBumo zSRH5OtcRlS4fob8qU}uEI(2jF$UzHT9ydl1siB8Wjj$DAiz#N6Xgr}T!}S_*;JuI~ zBP+6+m&$-L4_+?d{|@24!N`syrrhCfe*2;fMDj zy0K^>Aqc9-Pb|;px#Ol}CW_2u%rrB-e!CYtADaL)hoTG+?0mW-<797*Tsj6||N6_} zWbYg}uh!^|z5MsOQNs8m+aCS7*l(=VaSzsL{LF#5`tK(Xgx}tIY4Ztg+2VL&`2CXy zy7!*(4XI~iqRSim@t0?V$MX9-oxC&=r1J2(Vk!LO{8A7yLGBz2u-$)?g0{;h@)tNLm+ z_?j3B_UdOMV!qCy`Kn9vCGK(DP+-CT_Rl}qfp;P;K+UwXO!pqrf3 zgrP+f0r?yTSp8;|BbL=nYm<|c=&Yo*^cXeeq-ZAEr zo#pg|5f-3^VQ-HfLFi%*>pG0w<h*o}Fl-11?N+llLo|S_CpHO;xnb!HvlawH z7<4>dph*|67=H(jFEU;cOUhEU@NilMhzK@+mA+7`*#tL8xxF4z4v#>@*R?PjQf0~< zfl|U}VS`D8(YC&hQA^9;nR#|W@yHAJP4A)NURvZ79jRF8FqgE*2AUMOjy%w~Z8&g< z{W9tZAuk$c?=o#y_Whk-rw6aG&mzD2XMB5eBk5mk8Zz9`h-qbeG-mN9g~=#w#Z5eU zK)ZwS1Ojkd!Qzov1IU;7CY0NtNJyE`(}0xx!6I0Ufg~wlz~q$Nr>r6OC0v$M95|B2 zDZ5Vd3ogbyp_o-`ECeMSni%Xf7H!UphnhE6-U0Uo_UIpQ>!0TU>W62B?{B=O2Im#h#wF_T9T%-xNE(=bQ#EAnzY>WGOxvh^e4!0O((1@DpTg2HB!ARAdN% zECw8+{^1S|6@Vu4=G{H))ltqlFM$-#8WPL~?m`|kaw9VJDqYc;nUX(jbR3Yt@me(! z@gQJ8j4mh+(rNli%(#i`qQW5y^AhAFtt-BU3plEeF|tWkHCepUo0~18^U+`T(_!Fe z+if)~8VX zf7`4@&l+aw(Qo|qRtaVqJ?*Wgq9g&GZ!~KUqYgbyfr#WWh_QN27c9wbU_iyht3Nbd!lm)bME(s9K zVI&SB+D*&*0n7_S#rr7*@GBBB0udI&C8szbW@c&h$qC>)dzJ+3qiZZiAuh<3Y{650 zMMbLlkEjg*o<|zhib`M`$%0S@+<1_?u-&=0_uB{iI?q&-?EKn3@bRE~r;dB%P|Van zd#Ci#7t-zaV)u?2!5Mn9O`^Nx9X|s}$9R{k9E}lt-O-ubl3YfxnSjk;Fje+0{Lw$& z&%Yl1o=R%Ka*}NHiFkE!3xv!-JK;tE=Hb5t$uz?I}gK zw3g0#gC=?Lp&Tf4C>T1_#|RI(&htdLC5Z9JKeUY4k?G5o#IV zoflAPvY?`MiN+vH;td-FSF0-qI%IWd;x)d$Paaqx`-9CFI-ppSt>2f;I@u>bM4@JC zK;ZYE$NOD-R@i3oRAJ6L1CIvX2}+ff_Mb9PY|!32+$-vML5~3=fkv-rhWgKG4@a2XxZeqiV8gcJON1wMMPoWk*1Cpf!y{=V<;Zye|7 zg3lGEl0+4ao%NXp0Gho#M@C2$$E=wxEO~LKrg_2XfyV$;N1*3ii6p|g9Has#3MrKZ zv>u29H*4uaCyj1<1iHq{n`tfj2ym$nUdH_K*|S`auz%~F;UDg|_9Y8YCLuKfgBo6H zJbiwAwuh%jZ|-z{&o-oaU_Wxd8oa_KfQ;Q-(rT5 zAwacs@Qn>@2~xao-rX?p_x*}N9PTkE@C|&;luxY~4hHU;v5|6pz4fwiO-c=EJ}?&v zr}0biJ?B9U2QWB{!*gB|gmvSG&ehfro*l`&Pmd?sCAZQ<^o z3-{8Q4Y$O7OF;t+h&@<8Px7T)1;lchfY-}$s#6zG6{AVj0-6%6#=t0Ov&h|KKoseo ztE&|D=EzJ_q7EfrO2jOjwR33c+ZepQtRK@e!P~EbGied27$Xpvnd)b8d2pICU@Y>j zCcyX1K#3P>JMsJ~_BWC3E$jVynzHdRO)n>|SvbCiNkd~%I`^-D?x0+9F0d?=pljs4 zYmjAZSS~5lmT)3aS9`E_V**gLad}num5Z zhDr2OC3-fD|w6`gO*WxzZ2oDyXoR{*IVwfP!H@^v7uSmRV%Mij~BW$UGEjC_c zg;~^?lA@uKqvhO$!orIL~4z3Fz-s|}|5mLcmu`7&iaI6kl76RH(LS&g0sFNoI8&p-{QUU@u8ZsORf92OdBqk1M zN+b_-^JStb@piW%y=K?pUY!IHwX5*n!|Li;V$TpkKrc(@_LKEV-Bb$t& zLh|pGC0ENJ8%_!|o|Ud$O;a{&r;(I3GEABZW9T5O1DcvJZPobE5?Nv#hLJEZd5UVn zQkcVFVxuf*X7c_RP-d+(3d+6XIR$vn*&k(Eq-ZJ|)IGH<08-svO*LZ%UUkQ2p)U-p zDF(DL7POpaT9{s9IZa`T%72?gj%4tw9u{*g<+*67R}31DN_90!XlDd|nQy%Fd9Z)~ z<0n6v*LLrlAH2Y=r`VXnHio%k*VL?ari2QoZG$nKP@BN2>7ufEfZ}2}+tHgD2#!=D zWB|OI;46kROq5xWpFyLz^|A%o;MlU}^$l_qy~Y|Yj;H67XIv{1dYR$Hv@KjG@I59? zjaoCp!Q}cqMPGjzaUdJ}`}bG&-TQm5G1m4mt5XP&x($aP zt(ZAZQH@??Rs>if3Ckf(i2AqSzSYV#1qamfLJ`5@=YSxPR6WB$2L2+!Q*))-ql-@} zj-S11T}W{-Gc&QsGD(H8fMAMbQ`|6NT~G2x;Jg;4k4}Ao@fwj?UrPso6c(*8<%);< z_kMgBsqt;bGj9tTIcNgB@$>Wxpp=uB%8ogwo#bNuWn_tY_H~S@f#7Gp*?acp2kmK; zMcs%@@GLGQf{4^A8_E%ZV`Hv1U<822t7hPYMG$`zN8kF|1#i)lk2S`R`I;POK?n$D zoT7FPH5bN5j7oXt#=FI1VsncukM@akwy9ADoy6DYGX~7#U^#)ZVkE5}Lmeh{k>hPy z^b#yEhKoEhm^UVzcq;O<#4VhXj6o2@(8z%JV4Z}kTj1`MuX!0nt-(Le+~p0ibDCO2 z4?qWo%v5qztm#!^z?Bq3;}^Yh@QWFQu{!HP$?H)j+r8*AHSnZn0Xj*>rQ9W<)1qbH zZ55#{2m3!x+rV3@!X!0vgJqi9trciYO_Lm(D^|MIsrT%d#n00*gV{kVt{Stfz|4v) z*|lX-bpEiJ0U)pRT4f9uoWXv5>g_Lwv%N!SD_?B%Z~5lUTYI~gMsj<#=C6*LE{0T6 zThgSG17*b4OG=pid7&d3(Kr)`Jvd&268drmCUh2e3dCv{&o*8HdV>Lj(!q`=4wpM- z#+w$+8-F#ZR<4ALOcagBSc4!b)9oAOd=>|@sn}qvpu*9+U=#vKX#)GrubP0gRr4y~=TBeAD)_^PdtU2h==dDqT==WwS+n0ySg*mk1o+j> z*1NQR*d7jzec0X5MB;wGBBVf){!H6%8@*KCV&oxse3zpK9r5~-zuBpu7-%-@Tie5N zseu|v1|QTlqXP4+Pd}@IPf*LmDg*SjVQ8r%#^ginQK;W*tQI@nt7&YfCmzpsH`Wf^YCaK-23d(W#wQ5*NMdF_eYqyiR1FMorT6MVa z^!-U|$z#LBxZ2Swk1;12tz)je2XPapaJqVNPKxZk#9^(di5ky!St*rhu0|Gd zN=s!)FO^~F2j+QRI8)NL@P86I|Qi8#?WpAKp90&9()lPw_Fe?JAqfw!!AxaJb2-nda#9yQ9Q^du02(g zNK8HR@&cyAdhj}p`--Avr~uYp$|@Ugi%eHhf@N4+nOvU(X%uNYtK;4TS(%wX%ZHa{ zM9Qzq=UUnZp_|esQ`hzAyXf&a&^zy2?+~v;>|5ziR>N5;A%RPg!RfV$4CNU0$YE1S z)rUN-$kj~wbqN%6ii;|g8ZgCf)5lGUdU41oAlIN+;h-t-sJolB{nOw5o;GqvuL!Pp zv;FQ0w4xRD!ZhTH-uIq8!ek<*`hmTW z52O9X%hZpLqX#PwWSB^3!sbTLG#|YZk<)TvvjlT9&`3ylmT`bTU4*agI?3>!2dR06 z6jSS-mDyvq4y%PetlSpMD&0?dWFkYwp=fG8K{n`a-sos0rX&dO$)=SldyV49>KqrvmEco`k!srr2cOCdL}#%MxBk7W z<|VmUwJ(RIx+f8wP}X#GaF8Q3XP2TYN=&A5`UZAKl@)fcN@QF9ESKUbaK$!MAY*Lm zv|NCGH{pm2kuimhzD|P_*Z@KEV$07VrDmX`4mhY31r3}_;7Sb_nZR)cwD0aW_TG=U zcSr?Mo+8+n4YjH#&-&09$&MlZ`1Lc(5oEI~2dMWqd+)||)5n>~}Y}l3@D!eD>FSg8HbF)*53RFy9&(R^s@yiDY7qscnr+ z2xOqh$l%6u6^>EA`C#v^Z#+_J^yevycXm3>_VwY3 zpQC+&!txBrh?^6{oQLEyVkWxvFbgVtX<;vls~) zy5}U`E_E>Dv~=$|s4WNIGMa)d@%@FF8Ckx*8ae1^rV+Dv$|i5cRv9kjZU}xQ)>0eI zuo6j76BUkn55Q5C{o!Z9)S!ISvqH=o0z9+EBgdG8ITOMHG<61{`T|qcIs>?vy-bT& zS(?LfqAnEhZj3X51%wN5tTLhW8v|BdcE<-s<5eskHdDo_SMwtv)l@;viq6cU)5Iy0 z!b2~Ymtd;MnwMWy2Y|&}i)Vhk8azxG#^ z8;;y6JQQ>Qw@4BEG`HI@(wxW}1Go;_4m!pc|MLim-4uiFF-joE)zT_oR0vR%`pbn` zF-W%AjvI5*M+*L_fI&`f&U-xkS!G0lSt5uejQ((c-+`?;OPPPn7W~zcLaNRDZ;bnE7 z1oi0DktRM_D9OTnmDzk2D}rlX$=Mv91e`zYUxRj)vqt<1RcM*fG1*}G@B0^xqz>wm z4SBq162fHlJi}3bq~Qf@$tGs0I7&;Bi%C8I0~g`%#g>IJzbbi`AXjng*c3%OYymAT zMdp{}7{gLIK$#L*VyQqmZ%Bl|oR^se^@dljm)^Hx0=C4%3V3s7v1!;$#U>}}SZC^j zcULzyT@LnmIq`(RZr}LpU9Z-T6>6hHyxq)@mw9UKi*3FF2!6CTp7j9-!3-x@nXfz` zCj9b;{q^+hiwIfV$_{?AAN+$M$i2PYGYD)jy3#wyVBlY^c0AbtWaEZFwgj*i&6nG) zH+H-zLKa5q0bGy4W%Lv4Oaw^!wZT`8!D1i0WEd)<@ItU9yHetij?7fBn{cqC zPj)G?e@oFQX#yy(&Lk+a9!@Ad9lO1|wtx8Hd#|xoZi`j#fD$g13Ui$o>JG!xB96z@luR2=e0m z6l@GMk(OuJuxezpt0nS7Y8fgEHeefJ!!VZEW5lZOF>GT<@WAYNIt)~DT^t_3++|c1 zP4J9bPl@Y$UFatPO_}%Q|Rx{WQDgV%h7##V{Xa z2j^2y`p>~1?l3egAvoIs63A$vG^D`l&_QX$(W$X%%Bw?dCYgI)4iuRTq~+f$K4|fr z|2lv!ifwA8MFGR-JrFjxchXkVw=(m9T6y>S&R6*@s6_IbqKoyX$Jv9R zk$yBnc>`kkMYiiwV63>{&%=?p&$}B93`zkBP@$8BDdEgtacZHQfpSp0)+SkJks+lz z+LLflgvXUCFODa806`hbh)&~UAC5Aq?Jb;1FxD%rNO*2xlDbirlnBbpIv)n3=cK`0 zcOLv^{<-0EMxei#fmJo}o t${tRS{&nH=;CQMc4r#@_R}>}}`+s1k94D3`WPbnv002ovPDHLkV1mZEXHEbB literal 0 HcmV?d00001 diff --git a/interface/resources/images/samples/hifi-place-get-avatars.png b/interface/resources/images/samples/hifi-place-get-avatars.png new file mode 100644 index 0000000000000000000000000000000000000000..245a728db71360cc157b76b86c14f989319b0254 GIT binary patch literal 84340 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 to make a custom avatar by opening this link on your desktop.' + popup.inputText.visible = true; + popup.inputText.placeholderText = 'Enter Avatar Url'; + popup.button1text = 'CANCEL'; + popup.button2text = 'CONFIRM'; + popup.onButton2Clicked = function() { + popup.close(); + } + + popup.open(); + } + } + } + + SquareLabel { + anchors.right: parent.right + anchors.rightMargin: 30 + anchors.verticalCenter: wearablesLabel.verticalCenter + glyphText: "\ue02e" + + visible: avatarWearablesCount !== 0 + + MouseArea { + anchors.fill: parent + onClicked: { + adjustWearables.open(); + } + } + } + + TextStyle3 { + anchors.right: parent.right + anchors.rightMargin: 30 + anchors.verticalCenter: wearablesLabel.verticalCenter + font.underline: true + text: "Add" + visible: avatarWearablesCount === 0 + + MouseArea { + anchors.fill: parent + property url getWearablesUrl: '../../images/samples/hifi-place-77312e4b-6f48-4eb4-87e2-50444d8e56d1.png' + + // debug only + acceptedButtons: Qt.LeftButton | Qt.RightButton + property int debug_newAvatarIndex: 0 + + onClicked: { + if(mouse.button == Qt.RightButton) { + + for(var i = 0; i < 3; ++i) + { + console.debug('adding avatar...'); + + var avatar = { + 'url': '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png', + 'name': 'Lexi' + (++debug_newAvatarIndex), + 'wearables': '', + 'favorite': false + }; + + allAvatars.append(avatar) + + if(pageOfAvatars.hasGetAvatars()) + pageOfAvatars.removeGetAvatars(); + + if(pageOfAvatars.count !== view.itemsPerPage) + pageOfAvatars.append(avatar); + + if(pageOfAvatars.count !== view.itemsPerPage) + pageOfAvatars.appendGetAvatars(); + } + + return; + } + + popup.button2text = 'AvatarIsland' + popup.button1text = 'CANCEL' + popup.titleText = 'Get Wearables' + popup.bodyText = 'Buy wearables from Marketplace' + '\n' + + 'Wear wearable from My Purchases' + '\n' + + 'You can visit the domain “AvatarIsland”' + '\n' + + 'to get wearables' + + popup.imageSource = getWearablesUrl; + popup.onButton2Clicked = function() { + popup.close(); + gotoAvatarAppPanel.visible = true; + } + popup.open(); + } + } + } + } + + Rectangle { + id: favoritesBlock + height: 369 + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + color: style.colors.lightGrayBackground + + TextStyle1 { + id: favoritesLabel + anchors.top: parent.top + anchors.topMargin: 9 + anchors.left: parent.left + anchors.leftMargin: 30 + text: "Favorites" + } + + TextStyle8 { + id: manageLabel + anchors.top: parent.top + anchors.topMargin: 9 + anchors.right: parent.right + anchors.rightMargin: 30 + text: isInManageState ? "Back" : "Manage" + color: style.colors.blueHighlight + MouseArea { + anchors.fill: parent + onClicked: { + isInManageState = isInManageState ? false : true; + } + } + } + + Item { + anchors.left: parent.left + anchors.leftMargin: 30 + anchors.right: parent.right + anchors.rightMargin: 30 + + anchors.top: favoritesLabel.bottom + anchors.topMargin: 9 + anchors.bottom: parent.bottom + + GridView { + id: view + anchors.fill: parent + interactive: false; + currentIndex: (selectedAvatarId !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatar(selectedAvatarId) : -1 + + AvatarsModel { + id: allAvatars + + function findAvatar(avatarId) { + console.debug('AvatarsModel: find avatar by', avatarId); + + for(var i = 0; i < count; ++i) { + if(get(i).url === avatarId) { + console.debug('avatar found by index: ', i) + return get(i); + } + } + + return -1; + } + } + + property int itemsPerPage: 8 + property int totalPages: Math.ceil((allAvatars.count + 1) / itemsPerPage) + onTotalPagesChanged: { + console.debug('total pages: ', totalPages) + } + + property int currentPage: 0; + onCurrentPageChanged: { + console.debug('currentPage: ', currentPage) + currentIndex = Qt.binding(function() { + return (selectedAvatarId !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatar(selectedAvatarId) : -1 + }) + } + + property bool hasNext: currentPage < (totalPages - 1) + onHasNextChanged: { + console.debug('hasNext: ', hasNext) + } + + property bool hasPrev: currentPage > 0 + onHasPrevChanged: { + console.debug('hasPrev: ', hasPrev) + } + + function setPage(pageIndex) { + pageOfAvatars.isUpdating = true; + pageOfAvatars.clear(); + var start = pageIndex * itemsPerPage; + var end = Math.min(start + itemsPerPage, allAvatars.count); + + for(var itemIndex = 0; start < end; ++start, ++itemIndex) { + var avatarItem = allAvatars.get(start) + console.debug('getting ', start, avatarItem) + pageOfAvatars.append(avatarItem); + } + + if(pageOfAvatars.count !== itemsPerPage) + pageOfAvatars.appendGetAvatars(); + + currentPage = pageIndex; + console.debug('switched to the page with', pageOfAvatars.count, 'items') + pageOfAvatars.isUpdating = false; + } + + model: ListModel { + id: pageOfAvatars + + property bool isUpdating: false; + property var getMoreAvatars: {'url' : '', 'name' : 'Get More Avatars'} + + function findAvatar(avatarId) { + console.debug('pageOfAvatars.findAvatar: ', avatarId); + + for(var i = 0; i < count; ++i) { + if(get(i).url === avatarId) { + console.debug('avatar found by index: ', i) + return i; + } + } + + return -1; + } + + function appendGetAvatars() { + append(getMoreAvatars); + } + + function hasGetAvatars() { + return count != 0 && get(count - 1).url === '' + } + + function removeGetAvatars() { + if(hasGetAvatars()) { + remove(count - 1) + console.debug('removed get avatars...'); + } + } + } + + flow: GridView.FlowTopToBottom + + cellHeight: 92 + 36 + cellWidth: 92 + 18 + + delegate: Item { + id: delegateRoot + height: GridView.view.cellHeight + width: GridView.view.cellWidth + + Item { + id: container + width: 92 + height: 92 + + states: [ + State { + name: "hovered" + when: favoriteAvatarMouseArea.containsMouse; + PropertyChanges { target: container; y: -5 } + PropertyChanges { target: favoriteAvatarImage; dropShadowRadius: 10 } + PropertyChanges { target: favoriteAvatarImage; dropShadowVerticalOffset: 6 } + } + ] + + AvatarThumbnail { + id: favoriteAvatarImage + imageUrl: url + wearablesCount: (wearables && wearables !== '') ? wearables.split('|').length : 0 + onWearablesCountChanged: { + console.debug('delegate: AvatarThumbnail.wearablesCount: ', wearablesCount) + } + + visible: url !== '' + + MouseArea { + id: favoriteAvatarMouseArea + anchors.fill: parent + hoverEnabled: true + property url getWearablesUrl: '../../images/samples/hifi-place-77312e4b-6f48-4eb4-87e2-50444d8e56d1.png' + + onClicked: { + if(isInManageState) { + var currentItem = delegateRoot.GridView.view.model.get(index); + + popup.titleText = 'Delete Favorite: {AvatarName}'.replace('{AvatarName}', currentItem.name) + popup.bodyText = 'This will delete your favorite. You will retain access to the wearables and avatar that made up the favorite from My Purchases.' + popup.imageSource = null; + popup.button1text = 'CANCEL' + popup.button2text = 'DELETE' + + popup.onButton2Clicked = function() { + popup.close(); + + pageOfAvatars.isUpdating = true; + + console.debug('removing ', index) + + var absoluteIndex = view.currentPage * view.itemsPerPage + index + console.debug('removed ', absoluteIndex, 'view.currentPage', view.currentPage, + 'view.itemsPerPage: ', view.itemsPerPage, 'index', index, 'pageOfAvatars', pageOfAvatars, 'pageOfAvatars.count', pageOfAvatars) + + allAvatars.remove(absoluteIndex) + pageOfAvatars.remove(index); + + var itemsOnPage = pageOfAvatars.count; + var newItemIndex = view.currentPage * view.itemsPerPage + itemsOnPage; + + console.debug('newItemIndex: ', newItemIndex, 'allAvatars.count - 1: ', allAvatars.count - 1, 'pageOfAvatars.count:', pageOfAvatars.count); + + if(newItemIndex <= (allAvatars.count - 1)) { + pageOfAvatars.append(allAvatars.get(newItemIndex)); + } else { + if(!pageOfAvatars.hasGetAvatars()) + pageOfAvatars.appendGetAvatars(); + } + + console.debug('removed ', absoluteIndex, 'newItemIndex: ', newItemIndex, 'allAvatars.count:', allAvatars.count, 'pageOfAvatars.count:', pageOfAvatars.count) + pageOfAvatars.isUpdating = false; + }; + + popup.open(); + + } else { + if(delegateRoot.GridView.view.currentIndex !== index) { + var currentItem = delegateRoot.GridView.view.model.get(index); + + popup.button2text = 'CONFIRM' + popup.button1text = 'CANCEL' + popup.titleText = 'Load Favorite: {AvatarName}'.replace('{AvatarName}', currentItem.name) + popup.bodyText = 'This will switch your current avatar and ararables that you are wearing with a new avatar and wearables.' + popup.imageSource = null; + popup.onButton2Clicked = function() { + selectedAvatarId = currentItem.url; + popup.close(); + delegateRoot.GridView.view.currentIndex = index; + } + popup.open(); + } + } + } + } + } + + Rectangle { + id: highlight + anchors.fill: favoriteAvatarImage + visible: delegateRoot.GridView.isCurrentItem + color: 'transparent' + border.width: 2 + border.color: style.colors.blueHighlight + } + + Colorize { + anchors.fill: favoriteAvatarImage + source: favoriteAvatarImage + saturation: 0.2 + visible: isInManageState && !highlight.visible + } + + HiFiGlyphs { + anchors.fill: parent + text: "{" + visible: isInManageState && !highlight.visible + horizontalAlignment: Text.AlignHCenter + size: 56 + } + + ShadowRectangle { + width: 92 + height: 92 + color: style.colors.blueHighlight + visible: url === '' + + HiFiGlyphs { + anchors.centerIn: parent + + color: 'white' + size: 60 + text: "K" + } + + MouseArea { + anchors.fill: parent + property url getAvatarsUrl: '../../images/samples/hifi-place-get-avatars.png' + + onClicked: { + console.debug('getAvatarsUrl: ', getAvatarsUrl); + + popup.button2text = 'BodyMarkt' + popup.button1text = 'CANCEL' + popup.titleText = 'Get Avatars' + + popup.bodyText = 'Buy avatars from Marketplace' + '\n' + + 'Wear avatars from My Purchases' + '\n' + + 'You can visit the domain “BodyMart”' + '\n' + + 'to get avatars' + + popup.imageSource = getAvatarsUrl; + popup.onButton2Clicked = function() { + popup.close(); + gotoAvatarAppPanel.visible = true; + } + popup.open(); + } + } + } + } + + TextStyle7 { + id: text + width: 92 + anchors.top: container.bottom + anchors.topMargin: 8 + anchors.horizontalCenter: container.horizontalCenter + verticalAlignment: Text.AlignTop + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + text: name + } + } + } + + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + + HiFiGlyphs { + rotation: 180 + text: "\ue01d"; + size: 50 + color: view.hasPrev ? 'black' : 'gray' + horizontalAlignment: Text.AlignHCenter + MouseArea { + anchors.fill: parent + enabled: view.hasPrev + onClicked: { + view.setPage(view.currentPage - 1) + } + } + } + + spacing: 0 + + HiFiGlyphs { + text: "\ue01d"; + size: 50 + color: view.hasNext ? 'black' : 'gray' + horizontalAlignment: Text.AlignHCenter + MouseArea { + anchors.fill: parent + enabled: view.hasNext + onClicked: { + view.setPage(view.currentPage + 1) + } + } + } + + anchors.bottom: parent.bottom + anchors.bottomMargin: 19 + } + } + + AdjustWearables { + id: adjustWearables + z: 2 + } + + MessageBox { + id: popup + } + + CreateFavoriteDialog { + id: createFavorite + } + + Rectangle { + id: gotoAvatarAppPanel + anchors.fill: parent + anchors.leftMargin: 19 + anchors.rightMargin: 19 + + // color: 'green' + visible: false + onVisibleChanged: { + if(visible) { + console.debug('selectedAvatar.wearables: ', selectedAvatar.wearables) + selectedAvatar.wearables = 'hat|sunglasses|bracelet' + pageOfAvatars.setProperty(view.currentIndex, 'wearables', selectedAvatar.wearables) + } + } + + Rectangle { + width: 442 + height: 447 + // color: 'yellow' + + anchors.bottom: parent.bottom + anchors.bottomMargin: 259 + + TextStyle1 { + anchors.fill: parent + horizontalAlignment: "AlignHCenter" + wrapMode: "WordWrap" + text: "You are teleported to “AvatarIsland” VR world and you buy a hat, sunglasses and a bracelet." + } + } + + Rectangle { + width: 442 + height: 177 + // color: 'yellow' + + anchors.bottom: parent.bottom + anchors.bottomMargin: 40 + + TextStyle1 { + anchors.fill: parent + horizontalAlignment: "AlignHCenter" + wrapMode: "WordWrap" + text: 'Click here to open the Avatar app.' + + MouseArea { + anchors.fill: parent + property int newAvatarIndex: 0 + + onClicked: { + gotoAvatarAppPanel.visible = false; + + var avatar = { + 'url': '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png', + 'name': 'Lexi' + (++newAvatarIndex), + 'wearables': '', + 'favorite': false + }; + + allAvatars.append(avatar) + + if(pageOfAvatars.hasGetAvatars()) + pageOfAvatars.removeGetAvatars(); + + if(pageOfAvatars.count !== view.itemsPerPage) + pageOfAvatars.append(avatar); + + if(pageOfAvatars.count !== view.itemsPerPage) + pageOfAvatars.appendGetAvatars(); + + console.debug('avatar appended: allAvatars.count: ', allAvatars.count, 'pageOfAvatars.count: ', pageOfAvatars.count); + } + } + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml new file mode 100644 index 0000000000..c195a4c2b5 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -0,0 +1,183 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root; + visible: false; + width: 480 + height: 706 + color: 'lightgray' + + property bool modified: false; + Component.onCompleted: { + modified = false; + } + + onModifiedChanged: { + console.debug('modified: ', modified) + } + + property var onButton2Clicked; + property var onButton1Clicked; + + function open() { + visible = true; + } + + function close() { + visible = false; + } + + HifiConstants { id: hifi } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Column { + anchors.top: parent.top + anchors.topMargin: 15 + anchors.horizontalCenter: parent.horizontalCenter + + spacing: 20 + width: parent.width - 30 * 2 + + TextStyle5 { + anchors.horizontalCenter: parent.horizontalCenter + text: "Adjust Wearables" + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 2 + color: 'gray' + } + + HifiControlsUit.ComboBox { + anchors.left: parent.left + anchors.right: parent.right + + model: [ + 'Fedora.fbx [HeadTop_End]', + 'Fedora1.fbx [HeadTop_End]', + 'Fedora2.fbx [HeadTop_End]' + ] + } + + Column { + width: parent.width + spacing: 5 + + Row { + spacing: 20 + + TextStyle5 { + text: "Position" + } + + TextStyle7 { + text: "m" + } + } + + Vector3 { + id: position + onXvalueChanged: modified = true; + onYvalueChanged: modified = true; + onZvalueChanged: modified = true; + } + } + + Column { + width: parent.width + spacing: 5 + + Row { + spacing: 20 + + TextStyle5 { + text: "Rotation" + } + + TextStyle7 { + text: "deg" + } + } + + Vector3 { + id: rotation + onXvalueChanged: modified = true; + onYvalueChanged: modified = true; + onZvalueChanged: modified = true; + } + } + + Column { + width: parent.width + spacing: 5 + + TextStyle5 { + text: "Scale" + } + + Item { + width: parent.width + height: childrenRect.height + + HifiControlsUit.SpinBox { + id: scalespinner + value: 0 + backgroundColor: "darkgray" + width: position.spinboxWidth + colorScheme: hifi.colorSchemes.light + onValueChanged: modified = true; + } + + HifiControlsUit.Button { + anchors.right: parent.right + color: hifi.buttons.red; + colorScheme: hifi.colorSchemes.dark; + text: "TAKE IT OFF" + } + } + + } + } + + DialogButtons { + anchors.bottom: parent.bottom + anchors.bottomMargin: 30 + anchors.left: parent.left + anchors.leftMargin: 30 + anchors.right: parent.right + anchors.rightMargin: 30 + + yesText: "SAVE" + noText: "CANCEL" + + onYesClicked: function() { + if(onButton2Clicked) { + onButton2Clicked(); + } else { + root.close(); + } + } + + onNoClicked: function() { + if(onButton1Clicked) { + onButton1Clicked(); + } else { + root.close(); + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml b/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml new file mode 100644 index 0000000000..474ceb5f59 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml @@ -0,0 +1,57 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" + +ShadowRectangle { + id: header + anchors.left: parent.left + anchors.right: parent.right + height: 84 + + property alias pageTitle: title.text + property alias avatarIconVisible: avatarIcon.visible + property alias settingsButtonVisible: settingsButton.visible + + signal settingsClicked; + + AvatarAppStyle { + id: style + } + + color: style.colors.lightGrayBackground + + HiFiGlyphs { + id: avatarIcon + anchors.left: parent.left + anchors.leftMargin: 23 + anchors.top: parent.top + anchors.topMargin: 29 + + size: 38 + text: "<" + } + + TextStyle6 { + id: title + anchors.left: avatarIcon.visible ? avatarIcon.right : avatarIcon.left + anchors.leftMargin: 4 + anchors.verticalCenter: avatarIcon.verticalCenter + text: 'Avatar' + } + + HiFiGlyphs { + id: settingsButton + anchors.right: parent.right + anchors.rightMargin: 30 + anchors.verticalCenter: avatarIcon.verticalCenter + text: "&" + + MouseArea { + id: settingsMouseArea + anchors.fill: parent + onClicked: { + settingsClicked(); + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml b/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml new file mode 100644 index 0000000000..f66c7121cb --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml @@ -0,0 +1,29 @@ +// +// HiFiConstants.qml +// +// Created by Alexander Ivash on 17 Apr 2018 +// Copyright 2018 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 +// + +import QtQuick 2.5 +import QtQuick.Window 2.2 +import "../../styles-uit" + +QtObject { + readonly property QtObject colors: QtObject { + readonly property color lightGrayBackground: "#f2f2f2" + readonly property color black: "#000000" + readonly property color white: "#ffffff" + readonly property color blueHighlight: "#00b4ef" + readonly property color inputFieldBackground: "#d4d4d4" + readonly property color yellowishOrange: "#ffb017" + readonly property color blueAccent: "#0093c5" + readonly property color greenHighlight: "#1fc6a6" + readonly property color lightGray: "#afafaf" + readonly property color redHighlight: "#ea4c5f" + readonly property color orangeAccent: "#ff6309" + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml new file mode 100644 index 0000000000..3fd7bf22b7 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml @@ -0,0 +1,30 @@ +import QtQuick 2.9 + +Item { + width: 92 + height: 92 + + property int wearablesCount: 0 + onWearablesCountChanged: { + console.debug('AvatarThumbnail: wearablesCount = ', wearablesCount) + } + + property alias dropShadowRadius: avatarImage.dropShadowRadius + property alias dropShadowHorizontalOffset: avatarImage.dropShadowHorizontalOffset + property alias dropShadowVerticalOffset: avatarImage.dropShadowVerticalOffset + + property alias imageUrl: avatarImage.source + + ShadowImage { + id: avatarImage + anchors.fill: parent + } + + AvatarWearablesIndicator { + anchors.left: avatarImage.left + anchors.bottom: avatarImage.bottom + anchors.leftMargin: 57 + wearablesCount: parent.wearablesCount + visible: parent.wearablesCount !== 0 + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml b/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml new file mode 100644 index 0000000000..ee720d6a7f --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml @@ -0,0 +1,40 @@ +import QtQuick 2.9 +import "../../controls-uit" +import "../../styles-uit" + +Rectangle { + property int wearablesCount: 0 + + width: 46.5 + height: 46.5 + radius: width / 2 + + AvatarAppStyle { + id: style + } + + color: style.colors.greenHighlight + + HiFiGlyphs { + width: 26.5 + height: 13.8 + anchors.top: parent.top + anchors.topMargin: 10 + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + text: "\ue02e" + } + + Item { + width: 46.57 + height: 23 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 2.76 + + TextStyle2 { + anchors.horizontalCenter: parent.horizontalCenter + text: wearablesCount + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml b/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml new file mode 100644 index 0000000000..5672e04731 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml @@ -0,0 +1,48 @@ +import QtQuick 2.9 + +ListModel { + id: model + + ListElement { + url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d.png' + name: 'Woody' + wearables: '' + favorite: false + } + ListElement { + url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-1.png' + name: 'Damien' + wearables: '' + favorite: false + } + ListElement { + url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png' + name: 'Lexi' + wearables: '' + favorite: false + } + ListElement { + url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-3.png' + name: 'Judie' + wearables: '' + favorite: true + } + ListElement { + url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-4.png' + name: 'Alex' + wearables: '' + favorite: true + } + ListElement { + url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-5.png' + name: 'Matthew' + wearables: '' + favorite: true + } + ListElement { + url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-5.png' + name: 'Ogre' + wearables: '' + favorite: true + } +} diff --git a/interface/resources/qml/hifi/avatarapp/BlueButton.qml b/interface/resources/qml/hifi/avatarapp/BlueButton.qml new file mode 100644 index 0000000000..a86f7cdee7 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/BlueButton.qml @@ -0,0 +1,13 @@ +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit + +HifiControlsUit.Button { + HifiConstants { + id: hifi + } + + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.light; + height: 40 +} diff --git a/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml b/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml new file mode 100644 index 0000000000..7056582eac --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml @@ -0,0 +1,136 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root; + visible: false; + anchors.fill: parent; + color: Qt.rgba(0, 0, 0, 0.5); + z: 999; + + property string titleText: 'Create Favorite' + property string favoriteNameText: favoriteName.text + property string avatarImageUrl: null + property int wearablesCount: 0 + + property string button1color: hifi.buttons.noneBorderlessGray; + property string button1text: 'CANCEL' + property string button2color: hifi.buttons.blue; + property string button2text: 'CONFIRM' + + property var onSaveClicked; + property var onCancelClicked; + + function open(avatar) { + favoriteName.text = ''; + avatarImageUrl = avatar.url; + wearablesCount = avatar.wearables !== '' ? avatar.wearables.split('|').length : 0; + + visible = true; + } + + function close() { + console.debug('closing'); + visible = false; + } + + HifiConstants { + id: hifi + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Rectangle { + id: mainContainer; + width: Math.max(parent.width * 0.8, 400) + property int margin: 30; + + height: childrenRect.height + margin * 2 + onHeightChanged: { + console.debug('mainContainer: height = ', height) + } + + anchors.centerIn: parent + + color: "white" + + TextStyle1 { + id: title + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 30 + anchors.leftMargin: 30 + anchors.rightMargin: 30 + + text: root.titleText + } + + Item { + id: contentContainer + width: parent.width - 50 + height: childrenRect.height + + anchors.top: title.bottom + anchors.topMargin: 10 + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + + Row { + id: bodyRow + + spacing: 44 + + AvatarThumbnail { + imageUrl: avatarImageUrl + wearablesCount: avatarWearablesCount + } + + InputTextStyle4 { + id: favoriteName + anchors.verticalCenter: parent.verticalCenter + placeholderText: "Enter Favorite Name" + } + } + + DialogButtons { + anchors.top: bodyRow.bottom + anchors.topMargin: 20 + anchors.left: parent.left + anchors.right: parent.right + + yesButton.enabled: favoriteNameText !== '' + yesText: root.button2text + noText: root.button1text + + onYesClicked: function() { + if(onSaveClicked) { + onSaveClicked(); + } else { + root.close(); + } + } + + onNoClicked: function() { + if(onCancelClicked) { + onCancelClicked(); + } else { + root.close(); + } + } + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/DialogButtons.qml b/interface/resources/qml/hifi/avatarapp/DialogButtons.qml new file mode 100644 index 0000000000..46c17bb4dc --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/DialogButtons.qml @@ -0,0 +1,41 @@ +import QtQuick 2.9 + +Row { + id: root + property string yesText; + property string noText; + property var onYesClicked; + property var onNoClicked; + + property alias yesButton: yesButton + property alias noButton: noButton + + height: childrenRect.height + layoutDirection: Qt.RightToLeft + + spacing: 30 + + BlueButton { + id: yesButton; + text: yesText; + onClicked: { + console.debug('bluebutton.clicked', onYesClicked); + + if(onYesClicked) { + onYesClicked(); + } + } + } + + WhiteButton { + id: noButton + text: noText; + onClicked: { + console.debug('whitebutton.clicked', onNoClicked); + + if(onNoClicked) { + onNoClicked(); + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml b/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml new file mode 100644 index 0000000000..1e064e7a18 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml @@ -0,0 +1,23 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +import QtQuick 2.0 +import QtQuick.Controls 2.2 + +TextField { + id: control + font.family: "Fira Sans" + font.pixelSize: 15; + color: 'black' + + AvatarAppStyle { + id: style + } + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 40 + color: style.colors.inputFieldBackground + border.color: style.colors.lightGray + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarapp/MessageBox.qml b/interface/resources/qml/hifi/avatarapp/MessageBox.qml new file mode 100644 index 0000000000..eef8ebcb07 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/MessageBox.qml @@ -0,0 +1,177 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root; + visible: false; + anchors.fill: parent; + color: Qt.rgba(0, 0, 0, 0.5); + z: 999; + + property string titleText: '' + property string bodyText: '' + property alias inputText: input; + + property string imageSource: null + onImageSourceChanged: { + console.debug('imageSource = ', imageSource) + } + + property string button1color: hifi.buttons.noneBorderlessGray; + property string button1text: '' + property string button2color: hifi.buttons.blue; + property string button2text: '' + + property var onButton2Clicked; + property var onButton1Clicked; + + function open() { + visible = true; + } + + function close() { + visible = false; + + onButton1Clicked = null; + onButton2Clicked = null; + button1text = ''; + button2text = ''; + imageSource = null; + inputText.visible = false; + inputText.placeholderText = ''; + inputText.text = ''; + } + + HifiConstants { + id: hifi + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Rectangle { + id: mainContainer; + width: Math.max(parent.width * 0.8, 400) + property int margin: 30; + + height: childrenRect.height + margin * 2 + onHeightChanged: { + console.debug('mainContainer: height = ', height) + } + + anchors.centerIn: parent + + color: "white" + + TextStyle1 { + id: title + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 30 + anchors.leftMargin: 30 + anchors.rightMargin: 30 + + text: root.titleText + } + + Item { + id: contentContainer + width: parent.width - 60 + height: childrenRect.height + onHeightChanged: { + console.debug('contentContainer: height = ', height, + 'image.height = ', image.height, + 'body.height = ', body.height + ) + } + + anchors.top: title.bottom + anchors.topMargin: 10 + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + + TextStyle3 { + id: body; + text: root.bodyText; + anchors.left: parent.left; + anchors.right: parent.right; + height: paintedHeight; + verticalAlignment: Text.AlignTop; + wrapMode: Text.WordWrap; + } + + Image { + id: image + Binding on height { + when: imageSource === null + value: 0 + } + + anchors.top: body.bottom + anchors.topMargin: imageSource === null ? 0 : 30 + anchors.left: parent.left; + anchors.right: parent.right; + + Binding on source { + when: imageSource !== null + value: imageSource + } + + visible: imageSource !== null ? true : false + } + + InputTextStyle4 { + id: input + visible: false + height: visible ? implicitHeight : 0 + + anchors.top: imageSource !== null ? image.bottom : body.bottom + anchors.left: parent.left; + anchors.right: parent.right; + } + } + + DialogButtons { + id: buttons + + anchors.top: contentContainer.bottom + anchors.topMargin: 30 + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 30 + + yesButton.enabled: !input.visible || input.text.length !== 0 + yesText: root.button2text + noText: root.button1text + + onYesClicked: function() { + if(onButton2Clicked) { + onButton2Clicked(); + } else { + root.close(); + } + } + + onNoClicked: function() { + if(onButton1Clicked) { + onButton1Clicked(); + } else { + root.close(); + } + } + } + + } +} diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml new file mode 100644 index 0000000000..1ff8cdde28 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -0,0 +1,304 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: settings + + color: 'white' + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: parent.bottom + visible: false; + z: 3 + + property alias onSaveClicked: dialogButtons.onYesClicked + property alias onCancelClicked: dialogButtons.onNoClicked + + function open() { + visible = true; + } + + function close() { + visible = false + } + + Item { + anchors.left: parent.left + anchors.leftMargin: 27 + anchors.top: parent.top + anchors.topMargin: 25 + anchors.right: parent.right + anchors.rightMargin: 32 + anchors.bottom: parent.bottom + anchors.bottomMargin: 57 + + RowLayout { + id: avatarScaleRow + anchors.left: parent.left + anchors.right: parent.right + + spacing: 17 + + RalewaySemiBold { + size: 14; + text: "Avatar Scale" + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + } + + RowLayout { + anchors.verticalCenter: parent.verticalCenter + Layout.fillWidth: true + + spacing: 0 + + HiFiGlyphs { + size: 30 + text: 'T' + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.verticalCenter: parent.verticalCenter + } + + Slider { + id: slider + from: 0 + to: 100 + anchors.verticalCenter: parent.verticalCenter + Layout.fillWidth: true + + handle: Rectangle { + width: 18 + height: 18 + color: 'white' + radius: 9 + border.width: 1 + border.color: 'black' + x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + } + background: Rectangle { + x: slider.leftPadding + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitWidth: 200 + implicitHeight: 18 + width: slider.availableWidth + height: implicitHeight + radius: 9 + border.color: 'black' + border.width: 1 + color: '#f2f2f2' + } + } + + HiFiGlyphs { + size: 40 + text: 'T' + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + ShadowRectangle { + width: 28 + height: 28 + color: 'white' + + radius: 3 + border.color: 'black' + border.width: 1.5 + anchors.verticalCenter: parent.verticalCenter + + RalewaySemiBold { + size: 13; + text: "1x" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + GridLayout { + id: handAndCollisions + anchors.top: avatarScaleRow.bottom + anchors.topMargin: 39 + anchors.left: parent.left + anchors.right: parent.right + + rows: 2 + rowSpacing: 25 + + columns: 3 + + RalewaySemiBold { + Layout.row: 0 + Layout.column: 0 + + size: 14; + text: "Dominant Hand" + } + + ButtonGroup { + id: leftRight + } + + HifiControlsUit.RadioButton { + id: leftHandRadioButton + + Layout.row: 0 + Layout.column: 1 + Layout.leftMargin: -18 + + ButtonGroup.group: leftRight + checked: true + + text: "Left hand" + boxSize: 20 + + contentItem: TextStyle9 { + text: leftHandRadioButton.text + color: 'black' + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: leftHandRadioButton.indicator.width + leftHandRadioButton.spacing + } + } + + HifiControlsUit.RadioButton { + id: rightHandRadioButton + + Layout.row: 0 + Layout.column: 2 + ButtonGroup.group: leftRight + + text: "Right hand" + boxSize: 20 + + contentItem: TextStyle9 { + text: rightHandRadioButton.text + color: 'black' + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: rightHandRadioButton.indicator.width + rightHandRadioButton.spacing + } + } + + RalewaySemiBold { + Layout.row: 1 + Layout.column: 0 + + size: 14; + text: "Avatar Collisions" + } + + ButtonGroup { + id: onOff + } + + HifiControlsUit.RadioButton { + id: onRadioButton + + Layout.row: 1 + Layout.column: 1 + Layout.leftMargin: -18 + + ButtonGroup.group: onOff + checked: true + + text: "ON" + boxSize: 20 + + contentItem: TextStyle9 { + text: onRadioButton.text + color: 'black' + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: onRadioButton.indicator.width + onRadioButton.spacing + } + } + + HifiControlsUit.RadioButton { + id: offRadioButton + + Layout.row: 1 + Layout.column: 2 + ButtonGroup.group: onOff + + text: "OFF" + boxSize: 20 + + contentItem: TextStyle9 { + text: offRadioButton.text + color: 'black' + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: offRadioButton.indicator.width + offRadioButton.spacing + } + } + } + + ColumnLayout { + id: avatarAnimationLayout + anchors.top: handAndCollisions.bottom + anchors.topMargin: 25 + anchors.left: parent.left + anchors.right: parent.right + + spacing: 4 + + RalewaySemiBold { + size: 14; + text: "Avatar Animation JSON" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } + + InputTextStyle4 { + anchors.left: parent.left + anchors.right: parent.right + placeholderText: 'user\\file\\dir' + } + } + + ColumnLayout { + id: avatarCollisionLayout + anchors.top: avatarAnimationLayout.bottom + anchors.topMargin: 25 + anchors.left: parent.left + anchors.right: parent.right + + spacing: 4 + + RalewaySemiBold { + size: 14; + text: "Avatar collision sound URL (optional)" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } + + InputTextStyle4 { + anchors.left: parent.left + anchors.right: parent.right + placeholderText: 'https://hifi-public.s3.amazonaws.com/sounds/Collisions-' + } + } + + DialogButtons { + id: dialogButtons + anchors.right: parent.right + anchors.bottom: parent.bottom + + yesText: "SAVE" + noText: "CANCEL" + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml new file mode 100644 index 0000000000..8f8ad587e3 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml @@ -0,0 +1,26 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +Item { + property alias source: image.source + property alias dropShadowRadius: shadow.radius + property alias dropShadowHorizontalOffset: shadow.horizontalOffset + property alias dropShadowVerticalOffset: shadow.verticalOffset + + Image { + id: image + width: parent.width + height: parent.height + } + + DropShadow { + id: shadow + anchors.fill: image + radius: 6 + horizontalOffset: 0 + verticalOffset: 3 + color: Qt.rgba(0, 0, 0, 0.25) + source: image + } +} diff --git a/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml b/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml new file mode 100644 index 0000000000..5dc89d5227 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml @@ -0,0 +1,24 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +Item { + property alias color: rectangle.color + property alias border: rectangle.border + property alias radius: rectangle.radius + + Rectangle { + id: rectangle + width: parent.width + height: parent.height + } + + DropShadow { + anchors.fill: rectangle + radius: 6 + horizontalOffset: 0 + verticalOffset: 3 + color: Qt.rgba(0, 0, 0, 0.25) + source: rectangle + } +} diff --git a/interface/resources/qml/hifi/avatarapp/SquareLabel.qml b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml new file mode 100644 index 0000000000..afe6c751f3 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml @@ -0,0 +1,21 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +ShadowRectangle { + width: 44 + height: 28 + color: 'white' + property alias glyphText: glyph.text + property alias glyphRotation: glyph.rotation + + radius: 3 + border.color: 'black' + border.width: 1.5 + + HiFiGlyphs { + id: glyph + anchors.centerIn: parent + size: 30 + } +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle1.qml b/interface/resources/qml/hifi/avatarapp/TextStyle1.qml new file mode 100644 index 0000000000..58778de440 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle1.qml @@ -0,0 +1,6 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +RalewaySemiBold { + size: 24; +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle10.qml b/interface/resources/qml/hifi/avatarapp/TextStyle10.qml new file mode 100644 index 0000000000..171309a3d0 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle10.qml @@ -0,0 +1,6 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +RalewayBold { + size: 12; +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle11.qml b/interface/resources/qml/hifi/avatarapp/TextStyle11.qml new file mode 100644 index 0000000000..423496c1e6 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle11.qml @@ -0,0 +1,6 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +RalewayRegular { + size: 15; +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle2.qml b/interface/resources/qml/hifi/avatarapp/TextStyle2.qml new file mode 100644 index 0000000000..ae02189ad9 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle2.qml @@ -0,0 +1,6 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +RalewayBold { + size: 15; +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle3.qml b/interface/resources/qml/hifi/avatarapp/TextStyle3.qml new file mode 100644 index 0000000000..a8dea33281 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle3.qml @@ -0,0 +1,6 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +RalewayRegular { + size: 18; +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle4.qml b/interface/resources/qml/hifi/avatarapp/TextStyle4.qml new file mode 100644 index 0000000000..18be5468ef --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle4.qml @@ -0,0 +1,6 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +FiraSansRegular { + size: 15; +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle5.qml b/interface/resources/qml/hifi/avatarapp/TextStyle5.qml new file mode 100644 index 0000000000..1a393b745c --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle5.qml @@ -0,0 +1,6 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +RalewayBold { + size: 18; +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle6.qml b/interface/resources/qml/hifi/avatarapp/TextStyle6.qml new file mode 100644 index 0000000000..bd3c91aa7d --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle6.qml @@ -0,0 +1,6 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +RalewayLight { + size: 18; +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle7.qml b/interface/resources/qml/hifi/avatarapp/TextStyle7.qml new file mode 100644 index 0000000000..37155fa0e2 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle7.qml @@ -0,0 +1,7 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +FiraSansRegular { + size: 18; +// lineHeight: 16.9; +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle8.qml b/interface/resources/qml/hifi/avatarapp/TextStyle8.qml new file mode 100644 index 0000000000..f557405e5c --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle8.qml @@ -0,0 +1,6 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +RalewaySemiBold { + size: 20; +} diff --git a/interface/resources/qml/hifi/avatarapp/TextStyle9.qml b/interface/resources/qml/hifi/avatarapp/TextStyle9.qml new file mode 100644 index 0000000000..8c8c00df89 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TextStyle9.qml @@ -0,0 +1,7 @@ +import "../../controls" as HifiControls +import "../../styles-uit" + +RalewaySemiBold { + size: 14; + font.letterSpacing: 1.1; +} diff --git a/interface/resources/qml/hifi/avatarapp/Vector3.qml b/interface/resources/qml/hifi/avatarapp/Vector3.qml new file mode 100644 index 0000000000..245f804e82 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/Vector3.qml @@ -0,0 +1,47 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Row { + width: parent.width + height: xspinner.controlHeight + + property int spinboxSpace: 10 + property int spinboxWidth: (parent.width - 2 * spinboxSpace) / 3 + property color backgroundColor: "darkgray" + + spacing: spinboxSpace + + property real xvalue: xspinner.value + property real yvalue: yspinner.value + property real zvalue: zspinner.value + + HifiControlsUit.SpinBox { + id: xspinner + width: parent.spinboxWidth + labelInside: "X:" + backgroundColor: parent.backgroundColor + colorLabelInside: hifi.colors.redHighlight + colorScheme: hifi.colorSchemes.light + } + + HifiControlsUit.SpinBox { + id: yspinner + width: parent.spinboxWidth + labelInside: "Y:" + backgroundColor: parent.backgroundColor + colorLabelInside: hifi.colors.greenHighlight + colorScheme: hifi.colorSchemes.light + } + + HifiControlsUit.SpinBox { + id: zspinner + width: parent.spinboxWidth + labelInside: "Z:" + backgroundColor: parent.backgroundColor + colorLabelInside: hifi.colors.primaryHighlight + colorScheme: hifi.colorSchemes.light + } +} diff --git a/interface/resources/qml/hifi/avatarapp/WhiteButton.qml b/interface/resources/qml/hifi/avatarapp/WhiteButton.qml new file mode 100644 index 0000000000..838af9354c --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/WhiteButton.qml @@ -0,0 +1,13 @@ +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit + +HifiControlsUit.Button { + HifiConstants { + id: hifi + } + + color: hifi.buttons.noneBorderlessGray; + colorScheme: hifi.colorSchemes.light; + height: 40 +} diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index ddbeaaeea9..b275660c0f 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -21,6 +21,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/bubble.js", "system/snapshot.js", "system/pal.js", // "system/mod.js", // older UX, if you prefer + "system/avatarapp.js", "system/makeUserConnection.js", "system/tablet-goto.js", "system/marketplaces/marketplaces.js", diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js new file mode 100644 index 0000000000..3bad252db1 --- /dev/null +++ b/scripts/system/avatarapp.js @@ -0,0 +1,910 @@ +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// pal.js +// +// Created by Howard Stearns on December 9, 2016 +// Copyright 2016 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 +// + +(function() { // BEGIN LOCAL_SCOPE + + var request = Script.require('request').request; + +var populateNearbyUserList, color, textures, removeOverlays, + controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, + receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, + createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged, + avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; + +// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed +// something, will revisit as this is sorta horrible. +var UNSELECTED_TEXTURES = { + "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png") +}; +var SELECTED_TEXTURES = { + "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png") +}; +var HOVER_TEXTURES = { + "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png") +}; + +var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6}; +var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29}; +var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now +var PAL_QML_SOURCE = "hifi/AvatarApp.qml"; +var conserveResources = true; + +Script.include("/~/system/libraries/controllers.js"); + +function projectVectorOntoPlane(normalizedVector, planeNormal) { + return Vec3.cross(planeNormal, Vec3.cross(normalizedVector, planeNormal)); +} +function angleBetweenVectorsInPlane(from, to, normal) { + var projectedFrom = projectVectorOntoPlane(from, normal); + var projectedTo = projectVectorOntoPlane(to, normal); + return Vec3.orientedAngle(projectedFrom, projectedTo, normal); +} + +// +// Overlays. +// +var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. + +function ExtendedOverlay(key, type, properties, selected, hasModel) { // A wrapper around overlays to store the key it is associated with. + overlays[key] = this; + if (hasModel) { + var modelKey = key + "-m"; + this.model = new ExtendedOverlay(modelKey, "model", { + url: Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx"), + textures: textures(selected), + ignoreRayIntersection: true + }, false, false); + } else { + this.model = undefined; + } + this.key = key; + this.selected = selected || false; // not undefined + this.hovering = false; + this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... +} +// Instance methods: +ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay + Overlays.deleteOverlay(this.activeOverlay); + delete overlays[this.key]; +}; + +ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay + Overlays.editOverlay(this.activeOverlay, properties); +}; + +function color(selected, hovering, level) { + var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; + function scale(component) { + var delta = 0xFF - component; + return component + (delta * level); + } + return {red: scale(base.red), green: scale(base.green), blue: scale(base.blue)}; +} + +function textures(selected, hovering) { + return hovering ? HOVER_TEXTURES : selected ? SELECTED_TEXTURES : UNSELECTED_TEXTURES; +} +// so we don't have to traverse the overlays to get the last one +var lastHoveringId = 0; +ExtendedOverlay.prototype.hover = function (hovering) { + this.hovering = hovering; + if (this.key === lastHoveringId) { + if (hovering) { + return; + } + lastHoveringId = 0; + } + this.editOverlay({color: color(this.selected, hovering, this.audioLevel)}); + if (this.model) { + this.model.editOverlay({textures: textures(this.selected, hovering)}); + } + if (hovering) { + // un-hover the last hovering overlay + if (lastHoveringId && lastHoveringId !== this.key) { + ExtendedOverlay.get(lastHoveringId).hover(false); + } + lastHoveringId = this.key; + } +}; +ExtendedOverlay.prototype.select = function (selected) { + if (this.selected === selected) { + return; + } + + UserActivityLogger.palAction(selected ? "avatar_selected" : "avatar_deselected", this.key); + + this.editOverlay({color: color(selected, this.hovering, this.audioLevel)}); + if (this.model) { + this.model.editOverlay({textures: textures(selected)}); + } + this.selected = selected; +}; +// Class methods: +var selectedIds = []; +ExtendedOverlay.isSelected = function (id) { + return -1 !== selectedIds.indexOf(id); +}; +ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier + return overlays[key]; +}; +ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy. + var key; + for (key in overlays) { + if (iterator(ExtendedOverlay.get(key))) { + return; + } + } +}; +ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any) + if (lastHoveringId) { + ExtendedOverlay.get(lastHoveringId).hover(false); + } +}; + +// hit(overlay) on the one overlay intersected by pickRay, if any. +// noHit() if no ExtendedOverlay was intersected (helps with hover) +ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { + var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. + if (!pickedOverlay.intersects) { + if (noHit) { + return noHit(); + } + return; + } + ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours. + if ((overlay.activeOverlay) === pickedOverlay.overlayID) { + hit(overlay); + return true; + } + }); +}; + + +// +// Similar, for entities +// +function HighlightedEntity(id, entityProperties) { + this.id = id; + this.overlay = Overlays.addOverlay('cube', { + position: entityProperties.position, + rotation: entityProperties.rotation, + dimensions: entityProperties.dimensions, + solid: false, + color: { + red: 0xF3, + green: 0x91, + blue: 0x29 + }, + ignoreRayIntersection: true, + drawInFront: false // Arguable. For now, let's not distract with mysterious wires around the scene. + }); + HighlightedEntity.overlays.push(this); +} +HighlightedEntity.overlays = []; +HighlightedEntity.clearOverlays = function clearHighlightedEntities() { + HighlightedEntity.overlays.forEach(function (highlighted) { + Overlays.deleteOverlay(highlighted.overlay); + }); + HighlightedEntity.overlays = []; +}; +HighlightedEntity.updateOverlays = function updateHighlightedEntities() { + HighlightedEntity.overlays.forEach(function (highlighted) { + var properties = Entities.getEntityProperties(highlighted.id, ['position', 'rotation', 'dimensions']); + Overlays.editOverlay(highlighted.overlay, { + position: properties.position, + rotation: properties.rotation, + dimensions: properties.dimensions + }); + }); +}; + +/* this contains current gain for a given node (by session id). More efficient than + * querying it, plus there isn't a getGain function so why write one */ +var sessionGains = {}; +function convertDbToLinear(decibels) { + // +20db = 10x, 0dB = 1x, -10dB = 0.1x, etc... + // but, your perception is that something 2x as loud is +10db + // so we go from -60 to +20 or 1/64x to 4x. For now, we can + // maybe scale the signal this way?? + return Math.pow(2, decibels / 10.0); +} +function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. + var data; + switch (message.method) { + case 'selected': + selectedIds = message.params; + ExtendedOverlay.some(function (overlay) { + var id = overlay.key; + var selected = ExtendedOverlay.isSelected(id); + overlay.select(selected); + }); + + HighlightedEntity.clearOverlays(); + if (selectedIds.length) { + Entities.findEntitiesInFrustum(Camera.frustum).forEach(function (id) { + // Because lastEditedBy is per session, the vast majority of entities won't match, + // so it would probably be worth reducing marshalling costs by asking for just we need. + // However, providing property name(s) is advisory and some additional properties are + // included anyway. As it turns out, asking for 'lastEditedBy' gives 'position', 'rotation', + // and 'dimensions', too, so we might as well make use of them instead of making a second + // getEntityProperties call. + // It would be nice if we could harden this against future changes by specifying all + // and only these four in an array, but see + // https://highfidelity.fogbugz.com/f/cases/2728/Entities-getEntityProperties-id-lastEditedBy-name-lastEditedBy-doesn-t-work + var properties = Entities.getEntityProperties(id, 'lastEditedBy'); + if (ExtendedOverlay.isSelected(properties.lastEditedBy)) { + new HighlightedEntity(id, properties); + } + }); + } + break; + case 'refreshNearby': + data = {}; + ExtendedOverlay.some(function (overlay) { // capture the audio data + data[overlay.key] = overlay; + }); + removeOverlays(); + // If filter is specified from .qml instead of through settings, update the settings. + if (message.params.filter !== undefined) { + Settings.setValue('pal/filtered', !!message.params.filter); + } + populateNearbyUserList(message.params.selected, data); + UserActivityLogger.palAction("refresh_nearby", ""); + break; + case 'refreshConnections': + print('Refreshing Connections...'); + getConnectionData(false); + UserActivityLogger.palAction("refresh_connections", ""); + break; + case 'removeConnection': + connectionUserName = message.params; + request({ + uri: METAVERSE_BASE + '/api/v1/user/connections/' + connectionUserName, + method: 'DELETE' + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to remove connection", connectionUserName, error || response.status); + return; + } + getConnectionData(false); + }); + break + + case 'removeFriend': + friendUserName = message.params; + print("Removing " + friendUserName + " from friends."); + request({ + uri: METAVERSE_BASE + '/api/v1/user/friends/' + friendUserName, + method: 'DELETE' + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to unfriend " + friendUserName, error || response.status); + return; + } + getConnectionData(friendUserName); + }); + break + case 'addFriend': + friendUserName = message.params; + print("Adding " + friendUserName + " to friends."); + request({ + uri: METAVERSE_BASE + '/api/v1/user/friends', + method: 'POST', + json: true, + body: { + username: friendUserName, + } + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to friend " + friendUserName, error || response.status); + return; + } + getConnectionData(friendUserName); + } + ); + break; + default: + print('Unrecognized message from Pal.qml:', JSON.stringify(message)); + } +} + +function sendToQml(message) { + tablet.sendToQml(message); +} +function updateUser(data) { + print('PAL update:', JSON.stringify(data)); + sendToQml({ method: 'updateUsername', params: data }); +} +// +// User management services +// +// These are prototype versions that will be changed when the back end changes. +var METAVERSE_BASE = Account.metaverseServerURL; + +function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise. + request({ + uri: url + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to get", url, error || response.status); + return; + } + callback(response.data); + }); +} +function getProfilePicture(username, callback) { // callback(url) if successfull. (Logs otherwise) + // FIXME Prototype scrapes profile picture. We should include in user status, and also make available somewhere for myself + request({ + uri: METAVERSE_BASE + '/users/' + username + }, function (error, html) { + var matched = !error && html.match(/img class="users-img" src="([^"]*)"/); + if (!matched) { + print('Error: Unable to get profile picture for', username, error); + callback(''); + return; + } + callback(matched[1]); + }); +} +function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) + url = METAVERSE_BASE + '/api/v1/users?' + if (domain) { + url += 'status=' + domain.slice(1, -1); // without curly braces + } else { + url += 'filter=connections'; // regardless of whether online + } + requestJSON(url, function (connectionsData) { + callback(connectionsData.users); + }); +} +function getInfoAboutUser(specificUsername, callback) { + url = METAVERSE_BASE + '/api/v1/users?filter=connections' + requestJSON(url, function (connectionsData) { + for (user in connectionsData.users) { + if (connectionsData.users[user].username === specificUsername) { + callback(connectionsData.users[user]); + return; + } + } + callback(false); + }); +} +function getConnectionData(specificUsername, domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick. + function frob(user) { // get into the right format + var formattedSessionId = user.location.node_id || ''; + if (formattedSessionId !== '' && formattedSessionId.indexOf("{") != 0) { + formattedSessionId = "{" + formattedSessionId + "}"; + } + return { + sessionId: formattedSessionId, + userName: user.username, + connection: user.connection, + profileUrl: user.images.thumbnail, + placeName: (user.location.root || user.location.domain || {}).name || '' + }; + } + if (specificUsername) { + getInfoAboutUser(specificUsername, function (user) { + if (user) { + updateUser(frob(user)); + } else { + print('Error: Unable to find information about ' + specificUsername + ' in connectionsData!'); + } + }); + } else { + getAvailableConnections(domain, function (users) { + if (domain) { + users.forEach(function (user) { + updateUser(frob(user)); + }); + } else { + sendToQml({ method: 'connections', params: users.map(frob) }); + } + }); + } +} + +// +// Main operations. +// +function addAvatarNode(id) { + var selected = ExtendedOverlay.isSelected(id); + return new ExtendedOverlay(id, "sphere", { + drawInFront: true, + solid: true, + alpha: 0.8, + color: color(selected, false, 0.0), + ignoreRayIntersection: false + }, selected, !conserveResources); +} +// Each open/refresh will capture a stable set of avatarsOfInterest, within the specified filter. +var avatarsOfInterest = {}; +function populateNearbyUserList(selectData, oldAudioData) { + var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')}, + data = [], + avatars = AvatarList.getAvatarIdentifiers(), + myPosition = filter && Camera.position, + frustum = filter && Camera.frustum, + verticalHalfAngle = filter && (frustum.fieldOfView / 2), + horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio), + orientation = filter && Camera.orientation, + forward = filter && Quat.getForward(orientation), + verticalAngleNormal = filter && Quat.getRight(orientation), + horizontalAngleNormal = filter && Quat.getUp(orientation); + avatarsOfInterest = {}; + avatars.forEach(function (id) { + var avatar = AvatarList.getAvatar(id); + var name = avatar.sessionDisplayName; + if (!name) { + // Either we got a data packet but no identity yet, or something is really messed up. In any case, + // we won't be able to do anything with this user, so don't include them. + // In normal circumstances, a refresh will bring in the new user, but if we're very heavily loaded, + // we could be losing and gaining people randomly. + print('No avatar identity data for', id); + return; + } + if (id && myPosition && (Vec3.distance(avatar.position, myPosition) > filter.distance)) { + return; + } + var normal = id && filter && Vec3.normalize(Vec3.subtract(avatar.position, myPosition)); + var horizontal = normal && angleBetweenVectorsInPlane(normal, forward, horizontalAngleNormal); + var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal); + if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) { + return; + } + var oldAudio = oldAudioData && oldAudioData[id]; + var avatarPalDatum = { + profileUrl: '', + displayName: name, + userName: '', + connection: '', + sessionId: id || '', + audioLevel: (oldAudio && oldAudio.audioLevel) || 0.0, + avgAudioLevel: (oldAudio && oldAudio.avgAudioLevel) || 0.0, + admin: false, + personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null + ignore: !!id && Users.getIgnoreStatus(id), // ditto + isPresent: true, + isReplicated: avatar.isReplicated + }; + // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. + Users.requestUsernameFromID(id); + if (id) { + addAvatarNode(id); // No overlay for ourselves + avatarsOfInterest[id] = true; + } else { + // Return our username from the Account API + avatarPalDatum.userName = Account.username; + } + data.push(avatarPalDatum); + print('PAL data:', JSON.stringify(avatarPalDatum)); + }); + getConnectionData(false, location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain). + conserveResources = Object.keys(avatarsOfInterest).length > 20; + sendToQml({ method: 'nearbyUsers', params: data }); + if (selectData) { + selectData[2] = true; + sendToQml({ method: 'select', params: selectData }); + } +} + +// The function that handles the reply from the server +function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { + var data = { + sessionId: (MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially. + // If we get username (e.g., if in future we receive it when we're friends), use it. + // Otherwise, use valid machineFingerprint (which is not valid when not an admin). + userName: username || (Users.canKick && machineFingerprint) || '', + admin: isAdmin + }; + // Ship the data off to QML + updateUser(data); +} + +var pingPong = true; +function updateOverlays() { + var eye = Camera.position; + AvatarList.getAvatarIdentifiers().forEach(function (id) { + if (!id || !avatarsOfInterest[id]) { + return; // don't update ourself, or avatars we're not interested in + } + var avatar = AvatarList.getAvatar(id); + if (!avatar) { + return; // will be deleted below if there had been an overlay. + } + var overlay = ExtendedOverlay.get(id); + if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. + print('Adding non-PAL avatar node', id); + overlay = addAvatarNode(id); + } + var target = avatar.position; + var distance = Vec3.distance(target, eye); + var offset = 0.2; + var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) + var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can + if (headIndex > 0) { + offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; + } + + // move a bit in front, towards the camera + target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); + + // now bump it up a bit + target.y = target.y + offset; + + overlay.ping = pingPong; + overlay.editOverlay({ + color: color(ExtendedOverlay.isSelected(id), overlay.hovering, overlay.audioLevel), + position: target, + dimensions: 0.032 * distance + }); + if (overlay.model) { + overlay.model.ping = pingPong; + overlay.model.editOverlay({ + position: target, + scale: 0.2 * distance, // constant apparent size + rotation: Camera.orientation + }); + } + }); + pingPong = !pingPong; + ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) + if (overlay.ping === pingPong) { + overlay.deleteOverlay(); + } + }); + // We could re-populateNearbyUserList if anything added or removed, but not for now. + HighlightedEntity.updateOverlays(); +} +function removeOverlays() { + selectedIds = []; + lastHoveringId = 0; + HighlightedEntity.clearOverlays(); + ExtendedOverlay.some(function (overlay) { + overlay.deleteOverlay(); + }); +} + +// +// Clicks. +// +function handleClick(pickRay) { + ExtendedOverlay.applyPickRay(pickRay, function (overlay) { + // Don't select directly. Tell qml, who will give us back a list of ids. + var message = {method: 'select', params: [[overlay.key], !overlay.selected, false]}; + sendToQml(message); + return true; + }); +} +function handleMouseEvent(mousePressEvent) { // handleClick if we get one. + if (!mousePressEvent.isLeftButton) { + return; + } + handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y)); +} +function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic + ExtendedOverlay.applyPickRay(pickRay, function (overlay) { + overlay.hover(true); + }, function () { + ExtendedOverlay.unHover(); + }); +} + +// handy global to keep track of which hand is the mouse (if any) +var currentHandPressed = 0; +var TRIGGER_CLICK_THRESHOLD = 0.85; +var TRIGGER_PRESS_THRESHOLD = 0.05; + +function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position + var pickRay; + if (HMD.active) { + if (currentHandPressed !== 0) { + pickRay = controllerComputePickRay(currentHandPressed); + } else { + // nothing should hover, so + ExtendedOverlay.unHover(); + return; + } + } else { + pickRay = Camera.computePickRay(event.x, event.y); + } + handleMouseMove(pickRay); +} +function handleTriggerPressed(hand, value) { + // The idea is if you press one trigger, it is the one + // we will consider the mouse. Even if the other is pressed, + // we ignore it until this one is no longer pressed. + var isPressed = value > TRIGGER_PRESS_THRESHOLD; + if (currentHandPressed === 0) { + currentHandPressed = isPressed ? hand : 0; + return; + } + if (currentHandPressed === hand) { + currentHandPressed = isPressed ? hand : 0; + return; + } + // otherwise, the other hand is still triggered + // so do nothing. +} + +// We get mouseMoveEvents from the handControllers, via handControllerPointer. +// But we don't get mousePressEvents. +var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); +var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); +function controllerComputePickRay(hand) { + var controllerPose = getControllerWorldLocation(hand, true); + if (controllerPose.valid) { + return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) }; + } +} +function makeClickHandler(hand) { + return function (clicked) { + if (clicked > TRIGGER_CLICK_THRESHOLD) { + var pickRay = controllerComputePickRay(hand); + handleClick(pickRay); + } + }; +} +function makePressHandler(hand) { + return function (value) { + handleTriggerPressed(hand, value); + }; +} +triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); +triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); +triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); +triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); +// +// Manage the connection between the button and the window. +// +var button; +var buttonName = "AvatarApp"; +var tablet = null; + +function startup() { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + text: buttonName, + icon: "icons/tablet-icons/people-i.svg", + activeIcon: "icons/tablet-icons/people-a.svg", + sortOrder: 7 + }); + button.clicked.connect(onTabletButtonClicked); + tablet.screenChanged.connect(onTabletScreenChanged); + Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); + Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); + Messages.subscribe(CHANNEL); + Messages.messageReceived.connect(receiveMessage); + Users.avatarDisconnected.connect(avatarDisconnected); + AvatarList.avatarAddedEvent.connect(avatarAdded); + AvatarList.avatarRemovedEvent.connect(avatarRemoved); + AvatarList.avatarSessionChangedEvent.connect(avatarSessionChanged); +} + +startup(); + +var isWired = false; +var audioTimer; +var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too) +var AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS = 300; +function off() { + if (isWired) { // It is not ok to disconnect these twice, hence guard. + Script.update.disconnect(updateOverlays); + Controller.mousePressEvent.disconnect(handleMouseEvent); + Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); + tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); + Users.usernameFromIDReply.disconnect(usernameFromIDReply); + isWired = false; + ContextOverlay.enabled = true + } + if (audioTimer) { + Script.clearInterval(audioTimer); + } + triggerMapping.disable(); // It's ok if we disable twice. + triggerPressMapping.disable(); // see above + removeOverlays(); + Users.requestsDomainListData = false; +} + +function tabletVisibilityChanged() { + if (!tablet.tabletShown) { + ContextOverlay.enabled = true; + tablet.gotoHomeScreen(); + } +} + +var onPalScreen = false; + +function onTabletButtonClicked() { + if (onPalScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + ContextOverlay.enabled = true; + } else { + ContextOverlay.enabled = false; + tablet.loadQMLSource(PAL_QML_SOURCE); + tablet.tabletShownChanged.connect(tabletVisibilityChanged); + Users.requestsDomainListData = true; + populateNearbyUserList(); + isWired = true; + Script.update.connect(updateOverlays); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + Users.usernameFromIDReply.connect(usernameFromIDReply); + triggerMapping.enable(); + triggerPressMapping.enable(); + audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); + } +} +var hasEventBridge = false; +function wireEventBridge(on) { + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } +} + +function onTabletScreenChanged(type, url) { + onPalScreen = (type === "QML" && url === PAL_QML_SOURCE); + wireEventBridge(onPalScreen); + // for toolbar mode: change button to active when window is first openend, false otherwise. + button.editProperties({isActive: onPalScreen}); + + // disable sphere overlays when not on pal screen. + if (!onPalScreen) { + off(); + } +} + +// +// Message from other scripts, such as edit.js +// +var CHANNEL = 'com.highfidelity.pal'; +function receiveMessage(channel, messageString, senderID) { + if ((channel !== CHANNEL) || (senderID !== MyAvatar.sessionUUID)) { + return; + } + var message = JSON.parse(messageString); + switch (message.method) { + case 'select': + sendToQml(message); // Accepts objects, not just strings. + break; + default: + print('Unrecognized PAL message', messageString); + } +} + +var AVERAGING_RATIO = 0.05; +var LOUDNESS_FLOOR = 11.0; +var LOUDNESS_SCALE = 2.8 / 5.0; +var LOG2 = Math.log(2.0); +var AUDIO_PEAK_DECAY = 0.02; +var myData = {}; // we're not includied in ExtendedOverlay.get. + +function scaleAudio(val) { + var audioLevel = 0.0; + if (val <= LOUDNESS_FLOOR) { + audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE; + } else { + audioLevel = (val - (LOUDNESS_FLOOR - 1)) * LOUDNESS_SCALE; + } + if (audioLevel > 1.0) { + audioLevel = 1; + } + return audioLevel; +} + +function getAudioLevel(id) { + // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged + // But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency + // of updating (the latter for efficiency too). + var avatar = AvatarList.getAvatar(id); + var audioLevel = 0.0; + var avgAudioLevel = 0.0; + var data = id ? ExtendedOverlay.get(id) : myData; + if (data) { + + // we will do exponential moving average by taking some the last loudness and averaging + data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness); + + // add 1 to insure we don't go log() and hit -infinity. Math.log is + // natural log, so to get log base 2, just divide by ln(2). + audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); + + // decay avgAudioLevel + avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); + + data.avgAudioLevel = avgAudioLevel; + data.audioLevel = audioLevel; + + // now scale for the gain. Also, asked to boost the low end, so one simple way is + // to take sqrt of the value. Lets try that, see how it feels. + avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[id] || 0.75))); + } + return [audioLevel, avgAudioLevel]; +} + +function createAudioInterval(interval) { + // we will update the audioLevels periodically + // TODO: tune for efficiency - expecially with large numbers of avatars + return Script.setInterval(function () { + var param = {}; + AvatarList.getAvatarIdentifiers().forEach(function (id) { + var level = getAudioLevel(id), + userId = id || 0; // qml didn't like an object with null/empty string for a key, so... + param[userId] = level; + }); + sendToQml({method: 'updateAudioLevel', params: param}); + }, interval); +} + +function avatarDisconnected(nodeID) { + // remove from the pal list + sendToQml({method: 'avatarDisconnected', params: [nodeID]}); +} + +function clearLocalQMLDataAndClosePAL() { + sendToQml({ method: 'clearLocalQMLData' }); + if (onPalScreen) { + ContextOverlay.enabled = true; + tablet.gotoHomeScreen(); + } +} + +function avatarAdded(avatarID) { + sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarAdded'] }); +} + +function avatarRemoved(avatarID) { + sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarRemoved'] }); +} + +function avatarSessionChanged(avatarID) { + sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] }); +} + +function shutdown() { + if (onPalScreen) { + tablet.gotoHomeScreen(); + } + button.clicked.disconnect(onTabletButtonClicked); + tablet.removeButton(button); + tablet.screenChanged.disconnect(onTabletScreenChanged); + Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); + Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); + Messages.subscribe(CHANNEL); + Messages.messageReceived.disconnect(receiveMessage); + Users.avatarDisconnected.disconnect(avatarDisconnected); + AvatarList.avatarAddedEvent.disconnect(avatarAdded); + AvatarList.avatarRemovedEvent.disconnect(avatarRemoved); + AvatarList.avatarSessionChangedEvent.disconnect(avatarSessionChanged); + off(); +} + +// +// Cleanup. +// +Script.scriptEnding.connect(shutdown); + +}()); // END LOCAL_SCOPE From 8a8f599583640299e0e973b6b701f0f074b25115 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 12:58:55 +0300 Subject: [PATCH 040/182] rounded avatar images --- .../qml/hifi/avatarapp/AvatarThumbnail.qml | 1 + .../qml/hifi/avatarapp/RoundImage.qml | 34 +++++++++++++++ .../qml/hifi/avatarapp/ShadowImage.qml | 4 +- .../qml/hifi/avatarapp/TransparencyMask.qml | 43 +++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 interface/resources/qml/hifi/avatarapp/RoundImage.qml create mode 100644 interface/resources/qml/hifi/avatarapp/TransparencyMask.qml diff --git a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml index 3fd7bf22b7..d19e6395ed 100644 --- a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml +++ b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml @@ -18,6 +18,7 @@ Item { ShadowImage { id: avatarImage anchors.fill: parent + radius: 6 } AvatarWearablesIndicator { diff --git a/interface/resources/qml/hifi/avatarapp/RoundImage.qml b/interface/resources/qml/hifi/avatarapp/RoundImage.qml new file mode 100644 index 0000000000..378fe88ef5 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/RoundImage.qml @@ -0,0 +1,34 @@ +import QtQuick 2.0 + +Item { + + property alias border: borderRectangle.border + property alias source: image.source + property alias fillMode: image.fillMode + property alias radius: mask.radius + + Image { + id: image + anchors.fill: parent + anchors.margins: borderRectangle.border.width + } + + Rectangle { + id: mask + anchors.fill: image + } + + TransparencyMask { + anchors.fill: image + source: image + maskSource: mask + } + + Rectangle { + id: borderRectangle + anchors.fill: parent + + radius: mask.radius + color: "transparent" + } +} diff --git a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml index 8f8ad587e3..08552c12eb 100644 --- a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml +++ b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml @@ -7,11 +7,13 @@ Item { property alias dropShadowRadius: shadow.radius property alias dropShadowHorizontalOffset: shadow.horizontalOffset property alias dropShadowVerticalOffset: shadow.verticalOffset + property alias radius: image.radius - Image { + RoundImage { id: image width: parent.width height: parent.height + radius: 6 } DropShadow { diff --git a/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml b/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml new file mode 100644 index 0000000000..4884d1e1ad --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml @@ -0,0 +1,43 @@ +import QtQuick 2.0 + +Item { + property alias source: sourceImage.sourceItem + property alias maskSource: sourceMask.sourceItem + + anchors.fill: parent + ShaderEffectSource { + id: sourceMask + smooth: true + hideSource: true + } + ShaderEffectSource { + id: sourceImage + hideSource: true + } + + ShaderEffect { + id: maskEffect + anchors.fill: parent + + property variant source: sourceImage + property variant mask: sourceMask + + fragmentShader: { +" + varying highp vec2 qt_TexCoord0; + uniform lowp sampler2D source; + uniform lowp sampler2D mask; + void main() { + + highp vec4 maskColor = texture2D(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); + highp vec4 sourceColor = texture2D(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); + + if(maskColor.a > 0.0) + gl_FragColor = sourceColor; + else + gl_FragColor = maskColor; + } +" + } + } +} \ No newline at end of file From bd719286d8049a85263e3b88bb119646c427875c Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 13:10:58 +0300 Subject: [PATCH 041/182] move highlight under the green wearable indicator, use internal highlight of ShadowImage instead of extra Rectangle --- interface/resources/qml/hifi/AvatarApp.qml | 17 ++++++----------- .../qml/hifi/avatarapp/AvatarThumbnail.qml | 3 +++ .../qml/hifi/avatarapp/ShadowImage.qml | 1 + 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 38a9ea2bb9..a7eb96ed94 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -467,9 +467,13 @@ Rectangle { } ] + property bool highlighted: delegateRoot.GridView.isCurrentItem + AvatarThumbnail { id: favoriteAvatarImage imageUrl: url + border.color: container.highlighted ? style.colors.blueHighlight : 'transparent' + border.width: container.highlighted ? 2 : 0 wearablesCount: (wearables && wearables !== '') ? wearables.split('|').length : 0 onWearablesCountChanged: { console.debug('delegate: AvatarThumbnail.wearablesCount: ', wearablesCount) @@ -546,26 +550,17 @@ Rectangle { } } - Rectangle { - id: highlight - anchors.fill: favoriteAvatarImage - visible: delegateRoot.GridView.isCurrentItem - color: 'transparent' - border.width: 2 - border.color: style.colors.blueHighlight - } - Colorize { anchors.fill: favoriteAvatarImage source: favoriteAvatarImage saturation: 0.2 - visible: isInManageState && !highlight.visible + visible: isInManageState && !container.highlighted } HiFiGlyphs { anchors.fill: parent text: "{" - visible: isInManageState && !highlight.visible + visible: isInManageState && !container.highlighted horizontalAlignment: Text.AlignHCenter size: 56 } diff --git a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml index d19e6395ed..e70ee6af1d 100644 --- a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml +++ b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml @@ -3,6 +3,7 @@ import QtQuick 2.9 Item { width: 92 height: 92 + property alias wearableIndicator: indicator property int wearablesCount: 0 onWearablesCountChanged: { @@ -14,6 +15,7 @@ Item { property alias dropShadowVerticalOffset: avatarImage.dropShadowVerticalOffset property alias imageUrl: avatarImage.source + property alias border: avatarImage.border ShadowImage { id: avatarImage @@ -22,6 +24,7 @@ Item { } AvatarWearablesIndicator { + id: indicator anchors.left: avatarImage.left anchors.bottom: avatarImage.bottom anchors.leftMargin: 57 diff --git a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml index 08552c12eb..be2089048c 100644 --- a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml +++ b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml @@ -8,6 +8,7 @@ Item { property alias dropShadowHorizontalOffset: shadow.horizontalOffset property alias dropShadowVerticalOffset: shadow.verticalOffset property alias radius: image.radius + property alias border: image.border RoundImage { id: image From c54b2c8f26095f5ffe9fc21af2da24bad0594bfa Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 20:41:59 +0300 Subject: [PATCH 042/182] adjust avatar thumbnail image radius --- interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml index e70ee6af1d..39da0b67df 100644 --- a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml +++ b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml @@ -20,7 +20,7 @@ Item { ShadowImage { id: avatarImage anchors.fill: parent - radius: 6 + radius: 5 } AvatarWearablesIndicator { From bd48ff9ee6a71fc77b187cf611e6eb5eb30139c8 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 20:45:05 +0300 Subject: [PATCH 043/182] remove link glyph rotation --- interface/resources/qml/hifi/AvatarApp.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index a7eb96ed94..9caecd69c3 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -194,7 +194,6 @@ Rectangle { anchors.rightMargin: 30 anchors.verticalCenter: avatarNameLabel.verticalCenter glyphText: "." - glyphRotation: 45 MouseArea { anchors.fill: parent From 4d2a37aff2e7acf828f766cba3c99964adbaabe6 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 20:54:35 +0300 Subject: [PATCH 044/182] show add to favorites on clicking star --- interface/resources/qml/hifi/AvatarApp.qml | 27 +++++++++++----------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 9caecd69c3..89798c4969 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -135,7 +135,6 @@ Rectangle { Row { id: star - anchors.top: parent.top anchors.topMargin: 119 anchors.left: avatarImage.right @@ -153,21 +152,21 @@ Rectangle { TextStyle5 { text: isAvatarInFavorites ? avatarName : "Add to Favorites" anchors.verticalCenter: parent.verticalCenter + } + } - MouseArea { - enabled: !isAvatarInFavorites - anchors.fill: parent - onClicked: { - console.debug('selectedAvatar.url', selectedAvatar.url) - createFavorite.onSaveClicked = function() { - selectedAvatar.favorite = true; - pageOfAvatars.setProperty(view.currentIndex, 'favorite', selectedAvatar.favorite) - createFavorite.close(); - } - - createFavorite.open(selectedAvatar); - } + MouseArea { + enabled: !isAvatarInFavorites + anchors.fill: star + onClicked: { + console.debug('selectedAvatar.url', selectedAvatar.url) + createFavorite.onSaveClicked = function() { + selectedAvatar.favorite = true; + pageOfAvatars.setProperty(view.currentIndex, 'favorite', selectedAvatar.favorite) + createFavorite.close(); } + + createFavorite.open(selectedAvatar); } } From 3d4fb2d368483c08a8224a0d0a4610c67778168f Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 20:56:58 +0300 Subject: [PATCH 045/182] fix spelling error --- interface/resources/qml/hifi/AvatarApp.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 89798c4969..07033c591a 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -584,7 +584,7 @@ Rectangle { onClicked: { console.debug('getAvatarsUrl: ', getAvatarsUrl); - popup.button2text = 'BodyMarkt' + popup.button2text = 'BodyMart' popup.button1text = 'CANCEL' popup.titleText = 'Get Avatars' From e0a8d901411a753bc66e5de4514434f6745450c2 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 21:15:29 +0300 Subject: [PATCH 046/182] adjust link icon size --- interface/resources/qml/hifi/AvatarApp.qml | 1 + interface/resources/qml/hifi/avatarapp/SquareLabel.qml | 1 + 2 files changed, 2 insertions(+) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 07033c591a..db0e0a4957 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -193,6 +193,7 @@ Rectangle { anchors.rightMargin: 30 anchors.verticalCenter: avatarNameLabel.verticalCenter glyphText: "." + glyphSize: 22 MouseArea { anchors.fill: parent diff --git a/interface/resources/qml/hifi/avatarapp/SquareLabel.qml b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml index afe6c751f3..5c4bebd1c4 100644 --- a/interface/resources/qml/hifi/avatarapp/SquareLabel.qml +++ b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml @@ -8,6 +8,7 @@ ShadowRectangle { color: 'white' property alias glyphText: glyph.text property alias glyphRotation: glyph.rotation + property alias glyphSize: glyph.size radius: 3 border.color: 'black' From 10e5f03f5e9f3412c9520a9f166849a5f60ecbad Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 21:19:45 +0300 Subject: [PATCH 047/182] move selected avatar to the beginning of the page --- interface/resources/qml/hifi/AvatarApp.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index db0e0a4957..31ab589d81 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -39,7 +39,7 @@ Rectangle { } } - selectedAvatarId = allAvatars.get(1).url + selectedAvatarId = allAvatars.get(0).url console.debug('wearables: ', selectedAvatar.wearables) view.setPage(0) @@ -540,7 +540,9 @@ Rectangle { popup.onButton2Clicked = function() { selectedAvatarId = currentItem.url; popup.close(); - delegateRoot.GridView.view.currentIndex = index; + + pageOfAvatars.move(index, 0, 1); + delegateRoot.GridView.view.currentIndex = 0; } popup.open(); } From bdacd1b792b138eca7b82875febf2a2ca56999d6 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 22:32:19 +0300 Subject: [PATCH 048/182] left-to-right layout in favorites grid --- interface/resources/qml/hifi/AvatarApp.qml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 31ab589d81..97a4e6ef70 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -333,7 +333,7 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: 30 anchors.right: parent.right - anchors.rightMargin: 30 + anchors.rightMargin: 0 anchors.top: favoritesLabel.bottom anchors.topMargin: 9 @@ -345,6 +345,8 @@ Rectangle { interactive: false; currentIndex: (selectedAvatarId !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatar(selectedAvatarId) : -1 + property int horizontalSpacing: 18 + property int verticalSpacing: 36 AvatarsModel { id: allAvatars @@ -441,10 +443,10 @@ Rectangle { } } - flow: GridView.FlowTopToBottom + flow: GridView.FlowLeftToRight - cellHeight: 92 + 36 - cellWidth: 92 + 18 + cellHeight: 92 + verticalSpacing + cellWidth: 92 + horizontalSpacing delegate: Item { id: delegateRoot From 8a5abb32a72cad8b355973be7db89f0377ad088f Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 22:33:21 +0300 Subject: [PATCH 049/182] rework avatar selection logic based on changed requirements: selecte avatar always goes to the beginning of the list, selected page auto-switches to the first one --- interface/resources/qml/hifi/AvatarApp.qml | 46 ++++++++++++++-------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 97a4e6ef70..650242278b 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -22,6 +22,10 @@ Rectangle { console.debug('selectedAvatar: ', selectedAvatar ? selectedAvatar.url : selectedAvatar) } + function isEqualById(avatar, avatarId) { + return (avatar.url + avatar.name) === avatarId + } + property string avatarName: selectedAvatar ? selectedAvatar.name : '' property string avatarUrl: selectedAvatar ? selectedAvatar.url : null property int avatarWearablesCount: selectedAvatar && selectedAvatar.wearables !== '' ? selectedAvatar.wearables.split('|').length : 0 @@ -39,11 +43,7 @@ Rectangle { } } - selectedAvatarId = allAvatars.get(0).url - console.debug('wearables: ', selectedAvatar.wearables) - - view.setPage(0) - console.debug('view.currentIndex: ', view.currentIndex); + view.selectAvatar(allAvatars.get(1)); } AvatarAppStyle { @@ -347,20 +347,35 @@ Rectangle { property int horizontalSpacing: 18 property int verticalSpacing: 36 + + function selectAvatar(avatar) { + selectedAvatarId = avatar.url + avatar.name; + var avatarIndex = allAvatars.findAvatarIndex(selectedAvatarId); + allAvatars.move(avatarIndex, 0, 1); + view.setPage(0); + } + AvatarsModel { id: allAvatars + function findAvatarIndex(avatarId) { + for(var i = 0; i < count; ++i) { + if(isEqualById(get(i), avatarId)) { + console.debug('avatar found by index: ', i) + return i; + } + } + return -1; + } + function findAvatar(avatarId) { console.debug('AvatarsModel: find avatar by', avatarId); - for(var i = 0; i < count; ++i) { - if(get(i).url === avatarId) { - console.debug('avatar found by index: ', i) - return get(i); - } - } + var avatarIndex = findAvatarIndex(avatarId); + if(avatarIndex === -1) + return undefined; - return -1; + return get(avatarIndex); } } @@ -418,7 +433,7 @@ Rectangle { console.debug('pageOfAvatars.findAvatar: ', avatarId); for(var i = 0; i < count; ++i) { - if(get(i).url === avatarId) { + if(isEqualById(get(i), avatarId)) { console.debug('avatar found by index: ', i) return i; } @@ -540,11 +555,8 @@ Rectangle { popup.bodyText = 'This will switch your current avatar and ararables that you are wearing with a new avatar and wearables.' popup.imageSource = null; popup.onButton2Clicked = function() { - selectedAvatarId = currentItem.url; popup.close(); - - pageOfAvatars.move(index, 0, 1); - delegateRoot.GridView.view.currentIndex = 0; + view.selectAvatar(currentItem); } popup.open(); } From 1916df6c81a4269714ce688d132599e77f51d140 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 22:38:38 +0300 Subject: [PATCH 050/182] specify border radius for 'get more avatars' --- interface/resources/qml/hifi/AvatarApp.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 650242278b..d15c0cd64b 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -583,6 +583,7 @@ Rectangle { ShadowRectangle { width: 92 height: 92 + radius: 5 color: style.colors.blueHighlight visible: url === '' From f8917a835a80b81ed983c6af648f1bdd4cda9307 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 22:50:02 +0300 Subject: [PATCH 051/182] hide page indicator when only one page --- interface/resources/qml/hifi/AvatarApp.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index d15c0cd64b..ec6bc7a4ca 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -646,6 +646,7 @@ Rectangle { text: "\ue01d"; size: 50 color: view.hasPrev ? 'black' : 'gray' + visible: view.hasNext || view.hasPrev horizontalAlignment: Text.AlignHCenter MouseArea { anchors.fill: parent @@ -662,6 +663,7 @@ Rectangle { text: "\ue01d"; size: 50 color: view.hasNext ? 'black' : 'gray' + visible: view.hasNext || view.hasPrev horizontalAlignment: Text.AlignHCenter MouseArea { anchors.fill: parent From 3acde995874833be52884fe9ff494fe6985011af Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 22:56:42 +0300 Subject: [PATCH 052/182] hide 'get all avatars' while in 'manage' state --- interface/resources/qml/hifi/AvatarApp.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index ec6bc7a4ca..fe26915cdd 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -570,12 +570,13 @@ Rectangle { source: favoriteAvatarImage saturation: 0.2 visible: isInManageState && !container.highlighted + visible: isInManageState && !container.highlighted && url !== '' } HiFiGlyphs { anchors.fill: parent text: "{" - visible: isInManageState && !container.highlighted + visible: isInManageState && !container.highlighted && url !== '' horizontalAlignment: Text.AlignHCenter size: 56 } @@ -585,7 +586,7 @@ Rectangle { height: 92 radius: 5 color: style.colors.blueHighlight - visible: url === '' + visible: url === '' && !isInManageState HiFiGlyphs { anchors.centerIn: parent @@ -632,6 +633,7 @@ Rectangle { horizontalAlignment: Text.AlignHCenter wrapMode: Text.WrapAtWordBoundaryOrAnywhere text: name + visible: url !== '' || !isInManageState } } } From 34693a856216e8ad6d254a8ce3d9408f8b3027a9 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 27 Apr 2018 23:45:25 +0300 Subject: [PATCH 053/182] use correct overlay for avatars in management state --- interface/resources/qml/hifi/AvatarApp.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index fe26915cdd..646fe2d2bd 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -565,11 +565,11 @@ Rectangle { } } - Colorize { + Rectangle { anchors.fill: favoriteAvatarImage - source: favoriteAvatarImage - saturation: 0.2 - visible: isInManageState && !container.highlighted + color: '#AFAFAF' + opacity: 0.4 + radius: 5 visible: isInManageState && !container.highlighted && url !== '' } From d3848ea0c5cf76a9ec11715e9617ea1d4d4ad5ec Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sat, 28 Apr 2018 00:01:58 +0300 Subject: [PATCH 054/182] spelling issue fix --- interface/resources/qml/hifi/AvatarApp.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 646fe2d2bd..51a363b28b 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -552,7 +552,7 @@ Rectangle { popup.button2text = 'CONFIRM' popup.button1text = 'CANCEL' popup.titleText = 'Load Favorite: {AvatarName}'.replace('{AvatarName}', currentItem.name) - popup.bodyText = 'This will switch your current avatar and ararables that you are wearing with a new avatar and wearables.' + popup.bodyText = 'This will switch your current avatar and wearables that you are wearing with a new avatar and wearables.' popup.imageSource = null; popup.onButton2Clicked = function() { popup.close(); From cdc9a325ebcd888bafcba0ebcf43d4788925e7b3 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sat, 28 Apr 2018 00:05:20 +0300 Subject: [PATCH 055/182] use standard radiobuttons with light color scheme --- .../resources/qml/hifi/avatarapp/Settings.qml | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index 1ff8cdde28..a8703b901e 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -161,16 +161,9 @@ Rectangle { ButtonGroup.group: leftRight checked: true + colorScheme: hifi.colorSchemes.light text: "Left hand" boxSize: 20 - - contentItem: TextStyle9 { - text: leftHandRadioButton.text - color: 'black' - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - leftPadding: leftHandRadioButton.indicator.width + leftHandRadioButton.spacing - } } HifiControlsUit.RadioButton { @@ -180,16 +173,9 @@ Rectangle { Layout.column: 2 ButtonGroup.group: leftRight + colorScheme: hifi.colorSchemes.light text: "Right hand" boxSize: 20 - - contentItem: TextStyle9 { - text: rightHandRadioButton.text - color: 'black' - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - leftPadding: rightHandRadioButton.indicator.width + rightHandRadioButton.spacing - } } RalewaySemiBold { @@ -210,20 +196,17 @@ Rectangle { Layout.row: 1 Layout.column: 1 Layout.leftMargin: -18 - ButtonGroup.group: onOff + + colorScheme: hifi.colorSchemes.light checked: true text: "ON" boxSize: 20 + } - contentItem: TextStyle9 { - text: onRadioButton.text - color: 'black' - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - leftPadding: onRadioButton.indicator.width + onRadioButton.spacing - } + HifiConstants { + id: hifi } HifiControlsUit.RadioButton { @@ -232,17 +215,10 @@ Rectangle { Layout.row: 1 Layout.column: 2 ButtonGroup.group: onOff + colorScheme: hifi.colorSchemes.light text: "OFF" boxSize: 20 - - contentItem: TextStyle9 { - text: offRadioButton.text - color: 'black' - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - leftPadding: offRadioButton.indicator.width + offRadioButton.spacing - } } } From 7723d0f306fa34f3641f24f81c46bb3b8714a345 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sat, 28 Apr 2018 00:12:23 +0300 Subject: [PATCH 056/182] turn all input controls to uit-based --- .../qml/hifi/avatarapp/InputTextStyle4.qml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml b/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml index 1e064e7a18..3b1ea411e4 100644 --- a/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml +++ b/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml @@ -1,23 +1,15 @@ -import "../../controls" as HifiControls +import "../../controls-uit" as HifiControlsUit import "../../styles-uit" import QtQuick 2.0 import QtQuick.Controls 2.2 -TextField { +HifiControlsUit.TextField { id: control font.family: "Fira Sans" font.pixelSize: 15; - color: 'black' AvatarAppStyle { id: style } - - background: Rectangle { - implicitWidth: 200 - implicitHeight: 40 - color: style.colors.inputFieldBackground - border.color: style.colors.lightGray - } -} \ No newline at end of file +} From 00b54c3f4620e87b1fc710fe84a2c2e73e496566 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sat, 28 Apr 2018 00:18:19 +0300 Subject: [PATCH 057/182] disable hover effect for selected avatar --- interface/resources/qml/hifi/AvatarApp.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 51a363b28b..23cb965473 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -500,7 +500,9 @@ Rectangle { MouseArea { id: favoriteAvatarMouseArea anchors.fill: parent - hoverEnabled: true + enabled: !container.highlighted + hoverEnabled: enabled + property url getWearablesUrl: '../../images/samples/hifi-place-77312e4b-6f48-4eb4-87e2-50444d8e56d1.png' onClicked: { From 4bb517f620afdf41323fb0885dd7eb2cbff266d4 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sat, 28 Apr 2018 00:47:32 +0300 Subject: [PATCH 058/182] reimplement 'text input with pencil' based on QQC2 TextField --- interface/resources/qml/hifi/AvatarApp.qml | 35 +++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 23cb965473..90a3b9499c 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -97,15 +97,48 @@ Rectangle { text: 'Display Name' } - InputTextStyle4 { + TextField { + id: displayNameInput anchors.left: displayNameLabel.right anchors.leftMargin: 30 anchors.verticalCenter: displayNameLabel.verticalCenter anchors.right: parent.right anchors.rightMargin: 36 width: 232 + + property bool error: text === ''; text: 'ThisIsDisplayName' + states: [ + State { + name: "hovered" + when: displayNameInput.hovered && !displayNameInput.focus && !displayNameInput.error; + PropertyChanges { target: displayNameInputBackground; color: '#afafaf' } + }, + State { + name: "focused" + when: displayNameInput.focus && !displayNameInput.error + PropertyChanges { target: displayNameInputBackground; color: '#f2f2f2' } + PropertyChanges { target: displayNameInputBackground; border.color: '#00b4ef' } + }, + State { + name: "error" + when: displayNameInput.error + PropertyChanges { target: displayNameInputBackground; color: '#f2f2f2' } + PropertyChanges { target: displayNameInputBackground; border.color: '#e84e62' } + } + ] + + background: Rectangle { + id: displayNameInputBackground + implicitWidth: 200 + implicitHeight: 40 + color: '#d4d4d4' + border.color: '#afafaf' + border.width: 1 + radius: 2 + } + HiFiGlyphs { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter From 6059ef2904cb0979a6239a5d3836238d5c6a2a11 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sat, 28 Apr 2018 01:11:16 +0300 Subject: [PATCH 059/182] a few updates to adjust wearables & settings pages --- interface/resources/qml/hifi/AvatarApp.qml | 42 +++++++++++++++---- .../qml/hifi/avatarapp/AdjustWearables.qml | 20 +++------ .../resources/qml/hifi/avatarapp/Settings.qml | 14 ++++--- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 90a3b9499c..e27b11cfae 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -54,10 +54,25 @@ Rectangle { id: header z: 100 - pageTitle: !settings.visible ? "Avatar" : "Avatar Settings" - avatarIconVisible: !settings.visible - settingsButtonVisible: !settings.visible + property string currentPage: "Avatar" + property bool mainPageVisible: !settings.visible && !adjustWearables.visible + Binding on currentPage { + when: settings.visible + value: "Avatar Settings" + } + Binding on currentPage { + when: adjustWearables.visible + value: "Adjust Wearables" + } + Binding on currentPage { + when: header.mainPageVisible + value: "Avatar" + } + + pageTitle: currentPage + avatarIconVisible: mainPageVisible + settingsButtonVisible: mainPageVisible onSettingsClicked: { settings.open(); } @@ -65,6 +80,12 @@ Rectangle { Settings { id: settings + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: parent.bottom + + z: 3 onSaveClicked: function() { close(); @@ -74,6 +95,16 @@ Rectangle { } } + AdjustWearables { + id: adjustWearables + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: parent.bottom + + z: 3 + } + Rectangle { id: mainBlock anchors.left: parent.left @@ -716,11 +747,6 @@ Rectangle { } } - AdjustWearables { - id: adjustWearables - z: 2 - } - MessageBox { id: popup } diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml index c195a4c2b5..9a2fc3bc72 100644 --- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -9,7 +9,7 @@ Rectangle { visible: false; width: 480 height: 706 - color: 'lightgray' + color: 'white' property bool modified: false; Component.onCompleted: { @@ -50,18 +50,6 @@ Rectangle { spacing: 20 width: parent.width - 30 * 2 - TextStyle5 { - anchors.horizontalCenter: parent.horizontalCenter - text: "Adjust Wearables" - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - height: 2 - color: 'gray' - } - HifiControlsUit.ComboBox { anchors.left: parent.left anchors.right: parent.right @@ -91,6 +79,8 @@ Rectangle { Vector3 { id: position + backgroundColor: "lightgray" + onXvalueChanged: modified = true; onYvalueChanged: modified = true; onZvalueChanged: modified = true; @@ -115,6 +105,8 @@ Rectangle { Vector3 { id: rotation + backgroundColor: "lightgray" + onXvalueChanged: modified = true; onYvalueChanged: modified = true; onZvalueChanged: modified = true; @@ -136,7 +128,7 @@ Rectangle { HifiControlsUit.SpinBox { id: scalespinner value: 0 - backgroundColor: "darkgray" + backgroundColor: "lightgray" width: position.spinboxWidth colorScheme: hifi.colorSchemes.light onValueChanged: modified = true; diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index a8703b901e..485ba2794a 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -10,12 +10,7 @@ Rectangle { id: settings color: 'white' - anchors.left: parent.left - anchors.right: parent.right - anchors.top: header.bottom - anchors.bottom: parent.bottom visible: false; - z: 3 property alias onSaveClicked: dialogButtons.onYesClicked property alias onCancelClicked: dialogButtons.onNoClicked @@ -28,6 +23,15 @@ Rectangle { visible = false } + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + Item { anchors.left: parent.left anchors.leftMargin: 27 From 5007c259bbd009c29833eed4b28a728a1386e724 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Tue, 1 May 2018 23:35:00 +0300 Subject: [PATCH 060/182] query real avatar favorites (but use fake urls for now) --- interface/resources/qml/hifi/AvatarApp.qml | 144 ++- .../qml/hifi/avatarapp/AvatarsModel.qml | 12 + interface/src/AvatarBookmarks.cpp | 37 +- interface/src/AvatarBookmarks.h | 4 +- interface/src/Bookmarks.cpp | 4 + interface/src/Bookmarks.h | 2 + interface/src/avatar/MyAvatar.h | 2 +- scripts/system/avatarapp.js | 856 ++---------------- 8 files changed, 207 insertions(+), 854 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index e27b11cfae..d0a35f8f11 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -12,38 +12,110 @@ Rectangle { height: 706 color: style.colors.white + property string getAvatarsMethod: 'getAvatars' + + signal sendToScript(var message); + function emitSendToScript(message) { + console.debug('AvatarApp.qml: emitting sendToScript: ', JSON.stringify(message, null, '\t')); + sendToScript(message); + } + + function fromScript(message) { + console.debug('AvatarApp.qml: fromScript: ', JSON.stringify(message, null, '\t')) + + if(message.method === 'initialize') { + emitSendToScript({'method' : getAvatarsMethod}); + } else if(message.method === getAvatarsMethod) { + var getAvatarsReply = message.reply; + var i = 0; + + for(var avatarName in getAvatarsReply.bookmarks) { + var avatarEntry = { + 'name' : avatarName, + 'url' : Qt.resolvedUrl(allAvatars.urls[i++ % allAvatars.urls.length]), + 'wearables' : '', + 'entry' : getAvatarsReply.bookmarks[avatarName] + }; + + allAvatars.append(avatarEntry); + } + + var currentAvatar = getAvatarsReply.currentAvatar; + console.debug('currentAvatar: ', JSON.stringify(currentAvatar, null, '\t')); + var selectedAvatarIndex = -1; + + // 2DO: find better way of determining selected avatar in bookmarks + console.debug('allAvatars.count: ', allAvatars.count); + for(var i = 0; i < allAvatars.count; ++i) { + var thesame = true; + for(var prop in currentAvatar) { + console.debug('prop', prop); + + var v1 = currentAvatar[prop]; + var v2 = allAvatars.get(i).entry[prop]; + console.debug('v1', v1, 'v2', v2); + + var s1 = JSON.stringify(v1); + var s2 = JSON.stringify(v2); + + console.debug('comparing\n', s1, 'to\n', s2, '...'); + if(s1 !== s2) { + if(!(Array.isArray(v1) && v1.length === 0 && v2 === undefined)) { + thesame = false; + break; + } + } + console.debug('values seems to be the same...'); + } + if(thesame) { + selectedAvatarIndex = i; + break; + } + } + + console.debug('selectedAvatarIndex = -1, avatar is not favorite') + + if(selectedAvatarIndex === -1) { + var currentAvatarEntry = { + 'name' : '', + 'url' : Qt.resolvedUrl(allAvatars.urls[i++ % allAvatars.urls.length]), + 'wearables' : '', + 'entry' : currentAvatar + }; + + selectedAvatar = currentAvatarEntry; + view.setPage(0); + + console.debug('selectedAvatar = ', JSON.stringify(selectedAvatar, null, '\t')) + } else { + view.selectAvatar(allAvatars.get(selectedAvatarIndex)); + } + } + } + property string selectedAvatarId: '' onSelectedAvatarIdChanged: { console.debug('selectedAvatarId: ', selectedAvatarId) + selectedAvatar = allAvatars.findAvatar(selectedAvatarId); } - property var selectedAvatar: selectedAvatarId !== '' ? allAvatars.findAvatar(selectedAvatarId) : undefined + property var selectedAvatar; onSelectedAvatarChanged: { - console.debug('selectedAvatar: ', selectedAvatar ? selectedAvatar.url : selectedAvatar) + console.debug('onSelectedAvatarChanged.selectedAvatar: ', JSON.stringify(selectedAvatar, null, '\t')); } function isEqualById(avatar, avatarId) { - return (avatar.url + avatar.name) === avatarId + return (avatar.name) === avatarId } property string avatarName: selectedAvatar ? selectedAvatar.name : '' property string avatarUrl: selectedAvatar ? selectedAvatar.url : null property int avatarWearablesCount: selectedAvatar && selectedAvatar.wearables !== '' ? selectedAvatar.wearables.split('|').length : 0 - property bool isAvatarInFavorites: selectedAvatar ? selectedAvatar.favorite : false + property bool isAvatarInFavorites: selectedAvatar ? allAvatars.findAvatar(selectedAvatar.name) !== undefined : false property bool isInManageState: false Component.onCompleted: { - for(var i = 0; i < allAvatars.count; ++i) { - var originalUrl = allAvatars.get(i).url; - if(originalUrl !== '') { - var resolvedUrl = Qt.resolvedUrl(originalUrl); - console.debug('url: ', originalUrl, 'resolved: ', resolvedUrl); - allAvatars.setProperty(i, 'url', resolvedUrl); - } - } - - view.selectAvatar(allAvatars.get(1)); } AvatarAppStyle { @@ -225,8 +297,10 @@ Rectangle { onClicked: { console.debug('selectedAvatar.url', selectedAvatar.url) createFavorite.onSaveClicked = function() { - selectedAvatar.favorite = true; - pageOfAvatars.setProperty(view.currentIndex, 'favorite', selectedAvatar.favorite) + var newAvatar = JSON.parse(JSON.stringify(selectedAvatar)); + newAvatar.name = createFavorite.favoriteNameText; + allAvatars.append(newAvatar); + view.selectAvatar(newAvatar); createFavorite.close(); } @@ -318,10 +392,9 @@ Rectangle { console.debug('adding avatar...'); var avatar = { - 'url': '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png', + 'url': Qt.resolvedUrl('../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png'), 'name': 'Lexi' + (++debug_newAvatarIndex), - 'wearables': '', - 'favorite': false + 'wearables': '' }; allAvatars.append(avatar) @@ -413,7 +486,7 @@ Rectangle { property int verticalSpacing: 36 function selectAvatar(avatar) { - selectedAvatarId = avatar.url + avatar.name; + selectedAvatarId = avatar.name; var avatarIndex = allAvatars.findAvatarIndex(selectedAvatarId); allAvatars.move(avatarIndex, 0, 1); view.setPage(0); @@ -491,7 +564,7 @@ Rectangle { id: pageOfAvatars property bool isUpdating: false; - property var getMoreAvatars: {'url' : '', 'name' : 'Get More Avatars'} + property var getMoreAvatarsEntry: {'url' : '', 'name' : '', 'getMoreAvatars' : true} function findAvatar(avatarId) { console.debug('pageOfAvatars.findAvatar: ', avatarId); @@ -507,11 +580,11 @@ Rectangle { } function appendGetAvatars() { - append(getMoreAvatars); + append(getMoreAvatarsEntry); } function hasGetAvatars() { - return count != 0 && get(count - 1).url === '' + return count != 0 && get(count - 1).getMoreAvatars } function removeGetAvatars() { @@ -554,12 +627,12 @@ Rectangle { imageUrl: url border.color: container.highlighted ? style.colors.blueHighlight : 'transparent' border.width: container.highlighted ? 2 : 0 - wearablesCount: (wearables && wearables !== '') ? wearables.split('|').length : 0 + wearablesCount: (!getMoreAvatars && wearables && wearables !== '') ? wearables.split('|').length : 0 onWearablesCountChanged: { console.debug('delegate: AvatarThumbnail.wearablesCount: ', wearablesCount) } - visible: url !== '' + visible: !getMoreAvatars MouseArea { id: favoriteAvatarMouseArea @@ -652,7 +725,7 @@ Rectangle { height: 92 radius: 5 color: style.colors.blueHighlight - visible: url === '' && !isInManageState + visible: getMoreAvatars && !isInManageState HiFiGlyphs { anchors.centerIn: parent @@ -698,8 +771,8 @@ Rectangle { verticalAlignment: Text.AlignTop horizontalAlignment: Text.AlignHCenter wrapMode: Text.WrapAtWordBoundaryOrAnywhere - text: name - visible: url !== '' || !isInManageState + text: getMoreAvatars ? 'Get More Avatars' : name + visible: !getMoreAvatars || !isInManageState } } } @@ -763,13 +836,6 @@ Rectangle { // color: 'green' visible: false - onVisibleChanged: { - if(visible) { - console.debug('selectedAvatar.wearables: ', selectedAvatar.wearables) - selectedAvatar.wearables = 'hat|sunglasses|bracelet' - pageOfAvatars.setProperty(view.currentIndex, 'wearables', selectedAvatar.wearables) - } - } Rectangle { width: 442 @@ -808,11 +874,13 @@ Rectangle { onClicked: { gotoAvatarAppPanel.visible = false; + var i = allAvatars.count + 1; + var url = allAvatars.urls[i++ % allAvatars.urls.length] + var avatar = { - 'url': '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png', + 'url': Qt.resolvedUrl(url), 'name': 'Lexi' + (++newAvatarIndex), - 'wearables': '', - 'favorite': false + 'wearables': 'hat|sunglasses|bracelet' }; allAvatars.append(avatar) diff --git a/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml b/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml index 5672e04731..d736737f3a 100644 --- a/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml +++ b/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml @@ -3,6 +3,17 @@ import QtQuick 2.9 ListModel { id: model + property var urls: [ + '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d.png', + '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-1.png', + '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png', + '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-3.png', + '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-4.png', + '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-5.png', + '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-5.png', + ] + + /* ListElement { url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d.png' name: 'Woody' @@ -45,4 +56,5 @@ ListModel { wearables: '' favorite: true } + */ } diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index f97c02bca3..e243890bc3 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -181,22 +181,31 @@ void AvatarBookmarks::addBookmark() { return; } - auto myAvatar = DependencyManager::get()->getMyAvatar(); - - const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); - const QVariant& avatarScale = myAvatar->getAvatarScale(); - - // If Avatar attachments ever change, this is where to update them, when saving remember to also append to AVATAR_BOOKMARK_VERSION - QVariantMap bookmark; - bookmark.insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION); - bookmark.insert(ENTRY_AVATAR_URL, avatarUrl); - bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale); - bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant()); - bookmark.insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); - - Bookmarks::addBookmarkToFile(bookmarkName, bookmark); + addBookmark(bookmarkName); }); +} +void AvatarBookmarks::addBookmark(QString bookmarkName) +{ + auto myAvatar = DependencyManager::get()->getMyAvatar(); + + const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); + const QVariant& avatarScale = myAvatar->getAvatarScale(); + + // If Avatar attachments ever change, this is where to update them, when saving remember to also append to AVATAR_BOOKMARK_VERSION + QVariantMap bookmark; + bookmark.insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION); + bookmark.insert(ENTRY_AVATAR_URL, avatarUrl); + bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale); + bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant()); + bookmark.insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); + + Bookmarks::addBookmarkToFile(bookmarkName, bookmark); +} + +void AvatarBookmarks::removeBookmark(QString bookmarkName) +{ + Bookmarks::deleteBookmark(bookmarkName); } void AvatarBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) { diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index f2ff88c974..8ffe8fa66f 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -39,8 +39,8 @@ public slots: * @function AvatarBookmarks.addBookMark */ void addBookmark(); - void addBookmark(QString bookmarkName) {} - void removeBookmark(QString bookmark) {} + void addBookmark(QString bookmarkName); + void removeBookmark(QString bookmarkName); QVariantMap getBookmarks() { return _bookmarks; } protected: diff --git a/interface/src/Bookmarks.cpp b/interface/src/Bookmarks.cpp index 6e99b81e50..9a8d8eb279 100644 --- a/interface/src/Bookmarks.cpp +++ b/interface/src/Bookmarks.cpp @@ -46,6 +46,10 @@ void Bookmarks::deleteBookmark() { return; } + deleteBookmark(bookmarkName); +} + +void Bookmarks::deleteBookmark(const QString& bookmarkName) { removeBookmarkFromMenu(Menu::getInstance(), bookmarkName); remove(bookmarkName); diff --git a/interface/src/Bookmarks.h b/interface/src/Bookmarks.h index dc08d4b279..9be43ddfbc 100644 --- a/interface/src/Bookmarks.h +++ b/interface/src/Bookmarks.h @@ -31,6 +31,8 @@ public: QString addressForBookmark(const QString& name) const; protected: + void deleteBookmark(const QString& bookmarkName); + void addBookmarkToFile(const QString& bookmarkName, const QVariant& bookmark); virtual void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) = 0; void enableMenuItems(bool enabled); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index a3b07d400f..58a82240db 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -888,7 +888,7 @@ public: bool hasDriveInput() const; - QVariantList getAvatarEntitiesVariant(); + Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); void removeAvatarEntities(); /**jsdoc diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 3bad252db1..78e152f22a 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -3,9 +3,9 @@ /*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // -// pal.js +// avatarapp.js // -// Created by Howard Stearns on December 9, 2016 +// Created by Alexander Ivash on April 30, 2018 // Copyright 2016 High Fidelity, Inc // // Distributed under the Apache License, Version 2.0 @@ -14,660 +14,44 @@ (function() { // BEGIN LOCAL_SCOPE - var request = Script.require('request').request; - -var populateNearbyUserList, color, textures, removeOverlays, - controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, - receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, - createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged, - avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; - -// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed -// something, will revisit as this is sorta horrible. -var UNSELECTED_TEXTURES = { - "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png"), - "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png") -}; -var SELECTED_TEXTURES = { - "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png"), - "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png") -}; -var HOVER_TEXTURES = { - "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png"), - "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png") -}; - -var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6}; -var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29}; -var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now -var PAL_QML_SOURCE = "hifi/AvatarApp.qml"; -var conserveResources = true; - +var request = Script.require('request').request; +var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml"; Script.include("/~/system/libraries/controllers.js"); -function projectVectorOntoPlane(normalizedVector, planeNormal) { - return Vec3.cross(planeNormal, Vec3.cross(normalizedVector, planeNormal)); -} -function angleBetweenVectorsInPlane(from, to, normal) { - var projectedFrom = projectVectorOntoPlane(from, normal); - var projectedTo = projectVectorOntoPlane(to, normal); - return Vec3.orientedAngle(projectedFrom, projectedTo, normal); -} +// constants from AvatarBookmarks.h +var ENTRY_AVATAR_URL = "avatarUrl"; +var ENTRY_AVATAR_ATTACHMENTS = "attachments"; +var ENTRY_AVATAR_ENTITIES = "avatarEntities"; +var ENTRY_AVATAR_SCALE = "avatarScale"; +var ENTRY_VERSION = "version"; -// -// Overlays. -// -var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. - -function ExtendedOverlay(key, type, properties, selected, hasModel) { // A wrapper around overlays to store the key it is associated with. - overlays[key] = this; - if (hasModel) { - var modelKey = key + "-m"; - this.model = new ExtendedOverlay(modelKey, "model", { - url: Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx"), - textures: textures(selected), - ignoreRayIntersection: true - }, false, false); - } else { - this.model = undefined; - } - this.key = key; - this.selected = selected || false; // not undefined - this.hovering = false; - this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... -} -// Instance methods: -ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay - Overlays.deleteOverlay(this.activeOverlay); - delete overlays[this.key]; -}; - -ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay - Overlays.editOverlay(this.activeOverlay, properties); -}; - -function color(selected, hovering, level) { - var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; - function scale(component) { - var delta = 0xFF - component; - return component + (delta * level); - } - return {red: scale(base.red), green: scale(base.green), blue: scale(base.blue)}; -} - -function textures(selected, hovering) { - return hovering ? HOVER_TEXTURES : selected ? SELECTED_TEXTURES : UNSELECTED_TEXTURES; -} -// so we don't have to traverse the overlays to get the last one -var lastHoveringId = 0; -ExtendedOverlay.prototype.hover = function (hovering) { - this.hovering = hovering; - if (this.key === lastHoveringId) { - if (hovering) { - return; - } - lastHoveringId = 0; - } - this.editOverlay({color: color(this.selected, hovering, this.audioLevel)}); - if (this.model) { - this.model.editOverlay({textures: textures(this.selected, hovering)}); - } - if (hovering) { - // un-hover the last hovering overlay - if (lastHoveringId && lastHoveringId !== this.key) { - ExtendedOverlay.get(lastHoveringId).hover(false); - } - lastHoveringId = this.key; - } -}; -ExtendedOverlay.prototype.select = function (selected) { - if (this.selected === selected) { - return; - } - - UserActivityLogger.palAction(selected ? "avatar_selected" : "avatar_deselected", this.key); - - this.editOverlay({color: color(selected, this.hovering, this.audioLevel)}); - if (this.model) { - this.model.editOverlay({textures: textures(selected)}); - } - this.selected = selected; -}; -// Class methods: -var selectedIds = []; -ExtendedOverlay.isSelected = function (id) { - return -1 !== selectedIds.indexOf(id); -}; -ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier - return overlays[key]; -}; -ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy. - var key; - for (key in overlays) { - if (iterator(ExtendedOverlay.get(key))) { - return; - } - } -}; -ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any) - if (lastHoveringId) { - ExtendedOverlay.get(lastHoveringId).hover(false); - } -}; - -// hit(overlay) on the one overlay intersected by pickRay, if any. -// noHit() if no ExtendedOverlay was intersected (helps with hover) -ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { - var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. - if (!pickedOverlay.intersects) { - if (noHit) { - return noHit(); - } - return; - } - ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours. - if ((overlay.activeOverlay) === pickedOverlay.overlayID) { - hit(overlay); - return true; - } - }); -}; - - -// -// Similar, for entities -// -function HighlightedEntity(id, entityProperties) { - this.id = id; - this.overlay = Overlays.addOverlay('cube', { - position: entityProperties.position, - rotation: entityProperties.rotation, - dimensions: entityProperties.dimensions, - solid: false, - color: { - red: 0xF3, - green: 0x91, - blue: 0x29 - }, - ignoreRayIntersection: true, - drawInFront: false // Arguable. For now, let's not distract with mysterious wires around the scene. - }); - HighlightedEntity.overlays.push(this); -} -HighlightedEntity.overlays = []; -HighlightedEntity.clearOverlays = function clearHighlightedEntities() { - HighlightedEntity.overlays.forEach(function (highlighted) { - Overlays.deleteOverlay(highlighted.overlay); - }); - HighlightedEntity.overlays = []; -}; -HighlightedEntity.updateOverlays = function updateHighlightedEntities() { - HighlightedEntity.overlays.forEach(function (highlighted) { - var properties = Entities.getEntityProperties(highlighted.id, ['position', 'rotation', 'dimensions']); - Overlays.editOverlay(highlighted.overlay, { - position: properties.position, - rotation: properties.rotation, - dimensions: properties.dimensions - }); - }); -}; - -/* this contains current gain for a given node (by session id). More efficient than - * querying it, plus there isn't a getGain function so why write one */ -var sessionGains = {}; -function convertDbToLinear(decibels) { - // +20db = 10x, 0dB = 1x, -10dB = 0.1x, etc... - // but, your perception is that something 2x as loud is +10db - // so we go from -60 to +20 or 1/64x to 4x. For now, we can - // maybe scale the signal this way?? - return Math.pow(2, decibels / 10.0); -} function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. - var data; + console.debug('fromQml: message = ', JSON.stringify(message, null, '\t')) + switch (message.method) { - case 'selected': - selectedIds = message.params; - ExtendedOverlay.some(function (overlay) { - var id = overlay.key; - var selected = ExtendedOverlay.isSelected(id); - overlay.select(selected); - }); + case 'getAvatars': + var currentAvatar = {} + currentAvatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL; + currentAvatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale(); + currentAvatar[ENTRY_AVATAR_ATTACHMENTS] = MyAvatar.getAttachmentsVariant(); + currentAvatar[ENTRY_AVATAR_ENTITIES] = MyAvatar.getAvatarEntitiesVariant(); - HighlightedEntity.clearOverlays(); - if (selectedIds.length) { - Entities.findEntitiesInFrustum(Camera.frustum).forEach(function (id) { - // Because lastEditedBy is per session, the vast majority of entities won't match, - // so it would probably be worth reducing marshalling costs by asking for just we need. - // However, providing property name(s) is advisory and some additional properties are - // included anyway. As it turns out, asking for 'lastEditedBy' gives 'position', 'rotation', - // and 'dimensions', too, so we might as well make use of them instead of making a second - // getEntityProperties call. - // It would be nice if we could harden this against future changes by specifying all - // and only these four in an array, but see - // https://highfidelity.fogbugz.com/f/cases/2728/Entities-getEntityProperties-id-lastEditedBy-name-lastEditedBy-doesn-t-work - var properties = Entities.getEntityProperties(id, 'lastEditedBy'); - if (ExtendedOverlay.isSelected(properties.lastEditedBy)) { - new HighlightedEntity(id, properties); - } - }); - } - break; - case 'refreshNearby': - data = {}; - ExtendedOverlay.some(function (overlay) { // capture the audio data - data[overlay.key] = overlay; - }); - removeOverlays(); - // If filter is specified from .qml instead of through settings, update the settings. - if (message.params.filter !== undefined) { - Settings.setValue('pal/filtered', !!message.params.filter); - } - populateNearbyUserList(message.params.selected, data); - UserActivityLogger.palAction("refresh_nearby", ""); - break; - case 'refreshConnections': - print('Refreshing Connections...'); - getConnectionData(false); - UserActivityLogger.palAction("refresh_connections", ""); - break; - case 'removeConnection': - connectionUserName = message.params; - request({ - uri: METAVERSE_BASE + '/api/v1/user/connections/' + connectionUserName, - method: 'DELETE' - }, function (error, response) { - if (error || (response.status !== 'success')) { - print("Error: unable to remove connection", connectionUserName, error || response.status); - return; - } - getConnectionData(false); - }); - break + message.reply = { + 'bookmarks' : AvatarBookmarks.getBookmarks(), + 'currentAvatar' : currentAvatar + }; - case 'removeFriend': - friendUserName = message.params; - print("Removing " + friendUserName + " from friends."); - request({ - uri: METAVERSE_BASE + '/api/v1/user/friends/' + friendUserName, - method: 'DELETE' - }, function (error, response) { - if (error || (response.status !== 'success')) { - print("Error: unable to unfriend " + friendUserName, error || response.status); - return; - } - getConnectionData(friendUserName); - }); - break - case 'addFriend': - friendUserName = message.params; - print("Adding " + friendUserName + " to friends."); - request({ - uri: METAVERSE_BASE + '/api/v1/user/friends', - method: 'POST', - json: true, - body: { - username: friendUserName, - } - }, function (error, response) { - if (error || (response.status !== 'success')) { - print("Error: unable to friend " + friendUserName, error || response.status); - return; - } - getConnectionData(friendUserName); - } - ); + sendToQml(message) break; default: - print('Unrecognized message from Pal.qml:', JSON.stringify(message)); + print('Unrecognized message from AvatarApp.qml:', JSON.stringify(message)); } } function sendToQml(message) { tablet.sendToQml(message); } -function updateUser(data) { - print('PAL update:', JSON.stringify(data)); - sendToQml({ method: 'updateUsername', params: data }); -} -// -// User management services -// -// These are prototype versions that will be changed when the back end changes. -var METAVERSE_BASE = Account.metaverseServerURL; -function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise. - request({ - uri: url - }, function (error, response) { - if (error || (response.status !== 'success')) { - print("Error: unable to get", url, error || response.status); - return; - } - callback(response.data); - }); -} -function getProfilePicture(username, callback) { // callback(url) if successfull. (Logs otherwise) - // FIXME Prototype scrapes profile picture. We should include in user status, and also make available somewhere for myself - request({ - uri: METAVERSE_BASE + '/users/' + username - }, function (error, html) { - var matched = !error && html.match(/img class="users-img" src="([^"]*)"/); - if (!matched) { - print('Error: Unable to get profile picture for', username, error); - callback(''); - return; - } - callback(matched[1]); - }); -} -function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) - url = METAVERSE_BASE + '/api/v1/users?' - if (domain) { - url += 'status=' + domain.slice(1, -1); // without curly braces - } else { - url += 'filter=connections'; // regardless of whether online - } - requestJSON(url, function (connectionsData) { - callback(connectionsData.users); - }); -} -function getInfoAboutUser(specificUsername, callback) { - url = METAVERSE_BASE + '/api/v1/users?filter=connections' - requestJSON(url, function (connectionsData) { - for (user in connectionsData.users) { - if (connectionsData.users[user].username === specificUsername) { - callback(connectionsData.users[user]); - return; - } - } - callback(false); - }); -} -function getConnectionData(specificUsername, domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick. - function frob(user) { // get into the right format - var formattedSessionId = user.location.node_id || ''; - if (formattedSessionId !== '' && formattedSessionId.indexOf("{") != 0) { - formattedSessionId = "{" + formattedSessionId + "}"; - } - return { - sessionId: formattedSessionId, - userName: user.username, - connection: user.connection, - profileUrl: user.images.thumbnail, - placeName: (user.location.root || user.location.domain || {}).name || '' - }; - } - if (specificUsername) { - getInfoAboutUser(specificUsername, function (user) { - if (user) { - updateUser(frob(user)); - } else { - print('Error: Unable to find information about ' + specificUsername + ' in connectionsData!'); - } - }); - } else { - getAvailableConnections(domain, function (users) { - if (domain) { - users.forEach(function (user) { - updateUser(frob(user)); - }); - } else { - sendToQml({ method: 'connections', params: users.map(frob) }); - } - }); - } -} - -// -// Main operations. -// -function addAvatarNode(id) { - var selected = ExtendedOverlay.isSelected(id); - return new ExtendedOverlay(id, "sphere", { - drawInFront: true, - solid: true, - alpha: 0.8, - color: color(selected, false, 0.0), - ignoreRayIntersection: false - }, selected, !conserveResources); -} -// Each open/refresh will capture a stable set of avatarsOfInterest, within the specified filter. -var avatarsOfInterest = {}; -function populateNearbyUserList(selectData, oldAudioData) { - var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')}, - data = [], - avatars = AvatarList.getAvatarIdentifiers(), - myPosition = filter && Camera.position, - frustum = filter && Camera.frustum, - verticalHalfAngle = filter && (frustum.fieldOfView / 2), - horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio), - orientation = filter && Camera.orientation, - forward = filter && Quat.getForward(orientation), - verticalAngleNormal = filter && Quat.getRight(orientation), - horizontalAngleNormal = filter && Quat.getUp(orientation); - avatarsOfInterest = {}; - avatars.forEach(function (id) { - var avatar = AvatarList.getAvatar(id); - var name = avatar.sessionDisplayName; - if (!name) { - // Either we got a data packet but no identity yet, or something is really messed up. In any case, - // we won't be able to do anything with this user, so don't include them. - // In normal circumstances, a refresh will bring in the new user, but if we're very heavily loaded, - // we could be losing and gaining people randomly. - print('No avatar identity data for', id); - return; - } - if (id && myPosition && (Vec3.distance(avatar.position, myPosition) > filter.distance)) { - return; - } - var normal = id && filter && Vec3.normalize(Vec3.subtract(avatar.position, myPosition)); - var horizontal = normal && angleBetweenVectorsInPlane(normal, forward, horizontalAngleNormal); - var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal); - if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) { - return; - } - var oldAudio = oldAudioData && oldAudioData[id]; - var avatarPalDatum = { - profileUrl: '', - displayName: name, - userName: '', - connection: '', - sessionId: id || '', - audioLevel: (oldAudio && oldAudio.audioLevel) || 0.0, - avgAudioLevel: (oldAudio && oldAudio.avgAudioLevel) || 0.0, - admin: false, - personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null - ignore: !!id && Users.getIgnoreStatus(id), // ditto - isPresent: true, - isReplicated: avatar.isReplicated - }; - // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. - Users.requestUsernameFromID(id); - if (id) { - addAvatarNode(id); // No overlay for ourselves - avatarsOfInterest[id] = true; - } else { - // Return our username from the Account API - avatarPalDatum.userName = Account.username; - } - data.push(avatarPalDatum); - print('PAL data:', JSON.stringify(avatarPalDatum)); - }); - getConnectionData(false, location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain). - conserveResources = Object.keys(avatarsOfInterest).length > 20; - sendToQml({ method: 'nearbyUsers', params: data }); - if (selectData) { - selectData[2] = true; - sendToQml({ method: 'select', params: selectData }); - } -} - -// The function that handles the reply from the server -function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { - var data = { - sessionId: (MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially. - // If we get username (e.g., if in future we receive it when we're friends), use it. - // Otherwise, use valid machineFingerprint (which is not valid when not an admin). - userName: username || (Users.canKick && machineFingerprint) || '', - admin: isAdmin - }; - // Ship the data off to QML - updateUser(data); -} - -var pingPong = true; -function updateOverlays() { - var eye = Camera.position; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - if (!id || !avatarsOfInterest[id]) { - return; // don't update ourself, or avatars we're not interested in - } - var avatar = AvatarList.getAvatar(id); - if (!avatar) { - return; // will be deleted below if there had been an overlay. - } - var overlay = ExtendedOverlay.get(id); - if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. - print('Adding non-PAL avatar node', id); - overlay = addAvatarNode(id); - } - var target = avatar.position; - var distance = Vec3.distance(target, eye); - var offset = 0.2; - var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) - var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can - if (headIndex > 0) { - offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; - } - - // move a bit in front, towards the camera - target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); - - // now bump it up a bit - target.y = target.y + offset; - - overlay.ping = pingPong; - overlay.editOverlay({ - color: color(ExtendedOverlay.isSelected(id), overlay.hovering, overlay.audioLevel), - position: target, - dimensions: 0.032 * distance - }); - if (overlay.model) { - overlay.model.ping = pingPong; - overlay.model.editOverlay({ - position: target, - scale: 0.2 * distance, // constant apparent size - rotation: Camera.orientation - }); - } - }); - pingPong = !pingPong; - ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) - if (overlay.ping === pingPong) { - overlay.deleteOverlay(); - } - }); - // We could re-populateNearbyUserList if anything added or removed, but not for now. - HighlightedEntity.updateOverlays(); -} -function removeOverlays() { - selectedIds = []; - lastHoveringId = 0; - HighlightedEntity.clearOverlays(); - ExtendedOverlay.some(function (overlay) { - overlay.deleteOverlay(); - }); -} - -// -// Clicks. -// -function handleClick(pickRay) { - ExtendedOverlay.applyPickRay(pickRay, function (overlay) { - // Don't select directly. Tell qml, who will give us back a list of ids. - var message = {method: 'select', params: [[overlay.key], !overlay.selected, false]}; - sendToQml(message); - return true; - }); -} -function handleMouseEvent(mousePressEvent) { // handleClick if we get one. - if (!mousePressEvent.isLeftButton) { - return; - } - handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y)); -} -function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic - ExtendedOverlay.applyPickRay(pickRay, function (overlay) { - overlay.hover(true); - }, function () { - ExtendedOverlay.unHover(); - }); -} - -// handy global to keep track of which hand is the mouse (if any) -var currentHandPressed = 0; -var TRIGGER_CLICK_THRESHOLD = 0.85; -var TRIGGER_PRESS_THRESHOLD = 0.05; - -function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position - var pickRay; - if (HMD.active) { - if (currentHandPressed !== 0) { - pickRay = controllerComputePickRay(currentHandPressed); - } else { - // nothing should hover, so - ExtendedOverlay.unHover(); - return; - } - } else { - pickRay = Camera.computePickRay(event.x, event.y); - } - handleMouseMove(pickRay); -} -function handleTriggerPressed(hand, value) { - // The idea is if you press one trigger, it is the one - // we will consider the mouse. Even if the other is pressed, - // we ignore it until this one is no longer pressed. - var isPressed = value > TRIGGER_PRESS_THRESHOLD; - if (currentHandPressed === 0) { - currentHandPressed = isPressed ? hand : 0; - return; - } - if (currentHandPressed === hand) { - currentHandPressed = isPressed ? hand : 0; - return; - } - // otherwise, the other hand is still triggered - // so do nothing. -} - -// We get mouseMoveEvents from the handControllers, via handControllerPointer. -// But we don't get mousePressEvents. -var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); -var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); -function controllerComputePickRay(hand) { - var controllerPose = getControllerWorldLocation(hand, true); - if (controllerPose.valid) { - return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) }; - } -} -function makeClickHandler(hand) { - return function (clicked) { - if (clicked > TRIGGER_CLICK_THRESHOLD) { - var pickRay = controllerComputePickRay(hand); - handleClick(pickRay); - } - }; -} -function makePressHandler(hand) { - return function (value) { - handleTriggerPressed(hand, value); - }; -} -triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); -triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); -triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); -triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); // // Manage the connection between the button and the window. // @@ -685,80 +69,56 @@ function startup() { }); button.clicked.connect(onTabletButtonClicked); tablet.screenChanged.connect(onTabletScreenChanged); - Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); - Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); - Messages.subscribe(CHANNEL); - Messages.messageReceived.connect(receiveMessage); - Users.avatarDisconnected.connect(avatarDisconnected); - AvatarList.avatarAddedEvent.connect(avatarAdded); - AvatarList.avatarRemovedEvent.connect(avatarRemoved); - AvatarList.avatarSessionChangedEvent.connect(avatarSessionChanged); +// Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); +// Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); +// Users.avatarDisconnected.connect(avatarDisconnected); +// AvatarList.avatarAddedEvent.connect(avatarAdded); +// AvatarList.avatarRemovedEvent.connect(avatarRemoved); +// AvatarList.avatarSessionChangedEvent.connect(avatarSessionChanged); } startup(); var isWired = false; -var audioTimer; -var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too) -var AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS = 300; function off() { if (isWired) { // It is not ok to disconnect these twice, hence guard. - Script.update.disconnect(updateOverlays); - Controller.mousePressEvent.disconnect(handleMouseEvent); - Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); + //Controller.mousePressEvent.disconnect(handleMouseEvent); + //Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); - Users.usernameFromIDReply.disconnect(usernameFromIDReply); isWired = false; - ContextOverlay.enabled = true } - if (audioTimer) { - Script.clearInterval(audioTimer); - } - triggerMapping.disable(); // It's ok if we disable twice. - triggerPressMapping.disable(); // see above - removeOverlays(); - Users.requestsDomainListData = false; } function tabletVisibilityChanged() { if (!tablet.tabletShown) { - ContextOverlay.enabled = true; tablet.gotoHomeScreen(); } } -var onPalScreen = false; +var onAvatarAppScreen = false; function onTabletButtonClicked() { - if (onPalScreen) { + if (onAvatarAppScreen) { // for toolbar-mode: go back to home screen, this will close the window. tablet.gotoHomeScreen(); - ContextOverlay.enabled = true; } else { ContextOverlay.enabled = false; - tablet.loadQMLSource(PAL_QML_SOURCE); + tablet.loadQMLSource(AVATARAPP_QML_SOURCE); tablet.tabletShownChanged.connect(tabletVisibilityChanged); - Users.requestsDomainListData = true; - populateNearbyUserList(); isWired = true; - Script.update.connect(updateOverlays); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - Users.usernameFromIDReply.connect(usernameFromIDReply); - triggerMapping.enable(); - triggerPressMapping.enable(); - audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); } } var hasEventBridge = false; function wireEventBridge(on) { if (on) { if (!hasEventBridge) { + console.debug('tablet.fromQml.connect') tablet.fromQml.connect(fromQml); hasEventBridge = true; } } else { if (hasEventBridge) { + console.debug('tablet.fromQml.disconnect') tablet.fromQml.disconnect(fromQml); hasEventBridge = false; } @@ -766,139 +126,37 @@ function wireEventBridge(on) { } function onTabletScreenChanged(type, url) { - onPalScreen = (type === "QML" && url === PAL_QML_SOURCE); - wireEventBridge(onPalScreen); - // for toolbar mode: change button to active when window is first openend, false otherwise. - button.editProperties({isActive: onPalScreen}); + console.debug('avatarapp.js: onTabletScreenChanged: ', type, url); - // disable sphere overlays when not on pal screen. - if (!onPalScreen) { + onAvatarAppScreen = (type === "QML" && url === AVATARAPP_QML_SOURCE); + wireEventBridge(onAvatarAppScreen); + // for toolbar mode: change button to active when window is first openend, false otherwise. + button.editProperties({isActive: onAvatarAppScreen}); + + if (onAvatarAppScreen) { + sendToQml({'method' : 'initialize'}) + } + + console.debug('onAvatarAppScreen: ', onAvatarAppScreen); + + // disable sphere overlays when not on avatarapp screen. + if (!onAvatarAppScreen) { off(); } } -// -// Message from other scripts, such as edit.js -// -var CHANNEL = 'com.highfidelity.pal'; -function receiveMessage(channel, messageString, senderID) { - if ((channel !== CHANNEL) || (senderID !== MyAvatar.sessionUUID)) { - return; - } - var message = JSON.parse(messageString); - switch (message.method) { - case 'select': - sendToQml(message); // Accepts objects, not just strings. - break; - default: - print('Unrecognized PAL message', messageString); - } -} - -var AVERAGING_RATIO = 0.05; -var LOUDNESS_FLOOR = 11.0; -var LOUDNESS_SCALE = 2.8 / 5.0; -var LOG2 = Math.log(2.0); -var AUDIO_PEAK_DECAY = 0.02; -var myData = {}; // we're not includied in ExtendedOverlay.get. - -function scaleAudio(val) { - var audioLevel = 0.0; - if (val <= LOUDNESS_FLOOR) { - audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE; - } else { - audioLevel = (val - (LOUDNESS_FLOOR - 1)) * LOUDNESS_SCALE; - } - if (audioLevel > 1.0) { - audioLevel = 1; - } - return audioLevel; -} - -function getAudioLevel(id) { - // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged - // But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency - // of updating (the latter for efficiency too). - var avatar = AvatarList.getAvatar(id); - var audioLevel = 0.0; - var avgAudioLevel = 0.0; - var data = id ? ExtendedOverlay.get(id) : myData; - if (data) { - - // we will do exponential moving average by taking some the last loudness and averaging - data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness); - - // add 1 to insure we don't go log() and hit -infinity. Math.log is - // natural log, so to get log base 2, just divide by ln(2). - audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); - - // decay avgAudioLevel - avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); - - data.avgAudioLevel = avgAudioLevel; - data.audioLevel = audioLevel; - - // now scale for the gain. Also, asked to boost the low end, so one simple way is - // to take sqrt of the value. Lets try that, see how it feels. - avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[id] || 0.75))); - } - return [audioLevel, avgAudioLevel]; -} - -function createAudioInterval(interval) { - // we will update the audioLevels periodically - // TODO: tune for efficiency - expecially with large numbers of avatars - return Script.setInterval(function () { - var param = {}; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - var level = getAudioLevel(id), - userId = id || 0; // qml didn't like an object with null/empty string for a key, so... - param[userId] = level; - }); - sendToQml({method: 'updateAudioLevel', params: param}); - }, interval); -} - -function avatarDisconnected(nodeID) { - // remove from the pal list - sendToQml({method: 'avatarDisconnected', params: [nodeID]}); -} - -function clearLocalQMLDataAndClosePAL() { - sendToQml({ method: 'clearLocalQMLData' }); - if (onPalScreen) { - ContextOverlay.enabled = true; - tablet.gotoHomeScreen(); - } -} - -function avatarAdded(avatarID) { - sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarAdded'] }); -} - -function avatarRemoved(avatarID) { - sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarRemoved'] }); -} - -function avatarSessionChanged(avatarID) { - sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] }); -} - function shutdown() { - if (onPalScreen) { + if (onAvatarAppScreen) { tablet.gotoHomeScreen(); } button.clicked.disconnect(onTabletButtonClicked); tablet.removeButton(button); tablet.screenChanged.disconnect(onTabletScreenChanged); - Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); - Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); - Messages.subscribe(CHANNEL); - Messages.messageReceived.disconnect(receiveMessage); - Users.avatarDisconnected.disconnect(avatarDisconnected); - AvatarList.avatarAddedEvent.disconnect(avatarAdded); - AvatarList.avatarRemovedEvent.disconnect(avatarRemoved); - AvatarList.avatarSessionChangedEvent.disconnect(avatarSessionChanged); +// Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); +// Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); +// AvatarList.avatarAddedEvent.disconnect(avatarAdded); +// AvatarList.avatarRemovedEvent.disconnect(avatarRemoved); +// AvatarList.avatarSessionChangedEvent.disconnect(avatarSessionChanged); off(); } From ce3015662e7130410bf878d83a6fc32f326e87bd Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 4 May 2018 12:03:06 -0700 Subject: [PATCH 061/182] backend integration from Dante * adding some functionality to avatar bookmarks * saving work --- interface/resources/qml/hifi/AvatarApp.qml | 2 +- interface/src/AvatarBookmarks.cpp | 84 +++++++++++++++++++--- interface/src/AvatarBookmarks.h | 7 +- interface/src/Bookmarks.h | 8 +-- interface/src/ui/overlays/Web3DOverlay.cpp | 2 + 5 files changed, 85 insertions(+), 18 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index d0a35f8f11..a2a32959bd 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -11,7 +11,6 @@ Rectangle { width: 480 height: 706 color: style.colors.white - property string getAvatarsMethod: 'getAvatars' signal sendToScript(var message); @@ -486,6 +485,7 @@ Rectangle { property int verticalSpacing: 36 function selectAvatar(avatar) { + AvatarBookmarks.loadBookmark(avatar.name); selectedAvatarId = avatar.name; var avatarIndex = allAvatars.findAvatarIndex(selectedAvatarId); allAvatars.move(avatarIndex, 0, 1); diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index e243890bc3..64bb11f4a7 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -17,7 +17,9 @@ #include #include #include +#include +#include #include #include #include @@ -96,6 +98,74 @@ AvatarBookmarks::AvatarBookmarks() { readFromFile(); } +void AvatarBookmarks::addBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "addBookmark", Q_ARG(QString, bookmarkName)); + return; + } + Menu* menubar = Menu::getInstance(); + QVariantMap bookmark = getAvatarDataToBookmark(); + addBookmarkToMenu(menubar, bookmarkName, bookmark); + insert(bookmarkName, bookmark); + enableMenuItems(true); +} + +void AvatarBookmarks::saveBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "saveBookmark", Q_ARG(QString, bookmarkName)); + return; + } + if (contains(bookmarkName)) { + QVariantMap bookmark = getAvatarDataToBookmark(); + insert(bookmarkName, bookmark); + } +} + +void AvatarBookmarks::removeBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "removeBookmark", Q_ARG(QString, bookmarkName)); + return; + } + + Menu* menubar = Menu::getInstance(); + Bookmarks::removeBookmarkFromMenu(Menu::getInstance(), bookmarkName); + remove(bookmarkName); + + if (_bookmarksMenu->actions().count() == 0) { + enableMenuItems(false); + } +} + +void AvatarBookmarks::loadBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "reloadBookmark", Q_ARG(QString, bookmarkName)); + return; + } + + auto bookmarkEntry = _bookmarks.find(bookmarkName); + + if (bookmarkEntry != _bookmarks.end()) { + QVariantMap bookmark = bookmarkEntry.value().toMap(); + if (!bookmark.empty()) { + auto myAvatar = DependencyManager::get()->getMyAvatar(); + myAvatar->removeAvatarEntities(); + const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); + myAvatar->useFullAvatarURL(avatarUrl); + qCDebug(interfaceapp) << "Avatar On " << avatarUrl; + const QList& attachments = bookmark.value(ENTRY_AVATAR_ATTACHMENTS, QList()).toList(); + + qCDebug(interfaceapp) << "Attach " << attachments; + myAvatar->setAttachmentsVariant(attachments); + + const float& qScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat(); + myAvatar->setAvatarScale(qScale); + + const QVariantList& avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList(); + addAvatarEntities(avatarEntities); + } + } +} + void AvatarBookmarks::readFromFile() { // migrate old avatarbookmarks.json, used to be in 'local' folder on windows QString oldConfigPath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + AVATARBOOKMARKS_FILENAME; @@ -181,12 +251,12 @@ void AvatarBookmarks::addBookmark() { return; } - addBookmark(bookmarkName); + QVariantMap bookmark = getAvatarDataToBookmark(); + Bookmarks::addBookmarkToFile(bookmarkName, bookmark); }); } -void AvatarBookmarks::addBookmark(QString bookmarkName) -{ +QVariantMap AvatarBookmarks::getAvatarDataToBookmark() { auto myAvatar = DependencyManager::get()->getMyAvatar(); const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); @@ -199,13 +269,7 @@ void AvatarBookmarks::addBookmark(QString bookmarkName) bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale); bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant()); bookmark.insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); - - Bookmarks::addBookmarkToFile(bookmarkName, bookmark); -} - -void AvatarBookmarks::removeBookmark(QString bookmarkName) -{ - Bookmarks::deleteBookmark(bookmarkName); + return bookmark; } void AvatarBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) { diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 8ffe8fa66f..c57b4760e7 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -39,13 +39,16 @@ public slots: * @function AvatarBookmarks.addBookMark */ void addBookmark(); - void addBookmark(QString bookmarkName); - void removeBookmark(QString bookmarkName); + void addBookmark(const QString& bookmarkName); + void saveBookmark(const QString& bookmarkName); + void loadBookmark(const QString& bookmarkName); + void removeBookmark(const QString& bookmarkName); QVariantMap getBookmarks() { return _bookmarks; } protected: void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) override; void readFromFile() override; + QVariantMap getAvatarDataToBookmark(); private: const QString AVATARBOOKMARKS_FILENAME = "avatarbookmarks.json"; diff --git a/interface/src/Bookmarks.h b/interface/src/Bookmarks.h index 9be43ddfbc..88510e4eda 100644 --- a/interface/src/Bookmarks.h +++ b/interface/src/Bookmarks.h @@ -40,9 +40,10 @@ protected: void insert(const QString& name, const QVariant& address); // Overwrites any existing entry with same name. void sortActions(Menu* menubar, MenuWrapper* menu); int getMenuItemLocation(QList actions, const QString& name) const; - + void removeBookmarkFromMenu(Menu* menubar, const QString& name); bool contains(const QString& name) const; - + void remove(const QString& name); + QVariantMap _bookmarks; // { name: url, ... } QPointer _bookmarksMenu; QPointer _deleteBookmarksAction; @@ -59,12 +60,9 @@ protected slots: void deleteBookmark(); private: - void remove(const QString& name); static bool sortOrder(QAction* a, QAction* b); void persistToFile(); - - void removeBookmarkFromMenu(Menu* menubar, const QString& name); }; #endif // hifi_Bookmarks_h diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index ee267beb78..a8a82c74b7 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -42,6 +42,7 @@ #include "scripting/MenuScriptingInterface.h" #include "scripting/SettingsScriptingInterface.h" #include +#include #include #include "FileDialogHelper.h" #include "avatar/AvatarManager.h" @@ -253,6 +254,7 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("SoundCache", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); _webSurface->getSurfaceContext()->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); _webSurface->getSurfaceContext()->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); _webSurface->getSurfaceContext()->setContextProperty("Controller", DependencyManager::get().data()); From 1ccd4aaab7457aac112c6910af7f34fb5c54ead1 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 4 May 2018 22:37:26 +0300 Subject: [PATCH 062/182] fix typo --- interface/src/AvatarBookmarks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 64bb11f4a7..7499cca57b 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -138,7 +138,7 @@ void AvatarBookmarks::removeBookmark(const QString& bookmarkName) { void AvatarBookmarks::loadBookmark(const QString& bookmarkName) { if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "reloadBookmark", Q_ARG(QString, bookmarkName)); + BLOCKING_INVOKE_METHOD(this, "loadBookmark", Q_ARG(QString, bookmarkName)); return; } From 245ab7a3094a088d62dcd4d5d8c7580b44604835 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sat, 5 May 2018 00:07:34 +0300 Subject: [PATCH 063/182] explicitely specify 'getMoreAvatars', otherwise QML starts complaining about not-existing property --- interface/resources/qml/hifi/AvatarApp.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index a2a32959bd..d54c58643b 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -33,7 +33,8 @@ Rectangle { 'name' : avatarName, 'url' : Qt.resolvedUrl(allAvatars.urls[i++ % allAvatars.urls.length]), 'wearables' : '', - 'entry' : getAvatarsReply.bookmarks[avatarName] + 'entry' : getAvatarsReply.bookmarks[avatarName], + 'getMoreAvatars' : false }; allAvatars.append(avatarEntry); @@ -79,7 +80,8 @@ Rectangle { 'name' : '', 'url' : Qt.resolvedUrl(allAvatars.urls[i++ % allAvatars.urls.length]), 'wearables' : '', - 'entry' : currentAvatar + 'entry' : currentAvatar, + 'getMoreAvatars' : false }; selectedAvatar = currentAvatarEntry; From 4fa72eab6d09c99f2f5a95c19523bbea3e14c852 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sat, 5 May 2018 13:25:00 +0300 Subject: [PATCH 064/182] show real wearables from avatar in combobox --- .../resources/qml/controls-uit/ComboBox.qml | 1 + interface/resources/qml/hifi/AvatarApp.qml | 29 ++++++++++++----- .../qml/hifi/avatarapp/AdjustWearables.qml | 32 +++++++++++++++---- scripts/system/avatarapp.js | 12 +++++-- 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index be8c9f6740..9ec5ed19ba 100644 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -46,6 +46,7 @@ FocusScope { hoverEnabled: true visible: true height: hifi.fontSizes.textFieldInput + 13 // Match height of TextField control. + textRole: "text" function previousItem() { root.currentHighLightedIndex = (root.currentHighLightedIndex + comboBox.count - 1) % comboBox.count; } function nextItem() { root.currentHighLightedIndex = (root.currentHighLightedIndex + comboBox.count + 1) % comboBox.count; } diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index d54c58643b..a650a3cd9f 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -19,10 +19,17 @@ Rectangle { sendToScript(message); } + ListModel { // the only purpose of this model is to convert JS object to ListElement + id: currentAvatarModel + } + + property var jointNames; + function fromScript(message) { console.debug('AvatarApp.qml: fromScript: ', JSON.stringify(message, null, '\t')) if(message.method === 'initialize') { + jointNames = message.reply.jointNames; emitSendToScript({'method' : getAvatarsMethod}); } else if(message.method === getAvatarsMethod) { var getAvatarsReply = message.reply; @@ -32,7 +39,7 @@ Rectangle { var avatarEntry = { 'name' : avatarName, 'url' : Qt.resolvedUrl(allAvatars.urls[i++ % allAvatars.urls.length]), - 'wearables' : '', + 'wearables' : getAvatarsReply.bookmarks[avatarName].avatarEntites ? getAvatarsReply.bookmarks[avatarName].avatarEntites : [], 'entry' : getAvatarsReply.bookmarks[avatarName], 'getMoreAvatars' : false }; @@ -76,14 +83,18 @@ Rectangle { console.debug('selectedAvatarIndex = -1, avatar is not favorite') if(selectedAvatarIndex === -1) { - var currentAvatarEntry = { + + var currentAvatarEntry = { 'name' : '', 'url' : Qt.resolvedUrl(allAvatars.urls[i++ % allAvatars.urls.length]), - 'wearables' : '', + 'wearables' : currentAvatar.avatarEntites ? currentAvatar.avatarEntites : [], 'entry' : currentAvatar, 'getMoreAvatars' : false }; + currentAvatarModel.append(currentAvatarEntry); + currentAvatarEntry = allAvatars.get(allAvatars.count - 1); + selectedAvatar = currentAvatarEntry; view.setPage(0); @@ -111,7 +122,7 @@ Rectangle { property string avatarName: selectedAvatar ? selectedAvatar.name : '' property string avatarUrl: selectedAvatar ? selectedAvatar.url : null - property int avatarWearablesCount: selectedAvatar && selectedAvatar.wearables !== '' ? selectedAvatar.wearables.split('|').length : 0 + property int avatarWearablesCount: selectedAvatar ? selectedAvatar.wearables.count : 0 property bool isAvatarInFavorites: selectedAvatar ? allAvatars.findAvatar(selectedAvatar.name) !== undefined : false property bool isInManageState: false @@ -174,6 +185,7 @@ Rectangle { anchors.right: parent.right anchors.top: header.bottom anchors.bottom: parent.bottom + jointNames: root.jointNames z: 3 } @@ -364,7 +376,8 @@ Rectangle { MouseArea { anchors.fill: parent onClicked: { - adjustWearables.open(); + console.debug('adjustWearables.open'); + adjustWearables.open(selectedAvatar); } } } @@ -395,7 +408,7 @@ Rectangle { var avatar = { 'url': Qt.resolvedUrl('../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png'), 'name': 'Lexi' + (++debug_newAvatarIndex), - 'wearables': '' + 'wearables': [] }; allAvatars.append(avatar) @@ -629,7 +642,7 @@ Rectangle { imageUrl: url border.color: container.highlighted ? style.colors.blueHighlight : 'transparent' border.width: container.highlighted ? 2 : 0 - wearablesCount: (!getMoreAvatars && wearables && wearables !== '') ? wearables.split('|').length : 0 + wearablesCount: !getMoreAvatars ? wearables.count : 0 onWearablesCountChanged: { console.debug('delegate: AvatarThumbnail.wearablesCount: ', wearablesCount) } @@ -882,7 +895,7 @@ Rectangle { var avatar = { 'url': Qt.resolvedUrl(url), 'name': 'Lexi' + (++newAvatarIndex), - 'wearables': 'hat|sunglasses|bracelet' + 'wearables': [] }; allAvatars.append(avatar) diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml index 9a2fc3bc72..abcd1e3353 100644 --- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -22,9 +22,30 @@ Rectangle { property var onButton2Clicked; property var onButton1Clicked; + property var jointNames; + + function open(avatar) { + console.debug('AdjustWearables.qml: open'); - function open() { visible = true; + wearablesCombobox.model.clear(); + + console.debug('AdjustWearables.qml: avatar.wearables.count: ', avatar.wearables.count); + for(var i = 0; i < avatar.wearables.count; ++i) { + var wearable = avatar.wearables.get(i).properties; + console.debug('wearable: ', JSON.stringify(wearable, null, '\t')) + + for(var j = (wearable.modelURL.length - 1); j >= 0; --j) { + if(wearable.modelURL[j] === '/') { + wearable.text = wearable.modelURL.substring(j + 1) + ' [%jointIndex%]'.replace('%jointIndex%', jointNames[wearable.parentJointIndex]); + console.debug('wearable.text = ', wearable.text); + break; + } + } + wearablesCombobox.model.append(wearable); + } + + wearablesCombobox.currentIndex = 0; } function close() { @@ -51,14 +72,13 @@ Rectangle { width: parent.width - 30 * 2 HifiControlsUit.ComboBox { + id: wearablesCombobox anchors.left: parent.left anchors.right: parent.right + comboBox.textRole: "text" - model: [ - 'Fedora.fbx [HeadTop_End]', - 'Fedora1.fbx [HeadTop_End]', - 'Fedora2.fbx [HeadTop_End]' - ] + model: ListModel { + } } Column { diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 78e152f22a..ef7c3c1b20 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -21,7 +21,7 @@ Script.include("/~/system/libraries/controllers.js"); // constants from AvatarBookmarks.h var ENTRY_AVATAR_URL = "avatarUrl"; var ENTRY_AVATAR_ATTACHMENTS = "attachments"; -var ENTRY_AVATAR_ENTITIES = "avatarEntities"; +var ENTRY_AVATAR_ENTITIES = "avatarEntites"; var ENTRY_AVATAR_SCALE = "avatarScale"; var ENTRY_VERSION = "version"; @@ -134,7 +134,15 @@ function onTabletScreenChanged(type, url) { button.editProperties({isActive: onAvatarAppScreen}); if (onAvatarAppScreen) { - sendToQml({'method' : 'initialize'}) + + var message = { + 'method' : 'initialize', + 'reply' : { + 'jointNames' : MyAvatar.getJointNames() + } + }; + + sendToQml(message) } console.debug('onAvatarAppScreen: ', onAvatarAppScreen); From fe2ebe63e8046f05092ae9260b4c6f57543e28ff Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sat, 5 May 2018 18:41:51 +0300 Subject: [PATCH 065/182] glyph font update to version 1.32 --- .../hifi-glyphs-1.31/fonts/hifi-glyphs.woff | Bin 21496 -> 0 bytes .../fonts/hifi-glyphs.eot | Bin 33642 -> 33678 bytes .../fonts/hifi-glyphs.svg | 8 +-- .../fonts/hifi-glyphs.ttf | Bin 33464 -> 33500 bytes .../hifi-glyphs-1.32/fonts/hifi-glyphs.woff | Bin 0 -> 21548 bytes .../icons-reference.html | 64 +++++++++--------- .../styles.css | 24 +++---- 7 files changed, 48 insertions(+), 48 deletions(-) delete mode 100644 interface/resources/fonts/hifi-glyphs-1.31/fonts/hifi-glyphs.woff rename interface/resources/fonts/{hifi-glyphs-1.31 => hifi-glyphs-1.32}/fonts/hifi-glyphs.eot (85%) rename interface/resources/fonts/{hifi-glyphs-1.31 => hifi-glyphs-1.32}/fonts/hifi-glyphs.svg (98%) rename interface/resources/fonts/{hifi-glyphs-1.31 => hifi-glyphs-1.32}/fonts/hifi-glyphs.ttf (85%) create mode 100644 interface/resources/fonts/hifi-glyphs-1.32/fonts/hifi-glyphs.woff rename interface/resources/fonts/{hifi-glyphs-1.31 => hifi-glyphs-1.32}/icons-reference.html (99%) rename interface/resources/fonts/{hifi-glyphs-1.31 => hifi-glyphs-1.32}/styles.css (99%) diff --git a/interface/resources/fonts/hifi-glyphs-1.31/fonts/hifi-glyphs.woff b/interface/resources/fonts/hifi-glyphs-1.31/fonts/hifi-glyphs.woff deleted file mode 100644 index f26000caf42cd88ba2e2ba3ac079299b43515ce3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21496 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 delta 1295 zcmYjRYfMvj96kSg+uPC>Dy6j0)>8Vo7wCh&9|(xbODqpT5%GC}R#XryRiWaFz;G%< z*>!VSj4|%ZGBY)+bH*5w&14@Iv$$k2OXiko%wmYi3}I&R!(1>~vU75Bl9S(uU(Sa! zcZyn^p~MHKfIIWzkm$y_y>0U~Q)&EY1ST&5Ff`QG)#E-GU~(LQskNir+j=iL29PcQ zoxOwnaOHG-8K9m4)aX8bWcbY+(7%UzKyCuWKQPqGZ+j3a0K_{1;u_>fhp|j{10=li&>?;> z_9F6KCy?p_NM?9wBssmD`V$ye08AVNSW(;Fb2s)VEB}I=!U`Texyb$JZ+-zN+uK|U z3c@$!x&8~B!h4uS3r^!Ss^78{{pduKus{uxhe8=06E4!xbOaSxgGy8*g|(>1I;_WL z)S&^5*obnppbSmegc#zeMGdMDLeB`Adt zb~xaK3x2raffudVjy7ySJHkSbh)d5Q9~x*;fI{f772Vi|E_7lCc48NHV-NO%M-Te2 z5B)fRgBZji3}G0Dk-!L&ID(@%hEa^+IL2`T?;wo{oWvxiFpblA7c*}gIg2xx#rrsi z3pkH?d;kF-3Q{q*J5sTxvO2Z4e%<=bbq$Rh%UjBtHpSw#HC4e-pd;G6MQM!DbFjHa__0!s>`Z>^1OM`yt{d? z)d}@=^*ch~Xp7oDVcXdRyU4!cD9*+8aIZ@$ zOXf=cE^RBlRTeILX1{KK>`*%f950+_oD0rH7wfv|KIDGwk$NAO5=O$WEsqHH(A%w_8G$B%2Mr2Ytm!_<@Nh_nH zvnZ8{&Y=}Fotd|N>kuTVnoLLgb2ei?o=wDIIN=kj#_e?2>B}4(FDFtGCC#J-X5s3I zdKpQO5t1ZFa4oZRqMRsu_}+uZ_(UI*hz$(!eS#_dX_k~f$|w1R)04TMULaCQd?>MU Sk~w+uDiIo|Dir^pfqwyGg)xHw diff --git a/interface/resources/fonts/hifi-glyphs-1.31/fonts/hifi-glyphs.svg b/interface/resources/fonts/hifi-glyphs-1.32/fonts/hifi-glyphs.svg similarity index 98% rename from interface/resources/fonts/hifi-glyphs-1.31/fonts/hifi-glyphs.svg rename to interface/resources/fonts/hifi-glyphs-1.32/fonts/hifi-glyphs.svg index c68cb63a0d..4f1fb2e690 100644 --- a/interface/resources/fonts/hifi-glyphs-1.31/fonts/hifi-glyphs.svg +++ b/interface/resources/fonts/hifi-glyphs-1.32/fonts/hifi-glyphs.svg @@ -35,8 +35,6 @@ - - @@ -48,7 +46,6 @@ - @@ -103,7 +100,6 @@ - @@ -154,4 +150,8 @@ + + + + diff --git a/interface/resources/fonts/hifi-glyphs-1.31/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs-1.32/fonts/hifi-glyphs.ttf similarity index 85% rename from interface/resources/fonts/hifi-glyphs-1.31/fonts/hifi-glyphs.ttf rename to interface/resources/fonts/hifi-glyphs-1.32/fonts/hifi-glyphs.ttf index edc447c13245b74845bcce8687c7432f37872c39..e85c193fd02bab6f2eab51e12080a5e960a66e4b 100644 GIT binary patch delta 1316 zcmYLJT})GF06pJ#Z*R*jlu~F*3zSmIZJ{mnkJ}az{DJz%pMn)PhF&R96$A?kqOQv7 zf43=D6F0VHF^kEz%ov+2WHH8Unc0I`h*{hoT;dXA%;Jn$oO>a2!9DDphm&)j&UrZt zC+LF{Gz9`6568fuD;n)-pXt(a!0c@RrR!Q+qxcTX{!;+f&h9#Y=gqc50OWw;cy(@`CSO~iD=a#zHyDde<}KTLw#vV;Y56(J$%Be; zN>F#G)9TymrQA%OH}7jri{^ZOjrK)BqTs$x)a}uwb$=H63%@9QTx2VnD|)JL(k~he zh8sq+vCTMVd|2!(9xuLA{M_U;B}_k=saaz-n_J8y<`2zJN*YT>N=o~$ct4^&m;k;YjRlQVmr{;yr z?Mk~@_Z9bj5A`&8eyBU?)p@PnT5ra?;NyKmzRSL+{ucjd{>OoEU??aC7wY>(jd)pn z7U~N<4{r(2h3|#`iC7}>NILQo0EG?b$ggAyM);7O=d_0GRGFyC?iX&mv-`WB2=eEp zKgxmbrWD06wv!8F0jseIAP%8YsbO5iR!i%MkWV78nR$<}!T#mAoQL0*1Jd>wo z6k4@HrHs?^QikIw%kUajtDuzd3TJ^Xhtl09yUAoXkzT4Glw}!)RS-&fR*|D(SUtv^a1eqiUveu(@MBefCl`NmSMY4;>u8=C#E@@!zl$4C; k1ihZ*;K-O1R}PIOmlKtAP@0sI9v|f7Y(wt<-yqKa3!OYOvihb`H|Vm6m7W)JS7amzwXMhtU{4|CBy?3{=5avsh($+>ro zxqggEfdFJ-5-i&5>o>I|@tql%x&Xk`P+M1zFTnv*695X^HkNzaZZ?kt)CHikcQ7(i zH62?5n1=u}wmUL9^6VK{U@8GXwtHYacKOc!6#zF2jNa~#M*6bv53dJs4?wWLKbne2 z-1G}T8UVKb!Ngc$n{gRH^8on=hI=DhZin?i#&&>QgORZjEYS{tLeB#YMFyjf!e4a) z$u5A>Bg3PK>80c!z=RUuVlT=O(7x-`$2(P3e?r0FIqu#+FZ`#>dEpHGW&8-Q;x){o z6(?{4)h}3resrQqR5F8fM;v3K;;&3|DvV03KozQy#46NdHP&DQ>d=5jtV20kQHmz4 zM-(yCq6RAwLd4zPzDE_aKR5ZJn*6o zThWfS=s-xEXNA-p@}PwddgQ}^&FID!bfFX5upKX92X*Lm0z24r2mGa1<#_;$=+X6-?sRlj=8hPsBvb>*$4P3xnv+M1QYionL^mQ6;JxwoKj&p?sIYP0h~afz?Y z;dJ@k9&g*$_O%_M<*X!wlgcvX3T0NdYFUmtS2LHV)#>vMo4dDkiT5Sb;-Z8TqZy%; zOOa5_Dy}LPl)SP>`8exbwki9=>?f+zs>eAu)o%4g^*_1Z+~(XHxlc85%@xf;UU%MA zEvJ1;`;#uId!X0oyY(OF7xP2;bNScv7Y%O11;f{dyM_lwrO|KfG0vMLCci0Wy0$!N zE;b)8a2K2{{G`ZMbjvbrU9w$F+0FK5``dg6ALkeNCjuk5gdX8(aaHkL@!uuwC09#B zr4P%ll-+Y^90QI=&Xdkt&IK3mns*PmpL%2-tta8Rpw za4A?Hd{Qx4aW_;O`XO8yPKIxUe*?h4f*Jae4nYSG((iD~wBpRz?et|+P?loQggW|+ zZoq@pfZfXR0x$3ZD<|coTp9U;0ge|eywxJ`g14fYDuO<*&a2bve4LdN%*1M?8ZM8s zS_Bsb_=+IeNiJbB6qLy@G6~6~M3zkHs!S!vFe<6Bk+O6JDx*ASFLAjY&b1^dP%*^j zs2L(AOWr`N+`f&GC^U_Bnf+565s_TN63JLO$z^N~%h;|_8)slM8MT_tW|b_P{@M10 zL)<)GlYV!@?|gbZnMD~PIN=j#4wtbZr!W2a;c}8uGqq4FEaKusy`17SN(nlE%judU z<)rF~^zJ(piT82w=)iEKPaHb>ex@vPFp`MGot|_~>K4hQvElf0OZwZ%_eeZ9RjK^{ I_veLw0e84Bn*aa+ diff --git a/interface/resources/fonts/hifi-glyphs-1.32/fonts/hifi-glyphs.woff b/interface/resources/fonts/hifi-glyphs-1.32/fonts/hifi-glyphs.woff new file mode 100644 index 0000000000000000000000000000000000000000..534f8e5623b776bc9e4ffe0caef6bf895ff3c105 GIT binary patch literal 21548 zcmV(;K-<4}Pew*hR8&s@08}gh3jhEB0C#u*0RR91000000000000000000000000( zMn)h2009U908Z@y0B}JK*P`}DMpR7z08jV;000^Q0010%P!I-3L`6mb08kVF0015U z001BW!2kqLQ!g?A08lgl002q=003Z7X-+R=ZDDW#08oGc00DUb00MY+wtZ7*Wnp9h z08#t^001rk001@*Fs?IbXk}pl08%Uf0015U001NeFah3ZZFG1508&f<008I!00Ar? z2O21CVR&!=08=yo000I6000I6lu!U}VQpmq08=~w007_s00Fb@ImK3RZ*z1208|_R z000mG001BW0{{VdoUFVFcpTN0D6E!MT{jMK5}-U2Lb}6F0uGoZ5D2RQ8wmT3!L}@G zv$d}+snvVec5CldtzDMneK)o-V3x3D0TM6?Aqh!_kcCVp$z+n!Kb1G{Kc}i?8z%po zneYAGQupnuyPSK@xo5lQman3!s>0{H6vhvfFT;&;+U#YyZa#`i_%KIuGs(iF^Rb^deb7fcMU?pFv zRa%vW%Hhh1%FUHKDxa);s`A;&7b;(@e6{k8${$z$vhu^qf2sUS<-aeeT(EG#$qN=Q zICH_57A#qC<$`M#+_+%*f^RIiXTgID9$&C-!G;AL3;Gr$76=Qp1^ERd3$`rSzu@@= zuP*rBg0~j@Xu&TR{Bglw7yNrwWffDkxa#v&=T%)$wY2K;s_Uz6s=BS}zN$y7HdJ+1 zg{zWPv?^aU+0eZ1l66ZuYdUIcTQ=18t!=IDSl3Y9vTjLDYfD#0Yg1EgM`uHA^}4o( z)|T4NCC!aB_0`R_ZPn|JUxaJjZB4D!>!6p$wyv()P*+X&+S<;xhT4wWeyGvZSl`gq zTe}wimRwT1uCZ%fV@GXGS8GRKb8Sm^O;c-Ut^2X0v8AWBqpNmZTVo6S9sAi)+u7J( zy9B{%g63GayQ6kVZKw^dbkyR|OFEmYo0_~{++uTMOJg&%YOW4_l!=fJ6hY_ z)z#Wk?(aWeuK_Gnw{_Nf(_7Nn)!J4rdSlQ3=x!;OT55Y;o~~={=x%mPO|A8<9$alr z)inT2$A-17p_b2$Rb=FpQ)HIv`#K%VWFkRESuC}_Tx~saQx}&4D zw-XwB-#y+p^xOtm>-45{Y|JG~YZ^OhnrdAFx3xC5bk%mWHFb9`>1^+=hOt_`?`j|z zOuH6ud*$`ron4J}ecq3z+PW^U)PaOuTiwy=b-Sdqsk*bFv#+HFIH{?zWkXYIO?6je zYfBBl2?+AOn(A7c*41__X{s+5I-8qndup3tYK<**t<}JBT@9_xwY4ylj?UVyuEv)7 z&aMvVxpnP&V4EfN9j)DMHO)S%j0DkilWVv^O*lY68x8xERVN2Gvc;DS^U5(APFj`9|w6ATc>FaK3?gn=0 z?gVBA)?LyD7aN=0Ydx(^OD@KbrTB3Peq4$lm*GdqyA|?ohP>M$uR>_aWt|PxE(>?n zG}P8?=xk~POmw!jHX+_>J6pRuYHEROeQT?0HY~vca$T8BWyk>(TsYW#$29^i@E>CCa32NX(jZjhH6MUji^2t8xQ+%pV^XWbV1hM7I z__Ds7FApNQ=o|8te8avG->7fQH}0G8P5P#M8-3Hh8Q-jLlW((c3yAG)zU{spzMZ~Z zzTLh(zP-MEzWu%@d7JRen8Rin^kpF!DpUIcW;ZGESu;S}0rdRA=arD;r@A!|q{_(DJ@4ED^-`(}+yJzoNbI%j^ zF1z>L`{esSaew&!_a0;({M*CbkA7n1DJx%nY~u0FtGZXcy!x}N&se=`^^e#5!5Zri#yU~B%p+nUxvPqiC1tP9-i7ihf1 zTk%r7G(gFsx!~MlZu@NO`k+JD^=&Qd1I}mKe&V-GKABAOsi1SGKb0~v8PmuFe?04) z;$Ppkd3(?%?DpAB+XMEi&dnVB7#%G21)R0hojWVM{rP1J` zxlh^}H{IUBCKgY(xt~yuFaG;iY+dH$*-^?^W;&Tlrva4hvz*e_N10UGv@)h?1?|ZK zH{Im6*zlh|k6pL*#a~?LY@gwj_Htv(ZTyn+WtjQw_TW40_O@A=_#6HWZEfp=*Rtzp z+qMPl>#FQG{9~nX2(I*n2SQk(FI*af`eUUM%uhX^>)1BU-mv)p^l7(ta4`I~#eM!` zLy!0u*=N`joCR35(q<|s`f-LyledCfXSg#KCY)=yE#p(Wf;szasxU5iF+hD=V=(8O z#ng>;Z4KCos=R-cmcmMi#GSL5h}hEum3qX9p*cCwb~**>3BR9lLT3mMVh2ZEv1vS(@Z#*9Mwib!;vX@mW2Erz2notH`(d& zvFX6LUwdlr&cl@aoBuG~+n}u^&Ti*4&h~}acp?!8VB*2$l@oK6#O*UURa10T8=4*7zL^sHHve`y)H+g~>zP{7)g6g8jkS(;mO4wl zl+^fpX;-*&pe5AY+h1&(9Dgmhk=V2zm`Hm8KH6Kji~-AqH@->@uGfe4F?{B=%T#dh>g zOb5RY6d0cljQO?0dv^f^{sI)(Q?EZpCj8Try&b`u*^cfWjd~lKd=A559lU?85 zzCMuluWz5(9{e|U`^?Ptfa%{p)4o2~aW?m7b}U>91ycS{7`XH-HdG3a1uQ?ZX7HyO zj^6qBBX^}qi-P*J}c&KF+2>pZ4KY#GRbHNS zE>TM40`oc$I^%MUnG8C2Aa@|!WPeQG<1TE0GH(6`2svo6$ZoSw;}nsK zk}ON`qGZ4izcg7TZSUSd+rG}gWl{7lOR5a)B5R6D-m?c8MO9UelAK?os-#F{owJyG zo_4;?KrMx;vP7XzV)sP2Y|I#-UBqEo0bU@(+#z%#7nyXL7t%t4kBL#*Pa|eL7tabq zbcBvsrJ?*#Zp@qp{3ujsXi3cTmS82d7>&?DVK6Dl@Kz)Rp(6Yc1CZ&OI+`6dpgAq2 zb5U}c^GS{-)RZCUyd`Dn5Y6kEY$mH0=?I~@kj(VTgF=!|@j{Zuh<)do9A$H6CKsfW z)ZQXbk{q9j2PhloL1H;a*|?F+fkb_H?rWSW8$3-BXPBiRK17L7**WGM0nRA_YIcmq8_!MH1-d4TCSF?`$!|Yy5=pZuB@J(qW9duQ3&s8Bs9DP7*QM)1&CSH# z)@eXyAB5J zi@pr3JEDxj+99~uT`52xVo?etQ3^P(E(YlULcmG}9|m2@fKExeVqkeVOZ2KKr3b6my(p$>z=kOU!wrIiHb76do}l7REKDp%~&5d~{lDa)$?dT_pm&wc}k zRaDIzkks#D(k6n)^E6?m$*y_^MwD?x%gM~w#i*1Sg3F#VPYF*2MbLpzIyL`WXjo)FY_I3GZn)qA zMihCGm&vklnlk!~KCMkFSsnDy)-7AMZrw^Id#<`_EX3F~_I_?-$5iK3XCaR-9l#L` za_@@_tQIN=(O4`NkCQ3qR_?X&*B(gSA}6J!oRmQZQ93v@I20a|R7sPRSS~_ianjkz zB;raq7b8zpGawCQMG7TDapSAlgwwWiFZyP#kd z1?@*4=OXYLj96&4rODpCOd5-b!JFnD;zoy}gF)v-mWD&|NSZ8dWD=rup8-P8LFeV#ry{>+{|fT|64G?s!_jHWEI z;x@^ntUR?+d8Y$JK#kBoz&(2X)es z`Le?@=Aagq2Z&Q`vz)!eS;9RIGoZ~EJ+D96No^F8X8T&F$ z?2Y$^!_jD;9LtmYzIp9+pi2@mY6~T6nB<)w-v0s<%PXUW!p4m|vRe}5FTYDqGD5)| z%3JwNNrLJB{(L5zPzRIIR4kH?=%mNE=nTe;su5`r1}WLgxD=282?6A=$oJm_!q7Yt z4@d}R+I2U5w1U0NUiPsn%baD*B0Fi{#es1K!hpQ;@?EboybjV?0CkZx1ae??bu|O! zl)-~sadn3MPqxYBWGf*j_*Hf5SJB7ly1n!;oz6}U4;Kp~nQ@I!R?uY~#jj$3T-WAh zu_l5w#DEn7S^|u4uxCkYE91#zkoJnGrA;|&SaC5afL4u312jfkr#itKou!lG+APgx ztti+f+PiV4UBi)Aog-FjQ;*0cl%u+T6pf`oqv3AF!0_5uimgM zi~%_3_yFAehV)Xq_pi z+Ap(8l-3XQ0jh-v?WXc~&7MjI!hws z2kokRxMSv`bC%yV3SI4X)`>T&j^DWGEe;ee$h+tN@bmnkN{q zobhDk6V(TvI4&!rC@bxl{S=p#&7_&TwK9gFq{I}J+@;gjinfRZ3x^6SE*+ZDt+)iFO~cX&(>QIN?A}Dl44oWP zXOwKl%E{zgP7~(^VbGgD2fX=#Y-C&nGhG78*ZI|y7B}MH&6mkiD9)70GT?wm%9e}3i|y(l>%phMGR&Ij2-lQwmQ1< z!F!J1IQuy6|Mi5+&Vu9D7umJ;w>Vu=-MNEZCz7j|GZKdDsDy8Y*MON#5YsF*(FZa# z@L$22__}k*oj&w3KmEKreU~d?`oNX$^t;N_$6&_Oa)#Lb&QmPLYuu2PJ?X5SpFVJ9 zdHUJ-NU|gk!Sstb{ek1tAMmC>#9g@1UU4_KWTE}}`KcisD~>ZLteYQy9KziHj}Rh( zhQMwrd{wTXV43v8Ins{36R5h*92#oI156a8`ktg zkyejv*tTJ7{}c2OJv8#fwrynFwkJlOgkMkeZ`(lnopx?^rnH;xrF+A>+BUUrsw>sf zb+kU*+S=9{u7_f6scyCnmhG_-wEGyngknYsad?vhZAS9()}h z2Eax?S2E!DbUxV8L&^PGRktt4H_6S`u5Nv`u2hhN&-}7CJFAN z8!mwHKUN(8`&jizCN*DOIm)S{m+zDN!00&b_gvGq{wI_tl>;ODwrtrnyJL8_u7L-s zrHJfLifQ2E?v7*=-$rDx(>7v1`W7dHhXKRO z1pbu(b-fpW2w+h(MOQTNh_h-&%jsrTEht5~C=SW4r-KckI#F1R_60CdPjMpxx{rxb zDJG_6ASv`KgOlCH(3FhKOffDeQh})b953U(MXD(g2D1}460kD#gHwhwfBx-wvw_ zE;EWrffoaL5jfimCyb%?ddwBbhQNY6KXo;@p*>!kRPIrqOAFK~}~kF(snR zG)9|SqfI0c;)DUIPYKbKCYVAlS>%TWWHfN5DT~;1A@`RTi`%92rbn29b$~~k2L?&2pU(f_obybszbJ|j_)D55yOfvQt9KZJi=JB_a zoqEnLb!amPt$lWmYt!iavy@r%40vgPXkc5iarn92Z$HSGLPi0u1}7)2lS{sH%gW9H zCM`%wJq^l0(m@3;j&qa!^e*S~lzw}1=EzHh#;LQf#a<|QdDF*1aeK*NFW+5`pro*ulq!~ciE)zyUN7I8EFIbk za(lb|Cij(vkN0vY__g^U8z>e*Lp{XY6 zh+fNBHkr?5Gx=O18;>Ot@xbFD4}|}AM+m|qdy9Ps_w>GfPd#0;@6nYtHIJ^`SMyZR zzO-uZwhikrQK5Fjwmo~dZQBDLfK%B?p;{N0=eNvMs#ZcF917|w{K*CBV?Cr6qkoVZINB}p?Vf;V7%CH z@51WW$=gR6&8xA<{=?kYIM>S2$c=9^7+2=Y{s+1721XENcMt_kEr0jI2iG8a7n2gfrWfWPv|HBX6;5*e|B8FYZp+bUTHf(c;aXnaf zm?bdGi4+qAq?R{~f}3tG_Pt?r1wfx42DxMH{@<2|QHkAYQ;%`+t<0kfP4tgYbpP5V zWpu$@U36{R;G#>zlqwfpzXf!iNJU*&T#C%K+Kt@${Iz zaUHx?Ev_V#xDu!JbZzXh9%5g|9^AO6upPJKq~Y{1Kbjg#6jDWTn9k76*=-}_Z>K)P z(VUn?zZ06%I8BHcg%J`^OsUYfp4P#pAXlfd$hlZHby2B6STJbu>Sjo6gK+CXI2Lr{H@?Brj&cjkmp-*JTvm=GlP^nZH&5UQq^$9u! zJbU|GE0+WF&q`V;EkR?%xgxiTr8zBQWx(Utau_7`bh&t-C!nbQhn<}Zvnu1?ZP@-3E* zrc2Rcq!>2)a6Qqq6dCEu^dQSh2_ak@utI2uf(0{lcEGzCZCVxEFkCG4Ua{=fD9`)|GVe(?6H z2iL55Fh~crh!G|BDfTH$)QD(zU^p-ZBm!-nO-PAibYBgYR$~nj&k=wdB4$ zBOMuicHiDN-uSEW!~4k-`^KEpoWR(gUCaYJm!%(j9J=nbcXB|g4L~YbtRm*3q24F7 z?L4sXc|m}=!(EmabOUMnhjvPDgUUQF@B!LH;STTJ`R*#9_I)eY+)c@C4?OqUcb|X$ zyTKRe;o9QE#9rlBJ<)*?afvuTUdrv+vSrVn<}I~#P0h8nTbp--OTNlJo$Kypz^mdT zG(zJBtilYRqeWPO`D~W_;0Mg6O;hGbe<4<&g}h;ECMIBj4R3@-d#rY`L+p%nc9H9@ zV?v>L7j2^*xvsI$SZFFf1B!eqH#SBl$C+G_7UFr3?P`jqz*OUTaB=LzTxh&I*P(PM z-N`3lgK3v%!vc}5l@h-u4XzrdW8NF zFag2p-U&EeRl7EA+O@NFbA3ZgOGEwU)}2Azc2>Fj575iOCGNI0 z?uFI#pP;yUj^mt4Cuje{UhCZ9ul)&^#f+j9*}TramNFy7!GWN&mhF#3`UB1{{QZ&Q zNYKu)!^NSIz#{PP-r??XPPcEeeP8nT9^pV23gA5z`hP*0Vm=lLIzBcMi$wy?P5wwM zUkuu(v&DSA2u*)x&vN;QJOC@?MmEi-(<$CG13#dc8j($dp_5V*l-x?UHZhpQ2VCUt z_mR6^dHEGaQ^8(As|7d(?k0JGY}oJyWrk@11U6u3+-~7=->;`qKcmZT;!yHFM(oR{ z^BCJxERATTBqplKnqraCZ#fv#NFx3PIVGcqiTt9&z3*=3raBAFaueMUZSLyo?rM&% z2R3gmbWL?lf^D;%ZYyk=nj%wETfi*bMz=(#y2$8PIXWAg=^YP^x93}FD{YU3dV70g z?Z9cR`S$V9Sno`16D6DIOnz*9d^A5xHv!atv;T#=a^XnT*A_l~Ki4+Kss#oN4Fzm~ zAzh3F1s1c_l44Sd3sI41&Eu4gFj+05X0*I+T6BaM?8a!WHP8ndyf;NAKouqoA)7Pw zY?jyK30}aBjqy2#b53$<>=b33lk9HJ1jAAX*Z7l84THv|kQUSM2eKyRho72g3Q$Ei zp@t1LOk7SR>^y{>=dxbsuoR*5O~2zN+Im=XMTs)FgK`oFa%9odlsZ-RHI&*_Ps5O( zv(Myo*XAcS+-1PCPKYqA6b}?{xyTPXHWqNMf$wZS`0ExnNt1?{5i@Gm1aW(sy$GBz zLv@2AdHXDGGu@n@gjZn`-2&*}+&!`S;4->;bx%*Ug*F4FS|_^6WcTJ6hAirY_38rZ@xt8MGIZ)Y?>k|ydRpi=}$|IOaa<+6!*P)0oo z6BNHSww3K6Y435aY;IHh*X9atc;3F48a3u5G*Yo zd;lqn`43>sDlE!=Fyvr>9LU>z&|Qi$)W!l7fhmoQ=iLz54-i2iWznK#WikbtNqUwb z)^`E7jI5q@Cj$nY!j=p9YzBHy#sauYAOp0lSI9~kA;V)9q>A%WFaoPunc{qcMAFqw z2#w?>YZ*yBsU|R{so}Osxv=z3ZfbOFa;k5%t2@-!)jbxP1S$Er{Q~EuZh4OFysZio zM{kWdU-P^+`Qyf@XM7&DzxMZ=%d&i|Ic|~spPExm`X4q|F^UhSZ#9P=vW&4{a1eq?zzf3)`mizeaK&Tl-pfj z=l@&lF86ykI3;zvcKhefT@0--6os|jfY#9)-8;_hc9uKpWSv&K@%z)Odf+Ks(?pk%AqjVSQSM<0vkh0%kakCu_Welt7#P@9lV?QDPnAi zyzpxdY72527q#kczh%QQG&6C#VR`8}7Sk^j&~aJQWDu?M5V&5I8&^V^gCP@gPw83K z-5J9acNb{YO|!&(t1{TG^F`gzQjcju4I2<;BM-d4UCwH+Oe=6=1!Dx zB_G38N^vvaF>>yPvn0$HcDM0V+z-)!D6sUmR#7)^%R8fltHgs%bc1cK)2T@sIf14w zT@@s%haF5DQr&osi&G>dU=sfoOwdJt;c1b3%JO5LxopaYWVnRWT;Jpba^n<;=+axl z1otEjYf)HZiL8)wjY!aO@TZw1YBC6^h+9K4;upxKdd@KXqjBScO12lc^?vFS8z|3| zN$H+q08J#}Cbx<)ig$N&5t#+FY93T+%wv-gqZkxK#C{B|YFaslR$xOI%mNPvbDvc} z3{eLCxDh48bP@=ugt|q*-D)^L)o>v*@r^rgmTbxHUZzV?9VdpprM)0726}iwLfNQPd zV#V}m5mo$gmO=FMCkSrQO*6zzK(Q=GalR)`m;6HII#!O*;a$V^Bee6W$pif~vLU>! zAFZx6?i#YWgJn7Hg@kcOxcqiwv&c~{#2K&w%B-T6*SzA6N?m+Q?rN4msCUxjHcG87 zIyBBGUcki_*=`nsx5re}4{^ws2s$joFq@o;6XwR z3E{s?P_M||heRyN8(r~~g`1s#=b79Me>Yb^DBB6H_6I8|rTTU1t`_WBa1SYZJ)?@m z-FGS*T>d5@T~r>2P;Y%8M_m53sH2glr_TAp@5@~2TQNW`wXh;y-wW4dR# zTrk}k3e-ik86Xw)LRr1mFF?4mODhhnDH2yXqubVR9=GfXx2?Pp~Gb* zTAtVZtVFaAy;&j4x;_hnp_DCZSF?`1&AHMD*f;n^zsp@v2IKtab{`kX#frsTzF3Io zBawJ48hBticlE+M`nazzeEbzC{lp1yhG0tIbhybjt7R2wKc9(6As zmBW2pk$_?NI5f5wJjq490SZNLfJorYCD6Bh#j{)_A1@ShxneP%gFEp^z*(@zyO@WI zv0Nk?!;9B`z(w=1Vj&NG#PU%${$WSEp!-F0x6FN-GrVI$=d)<^@FKZp6}^<9yvPd@ z`Rb#L7glqzrjoBd&48M5P0L?ia|palxBPps*ff|L`T5ffo|V%j^6umB9GWkG$N2(B zU%z`NUCzWW;a=Kt*N&Ec#^ZEQ%Bloarcg)-o6A7 znxY9EEEjpWeQU@5#v}bNP~x0#Gu-&((+7Vd9isapJ35Bq*@Tr!CxN{2#Pob3sl8E&|Fqw>Y0(b+LW= zGRC8+%UD3fE)g9f0iECLJ{Y)jB+MARlHz$+<$B(OM*2&u`WR0Yfj-v2sxUPRkoM&0 z@T)**-bkCkXT|VH`=*Y~AXq^0YdV=ZQX@Tr)I=fXQWIIigge{!%|7d@RZPjklrE|S zQR%uF3Z4j~;LTITulRAH2B^aB9eA<3#1p&m_l%*MZV+2Zi#%Di?1NzjEFV$jiSwBA z3GNT}rOvHauI*vkN4YHpCTw7Y}*rVVGb?jrdwxP#(JY$`k(AOMC|LF z1ou?&%omut?OZAL)Q8TeUS_QRiHE5go!!0E7$kOueG+Fg@7_GX{C^}+0Lfdq120^T zR!!dC#jUyj7g)^BoyHw~`R*HUyc_>tK6>=!myZTtalXpsv$_(fN*I-r?|^&vC;Lz3U>omLa2NdUcdYbV?!w=F$l^KA3?~nZ!+c4^V6J8w zSy4+F5iOz&YMnx7vP(=P#FR+{untrcPqC?%l2NishBW+yqj%Fa^lpYm`9w_g4jU&a z$Qd(MjDpxIYgkwOfuRsn;(3}UZ?YgoN`VZ`Q^qr=K&%wa@(uU!86d+)vR#(P0$XBE)ot)q9p?9yo2%Xc3Q9ddKXkILd-XLH;OUDlzbfiYI#Wg>0@sGr%8a0{Ej9_#?^0qR1ih{1F2F z5Gh@OUgZ$*mTR+Q5-x9K6AXCmcu)g4WRdN&|Bbt5;cG+OwF^J;<89|u?z)AzgVa35kcfW$>-oFL5_5%--q#@Y>#Y~_`Z{IY)8Q(3 zrf%{DDp^c11#&p2`|VX5IRzLfxN_ynM^~;icoR2D>~rjM=xtM!{`Cw>I{}L~+F|bI zg+E&C{iC?Lmix*z*L~%JA71<6Z{PXhZ-UD2u6^f1O1hl;xm)1=9dj(Vp@B^#t!yTf z$p-E2#n}v=DNH4T=Wp}h?mWb;zr($qJ$^gk-o9zOKl35i?W{cdqpgJ(Uk1K z_O5#k4z`Nz!6x<2{X+ZmE^vnz=dHct&EX%TfxEnzrRKWC$C_)d5r`rEFMq&+a6vl* zgIKOFj_IuE`MOrLCyqn{^}#!lF>raHFOlUmK z$K$bBGA5*xLefa-V$LvgLwQ*5#WGI~Zbl6qQ-r2xr-q^f{w5k9FTpa27sQ8J%F z&(d~PF+*D%@-egsfNls#-} zP*TbUhvykdlCHb9hUcvcyrz5R$h_x-0D3v_#B-xuTLlkW!w-uA^h!aU^&Ab?-NZxE zvg^%yu(^S6*T<*xewPcfs421+f25$&RN3`f01Iv~0qi6Nr-WuT=wUf2BExtw7B{$n z-VQ*I^Gzfqi2?SIu!ZYnVod?Ah;A;n=w`*^Nj79X40!>HU5+79w(MHESf=wM<3WD+ z5hyZX1P?L*cc^ZB#dFo5Llw#3@(eJOE`pa>h?mmCqA6$OtdgaMl2uE3!5GS+$0Y;B z0d4M{fy>|UkKros002S`81+2x+>lw+ht({&LKdMVYPwdTdwfI#Ftbud%up%;P2KC% z3&oIhmhl`uAecd^7sBzvz%JV1iY`hDabvzpG!r($O1)m6*boN3217xx)SRW|3}9@H z>>GN@*f+2YjjgiQ&o)o>?bY_^`(}3UEszOoN*YUy#fSQ>Ub9E) ziuOeZLeUbZ8WHxO+AR z58U8GqVkl?PEO9uc2Bgm^@Q3bWq<}@fMr_No*QRk?gwJ&2Z0qa!G&N}c0V|MoYT-IJ}z6^tGq?7XaGtyJ) zW~C@+Og--somSI3mEIhQ6eB`R2?L74G(r<}FfI$Xry3;8P)N@20_Z6dcxHqimP<4* zZv(uOTVXz#uV)+|vHTYDphc-@FIICT>wMl%Hi6o_QF)0aY1#C!)0sD9+ znWs6LvDCa9H8qG*P=~;^SY!{{5m!CFVgEDdEhplv0GZ6BK}ORwX<-wJoDJBwgUL(N zbUouOIh8l5p^s1C3rsNZqk6`BCweBRClXwf7uPigB~lcF+zxTh;;4zSGYxeCFtsd- zvRV-Olq$Haxf-bq$b(SW+f#NaBlN2n)BOda%SEr zXob9p86d&{KbRa0xAt_k_lFY$ss8k1gO8Z2r^uR(8}<$!9z1M3v+>~M{#{!RmWcfO z9g6Z5W=I)T#z2Do%6?p-jm}jvy_eZbcLUZ*YCRLB4N4u@BqAH*{+D9W2 zQ5>C#YEdI@!qq+|DWt>{-xKK%MY}=`{Mtfoe%;K5ouOT!{rsN7kX|$k88T5E9V=j- zW(_*C-&2o(40 z*5=JC^~R3+j==}FtbxUPgj?U%x<1gLw9U|+3O>7#lASYcjlo58KJZvZhX(tCP6LZi zVGNux`xid~5wsgvl)wR}!4Jl>7`%>62#IupC%$!nND}85SQ(aW$L^(9(#v}vc6Ymz;rKu=P&woe4V1=$&#@sjJWR*P zhb$c{4UehhC3b8WA6?k%2hUTqc#Ag@TAVoFavtE)V4enk;Rg@L41UOh1CtJ1?oac4 zI%w~4Zst^D1Q{514}98f4L4yDjpfT4In5Q6u7o1GZ$?+sqa_73#-1ita0ACJTY z?2sRgw#+cd;?K{W#6hPtC9UjV+5c#NL$p3xA4yAsfXfP2p|~l$DZF`*xO!a4W^$Qq zW^`z@G+Ht-^$j|d1tCqseN10Ze|KLGh8z`nsHN0eYBds|n@nU3MW6ikoL^;K|dMaLT5T8rjBVZZG@?@AQ#ITULdcgL)}Z;;j`OEt;!_UW|-p8irL ze4r9c6E86sLogI#<9Rg(gq*1IDvv31qB0Pl~+slBYlJkJL(=jscbbfml_gDJh1CKnzV9eoq&bi3n8i}+9onf}E7?}y! z-|^2Bi_<~n1Nuw(1G+I%?4%^`40De^**43BOHnlxujyUeTho&S^}|~vTw(@`5v4!L zi)k?leqe#xH!#DBG90kK=^vrAAM@UvpDgE0G_ce+cD%9u^__XcqM5`@+icrxyO}mp zd`c3M5@t$0jc4#ctC8dk=OyS!Tg}XttVRvco!}|*nN(LS6z_>+3ez7Cz#D#FWhsPrVN;lfw zf*0e5H_wFr#Et4jJ*#)mHjg*vh6hHvip?Z_AxEc#$@D~z*e!L*#5swbqLb3NU`^cFn9ss172{HSWUxN+m~&VDrn zh8?-mUdD}zV=`#Dci4f!uFn1j5?ad9ZoS*=9oHt6DR4$lV!LR!+N-5|g>KqK9{UTI z6^g>BP~18?RT^%LH}`h8l6}rfE+mBcgs^eLj_y72f#L4y@GkPx3>T6^a-TF(Kh-%L zA?E?s>7+hkj(6)}Gp>i*lg+f5wpgu09XVszm@?Ai!UUZlvVq2%;%?gLU{2lkS1y~N zkz@kom6R@@)FD^@h~e>&xJj;kmr3JOa^MBVI#1rY28;qrg3_7xKY(Ayr(C()+KTcQ z%7z)()W+hsh0=L;)|Jwh!KVXl?7Q6@J%mR;OhYiz!BzgW0F8CQ3_9OfY*G-NVBhct zPsv(829?rKElBsVzjZi{y6u8{SZo@=gj<0R*08*Rvb(eDBe&hO-15EU?d8YmfWv0u zxqG~dnpn{Av0s9qq`h-EI!3*Kl`#&WQqt*Miibt<0IZIW?Sp>2|B1F45qt{dG`sFu(%9S@iK}Q&0TN* zoYP>%fiR|Qs;B3XWEkU`NqMcbT3#cOPJap@m(!p|6;X_X5sT3<^!6~VH3!pFl8PYn zVtF5&IQROK_<%QE)TpUu+6z3n)x+A=? zWn;6}NE77N8Pla)r3G3mlM?>pH+&C31Z!&*&PgOw1|y(0-45|B5!}D(8rORol;gs`z_{r7a`l z*<8^qx+y_mGLMgyCg}tn9W+CRin%4ENQ>!gd?X|_hx)yZZL;baPU558@$Tu&WY)kK z3G3!e;CVkxVfG$k=^i_Bcb<4RTp^Ryd$}l>-9c|h9N1swv!Ps9Zew;#HJ}%REhMth zz}IWIWI~B#ljY0<#cxRC>5Zw_#>A+E^;kh?c-_lyN#lO$>&`4^f6=LQzKHpES8>~% zFKl4#8Rsgzx&W^R+^c~NU@LvbzLrB>9*nY~0em3%YOrC4$AZ-MZ)gXNhwg7 zJbxG;wW{cDCWt1H=O1HC-hJ3pI!V(ySwBl1Kf}BB=W0Jl7b93;(IyBuioJwG3jr>8 zhVW|t*f1C()M2;HGQdHy!Ru+7O!MS{#~3di1fwycN**{2GMi=$Q#Y~-Nb2ov)Mgo! za0n|eh&+If&vg$hde83VmM^sLJIp=v2az_+tjvgh@aCB zLl*!%HAN#7eop^9EC$pth4?w|i|5|(g-T-`E3c+=JVIt0=#c3GqJxjsCF=LKZm-G% zWvWB|0x34}sH4ogBo9NTvCw$smAa;-H*5LN^2)i&Uv&U;Lyh06FreH@hS7H{SxDby z^^Zv7wU9Xg%<%8up}g%-=R=)pu+m#cU8AH+%EmX}ewAz>TfQRSl8MrZ;i02L$BU;- z2%FLl@gc@6ki3G=wzAwN^*$W!*wKpUWYO9ERD&)b4k_gGxUY=%XAjEJSZ9~kLAuFX zr+0oyJ|~kC_Bq3M^PY}DBKmlpU1j955F4*v$3kq4S9Ssd0Tfo{=`eBV;%Mb9N6%^a z4@k;9R{0b9GHeYhDGcM~1EMBpvLewgA`6WWBIY%*Ls2Y)TJ6x=Z(Ioqw=2M&@CIA zHnQ^$9i2h$#6AOYkWYds!RbpAFn5sEAg zKGeGGfx%@<9}TQr5qJuo5Ak5&iNJG#=D_yAzJL`N4V(#F3H%%k1|JEo2{s0|2HS!? zH7je@)aAt`|(_<2nzEp(Zvncw>q)7L>+~QG-G;a~63vxOwNgyOimjbBA^wJKsiqtAPX&;2;s70z5An5@LNT3Vm$zb7f zlulKSGR@^Eh0B+bZ4+0^ab-X{D|eis(_SX5Oet-vSeO^-YMK}65@Cl!3po0oDB=4ID5%xe+|GdF&inQ6Sy(9qb}&~W3$nR?;Y_2%c-HZ`wZ+jQ;L zPuH*Asuv*HP?gBf;w^{hGOYr9rW*>~JV-{&=&_Qx(25Ktqo`S&&Zq><W9j z1e>8za@k|aAl=Gf0iq0$(2KIzqg0LvWLO@EmZRlv_aLi#JV?6bC_UtE&ONb4nqnjQfUdn4e$C&a~R;Xh*0pP6twbMk``V|LR+C)AtGp1e3}pk@$zRJ3lWwE z!X!yH^bv9qafo^W6>V}gpZip_Ze$ix(X`VA1iAv`e^F|SgL53EHdyL$9;F7?04TN2 zkVAlGHI|e54@%9@T2Or9Y$u1QIZDlGt~6Il5E8yvS_CTziGo-va#AUo08&Zl@9!TN z94_@9F$!u~@siXt>)DNDFP?oiOuEvEsKTZy{f~$_+3OzaIvVTaVIPh%d1`pfgmKu6 zm-T*JLE)hz<>3fa+ELfSLwhVpq9n>9Q(}`ffMh{JLz`i z4q2V{PW8R`ZZ#g)k{(uaS;zA2qFpqLW(nl7u%Pdd-i_|kL|Kt}1ZXHgd*(7ZgMN+5 zr<_TB5|at}lf+52V0!f1o`W47=*e4(f57dq4;V@MN?};7428NA0(+~eI|G#`U#hH7 zy`(KFqMAwMrK_`{=xp6P>r}}~_$p=`FTsK<8*xdJlO@O^VM7`E7Hvm-7&SdwYRmkV^NHel;1T-Uqh(^>GSDP*8LQo z+{F-6lTIlX(5(;iP;82aVmHJ0A91n18tP5)P>;7ngoT5F!9|3B0g;o5R{#J2c${Nk zU|?hbf-|;@&V%UdoMy9OGyp_X1>OJvc${NkWME+617ZmV5MW|p1j5Nc%mU^y000V` z0LB0Sc${NkW@2ERz`)AD!RW)7#=yYf4yC^`NHVlAFfcK&ax%aGqW}W}1f;p9FgP$M zJ}_Z?@c#jW9urgzn3&4g&7c5;i~#9L3oZZvc$~G6En&B%vW0Eh(guOFFM)kVzgovdO_gI}KFg!a%IhA`|N6cnd{7Lbo6O zC{Az#(TGam)#)TyV}MSKRQ(EqC1Wz>_dc@on0#VJs{%m8_Rq z$}5~z*6Nxpds=PX(6G6zSYMFeSZ`}m$HcjxU&bcm6B3g&$=a0E-1OIs%sgFoj-$Py z(q)L1WL|P_g^#bFzcL^&C^$qFdL0%X5g8TT+|kz3nNfL@8){-p^ z;-J#d*4B_@l%`;1gD7qef(}VE1|* z5kolfV`E<`BMhls?1TxDq{}aFeDXkpEH!bk#!Fc#IXT@d=#ZgpBw46oIO3^{EJb=rV-oSJqG^|ZW=mOoNV|0Ly(E-}^v_B|l z;lR&)|C=}eya6!51s2+6VT`%(04}t!jU~>)4m~&FA^9QfVr)(6w44!@S$C`D0p~A- zZEUa>cIdeZ56O37m;6KtgDuNLfgMiRQ#k;o*&46do%SW0pOcT6qDF;dw>PxpQ2d6E zfP2FFKlpv75mS#?n*I&KMeFb2rV=wY_5YpHuaK2`u2kp;z2>|CydFgpc${NkW

-
  • -
    - -
  • -
  • -
    - -
  • @@ -179,10 +171,6 @@
  • -
  • -
    - -
  • @@ -399,10 +387,6 @@
  • -
  • -
    - -
  • @@ -603,6 +587,22 @@
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • Character mapping

      @@ -718,14 +718,6 @@
      -
    • -
      - -
    • -
    • -
      - -
    • @@ -770,10 +762,6 @@
    • -
    • -
      - -
    • @@ -990,10 +978,6 @@
    • -
    • -
      - -
    • @@ -1194,6 +1178,22 @@
    • +
    • +
      + +
    • +
    • +
      + +
    • +
    • +
      + +
    • +
    • +
      + +