From 0fe4e42511cbb67464131279ba6c44df3e209601 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 1 May 2016 13:53:44 -0700 Subject: [PATCH 01/77] added zone properties to allow flying/ghosting or not --- .../entities/src/EntityItemProperties.cpp | 27 +++++++++++++++++++ libraries/entities/src/EntityItemProperties.h | 6 +++++ libraries/entities/src/EntityPropertyFlags.h | 3 +++ libraries/entities/src/ZoneEntityItem.cpp | 24 ++++++++++++++--- libraries/entities/src/ZoneEntityItem.h | 16 ++++++++--- .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 1 + scripts/system/html/entityProperties.html | 20 +++++++++++++- 8 files changed, 91 insertions(+), 8 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 92849d6e2f..feacb4ab98 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -309,6 +309,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_QUERY_AA_CUBE, queryAACube); CHECK_PROPERTY_CHANGE(PROP_LOCAL_POSITION, localPosition); CHECK_PROPERTY_CHANGE(PROP_LOCAL_ROTATION, localRotation); + CHECK_PROPERTY_CHANGE(PROP_FLYING_ALLOWED, flyingAllowed); + CHECK_PROPERTY_CHANGE(PROP_GHOSTING_ALLOWED, ghostingAllowed); changedProperties += _animation.getChangedProperties(); changedProperties += _keyLight.getChangedProperties(); @@ -467,6 +469,9 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool _stage.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); _skybox.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); + + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FLYING_ALLOWED, flyingAllowed); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GHOSTING_ALLOWED, ghostingAllowed); } // Web only @@ -679,6 +684,9 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslationsSet, qVectorBool, setJointTranslationsSet); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslations, qVectorVec3, setJointTranslations); + COPY_PROPERTY_FROM_QSCRIPTVALUE(flyingAllowed, bool, setFlyingAllowed); + COPY_PROPERTY_FROM_QSCRIPTVALUE(ghostingAllowed, bool, setGhostingAllowed); + _lastEdited = usecTimestampNow(); } @@ -849,6 +857,9 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_HOUR, Stage, stage, Hour, hour); ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_AUTOMATIC_HOURDAY, Stage, stage, AutomaticHourDay, automaticHourDay); + ADD_PROPERTY_TO_MAP(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool); + ADD_PROPERTY_TO_MAP(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool); + // FIXME - these are not yet handled //ADD_PROPERTY_TO_MAP(PROP_CREATED, Created, created, quint64); @@ -1089,6 +1100,9 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem _staticSkybox.setProperties(properties); _staticSkybox.appendToEditPacket(packetData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); + + APPEND_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, properties.getFlyingAllowed()); + APPEND_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, properties.getGhostingAllowed()); } if (properties.getType() == EntityTypes::PolyVox) { @@ -1373,6 +1387,9 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_BACKGROUND_MODE, BackgroundMode, setBackgroundMode); properties.getSkybox().decodeFromEditPacket(propertyFlags, dataAt , processedBytes); + + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FLYING_ALLOWED, bool, setFlyingAllowed); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed); } if (properties.getType() == EntityTypes::PolyVox) { @@ -1564,6 +1581,9 @@ void EntityItemProperties::markAllChanged() { _jointTranslationsChanged = true; _queryAACubeChanged = true; + + _flyingAllowedChanged = true; + _ghostingAllowedChanged = true; } // The minimum bounding box for the entity. @@ -1885,6 +1905,13 @@ QList EntityItemProperties::listChangedProperties() { out += "queryAACube"; } + if (flyingAllowedChanged()) { + out += "flyingAllowed"; + } + if (ghostingAllowedChanged()) { + out += "ghostingAllowed"; + } + getAnimation().listChangedProperties(out); getKeyLight().listChangedProperties(out); getSkybox().listChangedProperties(out); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 2cf31e5632..f32bc977e0 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -205,6 +205,9 @@ public: DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector, QVector()); DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS, JointTranslations, jointTranslations, QVector, QVector()); + DEFINE_PROPERTY(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool, ZoneEntityItem::DEFAULT_FLYING_ALLOWED); + DEFINE_PROPERTY(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool, ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED); + static QString getBackgroundModeString(BackgroundMode mode); @@ -421,6 +424,9 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, JointTranslationsSet, jointTranslationsSet, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, JointTranslations, jointTranslations, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, FlyingAllowed, flyingAllowed, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, GhostingAllowed, ghostingAllowed, ""); + properties.getAnimation().debugDump(); properties.getSkybox().debugDump(); properties.getStage().debugDump(); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 90a7c1e2f7..19b47da8e7 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -169,6 +169,9 @@ enum EntityPropertyList { PROP_FALLOFF_RADIUS, // for Light entity + PROP_FLYING_ALLOWED, // can avatars in a zone fly? + PROP_GHOSTING_ALLOWED, // can avatars in a zone turn off physics? + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 6aabef5cc0..a28b8210c2 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -23,8 +23,12 @@ bool ZoneEntityItem::_zonesArePickable = false; bool ZoneEntityItem::_drawZoneBoundaries = false; + const ShapeType ZoneEntityItem::DEFAULT_SHAPE_TYPE = SHAPE_TYPE_BOX; const QString ZoneEntityItem::DEFAULT_COMPOUND_SHAPE_URL = ""; +const bool ZoneEntityItem::DEFAULT_FLYING_ALLOWED = true; +const bool ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED = true; + EntityItemPointer ZoneEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity { new ZoneEntityItem(entityID) }; @@ -55,6 +59,9 @@ EntityItemProperties ZoneEntityItem::getProperties(EntityPropertyFlags desiredPr _skyboxProperties.getProperties(properties); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(flyingAllowed, getFlyingAllowed); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(ghostingAllowed, getGhostingAllowed); + return properties; } @@ -70,6 +77,9 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(backgroundMode, setBackgroundMode); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(flyingAllowed, setFlyingAllowed); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(ghostingAllowed, setGhostingAllowed); + bool somethingChangedInSkybox = _skyboxProperties.setProperties(properties); somethingChanged = somethingChanged || somethingChangedInKeyLight || somethingChangedInStage || somethingChangedInSkybox; @@ -116,6 +126,9 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, bytesRead += bytesFromSkybox; dataAt += bytesFromSkybox; + READ_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, bool, setFlyingAllowed); + READ_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed); + return bytesRead; } @@ -125,13 +138,16 @@ EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& p EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += _keyLightProperties.getEntityProperties(params); - + requestedProperties += PROP_SHAPE_TYPE; requestedProperties += PROP_COMPOUND_SHAPE_URL; requestedProperties += PROP_BACKGROUND_MODE; requestedProperties += _stageProperties.getEntityProperties(params); requestedProperties += _skyboxProperties.getEntityProperties(params); - + + requestedProperties += PROP_FLYING_ALLOWED; + requestedProperties += PROP_GHOSTING_ALLOWED; + return requestedProperties; } @@ -155,10 +171,12 @@ void ZoneEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)getShapeType()); APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, getCompoundShapeURL()); APPEND_ENTITY_PROPERTY(PROP_BACKGROUND_MODE, (uint32_t)getBackgroundMode()); // could this be a uint16?? - + _skyboxProperties.appendSubclassData(packetData, params, modelTreeElementExtraEncodeData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); + APPEND_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, getFlyingAllowed()); + APPEND_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, getGhostingAllowed()); } void ZoneEntityItem::debugDump() const { diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 0326a0f441..56968aa9c9 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -70,6 +70,11 @@ public: const SkyboxPropertyGroup& getSkyboxProperties() const { return _skyboxProperties; } const StagePropertyGroup& getStageProperties() const { return _stageProperties; } + bool getFlyingAllowed() const { return _flyingAllowed; } + void setFlyingAllowed(bool value) { _flyingAllowed = value; } + bool getGhostingAllowed() const { return _ghostingAllowed; } + void setGhostingAllowed(bool value) { _ghostingAllowed = value; } + virtual bool supportsDetailedRayIntersection() const { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, @@ -80,18 +85,23 @@ public: static const ShapeType DEFAULT_SHAPE_TYPE; static const QString DEFAULT_COMPOUND_SHAPE_URL; - + static const bool DEFAULT_FLYING_ALLOWED; + static const bool DEFAULT_GHOSTING_ALLOWED; + protected: KeyLightPropertyGroup _keyLightProperties; - + ShapeType _shapeType = DEFAULT_SHAPE_TYPE; QString _compoundShapeURL; - + BackgroundMode _backgroundMode = BACKGROUND_MODE_INHERIT; StagePropertyGroup _stageProperties; SkyboxPropertyGroup _skyboxProperties; + bool _flyingAllowed { DEFAULT_FLYING_ALLOWED }; + bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED }; + static bool _drawZoneBoundaries; static bool _zonesArePickable; }; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e4aab94090..6b917df87a 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -47,7 +47,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_LIGHT_HAS_FALLOFF_RADIUS; + return VERSION_ENTITIES_NO_FLY_ZONES; case PacketType::AvatarData: case PacketType::BulkAvatarData: return static_cast(AvatarMixerPacketVersion::SoftAttachmentSupport); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index b98a87e439..42e5fff420 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -171,6 +171,7 @@ const PacketVersion VERSION_ENTITITES_HAVE_QUERY_BOX = 54; const PacketVersion VERSION_ENTITITES_HAVE_COLLISION_MASK = 55; const PacketVersion VERSION_ATMOSPHERE_REMOVED = 56; const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; +const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index ae5684d6c8..35a78682f3 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -484,6 +484,9 @@ var elZoneSkyboxColorGreen = document.getElementById("property-zone-skybox-color-green"); var elZoneSkyboxColorBlue = document.getElementById("property-zone-skybox-color-blue"); var elZoneSkyboxURL = document.getElementById("property-zone-skybox-url"); + + var elZoneFlyingAllowed = document.getElementById("property-zone-flying-allowed"); + var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed"); var elPolyVoxSections = document.querySelectorAll(".poly-vox-section"); allSections.push(elPolyVoxSections); @@ -770,6 +773,9 @@ elZoneSkyboxColorGreen.value = properties.skybox.color.green; elZoneSkyboxColorBlue.value = properties.skybox.color.blue; elZoneSkyboxURL.value = properties.skybox.url; + + elZoneFlyingAllowed.checked = properties.flyingAllowed; + elZoneGhostingAllowed.checked = properties.ghostingAllowed; showElements(document.getElementsByClassName('skybox-section'), elZoneBackgroundMode.value == 'skybox'); } else if (properties.type == "PolyVox") { @@ -1076,7 +1082,10 @@ })); elZoneSkyboxURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('skybox','url')); - + + elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); + elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); + var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); @@ -1791,6 +1800,15 @@ +
+ + +
+
+ + +
+
M From ef85cc7803b0360f04ee0196b044a409096c9e23 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 1 May 2016 14:47:12 -0700 Subject: [PATCH 02/77] hook up zone flyingAllowed flag to character controller --- interface/src/avatar/MyAvatar.cpp | 6 ++++++ .../entities-renderer/src/EntityTreeRenderer.h | 2 ++ libraries/physics/src/CharacterController.cpp | 14 ++++++++++++++ libraries/physics/src/CharacterController.h | 5 +++++ 4 files changed, 27 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 614f7bd9fe..e039633961 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -426,7 +426,12 @@ void MyAvatar::simulate(float deltaTime) { EntityTreeRenderer* entityTreeRenderer = qApp->getEntities(); EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; if (entityTree) { + bool flyingAllowed = true; entityTree->withWriteLock([&] { + std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); + if (zone) { + flyingAllowed = zone->getFlyingAllowed(); + } auto now = usecTimestampNow(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); MovingEntitiesOperator moveOperator(entityTree); @@ -454,6 +459,7 @@ void MyAvatar::simulate(float deltaTime) { entityTree->recurseTreeWithOperator(&moveOperator); } }); + _characterController.setFlyingAllowed(flyingAllowed); } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a6fc58e5f1..3eb3796c94 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -88,6 +88,8 @@ public: // For Scene.shouldRenderEntities QList& getEntitiesLastInScene() { return _entityIDsLastInScene; } + std::shared_ptr myAvatarZone() { return _bestZone; } + signals: void mousePressOnEntity(const RayToEntityIntersectionResult& intersection, const QMouseEvent* event); void mousePressOffEntity(const RayToEntityIntersectionResult& intersection, const QMouseEvent* event); diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index f685aee748..b9306b67b0 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -294,6 +294,10 @@ void CharacterController::setState(State desiredState, const char* reason) { #else void CharacterController::setState(State desiredState) { #endif + if (!_flyingAllowed && desiredState == State::Hover) { + desiredState = State::InAir; + } + if (desiredState != _state) { #ifdef DEBUG_STATE_CHANGE qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason; @@ -544,3 +548,13 @@ bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPositio avatarRigidBodyRotation = bulletToGLM(worldTrans.getRotation()); return true; } + +void CharacterController::setFlyingAllowed(bool value) { + if (_flyingAllowed != value) { + _flyingAllowed = value; + + if (!_flyingAllowed && _state == State::Hover) { + SET_STATE(State::InAir, "flying not allowed"); + } + } +} diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h index d810e904a7..eac8379ad0 100644 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -98,6 +98,9 @@ public: bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); + void setFlyingAllowed(bool value); + + protected: #ifdef DEBUG_STATE_CHANGE void setState(State state, const char* reason); @@ -147,6 +150,8 @@ protected: btRigidBody* _rigidBody { nullptr }; uint32_t _pendingFlags { 0 }; uint32_t _previousFlags { 0 }; + + bool _flyingAllowed { true }; }; #endif // hifi_CharacterControllerInterface_h From 4feb2944edaab41830588df38da2a12e788f401f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 1 May 2016 15:27:49 -0700 Subject: [PATCH 03/77] hook up zone's ghostingAllowed flag --- interface/src/avatar/MyAvatar.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e039633961..2513bb7849 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -427,10 +427,12 @@ void MyAvatar::simulate(float deltaTime) { EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; if (entityTree) { bool flyingAllowed = true; + bool ghostingAllowed = true; entityTree->withWriteLock([&] { std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); if (zone) { flyingAllowed = zone->getFlyingAllowed(); + ghostingAllowed = zone->getGhostingAllowed(); } auto now = usecTimestampNow(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); @@ -460,6 +462,9 @@ void MyAvatar::simulate(float deltaTime) { } }); _characterController.setFlyingAllowed(flyingAllowed); + if (!_characterController.isEnabled() && !ghostingAllowed) { + _characterController.setEnabled(true); + } } } @@ -1839,7 +1844,21 @@ void MyAvatar::updateMotionBehaviorFromMenu() { } else { _motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED; } - _characterController.setEnabled(menu->isOptionChecked(MenuOption::EnableCharacterController)); + + bool ghostingAllowed = true; + EntityTreeRenderer* entityTreeRenderer = qApp->getEntities(); + if (entityTreeRenderer) { + std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); + if (zone) { + ghostingAllowed = zone->getGhostingAllowed(); + } + } + bool checked = menu->isOptionChecked(MenuOption::EnableCharacterController); + if (!ghostingAllowed) { + checked = true; + } + + _characterController.setEnabled(checked); } void MyAvatar::clearDriveKeys() { From 0e6d9a1eecbbc82da30a5409f0c30912750ecbac Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 May 2016 14:48:31 -0700 Subject: [PATCH 04/77] avatar mixer can relay "client-only" entities between interfaces -- the entity server wont know about them. --- interface/src/Application.cpp | 6 ++ interface/src/avatar/Avatar.cpp | 85 +++++++++++++++++ interface/src/avatar/Avatar.h | 1 + interface/src/avatar/MyAvatar.cpp | 28 +++++- libraries/avatars/src/AvatarData.cpp | 93 ++++++++++++++++++- libraries/avatars/src/AvatarData.h | 20 ++++ libraries/avatars/src/AvatarHashMap.cpp | 9 +- .../src/RenderableWebEntityItem.cpp | 5 + .../entities/src/EntityEditPacketSender.cpp | 45 ++++++++- .../entities/src/EntityEditPacketSender.h | 11 ++- libraries/entities/src/EntityItem.h | 14 ++- libraries/entities/src/EntityItemProperties.h | 9 ++ .../entities/src/EntityScriptingInterface.cpp | 17 ++-- .../entities/src/EntityScriptingInterface.h | 2 +- libraries/entities/src/EntityTree.cpp | 5 +- libraries/physics/src/EntityMotionState.cpp | 12 ++- 16 files changed, 336 insertions(+), 26 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4a829b3191..0b1154d983 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -796,6 +796,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Tell our entity edit sender about our known jurisdictions _entityEditSender.setServerJurisdictions(&_entityServerJurisdictions); + _entityEditSender.setMyAvatar(getMyAvatar()); // For now we're going to set the PPS for outbound packets to be super high, this is // probably not the right long term solution. But for now, we're going to do this to @@ -1087,6 +1088,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Make sure we don't time out during slow operations at startup updateHeartbeat(); + OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender(); + EntityEditPacketSender* entityPacketSender = static_cast(packetSender); + entityPacketSender->setMyAvatar(getMyAvatar()); + connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); @@ -4186,6 +4191,7 @@ void Application::clearDomainOctreeDetails() { qCDebug(interfaceapp) << "Clearing domain octree details..."; resetPhysicsReadyInformation(); + getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities // reset our node to stats and node to jurisdiction maps... since these must be changing... _entityServerJurisdictions.withWriteLock([&] { diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 2cacb81ce4..0090dcedf6 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include "Application.h" #include "Avatar.h" @@ -160,6 +161,89 @@ void Avatar::animateScaleChanges(float deltaTime) { } } +void Avatar::updateAvatarEntities() { + // - if queueEditEntityMessage sees clientOnly flag it does _myAvatar->updateAvatarEntity() + // - updateAvatarEntity saves the bytes and sets _avatarEntityDataLocallyEdited + // - MyAvatar::update notices _avatarEntityDataLocallyEdited and calls sendIdentityPacket + // - sendIdentityPacket sends the entity bytes to the server which relays them to other interfaces + // - AvatarHashMap::processAvatarIdentityPacket's on other interfaces call avatar->setAvatarEntityData() + // - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true + // - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are... + + quint64 now = usecTimestampNow(); + + const static quint64 refreshTime = 3 * USECS_PER_SECOND; + if (!_avatarEntityDataChanged && now - _avatarEntityChangedTime < refreshTime) { + return; + } + + EntityTreeRenderer* treeRenderer = qApp->getEntities(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (!entityTree) { + return; + } + + bool success = true; + QScriptEngine scriptEngine; + entityTree->withWriteLock([&] { + AvatarEntityMap avatarEntities = getAvatarEntityData(); + for (auto entityID : avatarEntities.keys()) { + // see EntityEditPacketSender::queueEditEntityMessage for the other end of this. unpack properties + // and either add or update the entity. + QByteArray jsonByteArray = avatarEntities.value(entityID); + QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(jsonByteArray); + if (!jsonProperties.isObject()) { + qDebug() << "got bad avatarEntity json"; + continue; + } + QVariant variantProperties = jsonProperties.toVariant(); + QVariantMap asMap = variantProperties.toMap(); + QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); + EntityItemProperties properties; + EntityItemPropertiesFromScriptValueHonorReadOnly(scriptProperties, properties); + properties.setClientOnly(true); + properties.setOwningAvatarID(getID()); + + if (properties.getParentID() == AVATAR_SELF_ID) { + properties.setParentID(getID()); + } + + EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + + if (entity) { + if (!entityTree->updateEntity(entityID, properties)) { + qDebug() << "AVATAR-ENTITES -- updateEntity failed: " << properties.getType(); + success = false; + } + } else { + entity = entityTree->addEntity(entityID, properties); + if (!entity) { + qDebug() << "AVATAR-ENTITES -- addEntity failed: " << properties.getType(); + success = false; + } + } + } + }); + + AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); + foreach (auto entityID, recentlyDettachedAvatarEntities) { + if (!_avatarEntityData.contains(entityID)) { + EntityItemPointer dettachedEntity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (dettachedEntity) { + // this will cause this interface to listen to data from the entity-server about this entity. + dettachedEntity->setClientOnly(false); + } + } + } + + if (success) { + setAvatarEntityDataChanged(false); + _avatarEntityChangedTime = now; + } +} + + + void Avatar::simulate(float deltaTime) { PerformanceTimer perfTimer("simulate"); @@ -228,6 +312,7 @@ void Avatar::simulate(float deltaTime) { simulateAttachments(deltaTime); updatePalms(); + updateAvatarEntities(); } bool Avatar::isLookingAtMe(AvatarSharedPointer avatar) const { diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index cb35fbb5eb..ded5ee6433 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -64,6 +64,7 @@ public: typedef std::shared_ptr PayloadPointer; void init(); + void updateAvatarEntities(); void simulate(float deltaTime); virtual void simulateAttachments(float deltaTime); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index bad60643ec..f9daad923b 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -311,6 +311,10 @@ void MyAvatar::update(float deltaTime) { head->setAudioLoudness(audio->getLastInputLoudness()); head->setAudioAverageLoudness(audio->getAudioAverageInputLoudness()); + if (_avatarEntityDataLocallyEdited) { + sendIdentityPacket(); + } + simulate(deltaTime); currentEnergy += energyChargeRate; @@ -443,7 +447,7 @@ void MyAvatar::simulate(float deltaTime) { EntityItemProperties properties = entity->getProperties(); properties.setQueryAACubeDirty(); properties.setLastEdited(now); - packetSender->queueEditEntityMessage(PacketType::EntityEdit, entity->getID(), properties); + packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entity->getID(), properties); entity->setLastBroadcast(usecTimestampNow()); } } @@ -455,6 +459,8 @@ void MyAvatar::simulate(float deltaTime) { } }); } + + updateAvatarEntities(); } // thread-safe @@ -696,6 +702,16 @@ void MyAvatar::saveData() { } settings.endArray(); + settings.beginWriteArray("avatarEntityData"); + int avatarEntityIndex = 0; + for (auto entityID : _avatarEntityData.keys()) { + settings.setArrayIndex(avatarEntityIndex); + settings.setValue("id", entityID); + settings.setValue("properties", _avatarEntityData.value(entityID)); + avatarEntityIndex++; + } + settings.endArray(); + settings.setValue("displayName", _displayName); settings.setValue("collisionSoundURL", _collisionSoundURL); settings.setValue("useSnapTurn", _useSnapTurn); @@ -807,6 +823,16 @@ void MyAvatar::loadData() { settings.endArray(); setAttachmentData(attachmentData); + int avatarEntityCount = settings.beginReadArray("avatarEntityData"); + for (int i = 0; i < avatarEntityCount; i++) { + settings.setArrayIndex(i); + QUuid entityID = settings.value("id").toUuid(); + QByteArray properties = settings.value("properties").toByteArray(); + updateAvatarEntity(entityID, properties); + } + settings.endArray(); + setAvatarEntityDataChanged(true); + setDisplayName(settings.value("displayName").toString()); setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index fd13f8c370..9a0fc1a835 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -962,8 +962,9 @@ bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray& data) { QUrl unusedModelURL; // legacy faceModel support QUrl skeletonModelURL; QVector attachmentData; + AvatarEntityMap avatarEntityData; QString displayName; - packetStream >> avatarUUID >> unusedModelURL >> skeletonModelURL >> attachmentData >> displayName; + packetStream >> avatarUUID >> unusedModelURL >> skeletonModelURL >> attachmentData >> displayName >> avatarEntityData; bool hasIdentityChanged = false; @@ -983,6 +984,11 @@ bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray& data) { hasIdentityChanged = true; } + if (avatarEntityData != _avatarEntityData) { + setAvatarEntityData(avatarEntityData); + hasIdentityChanged = true; + } + return hasIdentityChanged; } @@ -994,7 +1000,7 @@ QByteArray AvatarData::identityByteArray() { QUrl unusedModelURL; // legacy faceModel support - identityStream << QUuid() << unusedModelURL << urlToSend << _attachmentData << _displayName; + identityStream << QUuid() << unusedModelURL << urlToSend << _attachmentData << _displayName << _avatarEntityData; return identityData; } @@ -1202,6 +1208,8 @@ void AvatarData::sendIdentityPacket() { [&](const SharedNodePointer& node) { nodeList->sendPacketList(std::move(packetList), *node); }); + + _avatarEntityDataLocallyEdited = false; } void AvatarData::sendBillboardPacket() { @@ -1389,6 +1397,7 @@ static const QString JSON_AVATAR_HEAD_MODEL = QStringLiteral("headModel"); static const QString JSON_AVATAR_BODY_MODEL = QStringLiteral("bodyModel"); static const QString JSON_AVATAR_DISPLAY_NAME = QStringLiteral("displayName"); static const QString JSON_AVATAR_ATTACHEMENTS = QStringLiteral("attachments"); +static const QString JSON_AVATAR_ENTITIES = QStringLiteral("attachedEntities"); static const QString JSON_AVATAR_SCALE = QStringLiteral("scale"); QJsonValue toJsonValue(const JointData& joint) { @@ -1427,6 +1436,17 @@ QJsonObject AvatarData::toJson() const { root[JSON_AVATAR_ATTACHEMENTS] = attachmentsJson; } + if (!_avatarEntityData.empty()) { + QJsonArray avatarEntityJson; + for (auto entityID : _avatarEntityData.keys()) { + QVariantMap entityData; + entityData.insert("id", entityID); + entityData.insert("properties", _avatarEntityData.value(entityID)); + avatarEntityJson.push_back(QVariant(entityData).toJsonObject()); + } + root[JSON_AVATAR_ENTITIES] = avatarEntityJson; + } + auto recordingBasis = getRecordingBasis(); bool success; Transform avatarTransform = getTransform(success); @@ -1526,6 +1546,13 @@ void AvatarData::fromJson(const QJsonObject& json) { setAttachmentData(attachments); } + // if (json.contains(JSON_AVATAR_ENTITIES) && json[JSON_AVATAR_ENTITIES].isArray()) { + // QJsonArray attachmentsJson = json[JSON_AVATAR_ATTACHEMENTS].toArray(); + // for (auto attachmentJson : attachmentsJson) { + // // TODO -- something + // } + // } + // Joint rotations are relative to the avatar, so they require no basis correction if (json.contains(JSON_AVATAR_JOINT_ARRAY)) { QVector jointArray; @@ -1678,9 +1705,69 @@ void AvatarData::setAttachmentsVariant(const QVariantList& variant) { QVector newAttachments; newAttachments.reserve(variant.size()); for (const auto& attachmentVar : variant) { - AttachmentData attachment; + AttachmentData attachment; attachment.fromVariant(attachmentVar); newAttachments.append(attachment); } setAttachmentData(newAttachments); } + +void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "updateAvatarEntity", Q_ARG(const QUuid&, entityID), Q_ARG(QByteArray, entityData)); + return; + } + _avatarEntityData.insert(entityID, entityData); + _avatarEntityDataLocallyEdited = true; +} + +void AvatarData::clearAvatarEntity(const QUuid& entityID) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "clearAvatarEntity", Q_ARG(const QUuid&, entityID)); + return; + } + _avatarEntityData.remove(entityID); + _avatarEntityDataLocallyEdited = true; +} + +AvatarEntityMap AvatarData::getAvatarEntityData() const { + if (QThread::currentThread() != thread()) { + AvatarEntityMap result; + QMetaObject::invokeMethod(const_cast(this), "getAvatarEntityData", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(AvatarEntityMap, result)); + return result; + } + return _avatarEntityData; +} + +void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setAvatarEntityData", Q_ARG(const AvatarEntityMap&, avatarEntityData)); + return; + } + if (_avatarEntityData != avatarEntityData) { + // keep track of entities that were attached to this avatar but no longer are + AvatarEntityIDs previousAvatarEntityIDs = QSet::fromList(_avatarEntityData.keys()); + + _avatarEntityData = avatarEntityData; + setAvatarEntityDataChanged(true); + + foreach (auto entityID, previousAvatarEntityIDs) { + if (!_avatarEntityData.contains(entityID)) { + _avatarEntityDetached.insert(entityID); + } + } + } +} + +AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() { + if (QThread::currentThread() != thread()) { + AvatarEntityIDs result; + QMetaObject::invokeMethod(const_cast(this), "getRecentlyDetachedIDs", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(AvatarEntityIDs, result)); + return result; + } + AvatarEntityIDs result = _avatarEntityDetached; + _avatarEntityDetached.clear(); + return result; +} diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 900da38ffa..72d34af9d9 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -61,6 +61,8 @@ typedef unsigned long long quint64; using AvatarSharedPointer = std::shared_ptr; using AvatarWeakPointer = std::weak_ptr; using AvatarHash = QHash; +using AvatarEntityMap = QMap; +using AvatarEntityIDs = QSet; using AvatarDataSequenceNumber = uint16_t; @@ -135,6 +137,10 @@ class AttachmentData; class Transform; using TransformPointer = std::shared_ptr; +// When writing out avatarEntities to a QByteArray, if the parentID is the ID of MyAvatar, use this ID instead. This allows +// the value to be reset when the sessionID changes. +const QUuid AVATAR_SELF_ID = QUuid("{00000000-0000-0000-0000-000000000001}"); + class AvatarData : public QObject, public SpatiallyNestable { Q_OBJECT @@ -274,6 +280,9 @@ public: Q_INVOKABLE QVariantList getAttachmentsVariant() const; Q_INVOKABLE void setAttachmentsVariant(const QVariantList& variant); + Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); + Q_INVOKABLE void clearAvatarEntity(const QUuid& entityID); + void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; } // key state @@ -333,6 +342,11 @@ public: glm::vec3 getClientGlobalPosition() { return _globalPosition; } + Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const; + Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); + void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; } + AvatarEntityIDs getAndClearRecentlyDetachedIDs(); + public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); @@ -405,6 +419,12 @@ protected: // updates about one avatar to another. glm::vec3 _globalPosition; + AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar + AvatarEntityMap _avatarEntityData; + bool _avatarEntityDataLocallyEdited { false }; + bool _avatarEntityDataChanged { false }; + quint64 _avatarEntityChangedTime { 0 }; + private: friend void avatarStateFromFrame(const QByteArray& frameData, AvatarData* _avatar); static QUrl _defaultFullAvatarModelUrl; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 75fb5e6028..62e87ce285 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -111,17 +111,18 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer QDataStream identityStream(message->getMessage()); QUuid sessionUUID; - + while (!identityStream.atEnd()) { QUrl faceMeshURL, skeletonURL; QVector attachmentData; + AvatarEntityMap avatarEntityData; QString displayName; - identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> attachmentData >> displayName; + identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> attachmentData >> displayName >> avatarEntityData; // mesh URL for a UUID, find avatar in our list auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); - + if (avatar->getSkeletonModelURL().isEmpty() || (avatar->getSkeletonModelURL() != skeletonURL)) { avatar->setSkeletonModelURL(skeletonURL); // Will expand "" to default and so will not continuously fire } @@ -130,6 +131,8 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer avatar->setAttachmentData(attachmentData); } + avatar->setAvatarEntityData(avatarEntityData); + if (avatar->getDisplayName() != displayName) { avatar->setDisplayName(displayName); } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 26aecf6050..891e1dca3b 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -174,9 +174,14 @@ void RenderableWebEntityItem::render(RenderArgs* args) { #endif if (!_webSurface) { + #if defined(Q_OS_LINUX) + // these don't seem to work on Linux + return; + #else if (!buildWebSurface(static_cast(args->_renderer))) { return; } + #endif } _lastRenderTime = usecTimestampNow(); diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 1e38c32964..ea86d3d542 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -10,6 +10,7 @@ // #include +#include #include #include #include @@ -35,18 +36,54 @@ void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByte } } -void EntityEditPacketSender::queueEditEntityMessage(PacketType type, EntityItemID modelID, - const EntityItemProperties& properties) { +void EntityEditPacketSender::queueEditEntityMessage(PacketType type, + EntityTreePointer entityTree, + EntityItemID entityItemID, + const EntityItemProperties& properties) { if (!_shouldSend) { return; // bail early } + if (properties.getClientOnly()) { + // this is an avatar-based entity. update our avatar-data rather than sending to the entity-server + assert(_myAvatar); + + if (!entityTree) { + qDebug() << "EntityEditPacketSender::queueEditEntityMessage null entityTree."; + return; + } + EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID); + if (!entity) { + qDebug() << "EntityEditPacketSender::queueEditEntityMessage can't find entity."; + return; + } + + // the properties that get serialized into the avatar identity packet should be the entire set + // rather than just the ones being edited. + entity->setProperties(properties); + EntityItemProperties entityProperties = entity->getProperties(); + + QScriptValue scriptProperties = EntityItemNonDefaultPropertiesToScriptValue(&_scriptEngine, entityProperties); + QVariant variantProperties = scriptProperties.toVariant(); + QJsonDocument jsonProperties = QJsonDocument::fromVariant(variantProperties); + + // the ID of the parent/avatar changes from session to session. use a special UUID to indicate the avatar + QJsonObject jsonObject = jsonProperties.object(); + if (QUuid(jsonObject["parentID"].toString()) == _myAvatar->getID()) { + jsonObject["parentID"] = AVATAR_SELF_ID.toString(); + } + jsonProperties = QJsonDocument(jsonObject); + + QByteArray binaryProperties = jsonProperties.toBinaryData(); + _myAvatar->updateAvatarEntity(entityItemID, binaryProperties); + return; + } QByteArray bufferOut(NLPacket::maxPayloadSize(type), 0); - if (EntityItemProperties::encodeEntityEditPacket(type, modelID, properties, bufferOut)) { + if (EntityItemProperties::encodeEntityEditPacket(type, entityItemID, properties, bufferOut)) { #ifdef WANT_DEBUG qCDebug(entities) << "calling queueOctreeEditMessage()..."; - qCDebug(entities) << " id:" << modelID; + qCDebug(entities) << " id:" << entityItemID; qCDebug(entities) << " properties:" << properties; #endif queueOctreeEditMessage(type, bufferOut); diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 26e4dd83ff..90c6cb988d 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -15,6 +15,7 @@ #include #include "EntityItem.h" +#include "AvatarData.h" /// Utility for processing, packing, queueing and sending of outbound edit voxel messages. class EntityEditPacketSender : public OctreeEditPacketSender { @@ -22,11 +23,17 @@ class EntityEditPacketSender : public OctreeEditPacketSender { public: EntityEditPacketSender(); + void setMyAvatar(AvatarData* myAvatar) { _myAvatar = myAvatar; } + AvatarData* getMyAvatar() { return _myAvatar; } + void clearAvatarEntity(QUuid entityID) { assert(_myAvatar); _myAvatar->clearAvatarEntity(entityID); } + /// Queues an array of several voxel edit messages. Will potentially send a pending multi-command packet. Determines /// which voxel-server node or nodes the packet should be sent to. Can be called even before voxel servers are known, in /// which case up to MaxPendingMessages will be buffered and processed when voxel servers are known. /// NOTE: EntityItemProperties assumes that all distances are in meter units - void queueEditEntityMessage(PacketType type, EntityItemID modelID, const EntityItemProperties& properties); + void queueEditEntityMessage(PacketType type, EntityTreePointer entityTree, + EntityItemID entityItemID, const EntityItemProperties& properties); + void queueEraseEntityMessage(const EntityItemID& entityItemID); @@ -40,5 +47,7 @@ public slots: private: bool _shouldProcessNack = true; + AvatarData* _myAvatar { nullptr }; + QScriptEngine _scriptEngine; }; #endif // hifi_EntityEditPacketSender_h diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index ecb9800e70..61f7fb0082 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -420,12 +420,19 @@ public: /// entity to definitively state if the preload signal should be sent. /// /// We only want to preload if: - /// there is some script, and either the script value or the scriptTimestamp + /// there is some script, and either the script value or the scriptTimestamp /// value have changed since our last preload - bool shouldPreloadScript() const { return !_script.isEmpty() && + bool shouldPreloadScript() const { return !_script.isEmpty() && ((_loadedScript != _script) || (_loadedScriptTimestamp != _scriptTimestamp)); } void scriptHasPreloaded() { _loadedScript = _script; _loadedScriptTimestamp = _scriptTimestamp; } + bool getClientOnly() const { return _clientOnly; } + void setClientOnly(bool clientOnly) { _clientOnly = clientOnly; } + // if this entity is client-only, which avatar is it associated with? + QUuid getOwningAvatarID() const { return _owningAvatarID; } + void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; } + + protected: void setSimulated(bool simulated) { _simulated = simulated; } @@ -537,6 +544,9 @@ protected: mutable QHash _previouslyDeletedActions; QUuid _sourceUUID; /// the server node UUID we came from + + bool _clientOnly { false }; + QUuid _owningAvatarID; }; #endif // hifi_EntityItem_h diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 2cf31e5632..fb24e711f4 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -276,6 +276,12 @@ public: void setJointRotationsDirty() { _jointRotationsSetChanged = true; _jointRotationsChanged = true; } void setJointTranslationsDirty() { _jointTranslationsSetChanged = true; _jointTranslationsChanged = true; } + bool getClientOnly() const { return _clientOnly; } + void setClientOnly(bool clientOnly) { _clientOnly = clientOnly; } + // if this entity is client-only, which avatar is it associated with? + QUuid getOwningAvatarID() const { return _owningAvatarID; } + void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; } + protected: QString getCollisionMaskAsString() const; void setCollisionMaskFromString(const QString& maskString); @@ -302,6 +308,9 @@ private: glm::vec3 _naturalPosition; EntityPropertyFlags _desiredProperties; // if set will narrow scopes of copy/to/from to just these properties + + bool _clientOnly { false }; + QUuid _owningAvatarID; }; Q_DECLARE_METATYPE(EntityItemProperties); diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 093fa73ace..0869ac40da 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -36,7 +36,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership void EntityScriptingInterface::queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties) { - getEntityPacketSender()->queueEditEntityMessage(packetType, entityID, properties); + getEntityPacketSender()->queueEditEntityMessage(packetType, _entityTree, entityID, properties); } bool EntityScriptingInterface::canAdjustLocks() { @@ -123,9 +123,10 @@ EntityItemProperties convertLocationFromScriptSemantics(const EntityItemProperti } -QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties) { +QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties, bool clientOnly) { EntityItemProperties propertiesWithSimID = convertLocationFromScriptSemantics(properties); propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged()); + propertiesWithSimID = clientOnly; auto dimensions = propertiesWithSimID.getDimensions(); float volume = dimensions.x * dimensions.y * dimensions.z; @@ -272,13 +273,15 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& bool updatedEntity = false; _entityTree->withWriteLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); + if (!entity) { + return; + } + if (scriptSideProperties.parentRelatedPropertyChanged()) { // All of parentID, parentJointIndex, position, rotation are needed to make sense of any of them. // If any of these changed, pull any missing properties from the entity. - EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); - if (!entity) { - return; - } + //existing entity, retrieve old velocity for check down below oldVelocity = entity->getVelocity().length(); @@ -296,6 +299,8 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& } } properties = convertLocationFromScriptSemantics(properties); + properties.setClientOnly(entity->getClientOnly()); + properties.setOwningAvatarID(entity->getOwningAvatarID()); float cost = calculateCost(density * volume, oldVelocity, newVelocity); cost *= costMultiplier; diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index e5f913dbf8..2bd08f8e3f 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -82,7 +82,7 @@ public slots: Q_INVOKABLE bool canRez(); /// adds a model with the specific properties - Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties); + Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties, bool clientOnly = false); /// temporary method until addEntity can be used from QJSEngine Q_INVOKABLE QUuid addModelEntity(const QString& name, const QString& modelUrl, const glm::vec3& position); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index b4f0c484d5..86bbf0b74d 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1373,8 +1373,11 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra } properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity + EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); + EntityTreePointer tree = entityTreeElement->getTree(); + // queue the packet to send to the server - args->packetSender->queueEditEntityMessage(PacketType::EntityAdd, newID, properties); + args->packetSender->queueEditEntityMessage(PacketType::EntityAdd, tree, newID, properties); // also update the local tree instantly (note: this is not our tree, but an alternate tree) if (args->otherTree) { diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index f0539110d3..070bf81e3a 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -547,8 +547,11 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ qCDebug(physics) << "EntityMotionState::sendUpdate()... calling queueEditEntityMessage()..."; #endif - entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, id, properties); - _entity->setLastBroadcast(usecTimestampNow()); + EntityTreeElementPointer element = _entity->getElement(); + EntityTreePointer tree = element ? element->getTree() : nullptr; + + entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, tree, id, properties); + _entity->setLastBroadcast(now); // if we've moved an entity with children, check/update the queryAACube of all descendents and tell the server // if they've changed. @@ -559,8 +562,9 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ EntityItemProperties newQueryCubeProperties; newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube()); newQueryCubeProperties.setLastEdited(properties.getLastEdited()); - entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, descendant->getID(), newQueryCubeProperties); - entityDescendant->setLastBroadcast(usecTimestampNow()); + entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, tree, + descendant->getID(), newQueryCubeProperties); + entityDescendant->setLastBroadcast(now); } } }); From 473010f634ea9fc68821d02c06c0ba028f0bb832 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 May 2016 16:45:09 -0700 Subject: [PATCH 05/77] addEntity has a clientOnly flag now --- libraries/entities/src/EntityScriptingInterface.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 0869ac40da..499b146d30 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -126,7 +126,13 @@ EntityItemProperties convertLocationFromScriptSemantics(const EntityItemProperti QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties, bool clientOnly) { EntityItemProperties propertiesWithSimID = convertLocationFromScriptSemantics(properties); propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged()); - propertiesWithSimID = clientOnly; + + if (clientOnly) { + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + propertiesWithSimID.setClientOnly(clientOnly); + propertiesWithSimID.setOwningAvatarID(myNodeID); + } auto dimensions = propertiesWithSimID.getDimensions(); float volume = dimensions.x * dimensions.y * dimensions.z; From 91ff851bf8851189dc28c59d37862e7cfd580301 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 May 2016 16:59:54 -0700 Subject: [PATCH 06/77] fix call to queueEditEntityMessage --- libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 6c4e3994c6..858b34c97f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -981,7 +981,7 @@ void RenderablePolyVoxEntityItem::compressVolumeDataAndSendEditPacket() { PhysicalEntitySimulation* peSimulation = static_cast(simulation); EntityEditPacketSender* packetSender = peSimulation ? peSimulation->getPacketSender() : nullptr; if (packetSender) { - packetSender->queueEditEntityMessage(PacketType::EntityEdit, entity->getID(), properties); + packetSender->queueEditEntityMessage(PacketType::EntityEdit, tree, entity->getID(), properties); } }); }); From 46c1049a353f5d6317c6c4adf91890c6ee4eb823 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 May 2016 17:48:06 -0700 Subject: [PATCH 07/77] bump protocol version --- libraries/avatars/src/AvatarData.cpp | 1 + libraries/avatars/src/AvatarHashMap.cpp | 1 + libraries/networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 3 ++- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 9a0fc1a835..c68240bdca 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -956,6 +956,7 @@ void AvatarData::clearJointsData() { } bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray& data) { + // this is used by the avatar-mixer QDataStream packetStream(data); QUuid avatarUUID; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 62e87ce285..612f4c6f96 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -107,6 +107,7 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer mess } void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode) { + // this is used by clients // setup a data stream to parse the packet QDataStream identityStream(message->getMessage()); diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e4aab94090..fbe31d1e8d 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -50,7 +50,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return VERSION_LIGHT_HAS_FALLOFF_RADIUS; case PacketType::AvatarData: case PacketType::BulkAvatarData: - return static_cast(AvatarMixerPacketVersion::SoftAttachmentSupport); + return static_cast(AvatarMixerPacketVersion::AvatarEntities); case PacketType::ICEServerHeartbeat: return 18; // ICE Server Heartbeat signing case PacketType::AssetGetInfo: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index b98a87e439..b29fccb45e 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -174,7 +174,8 @@ const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, - SoftAttachmentSupport + SoftAttachmentSupport, + AvatarEntities }; #endif // hifi_PacketHeaders_h From 0ab0409979b20c39ca61dbf6d1a46c2d87e66df0 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 7 May 2016 17:48:37 -0700 Subject: [PATCH 08/77] revert accidental change --- libraries/entities-renderer/src/RenderableWebEntityItem.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 891e1dca3b..26aecf6050 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -174,14 +174,9 @@ void RenderableWebEntityItem::render(RenderArgs* args) { #endif if (!_webSurface) { - #if defined(Q_OS_LINUX) - // these don't seem to work on Linux - return; - #else if (!buildWebSurface(static_cast(args->_renderer))) { return; } - #endif } _lastRenderTime = usecTimestampNow(); From 6e59abc8f6ec64d8810ae4c80a600d200ba5316e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 10 May 2016 12:13:46 +1200 Subject: [PATCH 09/77] Switch file selection dialog over to using UI Toolkit files --- .../resources/qml/dialogs/FileDialog.qml | 54 ++++++++++++------- tests/ui/qml/main.qml | 37 +++++++------ 2 files changed, 54 insertions(+), 37 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 916ef434b6..8bb92b60d6 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -1,4 +1,14 @@ -import QtQuick 2.0 +// +// Desktop.qml +// +// Created by Bradley Austin Davis on 14 Jan 2016 +// Copyright 2015 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.Controls 1.4 import Qt.labs.folderlistmodel 2.1 import Qt.labs.settings 1.0 @@ -6,17 +16,19 @@ import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import ".." -import "../windows" -import "../styles" -import "../controls" as VrControls +import "../controls-uit" +import "../styles-uit" +import "../windows-uit" + import "fileDialog" //FIXME implement shortcuts for favorite location ModalWindow { id: root - resizable: true - width: 640 - height: 480 + //resizable: true + implicitWidth: 640 + implicitHeight: 480 + HifiConstants { id: hifi } Settings { category: "FileDialog" @@ -46,6 +58,8 @@ ModalWindow { property alias model: fileTableView.model property var drives: helper.drives() + property int titleWidth: 0 + signal selectedFile(var file); signal canceled(); @@ -56,14 +70,17 @@ ModalWindow { }) } - Rectangle { - anchors.fill: parent - color: "white" + Item { + clip: true + width: pane.width + height: pane.height + anchors.margins: 0 Row { id: navControls anchors { left: parent.left; top: parent.top; margins: 8 } spacing: 8 + // FIXME implement back button //VrControls.ButtonAwesome { // id: backButton @@ -72,30 +89,29 @@ ModalWindow { // enabled: d.backStack.length != 0 // MouseArea { anchors.fill: parent; onClicked: d.navigateBack() } //} - VrControls.ButtonAwesome { + + Button { id: upButton enabled: model.parentFolder && model.parentFolder !== "" - text: "\uf0aa" - size: 32 + text: "up" onClicked: d.navigateUp(); } - VrControls.ButtonAwesome { + + Button { id: homeButton property var destination: helper.home(); enabled: d.homeDestination ? true : false - text: "\uf015" - size: 32 + text: "home" onClicked: d.navigateHome(); } - VrControls.ComboBox { + ComboBox { id: drivesSelector width: 48 height: homeButton.height model: drives visible: drives.length > 1 currentIndex: 0 - } } @@ -385,5 +401,3 @@ ModalWindow { } } } - - diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index e45749e1de..54ce16fbc2 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -211,6 +211,26 @@ ApplicationWindow { } } + Button { + text: "Open File" + property var builder: Component { + FileDialog { + title: "Open File" + filter: "*.js" + } + } + + onClicked: { + var fileDialog = builder.createObject(desktop); + fileDialog.canceled.connect(function(){ + console.log("Cancelled") + }) + fileDialog.selectedFile.connect(function(file){ + console.log("Selected " + file) + }) + } + } + Button { text: "Add Tab" onClicked: { @@ -246,24 +266,7 @@ ApplicationWindow { } } - Button { - text: "Open File" - property var builder: Component { - FileDialog { } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } } - /* Window { id: blue From 43ee64c251b83f8ef2ebfb1d1a8f9d77c0cd162e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 10 May 2016 12:50:32 +1200 Subject: [PATCH 10/77] Upate general layout of file selection dialog --- .../resources/qml/dialogs/FileDialog.qml | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 8bb92b60d6..d74913b301 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -78,8 +78,12 @@ ModalWindow { Row { id: navControls - anchors { left: parent.left; top: parent.top; margins: 8 } - spacing: 8 + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: parent.left + } + spacing: hifi.dimensions.contentSpacing.x // FIXME implement back button //VrControls.ButtonAwesome { @@ -119,7 +123,13 @@ ModalWindow { id: currentDirectory height: homeButton.height style: TextFieldStyle { renderType: Text.QtRendering } - anchors { left: navControls.right; right: parent.right; top: parent.top; margins: 8 } + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: navControls.right + leftMargin: hifi.dimensions.contentSpacing.x + right: parent.right + } property var lastValidFolder: helper.urlToPath(model.folder) onLastValidFolderChanged: text = lastValidFolder; verticalAlignment: Text.AlignVCenter @@ -179,7 +189,14 @@ ModalWindow { FileTableView { id: fileTableView - anchors { left: parent.left; right: parent.right; top: currentDirectory.bottom; bottom: currentSelection.top; margins: 8 } + anchors { + top: navControls.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: parent.left + right: parent.right + bottom: currentSelection.top + bottomMargin: hifi.dimensions.contentSpacing.y + } onDoubleClicked: navigateToRow(row); focus: true Keys.onReturnPressed: navigateToCurrentRow(); @@ -277,7 +294,13 @@ ModalWindow { TextField { id: currentSelection style: TextFieldStyle { renderType: Text.QtRendering } - anchors { right: root.selectDirectory ? parent.right : selectionType.left; rightMargin: 8; left: parent.left; leftMargin: 8; top: selectionType.top } + anchors { + left: parent.left + right: root.selectDirectory ? parent.right : selectionType.left + rightMargin: hifi.dimensions.contentSpacing.x + bottom: buttonRow.top + bottomMargin: hifi.dimensions.contentSpacing.y + } readOnly: !root.saveDialog activeFocusOnTab: !readOnly onActiveFocusChanged: if (activeFocus) { selectAll(); } @@ -286,7 +309,11 @@ ModalWindow { FileTypeSelection { id: selectionType - anchors { bottom: buttonRow.top; bottomMargin: 8; right: parent.right; rightMargin: 8; left: buttonRow.left } + anchors { + top: currentSelection.top + left: buttonRow.left + right: parent.right + } visible: !selectDirectory KeyNavigation.left: fileTableView KeyNavigation.right: openButton @@ -294,19 +321,22 @@ ModalWindow { Row { id: buttonRow - anchors.right: parent.right - anchors.rightMargin: 8 - anchors.bottom: parent.bottom - anchors.bottomMargin: 8 - spacing: 8 + anchors { + right: parent.right + bottom: parent.bottom + } + spacing: hifi.dimensions.contentSpacing.y + Button { id: openButton + color: hifi.buttons.blue action: okAction Keys.onReturnPressed: okAction.trigger() KeyNavigation.up: selectionType KeyNavigation.left: selectionType KeyNavigation.right: cancelButton } + Button { id: cancelButton action: cancelAction From 663a2ddc6446ce3eebb6737f0273ab8042e6fb17 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 10 May 2016 14:43:54 +1200 Subject: [PATCH 11/77] Style path and filter controls --- interface/resources/qml/dialogs/FileDialog.qml | 8 ++++---- .../qml/dialogs/fileDialog/FileTypeSelection.qml | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index d74913b301..42fe7120ea 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -1,5 +1,5 @@ // -// Desktop.qml +// FileDialog.qml // // Created by Bradley Austin Davis on 14 Jan 2016 // Copyright 2015 High Fidelity, Inc. @@ -195,7 +195,7 @@ ModalWindow { left: parent.left right: parent.right bottom: currentSelection.top - bottomMargin: hifi.dimensions.contentSpacing.y + bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height } onDoubleClicked: navigateToRow(row); focus: true @@ -293,11 +293,11 @@ ModalWindow { TextField { id: currentSelection - style: TextFieldStyle { renderType: Text.QtRendering } + label: "Path:" anchors { left: parent.left right: root.selectDirectory ? parent.right : selectionType.left - rightMargin: hifi.dimensions.contentSpacing.x + rightMargin: root.selectDirectory ? 0 : hifi.dimensions.contentSpacing.x bottom: buttonRow.top bottomMargin: hifi.dimensions.contentSpacing.y } diff --git a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml index 57ad2028ad..3d66b37b67 100644 --- a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml +++ b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml @@ -1,8 +1,18 @@ +// +// FileTypeSelection.qml +// +// Created by Bradley Austin Davis on 29 Jan 2016 +// Copyright 2015 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 "../../controls" as VrControls +import "../../controls-uit" -VrControls.ComboBox { +ComboBox { id: root property string filtersString: "All Files (*.*)"; property var currentFilter: [ "*.*" ]; From b8c0ec86af7d663636c1bde1d2b969b9bb763f55 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 10 May 2016 15:09:13 +1200 Subject: [PATCH 12/77] Hide file filter if only 1 file type --- interface/resources/qml/dialogs/FileDialog.qml | 6 +++--- .../resources/qml/dialogs/fileDialog/FileTypeSelection.qml | 1 + tests/ui/qml/main.qml | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 42fe7120ea..3be7fbc667 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -296,8 +296,8 @@ ModalWindow { label: "Path:" anchors { left: parent.left - right: root.selectDirectory ? parent.right : selectionType.left - rightMargin: root.selectDirectory ? 0 : hifi.dimensions.contentSpacing.x + right: selectionType.visible ? selectionType.left: parent.right + rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 bottom: buttonRow.top bottomMargin: hifi.dimensions.contentSpacing.y } @@ -314,7 +314,7 @@ ModalWindow { left: buttonRow.left right: parent.right } - visible: !selectDirectory + visible: !selectDirectory && filtersCount > 1 KeyNavigation.left: fileTableView KeyNavigation.right: openButton } diff --git a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml index 3d66b37b67..50a10974b5 100644 --- a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml +++ b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml @@ -16,6 +16,7 @@ ComboBox { id: root property string filtersString: "All Files (*.*)"; property var currentFilter: [ "*.*" ]; + property int filtersCount: filtersString.split(';;').length // Per http://doc.qt.io/qt-5/qfiledialog.html#getOpenFileName the string can contain // multiple filters separated by semicolons diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 54ce16fbc2..1745658193 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -216,7 +216,8 @@ ApplicationWindow { property var builder: Component { FileDialog { title: "Open File" - filter: "*.js" + filter: "All Files (*.*)" + //filter: "HTML files (*.html);;Other(*.png)" } } From 88207b727b8cfa6628883a88dfc2256abb1117b7 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 10 May 2016 15:45:10 +1200 Subject: [PATCH 13/77] Style directory and navigation controls "as is" --- .../qml/controls-uit/GlyphButton.qml | 2 +- .../resources/qml/dialogs/FileDialog.qml | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/controls-uit/GlyphButton.qml b/interface/resources/qml/controls-uit/GlyphButton.qml index c4ee53c04f..2625dda723 100644 --- a/interface/resources/qml/controls-uit/GlyphButton.qml +++ b/interface/resources/qml/controls-uit/GlyphButton.qml @@ -17,7 +17,7 @@ import "../styles-uit" Original.Button { property int color: 0 - property int colorScheme: hifi.colorShemes.light + property int colorScheme: hifi.colorSchemes.light property string glyph: "" width: 120 diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 3be7fbc667..1f710b4ef5 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -94,25 +94,26 @@ ModalWindow { // MouseArea { anchors.fill: parent; onClicked: d.navigateBack() } //} - Button { + GlyphButton { id: upButton + glyph: hifi.glyphs.levelUp + width: height enabled: model.parentFolder && model.parentFolder !== "" - text: "up" onClicked: d.navigateUp(); } - Button { + GlyphButton { id: homeButton property var destination: helper.home(); + glyph: hifi.glyphs.home + width: height enabled: d.homeDestination ? true : false - text: "home" onClicked: d.navigateHome(); } ComboBox { id: drivesSelector - width: 48 - height: homeButton.height + width: 62 model: drives visible: drives.length > 1 currentIndex: 0 @@ -121,8 +122,8 @@ ModalWindow { TextField { id: currentDirectory + property var lastValidFolder: helper.urlToPath(model.folder) height: homeButton.height - style: TextFieldStyle { renderType: Text.QtRendering } anchors { top: parent.top topMargin: hifi.dimensions.contentMargin.y @@ -130,11 +131,7 @@ ModalWindow { leftMargin: hifi.dimensions.contentSpacing.x right: parent.right } - property var lastValidFolder: helper.urlToPath(model.folder) onLastValidFolderChanged: text = lastValidFolder; - verticalAlignment: Text.AlignVCenter - font.pointSize: 14 - font.bold: true // FIXME add support auto-completion onAccepted: { From c572e1dc3a837e0c1f0d42678ba0ba67591697b6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 10:30:49 -0700 Subject: [PATCH 14/77] delete avatar-associated entities when the avatar goes away --- interface/src/avatar/Avatar.cpp | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 1819611af6..67f8d9c967 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -107,6 +107,18 @@ Avatar::Avatar(RigPointer rig) : Avatar::~Avatar() { assert(isDead()); // mark dead before calling the dtor + + EntityTreeRenderer* treeRenderer = qApp->getEntities(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (entityTree) { + entityTree->withWriteLock([&] { + AvatarEntityMap avatarEntities = getAvatarEntityData(); + for (auto entityID : avatarEntities.keys()) { + entityTree->deleteEntity(entityID, true, true); + } + }); + } + if (_motionState) { delete _motionState; _motionState = nullptr; @@ -167,7 +179,7 @@ void Avatar::updateAvatarEntities() { // - updateAvatarEntity saves the bytes and sets _avatarEntityDataLocallyEdited // - MyAvatar::update notices _avatarEntityDataLocallyEdited and calls sendIdentityPacket // - sendIdentityPacket sends the entity bytes to the server which relays them to other interfaces - // - AvatarHashMap::processAvatarIdentityPacket's on other interfaces call avatar->setAvatarEntityData() + // - AvatarHashMap::processAvatarIdentityPacket on other interfaces call avatar->setAvatarEntityData() // - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true // - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are... @@ -224,18 +236,14 @@ void Avatar::updateAvatarEntities() { } } } - }); - AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); - foreach (auto entityID, recentlyDettachedAvatarEntities) { - if (!_avatarEntityData.contains(entityID)) { - EntityItemPointer dettachedEntity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); - if (dettachedEntity) { - // this will cause this interface to listen to data from the entity-server about this entity. - dettachedEntity->setClientOnly(false); + AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); + foreach (auto entityID, recentlyDettachedAvatarEntities) { + if (!_avatarEntityData.contains(entityID)) { + entityTree->deleteEntity(entityID, true, true); } } - } + }); if (success) { setAvatarEntityDataChanged(false); From 51d6d99c73cf4ee520784e03b7b113f22b5c4e70 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 11 May 2016 09:17:11 +1200 Subject: [PATCH 15/77] Fix up files list data and basic layout --- .../qml/dialogs/fileDialog/FileTableView.qml | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/dialogs/fileDialog/FileTableView.qml b/interface/resources/qml/dialogs/fileDialog/FileTableView.qml index 93cdeebab0..d51f9e1ed9 100644 --- a/interface/resources/qml/dialogs/fileDialog/FileTableView.qml +++ b/interface/resources/qml/dialogs/fileDialog/FileTableView.qml @@ -1,4 +1,14 @@ -import QtQuick 2.0 +// +// FileDialog.qml +// +// Created by Bradley Austin Davis on 22 Jan 2016 +// Copyright 2015 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.Controls 1.4 TableView { @@ -8,6 +18,7 @@ TableView { root.selection.select(0) } } + //horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff itemDelegate: Component { Item { @@ -23,6 +34,7 @@ TableView { function getText() { switch (styleData.column) { //case 1: return Date.fromLocaleString(locale, styleData.value, "yyyy-MM-dd hh:mm:ss"); + case 1: return root.model.get(styleData.row, "fileIsDir") ? "" : styleData.value; case 2: return root.model.get(styleData.row, "fileIsDir") ? "" : formatSize(styleData.value); default: return styleData.value; } @@ -45,20 +57,23 @@ TableView { } TableViewColumn { + id: fileNameColumn role: "fileName" title: "Name" - width: 400 + width: Math.floor(0.55 * parent.width) + resizable: true } TableViewColumn { + id: fileMofifiedColumn role: "fileModified" - title: "Date Modified" - width: 200 + title: "Date" + width: Math.floor(0.3 * parent.width) + resizable: true } TableViewColumn { role: "fileSize" title: "Size" - width: 200 + width: Math.floor(0.15 * parent.width) - 16 - 2 // Allow space for vertical scrollbar and borders + resizable: true } } - - From de4c9530c9d81953f517f9c4cb3fc5185cd37684 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 14:20:58 -0700 Subject: [PATCH 16/77] carry clientOnly flag over from properties when addEntity is called --- libraries/entities/src/EntityTree.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 1cd2b4a47b..9d9c0fdba9 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -311,7 +311,9 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer result = NULL; - if (getIsClient()) { + bool clientOnly = properties.getClientOnly(); + + if (!clientOnly && getIsClient()) { // if our Node isn't allowed to create entities in this domain, don't try. auto nodeList = DependencyManager::get(); if (nodeList && !nodeList->getThisNodeCanRez()) { @@ -337,6 +339,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti // construct the instance of the entity EntityTypes::EntityType type = properties.getType(); result = EntityTypes::constructEntityItem(type, entityID, properties); + result->setClientOnly(clientOnly); if (result) { if (recordCreationTime) { From 1e849956c9ed74dd9156e44f94173b2a4bda9263 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 14:47:41 -0700 Subject: [PATCH 17/77] get rid of _avatarEntityChangedTime --- interface/src/avatar/Avatar.cpp | 5 +---- libraries/avatars/src/AvatarData.h | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 67f8d9c967..d3664b9f20 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -183,10 +183,8 @@ void Avatar::updateAvatarEntities() { // - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true // - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are... - quint64 now = usecTimestampNow(); - const static quint64 refreshTime = 3 * USECS_PER_SECOND; - if (!_avatarEntityDataChanged && now - _avatarEntityChangedTime < refreshTime) { + if (!_avatarEntityDataChanged) { return; } @@ -247,7 +245,6 @@ void Avatar::updateAvatarEntities() { if (success) { setAvatarEntityDataChanged(false); - _avatarEntityChangedTime = now; } } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 72d34af9d9..2242860e22 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -423,7 +423,6 @@ protected: AvatarEntityMap _avatarEntityData; bool _avatarEntityDataLocallyEdited { false }; bool _avatarEntityDataChanged { false }; - quint64 _avatarEntityChangedTime { 0 }; private: friend void avatarStateFromFrame(const QByteArray& frameData, AvatarData* _avatar); From 4b13fd969e0609e9abc5ac8e08daf92835f19e7e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 15:37:54 -0700 Subject: [PATCH 18/77] split code that sends edits via avatar-mixer out of queueEditEntityMessage --- interface/src/avatar/Avatar.cpp | 1 - .../entities/src/EntityEditPacketSender.cpp | 78 +++++++++++-------- .../entities/src/EntityEditPacketSender.h | 4 + 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index d3664b9f20..80e7aaa8a7 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -183,7 +183,6 @@ void Avatar::updateAvatarEntities() { // - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true // - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are... - const static quint64 refreshTime = 3 * USECS_PER_SECOND; if (!_avatarEntityDataChanged) { return; } diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index ea86d3d542..416d3c971e 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -36,6 +36,51 @@ void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByte } } +void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, + EntityTreePointer entityTree, + EntityItemID entityItemID, + const EntityItemProperties& properties) { + if (!_shouldSend) { + return; // bail early + } + + assert(properties.getClientOnly()); + + // this is an avatar-based entity. update our avatar-data rather than sending to the entity-server + assert(_myAvatar); + + if (!entityTree) { + qDebug() << "EntityEditPacketSender::queueEditEntityMessage null entityTree."; + return; + } + EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID); + if (!entity) { + qDebug() << "EntityEditPacketSender::queueEditEntityMessage can't find entity."; + return; + } + + // the properties that get serialized into the avatar identity packet should be the entire set + // rather than just the ones being edited. + entity->setProperties(properties); + EntityItemProperties entityProperties = entity->getProperties(); + + QScriptValue scriptProperties = EntityItemNonDefaultPropertiesToScriptValue(&_scriptEngine, entityProperties); + QVariant variantProperties = scriptProperties.toVariant(); + QJsonDocument jsonProperties = QJsonDocument::fromVariant(variantProperties); + + // the ID of the parent/avatar changes from session to session. use a special UUID to indicate the avatar + QJsonObject jsonObject = jsonProperties.object(); + if (QUuid(jsonObject["parentID"].toString()) == _myAvatar->getID()) { + jsonObject["parentID"] = AVATAR_SELF_ID.toString(); + } + jsonProperties = QJsonDocument(jsonObject); + + QByteArray binaryProperties = jsonProperties.toBinaryData(); + _myAvatar->updateAvatarEntity(entityItemID, binaryProperties); + return; +} + + void EntityEditPacketSender::queueEditEntityMessage(PacketType type, EntityTreePointer entityTree, EntityItemID entityItemID, @@ -43,38 +88,9 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type, if (!_shouldSend) { return; // bail early } + if (properties.getClientOnly()) { - // this is an avatar-based entity. update our avatar-data rather than sending to the entity-server - assert(_myAvatar); - - if (!entityTree) { - qDebug() << "EntityEditPacketSender::queueEditEntityMessage null entityTree."; - return; - } - EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID); - if (!entity) { - qDebug() << "EntityEditPacketSender::queueEditEntityMessage can't find entity."; - return; - } - - // the properties that get serialized into the avatar identity packet should be the entire set - // rather than just the ones being edited. - entity->setProperties(properties); - EntityItemProperties entityProperties = entity->getProperties(); - - QScriptValue scriptProperties = EntityItemNonDefaultPropertiesToScriptValue(&_scriptEngine, entityProperties); - QVariant variantProperties = scriptProperties.toVariant(); - QJsonDocument jsonProperties = QJsonDocument::fromVariant(variantProperties); - - // the ID of the parent/avatar changes from session to session. use a special UUID to indicate the avatar - QJsonObject jsonObject = jsonProperties.object(); - if (QUuid(jsonObject["parentID"].toString()) == _myAvatar->getID()) { - jsonObject["parentID"] = AVATAR_SELF_ID.toString(); - } - jsonProperties = QJsonDocument(jsonObject); - - QByteArray binaryProperties = jsonProperties.toBinaryData(); - _myAvatar->updateAvatarEntity(entityItemID, binaryProperties); + queueEditAvatarEntityMessage(type, entityTree, entityItemID, properties); return; } diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 90c6cb988d..9366fc9329 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -27,6 +27,10 @@ public: AvatarData* getMyAvatar() { return _myAvatar; } void clearAvatarEntity(QUuid entityID) { assert(_myAvatar); _myAvatar->clearAvatarEntity(entityID); } + void queueEditAvatarEntityMessage(PacketType type, EntityTreePointer entityTree, + EntityItemID entityItemID, const EntityItemProperties& properties); + + /// Queues an array of several voxel edit messages. Will potentially send a pending multi-command packet. Determines /// which voxel-server node or nodes the packet should be sent to. Can be called even before voxel servers are known, in /// which case up to MaxPendingMessages will be buffered and processed when voxel servers are known. From b05ab1b17e219186e2c8c64d122b64e41b9ca342 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 15:59:25 -0700 Subject: [PATCH 19/77] set simulationOwner to be the same as the owningAvatar --- interface/src/avatar/Avatar.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 80e7aaa8a7..cbe69185af 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -214,6 +214,10 @@ void Avatar::updateAvatarEntities() { properties.setClientOnly(true); properties.setOwningAvatarID(getID()); + // there's not entity-server to tell us we're the simulation owner, so always set the + // simulationOwner to the owningAvatarID and a high priority. + properties.setSimulationOwner(getID(), 129); + if (properties.getParentID() == AVATAR_SELF_ID) { properties.setParentID(getID()); } From 144715f00cf7b8bb33ed551d9f57e4fed5859051 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 16:05:40 -0700 Subject: [PATCH 20/77] don't make changes to other avatar's avatarEntities --- libraries/entities/src/EntityScriptingInterface.cpp | 6 ++++++ libraries/physics/src/EntityMotionState.cpp | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index e7b386e544..b160a23ced 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -284,6 +284,12 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& return; } + auto nodeList = DependencyManager::get(); + if (entity->getClientOnly() && entity->getOwningAvatarID() != nodeList->getSessionUUID()) { + // don't edit other avatar's avatarEntities + return; + } + if (scriptSideProperties.parentRelatedPropertyChanged()) { // All of parentID, parentJointIndex, position, rotation are needed to make sense of any of them. // If any of these changed, pull any missing properties from the entity. diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 070bf81e3a..f1897275ed 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -404,6 +404,11 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { assert(_body); assert(entityTreeIsLocked()); + if (_entity->getClientOnly() && _entity->getOwningAvatarID() != Physics::getSessionUUID()) { + // don't send updates for someone else's avatarEntities + return false; + } + if (_entity->actionDataNeedsTransmit()) { return true; } From 356ccdba26cb668e999c4af45dc691f134b17f8f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 16:16:06 -0700 Subject: [PATCH 21/77] debug icon for clientOnly --- .../src/RenderableEntityItem.cpp | 21 ++++++++++++++++--- .../src/RenderableEntityItem.h | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index d148145dde..011675fc82 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -47,6 +47,9 @@ namespace render { } void makeEntityItemStatusGetters(EntityItemPointer entity, render::Item::Status::Getters& statusGetters) { + auto nodeList = DependencyManager::get(); + const QUuid& myNodeID = nodeList->getSessionUUID(); + statusGetters.push_back([entity] () -> render::Item::Status::Value { quint64 delta = usecTimestampNow() - entity->getLastEditedFromRemote(); const float WAIT_THRESHOLD_INV = 1.0f / (0.2f * USECS_PER_SECOND); @@ -81,9 +84,7 @@ void makeEntityItemStatusGetters(EntityItemPointer entity, render::Item::Status: (unsigned char)RenderItemStatusIcon::ACTIVE_IN_BULLET); }); - statusGetters.push_back([entity] () -> render::Item::Status::Value { - auto nodeList = DependencyManager::get(); - const QUuid& myNodeID = nodeList->getSessionUUID(); + statusGetters.push_back([entity, myNodeID] () -> render::Item::Status::Value { bool weOwnSimulation = entity->getSimulationOwner().matchesValidID(myNodeID); bool otherOwnSimulation = !weOwnSimulation && !entity->getSimulationOwner().isNull(); @@ -106,4 +107,18 @@ void makeEntityItemStatusGetters(EntityItemPointer entity, render::Item::Status: return render::Item::Status::Value(0.0f, render::Item::Status::Value::GREEN, (unsigned char)RenderItemStatusIcon::HAS_ACTIONS); }); + + statusGetters.push_back([entity, myNodeID] () -> render::Item::Status::Value { + if (entity->getClientOnly()) { + if (entity->getOwningAvatarID() == myNodeID) { + return render::Item::Status::Value(1.0f, render::Item::Status::Value::GREEN, + (unsigned char)RenderItemStatusIcon::CLIENT_ONLY); + } else { + return render::Item::Status::Value(1.0f, render::Item::Status::Value::RED, + (unsigned char)RenderItemStatusIcon::CLIENT_ONLY); + } + } + return render::Item::Status::Value(0.0f, render::Item::Status::Value::GREEN, + (unsigned char)RenderItemStatusIcon::CLIENT_ONLY); + }); } diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 09451e87d4..9840bf3150 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -26,6 +26,7 @@ enum class RenderItemStatusIcon { SIMULATION_OWNER = 3, HAS_ACTIONS = 4, OTHER_SIMULATION_OWNER = 5, + CLIENT_ONLY = 6, NONE = 255 }; From de12680ff1dab61f4eb97b431c2d8e2228194499 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 16:43:27 -0700 Subject: [PATCH 22/77] don't put actions on other people's avatarEntities --- interface/src/avatar/Avatar.cpp | 4 ++++ libraries/entities/src/EntityEditPacketSender.cpp | 4 ++++ libraries/entities/src/EntityScriptingInterface.cpp | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index cbe69185af..83cec224b6 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -187,6 +187,10 @@ void Avatar::updateAvatarEntities() { return; } + if (getID() == QUuid()) { + return; // wait until MyAvatar gets an ID before doing this. + } + EntityTreeRenderer* treeRenderer = qApp->getEntities(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (!entityTree) { diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 416d3c971e..6cb0b6ef60 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -44,6 +44,10 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, return; // bail early } + if (properties.getOwningAvatarID() != _myAvatar->getID()) { + return; // don't send updates for someone else's avatarEntity + } + assert(properties.getClientOnly()); // this is an avatar-based entity. update our avatar-data rather than sending to the entity-server diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index b160a23ced..3b06c5a998 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -788,6 +788,9 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, return false; } + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + EntityItemPointer entity; bool doTransmit = false; _entityTree->withWriteLock([&] { @@ -803,6 +806,10 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, return; } + if (entity->getClientOnly() && entity->getOwningAvatarID() != nodeList->getSessionUUID()) { + return; + } + doTransmit = actor(simulation, entity); if (doTransmit) { _entityTree->entityChanged(entity); From e4e0be8fa3ba0baf38eaa5b5a0588c7a9f630dc7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 16:51:38 -0700 Subject: [PATCH 23/77] relay owningAvatar from properties to entity on entity creation --- libraries/entities/src/EntityTree.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 9d9c0fdba9..2da3604c2a 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -312,6 +312,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti EntityItemPointer result = NULL; bool clientOnly = properties.getClientOnly(); + QUuid owningAvatarID = properties.getOwningAvatarID(); if (!clientOnly && getIsClient()) { // if our Node isn't allowed to create entities in this domain, don't try. @@ -340,6 +341,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti EntityTypes::EntityType type = properties.getType(); result = EntityTypes::constructEntityItem(type, entityID, properties); result->setClientOnly(clientOnly); + result->setOwningAvatarID(owningAvatarID); if (result) { if (recordCreationTime) { From 872f1b0c64d9dc7479dbc1a9de3615263dc21818 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 10 May 2016 17:48:55 -0700 Subject: [PATCH 24/77] clear avatarEntity data for entities that are deleted --- interface/src/avatar/Avatar.cpp | 5 ++++- interface/src/avatar/MyAvatar.cpp | 1 + libraries/entities/src/EntityEditPacketSender.cpp | 6 ++++++ libraries/entities/src/EntityItem.h | 1 + libraries/entities/src/EntityScriptingInterface.cpp | 10 +++++++++- 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 83cec224b6..d959b298ef 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -229,7 +229,10 @@ void Avatar::updateAvatarEntities() { EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { - if (!entityTree->updateEntity(entityID, properties)) { + if (entityTree->updateEntity(entityID, properties)) { + entity->markAsChangedOnServer(); + entity->updateLastEditedFromRemote(); + } else { qDebug() << "AVATAR-ENTITES -- updateEntity failed: " << properties.getType(); success = false; } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index dc0d6d7e97..304aabdcc6 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -828,6 +828,7 @@ void MyAvatar::loadData() { for (int i = 0; i < avatarEntityCount; i++) { settings.setArrayIndex(i); QUuid entityID = settings.value("id").toUuid(); + // QUuid entityID = QUuid::createUuid(); // generate a new ID QByteArray properties = settings.value("properties").toByteArray(); updateAvatarEntity(entityID, properties); } diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 6cb0b6ef60..28f1871346 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -81,6 +81,8 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, QByteArray binaryProperties = jsonProperties.toBinaryData(); _myAvatar->updateAvatarEntity(entityItemID, binaryProperties); + + entity->setLastBroadcast(usecTimestampNow()); return; } @@ -115,6 +117,10 @@ void EntityEditPacketSender::queueEraseEntityMessage(const EntityItemID& entityI return; // bail early } + // in case this was a clientOnly entity: + assert(_myAvatar); + _myAvatar->clearAvatarEntity(entityItemID); + QByteArray bufferOut(NLPacket::maxPayloadSize(PacketType::EntityErase), 0); if (EntityItemProperties::encodeEraseEntityMessage(entityItemID, bufferOut)) { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 4286a9d6ae..c2e497e602 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -375,6 +375,7 @@ public: glm::vec3 entityToWorld(const glm::vec3& point) const; quint64 getLastEditedFromRemote() const { return _lastEditedFromRemote; } + void updateLastEditedFromRemote() { _lastEditedFromRemote = usecTimestampNow(); } void getAllTerseUpdateProperties(EntityItemProperties& properties) const; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 3b06c5a998..ade8ade1c4 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -401,6 +401,14 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (entity) { + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) { + // don't delete other avatar's avatarEntities + shouldDelete = false; + return; + } + auto dimensions = entity->getDimensions(); float volume = dimensions.x * dimensions.y * dimensions.z; auto density = entity->getDensity(); @@ -806,7 +814,7 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, return; } - if (entity->getClientOnly() && entity->getOwningAvatarID() != nodeList->getSessionUUID()) { + if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) { return; } From ac506dc4b77de0bd8831796de3ffe6fad855a6c9 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 11 May 2016 12:46:16 -0700 Subject: [PATCH 25/77] where needed, copy clientOnly into properties before calling queueEditEntityMessage --- interface/src/avatar/MyAvatar.cpp | 1 + libraries/entities/src/EntityItem.cpp | 11 ++++++++++ .../entities/src/EntityItemProperties.cpp | 20 +++++++++++++++++++ libraries/entities/src/EntityItemProperties.h | 15 ++++++-------- libraries/entities/src/EntityPropertyFlags.h | 3 +++ .../entities/src/EntityScriptingInterface.cpp | 5 ++++- libraries/entities/src/EntityTree.cpp | 3 --- libraries/physics/src/EntityMotionState.cpp | 7 +++++++ 8 files changed, 52 insertions(+), 13 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 304aabdcc6..33905d5e81 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -446,6 +446,7 @@ void MyAvatar::simulate(float deltaTime) { EntityItemProperties properties = entity->getProperties(); properties.setQueryAACubeDirty(); properties.setLastEdited(now); + packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entity->getID(), properties); entity->setLastBroadcast(usecTimestampNow()); } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 187c4f51be..04307892f1 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -138,6 +138,9 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_PARENT_JOINT_INDEX; requestedProperties += PROP_QUERY_AA_CUBE; + requestedProperties += PROP_CLIENT_ONLY; + requestedProperties += PROP_OWNING_AVATAR_ID; + return requestedProperties; } @@ -1094,6 +1097,8 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper properties._id = getID(); properties._idSet = true; properties._created = _created; + properties.setClientOnly(_clientOnly); + properties.setOwningAvatarID(_owningAvatarID); properties._type = getType(); @@ -1135,6 +1140,9 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper COPY_ENTITY_PROPERTY_TO_PROPERTIES(localPosition, getLocalPosition); COPY_ENTITY_PROPERTY_TO_PROPERTIES(localRotation, getLocalOrientation); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(clientOnly, getClientOnly); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(owningAvatarID, getOwningAvatarID); + properties._defaultSettings = false; return properties; @@ -1225,6 +1233,9 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentJointIndex, setParentJointIndex); SET_ENTITY_PROPERTY_FROM_PROPERTIES(queryAACube, setQueryAACube); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(clientOnly, setClientOnly); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(owningAvatarID, setOwningAvatarID); + AACube saveQueryAACube = _queryAACube; checkAndAdjustQueryAACube(); if (saveQueryAACube != _queryAACube) { diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 738e8910fe..354cbd03ef 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -309,6 +309,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_QUERY_AA_CUBE, queryAACube); CHECK_PROPERTY_CHANGE(PROP_LOCAL_POSITION, localPosition); CHECK_PROPERTY_CHANGE(PROP_LOCAL_ROTATION, localRotation); + CHECK_PROPERTY_CHANGE(PROP_CLIENT_ONLY, clientOnly); + CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID); changedProperties += _animation.getChangedProperties(); changedProperties += _keyLight.getChangedProperties(); @@ -541,6 +543,12 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_POSITION, localPosition); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_ROTATION, localRotation); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLIENT_ONLY, clientOnly); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_OWNING_AVATAR_ID, owningAvatarID); + + properties.setProperty("clientOnly", convertScriptValue(engine, getClientOnly())); + properties.setProperty("owningAvatarID", convertScriptValue(engine, getOwningAvatarID())); + // FIXME - I don't think these properties are supported any more //COPY_PROPERTY_TO_QSCRIPTVALUE(glowLevel); //COPY_PROPERTY_TO_QSCRIPTVALUE(localRenderAlpha); @@ -679,6 +687,9 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslationsSet, qVectorBool, setJointTranslationsSet); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslations, qVectorVec3, setJointTranslations); + COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly); + COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID); + _lastEdited = usecTimestampNow(); } @@ -1564,6 +1575,9 @@ void EntityItemProperties::markAllChanged() { _jointTranslationsChanged = true; _queryAACubeChanged = true; + + _clientOnlyChanged = true; + _owningAvatarIDChanged = true; } // The minimum bounding box for the entity. @@ -1884,6 +1898,12 @@ QList EntityItemProperties::listChangedProperties() { if (queryAACubeChanged()) { out += "queryAACube"; } + if (clientOnlyChanged()) { + out += "clientOnly"; + } + if (owningAvatarIDChanged()) { + out += "owningAvatarID"; + } getAnimation().listChangedProperties(out); getKeyLight().listChangedProperties(out); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index fb24e711f4..fb36834238 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -205,6 +205,9 @@ public: DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector, QVector()); DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS, JointTranslations, jointTranslations, QVector, QVector()); + DEFINE_PROPERTY(PROP_CLIENT_ONLY, ClientOnly, clientOnly, bool, false); + DEFINE_PROPERTY_REF(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid, UNKNOWN_ENTITY_ID); + static QString getBackgroundModeString(BackgroundMode mode); @@ -276,12 +279,6 @@ public: void setJointRotationsDirty() { _jointRotationsSetChanged = true; _jointRotationsChanged = true; } void setJointTranslationsDirty() { _jointTranslationsSetChanged = true; _jointTranslationsChanged = true; } - bool getClientOnly() const { return _clientOnly; } - void setClientOnly(bool clientOnly) { _clientOnly = clientOnly; } - // if this entity is client-only, which avatar is it associated with? - QUuid getOwningAvatarID() const { return _owningAvatarID; } - void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; } - protected: QString getCollisionMaskAsString() const; void setCollisionMaskFromString(const QString& maskString); @@ -308,9 +305,6 @@ private: glm::vec3 _naturalPosition; EntityPropertyFlags _desiredProperties; // if set will narrow scopes of copy/to/from to just these properties - - bool _clientOnly { false }; - QUuid _owningAvatarID; }; Q_DECLARE_METATYPE(EntityItemProperties); @@ -430,6 +424,9 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, JointTranslationsSet, jointTranslationsSet, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, JointTranslations, jointTranslations, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ClientOnly, clientOnly, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, OwningAvatarID, owningAvatarID, ""); + properties.getAnimation().debugDump(); properties.getSkybox().debugDump(); properties.getStage().debugDump(); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 90a7c1e2f7..394f61b5e8 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -169,6 +169,9 @@ enum EntityPropertyList { PROP_FALLOFF_RADIUS, // for Light entity + PROP_CLIENT_ONLY, // doesn't go over wire + PROP_OWNING_AVATAR_ID, // doesn't go over wire + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index ade8ade1c4..15c2bffd80 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -799,6 +799,8 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, auto nodeList = DependencyManager::get(); const QUuid myNodeID = nodeList->getSessionUUID(); + EntityItemProperties properties; + EntityItemPointer entity; bool doTransmit = false; _entityTree->withWriteLock([&] { @@ -820,13 +822,14 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, doTransmit = actor(simulation, entity); if (doTransmit) { + properties.setClientOnly(entity->getClientOnly()); + properties.setOwningAvatarID(entity->getOwningAvatarID()); _entityTree->entityChanged(entity); } }); // transmit the change if (doTransmit) { - EntityItemProperties properties; _entityTree->withReadLock([&] { properties = entity->getProperties(); }); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 2da3604c2a..087225c865 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -312,7 +312,6 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti EntityItemPointer result = NULL; bool clientOnly = properties.getClientOnly(); - QUuid owningAvatarID = properties.getOwningAvatarID(); if (!clientOnly && getIsClient()) { // if our Node isn't allowed to create entities in this domain, don't try. @@ -340,8 +339,6 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti // construct the instance of the entity EntityTypes::EntityType type = properties.getType(); result = EntityTypes::constructEntityItem(type, entityID, properties); - result->setClientOnly(clientOnly); - result->setOwningAvatarID(owningAvatarID); if (result) { if (recordCreationTime) { diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index f1897275ed..053bfcbd85 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -555,6 +555,9 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ EntityTreeElementPointer element = _entity->getElement(); EntityTreePointer tree = element ? element->getTree() : nullptr; + properties.setClientOnly(_entity->getClientOnly()); + properties.setOwningAvatarID(_entity->getOwningAvatarID()); + entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, tree, id, properties); _entity->setLastBroadcast(now); @@ -567,6 +570,10 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ EntityItemProperties newQueryCubeProperties; newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube()); newQueryCubeProperties.setLastEdited(properties.getLastEdited()); + + newQueryCubeProperties.setClientOnly(entityDescendant->getClientOnly()); + newQueryCubeProperties.setOwningAvatarID(entityDescendant->getOwningAvatarID()); + entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, tree, descendant->getID(), newQueryCubeProperties); entityDescendant->setLastBroadcast(now); From 73c06b3bf42524d5d1898f60ef2be4d059a936ee Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 11 May 2016 14:13:08 -0700 Subject: [PATCH 26/77] updating attachedEntitiesManager to work with new system --- scripts/system/assets/images/lock.svg | 70 +++++++++++++++++++ scripts/system/assets/images/unlock.svg | 70 +++++++++++++++++++ scripts/system/attachedEntitiesManager.js | 84 ++++++++++------------- scripts/system/libraries/toolBars.js | 14 +++- 4 files changed, 188 insertions(+), 50 deletions(-) create mode 100644 scripts/system/assets/images/lock.svg create mode 100644 scripts/system/assets/images/unlock.svg diff --git a/scripts/system/assets/images/lock.svg b/scripts/system/assets/images/lock.svg new file mode 100644 index 0000000000..1480359f43 --- /dev/null +++ b/scripts/system/assets/images/lock.svg @@ -0,0 +1,70 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/scripts/system/assets/images/unlock.svg b/scripts/system/assets/images/unlock.svg new file mode 100644 index 0000000000..a7664c1f59 --- /dev/null +++ b/scripts/system/assets/images/unlock.svg @@ -0,0 +1,70 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/scripts/system/attachedEntitiesManager.js b/scripts/system/attachedEntitiesManager.js index 9ddb040297..124b2f76ec 100644 --- a/scripts/system/attachedEntitiesManager.js +++ b/scripts/system/attachedEntitiesManager.js @@ -22,7 +22,7 @@ var MINIMUM_DROP_DISTANCE_FROM_JOINT = 0.8; var ATTACHED_ENTITY_SEARCH_DISTANCE = 10.0; var ATTACHED_ENTITIES_SETTINGS_KEY = "ATTACHED_ENTITIES"; var DRESSING_ROOM_DISTANCE = 2.0; -var SHOW_TOOL_BAR = false; +var SHOW_TOOL_BAR = true; // tool bar @@ -30,34 +30,20 @@ if (SHOW_TOOL_BAR) { var BUTTON_SIZE = 32; var PADDING = 3; Script.include(["libraries/toolBars.js"]); - var toolBar = new ToolBar(0, 0, ToolBar.VERTICAL, "highfidelity.attachedEntities.toolbar", function(screenSize) { - return { - x: (BUTTON_SIZE + PADDING), - y: (screenSize.y / 2 - BUTTON_SIZE * 2 + PADDING) - }; - }); - var saveButton = toolBar.addOverlay("image", { + + var toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.attachedEntities.toolbar"); + var lockButton = toolBar.addTool({ width: BUTTON_SIZE, height: BUTTON_SIZE, - imageURL: ".../save.png", + imageURL: Script.resolvePath("assets/images/lock.svg"), color: { red: 255, green: 255, blue: 255 }, - alpha: 1 - }); - var loadButton = toolBar.addOverlay("image", { - width: BUTTON_SIZE, - height: BUTTON_SIZE, - imageURL: ".../load.png", - color: { - red: 255, - green: 255, - blue: 255 - }, - alpha: 1 - }); + alpha: 1, + visible: true + }, false); } @@ -67,10 +53,8 @@ function mousePressEvent(event) { y: event.y }); - if (clickedOverlay == saveButton) { - manager.saveAttachedEntities(); - } else if (clickedOverlay == loadButton) { - manager.loadAttachedEntities(); + if (lockButton === toolBar.clicked(clickedOverlay)) { + manager.toggleLocked(); } } @@ -92,6 +76,8 @@ Script.scriptEnding.connect(scriptEnding); function AttachedEntitiesManager() { + var clothingLocked = true; + this.subscribeToMessages = function() { Messages.subscribe('Hifi-Object-Manipulation'); Messages.messageReceived.connect(this.handleWearableMessages); @@ -128,19 +114,6 @@ function AttachedEntitiesManager() { } } - this.avatarIsInDressingRoom = function() { - // return true if MyAvatar is near the dressing room - var possibleDressingRoom = Entities.findEntities(MyAvatar.position, DRESSING_ROOM_DISTANCE); - for (i = 0; i < possibleDressingRoom.length; i++) { - var entityID = possibleDressingRoom[i]; - var props = Entities.getEntityProperties(entityID); - if (props.name == 'Hifi-Dressing-Room-Base') { - return true; - } - } - return false; - } - this.handleEntityRelease = function(grabbedEntity, releasedFromJoint) { // if this is still equipped, just rewrite the position information. var grabData = getEntityCustomData('grabKey', grabbedEntity, {}); @@ -179,21 +152,23 @@ function AttachedEntitiesManager() { } if (bestJointIndex != -1) { - var wearProps = { - parentID: MyAvatar.sessionUUID, - parentJointIndex: bestJointIndex - }; + var wearProps = Entities.getEntityProperties(grabbedEntity); + wearProps.parentID = MyAvatar.sessionUUID; + wearProps.parentJointIndex = bestJointIndex; if (bestJointOffset && bestJointOffset.constructor === Array) { - if (this.avatarIsInDressingRoom() || bestJointOffset.length < 2) { + if (!clothingLocked || bestJointOffset.length < 2) { this.updateRelativeOffsets(grabbedEntity); } else { - // don't snap the entity to the preferred position if the avatar is in the dressing room. + // don't snap the entity to the preferred position if unlocked wearProps.localPosition = bestJointOffset[0]; wearProps.localRotation = bestJointOffset[1]; } } - Entities.editEntity(grabbedEntity, wearProps); + + // Entities.editEntity(grabbedEntity, wearProps); + Entities.deleteEntity(grabbedEntity); + Entities.addEntity(wearProps, true); } else if (props.parentID != NULL_UUID) { // drop the entity and set it to have no parent (not on the avatar), unless it's being equipped in a hand. if (props.parentID === MyAvatar.sessionUUID && @@ -201,7 +176,11 @@ function AttachedEntitiesManager() { props.parentJointIndex == MyAvatar.getJointIndex("LeftHand"))) { // this is equipped on a hand -- don't clear the parent. } else { - Entities.editEntity(grabbedEntity, { parentID: NULL_UUID }); + var wearProps = Entities.getEntityProperties(grabbedEntity); + wearProps.parentID = NULL_UUID; + wearProps.parentJointIndex = -1; + Entities.deleteEntity(grabbedEntity); + Entities.addEntity(wearProps, false); } } } @@ -221,6 +200,17 @@ function AttachedEntitiesManager() { return false; } + this.toggleLocked = function() { + print("toggleLocked"); + if (clothingLocked) { + clothingLocked = false; + toolBar.setImageURL(Script.resolvePath("assets/images/unlock.svg"), lockButton); + } else { + clothingLocked = true; + toolBar.setImageURL(Script.resolvePath("assets/images/lock.svg"), lockButton); + } + } + this.saveAttachedEntities = function() { print("--- saving attached entities ---"); saveData = []; diff --git a/scripts/system/libraries/toolBars.js b/scripts/system/libraries/toolBars.js index d97575d349..9efe533457 100644 --- a/scripts/system/libraries/toolBars.js +++ b/scripts/system/libraries/toolBars.js @@ -56,6 +56,10 @@ Overlay2D = function(properties, overlay) { // overlay is an optional variable properties.alpha = alpha; Overlays.editOverlay(overlay, { alpha: alpha }); } + this.setImageURL = function(imageURL) { + properties.imageURL = imageURL; + Overlays.editOverlay(overlay, { imageURL: imageURL }); + } this.show = function(doShow) { properties.visible = doShow; Overlays.editOverlay(overlay, { visible: doShow }); @@ -254,7 +258,7 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit } this.save(); } - + this.setAlpha = function(alpha, tool) { if(typeof(tool) === 'undefined') { for(var tool in this.tools) { @@ -268,7 +272,11 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit this.tools[tool].setAlpha(alpha); } } - + + this.setImageURL = function(imageURL, tool) { + this.tools[tool].setImageURL(imageURL); + } + this.setBack = function(color, alpha) { if (color == null) { Overlays.editOverlay(this.back, { visible: false }); @@ -478,4 +486,4 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit } ToolBar.SPACING = 6; ToolBar.VERTICAL = 0; -ToolBar.HORIZONTAL = 1; \ No newline at end of file +ToolBar.HORIZONTAL = 1; From 57fae3fc5121a979bb836cafccd1fd643a18a234 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 May 2016 09:28:25 +1200 Subject: [PATCH 27/77] Add FiraSans-Regular font --- .../resources/fonts/FiraSans-Regular.ttf | Bin 0 -> 403924 bytes .../qml/styles-uit/FiraSansRegular.qml | 23 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 interface/resources/fonts/FiraSans-Regular.ttf create mode 100644 interface/resources/qml/styles-uit/FiraSansRegular.qml diff --git a/interface/resources/fonts/FiraSans-Regular.ttf b/interface/resources/fonts/FiraSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d9fdc0e922030c6836a9ad5f808fd7e1c5970873 GIT binary patch literal 403924 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 r-(pXe}H&MjC7NAiF2}5 zIgw_jW98JKhx#~UwiZW6*^Jg?tQLO(TBp& z@v4+k!9?{K&eML-VaF@@>k3et2XvHDVG>)u@V%nJiM^c4_5g&ZwZA6-n_l4AiG;JP zjF7TmS)i%{GR>ioY~z@4)l)iCzeaL2>no|sig0z-`B=Fm(SH%wtUWi(eVuIzRM?kY zkcHoV$G2W#K*afafJSk+ZZ|65415b4vzP#nUR}w?$r}SAO_+lK0v~2bsl};IRT?xHF=0gRYsm zAOY@c_w+fx1?P2ibyc*Y$uMAOs~>GD_|lQ}R}J(XP3^zcecw*lJ(`Y%`6)yoRiqZO zfi^Q^K$D=;Grf+Xr7lt+NFV|eHfml;xgKX4kcttv=ZpDRUY_La^L-MExjyp=lV*?i zfBf2~Oq>!&`UCo;Rv!R-l2%~xgF;b=aq*&natL7Zj+Gonj`aaSFeY5{x^#rK>N>rA z<2ql1zeQfYdf7;*y|O+$7TteM_x_|MP!V^#&%NOM9S%oZS=nvZr>^ct9etcd*`35q zDo6;}Fx(EtXh1IWiUP9$cbqWdaxyw(HZcOZ6wPFS+^uFZR92RhRE8>pB^4#*wL!fG z6kY%`E;=0m6}N$PCb%N>4}IMSlHRha5qU{p;y|*r%s&!P-sri?<~-r6x=M00ddzYHe?i~GY{Y!D zb`HTmufW@RBC$5h(8^1W=}?vj)kp;$6K0?cRyS#yZ=g3eR#u84+|02^<)X@la8Rre z%Y(sc3&4&I;W#@h!0)VvI(_hy`A}_Vs-AN|^_*G&@z?Q5x@cWS2HO%d(A-KWEMPk5 zK1a-4g930xCqUfNLUjgFH&cl2TETpGjf;VsD7k_taYN{nup3lRTmG% zViC1I5dL7`TyVey^HOWe&XJLw@z$39RDFBO{7z6VvQ<8_j0z4ER{G)~u@g^2)zeh}_EQUlaJ8xb8DtmK^y&2mUqHjJ`SnirU&=K;-{9V^?mdG6AKh=;Si)jN2@KtGF4jBi-~Pb{ju zvN*B(70@}5+xsKNP@lk9=ev-mQ|m11ku#?f81Ra%OvivQl5_RUfD#q3ouigcFDW6U zq_U)<+)LcV>5~HJa)I#yd8&COd70VG;_Ncj7Wd~)Gq2i$PdF=|0^)$GG(8oFs1w8t zi_i?u&mxNpR!}$!?Y3gIwk3Z`YA>c7DWwMS`{~Odrade=rG72$+q>8p>mm+P0T~_e zf(CVz2}EFIAMgMIl{Ln7wF`xiGEa$%IEW(@fa@f{!Eo9ZLc7cWdxsklOiXr0rb^zO^jy=e_h?j)PwLFJjY^OR_S)mv7 zIxn_Y_u`O{JEUgQ0mVc_0=QxA;6VafI|g3pG{zsmYzjPur!M!QiG;PRtSnp>s`lGM zP^IG0h!1lPj0-5#kp3``%cJ%n5M0I2?cUHF+d2>#@ak6pd6IH6QSR-mSvqtDv{Jf! zWrjv=?uHR_!FO_))HsF!xSa}#3-p}@Fj*tosoA8>I0K`y7CG%jF00Gs1FH$Bec-TQ zUw}MXDfhaR%HDJmvVjaKjF3tLjEB%kK4-de}>yi z%^z9OH{$C%LSM}HjR+%UAOpb8089v9HS z6`KGaq4t75%ljM}n)gQE>ZAJncZ%GXAJAu57YPGPE~7+ny99v|uq0G77L!}r2_b}p zNhnZ<1}zr6s%UlKyae5s(>4>#1rB{(JQ0di^n@0*JL9pn>o3^Svm+GgSs&|n#v)T| zn#WsNm){@vl!OBA3QuuqayU5^4zzVN)p`A1t2f!*&>zBmQwnp%{r5cA-P7C`<>Qbu z=i~1H*ckh%!}E~W;^XflUpDlcx8j=HA;wlubz(zli6H1)P!ucV6vcAvZ>A_#3-9QX z!TR8*6vbv5OJ{_Ti_b)1(Pw7X;c5)(V1lV(Y zeK;1!%?aUd!|X{d($N`uFrrrG&nj%>8{U)P;zk ziGaU*lhMF*I<0n2!)#<39}tFvB%B zZ%%DR946&6$P(>s8ji=SN{c;GNkwgAfgdLK_0A?=Q?w*%G(v0M8Tw{2IrnQk7uIwI z&c~1Q1gzM7d|Ww|H~wdG1Kvm0^t1Fyb{;7vcCZ`m29b&Yn=LG0Y6n}LUIc+^6)tLl zl1(DTq}VPwZE!79gCrx)OOE+?>sr}KQ^~a*;mzNoUGA{dXS$}6zIjsl_cga#Pg6PB zX01&o;T&7jZF%#$g97;<$G88S#fS~??FAONRYq`YK!Lkv!`Aw8w+3>fxDBAQBR=}$ zJ5$3DZ|kWV?xBP9w~Cwo)>T~OvQ;(JE$&ohwL0&+AEY1?N<8V)^xNo@seMkZRM0`M zTVS7DxX&q8)@ors$tDBB9iu$569Q1Y!Fx7-qBS`^{90{%<>uJT((X0Em@i%z@7Yq( zP}W12(KoMmY+c(nu{a>dOI=;9d(0NGkxhwGBNlD6cekw?}v#MRocE`Yh`rLAY~y zf73#NfIa~y12Se9MasEzgCtNd*K(2xK)|X|P0q^+FD%~0umuT^tJAS9=S&SoyPKB} zH=lEH=TvljU31LcTwmQ2Y^<4XXI(3oc8!#~n!+-eONxX+Ug!Lp8;DJ51U5#&3wt^+{-}e z3jJ9=U;^^bcoQYuP0!&lu=(wr!+=vyae`RD)d~ByZrhVObaCpt>vruNrwsA)2iI>Bj2T=%G(#xOg@8Z;yRNo66p*VbvibXJ^QR#E?epFleS875|8DI)E{yq~ zdnotyX4i4A+Ba|!Fng#C1L0KfH$$UJ0dd9H_y|i0HADyW@q^B3;gIah?c!%a(g!7&&zoh=X%DoAFzc!7w5m=v+7!MK9{elServ0KPavZWAz{xZQjgm z+?n&~896}?73F6pkHN94cD$hBx^tWTHVyQ&1u?y#s?F5t+? zWVv)A!@?SDHCO2`m67uvz{Yro=GKEFa2xKfn8bv>M7AQlxoQ38)S-)6R5{wa$(|_+ z4|O>W+gE8S?#Njq{)TME8Csh6c?H#`<}Xa2 zn!lKTZs<2ZIc;%!Mp>UVTcLF2DQt>dm9Mkt{QhkkL>cD`(BZ8h6YZvOmDQvi{t zKUd%F2aK_9QV#vwyi}mz8$gc>=%vS7ALyVTc$QX3ZGC`4g221Bq;bQ6#=$ z{1*;Mx1<(%5mVQ!_E&pGTk_nC^yf+te!r9u2DOD}_E*bP9Ps{X z`cjZwPJgwLgXiwN;KIp?y_@zPXEF1$8Vj%k z(tbiGGD0#%0p5fg7~44&0kAfeYvlsazEZN;Wh_%CEfI~Y9UHP?6F|G2O~#viA+OaY z*~Q7MMEY4l#Fo(8+)7KHXs!jz0rCNVUKy%##fr&yw95zx&3M98q^Aw;`Sc*7XGJVvXaeC|2#pjKrQf;gC zBaOAa(WNZ0dT8CbVb47*8eFXWYN)-ww)WShYX7uTSl2hde_GC-u&Grj&_7Lp{%Ha| zwf*STGY{W&?*0pA?t1rK+CUA@J*UiZpBr)-P`7A=ia1y+IIP(ELM>*s*(5lfL8lz! z{T2OFPj9{9rcI|#y-cT+cc`7)tN1y%AB7I1YF2Dn1VL%=vr;KA+2k8CZhY$Z(?21ic;20)Z}i>MN7blYr)WT2ljW!U?5{ z;9{V$kBJM9keqnTAqOOgj>LsXe$AXy{>P`cUcaVvaRY9tVpqYzDOmmQpNKcu5_)^d zd#7;jIL8BgO~`?A^o;w9%UjXbZDtTs!@h5K(=TI8EuM&0QYKVD&nJw1n4$F>w7?>O z_*0!iU*N=FI&`G6-zW!*NrsPRX3D)VgZ>joc9~(13Dlw$mrBH)ogqSqTHkn3OT9|u$5Ew>~iFexJ&3v`dezFRnf z3zJtKra`lE#gR4FU#NTlQom>9!DT}ak9-5m{rB^Kf! zC8RYGQ+w*n0H}>lLQgJS2r`nb*Ch)I2mxZk0!M+pxQJMYIbhUUajNiK1Utbp1fSDK z*KIv_GPP~{VkWnVFecyP1A@ZMQN< zh3>W8w7{&q)y=;XcoLpq?+^iM#nqZ1Xxv*&%}yw_jz6(>E&Gib&j;MW+t@qAFIIx= z#7Y%kxg~v?yo;Y(o9e8S>fvqiP57>b)prs`ii`?q*IGtXv zGu#sQRC$&Ofl|A@)MK-Glw(WUY^_BVexM9MSsf!^6$(fp(LXE_+Eix?#iTffH6w1l zJXE_(63wi*)J=~CL$ukbJZ}}+fVNtdb0NEv*vOX?=B%9gnmj06tks-A`T;@x0UTSg!9jg2R)MJC3e zwwtjLBQ~qcju^4iRL0I{gs^q+(u4`zSOT0I_>2+}QRHj*6Pne=$O>ooF4p+_My8hO_SlAVGFEV;78tM`) z>`s5Z#cZ}%Earlc9EgN#3oRa##cZ&Is$?(@&_|*t**A%g>_}K_5*M8TEY`(iDCCa|kDZ0v*+{zwR0gG0#g#>ldX?`C?1Q*d0+Z*j*X z^!#;=jg!4&!N~H)>JfU&v(Hk~bI(2hyn42}*lol@^bhOdz2Ps$)sE3!>pI%jY+AQ= z@uJnM=`CBg-gw#CwTIRB?W67NHd07h5*E;V*tMLfilzXTa{$~QJWFO^9-&a_x?|!| zo8-p!yU+@_r3JbzyW^3U?E~GW+E9^qa&_C%<@Dxgv$4fkQc6uPL)jP+NOgKAJIH2< zo0O9P8A~j)Q-je=O$IhX48$ZDOq=i&uOddHH~|iCWVFC+(F0WiuxvzWvZ8{JiamhF9b|D#Fq0s|wS=eWgW04k!Ss`N49gq7YL2rn}WP3~olpZ@cYE`ycN89?U zhXXM5FtxV?m%o1e`0FDlZM$aZSWL`dh>Thb8E|=s@Uo*M z$w@OS%DtY_61Pi&UOAA8E;g1!@IsbkZ5dq*W*^~Q1%;q&fJVo0*%=WY+VizVheoZ+ zgVxbTZ@JS|N}bcwjmNiqOW8V8WOE1Q*Pfi7roGC^&Mw9<>gBcw>d_N>c=W6=bv6JE0{J$zTe>JDwnv zMzSb|D9P|LXBe}MM&Pd|mm~H2o}_p1YIH zW^Bhl3O5tK2t><*>1yb;&biCzAB84jB+DM5l+oc)>?`et8;1$o=6{Pf;ex*r;N0hZ zJ-2g=#3)HN9n8ZCq%@K*M*mm@b7>Zuj;w;&9D#Sv;M}0Bt;7u#hj`9cLInfxLqB7*5AAWO-NUrzj_AIEQ?ui<;p5h66<8E8s0SSZ!g5hL_)p+rwN0SlZi zGOB3|r$h58C9|=-j@nU8NU{J?ExW29*tyT|`qrw+)er1={;_Q*R!y$DV+-A*d>*s_ z{=0`ln`=}8un)}X)ASeYBnd(P1E>xcDuFkHF^(`n9$B!L@Xl?A!^n;M4s6pHB0)Le z3j_>CXw#Shb>&1^vTd`}!lS$Q*R&?42bV7!U0T@EIoQ^*YI*ah4?Ly>GCf zaZyX}z>+0B%a_C&I}&YcW|noYC^C#EyQX96zGZ`KmJ}lAjF{n^i6G}plq4OJqgXVS z0O4LVPGv}q8)$?LP8TY_y=1zOdJfXD3qLdWIGa@-x^WL1QP10;aFqDPCGc*{HENA7 z;5G;GUE;XuCJOij=wZ5(Jt`c9{;klXvCPQ~Iyz!D2}}fP2P1`sXpqf=Fx(V)G7+rv zjS3ttQCU=3Q4UJqCKX#D7#cJioj{n(sZZn3IDr}fqLZ@%iMm(omze7Ufkoye`ZZm2 z)7YrICbKAkcDEE(m6cZ(wsg-ed;NI+B0=MFzQSJAYxDRDBWP?#3FknQaIdSI3tQRJ z^MA7Ekqy(+)Awig5}VEHPmohD$_e;ZLB~9Xd%Gxonl-XnVg;U46GrS*oDbLP$N`Ih zDbeWSZF@6U32U6ry+%NL&$~sq?gpNv7g1N9(9Rj&DU9sJcLhD6E@LN&bv`Fadj?O@ zaV94TypDwn<)mVn&K?~eKDut*(c$5%)~?^Ob>hh<*~!&k+rRH?=dAkL`Fp>txIS1<89NE6 zJNy}NV}Hf<{{>}E(x;h^&5|OBhZmqN2PuR>c>id`MQG9{mB5t|gln5yZFYNvPIx=d zNfZtAx#U-79>06z6Mf5T&HY93ZhBf7pih7f!_UTZ0(yX%Yk>X^?YUnG1?;CpA|<2} zV|AiH#WgzM5;Euo!;IO)^x!?~f$~%Y?Jm+~rKP2nrIl{UA;JG7i?JMAsK_x6*oj4e zGhcMMCB5D$V(Ggi6nyKp)E#%@f95Vf|GFnO~54>=jxO}r@NHgRCU@Rge` z+^3^IHSD?QwLKRd*r!t(VD36Xm>c9o8&OWsUxKh%B0=P~7_K%-!S={Hmw3gZrA7#m zh~&4+X!JmPY3yvG!fxUcBh&ROqNzR8M<+Yc%y9Y2_DxfhqFmih)ff}#jH@&E4H3vH zNQ^{A^*V48RjZbeEUUD3*GXW_L?m|gs8wm3ruJ#Dr)K9)u$j3Ne18dT__>vdGM&J{ zI)P6{vNY{P7sHA6sWv1WX`yYdZtB&ESEsa3LKN?o^iJVAJg*IjTAZUE(!-Ha&OMeh z%N&%W5&Bxm;hYbbgzHAFb20qHz`ks5!ISR-S%*d(i3BV)|A|4^$xLeOG` z+iBC2W2;t``8W?}P)eV<8(6R2-UTkcq||-KW`zz;T^p zBmNR>#CxeB|JygN%=voW-;4b#nkr4LLmr%z5Rr&q07wPH<15OZ@Y*zDX_ zG--f5iqfZrootr4xCSVsWH4(tFDMhYn-F)U8w!M6#Oamt?B?u6kSiPZOPJF4a@~vb zmaop8+dM}j`^MCLB7BL>5*zVA#Mz`r5;mhqXZZdz14n*J-GFvGA$E_ww8Vw6A^0a? zF?zC=J&=~+(NwFKq=;a<`o;?%Q+_i&P0OdJ23LG>!!-Tc_L=jx)3wU|Y!-V$zNMU^lADR)aCEsxzKcbIpi6NC<9z5E9d;=Jah^%XbkMOL)|;>t`3p}is+CZ z&N%io%2X7o?YezVz)m-}#93M8)}C{Y4kte|*0rWS+C8vhyMcJyvq@fA^wq=#>wsS+{PuArW3&w{ZzO`QT#@ zJYMZz;w}60@UXCG4S?A3Tr>e2_vPw+X8!L<^? z?#KNaPedJ*>Uq2ux<7*HjXOg1Nz@HN2<%g6;33<=?*w#&7K6s&yq;H#_O6;aTJh1I|Z}s&DgUZWngbVTY70&Wf#MguG5HcHqgAbfj zEJXpv=!g_)X^GgRVh(VQuxpR>_a9kv=@$*-)6)~yTQ7X%k+mB(p2JSAx^w@&J6C`E zgz_w#Ret{YJLawi8|V64Z@h{y;y|1L*yS!bTLwyXygU#H?66NFRtIJ*9}PDwp#dyO z5_rvUwSqT;J2+yO)EUt~Oy79*?&CeZ*Ic<}!^DcwZoFiz%6qh`bo0`!dzJfWVc$T{ zQm}EX>4W%Q%fSxuQlhf~QSOLA&*&hwG^0ATT9F%KnXm@16iSkP5nQ|+_`-Q~d8#=7 z1y4+tE?Q(e<1OE?ZCiQ2t+uo2n*6s7`?NECS}?=@GlTuX?~e_ri?DM-mi5&{c?%D4 zx+1Dd;RwnQsf@+YmMh!6>ZfpPy>}T`ZH<$x&kI%B?*sFCW#x?lg`a9$r|LC2!rX{3DxHezxg`fqaF=Fy7OH z_D2y|e}&kQ(EyygV4`luKm?jnQJO3$C@3l@;-NW=HX+HnAR|gCY@pY)uWcRQ_}ZaY zx9m+OX}R(n_!sx*t`9%tu_OUvInedAH4$@CgPx99%*+5zJvETcMyl6GMol_FzyO=W z0gX=WHvC422Nr;*uYIX{UezyWhw#g%K0iJEz^?om@L4EN(1E!XI1|`&U<02in9?1n~hJT*p!J%;sM3>7lw!&_p#HvZdx_D`m;NpT=#WW zaL@R6Z&zNVapigZ_v+vF_Wt(u%-y>iW4<21SUYXZVxS|X)JTWT)CkfhM!KnhS}cYM z(0P$jJ!E>oc;Q`l-GJ#(|0yoapAHv`4|;MKB0+at*u8y!*JSs;9asF_^fWbok-mQ0 zsPfb6ug^>@t~}3Xm6vdOufGnmsYut*7loUMp40$iM3^$cPpQa8bRw`}ks*hj(Lo&~ zmw>ntJQzftpcY|CgMko3jiEXe49LDpq9=NT(Sh_>akl}}ZH6MI=GCDbfoB}#7+io( zL*$>Xt-kz7_}JJ5dnTJB&Op%LyL4>r@{!?@&HtwSj2=H##QwOaVdct(J<74|8zy&I z^o4e--EZ$K>#u8WHJg?u`Z{~xUcL-?)YEC~C2%X~8Xedbz*(gEz%Eh_T@%$Vu$!{E z_V!Vrl+g?dx4XEQ5O=w|%u`zIDt6kTlWkGRw4hsTfE(P_62}6qT-SK@RwKQ_uw~7< zt)g;;Zrhr9PVxm8UvgmXrx#y&<;9@4AqM*;c9KYBFku1Al9@piO65z|YKM?r6c9`HKmdJed&F)>1`*W^5@h|hZ(nU2i2D|o1j;K*E6v~9#%AXpqrXVB zJMAs5GXETu0P>!|>`TIH#4nzw))U-Iz7AMm_R!aX-Wr~Xr|ZPy>?HA%?|_WiGwczx z;mm~4kMJHuE^n1T`B^s@uXeRSOb zb*xf-r#jZbW=RDg{L3iQL$^+iwpYw_uST*h9#RFlWDK%68e|ou9NRz#RXZjxdY-xP zAa>mnq5rlk6GrDZ9>%W|yXg}LwkiLqhSO;!UU{#w+2z0Qhxe&V?-WMaEGb2sv5+wX z9WjA*!z=~{=Nw$!DYKD*_S2Lt(&d*zYYP=SBS~=5!M|K6)G647(NSytylCfRf4BVg z;|qm5m6Hk(?L*AdiZRa?IO%n@Ow>iFAY$;-3{3Mt4ywzR*YPGTYMJXsqSGNfzg?Gv z&}VHRWH5Rma=~-z*%G^O=+gtD^cM%FM*BNjYu!?XtE@a2YDvjwjgFqb&$4(RvZS=d zXs+;7c9iHBg%=K$!WzNHUdv`lF|VHH^g##|g0v_h1~LVnaby%=0z9!(Oo~}>6*HLOMfpSoL>F%JWxWV7)_2Ye^k)LJZ?>FFk7AdDE;CLo%D5Cy9CoO=%L zf5>||<(PWQK{=5Dr|-UGj1!Y?`oUvw`uWWHwON}i)r z-{TiBCexH?fHuSi-PzyZnPC|jP%;NVp)3;;Q*LvL#(7Ji=y4 z6-2cwC^O{i7T7mTFd)~rYA1jJIA6I`kxDS61A!oHBjP%nc7ZIB)DjVfbM;HpU>T2q zCU$_|r2InDFExGrvaI}+2S$}2qpc5n{ARS{Y7=2J?sb^4R25%#XJ=ZqsX;X^6`#az z28lqx72;GFbLFxC9*t|*?d&A*Z$TG3CuP({Mb=Qo1RWH}38ETT6^Tv|b(k9j$`8DU zw0pER90-JJz%d8?0Ih|Qjvp)lB=^g9OU`n0LC90XBRJXZ$$|EgL|J7p+&pn?B;&ZR z-LY%RX$}PI$6~9dlyqf9Q=~c=u7Bpr;GnM|5L#qj{k8q7Cx6xFZ@z{`CK@8En>LQN ztf+BMbT>4{n_&OO(^iZn*MYB8K?PP@$wZ;f%mgZe4iup)Bo#!h#CW6DbE#cO1_Jeg zkj_}Tpype|&Rk(j+|AgkXfjvZjqK~rS&j^boltpp^b+&KeyxFduM3# zz}s&R&?CyXDD-H?dy>`T`EE?q7g>xtQ73@<(gE4<6u^PNW!6-*l~~av&unCY=l<-V zpX*1h;XCf!_OB;*-*Dr$@7)7Oe6jMcbT3^B_(SCf%FhTz?8O-78(R}G8x;i|&~!rW zD$|Q>2I3z?Po`AQPWxUbaoQmMjR|+Wj^~BIBN78~X2Rg8VDiG?c(7# z`z54B&cmV%0;~#%-sly;X zzu&<2vhkb&6?x1k7gm;uZ{$GAjJ%R2Csq`Z(5_PBMAP$PM6=4jK#rK-;WuE6sx=WS zrc7s{dZ>cd5ugFgyesrqv&zmzTz)CD*>C^04c9O`X7P7-GClmEX18}*H7!6DWu)9by)TS8qLy$%XO9ldg+CXhk zuJs3fXigA=4`YzLDSVhicHt+dBf=ehr`j);Ju4_Z=I=z;)-*Nm>%Z){pnxP=rEggCey!G#`G zNf@=?FeE1uQgd^2YjbN*t_{mIKFMq>%US=t!n%0qZ|e?l1} zNqJtEmQG-O87Xj6jm^%vG$ZIUN;dDV>WA)WpQ-1S#R;O2~fck`UWgv-96{2QB9Ec64oztq@39L@*FikWM6WMc+# z%sT*vOfcO*GyQ|;t9YIO55&whyU}eR4e8Tt6+20s!~+o-o5U#aGV(UGkmlj>^7d>R zv4bRCE<#)$S80ixIElmW2xg#Wyas|Z=0H+EVvGA+5&G1vgO{$n`bL31C2m+dwH6fW zHw%@oir-=1;_?09FWZ;C<8tYvf<-50Cq2f!+0_? zehbs#IAR*`c_^TR&woj?BPiepP0;rYN9YuwYPA%IW-6PA3A8mBEOymW_8EQvb{$5d z=H3P50feDc%+$Z)cdb>|xDG$roUX(8mzN|H!-Z7T6+knQ5$FR=nQor}l55DGWTqyQ zae^3)5lCf5A^ERew7fqP*OV$UgxAW2)Zl&WWGt z(sis3<5IneM2XX874a-|Fn>b1)_7fJO1@m9 zJUgc|ZF3Eyf|wKw$PKFp^i+W4zQ8u~Spj|=5fb2X*ouwDGG6U$mt)-F5F$>Qxp&`j z=eF-3yZT=62JXA@TH2z#K$i{B3gyq#sJu2n7}P7%pCN_-aT8EhAyNd;12Bb(BAJJq zC<54kIp71_1eMgXt4Jx+@^n9Y`l1_dJUp3Nv1;WqP~_Y8?A%9%xv$WYzU13SGEooA)=bZI`hAd`5t*LnzQD#5Ip=E!Nmf-stB`<0mV$u*Mi#Ur zCMrK9zW{6tw3cp(MZ*!&qE)R4rM){k<74iM@#dC|oi`qyoNladTdtc@q@|A3ojozy z-m(5rTXR!$bMwaDy@#kU_ffoMYWJc6-_dlA&GLFW zH)Tj_PMBU`5PwF$4s-MbB5)bsu7s5M0zjGwMu{3~J#&I|Zq(X!ckZlifNL_4GnfD3 zHrZ)m51S=EU{)^%%pcT7iNd}t2n4Ec)D)mY_BsL(MbKVmD3)iIWnQMRr*}tZ^XkR< z78-wz{PoqWh|?$YVdm;vRCVBK;V7FW)4 z7{oqwRHQm_A2@&-G8>K!pBYTc5r7C(c2XGrkh2qF=kp06USZyJ=*g8Ul~H~gQ13US zPgBSdGh|i<8@vuNo~00O=2dZBO*3kP`kLhBgFa}S_D12RIz^9 z=NQ85Yk4nP+joF9Eg^M@>Jq0(2VK1A2;h)%rlP7&Tba#P4BHM_BJ5$=oxz>@AeJID zg7W;=o6n~%Q+02bI~;ZxtEd2B6|T15r^?HRFONG$to4&q^a>td(ej+^ZH&8@g3oL% zV0s;ZxuFOQ_%8qgsgdhOMh$?~iqfPc5h9gJC2l7~;T=B7ZvqIjz`2sqWsgS$dj^}u zu6iP|Z{75x2d+H4>7V{Zx#*lV!)sR4mbN)IdtkHqSIR@m9dxK?)nLE!D#vCBCbZov zpu4&OBlEz4O?CQB7HT5YV4|ChU=aZO9iR@NvPunHqtRL60u%}NUqX^36(H=-mxI>8 zN(ZOx6g1R^U{bEtU>o!$3&7>qBYGp5%)xFXUroMEi&Xpu?)BR+?#jrkxZg?%5qLZf zic)iP49$r~yHs3QU@;p+!C`}l5rnZaP1rM`7_1%O5XekIBOZ>s;}L_~5H_%0j&1JV z8#-@r%glwL{cT!keCI$%-^Je??id1M1>Ebx-E5XrL*6K)=H4nyjQ7mUO}?om={#9E z?*LSjuouJQpO}2h9tRAPx911R%k#j|^!mikGos}IS$Sz598UQ+&V2}R{>SkyTaifm zd7K~cT-f?k95GW9VWNr65CahfgNTjiBMZj*0|eC`E(ZX-pBU}uS%{X1usnY;Wz(uE zJWI@9dmFS0!dQgu|1fsuZotk=PEKEceLi-ksDHzT{*AfVnQYBa5%EH1)?Uo? zB6R82!=}{fbDL>eSpdCb*X28R9+B6r=ok!!<yLl=z^&6>M^$ORD=0~ID+d-um6fZ{og7=-oS51QJ_eaath*Pn{C?7x z=rK@%LR(cU6`_G6BchIpGtl@8b1|J+=Q&D}9#3(x$M5k~RY3RAYJVQyR3%O`gbYsA zHd7%~&Io;c>BU>0oEqD7U~&8Bt2Z>q+dC4=5*=GsQ+J|c#WH5J4=t^0FM3S>#Xwzk zK)JvdjRe1q`U2*AFS8OIV6&0UhY%9uUIiimbVMg%?E|K%6cSW;QStj91; zx|8Xdb>W(<*#EF5E5h`=GBH?E<~9`3k=Wv^!k8~I$o6o= zjd|~Qgay8|^|FES{>!&~>8g!;2F3^XPSXhVlaR>Xk}8c(-|&qWbx2U}$<_sM}e4t<1Eox>H44GD59x>O2P`?^>p zzO?Vi=7Z-n`G*!=Iova#Z)xpoZ>;IAs;^G9Gp{evP~U2`bhfTqv$U+rPk3}xYabrd%rvLuRI6hEG9>^>ltXW@N9bQBjlwJEO&ZzH_Wr(e zx1>xk>+fUd7t?QK&&^*19r26*3v`{i`l8%%I8QJxn7%<@L|r(Y-pQr}9VsL}a$%y- zMRUrzMjxt$`g5(A%cznnkap~v64UCsp#FTJ!mrE&6A2GOUVTNrs!FK%&8z*gomu;p zzhvp1{Nk_3z^J@3BLsFdr}*nfzdoKX6s1Fz^&GQu?zCJpDV7gV09S$9aYI5$L_9)A z9zO5$y{T0H)my1odE@=}ne`mv+>o%GezT~?E~t=;V}D>6Enb?!a-!-sb5Y_km)gqCl5kNdlwgs>TA zB@)E7tVW$c!Q+K?Qt<+Af@Fx_m9jfR*WTrnHx8w-&DREnSSAL z>(1f5m$vR4x$fF)k1_8Tj;y+@AOAD*!t*ZxsEyD6U1nA5(9|McxQkT(gV&)U(9|c% z^wv`;dP|D_Q0WBO!@Ew`F$dTWY;8slK8^Oo<@ouarGKD(K6nloT-0tNyEBBsjj&=o$ zD`z*~bp5oD+GQCs`C|>tszl*@r_=niK_4*C<;+~2fc+zou5=yyBl|t^kPul+#uIBw zoJ?ek8Y;>~ooF^Nq7y~{6*gotGAL((MteqqG1fV1Hkbs!OM*k$L6YGx*hk^T;imdp zZ^#=|t)x)ElBosvhb4oWGzXeix8#;uAVwvr4I&~{(4og~kX)_^yJPjnHKU8hU47xP zsg+YJ8^;$dQ(lcb8xo6}nj7e3bf&w;+3aK9&t84`@m?iup#@d)@yoBext$hTl#e?V zzg$V(om;nV-L`G(3th@vRX&gdyrW+;D`5=!kWdmQgg(!##18pv+x!~1EyxY337^v* z$=Mn>HI1V+go?-L^QrPR4Wr3aO`W{V1>5Eq-pZAuKmK3M4OOLY;~q@!WbJr{pypbu zfs6f3HE7f4z|H3&*?-UAAcjeW<#s1qhaD}7Rxd-s``E1p%fElZ@pl{)vb8FyY5C;FG8n}oQ zp>KV8)OuU$r>Wzr5cnO2_3TjBc18_cY{6LWC)dC^BlO_TgX0gU26~pX4fL;|TT^Q$ zCYkkqMTinwumUb$BA;9Vm$?kH6>!c7yBQ{RN6y_yS9HQF(x#VpO=jf9 zz-JIFcvqG}1bbfXTP(l!&1NISR%(NAl%#?}WvqPzhYyjGU>o2o18fOUkys9`yYEyg zbt=_)MccrVLAvbjRcon1`3ti?`|PvIUpF>2K83m%YRMkLdj$HMQM9Lj$h^b@IAN{k zjX}*DB|5AvgLYn-y0^}3vpQZ|<{_n$Ex>Eb0^oyYtbdiW2#9=K5!xHCtQ(xzb6MMt zf$6^b%2>scQSoO#Gac=AxI5c#x$>OL``X&vw$;lA9^mJ*B7K^Ei+S^F-i(xD?~Mg& z-saoXUgCkWyxf{Mt-N23MRIE1S}we3-{$`M@_49yQ1|l7`mRp7v!YgB9b?{Gj(+Cz z?Hw-1>WP8X%k9qAa;V2kr!lks&q4w5Q{;O#{T=xojHllsgf^mY!pBblZYiBkf0(W( zFXHnbpq=)2f;DN#28%UmJdA*i&O5fnW9JTTyui32m72C(nCk6Zy5vZri+TGlUA_FW z;bZ54eRb|<6nWd0joY8!xC7QB6GER;_0uQRq-Cr`O+ytI@&0gHO$%i{z2{y&wQuX{HEnIN4!G`Kc;Q#|Gs{<}|Dmq2xgPd*X}TWe>nGb2Qk5Obi$=U2 zCJX@xX^?~6%5~5=Pm7cjqMw9P0o+YdAei%t6d-z#!#U$`Ind2de3D&~?2XxYA}?Lb zBKZMz5l3KiON8FNGSJ`U4c9vBYHAW?sbn(s)vtDKVBWQ(%00v5@v>f<+#RQ@TNgFe zE7!3N16}5!fiJItebeypt1@Kk-JB76=)%hm+;Yx2 zsjDrQ#0UC&dJ$-`__4KC$=UEO7>`{Kx*cFd(pB?u5UCc|W$X9qy$mXDW!AV6u zgQ7P|Owh1t-XJq!wINm70Gb3%npY23oAVI-wblt-Js$jNEsbO&1o6|h zfFIM}NQ$6pHHZ;_+J(qQo^E4))NR`j8}Mu#-gu^hyYhnHGV3pPUz)4q7}lzMh&IUC z>fmCT(v10aaJ;!1`cN{|!L3=GZ0t=vv-|1k$*!&!UU=aJ^x34xALn-u#zsD|1}^p~ zHE`M&##J%IBlyLvqCQ!hSU+%HDs|1eJ&#VUYj1~d$M2t;dym>411=ICGX{G>`jpzY zY?NkR?OP^9lc{}^*^v!fhQ}Z=YeXn~fpblT~kPPbtz zJ7vP=lqqeb-{+kBp7dlZ2{WJh&*#4kpARiQ@9Dj}y>rh!=X}riYxI!Vo`9m9If#n| zK+b@F4fQJ_^!p3|#M!Y{VKFtEkStg$o}e9es-M}lD?U0(7w6HZzSR5fyS=X>er6^a zpdT_FDFZ)qv6-boR>FLcRd`<3Bl0PfW`L~FSuW+SaFCT53WT|#6evA1mMTVE<*$sE z#laU`GF(TeR}6nRIP7c5(++&tSXbqLn(6uwN0<6T*%;KIDi#S>=X8NO< zp(Sb1fyc9XNmhSl!%FmLE+5OS>j#h(zz<4Wij?hhU6(1hrgV;D1x$A)oK?!xLP$28 zm89DtB*_Y5I*;gayW`-%tB1MEZIJB)QUpXskC_%qkg_ z%D1Q`-;rC|VxVDrv3tvfO&j}ahdkBA;qqu-UsTgHGSEBnhdO^nc}cnJ>t~l6$_qTz zX4>IYfAQjF`+FUwEhghx>o=?!i*`)_J)E(x))lDr!}rmg)S)l53UcF107q5I6bL0f zJ&dYN81#4p97g5M3Lu@|wT8wB{YhJo3^eNZuUIYbOYg$6Q2BydOtz`m<$?BEXNY?n$;q6hI{gfwbx!Vqx(bd8;m4Y(C^BAUjW}s9x^xsQ%WDCKf)(N z2%Qu$Q3g;l-02R1r=Mu<3_)ZaruFrf(h^c=hlruTab1e)`iBzvg~~NOCi?q5X$> z^K!rLresoCX1?S6;zMh<%)Vj^6{eeX|pheQpo{V zB7|yv282rH?X?rT-e{~hVH2p{9Y**Rx>tBfT>wyVx34o4QRT-5JFRiDqtcWF^r1g>7+Ums9?9bZ79sX+W%VllsePYGB{<+9l zuPb1Re(1YW!JyVW!tB!EDJ!vh0Xvlf*v)w{f~VSIx}o&j`$j=CXTE$zY!>QmBxhL< z*sY|Jv_+daeo72%gB3X!K{Y3|?W%HlTzX6>%m{AJ0Z(N_x>=7r(!FV~iuUxG_03vs zW4?BGd|uG|vfb_BlDZ;?rA!~(dO(bRqs~?)OR?>Mo!S%Cdp&@iqLkPT8i8Y{0IKJg z08u$~E_4ddw^By8BkIX=XC`v04UkiLdanyWQ!YJN4YGcp5jAznVW_U}s&8_*H16W& z?*7TMm8TsMx4tA)Qm5`!H&^#8&|Y%({NX{awpprY#|eW<&dDU=&wqsRX7I%e+6r<^ zv<(C3<39stN_KjHeCr4=!+9Fa6gS->K{Y+HICW~MZQ6uE+MB3%B=)3uV)$v(g14CN zwbx$9_IQ(9aQ)_Fb!UTXdz5OO-alw^9*k5eN6<`>hQ{Gr%)T8SZ7Q4g&6FWPy~Yq_1b0{T49 z&;u;>W{jm-LC;&};-l8f@li4*Yb90)sc}6|^>TcaJg~N07aNl)deE?kU%d^YYtQ@` zqH9n}m{8J|+{~WA9F#~jWXR1CREUvML1m*1jFjB&1Zc0`WEQs?=k5 zM#(D&UOhZ7QVu4slxqgx1J5}vNdEUy~c z(6LaFNc6NZ$BmaQ+qdYNFE|UvM=Xx^&V*}dJSR5B`vh(OEA&YTg{7fSzme$#LR?%Y zXpS&H$}wLD=wo;v#Pj~fUQOky+%G;C<@kG^!*YbaFO4D7q5N0k zbD{2%kn-i^69chkul>`OQTxYegv0}*)o3xB3Je8Og<@X&M}%CUSqg=J+#F_>PC&RQ zkLvRG?$ahd)b7tnMEK@66Wi{+H#KIwt>|BZF?${RfH4>B1GZgr1oGXa_&tsVB%TPG z`yuyAlH>hcegx_cr{v&A;_~m%mr#zj5Z({*4tPF~gU=*4W1e1Ce3Z-#&Wexv5tzW} zJxUycur%l`o*Tn1#&^BK-^F;{Sz9uIKC91o6xq~aEB-5_0uR|$( za4Rj8d|t|A0CPlaXU0Z>2KmonqvTfe2dU<`NVL33b=WKc7jZeG&_2)nT*DgM99m!exDeUd^D%3$8 z$&ZC z&+GPJj3xzKO2;bo)-)aifJ>>PtWe#tp|y3TDYA9d*$t~YN801H)sbH1bfT=)Zg2Ir zFY0KaO#okN3UB$+zLEWX3#lph@_PlsQU*(g?Gor7PR2e49G>5pQ%ii@|T zTeNoI)x*!Fn{gA2k2m?T0Qn3~u#faqP!v*hbI(CAp{bG@n`{UsnUR!+{a89Sv^I>? zW!g)8GWiTv4M(U`ZaaN7(m1uHo{!B;@?*izbfg5%GDIyvVtxj6iR&qjUrJ+tvOtm~ z%Si^66a2hwXxGeXQV&{D(3Z)wLmUItoIF9_!rZSuv^$i^k6AbRp|jzUcutF~O=rX- zb@SGMU*S8@8pSlN2u$wb0~CuT-h{SzhfvSL{`)g*%TdwB4Pfq}jnqdMn2c&+9v}&q z4z}kEfTYwc($`Y@NfNa2xCO{QNB zd^&3O6zHrU?jFriI@(&TuCfAU!Jon30DAK11Dm!r8y4q#$KoIT^rt5<=9CNw7BP;0 z6OU|~iXFznTqr`P;$S3jxH&S!3cx6Vc!K}LPLxVIApo&!I0jWJ$&?z%Xt55h3y#dF zBqWtb6^f8}n-tA7V*%PSb14>}H5uF{H^Q1Y8hsUFJnRjBXkT!%(DFI!HOCE7QQYga_U6B5XR4b${Mzq>f{9s}tT38dP^c%cR zABKZ56gI|l{-Bs#fc`(=*fkXwNfF+LQ#5&wjSH-fm>-XkPpSSTqZE(wq zw_KGQcTRP-H$@fyaq{CGy~>)a7u5Z=X!!tS!tdYu@<*ng+V0W3n=f=Momx3@RJ8rrI+!{jx$wOhageVoXYEf z^fG$E`TKWCX&pVimqk11$g=GmDlcFPrPF2+ zR0<8y^Qr_Wz6;c5@WFdz&@8e$h0BC2J(Y^a38k%&gHw@t8|1P`&@DK4rf;2y>da=d z$L#i)-6j`S3HPWth|87Mca{{AE2Z&FHPwrkHkOn~N^ireU+Jco_Pa%;_sE6r3&wLK zz4yff&nmuh-hy*8^d9(`Cvm?iA#UJQGf{O;?&&zy!dW@h+@CmxwNEBm zX}&7&(!{|B7OwybPROZGZWdaZjud}hPPOoVic<~rO9rPJPhpG%)pAa?)@}Rkz^V3E zorGLsWAWmh8?H^YQSh6bZXFCO-8h1Wnls$e*KDDqr1DXxFp7i7RpHfy3`q@5uC-Jg) zf@C8vfuC(XkkexATYuC8)sUzXv(^-1CCoI~K#J5#cmu?SI@xCri%?Kn2GtnTz)wD| z5c|Fc^F6XXKjqXy?Z+d*@cgu%W0&55TI~dVHWA@U4EHTvax>P&)`86k0RiM@5hegg z2;?VBsXPPs2^18BU~N#(W@EZU^V9VA+IvN=menI8Cy$HdC%oR@_2`S7|1L-}|Ge+Q zeE91jAO3sv+T05;C!Rjr+nczIXT?L_=PV%%Y7t{5B=^4wC1@+}!u=cY=%26vEpC&?$NEZ0US%dp6!Xm7Mo3BP;I=?NAZxFYd- zeS1^G@3B4b{sgo~00L87PxL_+%H+MXL6i(g%=LwW$||lPdiPs#x|F`1urZ*y6ZzI2 z1kQ5hF!W9H0Z?70o_Xv{ym!)D=JDQ17czM7pdDrjUio1DTk-#3$M!9O#_SV0@90Su z%8swJ$?=tTU|7uLyi=Wm^DYP#ITH8jVs#dqsn9|j<5%Yu8p}&RMNMmt?!M^mgoS=1 ze&LR8i`eh)vk=M6#&>4}eSn2C?J2%HpiT#m68!7bJL$LHeCC z{CynfWjM|W5o-r?`F)|3xCE%q$uyrq2_snlSc5)v8l0q!<0S1QgYQl?m+vlxlWcut zKEr5Q~5X9c)Nyk-@N9^K zw5K@mVEs!Rc!-1Sd2`Fpo8$4;AFY$t7Yn`fj;L!`6^_$1`g%VgcjLPXlC$);IA7Uu zkT&=x?6|TpyGWrDS@5LI-$;KO|E^K%Xl}MTOY$`~7CQL?efRmT+d7Pk3j#yyX&%lK zaNzAjy}2(vZ}>OozKJAHun_AYO!#7I402c~lao%~e`yh6>JQ6_zaOV}(RUI>@X?F9 z^<#Ygqxg3ud4m2KpKrjwfs^iWy#9aT`$+NxeT{`q%}HnDoOG&roOF#vO-tJH+geS| zM^_)de)>quB6rS0ZB-rpYT`D!9@>J>#Bq1`Ed0qi`2Vi%!j zzIS3GYM4uar#dqM9(&+R2Pdz(;pWRHue#yni>qeV#Obd1s+n)?Q$(Yx{pao7r)X_e z?LTjNLP2*cC#JtYF+DXYB(4*&f4_%0QK)fFDiHe@;gOf#~T}$)WITs)oz^``R%VG!2pF?ys-b^`9#`(B*gxrOE`+aH&VR>VIeGcy z4_7=g$wCPWeGmTSZ4_$pd(FW9+2q(i-bz_ApehUhU2RNv@&6l`e(pdCsb1BWE%$IHAj5ye`4V%H>!yr4eFw0FmJcgK$dJP^kT#vkjG z;5VBEA81SC1MOr!N1iGhN1i#zZMwK*&~2U=pP6K#=Mpb9bhZRJPb$2)Jexf!;sgH^ z_T(IV;J3k^yyp)9~YEb{_cg()hsbd-rWQ67Ql{ z#^3Ai?vD1P#^AY=FR)O;C=LX2h4)2HITkXdu^9m)XdwK^<;hj0`SD=yNP`5yiNk($-rlpm^_O`3FY(yEeX%8UYQ@;(I5j8! z!a{%e!ygjwpS^+Cx3ncsP{{FT0>`dP`^)Fx#Vbki;+3X&@l>ba#RFgt+KT! zek(nkb8FKiJ%9DeC!W|=>^MQ+@9)1ijQS~gf}LO;#6XJ4ZLy=4;qh#Ic<|Xu*uzo+ z-uR;7@u)XNiU%kxB+jz|^uwMe#DkkN$n$Jzpkm4VdnYDD^jWddky_vek5)ce;sS?q zN7!S8DegRY`K~ROsR!cm!Gir~9X{MUw6uqHeC3*JzIN@!Ny0+!oqI`wy!6MdyLLd` zV2(?okFe00c=J*tKA$%a4xixVNCg9oDUjkL@tr$1UE0)i-p`|fBAJd+z^7btV>?ShFFLc0}q6q5~cohoO#j-m=ID-AUn)hBQ9d7&f@Q2;rR>T zqv?^6B7cPrSHbP{j=Zn(RdCMucu|+mUFY8-Uk$J}+YpNyB1{{`af1I*qHa13U!J6* zxLG|1J_!G0YAEh!mht78gY>#R7j3zF&z|^!1B21-MT-W8xT}Iby0E!<;R?xH0rt!Z z_5|x7ZgN+&K!OdrMXo$ty)xV%EKA7nwQ>48$a0mf-t0HR@|DQ)`0AD2hmQiFEm2C# zS9b5jM6|$JCNk?8TyS>g%z9FUMNX`j(t_-Xl8L4*SLG~^$5-U^Zi!}7g;(!C$wKch z+0mS#4PhMvf0qJnnp0s3Z5&HzpTnsK+F7c8#36k&-Y`@fi~nNB&u7+jE_~ySH{QT= zPxa4!Ce~Slz4WwLLYs&sw9_nndN3N|3ptbyNO(AwunS*2vM(M#yej>6cs~@)J^6yz z2Jm6r%tB|x5~e*FX)K{i4&_T@37zcb^?TZ^RmRJ@DFWyyshnV zh_9gyCxC+su!Nt7L(e863hmiB^fH=~i7EsRJ=>bKb(0ON*K9rzkH3B?z3;L`i4*6a z`&ayjg+BXnN5_vJ25=>C=MB+Ygj(dz!^{#Un2Xj;6fU4aP_hS&xOw;25H?mqRk}DGE(Q^;A16|(n=Kff;E!wg4-V=27QSH>) znWeRUV^L|bz;^DeS+?vSO-;T)oi-;_AFk^>*5CWU_>v{d^K)_v@(ZknCeSf%ald0~ zz;Z!taehvQN)E-@bLC3XxpJjM;>jzvI&4L9o;+1H`gqQh$7%8|+uhl@dvI`fG`f4R zXG~2y)T6y$&PhC}Sl-Jzx(~$Tm-loZoSr<`yLe)HdLps-sST?{jyx~xB!$3{mqQuR zNE~^faMB!k=+3iC9C^4EVm-VPaP!%bBhT%@Grq);m!`;Dd(&2Z^8&ZWp)=^s>YLAE zp@fg_X>Bk>RR(LK5p^?s>t104aVf77`SA`BFXzYerup$sD27-EaiPyaP~bE5J%=ApmBEh}cB&t{=9>7*m2_!7ef!J(@NM?Lk>Xho;670TxJ?ITD$^cg zN=2Y=RENINHo$JyQk8lERjQh)LTM{vg2Ey&feHDPGAPX?LvKo@<;2kxQJf_zz;1ec zCQn6<3bC7JEGx%(pTz=;5vk@`HlL{szxnM=qjfdCjm333TdC9PsH)9f*OlFi+RXi| z@#&meMXjqYl%E?aG@8xk3C*H;9@Uh;RXmTkv(T9^oHh(#aR-2mD^GL)h^1x0RVY|l zdrapMy@fCSlQ{0+E=-L#_;PhDbY>hUxHHg8DCew0)t%?_U7)3cLI<@U00zvdXCeLo6}D2Z#p?F@LbXc9 zmlssy@Uk&@+jYm4sdv5KuXqFA#QjXu_?}Z>I_0xC-;IM^0^>BvfUtyxNEzg^lu)M5 z)L&_6r})JwI2E4_5uSDdR7Dv8&ryTH+MGkWqn9)ov;+7(*@ts<7_^C7=l>xgDF|BU z*vL;%j{!g4SJ^1*Abt`AOsA1DYN85OMFpk$H1v451xXQ=Dxb&Wt$_16WJv&^Go2AD zvbMsZN+icaJg1x+Z?vnus=TSe>+@asMd|3?);}H}%~#p&W8K}Oe>7XG7C1}Gy+<~e z)_MGmH923s4o~vURcG(mxrO@rd|jhUI&0dUhQ5%~QvqnENK%Ks>?$}50hY58u$)zA z=EN%jPCNi}sA$Wui;IwkASHR>uQ@NTUy( z@=oo<7`Wx@gUX}#=Qybj^rd*N#@u=xr3&uzk#pLmR5Br04$SB>LL2}TOWM~HgwjIy zh<={9+{eR2oxyeUa`Z1XMZ-GEl-g7A*~t|sa7gKmF8O#%It*Tlh|J|N-6$Gh@utZY z(*v&_M(+~5dG*Tk@a#mAC)gg=L7b!l`8-Wbp;A*|h6OZZ7CJk0*kSr+DI0XTTotYg zKxDdIdeo{s6O-2)%MQroHNMy$A#p%4)VHj+dTh_Oi`8l%vugt~JI$qMox69E4KC^) zR8g&U)yhy~-^KgVG;Nn$cFR?^wehKSV*87*{dFV)HT;pw4%%+8(>@x9{ zCF;TY`ua$H1QC`MF1=R8Y3V@2T0b|ya$dLUqkSO>UpY3j{q#MoGL%WIbOW_H7oVHG zyK{Y*xOM`V8&V9F2D9_eiCZGKBKS!MeLfy9D4{v~AR3)%AmzP%DMP-YWpg|}aLdeX zv}xJ!FU2kJ#iqKyv(SehVm*w4I(f6S{daTI z2lYS1@wWdL?JJHTmEr<=wjE%FSq&`Yo1@^Hwt%iM&&5+_@Xg6oqlH*RJSFxs3!XBf zr%wQ!(!DUSbUGdi_1A)GJpAe%5V3sz=Mb@kEe`nR*uO}=aD!HTN-X6ZRb|ivc{y$k z97_qshOH*4+OU!ROK;5GY0SH5{8FkhjMsnRf^+v)4ZM2z)0?Lfll1Pu(l30>LWzy^ zR+wTzfy>u@W5+o+fOUN1t{tC#N^^T( z|Mjn52fP{QhJ)`+%omVz!)4DGNO8mQBqmS>jlp18p9)5O`|d6KF5J8%*0rc-A?V!k z>E)}ROBi1cwYArCon%YSvLCV#DIq@azLu#^#SI4;8Ndw}hoUm2WU#cf)K}_tn_POY z+k@sAhgiy~;Q;R@GQ%k(VmS80=Bd!SDtoM^ZdqvWmL>7Q>Q3ck;-cb#L*W1=b&J>4 z4*Go+!!0u#o=Z%Q^bA)we>QRbzOd-at4N+;8(EmR$=ZiFoRY^QOI5&dD5r$8$-~mX zBYX<#xQzQIqFM=g3HFN^BbI|3t;p3NLtbv&I2s*uEnPp+*}1Uyob7LQbS|}4l?8%| z2}u$MBsCXKzsb?)}h!Cpc{ z&`O_*=|b0R80(I9_HN(xdRyl?KsVi68>$QReY2(Cav$h{g5)fF3;iAq(b_^FogdI; zVUI(bM)Dt+Tn?iL&f5~AHmpyl&N)-l&E9H`hx;-T>F6ZRG@s%n{d7j25s{c2?HR6V zmUQ6>p%nGg8XlF-${UB#WX>ChvAV3haa?nz5SpcAa3Y#(f84aPU1_u*&wbL7s#Tu2*P5DjGDGDSz0R0Y?D zzJHM=4rALg|GR8{>2FiIQ*JuT>{eDKpH*><{R+q7)R@ea`sA2Q6>!95Vls{BUSPih z{rTW|TV#ng{{emmP}z^YAWP8%cLUdNaMnJ8wzCzu;R2;6Xx;wvMDLTWgEpb4_3+anVL@w?WYvtj!FV~MxBoBR6y8hD$MJ6)Aq?|dfNELvVL(prnQ@0HSB8i? zgtWl4<35c$WK}%Zw`;O|_1L=IQ{}7Mz^BbZ?~H_lO&f1$Y-kqq*$a{Z!OlX&5B7_f zGF7RG39KX=DO}29i`I+mbmk{M$EhMtX@9LO-K#Ok9-HH}m45?tGOPbKBXOAfalt1O z5Jp*uxPhX?-I^iZSUOr63?IB%pe!mMoNi?#G?IY#1q;P?UMJQpOXeKy;2wn~)Y>WVy5U_++fD#5-tq5)@@fUV=PTFk>!9rax z=UjdM`G*Uuy&UA?STUYI9^1J+P_H$V0NSOe`IeW*R@eygIP=PAdk4QQMC&v=@Z93pz8Pg?R(8XGc-R7c?2d647n)zbl`oa8sTY1qB?iw7V zio`zvOIW`DYscPx`|YM|7BkjSY)z2)2V0PnC^49nrQOj zWL4VFG#^Wdg`Zi<&m_SZz|Vw7G132`pARHt`SFiZJ2(27UZ6taDbd%&&VTDI)bYyX z1>`q~UAqSND#$sAuwz6`w6J??z(bB+YC;ucEn%z&qqDtqFefKRo1=xPQfZ0;UMH_n z?iU35^~B#pt6$plZ|h@Qw%&Ii5LACgHzmH3I-dsdeLm@LkPI&lYeA`a`Hfk>5>CJxZ^*oM~z-ydYhV2>;%guVprr$8Ra{rcVb z9>BTC_^Nu4uc{XSbbO59`Jj~%zUZcJie#KKY4qhusNaS8F|%wRdkpy(OTaG&MVy&Z zu||!Iiz-U#5*!XNCWi=GE+xedJMb-9OhiWtd^wtIsWBMk(--0il^#2=c=3UiD-SGQ zd~n6&`VAA0KYqanAF#*9?%1{Cj%A~F?A-p%z9Jh5jS%zR5kj;YX zw6l<922L^jqF<*I`l|rPWSR$6^c$$C0>4mTbZ1k*b|_*?z4P~9RCIT%Z@TD`>lMAd>Kpg0-=v^lRj*(3iwzrAZB!)A z!u1UOv_iiruU`f^b2qbN!~*9ygIWOzKm!Dt$;b|+LJ-m;KnVEb^+uB!6fr~{)Mj_s z6V^lJA*(*9Z)^qBpDu3(Yq3H_0Up^ zV;(`A+hfED^K8*FLKhRpI8<{9rpeF=z?k;}iWbXTTYP$pT9X262F>npBPW#sontj4 z6%SUkqYobFYZ*A_cYO=jO--&})cw0ni&_>RT)`e2IkIi*zRKa@qJ!sOb@1}5F5F)< zJnXq-+m<8CaC}M$VJq2F#H9s%AtMUFs&jqJ`egE+v(obK%H-SAa#|^q?~&xmS@s8+ z`~lHF4)6V4N-oek@TZFJmEEsA&hJltNC*uJ`-zL}LI3MDlw$*N9fO?KiE@tFgSf$m zxZeUl8G{PFOjsjRu7nUZQS0@3rN$aC@(slN)XWU6kv5JY_%;Dk(1iO?b+l3e%LFKp zVV0B=DyoDnkHHs7WY8}Sk86YV0J?xRjeKe5OC#)9;?-ZByo0U&HHBI~qV7RmtAITZ za#9*`e<~z)5_?z)X()6cc67n;c+^4|&w+$>A_z!cKLD5jBlL_2WIA;N!!opby$nhcjms9=J&F}D*RFT@M6#`6QFXCu(Nv^?YfJt)- zg4^hae}k01v3`2_HNl^({+oB48fb3f1l@UMLd_<|PcDT$D;Z!bagVk_E|bN`R3w)Q zR7;t_>jwe+Wrvay(i4NdwHNF(%ncY|g9?Shs<0Xi9*@2-M`I5dgL?NImp`RJYFs}v zLq8erTiD<+m08=%6CYC5*2Ldm1~YW%(BQDIJui2llkT7k6F*CEZeRE|FJ+GrBUv2H z*HT9GxdKrLj1BPeHR+6BG#q=Sj9>hwFynU#G_)5oeoY<^4!2PV8Y5UMS<3m?fk|lwk^Y!Lw=Q6W@V2N8??POC z-6FSFY@1Z*F@iX=FenYRAIg^>ym{czMDMz~1(k!r=`9b1hU%&tH!ft4J$&D;Z;VxX z+KNlAxPe-V>}_r*`g8dhxl~ZGL(tq#f04MFXpT|(!^G8)a1Ax6#kRUsIpRIt^X`E? zV>$Z~acOfwUn+kGG|b|j!R1hQBUAoo_7HJt^ISSC4~K+JxW|!y(q_8xS|}|34B=a99|i*x!*3F;DUsLj5%GS-F z2(M`j>y^v-x{>6t4!CWF>mb9n;&NOE8S;B%?SDzuelF+jm&t{&to<*g+b^`p+W%6z z{X(6r{V%24FUooQMLDc*AuL<>;`2qhG!KF#$NqAgk@r_-Gm3KFUrD|`*@b(hO9_sC znCB8c&dDy>IJmICaP?BzI!=#A*6Xryar5@U6NCck`LgkW>&WA!)e6Sc_4GKw_4j%E z#9WoqWS4Bb=3R$%DjTp+zAbs(32U$zcjNv{L?h-@9a zl3oY&e%U&BCA|(rIbR2&9Oj#TU$zd!=ZkWwAA%%@bs(M*_&P|P5mIu#4n%qK?}X4Q z)PKAN^%a1^;(IuCp#J0X8)>sRKeOz2?6K7T@Fuvm;5$#wve!_~$LCFH+>*1xIVk7* z!<*9hBxi-)DChgbo6`6sXN4J*^ZnsX@@MJ!qMVP9B-bWq+3P7=gOLa3or!pVU@wE-2q4hxvO65f;nl@jY6Rl24K9*yArppPC& zwG+-_qTWuO(V(5_Gg|7*rG&a$urED>&*5^|m$>{!`iRt?jCnaO%?lwJ^Kv}h-;8-V zF3k%e8S`>nnioPc=H<9FFDW@62T_jW(2MiP_wD24+0^)OIp4QMIgXEP9+k(Xc_c(U ztKvLLXH^nOcHueBr93X`FE<~DWS49lTsRJ(zxK)WS9+bga6I7pq4agxxVZVap#CyR z&zFr4T$l8hEpre&otpveJhFzz%3KNxnXFHHkI1UZT3N^^zB8Sr_~*&`b5=LU!(&~gODC3vwDsYxj?ivFoZV zvs=C#4pa_DBFp@>Wf50xjj17M?grrjBrHV@00vg0HdtN*q~TfqORWb$}WUm7nx1T|yw@(3~7jW(+#&yc6r6_8w_^=L30 z!SmZ7YfvR(r5hVLTKb0bFWA20{5{(jEmY6~MYOA?sYFQ3DxzKN_|L!p@N++Z(%#|H<1L_pJZYejK044=6HqB?~DI)spZGFU`4=0L=Rt17GyK5Vj=wAIJ2 z^SSbR3OhULb)b)8$yt`q97IFR#2&SpO}bo7j*9dbLP8pPnvC!igM|~5`6Eim!Gm?` z_Z#mwz}V4|C%$NOTS`22&WB&7pG^%W?&CvO@vrw;Z8_Zvm;VoxavuZz8~adt6C?cv z`FS~NrPSUs{w7#aR`mkr7yQQH0_Kn0J{dN7n!^4S6^#v!YbNfg%yV~y3fP~U)-L<% z(X$q~8qfZxs}lhWQ`WV4HPC8aW13M0Sqe4Lsv^ix$fJxfHlQzL0_g*%Y`;8yXK)o& zfQ!z9UE?Sxzr|vP%I%Pt21qCRwtYpeMp4ukaMu)Sa;D--hwJ0R!+CapIjw54I*QvC zY|dqGB(9rSO}8e#1?@n)6!fKrn4)@3jzATppO32vduudW1ve6N+P?E(hH2qb?}|Cnb%N!S@~XsYYY>uT#QYH4YUy6_`5E~t+VR>v1LM;9z> zV#giDO(tWgrY#Pp@i)_9IZzFumRpn-f&Fp9xTR1YMDeP;ijX@1ulcA90lRPhv;fWq5PRL_61slR4w~UVGsw=NNltIJu+_TT|no7FlEDJNJ zbbwqJL?~3C{!midr&KWTiBAzh=uayT3$b|hdYc%_5kDYcBKL5&b%I7XD3}m6YcaVg}Ps(!rUoFCX)f=nJyp>(i&}o9U=!AH?$h~ zUcqAz-qvBnwo3b*PDhB&p|hKfq=4jS41-^fJ3im)T`IE~;0L$wteftds*P{k+_b2@ zt-IOS=4FoSFCW}L(3K>)^o`38B|IC~Zd$i#%Y|;S&SrD668St`#2a-%Xp0V*O@e@* zB%OjF-!QHw7jb$8)LrR4YVH!k#S^4ML!KH($QkXoMw;qc+_5oVUA>_)uyXm{t<2#l zZ8MwJPKVnoEJjz*8(Hesf)&_;qaQpe1skQBojp@a!MRE_pfe}+k* z*XxV*4sXa09#RTltV!G~fo*|>Lx5O=jVj3M_E(0QHZR_O(Sb|1TzRmvOc`6|xsv`& zW8Ks@y07O;7hiOfDpC7f-$r}I%7JI$yHh4->AzrYYk$;JfMb)dRDkvXlan_F2ggW^ z-b&bOC`9|zK1j}0ardXwOl`PQdi-v_P}b0=6VHMU<_jgYNGRxWdIC8G9MPhA8p`B0 zdO$;^lT~G`CiKLjv)h)O6|b#s9I;Q+%MMKLX^xN4t%=(rjjOg?3a0`_7A6Di4(1?5 znB|n`F)*T_1Gr&ui`qaG3ciu{V^Y8nSO5@C>W4~w_>l}t7bbpCQE4{nl^TQqo7FsK z6yf%=6|GUdirWORKK1O516Ph8_(FMc+~Hg@HnsGTXJh3Ce|bgq!9zJu@@$vBy`?Ou9eq4V zHT_LuLC%`hmmRw90K9Wya+W>Ibi@a1siZhZfF%t%0^)MU^!_0-X!3c@9uH)~5YbBD ziT$9QQ)iKIIKuUx&mUypc5NDM>euA=bu3-)ES@TNG&EGzj4#_*QXZ$zT{XIYUu#Qy z(}kCeUDdt(VE@$ehMw=`J$k_dJqK6dISy=xe;2BWOSK!;h}H}?S2CG=GdWAEfYS@* zz#rBxlOImqZ+(XQO>phvb1ueno|$l-GcJecpj>l=z9zkwp1|jToN2AA`Ezi8fakD) z_#B=ih2w|MKfvXpf2SGFi6{qLkDxV^f5qn`R}RXzi}I&YjBLE}fC>#%c;_8%^f;#mOE{*6 z30S|~87C-27<+87!mVk9M zlbmHv)Dh*;5)DIMS%H#1(A526xY4Y)Lpp|st0J+kFxcqEpq_qWvX`|s6e|9-eJY3j ze#=HM1{W{#KkrRUFx~O+&O&-u0?bN6k)!VC3{o0T6B9y{TiUQr9}_|-Gsc8^E^XgA z7#j0ec^Z9-M{lfZcUD)Ag_+~amoD48u%x8bY?xX9oWa~uW(S`f%;jsqEvx#581K0w z(IUo#TEv(T@aaKYT`Hirg1Avh<~?Y8@I4rqihNPGQXwz}>k}Y2yju!Q4?_|l_C`?cet4UEfz@jAmu`p;_dS*O z0DW@tL2ce5wK{QKK3hCE42ux3ZnN}J_7O1?)q^?(^jpn$#7IQxk2FH`Ql(aV>7yHb z<*E^-%Qu|(XWMG=zhktHUibWxl~skInckbn2YxF4=T$55*~(-!>tn}>74ng+7L#6= zt0Mhs72{u5FBvo!Lv{h-?ht=tI0V6QtIto})aQ>-!!I}445MWwhbMoQ=U9;V#}xGp zU+H3ZKk(rDAcGz+w7_GrW&q;QLW&4*97PoZ-=tMfKQY@ZOi1Xb)fF_SV9Abrv9A|q3<3aV7F z0f1@2d37DnUnJl~%b`lndpAS|%gZemLdpZ>RX&fU%u)($qF%2?N>q~-D}hEg27M~E zSh-9zLDg!NB}jib)VQf{@0Qp_eZ#%e6D7vcxYoONqO91`9qg-W_Oj*y`p^ZdI<|H_ zbo7!RPA{(S+U&^r|dpzgh8$tFjoTvw<=ohqMSiH2@iN;`T)jq4H;hV*Ul$;$sidT&vZSKgTLXGbVL^Wa z*yZRYm7J!aff+wLHDIb7y}T=Lx!PP^KRL2(^OoIps}@ewTbf%(nvLzAj_!`ub1Gcr zI$dr~!SD*oE<)lzDhSIqB^$e$L+YTuQ(Xa2ex=M?|ukY3y z^#-lR7O?m+say>tv9cAAG4*fO-hO+0>C$bRx4|KL1kTQ#i`v`ry3X(I&RyC|mtKAd zPtx=iJUzoPn%gE|f4YcEv_Fdo&;8+cX83aO5%=WGz$!?a~kdp9LND9Q8P74N zp0kf)oQyvo_>B4ZXzr(+N1-{{h4CF}+_d!H#OHT0PwKsHt%qERc6#>fF0FSzMwQ^# zvfoSN?&k9Wai@&ur?2C@obtL0=STc?mgl>0o=$z;&F4$H1LpzyC%xZ=^EUtY()_t_ z{^nno=Fx@oIRCmdKW?pt{$bAe&C;LY`}w)3h`b>7D^qSKS7ebhtpnVQ*bn*rOk2$L zjCk&X{=xH^wq$Si`%A^RC_kscI>Yv8?xNq6JwGkqOL?4`pWop5{QOo#KE(Uy%I$=& z*ST_s$oG;dXWHV;yq%eH_}*^M`utLIwM-841?^$lGT>fHCdKE#JaKtB@Vuww&@XJi z<}Uj4tn$6|$1*w0Cx4Dl{v15-;B&NE%Fpw>{nGQ{`}wLk|Jkp53Gk%M{k=54@cn#v z+Uq#Zr};gO_bIQtaNNbW%leysKaRh2M{4}i*Kr=^U6_&a9-wLm&O~u zpTA`EAN(b6;`_P3q-d14+Yd1qncNO(8Ca)emTF~k$D1fmzMY&Uf0fCLU*PvUlC#t+ zlb48kO`V*jdYQZw(utDEN=+NMUl85}AB2EtNe7#)Ch{$Dfn@FgZ)hP~Y?C>;o)T68@Q`g{bej{C>KC5TX$M zN&AR^$Wbquj26O1PSM8eFE3+?zSvRU@Oab&6~pP2N`Po;ANU>n6@!kb0R;?-#34ka zUokilH5C>r6kZPiLoFtSPEk;y)<87hA5n?UGYGLY$o^Z9{IkLD`L1lDPG^zL>9oD) zblQrX&Y~6`A!F~m_cwmcSp=f=zbEfa2hRRS`$Iohf2iR~-}!fD?~u^9-+kk0JCvHE zpl~s<6CY_HZRAi?Z?!N+H91TmY#`Fea>PmULR46QnP3_!D4DPhaxs)sxoWL|r?H%z z{+vNa)IIM$yhBA5IZDMke%C~_)bDpV2x)8eH~8yo0uGnwxBXRp_j2W|)e2uSCi z28cRHCh?i4`N1-y&YW9Vl;32GxXqgqAc5+Wvxr1DqMb5OvoA`G-_xUB{ zwk-Dx+ETEWQtg579NVM0i*o-be@uLj)KCi1> zrXIB#^J&$IL5iMb#FqN9sLHMKcvE(TnYd$K6zcz-{l7PhX3gk-cKv#q{!2QxkK7*3 zlj~T%0P50eGf%Ew3)K0jUUR8N(+`B|Ia4GjX9zUyf_Xo|n^jbyQ7YEW`@uxi!Wm{Ad3npJM6ynf)^ z`yFC#=v;X*&jFn$FCizhKEIT_E0cqc;_WFTf0W5VM{#*M`H4&px(MZ(yO=ktd@pmz zW$S>J8GW6ZY-tutem>MADz@jF^G!ad z8`;vp$?et$u@afzEV?wgdmf6SBre|h-J6cLH9Wj5*tw{C`QpBo_z*ShSQ%rXn?jBG zPkpPpvc{d5@Hd7QJb~*2aIwHi%s8(wV2tQLSZH2d2&~eAbs><15V?xQx)Agq3GI`+ zo}>kbSSVqpfW_f;A^z{+=LKxQJtEmS+;el7PPXjWwfUt#o`AU0y!yYqt^n|l3Pb}DE;T>*+4~#&F{4p~R!3QteQzhxOmU=PIjTq$LJmO_*P^L8|zdn@dQpAy;2m zK(s`o_sF;*rJ4=+Rb6o5#b*!1?_AV(O>Ykr7Fsp=JPRdWs*i4c3Fa2+HGB=$q&kT6 zA^n{PZ|C(Jpe#3o8V)p7zyTY91sv>R0S6!>l?pgS{9z~kT4MH|duU$bx*z|T?xufv zB=peY{(t`MVt8i;C+)nt_E?7jYoX~$5h=^4!+>P3={gM3Sj?@%@Oju<@4Mx|VfkV&CROw&)RgZqPbal{<@)21zn^Z9=v^YXkbK0Jci!` z6eX7Pw@Tba7N~|J)nVZE7cx|jtOx@Pmzft~kZQ&AA`D@+_=0n$-u^%Je((O7IO((=Sqb2xf#jCCy9zhe2O@_~Ny=GB|d+PG=`26O*F z@s{PwF7BD9Cr-_EcxnzrkMqEtT#I!VRASu)bI|;gc>J%FB@!(3Hs)4h z-36?XmVb)63sMy^LP$Qz$67$hCjdMRyzW9c7^WYM?27Lip)B;y?ZAtgLY@3@n-YT2;5Nx@NkW zMZbLAs>{3GWzE*2Ej#{bG&h$NgB}B(P^f$9AbyCG@vLAa0agP5lq&|*R7n&RyeyF3 z3U)+BmH*;mLW=#xK3BOIb>w`j>Euqy#Ueq>F%m77x~1n2FQ|35`j*6}mIPWV7Wm_B zL+3B-zF@#vT2@q4T3W<99Hq?`!={}(He2j1W$s0nubMj8{qLnkmeNv-wG3n9(l~md z{fIcHN~Wb$p`j$B{eV7CwO`ts?RG-!Uc0BfR2*I2{!@)^M(eYWv`T7wmJ$CT)p9=e zji?)-_C{sYCl=qBTU?`rL2{~CSwn@DHPmi-Wev!z1;`mFTM0fuo=Kl2YD!!s2}Im)v(b)XEjq<0#-Q~V_wSlKkDj@ zHTBcuhrf7X{yEbdi`v>EZJyZJt(Jh(V6_-rJI=oB$k5gejU81*W@jBBGx&QYd+F^)e~Wzx;S0_uM?XZ)m^n2yY+_9f&P^}Y)BuwRE1_ad z4I{l}cwlh1|H$~S%SYe&?Qd!Qro={iy!Ryd?8TZIF0_HHq$ApTs+t;N9XTki0Yq2G zv64|x1E}BT7t|1pLBYB{9;c6|a)e6Wd}-sqJqU%R)8(_l;!BeEB)ywJo5O{AH>J1L z+}97-OErrUrr=m+{9EWGF8G@;%?GPe`v6np{=ns*fe9mW2|_JZrsf=ZU}xvx-*qWH z5B4ilS4n>(A2(A&@oQMWlI}W99gGZZg|*KyHv)C&skPK8HB)wtB=%Fz4J_Zc)cm{+ z>;7ZtkHGDd44atO=tMqH1F^&W6San_h+4Hy0d-)Y>^dw%~P_&cqud7jbSzq6i!;QN%HiT*$Q_uMnv|NhO-`O+Eozk599`G64{(nx>9 zbR?Ik9#j#)GNJnoOJ@r7H>)nccGaVIZ#Z}RnmeAO;l%Uwa=I+>F)c{kOBm@$2I!~E zLF~j&avmyoSOivw^(Ngee*{y2MTS01Znj2PV z>Rh%^wV|!p-s)}Z?r34Ule3%8sqQH6?3?iiZajZ*Z*PBFqRa0;?sBhNHM0izujwIL z&Avel#74G7wS|-kI?UCL9W5Ikj}{Y36iN!PXeA*^GLt&JO)$s@P(f`)Dli~0lijxx zVwiBWQ<9|-CNPfoNMlds5L} z=`F5pZ&vLqD`ns4?~V>FUYK9dZmKDJtzBh@Od0{(Un}GiEqcGsjvciPk4H-+Fn5mF z>U5Fcbx>%|qoPzzfqM?fxDfWu?_4RR&||yp1_O8v4Mhg4*_5l#EiA|f-=SNhH6vOOHrK3lir$k?|bLaN$J9lnh*x9)-8jb!R_TB@&t?KLpex7r+ykg5*wk6Bf zuq1C=vLsJg-g_lZ9NQs@Gsz$u2zw=j7AT>NLMW>=O?YWbLP#3gx=>2r7CI=Dj+eIY z3u&P(Wpt#_LVWM{d(OR*WycAD@{Zs4eYAEgoqO&*_l##h|0l{096osPvBwS`Jlu8l z-o4jcv-g&30Ley&E$m*- zYrWwBwk-L)@A)BCx2%b5%Sg2kaFQK>Y#55Bm`w~A`N|0qkY1be66Jhnes>O8Fh&vK zMh@T!w8<1CgE6F#a5*{2tGB3=$q>VB1zLyJ5sXlpXe_=2^T+++Z9A|uMZEC(Efw|t z!iXbMxGKU9R4k}!*mCWTzOEauSst#cb9yTRem>50=_G!W9e_eOKbqn7FhQlE5@Q(* znV<}oV5#MOPhm0y20BXF#@w30m&&FgdyPQ$n%I@o&7)CLzzV@fND(F4(qx!7THaCDGF*LrYr`5(*wI$l*xB0TPjObY)d##y&WeTW*@3eDvW9_q zn&HmcrNPqDfHS|NzB$wpsV)!O^1}nP7G$@6mmPq4WMk8iV#5nbcJl27D8K~al@Q4D z7{P5D36eZHr?`NMp*n&Fix42(czkzcR6Ox)-_dXB21oYq-`|W+<%??{e_YnlDK_o< z>|U_L1<@3SDomfp1U0QU?V|!vs|1x=pgL4UI%st@qoC1@Qt}6nqF=#jO#C{M^)-~h zr(8dM)o3(XpXn|raAs(et%NzQM2XQ6O_)ljM(ff$@QdOHf$Bx)*RE@8SYB9dZ!L~? zHCN}9WOw3vw(z?-TUR!(ttl=FIr0X(x|&l`aY)9h#<$o3$O0E^j;51MPLBf1H#4;= zwkh(68miQSYGn-WWOTaz9N!g`Nyza-;#bB9rF@W$+1UWuu51c;&jO39z(bT$+1oO? zp)_bM-`ydeElw{*WM}QLzhIxeXD z%H0?2A1`dk{q&B-;_k&gw^SByqJSiXZ0TLRkDvFNXfkjQ6+?m$k&nD`@J44rh>TL# zzR}0^20HgBcsbTG{SX9UYG z3y)M*EDA@4ZPvc3P`}U97xH)Jeh~6k_)VIe+M@F6sb58d?#co~QdUz!Be##lUS1%Mnn}rB=6rLW zHJcKsr(Gx2P@sl~u!)wG(x1AyyZyi`EC& zfg3OR)Pa?rHqW&i-6g?^rq)$K@%9CqC;B_vJGf0mI>|yT!lONlqTV?f5wG^cV>dZo zzC5Uy30J{+NaRG9?QCh;+1ty1cdl5`Nk1&owrhNRcYFKp@$p@4T^qJ+-MDe<7Rm*V z@g&9f8DmNb{AVuigD(>Ik$nnJu!vm4DS`LQ#aqsivE(ib6kKuz7Nm%qKYf+2(O=Z= zsw-OE#3E%w{;Hvk1ASd9F6bz(sBsrpP}Mccp}Y~l!6HyZReWdjl{xqY0FEeJ1A+1~ zilFeLbBs^`bH~cN>N@7vtZP?TMsrtNt3SmV>WBo1WfU)~W|6Yq(%Qi^-Ed#+c?!>H zY^v+150-^&`C(pP+$i0Fe`XPwN9axoJOfk$surHcIaJtpX2$X7;SurqXZw!6pz9y@ z-F90Pf1WQ69h2)S8l@ZYXUxwOY7MBAqMz@{&9 zK5)?(=L7!YO;lMEAzk_u-p9|FGnzNe2Phg%Mvan5{w#w3_u)6iALA|f&eT2|Ywh{} zG`;^6{)$C7y|0hfB+~n{Q1p*W&i7qyy5_T5%Rt3!avr&H=QaD+7PPsqU*RmSS`lqo z85BP^|ALAB_BOfJMYVJi|G_4?-Z7*0#zdmZ@lo`O8@9;*#r2BGqmLdsdi2nt$GdiY z?ygkh6FiSy5ophm!nZ67)LwHDHl>IY z*IyHeR#!ARB86+(*`$AQxbdP}HuQB}vv*B(byG}rP%svL&nBUes^rCKe5Ri34slf= zrZh|>Dh)A}z*SJ1pb~I2wi$BlX&OP@M6j>EsH>!5Q%B=APi@{{ar?kcwJC+6?#^&= zi_5=oJDV&YD32^m6AF9%b<4x$Wt9$RSL4+k^)U>jXsF zpL$AuhF>~~Z?MB)Bg{3T?eo&OP7tTV}0aJx$ftviwfm&epw{yM0aTy4r=I=uls8Yf36o4l;vA=_F3ENw9#E(Erkr zVFn5;&&1{XxVRn@>?dXj_E}i~St)l;TtfS{k*eC2^{+*C4E8m+ z%d5LbgvJ%F#%(3tIV{?SPgkz!=-yP1gWLY?yuyauy_b!OmyHhG?l0aU%R{}=NqmhR z1`XJw))=jp(c<_QxNII5$|q(BuPt00<}wn!KJm; zb$c5cJ9OoR4WUT4t*Oo&sOXAd_0=__{))vlm)2SbLbXGM-htXcj~%r&;i{mSW!IGk z>!!Zj67*Gh^}4K{>e@zH&w%s>{*g^`IvkAp6Y2120`~;#{a6I}k}W;=>^6P+R#jHN zKe)(Wx3a!H%o>{OqV+*G`I#$k`RXc9n`ie{S7~TfOXu2ThQR}b}dchUF=mpJni zmZw*4jYPKg^zh$pi$_~qM;EuTNZWNwmh5V8-?e1Pb!}biw~{=)l>!fts4V`I*GZm1 zi;-Wa>9 zVUe=IKup$dFG-NKebSBiDvN-RWbFdCV#CQu=XCg7WNjaK-42&~F6&I(qA-gRhXUBN zq`a%9ZGO#$&TD(Z&i3Ngo|{7{o|?|a>Y`?6Me*`FE@#&arRf&-)}9v}3%K$+BbRs7 zhsuJoluh#&knUhPpNyQ%awbN^7oP1q`n;}xVz!(ekZ#03u_~~CTsiyPO)lRP|5Dkw zcthRB_UMYj>b$nH=H9kiTZyd`cbqP1bCU}&h)O3Ja(kgW(dOd#gl#T7^vIp#%FC5Q zk7iPDS#OQoTxJ?wE?!hsyRzYuO_2+S1|x2Nb>CvaF~2ImX={0pO=xOAUD_@$md!3D zT-@es{V9CmqsrSakb&h%ye~oC4oD}NO0ln0k$t7?ld-Q<`2ru)z7jL9Fjd`#*0y!r zyi$8!h{$~Htql!LnPtBEaAQ}~0;|8GGkO;5N@c*Pj$2oPb+)9ePBN}g%-pB&o&OnG z`^v37_g-)M^zE9gA%AF*ziw51cZfxr8(JEJBx_&0e@#JK!L_UBlC=fGTTyKM2C%|; z(WLAweMW-qD~oXB6x&Y^MaIR9TvmcfLA7lCN^BU7nvF&h&W%=MPO2d_lbijubC~@t z2HD)@70iJI(|_hMYiTr^`88|V4DCJJJ!oeLuSgX;H*UQs zDDEO3fRLce(EQd$!}0l@p4f>Q6Z>%{Z(6^hX;UUIQODWUfX`%2J-h3!>*~dN3;Cwv zJpR9T?q>3R*QC9mb+qUyd--X+e|-oy^SRp|7H=e+c9ce?$FP{aO*tS(1j1n?SrLoH z_4W8U_QI)GbA>$a_ivIOW8YwJgHz|BIoBnl91qYWJtl07fA5mN2SJz17oBK-a(%JYZ$`nr(n zcrhrG-oq7mklP8)WcU3jRsv&xe{XMpf3L^o@_5{C4<6jSZrzqG>(*_q>s+{~vvbiR zvQHp10bnLRDCfC+sI51%Z-w)wHk@A*Tp-8#<6=Ar7Q)9f85vL*GO#n*dM#k~>dmqf zk>ZWDftfzU017>h?#@Y___yKCJa=x6$5a_!hzDD%oQ;D`k=miYkj?HiW_elxp-OrW zm*7DtfR<>~g(|fZ1r<^7d5A0x0o8L!pK-?_02F}R?Qpw_WE%h_J#g9?j|Z5;C8+Ky z^OaOD>>Q~o^;8uU7kZsZmVBSvo?T?})%N1S`cjWSSyfdQ?y4^;^f=sk)ed`(J=0J^ z^??zJ0B|)P{8)OwD`>E2WnbXxVDHzjZr-|H+ZtTHe0c~nv;Vek9o=7^lLsN2qA5w| zPeVbS$_v!R)&4X4_Vjv}%dI%}TrtPq^lZ4CRF4~*_B))_ZHqextIG-ko{~4{QpFMe<0^lmhmNa> zd1kxpc5%6RAVZ?3JuW8(%puAJ;I6rxwgu6_o~84Hg}IKr;()5KKFQTs?qp6cnmeP- z3pXrVcV$jxW@ohgwaTKQ(w2HaXp#PbeSH5@E`{QcBi;UthFDk7fqh@-xclpvjO)eE zWZ`fF2v8)whimX47{LLhgqQ8J8k0Hyp3|-7bec@gQfG>?smo3+kuFa{jJ2~y~OMk|tcn}O=Cu^Ds1(wV)wz>Q4Eap_B z5x{6S=4M;K0GV@o#|TKooZuVF=nrlf%UCp~O|Q(WYG2go%R`69>2cZd;7IqvjZLO% zQ%8d_yE_u?{Z(mWsMuR5=fISq4p*=zP|#TcP!CCmfgMLZp_s~a3^Op3+i0xOX*xFT zPL+LDgcwT^6AB&6kSQ6RoiJwLVs;!XgnOSr%XlMFbJH@u9bs9bL6b%Vg0O@S=o2Sq?D*M;8A1_e;A(dKY_trKr!Ou^jB7xO zYnYps;g)G)7Mqj9kWD-C477|;EjfIDESy29{IZG)qkmjt#x9;o%p?gaQH#siaTIh^ zRv<&!dl~V)*Md5vdf^{#9~8Wao^sSNvgf6C1`{r*K#DX!uD zsSXrx7ZUsXB@Zs;?e19njQf{i4X$8sDDxvE`NL++WWSZ?Pj1KK3GHEQ#>KpU zF3iMc9n1d3)ZR?Q(y5e*T>8l~{wzcz@cXr;on{Yq5{_cgEUh zJ--Im@cy*_cz+@B{ru?1rM#W?->myn-Vv_FhuCjn9u!5>=cT14>9iUGipEqWxaBAS zj6<*W2mq`bJ(7dTred^cy;^VB8@6}@+OP$a&+i=Q?_7JA^)u;X>7TK&j2*%2udlk$ zF~8y7`x+@19buhx7v3*;r}ZecRtV$$1HbQIXb`U8x)%81FEELhLki?YZRG3%93h-< zQuk#CKUI_wl6iR+B{nh?xWnhO=lXoPc)2e(x3DlbmqNF|53gY+{`*I~*Nf3;MMX4P zf&boAQ4ysdl8Gn{=v&+um`+H@l*+^{5Zo2WD=Uu?kTgvnAEBB?$T_GC#ib1xT;s|p zeM2d0m_IhswPH}Vb>mm~{e9&1@f0ZaA5cIhY*8mXP7qW}2zhFSN>(a-sb04;%M!P4 zBtDhNAti)b93(V!1%o@_r$V}~D8o5#q{QmUFDe?(^*KE5Q2Ah8?8@?;md4k_NAr!a*0RsBzqLD0+()xqkFh?L-<#cmS}X`?{N~o#p^+*(}?1d1~0M^ z3#wD82wbf|pFv7b*d+uQ=^08m>jOh6YmCbDy^cSa`f+SAmHUU_Tlfy7Kst2E^~sGS ziOJ|;g20p9mr~qbcEZ<;2@91*f%Wn5$C{V-$hwvTR^E8O5 z4gz&zJRkl&b}v{V4<3;t)O_h025N!v>?}gZ8SR;uktS%hy3y2RrqeCyAy?KI>$S>J zSH2WX(j{$6>~)6gqaOKEx(&YsblMe({gfMzjz;YOdG=fzSizc;onA*Sf;|Cm0CRPk@vs`rf4t;3Kw{K2yJGpQ+yz?Y1>_8~cs>jQz%zefwCVaKRSAF;22gF`<*doDMkY0-ipUC~H#Zj`*O%*cIlugo8f`M3-5D|~PIs3y-WIs>5Q z$$Kl~RA%WXNo9Qbg@xlbpTkq2P#Ihm8{5=(i#rQ_VW-(v?ky}Us&$#O3Kc5D*RUQR z!3FFvICTrT?PVcQ04>t@a6doG2CCa8$61eJubxL)8v=r>er0+aJ1o90--pv8POoUO z=B8Jt@hOu1@c!Qq{6R3Cy3Qb64h-A?ER`MR=c_B)p19f1cC`N0-T&>}mR@ted7ru8 z+|-@h+^d{~rn~p$?!#wam3RNE?O*u97rxN`s=U`r@CPg%G=!=W(~;#$M-=C=B*Fd7 z<5GVh;qXcE-|=qz{?sZPyR=V|;_u3I1kjA2m$u=1(zii?tf&cS(rHcTet>=Q#9JID zhaO)Sa~kQ$kp?7{u=}LfAcZQfr6j8a)pY5ArO@lnr{Gu>H(AJO_$WSl=zd?vjSCmv z*x`F=SFo(OxGW^E;db#c%)%-Zbn5`5N|N{RxKtUJ_omy|pWe>ncz2>nXazgz`dXw4 zo0KD^QWMH12&fWP0(l!Ku#ngRvH_Fcq_^?tZluCfvcZW0MoomUU*&NH36Z`k6hKp; z#O?LDN&>6q=Xt$(^H;H{yn=TErD^ku0`C;$b+irsXOXklUG$%WZ8VlrX(tx)u~bKc zW?rX)dtO0CF+c(6FwTtK0{}bNZF-a5L|z8 zhT!gX(hxdmY(?|ZO8MA$&7PYuL#O~Pk8Km^j8HT=V?45rI2W=$y1BC6;HJHT+Pob`<$R-7H!^2Z@JxG?!^=BS1ws{WxJzh zcvE}(rr{cSzD~PGrotEG8k64e4|wlNekXiR>FZGLrJM`66SIUY`COn$o-s5{pNn|= zdN{3}@*SkjXcPKC51J$TBzli|jMoc@zlg1>!R2bGs%`G5tn6qO3i>YDw|33G%X)oF zFL`+L=7%q#B>GwRq4|iv3(q>C!hgJft#UW^j?&xLd)dAkC#lOAMt%8 z3Gy)$|A5PZ=`Q9xBp(~uyLdA)-9=Cey2F5nO8MmQYa8(oyzS!C+Ul_pM|j&MXKA}^ zW*Zy$8Sapu zQ;(TA^5J8vN2~Z6e-52Ti1}q zTk3r|-ki_H|H3y4a#FKRpSja$&nZawlkmWlF}i5%?0OVv^z3@bB)-kAKNO&4mH*)L z;=?!KIDe=5qdLwKEZ62WKVQ?wyrTIjhkTL;s%@ijgN*;M+6 zv>vV&LXZsYG#`Aqlar_%3C}{J(#6P8%SVARve`sU@>dLiEd<7B{uuO)9@Xm&Lb8P; zhZ7=Hz#G0goL0OnV{>sDd%H6!d}^0)Q#gtI-&afPLC2rj6-`e<20AT6#^KwhAIosN za(5Uf)z$?3EBcP4LmCe-{#ZRdR?r3rhRUM#hU<$rXDlmD3x|c$@Tncb<>91GDi@2= zH>LH^DwM-Kk{RcvrKTh&>9n$4iY{1ZB0H3U&X=D12E1WQxG7YVQkhYiQWHuEg#WZWt2;`_5kT7yrq zFGDW*PBZ4E2n=)eOkf-oNuUU35P;kca)c@Cb$hZ5Brg?cv?c`(DI5%#W?{PEQ`L=w zV{6uq4K#&HY{C3!M|)FVReo7%@~yX~-n@PL9cj1Uo>W?EG*Gb{gNfdSK`tz22DX4> zVy%xlvdo!O=rub%1yuq|)~T2Z1fkoaXV{rURgz`Z-k<U5$-(b4J8nY268M+CD?OyS`||@cOR!r{xO=FPk6gT-7*~9jRTrY{lSUMV~9^>?#hnWJT&5hXb3=pLg3{ z&0t-7q{dO=%*-F@ZeN=8+1u0wj@o=%Wm&G*VK9{S*AuOVWT{xSiroud2tfq&{h_^ePa+{W|v29Cd~7{mkU?eO+m}<;A#2eAr)OLELuxx-A~`>m8PSo8KO8x%P~u zhbe#bB=fQZPyoB4nOUgSWTRTErmAj;pi+`N3w5YYLduGsNJb$ADXDt`v|7!$oc5~} z;ax%e+zT4esx{h`bM~3Oax|J*;C4A3`Fgiguh(Z5I8u{yi$h$|Hpz(-u5cje3I~+b zS;87{XkDg&*7eF@kGIL~=`2}$jd5{$`@;0@tQ(fCT;TRZeV4gGN`Lpa^IqILU^Wb9 z8ZW=NVJx!cvg$^Q@&3qI13$MqsRVE3_3Xx@=~h&$YzS)Botna{oKfOz7G;KWnD_A> znZXMPu!P>v3@WY+t5?owkW<6$kn7T0^#-m2YfLm&Jz2O($slEt*56M%!S%NL>g0>u zZ4s-t$TU1W((kTug0Yi^H~-OjlzVzuM8+x*%;{i&4(~W;Sj=?TMPL zWTwp2XQaz4NTb%Xc&a}con#0CeqNO1UxwS_=K*?0j}~S-iR%?;G%ikadtfGKRz z@*e^oWH!f@>+Y$}9tv59#`otu`Nf=XwRSGf|H`6~ALfKT-Ptt-(o4P7qdk4qiw}ld zNq&g&A*Pe-1jTp()5&!MVlkN^T(`V4W4hKxKr#irEpD7L) zmtL=+L;v{EN5^}5dU`&0!IoJYBOQy);@gN_wJniiAGiO+=FkBvINpPxCSnl7d!TH2twI8nLoy5aAs1Aj z7F24A#fe>+hyanIHx#(^W__kE*;X8iV?WT$#19Bhe&!%-Kg)4pG({YDIn$b(SB-`j zyBo5C1(m^{{N(OxV;`q{7uIiHu(q(M%$8FV+){rL&Cx<>4gUJSK_2?`N0o;pY33QA zd}wJ6{!9jI@Q)H+LCnfW8VZOfAP?b8%E>hg2_Wp*FQ4Fg0wWx1i99hT(=_|CN4AoKqj za>H5V0=ZvFj1AA^_%TL2OFD>Fo&EASaho9@{Eu+$dnYIN99Fn?-^9g{r;dradIn$w?@H!D!#($Gx$}7%&%UV&9A;0ZY6#|c!O5g`e?bqHGSom|1EO;9mmBAL6fL8Tf~^_oqF%$P4ZBZcOQ1c4dC0EZjH z!m5^soB6XV~At2EubE%`AT=xuh^vwFGtU$}^=wFvD7_3|E33V=O7|gD( zS#{pB-of%7XH|44(2~_q*Dw^=OzNnr#3SpYu1v?m?zW}LpS?{}kY8I*>ZsFTEaB>C zoUi=<`e=d{8)JiK;A{Ug`sfVK#`Vz;;cWk+K3XK5WaaDtct{`3My)32e@-9uaAv44 zI0t=HvCe1%vO@ZozAjHgfv3G_&6P1`xcj`yks`OJ!MoG>kMwtbJ?{s%^qY)>27{uI z+H5h6RHk|V3wqt0d~!~`F3u{_{Y4o1wmJ01Iv%YVOk{tW&*hw_Ke&T&2t zKtAbyCS#oc$~M|hME7%_P4|mIigO&;zhMS>I13h8NTS&9M5Y%(7*rKmWrxw>6rdj7gBRZLSMt<~|zDIA{Jo z9Ov&(uKD6aiTu4q{123K5(xA33N{H$E!V7c!Gri+{2e%T-+=?jbl-thpyzQV2W0=$ zp;fXUonvghkKe_s!9+L^hD^p#r{&c`fsAz{6y+e-36=8>m_QL5CNR2PZnB-b<@$02 z=IJ+EDBOjjF)_c)nqf{Ua#Z_Aa!ah4megWRS;mx!8 zsL5{Aed^Skl-35_FpM481}Rj#0Mx+L3#XywL9Hf#lA1@ND5#4GR-eF|zd-jVlkXS~KkIsGa(KWeKip9~{1M2zPyYFzgra=pAG2w=Z0M zV4^NO=D&Gx^}!YXvH|g-ZN>P8Z^6yO;*aOwG~9k^v1qL1V~xNwcnn_zr|v&s8DFXU z8vKug03#VPNW)xjiEc8--4m!hKZ%Xo^NE>{-bwwFaeaeEzbM>C!D8w^q|+C1cUL zHBQ&ErfBSM_p+$?)eeWXtGc|lwj3X?tu3!=b6dNsSxU{)nwq86ZMF3+@?WiW4b9?L z+FPo6+9Q#6q+hMARRj|v{ZcB%dxc@hp|~=tPLm-pM&)UkAXiFZ0#y%Y+#awLX-4IO zzj9usjDBO%^SIL(eukc6%37-(w$f5>USFHr@5s&anDQJ0UBYl$db82vJtcN@rKY!} z=-dv0_4M+%_k=a&F$ik4wjfx5Pm51rheNy-H{-89v$y+^rLx`DA79g-oqbIKAp$q! zFQW%y!jT2BU~mGGi2$&G*qY-A%Y$} zhNLE)F^-U8T_U(syaBgUgg8RC^lNN~+_Uvj=tyo8KOguywlgmZx@SSz|CqS{JK-Aq zGQQ4nsgvUI8ggPo7{7e!rPZ0skDcnqb=bki zP|!U$XKd9u^Fv-6MdpXZHRE1_+EX8n|Ou#n?^1h`9A^+4n;9HrMa<6R`6*@-qcGlh)Ic1S!(y>GEcrzq z50N${{4b8T>Ni`+@R1qv#whe(Z|exIY_6TC2`mbhb!BUIUut^15la?VG&Y6VJsTgn zYVh*5s@3h$mBC`y`di#BQ&+EAeO_m4_aG2_dTgF30+Z^vSHj-tJccR_b_=%1y6Ok#ALpm=wbipWQ3r%F(EkstQx_X>X;s zB38;hBPS!1b^ZGz4bVW=@#P%yXa1{Dce+)%0}dPyhL0Q;d%( zb1kk{CO;zo`{9#5d-4<3fcS=dZd1S!wI@imj0*0l)k}C0HASrDHcVa{DHuT4XFof9 z>#f7MwfBxYdhb;D;co1Ji~c1!IX6L0j(h|f(v7!Z2fJF~AzZec!CPZACIwoK5oc7z zu(Ic=1OjyCF=`wx+mH~R#@i@DjicA6K#JaAAS!}-ErogMT|tKlUmn{()``DkL#JL# z5j^}?FUeqWI;NxiLCRv3i&%1%WkN=3H_8GEx&)UHHW&izN5_AC{(+%AFaA@|p86*~ zYZWhJf2VuJXkaSzM>FKQrARePb%?A>R&6N?i)a6FXnX!b+<$GD4xNvGJ9^BhH^_1G zv8tv9A?U#GioZO59DU*iKl~xy#s=OfdaJ*m*S?Q` z#H)!9yXo^1aN$de?)RXg`#mV@e)coyey{|d!Ixy+!_IXN`jK}6$^W0QE+9z|l#20v&QD)DPoA6DT>Wg;T>b3q zbH&&3W_$_PkzB$1E{^WOG(SIgAKIP+G+eCbue&wdr>N0qQ7Iy#eg z$C%JuOnagyffdL2$S2E(pF1}`e0Zv&i2cMKBm8kb{Jq#WGx=~pdR28r!h6encw2Nc z!$dwDd-s`4beaKA%w)j-tK9UFvrp8Sx#@>a-RvohaZ`Ra_To=v8Cco-fG;UBuwCRb zaNe9UFwaXaX36Y0q>wJF)d)-l9h4obP;^R-Q|LA4J2LNK$t?=+*?8)?6rqX#B31sA zCpZ}@(m-e?LfrsyJ%6k^yqR+E+ zynXF2!936$Nz+iQn?|G6dJ(l=kH@Qb;T@^Drq6}325Z8fGv%g^ zv0SsmaAN~zi@$HU(cmyo{g$7DuR||;j*U;}!-bJTaJ^x*LF@Hah4Jfnsn2Oh{kj_O zGmoTVvbg6{c)1Rf7nsE{^>;0v5^)!e=Ta~UMs@|H(RdWxy|5B)=kaV97oIdgThHsR3EZEl79aagnkLfB$4YLPuBUN29MxPng(UV`t7yZG$j zm-y`9%})x?HPUFb4wJ)du~3xCzwgP)HZlFwJ9*wcE#g~fV6C@XEop%!* z97#=BD|sjuJ}@dL6ISh&SMAhaW20)JVG8qREMaVjH8fy(IT1c3HM0+dRgevuBiX6^ zefh?mu@dr@WgqZWn7Jm(?qxlEMNYjkbA^O_WixZ1N4<2C-7Z8S6%3#|nwhQ!R#B|- z1obXNiZgb(3<0k;pbsG09>x+ZdDv?C!u}0UKDpsws?Cgl#v<|OVusnq1|}z`ek0pZ ze2^yG#a1i#)Te9JtfJUZ6$&_lh(U+b6SO!?VXw*IvzeKZ?lASH*;c|jCMTz!!0Rm5 zL*h%~ONVS`&E_Yc+$`U-Uit#NoxMGCoM1N5zyf4E4hSv_CHUY^tPfN7hiztMoa}9@ zMa&R?jz#!qi*@So}njC(!$y5;3T7r(CaF^9G^(Nhc z87A%FoR7TEuDx zIujNGE#~+N%$RzmZ)|=vN7PhzaK_vuPRh*bpgldkUYa8s70#N2nJ=I4+I$WPDTrdF z)sO^%Xi}0+E2z$`K3t|=t`o(JU>ghq{{7g7W5+f;ebY@h;gI+!4q~79xcE0r#bjWx z0s!CP_y=0h9aXCs?Nf(C?@;RldVELRjyH?X|BM}8B5LuUk`x~oOKMcs8dwrL%*Sbq zX2-{wP{3~XfLPKuHhkk78@_kzt+(R1xF5%GMEp1Lk7!1t>?0}#strxH!^U))#|NZ+ z@HdY8qmRyuCs$3UR{d?kf;Ga;@_wGaR?a||eW?>X1wH{DC6+mEaOo+JjBc@D0e%BF zp4yHZN#|*oiiKlr5`xeOW6=>4>R5F+Nf2~}sD>ics8!^x)d@_u5>zVfcrt3W6Fn(O zjBJ>M)iTxtYBf)~SgPp_g#bd0p-5c~1fk09aTmDV1*yqaMLnVlYTN^*gjDN|<^UP- zWPhPiaq&(YKr_QZ56@Exu-l(nv1+2aG0D&o8a!|3ilXj9r*nPp?He}jU%c>(+m=ma zSFdcm{>R(5|73Te!{NEEqKr*$cznaP2P1_gH9J>bxhK;sPDKK*PV606`RL}&k1TU` z7!8|$e8aUbUNUv-qW99j(?jXLAi$7xk{xD~kO##OB&@n%RY7)oYI2fRlPnPErog)A zA#^070mHOCH$NZ1Uy)y&Uj%tzH|RCVM&;eLmQdKD;pt$p7gT_Ibzi7IWGi>X!Rs8LguJ09hdj9O5swJHi+(J-}!YG?w3 zWz**7aVoM|EE}1%D6E{j#|QpbCC?)QM_ctX5{Z232Z7trE6?7Ls(@q?IX&)!vd3 zah%SgZX9&FB|HhuI=DDm8t=w|YqiK$p4LC1Q?%;L*Mkn!DxG>le(p$$f-X*JkE2mL zO?j2SytKH`=cTB%gh?mM{%iwDrZU?UDL^Mbq|sVjUKhb+_)S_)7F;g8{=$})3p?6& zw6yGKs~ryn#%rs`0)esW3*5QgEe%;U{g>Wi4_R|6=PvG-xKU6>yEQ%WPP+*Z@MZQI=K|w5%kH-nrfrbB)8LUbGzuI$VjI#=X6y(i&8#0@rWX$ znF3n~CdFg6m}MH^@e+|V;S(E2COVvXO@$@Z#z1gEXQ3lId&A&Fx62Xr6;~Om0>hn! zuHSj9gV{z)W@)J_-(bF>p=H1yF&UhBS@s;gHCp3tGG|sen)^ewCZpS)Ww+^V%{9(? z%eI2d%oKg0AuBICBW<3c$iR7=3yXvjb{Mpf3%aA(It`=x!Ibn&LDMcTC9sk)*hQkR z%MdUa0(kxnBE%j4;;tn4a?_{+-!^uu}IUxW?p2OLpdcQiLkpTsJPQRIN%DxOY#dW1~qFSHdmy z6!OBaQu9n%ek~_96FoDo9F5uuwyUJLs8ELPvhfUxd1;UeDQ=kp#G%4M0d%3+Y$A~a zJ)WRGV3yrYJOV{j}))~z2KTVF9+C;rz};vY2H(aX^)B#*uw4hI6^aNwI1?7${> zu3mZBC99iORZq3BuC@yoh|jRYQ^#_y4dKq}-adi{@9Uj~jlZcBKc0anS+ zb0#>zC<8D#iK*3^Q5|YD(~nNLXlA=s8}qr0MhQUD0Ml@CJOyB8{C_ZD3Ye5((b#bE zg}(dm?|Y%|w%hnz>fuW1vns#P4e4NpTyR4%_@M?Gp$&RrJ}ic1um&!G9dH?;Kwfsq zwk;QITrsh1Y|-%ifu4@G=7zfJDt~FA*X_*CGMkc_o|J}|TqK`3aA~w=OVI2!g~CJ; zwPs5wtnzsIKiU9&sq)gd0k>D94Fp44Q^4Z|3pKb@;h@WGaa%%Rm&qOGcT!n`bhV%N zG`Yn$jto z<_m%?8en3FzsV*%H1+#|Y~iV^`@bz@Pu<^H*=!TOfK3BW3E5aQlr4N4Z`3{{*jg&Y z>(#wo<)f|1<0V~v;)`3Fx{Diq`1+P&Z)0KaTfe&c>R(-R4gZb*wtSOG_&wt91=IRf zIjxm;!`gx2){Ob94EF1<9A0e6Te&jNvS9QEbKlT)f^glUPVsAN^0F2W?-6zl4rFC@ zFWjZMa&$pf-XVNlFl}14a=l4-eQ8U^z*>uSM{9-EvT8o34>4U)18HCZE2Kq}bF$1C zsYz<4swg&a?cJfM?gmoh3BSvs4Fp}U$!fg#{x7%|Uv9O0!E8g#FAg4LX0FZu5nCrG z#n;6tv(09{@G==oa+P#awSzs&X}#{KIfL5?%pM-MMXEBH3LFJtOV}IsYQ0*E_G+8W zEPUBwwOURMWBtB;EwTTn+RSV#eLr=J+4fob_U^WO?rFPwpRA`zC)GywEF^=O>=CIt zB-;VTAA$1SE%YQGUM@uV6P{)l$M4^=50~RQ`SwgF-&Etst4ou*Qd;Q55&`7 zA%d5SyG}ig$+7Xb?2EmE_&M?RnJ-{~ozh8lJ)fsMD2GNEfMu{5u7X?Oes~m~grCFf z@Gff5j2;ZZf+io9pcidu!Bo`3DR>Xwgx|xj;TP~Md>f9!A=nQOz#Xs`cEiXiCXAIsy_t!x zn%tl8W0YY#lxcC?ZnK$H%V{@#@PIWnRoAVfUtj(Re_c;Ku0QqpkI+Sa$oErqA?mx7 ze(6$A-SCm`F!gw9s*bIp{!_8tshoiOVy{AtANAM%kKRT6b84#YWc*>*F8%1YNO&qg zvvx@5bxrh;1G=M@%v7QY5DL(~;B<%E>-1s0gPZI0>|&ewep*QpxYe(`RJ?d{l2x*w z(su!~?WJ^&r}qu6I!y&GeM)jp zF`8WSSWI(O3YW`}k*=VdKvZS9S=hNHY1Of-_dV2lQFryivLgrPt(*AFS2E5QAM5sg zYDakq{-$@!H@5xTJxgxvDefv;bwSeT@R85=Hdn80(HkVCrZLV?ES+;pH4au2YVM?EumRV$=_3pu2@iz7XvZOwv zk@5-HA)_2Hj!*-D@3F%a(<3L!o>^WrB}jH8Q^rsm&Ll0i9yzl3$dSd&-Y1sh3w^h7 z**5k(1FVprmmmtBSzgT7p@sxS(5NR8Mlp>r@e!m&c~~?yeBqhCr=IG2hD(c*1jEu3 z_#3vL&c!@=-mbvg$hVi&XQa<90Qi=CF0yBSUXV@u%P;*M9!0snuv|AtP8^{!_Awah zIqC)_0zoJKeWTlL&2hVPtjfPQ>CPc2wj8&3H1?N#9#O(>_9u=R*H4&n(}wBv1ZX(q z@L?7iCJt~U0ZtAnJZnzWIIi?l>V#|xB@;mG(;k;LsXpYkxGcxxf3ZI~EGcH=ReSU{ zizDGrzV;DJ!oApkwpedDK779TRqSt*`@pq$3;y)nGOjKf}B65y&74kfv5K6~a7Z$asqHwrQN$B?5g*0jU{~?lcnx2zi1*Z7>`#85Mim zE8ROU8i`u+jBbDH&)OrpXm6ywJteh{%7p=g2pq<}_|Q4$**Hx6e-ZxvGzSm0o!>@d z#=Tf5q;OxHmV6$DpbT~;S+c4xhUy2lwt(r&pAPn~hmT zX=!gdQ9_(@Mxc)yL4g{`?}^y4j|dsezgO@}f3wdgbMu z#|T@B8)4O;*6U>>ELE_^TJd);vcppYEo@U_e=Q-k6Cl}ipxqLx%q!nS3tGKg2OAaT z6KlI){aEDF&24b+-{zBj7@=WK;Ge^v9eW1rE}tO#xglzI0LJ#owxX-~rBO zDJJ_7W(B7D9sf*N*e5C9Ch3QKto71ma1yQoVYc5T$y{u483OmO+}^euHL@&$PK>a- zAw!lW45{2V$xTt5*@nFVOE|!|3C|et2Y;5S_qbeM_l5?O}KhNU*TvTqz{?J|SJDy|v7nhO46~!a$ zI_ZR9f)r>trqfc$P^WBEmBA^gQ1%2tSIOX%Bh+UGIAyGQ1(P%X!xoLkb#j5G!PMGe zYS1i=^H~0tc!aHyPOyJK3PhCg zC~rw3B7d~}nwg1+m2xeOQI3`wACR1XM0mr2OBQMxOdYMJ2F-#7_K$=Cp?HW*V84(9 zDNuDZMXM2FkK>a{I43I=1gu#`>rKpa%_PM$;)~Sta9J>IvUc(7I@o+xIlw;JflF z_PecHONXiaAfWgz`~0?+eXzsQ5LHvv;N63RglZ(a$6u->@7vHF0YK zaAPKcrYifAEVvriUxzgfmS$&pN`q!m1ADhMoYw5hOAEIGig&WVN+*Py&t=TQ@=cpc zhsaz4D1IQNOD{qO7@;Cs3dn?HM3y8A(*j2cKg|K5H@XW5Ju_D0ZyGbh6%O23QQ97| zRitOz9Tsa*`MkPnRk^dGDlMfd!(cL*w$n53Wba8QgwMvt$^+2}-qRq@uQwd9XrH;# zcSFM-uh2PEx@l7>AFH%p{H1h4I&#jqubLU+PNO3wy)c-Y<;?2K&q~YBU$n!Nla&v^ z#CwHQ>4b14uNPSoEu1Z2jT5LV49p?@R} zTFPP{NGF6}gMp-5T4TKq1O|*-r?=1cGnril92i#dd&ue0BtM#5k9+g8jCQNJFc_#Q z^0o;BYP~5lGd-A9SyWK0si1dT!aAfAf*MjFBbugBGx-GZN<{)0-@ReFm+Ol)T5YGH zxt3j~DYfKfraLOjDZYz|cd>s+&k5h+vX}0tRxOY&O+1JYYJa@Ew)ge*wTpy1?#ez` zcyIPsLA1u{R9y7bf_@sFo`dqUdV21115^>ZRv#YG++M# znJ1Ze4P!ZP27n9yoRH=ud`}+a8UEnRIkrgR$|>1I-1o3F8uk05QGd40mYtPr%NCw) zC@pQEAHO{($4)=|e11-tC%q=@f*i_ix0o5jB;AMRwohlVGC8-MC_I-o?MxPZHLiQ| z4C$tnjI6Y3L)c}q8w5j!ECo!n=x8Jw3yQhPK(tC_6zAD#0^pF7ouM2b&;%Y1Bt-6EY3 zs4iJZ9?`UH$cJ+dvSHlA8EB5NgVbqu04PmJHPQ*`7^Fb3=a6e~al$i}o{K!JnKl@> zE>j-yz1R~ZNjxl-;DGcR2)ZTWVR|h<@tF9WA}10?#H?I&lGzwfw@wt$NUd|2?r6LS zOYVH&fx0?=pG3Vv&0|-&R^io(9fcQOw8ysbLUy92zki}M65-d+t;=Nn())R0eP_~T zY%dkEM=KY}yivT4EtgIR^FRZE$7!gYa-FcaZo<>z=Fm-y)4GWzV1E|x2yguwo4_rR z^X0Y9Vk@N+LjNbubJ@3N@Z4IO|6fYQ(g{FFqbLVnS^znBSK!mRNrdX0$#f)>TSYbU zsg=vox!(1=jd_;nlA-pWd1_OWyPIo!$$~=s93PZUu$7QPb&C{!j;Khj^n`(Ic_yZr zyH;lI(R%z3p+aWvfm#9B+0Xg07;m>E@^-0<%?A)tKu!8xGQkvc^0=0)JjfQ3B}2hG zutqI>@15Q<%9k^4;L!8k*SJ$idE&}CdPlQ(zjQ+SHfTwSG7m0}!?MS*#;n?)yp`Vbjjo zqZCGWu6tC^i#Z8ij%IKKpZ(g_Fk6mo=SKj=7e3NhUO>GvmKWGP(h2qo2t;2f#O}<+ zAQbsaR&QsEVCy(UgdC#0{Up!;C1u}CKIe(6=d z?;`TP%aqIUo<}E--kuL8i+{;2DGYETp)_G!xhCWa3T%7irkkSC$7^;V)pwOdQG8Q6 zAw7jc(lLNjmn%H-@;Hwa*%s-9&>E9}3F&JN`Il3`|33M*bx!$L+9Hn~(s=zV*-p3; zC=!+wW~$l@!hlh4RnKZ>4WT8u+3GG{=mJ zdJ%dOiSxWRwTuy!25CmUk&>Mm7urp-l4m+k5!}VM;sU#$%UWa`dIoY}!BGn@f8N7G zBMn*&QH-{i7ZVU!Hs zq*agw7ExG8hS>jyy>|hVx~d=lU+?qz%w=b1?=ySfVV9X@2G|?yWp-hKMG#h4U=f7n zl8Z1RA{izX6&eZ^CK)Oj79}MWCSSusL!&~aM8iU(q9Q{hLqj7a!^EBcbI!~x%hl{# zzu)uxpWi;ud(ZiN&Y5%jywCe~&Uqh=d6rDFZQEtLaufG%Ix74X(Z--(TW@-f9NaW^ zYg1dB#<6#Nk{f?T-(TeZ@X6zPnB(!D`y@BM+r0Z|&-;__*2ml9FUEf;R@x8Yee5%*Z5U*UN>N~ z5*DL>MP2&J`PC)0?p*3pJl@mBKjSaTN-XyiJ?6MkJf4z~#7bM%v%JTf=zotl)&Kad z>VxqYt=Sh^W3lS}!4J)xY0W-@#6QuqFaE3ejtk}2RN@ly5qGr8pTGFsFPgH1fSSky)}{ZdPIdbIL}Poe2~rc!xF<;8 zefb%E_ocDxTWNc_OXE8Mb)#fzn|a#i#2A=&3pip;#!CB!vFw|7y<>H6?^vbQ3arGv znTgHu`p9eVy_vT!xIT4nrs>;K)>&i8U{qoqR=aG_izxc4P-FJ)-9?;udj`qW&JPkq zGw(E_xh_RC)z?l@&C~k!u~U8P*~f=8?xj7>J&E~EKz)!8Y5Yxl483BUXlrs@`mPg5 zW}n3bVYIx(7WMs8w$nUUIJ#wMqfb4!!~JNgoK-UEKVa+6?PKfz$oXvjW{j_w#pkmF zOTrE;i_bI!YrK0yGW*(r^}2{;_AvzOJa5XR_h1K3wmD%3UVdIXFah4C9 z8iMurjc4(9va!I=t@%iku`um*APOHnZyt2T$QS0P#6E`MyEZn%Qey~Z) z;2!HUVx@hBA0*Pmq@912{7&H1~Ac z(nq=1nmsE z$zEN3we^A9OZCO8x_w`g+W&=~l-F3{4@go?GWb}?%m23CVocC|^_G{)TH((XD(-%MKHz9rWzoc=TE_m5AlSrF(sz+M09 z`li!AIB}vYwVmrjy^8>+t#6vPZj}XAAjyvn?x+OrXxeDf-s9S*MuI*z=nHa&z}ZG{ z)29F79&1oi4mcB^P1?Jyi=NSlUIsBJWu{KllbSbLCO@;DbIu#hJDxFy;pBJjG%|&j zH_jZp4La@g^@_9jO5-UzrYY|csZ~8m8Rg9On6!7X3u+unGKEJjzSxwRVccd*P zyY=#kvbJJl;OaA@jV&t}QqKJM#1RthzfF`iKPg9@IUXj>;8JZ{)%ll7&s--x<4azU zAIv|8oyd&q1G2;lC1toX$JM0$TWhG!f`60PKX5Lifc*c*zJbKE;upnVjQ@rVu4?n> zQ@`nEBW7a%POibXg^2X7JCr1BIC?9V-l^FqLEClAJEdf1Dn(T1tc1xv18;wGFx)x7 zEL0@7Z$3PH%AlOWto}8PKWwS1sT$-?t2N6`0Xf87>icBqJ&uX{fwe5A8JS#>I%%G` zlGto$IGcj1@1M4es(EbRGHqkrJG)H3x#l?E@6$(&6Gw1A(7&*(abQ+@g(ceG3+QY9=#{;&(U@*9-nIeuUf}V zQxBD&>cg|yPic3Hx*z-hBmJnIHm+|!JU{b4byoTR`+ht-uC?~GeiS~H{WD!p{AIaI zqUuqKi7>1!+SmH1?*!Es94*k-``Hd-nb!tJgHDlfMZcsG(WTw%o>w?@o%R*KcYaZE zT6slLQMotA*QmyxgN`iCE7u=8v%Fs@(e5uuRDIvHdrVu~sdk6OVOxFM?vgVaUs3`k zk&;NTBH01=g*(u@Z&~B`1v+q%FUMP6R8&!(r!GE62ObFZE6>zjD9_VNxCkD3g$?S7 zQ!=dPqZ=;1S#}%H*>(Chg7JFq#09nyXm`ZfCE3ME7sSpq zE#!uyW`|mG(^y|2N=inIESq$uj=kzzp6_lyOPUf&)OhZ+^xhFIzngaU-Ca6MkBeUB z)>p|}X-Uzom+GC`JYx0lL-peERJmp^%!6m|g-N9+>P^tZ^VRE5)q9FEF6pI=y53Wi zQBk5sOQq>g4ujhwIoa-Bx^=$t&(SYUy-xM*r>*0roz_1?y%Kc|>RZ<-8hO6D&e0cL z*Qx$!N_kqJ`qcFw@7K82+SB@FsM$Hj@ITNmO*v2NSF)~v8ZAq#D-Hb_-qw7I{wQ-G zD{=Qqihd?0P8sWS^2l^1sW9R6=8H6)Mr*8N!(kV@d&qt^JJYY?;oI$44 z(cq0uUpk9S{U2%T1!?NUH3L3;?-`A?#@`sc{p`kCl&ht3t@R1xzkVny&s>Yt450iJ zg1E(UC~JXsyY84{E>Cfc*G}srXJs*ow(Xdi$Yor$JM1a5+Df&zTnG;h4@vo~PqO6n z3u4y!KFw?Amo7eg<{+e<1JlB1f58NQJ?C0ZAA9S0ZL+7W_nfldO+RyN&CZ)MS8@%5 zy}(MY+2592$i3irU108=b0Ie#CnB$~L;X!G_pP8ll~}V?pGvLS5-{sh4l)?=uqW-D zXHbS+XwO{+-b%}zw5u7fTza#=wyb#uhe9=;#5t5vO&R5-W&JXmMw#zmiA=IqC3tVL z(moo#=3T~SXUg_mxNJ+#DBIWrga2CDDSP1g+WpnOWvlr8@xgMLdXPNIXlkp^$yAo) z=UJ9*8D?SGV(BY+W=+&?V2loQIUPQqugq5(&J9&MJ%xiE1Ckp~&5_1}ocf&n0%ub+ zag45BM!A~%d1qd7QTz1C!Tj##=Ho$C)USHb$oZDvJ>in!Lz_of>YMM3%PfjjH#$no zY7%mzBYvaGGUsT^+sb;^#m!Q<-btorsgBf&xFcv;!8~(ysim@(eCf*zfBMLhPp?_{ zy_@ADJ-5h1a%0b9GOFj(Mt6zDn^e7ekQ_o>-GI_fIv(KaP#%w5K=MqC7TI0tpkGUherfsn1n=%1gS<1GS z>Z||sceO0$>a%t8>B#S&H^3hZx{T*Oe|E5cuiHND7%AbYuPLSSo1KOA>iMeuA1Tjn zA36V;s*1S-LWT8%Ylgh<%5X!VvU0Av>iakKJoTYr!*Bn_%10*6+`hc)s_Mp(zxmCw z@Kx{o$oARsIHTjo)l=rqqg4rd1`;#hePG)Fr$bqmJqz2OHPIYC(wnbln~S0Jy$AZw zXM=AAfb?3x$huGR3 zoAyIhq%@M3`brFvJ1>Of{uGYS7&mr8635@@uZsH3yhx*X`z6Ebn?_h_&^u3L7R|4& zcSLJ6y>P@g$^q3t`kDBRnx*l(hy;G?s+0I#5Usc5z@k;3T=3m{ulnG!1^33JqUR6N zEiM22XU|Vj8n>lKHIPk();qJsvitSo_BqkD=oDrb(~*;tQ;}01tcYk_bruiC@6+4K z6S%5Rn&cZtAkn0gHjSt~&2t(@Md#k}TIRA_?)#$?LPdX6J$E!T zWgMP2w0g|6no!RYy^<})2aWD~MB}9UESxOrgOdR|{K33Re<eb#4UrSVZx zZ*Gi9BIP6fu9-gk5>ICP=&4s#^y?}wsc)*O>8h?8GQ6~6uKaq{)LT~%8$P1>rrV~i zy>$Mk+Lm5EWZYwqjcT6$!LgrO*o%jg%=bAB4~tIWVe#pB=sX<{|G2p4*{iRXS~=P? zx4m69x9imueTF$cSbbf+Mu;KY)mB;}wpA+1_KQIXDMb5bXz>(yj^WP9|usl}6OFTZ-|<)fwz z?;Jlmv3I%ccx!3#@X8TmMh?5x;~h4jv*Q!t!Rv3BdgJ(uS9Ej^817A<@XnPJ_JqTt z`Ue+YxiFR(r=s|Ab)9;RAhrBtqVZeSue7AVXWLeJc2>IWP*sB~EVs)oxNUvUlGR>v zz8sRuF)LBfAZ;{q+78Xw%+Ovj(cDL$CQfI}To{lzfH7Yi5o#`&S<|EBFEA8zR$D=IRk=;16&1-QGfD+%l=`1Q+=-4*Vy0X*H`hm z7VDvGNt^2PnrkmT*>nAw=GM#XjF!2j7q<-`*XSS7UzOZ`^QAXjJZ_j;sBgklRp|@8 zb%8n7)suVrFIaR<_oXK*%zi>z;>XqJleqW!wm_)7%yK%43VpVXohL3U5_VZ`mp!3h zKsnkILp4x)fwP=w&m)dGI2>lE%%Ev3bh{H)%h_6-s=iQOGf?^m1}o=luV^`yl^XlP zmBCS6j1!l`=~{5s@6OIW zt4+HL9>l=#0DVnuZm@rD(CaB1jPXpWiSns|(eC7oAHn1uiGF<-bbh_G=qpkSa_4&8 z27%HXmtQ_}a;T-Eth#nkXr<@FWx3K?7ntLGOR5}AWmib&`1Y~WGBU^bD#MqIU+yzn zEY>PjLM7ESa%)>jLsdz>?eH|#R1c^OInx~3S)Mef!-=iMC+%LaQr|_4!(O1T57u`R zr;Eqqoa1&W?XgZ9>=NiQ@XUPZbe-8X$1N^btT*qhw%nn0gCfEHq5e6(9DO&S9*2-V z`Pdjvf;pHSZLBXy?ZMT)pN2*YDjOS&14l(Ubebm&}dX)w0gQ|pn4CJG2{)k`M?tvBk0=CP}%1m?ESgG;puVqHGciN*F~@1}M6ZY#L>X{aCWA(9xAC|fOJ$tZ<_+fsb9_1Z`mV&(m}pO;{$QgyWN3y& zkBQ&k6xF`)8nj&Q@~f*`!gGf%^jFQT$ZJe%2~TmCP8-@jwIZ*v=fzor1`cW~)%|}s zz0m1-`E?m=24M1_x)~F1l)-u8dw+9HUG-R0{Hge0$PuPZTi>?y9YORKNEe93~LZH2zp(9Iw5wl4K}^z5lm|DdgBj|z{E3|_Zt#Rhr4 zCe#z}SMk)-riIgBADuIaKq+TKdSyy;DMMaD`o2w^u0AX&cxtrtr1{(HN(*xe^X0Mjj*OhTz#PX<-*M@&*?rCMR_AQ( zOGC_fJgCa(udRIf;;;*Hl;!AOrks{H-DMm4nj(;zr-RSBYEqaKRT5ub9;`j>TTUI9lupctmWnyfy@L6MO&y))q}4)?N1vP4a)|s`l~7M} zTVqfxRX?;gR9sl#OB0V++PH5`NcjmiZ8Ngid^#cnD*6R0wc{3C39+?z+hM}fuPfYS zyZ{-3W#lTYU>R?~(E|B)S>U5HmrofoW@^KT{HBKTg2J+l5k(cjAv2qrW`x6`(V^;w zA#L^jatpj8MEOEZlP|rvxqfhENkvhn+hdg$_N(=Wq65cN1_Hwh@+-^BLpd2)j(lIa z=5O)ia+|uH2(#K|l=myKTq=~|b=&%u$27MR(u_o9x9cs;ZoP%sg*(mVUf>k1jwNQQ zN7B+#eFY+fgZ=fDt-(-GI~ENk{b#4pr@-*2Xp%9d=AtjZUM{W-He57jVrD|d@y(q( z_Xf}PMcD?qGV;s%w-t;EO}b=-TVv7n<3Eyq^Z0wR>jHD^aRVz6GC{VfEXtVJ)}AM} zmS-|;r&Z=jbJ#9ri(ngMa%qC&)V@(%Mwl@-32F~HDWx)~d2BEz+wCbG9IbFwG-?Y* zLhy)c&C=CNLT-`#^q$Xt%9}v@eu3_{CAKQVV_Mv=eA1+gA8|I7U6HwJ^0?)IYdZM$2XLLHRf(qrLJ`Sti%W6*5UKlHqJ(jOLuw4bF{@RP{p(^6NEn z7%M?Q6=*ML`kybck=1D~ATa{v0CQ2O)0L_8JtFz}#&vnPMunr{a8pw>!3Z?IX=u=A z&!bM4K|ohne<-KlSKz8q;h?_G#+5`vQ>~7h)%{GReP?PpH#J7J zpVn|gQ?xN$qZ)NPo0?AL(fA%T)`J3?ApP+h2d^BlLDhSr7a5!oxQN7`1;ihQVs3O|ezH#B?GCc|o7|^Jjm9Lwnf6y3? z22_E|>GCJ%n=4$I*biJ#uwu&aIUi^)pIz;&4GkFLSEBrplCtQqW|yT8G`b2(-Oh|0 zr*C+6c4cT9aFZv;?bnHhh z|0RX}(;BO+s+D(&?!Q1$@y4X;rtwLz~RmFH`QNSU`cMCB?V=^ z((+7)BhSwCIvcH_6Sp>org)Xh?Q|<=KbPB)=BgiBG`-EPQe!%*l*;rE@Od-y(qxhv zc4c`{h9jr6V%#XF<&%Po0u5cw9(B{4tbEH+Z+R?djz6Q=vZvG)_7itaMv2%(!7OnE zODtEdue93flq@Y-rB%AT&fNYE*YHN$ zvanXE{fWt0>KJWE7QOIZREGw7g8OZ-75jsj7}Ks>*JgUNp4cmF94}`Z<-`>2@ja zlu+Z=i9@YMr#I8ib2u`~OMPVpVijhHRd8v2lRwYv2zYbm7p8qIr>3LOQ(W!{msUEx z13i-C@(#9pV<1Dc(@7O3zWs zswk7ul_D?e*ak$p*Zsjt>$l>{A<6)du)B%{VH{R-2sT(`_qUmYeBtd#r4+LK$|MIER#4z99pw zY)5veNIZF>iVJ*YUMr_5%adih4dp#6^*pfPC=V*{OFO3FKuXyCT#a% zD?LqRsl4={?et`%djd{ZmKBitOequz+Uag*h9#p_j#~z2c_cGUse!h#y>_m{YO(Ab z%Ti;#F3a}jS%Z`#)9K0jy~}OqXNuDSU=Uz9J% z?Q)ykDxZ>@la1b==d~@DR~31jnGU6D z6Qa_y`L^4YVHes3wzJmCbUBr6S1P~l$n<+$dA72Oy(-TZ%eE>C94bLaMax$u{g;2f z{bSu(t$)X|j%f4VbH2_Gn=rp#z2ob-$I)qPH!Lo5#8?06ok`#9`3E?*zvFwm%h72{ z>htFrufE8~P1b}}6g2|pe-*w%^nB%8b@AgVQ1giO!gukgF~rmod?u_T`mV5=n4JW* z4xfS5#0==?RQ%U?OneLhyM>qrRq(Em-zU!%ed6c-Z5q|8fF}6|k>0#Eo+MPuUx+0j z{)qYrQ3Hc;_Qq51bU2BqJcmc!L{x1drnV6@p^j^K8;^<+QM0JCo+2h;Vyer8B@&Lm zE#dg95{@60aQrz5$B!j|OCxHX{ z$?L?dpb1Om1X=p|Ot3t}lp^0MWn?dWo!RPsV%9-oI(%2yr1QidR-59FSWgg3JQGvf ziK?|kO{nAl3HTAwbHa9_)>kf2M;*R%)o|B0af;Ol0;-6J$#)mC4Sa`SZ#?zx z-NMbxJ|~>{JN>QtzN^BiKFw@RC#+!k!lz%)^B7vJ z9etj4o*9g^(-~>qnGC{yOq(P3Jil^S0Q1?xr^?4-zrU@UV zNFKqVpLN`%>oO+LtvZvTj-L**tq;*H#dJuq3H?$W|GN~&|D1%qQXGFJ0Va>m*H4NW zs`II#=Gm6w_%W$W(&*y&OHvsBM-p`WC8>{pLt1dBKtXp3O9u`K56Z={0rd!7xE;^nMTVwp`8}1@Emph^VB;zOyHdLTttg?H!XeVpIS!E zY0pz>Nr80*Epjv68dN&S&9uwSOqH9dmYeC2rOcM4c$0ZNax*h}V3>Muz+B1JoWC~!fTj)PPoADCjDYmG*JFVgzB(Yc4VJr-UnK1&x-ThRNVnr!BBn2w z6I1sQQ-1{wx~*o+rV&ewH8EYD&TGagWh>J560K+6LUr#LYx_n@pYN-Sms6A)M-#?~ z=x?US%lxJehZ)$TBt+{kJwsfs1YfZB(jzXw`C* zk^d$qbS+-> zCy2kI<#??>SUGg7FhO-0MV5!rDw`tp7~M+ns2|g2#(5>|t^*WupT}J+S8;?DR z7I}ms)t_$lhh*Oh=~h0vRTf36)!bWP`fAOfTXiS(zdn6YHV!q^&`NE;=vJ4}t!~7j z!RP}&$EQl^R#T`n<1&wK)9=S=?bTWJ*w!W?ImTKC>mB1hvb~nAd*v*xgGGqyZ>Gmf z_eI{qVa9d_Bdr)C)%`@|$3(2jR9f9hy?q4^qZ{h)wTDWpoNjA8K_ho-nb1YI)lQ|> zRm^y5o%v>>*2U=asFiAJ{6Whmrf#L%$|XyE6Nj-)&Y)V$NlDo!COBR8bi#QNlaZ!Or;Yu zxbzw^%OcadgqYz~ZxfY2Q}IM-$oEqMW#R6K85~+stVJeOfx1QFwYV7T}Gy4Q!R(;G1hWhN_OiVKiyW{=j6D_ zQP5+izmXmv-FA5chZ)OsiqtWR)ICJx86sAIOlyXzJF4cArDa9)efE>7G;RMQJ|i<| ze%V0G8bPM@Av10fBTvjBVoi;|smAEG#~(KIUDL-TPtx?<&`E83(sXn?QDd9cc2sTW z(tPKBViKpiFYhrn<0QSd1`#y0F~tYoM?l>`z{(^bpCw>r6Ho^eq2@7$#-|xxl4WSI zro#cE8fWB5d};ztBhzVH*^78gsPkwW6=&ki6mg%O~CpJ0c(8{?vB4~AQO-7 zzaG;|@#t|(*}rwURv`iF>jbQC5wPw`LNOjQ*7p%m zI|*2m30U(9SnrF!tbT|`e}fc%J#QH|#9ua$Dofjx&sSeEuc2LY=$04bZ);wr>6@%( zfa+wT8cB)r(2H$vq5KlSoh5NdB9l8ACme=K(XedVK#2kQi&iqHiH8 ziT_zjD3KCeQWAe#O5!g_3B^)Eb|M}>CMEHB>f4y_(}!oCGd_vg22$Vf-wgG4|8IeW z2^fAlIKfAi!fANuFA1dRy5S32CUzM)d8B?Xd#OH+XKI~ZLz361eFP1iPw_CLhbQ&z z6fZi0!<3=qJS= z^&5V!+pTraETTq#*LCP;t%HY%r1;u@H9Wr^C1C@3FE)Vpm^U%*y*7~d{G9qe=M9s3 zecx~T@5Ikr{(0;T@8!M0@n7T6@~TWAAd?8F8lv(bL8IHgL6-6nk-t-{{6u9ZMJi0S z%Ai^;AR^x+D94NprR|hHZKGGEMCCEEdh^xMtm^1gbyTZ50;-N7s*a(mj!~+P4pm33 zs-r%crk|@+9g*Za{l2YFJgVEL>rR#%V0C|Lu8#v*Uv-$ujTt5KIP_WjxkimGL*kjFWMe)jG-K3fEvvv z^swSc`JppGv%UwiLz+p~7 zYAou^=?Z<4&OqWXegfhvA-)q62eW=yN14i_$o?80)kcwAhFet-Iaiu~Z}2jYR+T|S z72-JUxsJP-K1A7>}yu0Ht&CG!nBBi}exM~*39%i>1}YS8_&K0#ED zGg9NctYdBb>3u| z=PECmo+zV-fHjP2do$JYhWVXdbJc6sgXA9!Ju6+;rOUsa34QaaI>skquBtPj=}4!w z+LYCpq$~PeDt%!xKGa&xrS>*MKT>G}bp5K1vHy#5hh3oD$<}Iau~ze_wVID5!_@Oq z{p`Smx2S!zTDyp(LhA^fI&Pki5Ha%qB<7niK?|$pPBv+gT0L#b@}!IVvglR9LtG0wzJ&QI+LGq`nRdP z)~5}wPgZqI?A>cn$Mod$7*)p@USz0zn27w0k@5^9<$DB7e0;(ds_mht!4^gu`$-+G z2IlI!uX|ok!g%Oe1wG&G1KXhI=_K3*2Hu38?aBDti7>txdcF+t`JmI|g%F<$JsY6s zS?KA3o-aT=J^4F*+r|4aztdq)3`|{LPL^{8Xwdh<^*juwZK-yyN|fJoAX&#Yh+hKn zeCTOU_CeRFLHFlR(DQsEjQf*q>4ErCh+mhi`)PQ`2R-*e&-x^^CF+jf3h_@s&*R4X z?>`u7U@rmr39b5_I0=}rj#eH>#JLj#<~N%etvzOIFV7MoAd8dnU^2Xk06UXupGwF8 zQcgfVO+fPcfY+xZC782kw0y;Ex zK0rwdWCjVCNr{1E`Kk8FZIt)+OV@jti0PXiOWnsy;`b{b4zn+C4&B;bZgke06KO`? zA56^h(W>*CeHh=ONIgJ{Y>XeZ?jTFur1jwVQ}PU1@&mGr-SxKkQQ4)>>@f&<8`K;2 zb0)6;w1@sapuMq3DM$mz5m0VWJHgrx_HM8bf_)quYr(M@LVD zgq#TEY=@k^kaHMvPD1W($UO+T$02Vk_?Ce0Aoz}h-v|B>__u?9FZd6G|2X7(AwLB9 zJ0X8R6m&pA3<_35!9gfE4uxJQ3_(#g6h)wD2NWHE;zdx>3Z)HD+6ARcp>z$DZi3Ps zP`VFFk3*Rk$__waEd-80-~{yB2>rH0c?Xoop!^tAv_i#nsMrVnmqPzF5VRp!3c&^l zc0e!&!4(kdfY2_eTn?3cVL&GgSOf!B!hj7BE{AX(gjYbM0V4AtvK%7oAhHD_yC8A^ zA}3*>4+idlf%{}2Vek?dyc!1YhQSA+suil%Le*xd+6mQ5 zpn5Y@>-c`CX@HszsEI+%3aHrxH9Mf@0MxcZ?G~ur1+@pDZWGjt9c3mVoy!xm^<1dS`93252|O?#l}5Hy{DXf{O4A=(Vl z@K)?8I0?I@nd299%x?=?K_}-A6#+(CLDndFLbPgjt$VU4JLKKq&+Y> z22+;6l%vpTLuV;;Hb7?=bS{O?^)NL8Q}@B8>tR|OOzVPaOJUk3n6?9^AA;#8;IeGE zECQFc!Hi~@(GD}#z>K3X(}tPrVdhqtxf{C1Lf2gAS_WMQpz9dgPb#xk!|V{uUJJ92 zU=AG5SqpPE!g z-3PZFh0iU6&#i^eZHCY7gmo)n-3GXQ3EaLK?pOqO9ECf#!TM$J`DVCl9^ADW?%D`n z*adgj!QIp0?j`WWI{4B~*mxK=o`f&&gs*7W51X=KQyqM5I(+RgeC;IM>w|l%lB}l? zzOJDezEKC?Tnk%HzX2TS3>b?}|l@STnDovrZQvGCowuss6Xcfdm(@X&F1xDdW~0v_pvM;5^& z$KX*nJX#KqHp8Qx@aQ7={yNxM4m-EO56a;O+u?^1_~AkL(FWMH4IXcR$2Y?hbK!}V z@Pv-+VJvzP)_H2PY zyI{`|(2l{M+5u1d;OSlP^Z|JK82q#je%cN{od`r)gKRmYrp4$Y^?SNmbg@F4SsVHe!C8Sy9Iu`3x0PHes>%Wdf{LQe%}SZUkblp z1Haz{&o6>M)WIK)!XYml+6;$w!V7iqLOZ-L4_;UbFRXzVHo*%!;KjA@;#Tag$Z|Nc4qlGH%lqJ!-SC$sa5M(59)MSm!D}1gwe9fQ zUij-WI93YB_Q2m(!|O44{V=?-72enlZybcbkA*jP!#~!;iFxo=IlR>jZ*{_3hvA?5 z;ba4x+z0Py-D4}nw;>!vWCbGY5w!$Ss}Z#kQQHx<7qO}kYYk#|A@(7}K7lyaAjALNw<;oQY5_rN$)_s%aDvtB(oXGJcwjUbj z?a08r$iTzMphd`_-N@j1$lz^Al^3ZBAyutN)pVq42~xEhsdgjPkEj?~UWYL_E*ok-mxq;4fL#ElGDiww1qq3e;Mn~|a0k@`hQ!!o3C5z@FE zX*`BBEkl}4AW=6GosLAeAkCdf^M0gdIWnvr8FmmE-ii#LjtpOdw3Z{SZAj~JWW-Tq zyIWnpl8Fc^|bpmNyhm2m1T(lIqXgzY#R%FZyWXyVG%vNM<2QoH>j9r6_)z3ST zvHOvWmmn9fM#i-x;|?L?P9WpgAmcY7<98tK(~?k*;#2Ydg}l7nwC3nY9U-wF8;851DlYnGG^~ zBQkqCGJ7vF`!F*5Br?Z`%&9`=oIvI-Lf%)3yibQmkjp_XFGMb{LoT0-T)qUkd^K|U zM&$DC$mM&H%a0-5ZlrrF(!Cq$K8SQ5N9GkG^IDO4(~)^gka>HMD}2ZmRmc@fkSkUr zR~$m-S0VGqBJ<}W^OqnCe8_?hWI+sBunSpm7+G)A#&3;pt;9YF4{L+)=!?w^O; ze+b#S4%vDb*?JQBA0P5Rdyxl1$OEm&1DlWswjmGfLcZOGe0wGG?RChvHzD8NhHUd9 z+d{~;J;=5L$b;?3gPV{Ck0Rfhj(le`^4&t@yBm=05oG&bh3tqRJGLQ@v?GsnA&(qI9^H?8zYY2RUSwxGvhxV?gYC!+-IaTM9T5!rnL`SC&ICmWET97CQ2 zd2$}|~$l1HzLpYkY_d`KXW5LTZ8;O8~J$z z`FR`i^DgA)$B=z)WM4V5uNm3bj_jL<>|2iPTZcT`fIQoQJR3uvU4cBi9(i^v^6YNp zxkBW*I^?;1$S;;4zc`5O??is-Mt->!`BfeAt3AlCS0le(j~vKG4wNGYwjjT$Lw>Ux z`R!8Vw=0p~u0ww3MSiy*Ik+DAeFXXa9_052kmuJTe`rPiumbtRdgKp>kwc4+Lnn|I zvXK`e$P1g07v~}`Zbx3+i@bOk`Qsksa4B-Q0Xe)8`I8&@({$uddytph$V-LDOA+Ly zMaWCLk(Ul4f8LAy`5ycNtBCqa7UOkAsdK7tWC-T=;fh^*zWNZO9vYkv9$_ZyZOC z+sN@H$nn+4@r}sw6Ug5;B5zh9|0qZPu?{(5BPUiMCk`TSwIOe9K>j%vc{_x>eGoa> zfSg>0oZOGR(}ujW3+d@VdUhi5RwRA`B|emdP|}K$y(l#nrFNsN2+G=uvKvr#2g;72 z>=h_SIm*$Da&)2`i%^c`D91XKV++c$3+0?{JZLNDYLs&$%DElo+>3G^M!A|%u1=Ke zD9XJWBZwTexgz|1fWi+EQwxcq)p|ZWG>{3*A6)Jl( zDrXTYw;Ywb4VAYAEEzRKZSE!Cq9sK~%v}RN)d-;R;mY z4piYjRMA*e(H>NBIjXoBRosax-is<(iz+#cDmjTN^`T1lp~^z2vQ|{tW>ndBRM~D+ z*?v^OhYEyHfd*7yEGn=G71)95w+Gel0IIwlRlW&TeiT(P9aXUh)xQGt|RAdcmU?FNy1T|QKNRFMjb?rI*w{< zN43pEwVgnXUW*!i0(H?6)RR`i6>AUJ5ZBWq9&a{O!9x= z;Qy(v>h6i%|Nry*^P#J&PoFw<>eM-P9(`vCX!66L$uEK?Zv#!<4LWNy=&Yrnv(|#n zo&Y-gMbH!vG-Uy3%68B>^FZeg1f4q^bna--xf4L=&H$ae6Ljt#(0SdU^VWdQ+Xy;u zJLtSUps9mFQ%8WN)`6x@1x+o2rY;0c?Ey_)51P6eG<7HFd=+&5FwpsRp!2h!^SeRk z_khmt1)aYgG))Cf8w#4{fu_}ire#6X=7FXy22EQFn$`=NwiPsO7ifAZX!x z5ui*RC^Hq5DS|Q!L75&Gqa$X^FT8fgJ!M;&Flru z+zOhx3)EN&Y8(b?90h7@05!IO8oNP_OF@n6K#dzgjoU$udqCO2pzH`xwhok?3d$Bi z*@d9&T2OW)D7zDsD+T37fO7SqTpK925Y&_fH4g(dcYs=KP|FTb>k`lfLqQij0lHup zXx0qStc{?y22k4)(CjQ|_7+h4R8adaP<{cZV<4zwKB!|G=)$F-3wMAzr-C|nfeNLd z!Z1)_6sXVuDzt$L-Jrr!P+=XYun|<)4l3*c73)F89#GdnP}dSr*LqObHqb>4po_ME z=G24ctO3m}1I^t4x_A-jlF^_`CV(y-0=jfAXkI;N-d51OU7*WU&}D-`mvw_K_du7| zgD%g4E}sXwd@<g@UA_x+MJectVW2BUfv#u(UC{=*q8oHY59o?s&=uQ3 z^HtFNVW9bSp!r$Q{BF?v9?<+=(0u-U59rFlpesj!uB-!HITdte5p?B3(3L%)E7yar z+zh&MC+I2_bk$JMRUYW7deBu_&{gw5S1krzwH9<$FX*c6psQ8T)x$tn*MY9ig0Aic zUEKq^x)*fyc2Ktp>K+E_9tG-d0Cl&4y1PN$OF`Z1K;0Wb-5hQY=$gTxYes;ssRLaz z74)+apq~S@U=(OU186}TXhAn*uZvidd0lIAg=(Z)G+e<;WF9+TJFzEIdLAP%M-ToEm z4ij`o8R!lhbjN7W9TPxz%mCfd0lMP}(2~KRCBr~V)`EV~2D)=N=*~5uyUIX!Ed<@Q z6|}SxwDd*L(k-B+J3vc!gYF&(x_db2?n=HcF;Y$K+9CnvcaHb!$8YC(6TzvvIfwyENEE~w5%JnY%yqA4`|ss(6V08 zvdy4n+d<2AftIVF<%2=Xhk=%RpyhR-j2xYzM8_ z1zM?sRt^TO90pqHfmYUmRyKfEWt=bG)wH>r-7pO-C^$Z5} z3C*|P|q&VY8AA4FlhBK&}t8~ zx(>9u0kk>`T3rOK?gp)13|id-TD=amx)-#1Gidd8(CS^FdsWcAgF*KW1KsO^?yUpe z+W@+^1$1u@=)Pf~`?iDD^nmUk1-gF`=$DnC2Zn+km=9W84|;F}=)r}cUv+~Xss}x^ z3G{F&=;1}6M<#$Cc>?t72GFA(=+Swgbw$u)LqU%%20gYD^qV5+aU1mbV$c&KKu>G~ zJvkioI~eqCCFtF4pe@5dTRhN~7SNV; zpe>t0TXuo|I3M)hK+t0m-`wKwtZv?%+6STDy zw6y`Wbt!1;deGLbpshPWADEyIhJij94f;`=}0`%c1(1#tM59fnE zTm<@XJ?O(7ppR70M`fUoDnTDj0DUwM^wAp7M;k!fvY>5?K_9E2kB5Oio(KAPA?V|W z|M!2N3)I^`IX% z^S?6y1{(}*V3-GnhrzIo{|QJiWfYk51eme~OxX=aL%^sGj2ggb0T`_Vqh2uj3XB1a z4FqFlV5}aDO$B3ZU~C>3TL{LMg0Zz=Y&{s;2*!4UsV0~@1WX+cruKqqUx67#Fk=Il zIS|Yo1!hhKGdF=*L&2=&VAe)3Yb#h{AXs7)SfU;*F&C_43Rp=CSP5_UfR(HRE9nI* z*$h^)9jtUdSiiYo{WpO1-vriw57>ZuumMZJ2J8VFI2>$XCD_0=uz^d!2Ce}cxEXBV zcCdlFzy_&cgKV%tqrnDE02?#|Y)}W-p!r~f7J&^~4mRjvut8hF2JHkJvA*v?cV~ne-GIH+rSPO z3UF&J!w4K`vq*m0x4j;jYdZYtPuZD7aE13PXP*vO?|BR7E^Uj}ykLa^gE zgW1Eu>=|J8axi-bm}7%EbHSYTV9rJ`XDgU%g1IeV?gB7(EttC%ENO!!mxCoYfO$5U zw;0UZ4VKD+rMkgV>%mgnz{)GZ$~(Zyw}Vv-1*<56RqO<-tOTo^3Rd|dSd|G@H43b% z2zJ6iuoISooj4He#Iax}&IdbjIoOH2!A4C08?^`QBpd9c31BB}2RnHz*vV_aPF@do ziVAj$2X;yW*eP?tPT2%DdKB2`C19hsgH;a!tDXl|y&bG(C|FGgSj`%+n%!WvW5H@$ zz-l*x)$RhTYXGY&f~5z7rL$n^^W_~*#Ts}Amw=tR7VOjwV5e>dJ9RhMX+yzI zTL^YqFW6~2!0JbX)z1a1UkFy;3s%1ktbPyJ>1AN2j{-Y=3fSpIu+tZTouPuAF$(OA zjbP(#u<;FG;}?L9Ukf&V6WD|@un7yn&KwSQ<}R>_L%=3_U=yc+O)P><+z2*t2iT-C zut^@+q%~lZdch{`0c&UgYv=%L*a0?K1)DqpY;qge$e2flb{FcK%SX z^GAc7p9MRA0oXJTY}!JwX=}lzt6kP2g4zSi2!7i8rcEMV(Sq)&b zc7wG|0GmA*tbHR`eizt<0P7qMRwx52)`4|3fL#Qzi#CJJSq?ULEZDroU{~~j&0hm{ z&ENR1NhZc9&tqRq3x0KS7(C1YaA(OKP=nu^9IBiZ{pKY9ZA$)2mi+$~!r!7K7&u4v zGaQT=vaQ0xrLxWcW@fi+>llceW!r#<6|!yO7`!dp7LLLXvR#7x75*0)_&=rCUpY~> z`{4xTOxf;_gOuyRpr{sr<;rT=R~iJihbxV+179nJt*5c%IF2z zHqb(!%C?CHHeR+Z)U#ExU4jwpOW7{P2=!pu?uVi}Mz;IIQ=h79ZEh{LUXsg}Wi!Q0 zS!2HA;?CCQmSWlcWv=bG$GNtX8d28Jk}Eqse@SawTc*sdTdlnK`lSsIqi6 z*Iwvo&CSYn&d#*AmsOAXZ^-`z)lw{WoN)Z{bLPw$*(gwtY|PL8-@Wy%jk)$hE?d^s zp3QZZ6)4 zk00;XqYkZTMk|VF#U;ogi!x-9K@l00p%Hm>;9_(JP%S8;4ELi9E^Ih(ahz9u1T!o{F`zz+9A+bfPRv3X@@V@1;8rd2=tM{Gf4&ZaUn5Z)yx)nu#QtMA&iOHz ziHRt~Q7A(?7?pFZ1FgtmRxm?OMSH;iYK-|m;rzei;?hw>2Ts88I39C?e@6zZ*%?vI z@AH`b{|mN0kcnI{6Cs~MmiT&c{tB5JkB$JQEReSLKvMU9lS}ADKU!rckRHAw92b|? zVgRxCTncCmc0dP;C}3nDC2bfPyw!~3F&;knAMpVN+8aMS23NH9{wFae_>)rvhGBR~ zVT!6~imn)nsaSYHNhl>ssnSpBuMEIUWuP)h*+l@ZEu%1GsS z#a0}}Rgwz->!CR%rIafbcu}cTs+1Fy6O~cQNqAW~Svdu-D5I5XrADb$>XfwND`S+g z$~fgzn=%_Cm3Aesbl`a9LZwqFC`F}9 zxk#C#%vCPNC$N=EluMO)aFolG%atpX`O1~bRm#;$w{ng0Gv(*X0_9reI^}va!BrMw zlX8P{qjHn72yZAiE4L`ODvOodl-tp)+@UN{exclnx0Jh-rOMsPJ<2j=xw1l8sjO0Z zl-0_;%6-Zj<$mRtxLJ7s4=HPv2k|hH%CD4%l!x(Gc*-M4DZf@8Rn{qwDZf!3SDsLw zRDO$c^Z2bEr^ymFJZ|C@&~4DlaK7E3YUUl~%opP@?mT=@bgC_9zEDt}YHRK8OFjuSBoUnu{;Q_9!MF6E!fzm$I~yOsYa-zeYW zBxR5C9sZ(xulztDg^2$ZW70?`gG{n0K_yg5{kTD({xpCF(jeN0_NBp?Mf=hIbN~&Z z1L+{N(ZMv74xvMF0UbtVbT}PBKcQi0rz7bo%%-1WISr?yk)~tlSQW&BlMIo$}N{7g8q`s7PIO5zV2wbTM5*m*O;3F-Hg-e7P^%d(`|G+-9bxm2K|EWq`PP- z-Hq{d4=tnRw1QUBD(a!tbT8dUYv_LZB|SiE@h5r^6X;j;5Iszf(64bOJxc57G5QTX zPEXL2^jmt0*3;AU4E>Ha(6g9}iMW`4PtQ>=J&#N25A*`PNH5XL^a|$DMtYTAqt|f> zZK5~mO?rzq)7$h8y-Qo@kMthBPh05&`j9@NZCF7c<1+e${zTjH2qw{==`Zvt?V!)_ zH#E@a^abt2Wcn-pjlQI>=OD0oJD)+JNh1f#vb~Cfhmj_ z!?jFh8q;w$GnmONmS82Ul=Wl%*#I_>4PyJSec52PAKRZDz=p5`*+J}JHWX8EIXi?M z$_~SPR)#Ct;p_2;1!7^+nYh+oLV@<4?wXjxp0h`6z z*lgC$@~neh$U0ep6}TxfYyrEL zUB|9x3)v0qMs^ch#BOG{uv^(;b{o5$-NBZyU$8sbU2G}4o87~fvE^(9Tgg_j9=4j@ z%kE=q*!}F6>;blxJ;;8=9%2u(N7%2~qih{}jQxf^&YoaTvfr|&*n0LfdxrgvZD7x` z-?QgfFMFQ-fxW<9WG}Io*(+=#dzHP$UT2%w8|+Q?7Te6;X78|f*%tOk_8xnmZDk*@ z57|d-8~d1j!v4gzvp=)Huus_z_8I$}eZh9Jzp}rvFWFb@@9ZD!YqpF1ll_bRo9$-* zVc)QC*&g;C`=0%vLRC~!nX0Oqs;h=-s+O8iOVm=epW0s?pbk_Asr#t=s)N=2)cyJ2 zbx{vg4^j_ShpLCDhpLCEW$NMT5$aFWVd|0UQR+|C;p)-qG3v4E2=zF1qO^&t+MrHW&r;7;r>N(s=c?zaQ`Pg;Y3g)!hMG}ls*P$^&8balv)ZDzsu!rU z)HZdt+OFo+4)sE{Q!S`PwM)H7oukfGFIF#6FIDHMm#LSlSE%#VE7hyitJQAx8ue%D z&(#I$wd!^1_3A?P2K7euCUuc|vw92e#y#q->SFaa^>+0Rb&2{5^-lFJb*Xx{dXKtH zU9PTBSE{Sj9(A>PuX>-lM!jGCrTT!nR((+YmHLqSu=_v&+Mull_D2lWN@MfD~1W%U(xqx!1)n)YSHDnqs()4g zrhch@rT$(0hx)a;OZ}(%FZJK*ZuLLvH|n?Q9`!r*d-Vqmnxc`$G*#0yT{AROv$TX( zqLpg>wEo%vZJ;(t+eh118?5c8?XMl64bcwN4$=NzKzzTDexCRcck*3EGL;DD5QeWbG7fv{tRv zXti3Mmezc2j5byqr=6;urqyewYiDTVwF%mp+C*)V)}T$+&eG1-rfBDA=W6F^Q?>K8 zY1(vchL+K0YK>Y}%V|wov(}=uY8Pm;v^H(F)~@BX4(&p%Q!8jitxLN|o1@LuF4iv5 zF4g8~muZ)4S7`IKE48b%tF>+vv!MitF~CX zO}kyYLtCQ#Lc3GDOIxblt=*$7)0S&1w3XT_tw&p}-K*WFtpnrwqAQ$dq(@6wn2MV`@Qy@)~h|Q{Xu&{dr^By zds%x$+o-*&y{5gcZPMP*-qhaGHfwKd?`ZF8TeLrF?`iLATeT0g54Df9ZQ94$C)%I1 z?b@HUzi6LoJG9TV&$TbKo!VcuziD4;Uul2W{-J%X?b80K{Y(3|wp;s;_Ko(fwnzI; z`(FD&hpy1F!i`Vsn1^kMpu`ce8%_2K%_`Z4;k`Uw3veWZT8ZtITj>Pg+xQ+l~x zp;ziv`U(1p`Y8P*{bc6hu3>sRRW^(*zO^sDu5{Tlse`p@+R`nCFX z`t|xk{RaI;{U&{pezSgyeyhG%zfHegze8W5|3bf0ze``L->u)HFVmOnEA*B6D!oTv zt>3HPr?1iP*MF%$ps&>*)PJQvq(7`bqW@ZdR9~k*rvFBNTz^7;Qva>~l)henT7O3W zoxVYTR{y>JoZhQHum3@RL4Q$yNq<>?Mc=5us=ubcu5Z%c(BIVG(l_gG>+k6A>Ra?b z>hJ0A>s$2?^bhrq^lkdb`X~CI^zHhe^}pz!>O1t$^w0G#^qu-&^}pdR{Y(8T{qOoe z^sn_@`aku5>HpSu>;KWe(ZALA=-=tz>pvJU6oU+AsD@_fhGCe7Wh9IeqtxhU^fv|= z1C2q(KE}SrU}HaHf8zjSh;g8Cka4gv)HuXA)Huv2GY&V7Fn(eTGmbQlGJa|dH;y)r zF^)Ax7{?hSjpGg5a17T-8lI6d%8d%6(x@^{Fitc^87CPh8>bkfjcTLDs5R=0wBZ|L zjIqWz<5c4`quw~(IKvokOfb$gCK{8B24k{umT|T*#W=?}*Er9ZYMgIOGo~9ejEphU zXf(1$&S)~4jTWQTxWJfYv>CIFb|Y_e7#A9yM!_f=UB*Sm9AmC=v2lrUsWH#E%(&dR z!kBMdX8H6lM&niE zHRE+-lktY}rty}s*?8M{$9UJ+V*Jr~&v@V1YJ6aPXnbUBGd?yxG5%z1H~wt=#rV|N zVSHwMZhT?vH2!M*&G^#z%J{qS594cNm+?>IU&gxmN0`T%BhBMY+jLCVOq!mVGRw^hv(l_GPcTn3 zN0}#?C!433qs?lw#;i5#%(Us7W6ZJUIP+BVG_&43-8{n_Z%#1J#J(7e{jfg{zz`gW zgK#j0niI`QW`jA|Jj*=WoMN71o@<_GPBqUrrm@=rMb%NF;|=Sn)jJ&&=05JIsDwb-~1&G!2t6CJOCA|pqpzk2uENgG$e2+ z_Awtce`P*|8uMZE5%brWh8Z{+qs>Rnb>?H{Z_LNdC(I|!-rsMI^J()L^LOS3 z^I7xv=5u%yzcza@(0tzfgZYB_qWO~fviXX+5%=Slcm}`2^H_(+@EbgiUh`G+HS=|I zllg}Erumk+*?il4$9&h^f^Iww!~CQ99_}^YH@BJ}m>-%SncK{d%}>lfncK}jn}0Ds z#Wm&*^E2~v^9ysQ`B(FA=9lJI=HJbKm|vT_%zv8yGXHJvHveOOV}6U<%{{me8S^{y zdpu$OV8K!>vY4f!%+f3!hvPQOz%5vc!>|}VmWeMd%SzxY++>wlrB*+yzcs)bXbrOV zvG%nFTl-o2TL)M}tOKostb?tg)*;rR)?rqeb+~ne^%E?Ci9evfH4L{}N8(Pb#xJa+ zte;xLt)s1DtYfVa)^XNI>v+qy9Lu$mmS?4`a;w6sw5qHVtP`zK)=AdM)+yF#tJiPj{m!J2HHWu0wJvCgs1wa&AqTIXBS ztm)PaD`U;H8u6r+wQ^RI)r{%jzaeB{0j|e&_!$=BYOBR+wJtz4uEOuFSyr1h+iJJ+ zR)=+=)oB&1qSa+xWX-YWS{GZFSeIJ!tjny+tt+hg)|J*(*40+Gb&d5i>*v-2>ssqN z>w0UUb%S-Ib(6Koy4kwLy46~2-Dcfx-C-@Ueqr5d-DNGc?zZl+mRZZK71l~?mDOXd zw(hm=v({MmTfej(u+~}+TEDU$vL3b`v3_kmYOS*#vwmYeZarZ=Y5mrE%35zdZ9QZC z&e~u-YyIAO&g!+ExBg(gV7+L)WW8*?Vr{fuwO+Gcw>DXCSZ`WyS(~l5t#_<l5ow)^_X9)?choaf7u3@8e_ZGwXBf3u~wKSL<)qm)2L- z->rXGUt7EIA-3Q~Y(*_Tvi@oP3-4hYKENNXe_Ok)|5)Ex-&%XD@2u~w9}-9?2}-bp zn$Qw@!bq42E0IW)BuW$g68#ec5(5*168j|fO$<)#m)JjXKw?PZz{Ej`gA+p&ha?V7 z9F{0c9G*BL@sq@`#F2@k5Y1Ini*iO+d#jVKPK#?zxxCu)>dLFfueMWLC+@3*dna98EAQod)s^zTI)HcW0FUj| z1^8{(4dG;eT8>j4jAOfYaCK|T>x}Bzna0k1yHTBQ&bQ}gC8|4H+nY0uUB!T8+f7y* z)#>=&NlQ`%3MXw#k}I9kfb>ddWT7A)+OC%j=yNLF{+#!*&u0hZ7-4DYZ^|v zCPWb~Qn`_q%uWkti)mHKMXHnpR#yf6t``c4oJfsei<6dcA$~{RS4lScl1){fJ|>t^ z$(WegrDGcNvu9@nyAxyfBH4DT#Co}&M3wf`u`@HBf*vPbGB!pec<%;dRaXc>tgfi6 z<5Iw{u@pFdIgLA29~bQd+o|;hN?WqX4Y@4Wsy67iUAx*CCs{UbFO~(Yv7M?4eO$4% zEt@Mjb#KaS*Apn5bYr}~ywH11OEZc6fPOpz-z)2@-OY8f*s@7-oQpS>U zd_kv^PD=Vcxs+9M$!e?hGXnhm#U&Jf{fs`tc-5t6^chAl)(aTxq=gXJj$g^r?agMo zy_qvsE>Bg!ZQH2|xE<~iA>XbmXmMS)|M$~CG?h)PTT#)w{*2PXF9uP zw`IEIYDHQrZH$-F5N!OEq%9@6o))w?Y03S{@{;j|woIWVT4E>diD#}WM^4FkrR0hV zjcdDJwLUR;O`jMnRq4bZEs^aya;hoG0!JtmR}A6SmV10J@N2MS`XnI<;u1;%pGkE^ zWlhPXSb$3>{b(#FT`tEems75id%n_F8)jxY^@d2EY$t@N5NvTBNna?W<#G|qC7VLy zQZ8|Z`ld2qjqOxRy(5g5?RuU*xvvCCo#9CEItgAU)RI6F#acS)bh_W<|B?*XDK{odiJ071Kh!wdWNydw$$@qd16RmRFO042dMW+v zU`a~Oj(KmLoo&r^<_fI^{p{w>j4<48ZG~7{$FC?kCk7UHeNR6p0=1o*fFDlUmpdgX z#li{ME&DyGGyF>ZTp=0a5=utm+`W{uldh78gwC~{nt-9z71hCE!FH>p-4N?w-FDqTn4ENtT=iOsy;f3F8(?wLwUT9aDv{f3 zo=&TQu!JLzOyX0V+4?scrGeE@Yq-zH%uw3w4l%6%eF z&TfU&PN5Ml_w{z6<;5k^810#ke4*Hx?`X;C?R`k~YD(Mxo77;LowOHTLmpSLTzj*a zjx<$nMYzx9{;x{+&+nzIBzQ{C_mzx55}mX-7dmNS#htXU@=iLm;?l~yQX9Fh`+V$}gNpMU`F?kEfkNFE)R9BR8T_9Z8ue=7Wuie7)h+{ilBt;h0Ny# zUFu>-s7^x(6u26#es?y48tt&XUc14QRNmoeyDgqa7ZOGTkT76Ei)FpG`wY26$=hr#Wq13IF z%D|O}2B*d^xj24FY$HFVUmU$+J0ce4qv7zYu-G28WQ)-=79HBE0z2tTTBWTBBaZKtv-YF0UM)00gekBFK) z6wx>!ib^yFfeC@8syu?Kj*z>Rw(J$ITkx)I276w-Tf@gyGb0O)RwkHs& zqE;xYR%4*gMiv@N8XI$2UNz5`qR*+G&mun1NAy)zhD|qF%SXzNEHvu5VhbNooa!Um z9Zrgs4MLVtrya{*DqadZUZ7MctF}|gM-Wozb6lcX%K=o*GE4Y4K`RtUOC0V)XxHU} z(m(e3B`SafU3O(s-};m1|v#0 zfxjg^g=m;m44aC19;|M_2G@&eu8DZ+)J09V$_2Rvt3E~+$71eME=!H@o7 zzw2V66X`;WDFN)Q=_o~)%Bw|f${#;HmpMft3 zcNb!iP~dDQn6lLtoH6+XX2(F0q`Bq1sq!H#=?WAY2Su$=6ZP-o7X+Z8ViFwpI7bKc zbr7PVyl_uC>1Z$5j?d?<=Xm(Iu|!;AW9C-yJt1^x(2O5BwOrX)?V)~F^ZZsi5{1S= zQ7a@|tawGRKZIi;DYKnAK1%=me+eA-HX`!tIH6*ab+MwUizihV@!JiQW@-LMa>|{N za7;H4SwT}W9;B8ptCjze!19GL11}{eA;iRemAE(5Cfs9*dIMuQa4)|lB*W=zGlMsz z0~5U*STQkeVM#@RDjc3WF(EQH#kGVbT$zBQu~QQ~2-ixW^WqoXK21IwEze(2J7OwA zHrs)N(`Xj?NteT17>yo?p2`)HC7hg~6^iG8zAjGmjuW%Kj{>iV$q8(tSf+qpl^;?p z>B1Bm2Su$=%&bB!<4v`T)6o?walxbD z{A&qsCL*qmi$(vg|Jq)*>u~g?T|eF@PF*yXQx_hXx#JU~jHeUWBdcrgEtBSZ(G1+7 zXx@P`7CiDp9&x`Y1_&9$y`zzZ#({e|Na5J7Blfv)lY-vJT$q8A6!2wlS+59I@S+Gu zWjLzs28Skf4u45_O`KVM94F49$ae~$`c9R5P$5<)I3cP04fSFUd2zTn{rmV-9N)!1 z;<%F*=LX?s#rQ(2&z-HX*G)zPx=zS3+x2376R33c5>A2ixC)JfqE@s~E(teEzt{nj zgK!@#8ZS6LNXg{O9)yP+k>|D@k@vP85&yLv5&yMakp-|_QORJtVRA)8?Oi*JW5~(| zS7iQdCkzV9q_!&}LAEQB+IC9h=j@csFQ#OEF(vZywkMKrwkP5zwkMNso{W!pGE3k| zr_z(zDNlO)o~)SgWcI;xM8MSJo_RaRCxXqMBfMKr#<@IEO<;R6spiQ%s3(IQp3L%i z(o6Paa^90cWlv^hJrUKnJy!%)JP~)YJz1sT$s~^_onudAMQl%0cGzCX2N9>YJsI`) zB;P#AH&4dpJQ3-&Jy9KDdotDG$?^?P#=ksU^1+sT5E**g6WIsblk4EgIG-mgJv-Abgz{wa(UWmjPsT|-8NKvma>|p*DNn}VJQ>9IWF*~_k#tX{ zXgnGC^<>h^lZjGKhUh#QS@&eHC>8Qq29-P+*Y#wo$CE)MPs+C^<=c}%3Qx+bCyP8% z;rhrVswX4=p4>m4+&`Y&Kc38=cp|@Idos@K$t=Dn;-|JJBlez%&)S~M!+SDn<;kd( zC!%bI*Yuk?aAD^Cv)eXEHd##KGybRwTvf=OFWs~^nAIl zzKBD5zKBbDzKCO{0_w?P8&A|L*q(@2+n&tUdNSJO$yA^xi)*~3lxq=*w>?>V z;K|stCo4ldnMd%Fl0QkQM`SX~lT}DwQu0kEy*yd-^?@?`SN6LloECo%@MCo=|~tSj+knT#hhFP_ZEc(P8y^CW#TwC$zBd4zO} z=&2{_C~Qy0p*@*>^kn?plgT?z#<@L_{kA<3MYKIp%VB#m1 zPej9QFD>nah=bdn%(-|n=iU0C z^y0Y%(Ii1bst*nEEWCJbf%7Y2`>-Sih&^a8aMJ~B?1Ou89M_8iy<9y;M8*!X=k=Le zOj#=CLwU@d$Tj6UGeTA#Prf{+w>(~!*h}?d*VK!hTQBx|ydbC}CR7pQ4SX)q8+#L8 zRZK%w3>!q>By3CrFJBCM<7vie2rrJFq+&0{i_#uk>qZ#jw1*eD-CX@f08#pdE8(~| zCNc;G3nJslbSg@a*k0_odr`2QYvu@9Z7hgER9+yfjn}v~7MLKuCIRB*jsjv_Q%88C zC>YnqP{!3G|zpf`3_y}%chy)j-t z#_LDiNd?)c05%opk5X|)Cnb^q+zm?wk!abB@TTHqNh;2eq@o}b*YY8~sUY?tFvy4k z$LmBqOGVKeuJq&AVtU<(lc^wT79jJ2w4!YGfsHZ5LC91bgiHl_m%znLR`^<%q$JnJ$W6iw`KCOZ?(S;tXyQ<7elZB@hP>V;%z}P7OL!xgsgg6-nuzB;~mxDgBeAIA1uv ztVH)^MU*eUBJgF^oiF?n#~0@c#~1#I{-EQFVg<*q z49?MxFTYOkg+Ji@r7UG_`wCm&Mq=EQaw#T*UE(U*q^9 zKH~Vozj1sa$Br+~$&N4lZO4~i1o^VM))#RM$CveKzAW zHFG7TWpi!CjFFQn=n}4i_`_nLf?Bx>niZ&^wm=1meO*&a?Oimt)yNAy#5!B@;z_Nm z6#`{c%yb!DQW3^&J*lMh zq>|E;N=nZwDI+^c>6s-(g`J(05uT)sz$B#ym6VZ~r1Yec(vwQcNK8_CP{~j-L}HD7+a=F2*8U)F*9@>@1v z)(`ojSlaPLS-InjQZmOEWxS3rYCj!cly^D4DC>26QO4!?vfkX6pIi8{V$K&4A;%XU zo#TthkmC!_&hdqXaeT3EjxQoXjxXz}eGw6Id|C15i%6j33y?$Esih0_wZ%?oG&6~jxQo+ zjxWFB@nsdKFTcp~Wd)`$B1w)f>)U-%@#y$s|2e*hWIDe5#?hDG3;6OIBVX2-`XW-{ z`0`U4UzRxf@^cwqmM!`sa_#sc^5pokT+tU1D90E3-SNeKcYF~Eb$qcO9bfEE#}_9N z#}|=j#}|~10)5RJ zfy(BGSE{RH-^KQT57XBq1Vt0lDnt{H zo#bTfBqw9fI9U;SEXj%}nvjgM-pSZ=OU9mCGO8}%HX|A>_UDqZ@0Sc}c*HuyE@Lt% zw2-}#CzK5G!?HK>&XZA!ms^@>wAlAc`cb$c8M}eW*bPkjkzbpPeZQn1&e-wo5HB}E zatLmR;$}!Bw?=VqNF=vOac}7Oa?2F;Mr*5XCx+?YkLMQ$rMX>o5%q!SMjIkVinMX-_daU&P?Ms6!N zb#ZTu*NyQ;Q5A0XV%V7UiWoME0&x= zTgMn69xVzfaWfeKL@`rtEaTpIh$v>tt!4}m4-v&dx%rF$D&m2o-OcT21Q6{)ZcXFf zc;ILkaQ9s>9*F@_~s?K&iu%Hp~=X%=Ynf-PH=A(TRL+=kJ*&( z>g12irq+uDOrdoy@30EFi*oHjE7#iGQVf9FTP30ZbFoS1h>J}+$6IVN1=?bh5n$z4 zj~Csl3b+j9X;Ev3P1M7vPJXCd3A0r~Pq(3)+}&kpzzG&nzaL-K<+1wTZK z^CN)c{0QI#KZ0g}RsaV42wn-M+TJz0GZ-Ui@G*Epj=`H^4AJ6a1aN$e08We%G{qPJ zU@!)61^lyxd*|B1y>o5h-nq7L?_68BcdjkmJJ%NOoofsC&b38A%(X>8%(Z2J(3S~N zTNYf{!o75D;fA@kaKl_%xHqmX+#A;x?uTnvR1L_@ZEVZT&gSQ|k8I2oa)U1H$`$yp z-$y-S22OeZ_O978`R}?n2YaGlpW9-S0P?B_^zG&Qj*E{i;!mzE;&rZFneIQcGtHQ(oZw*a>6=|$7XT%iTARB%a~!aL+~hlt zbB2TU>(gCq8qoJHUufE=xvRCUEjOE&s!Lw&8+FCA4ZsSm?agg_g9TkNn3ZYI7jtd7 zRvC{9CuUb93LTlo+{mWROb~Z>ZCP?+3&+g0MTpn6MM&4RMM&4RMX1-cMd;U0i%_v^ zSNm$Zt5c{CyQbRYKU~A7UE0{y*_mr^yqMo4viY{QOrU*B0zs+vdAG^mk2xO|rc!xx)MqS1>@mK?m;#7!xun9Ff6bl6o> z9XIRZW);U24O?u|`KZMvOAL}PH1&&z;j;(_;?zb%aeXWXi@N!6@l9Rdo2uZZUpy#h zQ!s3}$s7@iab2gXUu%0)YkO<);y?rgUbs#wk-M-f(-ug7Vs>kLS3&5&jyBmcviaid zOtHnx&F&~(T*w6<{<^k^wY#?P=v`OVB-tV-@7nT%JG-Jr4#4-19Dp~)0Kq29baZs) z=L$0Ng{JgnxWX5Y0I`Y zHSznBj$CIb%L6)coyC@XS0U4$<(|bbmp^Njlqy5otZ4BAb=`#&7Lj( zqDQ!hw#an2c13mnzT7G{_3OhdF1vzP{2&ofT5L*$oGLaA*o#rcrjm$HL0ht^*klA; zDmInIOe!|@>%*gBlPP9VY$}O3RBS4V7*uTHiZtR+u_+O=E4HOB(SbUuYV%H%Isq#XJ#rAy@dm z6nx<#+J`fI=)Sz+!%6ON3^9NBUCbdaxO?%4(-v_lc-ogwe0aerK71cu1+RtN;x`e$ zIA9-+aarPj#br)cN3ivss)|H#AJ_reRcQrX+{nwGD$&F3hwLdAJu~yg7SZbJ$VzAt zCUhN|N#LLB|()e92!*VD- zk&+NPXiIp$Y<=F4DxkVTobhZ|7MHr=j|j+Ahbxl3)fKYH+Lgtst}HflWs#XHi&tG) zyz0s#G&f2Di{jR#EFwH-7(K=a%o0O&?Da*K$vV1Hl z%M+8bygDh%Hz&FzPi*l{|KI}! zK8N7hJ}vqDtjx^(MRAuH+^OmN?p`-u-zU|=uIeonXsc2btWCPnG4QzuIqlTNu6%~Y;d zs^4Cdl|;%!oh;!?%2L3jEDcV|QsJabpw!9I$)qg(P0B=Aoh+qH%FBBAf z0yU?PyDCq$9NsHAD07{(GCu|URX8Mi2e`C?9_?Y?Oz1@w>9>st<;Z^2|)3Q&UD| zJ18vvPY}k2qnWDl43E zo1Ho%rK*5)PP5f%DP4yx%yLEy%$~oa$N~H#dxVWt<)~`ocMy)Sy zp^1xoX5!+GvbeZ+FxDMvGcN9*iHqAiV*Q}njEh^ZV!fMrGuFF^Hsi8m`N53xqTwH# z1yx1V9cGA!#IPJY+j=r%XIoEZ>}=~b@}2GSDmT5hzO${@ymz*DB5XX2S2P+%J!Z?f z#hvYH(F5ZY*=>f!(sNjBDhZ2CC1J6tBrG4f8S~wZH!FzvlI${~(># zYYm1JVM8(`H_1&?5@Ie9a^$I)pC5f~I!9k?=>+C0bE(uT)K{nA{(UmFT5}3VfN%=r zP7_4qQyL`*eIK0kf6=vq2hUIAn*8>jFuE1T35og9Mo`z~Df$ z`vOc(M-;vsfVF;ia?zLzqG5FKPU14x87CA5gT#e?>QLw+*G*F@H7Fm!#X;g05`_&6 z-$3M-gM{9v<8e-5ZjfXS1&U#F$X!M5YUX5VnmS;3Y07{Ml6z??pvGxL!D{6r3U)9^ zz-B(wneVu63PvWt;>=|(9ZCDprx>C$$4{`cXb82oOQ9anF&%^9$ql;&*7|;u4V?TS zO<3wu=-*1f@EojEk+T4s!%W~!h^b9e=P<*3Mu9Lt2lMbg7(*~Ju-gJ=IK@KF2gAn} zIiy&2=w~THvQQPW!6!5GB#8XBTN!(npEUlQPQ~yTkN~V!Mx!yyu_2VDh=bMy>OjA; z57b&L4|Nvvr$mRfL4uL1s!c~~qR%jYGCx;~64zmF+I9sjaA}$mjPg*xA_pU*6q>-qGP0#cbuU*mhHB2D`8j>}Ft|Y^qZlP!h7jLNEd#*C&QysE)G2 zi6mxpJAozU(+K974|5Fc%%>R``QMk94YEjlf1Js)bq_xFoR?Dp=eQ&*o`CaQ8c0{U zG?8B5vWE0~Tv|w<95)rPQ8kaT3a_-K zZJdvijZ`(l=Coy=4&LJQiYo6VPQZws?$Ka)&+O@_C#!!`50mgZ>&h+Td|SX}PJ49s zlo|mE@~(g@ymO%^;Oe(Q!3c(30oz=fWVJ;*bQe1>n5`S*@h;nCIZX~792S{BxoQ~a z&$R_~IgnMbT41#l1ZD&8-nhdk?TxzvUOf(`9i0YP%tsW34EuY3KY3)*8g(9->$T@o zAIHE(K*vm>S)73tJlT~p3Wlc#do##J3~E6>8YEx_xdXKz!!hW|I60#ftaTRWU57y< zh{0?@BT%_y^jVoO%!Pp!WN3T4m4p3Ms$i|>`1nwTXl0{;9_5bnN(Ed)8&FEYUN3CG zdR+mpDK#{hPbrPrE@p{q)rh^|W(k ztY{gAZv1*tf}m`g3?I)=8m?^0>zvUJS}i_EXfN%^j&aTt@J6K`Oso5qTA|CRConr4 zx?UM_B2k~itKOK>9y-go`wR?NK?gdXfIyiG9k8@6Iv1lo+F{+Ij3Ji|tL0aoTGsd5 zU2*qWj`3Z*5ABBIOOJIOcdEVJv?ybQ#wd*%WBpyUJQlI2M^)ur4&=PkhzT*WbPnl#}n{V#31uFVvtD?gG`E;z!Pv6G041w z7-W8i7-Zf>Oza8xIbx9c1!9oN5Q9vP7~d0ch!|vsh(TtA7-a5oxK^3@J_>N%6YxvL zz2OP?mGY-+9`ro{zvl2-h5ii+3jJHfA^JVVA@p|~dKL8dC@AP36o=6F6^GCdIJ{m# zKSV)6A1Dr?A1Mx@AM;1EmRWvz@;zwWgWB5Qlk$-x2-$l~{&kQ@!}v=IhW`5fz9e=3 zAEQC9O>ELeq2GMF5i~Yun0>*r8-*&bzg53xw`(t$N;U6)u}aR6=BKlyPKZrr+mbB% zUyy$FuXHsM$~EKReggf6iL8x6xsK9jjZcYT^&bvzCkRQ?H0?LjM%1`8)3Tql>}LD1 War7s!{wz%rN_!OQ*CO@X^#2EvSW`a$ literal 0 HcmV?d00001 diff --git a/interface/resources/qml/styles-uit/FiraSansRegular.qml b/interface/resources/qml/styles-uit/FiraSansRegular.qml new file mode 100644 index 0000000000..4ae0826772 --- /dev/null +++ b/interface/resources/qml/styles-uit/FiraSansRegular.qml @@ -0,0 +1,23 @@ +// +// FiraSansRegular.qml +// +// Created by David Rowe on 12 May 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +Text { + id: root + FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } + property real size: 32 + font.pixelSize: size + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + font.family: firaSansRegular.name +} From 032fbd9a55b6ff220c7ac0e17f989ded353bae6d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 11 May 2016 16:41:00 -0700 Subject: [PATCH 28/77] taking something off now makes it an entity-server-aware entity --- scripts/system/attachedEntitiesManager.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/system/attachedEntitiesManager.js b/scripts/system/attachedEntitiesManager.js index 124b2f76ec..e0261b3c7a 100644 --- a/scripts/system/attachedEntitiesManager.js +++ b/scripts/system/attachedEntitiesManager.js @@ -166,7 +166,6 @@ function AttachedEntitiesManager() { } } - // Entities.editEntity(grabbedEntity, wearProps); Entities.deleteEntity(grabbedEntity); Entities.addEntity(wearProps, true); } else if (props.parentID != NULL_UUID) { @@ -179,8 +178,23 @@ function AttachedEntitiesManager() { var wearProps = Entities.getEntityProperties(grabbedEntity); wearProps.parentID = NULL_UUID; wearProps.parentJointIndex = -1; + + delete wearProps.id; + delete wearProps.created; + delete wearProps.age; + delete wearProps.ageAsText; + delete wearProps.naturalDimensions; + delete wearProps.naturalPosition; + delete wearProps.actionData; + delete wearProps.sittingPoints; + delete wearProps.boundingBox; + delete wearProps.clientOnly; + delete wearProps.owningAvatarID; + delete wearProps.localPosition; + delete wearProps.localRotation; + Entities.deleteEntity(grabbedEntity); - Entities.addEntity(wearProps, false); + Entities.addEntity(wearProps); } } } @@ -271,7 +285,7 @@ function AttachedEntitiesManager() { this.scrubProperties(savedProps); delete savedProps["id"]; savedProps.parentID = MyAvatar.sessionUUID; // this will change between sessions - var loadedEntityID = Entities.addEntity(savedProps); + var loadedEntityID = Entities.addEntity(savedProps, true); Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'loaded', From c784ca9771b9e733d45e25083fdfb43c0376ee8a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 11 May 2016 18:45:56 -0700 Subject: [PATCH 29/77] debugging --- interface/src/avatar/Avatar.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 1e8ff7775e..95a3ef6132 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -197,6 +197,7 @@ void Avatar::updateAvatarEntities() { QScriptEngine scriptEngine; entityTree->withWriteLock([&] { AvatarEntityMap avatarEntities = getAvatarEntityData(); + qDebug() << "---------------"; for (auto entityID : avatarEntities.keys()) { // see EntityEditPacketSender::queueEditEntityMessage for the other end of this. unpack properties // and either add or update the entity. @@ -206,6 +207,9 @@ void Avatar::updateAvatarEntities() { qDebug() << "got bad avatarEntity json"; continue; } + + qDebug() << jsonProperties.toJson(); + QVariant variantProperties = jsonProperties.toVariant(); QVariantMap asMap = variantProperties.toMap(); QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); @@ -214,7 +218,7 @@ void Avatar::updateAvatarEntities() { properties.setClientOnly(true); properties.setOwningAvatarID(getID()); - // there's not entity-server to tell us we're the simulation owner, so always set the + // there's no entity-server to tell us we're the simulation owner, so always set the // simulationOwner to the owningAvatarID and a high priority. properties.setSimulationOwner(getID(), 129); @@ -225,17 +229,19 @@ void Avatar::updateAvatarEntities() { EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { + qDebug() << "avatar-entities existing entity, element =" << entity->getElement().get(); if (entityTree->updateEntity(entityID, properties)) { - entity->markAsChangedOnServer(); entity->updateLastEditedFromRemote(); + qDebug() << "avatar-entities after entityTree->updateEntity(), element =" << entity->getElement().get(); } else { - qDebug() << "AVATAR-ENTITES -- updateEntity failed: " << properties.getType(); + qDebug() << "AVATAR-ENTITIES -- updateEntity failed: " << properties.getType(); success = false; } } else { + qDebug() << "avatar-entities new entity"; entity = entityTree->addEntity(entityID, properties); if (!entity) { - qDebug() << "AVATAR-ENTITES -- addEntity failed: " << properties.getType(); + qDebug() << "AVATAR-ENTITIES -- addEntity failed: " << properties.getType(); success = false; } } From c29b82e24de6a0183c3cd838f01e03f9720ba877 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 May 2016 17:08:28 +1200 Subject: [PATCH 30/77] Restyle file selection dialog First pass --- .../qml/controls-uit/AttachmentsTable.qml | 7 +- .../resources/qml/controls-uit/Table.qml | 163 +++++++----------- .../resources/qml/dialogs/FileDialog.qml | 76 +++++++- .../qml/dialogs/fileDialog/FileTableView.qml | 79 --------- .../qml/styles-uit/HifiConstants.qml | 19 +- 5 files changed, 155 insertions(+), 189 deletions(-) delete mode 100644 interface/resources/qml/dialogs/fileDialog/FileTableView.qml diff --git a/interface/resources/qml/controls-uit/AttachmentsTable.qml b/interface/resources/qml/controls-uit/AttachmentsTable.qml index 6022de7f93..ce93b8f4df 100644 --- a/interface/resources/qml/controls-uit/AttachmentsTable.qml +++ b/interface/resources/qml/controls-uit/AttachmentsTable.qml @@ -21,7 +21,6 @@ import "../hifi/models" TableView { id: tableView - // property var tableModel: ListModel { } property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light @@ -46,7 +45,7 @@ TableView { RalewayRegular { id: textHeader - size: hifi.fontSizes.tableText + size: hifi.fontSizes.tableHeading color: hifi.colors.lightGrayText text: styleData.value anchors { @@ -87,7 +86,7 @@ TableView { bottomMargin: 3 // "" } radius: 3 - color: hifi.colors.tableScrollHandle + color: hifi.colors.tableScrollHandleDark } } @@ -107,7 +106,7 @@ TableView { margins: 1 // Shrink } radius: 4 - color: hifi.colors.tableScrollBackground + color: hifi.colors.tableScrollBackgroundDark } } diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index de6950c07e..2a0fe545ef 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -17,20 +17,54 @@ import "../styles-uit" TableView { id: tableView - property var tableModel: ListModel { } property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light + property bool expandSelectedRow: false - model: tableModel - - TableViewColumn { - role: "name" - } - - anchors { left: parent.left; right: parent.right } + model: ListModel { } headerVisible: false - headerDelegate: Item { } // Fix OSX QML bug that displays scrollbar starting too low. + headerDelegate: Rectangle { + height: hifi.dimensions.tableHeaderHeight + color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark + + RalewayRegular { + text: styleData.value + size: hifi.fontSizes.tableHeading + font.capitalization: Font.AllUppercase + color: hifi.colors.baseGrayHighlight + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: 1 + anchors { + left: parent.left + top: parent.top + topMargin: 1 + bottom: parent.bottom + bottomMargin: 2 + } + color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + visible: styleData.column > 0 + } + + Rectangle { + height: 1 + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + } + } // Use rectangle to draw border with rounded corners. frameVisible: false @@ -50,8 +84,10 @@ TableView { style: TableViewStyle { // Needed in order for rows to keep displaying rows after end of table entries. - backgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightEven : hifi.colors.tableRowDarkEven - alternateBackgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd + backgroundColor: tableView.isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark + alternateBackgroundColor: tableView.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd + + padding.top: headerVisible ? hifi.dimensions.tableHeaderHeight: 0 handle: Item { id: scrollbarHandle @@ -59,33 +95,38 @@ TableView { Rectangle { anchors { fill: parent + topMargin: 3 + bottomMargin: 3 // "" leftMargin: 2 // Move it right rightMargin: -2 // "" - topMargin: 3 // Shrink vertically - bottomMargin: 3 // "" } radius: 3 - color: hifi.colors.tableScrollHandle + color: isLightColorScheme ? hifi.colors.tableScrollHandleLight : hifi.colors.tableScrollHandleDark } } scrollBarBackground: Item { - implicitWidth: 10 + implicitWidth: 9 Rectangle { anchors { fill: parent margins: -1 // Expand + topMargin: headerVisible ? -hifi.dimensions.tableHeaderHeight : -1 } - color: hifi.colors.baseGrayHighlight - } + color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark - Rectangle { - anchors { - fill: parent - margins: 1 // Shrink + Rectangle { + // Extend header bottom border + anchors { + top: parent.top + topMargin: hifi.dimensions.tableHeaderHeight - 1 + left: parent.left + right: parent.right + } + height: 1 + color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + visible: headerVisible } - radius: 4 - color: hifi.colors.tableScrollBackground } } @@ -99,85 +140,11 @@ TableView { } rowDelegate: Rectangle { - height: (styleData.selected ? 1.8 : 1) * hifi.dimensions.tableRowHeight + height: (styleData.selected && expandSelectedRow ? 1.8 : 1) * hifi.dimensions.tableRowHeight color: styleData.selected ? hifi.colors.primaryHighlight : tableView.isLightColorScheme ? (styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd) : (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd) } - - itemDelegate: Item { - anchors { - left: parent ? parent.left : undefined - leftMargin: hifi.dimensions.tablePadding - right: parent ? parent.right : undefined - rightMargin: hifi.dimensions.tablePadding - } - - FiraSansSemiBold { - id: textItem - text: styleData.value - size: hifi.fontSizes.tableText - color: colorScheme == hifi.colorSchemes.light - ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) - : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) - anchors { - left: parent.left - right: parent.right - top: parent.top - topMargin: 3 - } - - // FIXME: Put reload item in tableModel passed in from RunningScripts. - HiFiGlyphs { - id: reloadButton - text: hifi.glyphs.reloadSmall - color: reloadButtonArea.pressed ? hifi.colors.white : parent.color - anchors { - top: parent.top - right: stopButton.left - verticalCenter: parent.verticalCenter - } - MouseArea { - id: reloadButtonArea - anchors { fill: parent; margins: -2 } - onClicked: reloadScript(model.url) - } - } - - // FIXME: Put stop item in tableModel passed in from RunningScripts. - HiFiGlyphs { - id: stopButton - text: hifi.glyphs.closeSmall - color: stopButtonArea.pressed ? hifi.colors.white : parent.color - anchors { - top: parent.top - right: parent.right - verticalCenter: parent.verticalCenter - } - MouseArea { - id: stopButtonArea - anchors { fill: parent; margins: -2 } - onClicked: stopScript(model.url) - } - } - } - - // FIXME: Automatically use aux. information from tableModel - FiraSansSemiBold { - text: tableModel.get(styleData.row) ? tableModel.get(styleData.row).url : "" - elide: Text.ElideMiddle - size: hifi.fontSizes.tableText - color: colorScheme == hifi.colorSchemes.light - ? (styleData.selected ? hifi.colors.black : hifi.colors.lightGray) - : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) - anchors { - top: textItem.bottom - left: parent.left - right: parent.right - } - visible: styleData.selected - } - } } diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 1f710b4ef5..ac0858c134 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -28,6 +28,7 @@ ModalWindow { //resizable: true implicitWidth: 640 implicitHeight: 480 + HifiConstants { id: hifi } Settings { @@ -184,8 +185,9 @@ ModalWindow { } } - FileTableView { + Table { id: fileTableView + colorScheme: hifi.colorSchemes.light anchors { top: navControls.bottom topMargin: hifi.dimensions.contentSpacing.y @@ -194,10 +196,12 @@ ModalWindow { bottom: currentSelection.top bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height } + headerVisible: true onDoubleClicked: navigateToRow(row); focus: true Keys.onReturnPressed: navigateToCurrentRow(); Keys.onEnterPressed: navigateToCurrentRow(); + model: FolderListModel { id: model nameFilters: selectionType.currentFilter @@ -218,6 +222,76 @@ ModalWindow { } } + onActiveFocusChanged: { + if (activeFocus && currentRow == -1) { + fileTableView.selection.select(0) + } + } + + itemDelegate: Item { + clip: true + + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } + + FiraSansSemiBold { + text: getText(); + elide: styleData.elideMode + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.tableText + color: hifi.colors.baseGrayHighlight + font.family: fileTableView.model.get(styleData.row, "fileIsDir") ? firaSansSemiBold.name : firaSansRegular.name + + function getText() { + switch (styleData.column) { + case 1: return fileTableView.model.get(styleData.row, "fileIsDir") ? "" : styleData.value; + case 2: return fileTableView.model.get(styleData.row, "fileIsDir") ? "" : formatSize(styleData.value); + default: return styleData.value; + } + } + function formatSize(size) { + var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; + var suffixIndex = 0 + while ((size / 1024.0) > 1.1) { + size /= 1024.0; + ++suffixIndex; + } + + size = Math.round(size*1000)/1000; + size = size.toLocaleString() + + return size + " " + suffixes[suffixIndex]; + } + } + } + + TableViewColumn { + id: fileNameColumn + role: "fileName" + title: "Name" + width: 0.5 * fileTableView.width + resizable: true + } + TableViewColumn { + id: fileMofifiedColumn + role: "fileModified" + title: "Date" + width: 0.3 * fileTableView.width + resizable: true + } + TableViewColumn { + role: "fileSize" + title: "Size" + width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width + resizable: true + } + function navigateToRow(row) { currentRow = row; navigateToCurrentRow(); diff --git a/interface/resources/qml/dialogs/fileDialog/FileTableView.qml b/interface/resources/qml/dialogs/fileDialog/FileTableView.qml deleted file mode 100644 index d51f9e1ed9..0000000000 --- a/interface/resources/qml/dialogs/fileDialog/FileTableView.qml +++ /dev/null @@ -1,79 +0,0 @@ -// -// FileDialog.qml -// -// Created by Bradley Austin Davis on 22 Jan 2016 -// Copyright 2015 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.Controls 1.4 - -TableView { - id: root - onActiveFocusChanged: { - if (activeFocus && currentRow == -1) { - root.selection.select(0) - } - } - //horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - - itemDelegate: Component { - Item { - clip: true - Text { - x: 3 - anchors.verticalCenter: parent.verticalCenter - color: styleData.textColor - elide: styleData.elideMode - text: getText(); - font.italic: root.model.get(styleData.row, "fileIsDir") ? true : false - - function getText() { - switch (styleData.column) { - //case 1: return Date.fromLocaleString(locale, styleData.value, "yyyy-MM-dd hh:mm:ss"); - case 1: return root.model.get(styleData.row, "fileIsDir") ? "" : styleData.value; - case 2: return root.model.get(styleData.row, "fileIsDir") ? "" : formatSize(styleData.value); - default: return styleData.value; - } - } - function formatSize(size) { - var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; - var suffixIndex = 0 - while ((size / 1024.0) > 1.1) { - size /= 1024.0; - ++suffixIndex; - } - - size = Math.round(size*1000)/1000; - size = size.toLocaleString() - - return size + " " + suffixes[suffixIndex]; - } - } - } - } - - TableViewColumn { - id: fileNameColumn - role: "fileName" - title: "Name" - width: Math.floor(0.55 * parent.width) - resizable: true - } - TableViewColumn { - id: fileMofifiedColumn - role: "fileModified" - title: "Date" - width: Math.floor(0.3 * parent.width) - resizable: true - } - TableViewColumn { - role: "fileSize" - title: "Size" - width: Math.floor(0.15 * parent.width) - 16 - 2 // Allow space for vertical scrollbar and borders - resizable: true - } -} diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index cb4d2157ca..4d5052f086 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -89,12 +89,16 @@ Item { readonly property color transparent: "#00ffffff" // Control specific colors - readonly property color tableRowLightOdd: white50 - readonly property color tableRowLightEven: "#1a575757" - readonly property color tableRowDarkOdd: "#80393939" - readonly property color tableRowDarkEven: "#a6181818" - readonly property color tableScrollHandle: "#707070" - readonly property color tableScrollBackground: "#323232" + readonly property color tableRowLightOdd: "#eaeaea" // Equivalent to white50 over #e3e3e3 background + readonly property color tableRowLightEven: "#c6c6c6" // Equivavlent to "#1a575757" over #e3e3e3 background + readonly property color tableRowDarkOdd: "#2e2e2e" // Equivalent to "#80393939" over #404040 background + readonly property color tableRowDarkEven: "#1c1c1c" // Equivalent to "#a6181818" over #404040 background + readonly property color tableBackgroundLight: tableRowLightEven + readonly property color tableBackgroundDark: tableRowDarkEven + readonly property color tableScrollHandleLight: tableRowLightOdd + readonly property color tableScrollHandleDark: "#707070" + readonly property color tableScrollBackgroundLight: tableRowLightEven + readonly property color tableScrollBackgroundDark: "#323232" readonly property color checkboxLightStart: "#ffffff" readonly property color checkboxLightFinish: "#afafaf" readonly property color checkboxDarkStart: "#7d7d7d" @@ -140,7 +144,7 @@ Item { readonly property real spinnerSize: 42 readonly property real tablePadding: 12 readonly property real tableRowHeight: largeScreen ? 26 : 23 - readonly property real tableHeaderHeight: 40 + readonly property real tableHeaderHeight: 29 readonly property vector2d modalDialogMargin: Qt.vector2d(50, 30) readonly property real modalDialogTitleHeight: 40 readonly property real controlLineHeight: 28 // Height of spinbox control on 1920 x 1080 monitor @@ -157,6 +161,7 @@ Item { readonly property real textFieldInput: dimensions.largeScreen ? 15 : 12 readonly property real textFieldInputLabel: dimensions.largeScreen ? 13 : 9 readonly property real textFieldSearchIcon: dimensions.largeScreen ? 30 : 24 + readonly property real tableHeading: dimensions.largeScreen ? 12 : 10 readonly property real tableText: dimensions.largeScreen ? 15 : 12 readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9 readonly property real iconButton: dimensions.largeScreen ? 13 : 9 From b6fcb77d6f76306ad2a4022c2430b7656e9583fc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 May 2016 17:16:56 +1200 Subject: [PATCH 31/77] Update Running Scripts list to use updated QML table control --- .../qml/hifi/dialogs/RunningScripts.qml | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index 071789fe16..31bb553809 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -118,11 +118,89 @@ Window { } HifiControls.Table { - tableModel: runningScriptsModel + model: runningScriptsModel + id: table height: 185 colorScheme: hifi.colorSchemes.dark anchors.left: parent.left anchors.right: parent.right + expandSelectedRow: true + + itemDelegate: Item { + anchors { + left: parent ? parent.left : undefined + leftMargin: hifi.dimensions.tablePadding + right: parent ? parent.right : undefined + rightMargin: hifi.dimensions.tablePadding + } + + FiraSansSemiBold { + id: textItem + text: styleData.value + size: hifi.fontSizes.tableText + color: table.colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: 3 + } + + HiFiGlyphs { + id: reloadButton + text: hifi.glyphs.reloadSmall + color: reloadButtonArea.pressed ? hifi.colors.white : parent.color + anchors { + top: parent.top + right: stopButton.left + verticalCenter: parent.verticalCenter + } + MouseArea { + id: reloadButtonArea + anchors { fill: parent; margins: -2 } + onClicked: reloadScript(model.url) + } + } + + HiFiGlyphs { + id: stopButton + text: hifi.glyphs.closeSmall + color: stopButtonArea.pressed ? hifi.colors.white : parent.color + anchors { + top: parent.top + right: parent.right + verticalCenter: parent.verticalCenter + } + MouseArea { + id: stopButtonArea + anchors { fill: parent; margins: -2 } + onClicked: stopScript(model.url) + } + } + + } + + FiraSansSemiBold { + text: runningScriptsModel.get(styleData.row) ? runningScriptsModel.get(styleData.row).url : "" + elide: Text.ElideMiddle + size: hifi.fontSizes.tableText + color: table.colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.lightGray) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + anchors { + top: textItem.bottom + left: parent.left + right: parent.right + } + visible: styleData.selected + } + } + + TableViewColumn { + role: "name" + } } HifiControls.VerticalSpacer { From d798e562fc7b101c836e0ef74f711b09c507f5a9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 May 2016 17:18:30 +1200 Subject: [PATCH 32/77] Update scrollbar in tree view to match table scrollbar Used in Running Scripts and Asset Browser dialogs. --- interface/resources/qml/controls-uit/Tree.qml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/controls-uit/Tree.qml b/interface/resources/qml/controls-uit/Tree.qml index 6d4d202e2c..aa1d10f030 100644 --- a/interface/resources/qml/controls-uit/Tree.qml +++ b/interface/resources/qml/controls-uit/Tree.qml @@ -1,5 +1,5 @@ // -// Table.qml +// Tree.qml // // Created by David Rowe on 17 Feb 2016 // Copyright 2016 High Fidelity, Inc. @@ -85,27 +85,18 @@ TreeView { bottomMargin: 3 // "" } radius: 3 - color: hifi.colors.tableScrollHandle + color: hifi.colors.tableScrollHandleDark } } scrollBarBackground: Item { - implicitWidth: 10 + implicitWidth: 9 Rectangle { anchors { fill: parent margins: -1 // Expand } - color: hifi.colors.baseGrayHighlight - } - - Rectangle { - anchors { - fill: parent - margins: 1 // Shrink - } - radius: 4 - color: hifi.colors.tableScrollBackground + color: hifi.colors.tableBackgroundDark } } From 7be33a85848fe9434201ff2ece0602604ae788a0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 May 2016 17:25:44 +1200 Subject: [PATCH 33/77] Don't show header or dates and sizes when choosing a directory --- interface/resources/qml/dialogs/FileDialog.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index ac0858c134..93c89e7393 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -196,7 +196,7 @@ ModalWindow { bottom: currentSelection.top bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height } - headerVisible: true + headerVisible: !selectDirectory onDoubleClicked: navigateToRow(row); focus: true Keys.onReturnPressed: navigateToCurrentRow(); @@ -275,7 +275,7 @@ ModalWindow { id: fileNameColumn role: "fileName" title: "Name" - width: 0.5 * fileTableView.width + width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width resizable: true } TableViewColumn { @@ -284,12 +284,14 @@ ModalWindow { title: "Date" width: 0.3 * fileTableView.width resizable: true + visible: !selectDirectory } TableViewColumn { role: "fileSize" title: "Size" width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width resizable: true + visible: !selectDirectory } function navigateToRow(row) { From 30fb9349c46727a5700b4ca9e54a83eaa7767198 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 12 May 2016 17:52:40 -0700 Subject: [PATCH 34/77] dry up some code --- .../src/RenderableModelEntityItem.cpp | 50 +++++++++---------- .../src/RenderableModelEntityItem.h | 2 + 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index c4ac9b09e5..a9aee1e527 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -162,6 +162,19 @@ void RenderableModelEntityItem::remapTextures() { } } +void RenderableModelEntityItem::doInitialModelSimulation() { + _model->setScaleToFit(true, getDimensions()); + _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); + _model->setRotation(getRotation()); + _model->setTranslation(getPosition()); + { + PerformanceTimer perfTimer("_model->simulate"); + _model->simulate(0.0f); + } + _needsInitialSimulation = false; +} + + // TODO: we need a solution for changes to the postion/rotation/etc of a model... // this current code path only addresses that in this setup case... not the changing/moving case bool RenderableModelEntityItem::readyToAddToScene(RenderArgs* renderArgs) { @@ -172,22 +185,12 @@ bool RenderableModelEntityItem::readyToAddToScene(RenderArgs* renderArgs) { getModel(renderer); } if (renderArgs && _model && _needsInitialSimulation && _model->isActive() && _model->isLoaded()) { - _model->setScaleToFit(true, getDimensions()); - _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); - _model->setRotation(getRotation()); - _model->setTranslation(getPosition()); - // make sure to simulate so everything gets set up correctly for rendering - { - PerformanceTimer perfTimer("_model->simulate"); - _model->simulate(0.0f); - } - _needsInitialSimulation = false; - + doInitialModelSimulation(); _model->renderSetup(renderArgs); } bool ready = !_needsInitialSimulation && _model && _model->readyToAddToScene(renderArgs); - return ready; + return ready; } class RenderableModelEntityItemMeta { @@ -348,18 +351,7 @@ void RenderableModelEntityItem::updateModelBounds() { _model->getRotation() != getRotation() || _model->getRegistrationPoint() != getRegistrationPoint()) && _model->isActive() && _dimensionsInitialized) { - _model->setScaleToFit(true, dimensions); - _model->setSnapModelToRegistrationPoint(true, getRegistrationPoint()); - _model->setRotation(getRotation()); - _model->setTranslation(getPosition()); - - // make sure to simulate so everything gets set up correctly for rendering - { - PerformanceTimer perfTimer("_model->simulate"); - _model->simulate(0.0f); - } - - _needsInitialSimulation = false; + doInitialModelSimulation(); _needsJointSimulation = false; } } @@ -592,8 +584,7 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { if (_needsInitialSimulation) { // the _model's offset will be wrong until _needsInitialSimulation is false PerformanceTimer perfTimer("_model->simulate"); - _model->simulate(0.0f); - _needsInitialSimulation = false; + doInitialModelSimulation(); } return true; @@ -807,6 +798,13 @@ void RenderableModelEntityItem::locationChanged(bool tellPhysics) { if (_model && _model->isActive()) { _model->setRotation(getRotation()); _model->setTranslation(getPosition()); + + // { + // render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + // render::PendingChanges pendingChanges; + // pendingChanges.updateItem(_myMetaItem, [](RenderableModelEntityItemMeta& data){}); + // scene->enqueuePendingChanges(pendingChanges); + // } } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 59208d209d..5fd767c6ee 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -38,6 +38,8 @@ public: EntityPropertyFlags& propertyFlags, bool overwriteLocalData, bool& somethingChanged) override; + void doInitialModelSimulation(); + virtual bool readyToAddToScene(RenderArgs* renderArgs = nullptr); virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; From 3b34b908d7f066635b055baa6c32dc9f78eb5c78 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 13 May 2016 17:39:15 +1200 Subject: [PATCH 35/77] Tweak level-up and home buttons --- interface/resources/qml/controls-uit/GlyphButton.qml | 7 +++++++ interface/resources/qml/dialogs/FileDialog.qml | 2 ++ 2 files changed, 9 insertions(+) diff --git a/interface/resources/qml/controls-uit/GlyphButton.qml b/interface/resources/qml/controls-uit/GlyphButton.qml index 2625dda723..ac353b5a52 100644 --- a/interface/resources/qml/controls-uit/GlyphButton.qml +++ b/interface/resources/qml/controls-uit/GlyphButton.qml @@ -19,6 +19,7 @@ Original.Button { property int color: 0 property int colorScheme: hifi.colorSchemes.light property string glyph: "" + property int size: 32 width: 120 height: 28 @@ -65,7 +66,13 @@ Original.Button { : hifi.buttons.disabledTextColor[control.colorScheme] verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + anchors { + // Tweak horizontal alignment so that it looks right. + left: parent.left + leftMargin: -0.5 + } text: control.glyph + size: control.size } } } diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 93c89e7393..1175c8a14f 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -99,6 +99,7 @@ ModalWindow { id: upButton glyph: hifi.glyphs.levelUp width: height + size: 30 enabled: model.parentFolder && model.parentFolder !== "" onClicked: d.navigateUp(); } @@ -107,6 +108,7 @@ ModalWindow { id: homeButton property var destination: helper.home(); glyph: hifi.glyphs.home + size: 28 width: height enabled: d.homeDestination ? true : false onClicked: d.navigateHome(); From efe02c0fa120666b5296d0e559e272b67b2fb412 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 13 May 2016 10:57:41 -0700 Subject: [PATCH 36/77] make lock/unlock button easier to see --- scripts/system/assets/images/lock.svg | 32 ++++++++++++++++++----- scripts/system/assets/images/unlock.svg | 30 +++++++++++++++++---- scripts/system/attachedEntitiesManager.js | 4 +-- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/scripts/system/assets/images/lock.svg b/scripts/system/assets/images/lock.svg index 1480359f43..bb9658de00 100644 --- a/scripts/system/assets/images/lock.svg +++ b/scripts/system/assets/images/lock.svg @@ -2,11 +2,13 @@ + id="defs4"> + + + + + image/svg+xml - + @@ -54,15 +74,15 @@ id="layer1" transform="translate(0,-825.59055)"> diff --git a/scripts/system/assets/images/unlock.svg b/scripts/system/assets/images/unlock.svg index a7664c1f59..789a8b0ed5 100644 --- a/scripts/system/assets/images/unlock.svg +++ b/scripts/system/assets/images/unlock.svg @@ -2,11 +2,13 @@ + id="defs4"> + + + + + image/svg+xml - + @@ -54,14 +74,14 @@ id="layer1" transform="translate(0,-825.59055)"> Date: Sat, 14 May 2016 13:04:18 +1200 Subject: [PATCH 37/77] Add icon to title --- interface/resources/qml/dialogs/FileDialog.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 1175c8a14f..a3c5c02f9b 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -49,6 +49,8 @@ ModalWindow { // Set from OffscreenUi::getOpenFile() property int options; // <-- FIXME unused + property string iconText: text !== "" ? hifi.glyphs.scriptUpload : "" + property int iconSize: 40 property bool selectDirectory: false; property bool showHidden: false; @@ -69,6 +71,8 @@ ModalWindow { drivesSelector.onCurrentTextChanged.connect(function(){ root.dir = helper.pathToUrl(drivesSelector.currentText); }) + + iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; } Item { From 14c733cd6e382fc4db961bc6c72eec5d937aa87d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 14 May 2016 17:19:18 +1200 Subject: [PATCH 38/77] Make columns sortable --- .../resources/qml/controls-uit/Table.qml | 18 +++++++++++++++-- .../resources/qml/dialogs/FileDialog.qml | 20 ++++++++++++++++++- .../qml/styles-uit/HifiConstants.qml | 1 + 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index 2a0fe545ef..35029ad8bf 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -29,6 +29,7 @@ TableView { color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark RalewayRegular { + id: titleText text: styleData.value size: hifi.fontSizes.tableHeading font.capitalization: Font.AllUppercase @@ -36,12 +37,25 @@ TableView { anchors { left: parent.left leftMargin: hifi.dimensions.tablePadding - right: parent.right - rightMargin: hifi.dimensions.tablePadding verticalCenter: parent.verticalCenter } } + HiFiGlyphs { + id: titleSort + text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn + color: hifi.colors.baseGrayHighlight + size: hifi.fontSizes.tableHeadingIcon + anchors { + left: titleText.right + leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: titleText.verticalCenter + } + visible: sortIndicatorVisible && sortIndicatorColumn === styleData.column + } + Rectangle { width: 1 anchors { diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index a3c5c02f9b..f185508457 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -208,6 +208,10 @@ ModalWindow { Keys.onReturnPressed: navigateToCurrentRow(); Keys.onEnterPressed: navigateToCurrentRow(); + sortIndicatorColumn: 0 + sortIndicatorOrder: Qt.AscendingOrder + sortIndicatorVisible: true + model: FolderListModel { id: model nameFilters: selectionType.currentFilter @@ -228,7 +232,18 @@ ModalWindow { } } - onActiveFocusChanged: { + function updateSort() { + model.sortReversed = sortIndicatorColumn == 0 + ? (sortIndicatorOrder == Qt.DescendingOrder) + : (sortIndicatorOrder == Qt.AscendingOrder); // Date and size fields have opposite sense + model.sortField = sortIndicatorColumn + 1; + } + + onSortIndicatorColumnChanged: { updateSort(); } + + onSortIndicatorOrderChanged: { updateSort(); } + + onActiveFocusChanged: { if (activeFocus && currentRow == -1) { fileTableView.selection.select(0) } @@ -282,6 +297,7 @@ ModalWindow { role: "fileName" title: "Name" width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width + movable: false resizable: true } TableViewColumn { @@ -289,6 +305,7 @@ ModalWindow { role: "fileModified" title: "Date" width: 0.3 * fileTableView.width + movable: false resizable: true visible: !selectDirectory } @@ -296,6 +313,7 @@ ModalWindow { role: "fileSize" title: "Size" width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width + movable: false resizable: true visible: !selectDirectory } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 4d5052f086..640fe8625b 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -162,6 +162,7 @@ Item { readonly property real textFieldInputLabel: dimensions.largeScreen ? 13 : 9 readonly property real textFieldSearchIcon: dimensions.largeScreen ? 30 : 24 readonly property real tableHeading: dimensions.largeScreen ? 12 : 10 + readonly property real tableHeadingIcon: dimensions.largeScreen ? 40 : 33 readonly property real tableText: dimensions.largeScreen ? 15 : 12 readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9 readonly property real iconButton: dimensions.largeScreen ? 13 : 9 From 880b52aad288ec91d73fdfc478acbe57a110eb3e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 16 May 2016 09:52:48 -0700 Subject: [PATCH 39/77] update attachedEntitiesManager to work better with avatarEntities --- scripts/system/attachedEntitiesManager.js | 104 ++++++++++++---------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/scripts/system/attachedEntitiesManager.js b/scripts/system/attachedEntitiesManager.js index 780cfe06fb..79c5973a6e 100644 --- a/scripts/system/attachedEntitiesManager.js +++ b/scripts/system/attachedEntitiesManager.js @@ -117,10 +117,11 @@ function AttachedEntitiesManager() { this.handleEntityRelease = function(grabbedEntity, releasedFromJoint) { // if this is still equipped, just rewrite the position information. var grabData = getEntityCustomData('grabKey', grabbedEntity, {}); - if ("refCount" in grabData && grabData.refCount > 0) { - manager.updateRelativeOffsets(grabbedEntity); - return; - } + // if ("refCount" in grabData && grabData.refCount > 0) { + // // for adjusting things in your other hand + // manager.updateRelativeOffsets(grabbedEntity); + // return; + // } var allowedJoints = getEntityCustomData('wearable', grabbedEntity, DEFAULT_WEARABLE_DATA).joints; @@ -156,9 +157,11 @@ function AttachedEntitiesManager() { wearProps.parentID = MyAvatar.sessionUUID; wearProps.parentJointIndex = bestJointIndex; + var updatePresets = false; if (bestJointOffset && bestJointOffset.constructor === Array) { if (!clothingLocked || bestJointOffset.length < 2) { - this.updateRelativeOffsets(grabbedEntity); + // we're unlocked or this thing didn't have a preset position, so update it + updatePresets = true; } else { // don't snap the entity to the preferred position if unlocked wearProps.localPosition = bestJointOffset[0]; @@ -167,7 +170,10 @@ function AttachedEntitiesManager() { } Entities.deleteEntity(grabbedEntity); - Entities.addEntity(wearProps, true); + grabbedEntity = Entities.addEntity(wearProps, true); + if (updatePresets) { + this.updateRelativeOffsets(grabbedEntity); + } } else if (props.parentID != NULL_UUID) { // drop the entity and set it to have no parent (not on the avatar), unless it's being equipped in a hand. if (props.parentID === MyAvatar.sessionUUID && @@ -225,20 +231,20 @@ function AttachedEntitiesManager() { } } - this.saveAttachedEntities = function() { - print("--- saving attached entities ---"); - saveData = []; - var nearbyEntities = Entities.findEntities(MyAvatar.position, ATTACHED_ENTITY_SEARCH_DISTANCE); - for (i = 0; i < nearbyEntities.length; i++) { - var entityID = nearbyEntities[i]; - if (this.updateRelativeOffsets(entityID)) { - var props = Entities.getEntityProperties(entityID); // refresh, because updateRelativeOffsets changed them - this.scrubProperties(props); - saveData.push(props); - } - } - Settings.setValue(ATTACHED_ENTITIES_SETTINGS_KEY, JSON.stringify(saveData)); - } + // this.saveAttachedEntities = function() { + // print("--- saving attached entities ---"); + // saveData = []; + // var nearbyEntities = Entities.findEntities(MyAvatar.position, ATTACHED_ENTITY_SEARCH_DISTANCE); + // for (i = 0; i < nearbyEntities.length; i++) { + // var entityID = nearbyEntities[i]; + // if (this.updateRelativeOffsets(entityID)) { + // var props = Entities.getEntityProperties(entityID); // refresh, because updateRelativeOffsets changed them + // this.scrubProperties(props); + // saveData.push(props); + // } + // } + // Settings.setValue(ATTACHED_ENTITIES_SETTINGS_KEY, JSON.stringify(saveData)); + // } this.scrubProperties = function(props) { var toScrub = ["queryAACube", "position", "rotation", @@ -262,37 +268,37 @@ function AttachedEntitiesManager() { } } - this.loadAttachedEntities = function(grabbedEntity) { - print("--- loading attached entities ---"); - jsonAttachmentData = Settings.getValue(ATTACHED_ENTITIES_SETTINGS_KEY); - var loadData = []; - try { - loadData = JSON.parse(jsonAttachmentData); - } catch (e) { - print('error parsing saved attachment data'); - return; - } + // this.loadAttachedEntities = function(grabbedEntity) { + // print("--- loading attached entities ---"); + // jsonAttachmentData = Settings.getValue(ATTACHED_ENTITIES_SETTINGS_KEY); + // var loadData = []; + // try { + // loadData = JSON.parse(jsonAttachmentData); + // } catch (e) { + // print('error parsing saved attachment data'); + // return; + // } - for (i = 0; i < loadData.length; i ++) { - var savedProps = loadData[ i ]; - var currentProps = Entities.getEntityProperties(savedProps.id); - if (currentProps.id == savedProps.id && - // TODO -- also check that parentJointIndex matches? - currentProps.parentID == MyAvatar.sessionUUID) { - // entity is already in-world. TODO -- patch it up? - continue; - } - this.scrubProperties(savedProps); - delete savedProps["id"]; - savedProps.parentID = MyAvatar.sessionUUID; // this will change between sessions - var loadedEntityID = Entities.addEntity(savedProps, true); + // for (i = 0; i < loadData.length; i ++) { + // var savedProps = loadData[ i ]; + // var currentProps = Entities.getEntityProperties(savedProps.id); + // if (currentProps.id == savedProps.id && + // // TODO -- also check that parentJointIndex matches? + // currentProps.parentID == MyAvatar.sessionUUID) { + // // entity is already in-world. TODO -- patch it up? + // continue; + // } + // this.scrubProperties(savedProps); + // delete savedProps["id"]; + // savedProps.parentID = MyAvatar.sessionUUID; // this will change between sessions + // var loadedEntityID = Entities.addEntity(savedProps, true); - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'loaded', - grabbedEntity: loadedEntityID - })); - } - } + // Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + // action: 'loaded', + // grabbedEntity: loadedEntityID + // })); + // } + // } } var manager = new AttachedEntitiesManager(); From 1fc3d9229c9735e445533f678a1b7777df65de81 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 17 May 2016 10:51:42 +1200 Subject: [PATCH 40/77] Fix initial drive change and go-up not working properly on first screen --- interface/resources/qml/dialogs/FileDialog.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index f185508457..9c68767e2b 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -72,6 +72,12 @@ ModalWindow { root.dir = helper.pathToUrl(drivesSelector.currentText); }) + // HACK: The following two lines force the model to initialize properly such that: + // - Selecting a different drive at the initial screen updates the path displayed. + // - The go-up button works properly from the initial screen. + root.dir = helper.pathToUrl(drivesSelector.currentText); + root.dir = helper.pathToUrl(currentDirectory.lastValidFolder); + iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; } From e0483585b85822a4d6220fefc8e08683a89d236f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 17 May 2016 14:43:49 +1200 Subject: [PATCH 41/77] Consistently capitalize drive letter for Windows --- interface/resources/qml/dialogs/FileDialog.qml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 9c68767e2b..e8b6b9b2b7 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -144,7 +144,16 @@ ModalWindow { leftMargin: hifi.dimensions.contentSpacing.x right: parent.right } - onLastValidFolderChanged: text = lastValidFolder; + + function capitalizeDrive(path) { + // Consistently capitalize drive letter for Windows. + if (/[a-zA-Z]:/.test(path)) { + return path.charAt(0).toUpperCase() + path.slice(1); + } + return path; + } + + onLastValidFolderChanged: text = capitalizeDrive(lastValidFolder); // FIXME add support auto-completion onAccepted: { From 17d41dd7b977c833e97961c64df3752ed574a73b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 18 May 2016 13:25:11 +1200 Subject: [PATCH 42/77] HiFi glyphs font file update with fixed up/down carats Update QML and HTML spinboxes accordingly. --- interface/resources/fonts/hifi-glyphs.ttf | Bin 23944 -> 24204 bytes .../resources/qml/controls-uit/SpinBox.qml | 6 +++--- .../qml/styles-uit/HifiConstants.qml | 2 +- scripts/system/html/edit-style.css | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 1b0d4f3fe61a60a3cf499f3557dd29bb924bc924..89d47670122b68a27ce80d77da0227390d817ac1 100644 GIT binary patch delta 960 zcmYL|TTGi}7{`Ba`yriL4&&T@UxC6X9V?WwavHFXL17!GaVYJ8!pE_&GDtz0GsrG> zu`Ih-{H88*F_Q~ixG-j&-iTg!VUR4*L?b3M1e3)qlPfc$$zm|=miQ!Z-v9g0`#gCM zzvt?g;`&!&1pyejjGiv9x3_(Px3?fT56CS|&0cQt3Iw}=yw097SLa4s2M9ml8=F<< z`~F?;0NoAHS*O&6`Tc$L5NrhuQ{lHJo$}rqBwUB?kkN-@vaxWhgYUqDoG& z3nCpT49zZH%zm2o9f;3>o$%b4S~l2R1kwZGms$1VJewj2qB?e<3+n8|AC;Rv=-&eo zou6A+Z2mCmJqX+dX)S&^0jW=3T^dfV`-|6f#NtOay!>L;g_qwI_2RtNAcn*N?T#4I zo{JG}SXWx}1O05X9Z%3he8};N2pN4`$w;yH!+?PorQg{-pl#@smOAQb;5a8}qsG*i>`sv~^VHQh96!-`@^rJe@6?&Lc2~EfqjP$u zwaHUcTixGv)-Nl$Q+fH;g2F>ZwqpC?k|UL+WzO=dilgqHq23nXG0iBMq(E?BP%D?N zXdcO+eJ<6lIA8zPc*NLb3>#OCJ4vRb_2iD^pHfCsK22>+T~Av~dy?);|04ZqhLjP> zxSx4Ab24)^>tI$m>+9^U?DunAIk!zIrbg3M)6Zs$dD6UL{#~}nL3vC5N0F2irB)eL zZYVnzmu1v))ABg?UY;-SL4Gj*8|!XCcfnra{q!Ph(L-CC?UHS?_;T@Gd$zsM?u=eZ z_#i7fpj@*?Gd#PQT8`q;?B#ztws|BR;$ZBT4%sS%uEh7K3Iy9c77k&K`68i_TZ5K delta 954 zcmY+CTTGjE7{-79wm`=y$I{ZnR|iUW=)K7P7OD@g{aX8H`CySUJ!PN#%MB3nK!=4oA=3+H&31??;Cw0 zZao&O2*ALbXz6Wh8|;pJxV;X+dBEP@(%Qy-5+T?HIC};>-k#69xxOTunxNSkHQJ~?aPn0CerHu;JAi({a9|F9h{X3#w%&9&R6ngcc2#W93zl5UQ zVMh&UJRah4j@o{thOnYNLUwe#jK@UJE7=>ndi5)fv|5jk2F}q$Gc8=8jdlWb(nU8H zDd!TW>7kcC`Z-T4jns0MGmJ36Afwok$i+z>`4k{iND(fIIl)OvIE9;PJd{#~ml`Ul zqzXTm8KQ$>>eMl9rDY6-aV7|Ig-NEEW`d)s>>o^>rkkcsv)3Fo|B@5V`NYz1`OI2sy<%Or z{%P~sZrYyP6YW0x4f~#yBgs;!R3~*v5$S>Sjl<-ab3AZt<=S(1o$omJ^X}#SU65le zxGl@_UeSn4=UQ_;DyH~Th(#0bo7H#yzF3|V&G!g)cq-h2_zeqPMEcZKR6zANU`&Ea*u7!ZTv68UOmL{{G$ z`A45LvlMwYO diff --git a/interface/resources/qml/controls-uit/SpinBox.qml b/interface/resources/qml/controls-uit/SpinBox.qml index 599d94c28d..5d78cfc5a8 100755 --- a/interface/resources/qml/controls-uit/SpinBox.qml +++ b/interface/resources/qml/controls-uit/SpinBox.qml @@ -56,7 +56,7 @@ SpinBox { incrementControl: HiFiGlyphs { id: incrementButton text: hifi.glyphs.caratUp - x: 6 + x: 10 y: 1 size: hifi.dimensions.spinnerSize color: styleData.upPressed ? (isLightColorScheme ? hifi.colors.black : hifi.colors.white) : hifi.colors.gray @@ -64,8 +64,8 @@ SpinBox { decrementControl: HiFiGlyphs { text: hifi.glyphs.caratDn - x: 6 - y: -3 + x: 10 + y: -1 size: hifi.dimensions.spinnerSize color: styleData.downPressed ? (isLightColorScheme ? hifi.colors.black : hifi.colors.white) : hifi.colors.gray } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 640fe8625b..16f150b2d9 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -141,7 +141,7 @@ Item { readonly property real textPadding: 8 readonly property real sliderHandleSize: 18 readonly property real sliderGrooveHeight: 8 - readonly property real spinnerSize: 42 + readonly property real spinnerSize: 50 readonly property real tablePadding: 12 readonly property real tableRowHeight: largeScreen ? 26 : 23 readonly property real tableHeaderHeight: 29 diff --git a/scripts/system/html/edit-style.css b/scripts/system/html/edit-style.css index 5eaa3c6497..4083e580b5 100644 --- a/scripts/system/html/edit-style.css +++ b/scripts/system/html/edit-style.css @@ -268,7 +268,7 @@ input[type=number]::-webkit-inner-spin-button { height: 100%; overflow: hidden; font-family: hifi-glyphs; - font-size: 50px; + font-size: 46px; color: #afafaf; cursor: pointer; /*background-color: #000000;*/ @@ -276,17 +276,17 @@ input[type=number]::-webkit-inner-spin-button { input[type=number]::-webkit-inner-spin-button:before, input[type=number]::-webkit-inner-spin-button:after { position:absolute; - left: -21px; + left: -19px; line-height: 8px; text-align: center; } input[type=number]::-webkit-inner-spin-button:before { content: "6"; - top: 5px; + top: 4px; } input[type=number]::-webkit-inner-spin-button:after { content: "5"; - bottom: 6px; + bottom: 4px; } input[type=number].hover-up::-webkit-inner-spin-button:before, From e1d885004f55b98681f94ebc4e92170cad584472 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 18 May 2016 13:59:13 +1200 Subject: [PATCH 43/77] Fix carat in drop-down boxes --- interface/resources/qml/controls-uit/ComboBox.qml | 2 +- scripts/system/html/edit-style.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index f99e18b12b..cd6dc8ede0 100755 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -91,7 +91,7 @@ FocusScope { HiFiGlyphs { anchors { top: parent.top - topMargin: -8 + topMargin: -11 horizontalCenter: parent.horizontalCenter } size: hifi.dimensions.spinnerSize diff --git a/scripts/system/html/edit-style.css b/scripts/system/html/edit-style.css index 4083e580b5..19d1cd95a9 100644 --- a/scripts/system/html/edit-style.css +++ b/scripts/system/html/edit-style.css @@ -613,7 +613,7 @@ hr { margin-right: -48px; position: relative; left: -12px; - top: -11px; + top: -9px; } .dropdown dd { From cf829f1babf58f4d442bdebdb8696f2c7f164fdf Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 17 May 2016 19:14:44 -0700 Subject: [PATCH 44/77] magic number --- interface/src/avatar/Avatar.cpp | 2 +- libraries/entities/src/SimulationOwner.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 95a3ef6132..efaed5b83a 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -220,7 +220,7 @@ void Avatar::updateAvatarEntities() { // there's no entity-server to tell us we're the simulation owner, so always set the // simulationOwner to the owningAvatarID and a high priority. - properties.setSimulationOwner(getID(), 129); + properties.setSimulationOwner(getID(), AVATAR_ENTITY_SIMULATION_PRIORITY); if (properties.getParentID() == AVATAR_SELF_ID) { properties.setParentID(getID()); diff --git a/libraries/entities/src/SimulationOwner.h b/libraries/entities/src/SimulationOwner.h index 1afec426d7..5f940bbe25 100644 --- a/libraries/entities/src/SimulationOwner.h +++ b/libraries/entities/src/SimulationOwner.h @@ -27,6 +27,7 @@ const quint8 RECRUIT_SIMULATION_PRIORITY = VOLUNTEER_SIMULATION_PRIORITY + 1; // When poking objects with scripts an observer will bid at SCRIPT_EDIT priority. const quint8 SCRIPT_GRAB_SIMULATION_PRIORITY = 0x80; const quint8 SCRIPT_POKE_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY - 1; +const quint8 AVATAR_ENTITY_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY + 1; // PERSONAL priority (needs a better name) is the level at which a simulation observer owns its own avatar // which really just means: things that collide with it will be bid at a priority level one lower From 551c6698835683a8dab27040904c9c1f711327fc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 19 May 2016 09:24:15 +1200 Subject: [PATCH 45/77] Move FolderListModel to be stand-alone --- .../resources/qml/dialogs/FileDialog.qml | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index e8b6b9b2b7..4f6e7ef050 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -43,7 +43,7 @@ ModalWindow { // Set from OffscreenUi::getOpenFile() property alias caption: root.title; // Set from OffscreenUi::getOpenFile() - property alias dir: model.folder; + property alias dir: folderListModel.folder; // Set from OffscreenUi::getOpenFile() property alias filter: selectionType.filtersString; // Set from OffscreenUi::getOpenFile() @@ -110,7 +110,7 @@ ModalWindow { glyph: hifi.glyphs.levelUp width: height size: 30 - enabled: model.parentFolder && model.parentFolder !== "" + enabled: folderListModel.parentFolder && folderListModel.parentFolder !== "" onClicked: d.navigateUp(); } @@ -135,7 +135,7 @@ ModalWindow { TextField { id: currentDirectory - property var lastValidFolder: helper.urlToPath(model.folder) + property var lastValidFolder: helper.urlToPath(folderListModel.folder) height: homeButton.height anchors { top: parent.top @@ -161,7 +161,7 @@ ModalWindow { text = lastValidFolder; return } - model.folder = helper.pathToUrl(text); + folderListModel.folder = helper.pathToUrl(text); } } @@ -172,7 +172,7 @@ ModalWindow { property bool currentSelectionIsFolder; property var backStack: [] property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } - property var modelConnection: Connections { target: model; onFolderChanged: d.update(); } + property var modelConnection: Connections { target: model; onFolderChanged: d.update(); } // DJRTODO property var homeDestination: helper.home(); Component.onCompleted: update(); @@ -194,18 +194,38 @@ ModalWindow { } function navigateUp() { - if (model.parentFolder && model.parentFolder !== "") { - model.folder = model.parentFolder + if (folderListModel.parentFolder && folderListModel.parentFolder !== "") { + folderListModel.folder = folderListModel.parentFolder return true; } } function navigateHome() { - model.folder = homeDestination; + folderListModel.folder = homeDestination; return true; } } + FolderListModel { + id: folderListModel + nameFilters: selectionType.currentFilter + showDirsFirst: true + showDotAndDotDot: false + showFiles: !root.selectDirectory + // For some reason, declaring these bindings directly in the targets doesn't + // work for setting the initial state + Component.onCompleted: { + currentDirectory.lastValidFolder = Qt.binding(function() { return helper.urlToPath(folder); }); + upButton.enabled = Qt.binding(function() { return (parentFolder && parentFolder != "") ? true : false; }); + showFiles = !root.selectDirectory + } + onFolderChanged: { + fileTableView.selection.clear(); + fileTableView.selection.select(0); + fileTableView.currentRow = 0; + } + } + Table { id: fileTableView colorScheme: hifi.colorSchemes.light @@ -227,25 +247,7 @@ ModalWindow { sortIndicatorOrder: Qt.AscendingOrder sortIndicatorVisible: true - model: FolderListModel { - id: model - nameFilters: selectionType.currentFilter - showDirsFirst: true - showDotAndDotDot: false - showFiles: !root.selectDirectory - // For some reason, declaring these bindings directly in the targets doesn't - // work for setting the initial state - Component.onCompleted: { - currentDirectory.lastValidFolder = Qt.binding(function() { return helper.urlToPath(model.folder); }); - upButton.enabled = Qt.binding(function() { return (model.parentFolder && model.parentFolder != "") ? true : false; }); - showFiles = !root.selectDirectory - } - onFolderChanged: { - fileTableView.selection.clear(); - fileTableView.selection.select(0); - fileTableView.currentRow = 0; - } - } + model: folderListModel function updateSort() { model.sortReversed = sortIndicatorColumn == 0 @@ -343,7 +345,7 @@ ModalWindow { var isFolder = model.isFolder(row); var file = model.get(row, "fileURL"); if (isFolder) { - fileTableView.model.folder = file + fileTableView.model.folder = file; } else { okAction.trigger(); } From 862a88cf292cdb2cd1e6712cf156db0d6793f427 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 19 May 2016 11:29:21 +1200 Subject: [PATCH 46/77] Insert an intermediary ListModel suitable for sorting and drives --- .../resources/qml/dialogs/FileDialog.qml | 59 ++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 4f6e7ef050..fbe8df79c0 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -184,7 +184,7 @@ ModalWindow { return; } - currentSelectionUrl = fileTableView.model.get(row, "fileURL"); + currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); currentSelectionIsFolder = fileTableView.model.isFolder(row); if (root.selectDirectory || !currentSelectionIsFolder) { currentSelection.text = helper.urlToPath(currentSelectionUrl); @@ -220,12 +220,49 @@ ModalWindow { showFiles = !root.selectDirectory } onFolderChanged: { + fileTableModel.update(); fileTableView.selection.clear(); fileTableView.selection.select(0); fileTableView.currentRow = 0; } } + ListModel { + id: fileTableModel + + // FolderListModel has a couple of problems: + // 1) Files and directories sort case-sensitively: https://bugreports.qt.io/browse/QTBUG-48757 + // 2) Cannot browse up to the "computer" level to view Windows drives: https://bugreports.qt.io/browse/QTBUG-42901 + // + // To solve these problems an intermediary ListModel is used that implements proper sorting and can be populated with + // drive information when viewing at the computer level. + + property var folder + + onFolderChanged: folderListModel.folder = folder; + + function isFolder(row) { + if (row === -1) { + return false; + } + return get(row).fileIsDir; + } + + function update() { + var i; + clear(); + for (i = 0; i < folderListModel.count; i++) { + append({ + fileName: folderListModel.get(i, "fileName"), + fileModified: folderListModel.get(i, "fileModified"), + fileSize: folderListModel.get(i, "fileSize"), + filePath: folderListModel.get(i, "filePath"), + fileIsDir: folderListModel.get(i, "fileIsDir") + }); + } + } + } + Table { id: fileTableView colorScheme: hifi.colorSchemes.light @@ -247,7 +284,7 @@ ModalWindow { sortIndicatorOrder: Qt.AscendingOrder sortIndicatorVisible: true - model: folderListModel + model: fileTableModel function updateSort() { model.sortReversed = sortIndicatorColumn == 0 @@ -284,12 +321,17 @@ ModalWindow { } size: hifi.fontSizes.tableText color: hifi.colors.baseGrayHighlight - font.family: fileTableView.model.get(styleData.row, "fileIsDir") ? firaSansSemiBold.name : firaSansRegular.name + font.family: (styleData.row !== -1 && fileTableView.model.get(styleData.row).fileIsDir) + ? firaSansSemiBold.name : firaSansRegular.name function getText() { + if (styleData.row === -1) { + return styleData.value; + } + switch (styleData.column) { - case 1: return fileTableView.model.get(styleData.row, "fileIsDir") ? "" : styleData.value; - case 2: return fileTableView.model.get(styleData.row, "fileIsDir") ? "" : formatSize(styleData.value); + case 1: return fileTableView.model.get(styleData.row).fileIsDir ? "" : styleData.value; + case 2: return fileTableView.model.get(styleData.row).fileIsDir ? "" : formatSize(styleData.value); default: return styleData.value; } } @@ -343,9 +385,9 @@ ModalWindow { function navigateToCurrentRow() { var row = fileTableView.currentRow var isFolder = model.isFolder(row); - var file = model.get(row, "fileURL"); + var file = model.get(row).filePath; if (isFolder) { - fileTableView.model.folder = file; + fileTableView.model.folder = helper.pathToUrl(file); } else { okAction.trigger(); } @@ -360,7 +402,7 @@ ModalWindow { var newPrefix = prefix + event.text.toLowerCase(); var matchedIndex = -1; for (var i = 0; i < model.count; ++i) { - var name = model.get(i, "fileName").toLowerCase(); + var name = model.get(i).fileName.toLowerCase(); if (0 === name.indexOf(newPrefix)) { matchedIndex = i; break; @@ -401,7 +443,6 @@ ModalWindow { } break; } - } } From 585467094c58e177c59e9da30b18e5b77bcf8d7b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 19 May 2016 14:16:00 +1200 Subject: [PATCH 47/77] Sort files case-insensitively, directories first --- .../resources/qml/dialogs/FileDialog.qml | 64 ++++++++++++++++--- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index fbe8df79c0..e7d96cedf9 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -238,6 +238,8 @@ ModalWindow { // drive information when viewing at the computer level. property var folder + property int sortOrder: Qt.AscendingOrder + property int sortColumn: 0 onFolderChanged: folderListModel.folder = folder; @@ -249,16 +251,61 @@ ModalWindow { } function update() { - var i; + var dataFields = ["fileName", "fileModified", "fileSize"], + sortFields = ["fileNameSort", "fileModified", "fileSize"], + dataField = dataFields[sortColumn], + sortField = sortFields[sortColumn], + sortValue, + fileName, + fileIsDir, + comparisonFunction, + lower, + middle, + upper, + rows = 0, + i; clear(); + + comparisonFunction = sortOrder === Qt.AscendingOrder + ? function(a, b) { return a < b; } + : function(a, b) { return a > b; } + for (i = 0; i < folderListModel.count; i++) { - append({ - fileName: folderListModel.get(i, "fileName"), - fileModified: folderListModel.get(i, "fileModified"), + fileName = folderListModel.get(i, "fileName"); + fileIsDir = folderListModel.get(i, "fileIsDir"); + + sortValue = folderListModel.get(i, dataField); + + if (dataField === "fileName") { + // Directories first by prefixing a "*". + // Case-insensitive. + sortValue = (fileIsDir ? "*" : "") + sortValue.toLowerCase(); + } + + lower = 0; + upper = rows; + while (lower < upper) { + middle = Math.floor((lower + upper) / 2); + var lessThan; + if (comparisonFunction(sortValue, get(middle)[sortField])) { + lessThan = true; + upper = middle; + } else { + lessThan = false; + lower = middle + 1; + } + } + + insert(lower, { + fileName: fileName, + fileModified: (fileIsDir ? new Date(0) : folderListModel.get(i, "fileModified")), fileSize: folderListModel.get(i, "fileSize"), filePath: folderListModel.get(i, "filePath"), - fileIsDir: folderListModel.get(i, "fileIsDir") + fileIsDir: fileIsDir, + fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() }); + + rows++; } } } @@ -287,10 +334,9 @@ ModalWindow { model: fileTableModel function updateSort() { - model.sortReversed = sortIndicatorColumn == 0 - ? (sortIndicatorOrder == Qt.DescendingOrder) - : (sortIndicatorOrder == Qt.AscendingOrder); // Date and size fields have opposite sense - model.sortField = sortIndicatorColumn + 1; + model.sortOrder = sortIndicatorOrder; + model.sortColumn = sortIndicatorColumn; + model.update(); } onSortIndicatorColumnChanged: { updateSort(); } From 41311bc3df3859be96291d7a06fb44605f98146b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 19 May 2016 17:13:07 +1200 Subject: [PATCH 48/77] Finish decoupling FolderListModel --- .../resources/qml/dialogs/FileDialog.qml | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index e7d96cedf9..5674123b4d 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -43,7 +43,7 @@ ModalWindow { // Set from OffscreenUi::getOpenFile() property alias caption: root.title; // Set from OffscreenUi::getOpenFile() - property alias dir: folderListModel.folder; + property alias dir: fileTableModel.folder; // Set from OffscreenUi::getOpenFile() property alias filter: selectionType.filtersString; // Set from OffscreenUi::getOpenFile() @@ -75,8 +75,9 @@ ModalWindow { // HACK: The following two lines force the model to initialize properly such that: // - Selecting a different drive at the initial screen updates the path displayed. // - The go-up button works properly from the initial screen. + var initialFolder = currentDirectory.lastValidFolder; root.dir = helper.pathToUrl(drivesSelector.currentText); - root.dir = helper.pathToUrl(currentDirectory.lastValidFolder); + root.dir = helper.pathToUrl(initialFolder); iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; } @@ -110,7 +111,7 @@ ModalWindow { glyph: hifi.glyphs.levelUp width: height size: 30 - enabled: folderListModel.parentFolder && folderListModel.parentFolder !== "" + enabled: fileTableModel.parentFolder && fileTableModel.parentFolder !== "" onClicked: d.navigateUp(); } @@ -135,7 +136,7 @@ ModalWindow { TextField { id: currentDirectory - property var lastValidFolder: helper.urlToPath(folderListModel.folder) + property var lastValidFolder: helper.urlToPath(fileTableModel.folder) height: homeButton.height anchors { top: parent.top @@ -161,7 +162,7 @@ ModalWindow { text = lastValidFolder; return } - folderListModel.folder = helper.pathToUrl(text); + fileTableModel.folder = helper.pathToUrl(text); } } @@ -172,15 +173,13 @@ ModalWindow { property bool currentSelectionIsFolder; property var backStack: [] property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } - property var modelConnection: Connections { target: model; onFolderChanged: d.update(); } // DJRTODO + property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } property var homeDestination: helper.home(); - Component.onCompleted: update(); function update() { var row = fileTableView.currentRow; - if (row === -1 && root.selectDirectory) { - currentSelectionUrl = fileTableView.model.folder; - currentSelectionIsFolder = true; + + if (row === -1) { return; } @@ -189,19 +188,21 @@ ModalWindow { if (root.selectDirectory || !currentSelectionIsFolder) { currentSelection.text = helper.urlToPath(currentSelectionUrl); } else { - currentSelection.text = "" + currentSelection.text = ""; } } function navigateUp() { - if (folderListModel.parentFolder && folderListModel.parentFolder !== "") { - folderListModel.folder = folderListModel.parentFolder + if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { + fileTableModel.folder = fileTableModel.parentFolder return true; + } else if (true) { + } } function navigateHome() { - folderListModel.folder = homeDestination; + fileTableModel.folder = homeDestination; return true; } } @@ -215,15 +216,11 @@ ModalWindow { // For some reason, declaring these bindings directly in the targets doesn't // work for setting the initial state Component.onCompleted: { - currentDirectory.lastValidFolder = Qt.binding(function() { return helper.urlToPath(folder); }); - upButton.enabled = Qt.binding(function() { return (parentFolder && parentFolder != "") ? true : false; }); + currentDirectory.lastValidFolder = helper.urlToPath(folder); showFiles = !root.selectDirectory } onFolderChanged: { - fileTableModel.update(); - fileTableView.selection.clear(); - fileTableView.selection.select(0); - fileTableView.currentRow = 0; + fileTableModel.update(); // Update once the data from the folder change is available. } } @@ -240,8 +237,12 @@ ModalWindow { property var folder property int sortOrder: Qt.AscendingOrder property int sortColumn: 0 + property string parentFolder: folderListModel.parentFolder - onFolderChanged: folderListModel.folder = folder; + onFolderChanged: { + folderListModel.folder = folder; + currentDirectory.lastValidFolder = helper.urlToPath(folder); + } function isFolder(row) { if (row === -1) { From dac043a52d5c7f47255373e10cad89446d3297be Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 20 May 2016 10:49:38 +1200 Subject: [PATCH 49/77] Provide navigation up to the PC level to display drives in table --- .../resources/qml/dialogs/FileDialog.qml | 87 ++++++++++++++++--- 1 file changed, 74 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 5674123b4d..0e936ff0a3 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -194,10 +194,8 @@ ModalWindow { function navigateUp() { if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { - fileTableModel.folder = fileTableModel.parentFolder + fileTableModel.folder = fileTableModel.parentFolder; return true; - } else if (true) { - } } @@ -222,6 +220,42 @@ ModalWindow { onFolderChanged: { fileTableModel.update(); // Update once the data from the folder change is available. } + + function getItem(index, field) { + return get(index, field); + } + } + + ListModel { + // Emulates FolderListModel but contains drive data. + id: driveListModel + + property int count: 1 + + Component.onCompleted: initialize(); + + function initialize() { + var drive, + i; + + count = drives.length; + + for (i = 0; i < count; i++) { + drive = drives[i].slice(0, -1); // Remove trailing "/". + append({ + fileName: drive, + fileModified: new Date(0), + fileSize: 0, + filePath: drive + "/", + fileIsDir: true, + fileNameSort: drive.toLowerCase() + }); + } + } + + function getItem(index, field) { + return get(index)[field]; + } } ListModel { @@ -237,10 +271,37 @@ ModalWindow { property var folder property int sortOrder: Qt.AscendingOrder property int sortColumn: 0 - property string parentFolder: folderListModel.parentFolder + property var model: folderListModel + property string parentFolder: calculateParentFolder(); + + readonly property string rootFolder: "file:///" + + function calculateParentFolder() { + if (model === folderListModel) { + if (folderListModel.parentFolder.toString() === "" && driveListModel.count > 1) { + return rootFolder; + } else { + return folderListModel.parentFolder; + } + } else { + return ""; + } + } onFolderChanged: { - folderListModel.folder = folder; + if (folder === rootFolder) { + model = driveListModel; + update(); + } else { + var needsUpdate = model === driveListModel && folder === folderListModel.folder; + + model = folderListModel; + folderListModel.folder = folder; + + if (needsUpdate) { + update(); + } + } currentDirectory.lastValidFolder = helper.urlToPath(folder); } @@ -265,18 +326,18 @@ ModalWindow { upper, rows = 0, i; + clear(); comparisonFunction = sortOrder === Qt.AscendingOrder ? function(a, b) { return a < b; } : function(a, b) { return a > b; } - for (i = 0; i < folderListModel.count; i++) { - fileName = folderListModel.get(i, "fileName"); - fileIsDir = folderListModel.get(i, "fileIsDir"); - - sortValue = folderListModel.get(i, dataField); + for (i = 0; i < model.count; i++) { + fileName = model.getItem(i, "fileName"); + fileIsDir = model.getItem(i, "fileIsDir"); + sortValue = model.getItem(i, dataField); if (dataField === "fileName") { // Directories first by prefixing a "*". // Case-insensitive. @@ -299,9 +360,9 @@ ModalWindow { insert(lower, { fileName: fileName, - fileModified: (fileIsDir ? new Date(0) : folderListModel.get(i, "fileModified")), - fileSize: folderListModel.get(i, "fileSize"), - filePath: folderListModel.get(i, "filePath"), + fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), + fileSize: model.getItem(i, "fileSize"), + filePath: model.getItem(i, "filePath"), fileIsDir: fileIsDir, fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() }); From 11aeaba114fc4970767d8640d1f7530116d6d19b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 20 May 2016 11:32:14 +1200 Subject: [PATCH 50/77] Capitalize drive letter of chosen file --- interface/resources/qml/dialogs/FileDialog.qml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 0e936ff0a3..56b7cb0505 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -176,6 +176,14 @@ ModalWindow { property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } property var homeDestination: helper.home(); + function capitalizeDrive(path) { + // Consistently capitalize drive letter for Windows. + if (/[a-zA-Z]:/.test(path)) { + return path.charAt(0).toUpperCase() + path.slice(1); + } + return path; + } + function update() { var row = fileTableView.currentRow; @@ -186,7 +194,7 @@ ModalWindow { currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); currentSelectionIsFolder = fileTableView.model.isFolder(row); if (root.selectDirectory || !currentSelectionIsFolder) { - currentSelection.text = helper.urlToPath(currentSelectionUrl); + currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl)); } else { currentSelection.text = ""; } From e8d7f9614aed57bad25f940c82282c25b91ed92e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 19 May 2016 15:51:17 -0700 Subject: [PATCH 51/77] refresh API info during re-connect - case 570 --- libraries/networking/src/AddressManager.cpp | 44 ++++++++++++++++++--- libraries/networking/src/AddressManager.h | 9 ++++- libraries/networking/src/DomainHandler.cpp | 18 +++------ libraries/networking/src/DomainHandler.h | 10 ++--- libraries/networking/src/NodeList.cpp | 14 +++++-- 5 files changed, 67 insertions(+), 28 deletions(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index a97f4df35d..35a9d9293d 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -144,12 +144,21 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // 4. domain network address (IP or dns resolvable hostname) // use our regex'ed helpers to figure out what we're supposed to do with this - if (!handleUsername(lookupUrl.authority())) { + if (handleUsername(lookupUrl.authority())) { + // handled a username for lookup + + // in case we're failing to connect to where we thought this user was + // store their username as previous lookup so we can refresh their location via API + _previousLookup = lookupUrl; + } else { // we're assuming this is either a network address or global place name // check if it is a network address first bool hostChanged; if (handleNetworkAddress(lookupUrl.host() - + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + + // a network address lookup clears the previous lookup since we don't expect to re-attempt it + _previousLookup.clear(); // If the host changed then we have already saved to history if (hostChanged) { @@ -165,10 +174,16 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // we may have a path that defines a relative viewpoint - if so we should jump to that now handlePath(path, trigger); } else if (handleDomainID(lookupUrl.host())){ + // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info + _previousLookup = lookupUrl; + // no place name - this is probably a domain ID // try to look up the domain ID on the metaverse API attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger); } else { + // store this place name as the previous lookup in case we fail to connect and want to refresh API info + _previousLookup = lookupUrl; + // wasn't an address - lookup the place name // we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger); @@ -180,9 +195,13 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { } else if (lookupUrl.toString().startsWith('/')) { qCDebug(networking) << "Going to relative path" << lookupUrl.path(); + // a path lookup clears the previous lookup since we don't expect to re-attempt it + _previousLookup.clear(); + // if this is a relative path then handle it as a relative viewpoint handlePath(lookupUrl.path(), trigger, true); emit lookupResultsFinished(); + return true; } @@ -276,7 +295,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const qCDebug(networking) << "Possible domain change required to connect to" << domainHostname << "on" << domainPort; - emit possibleDomainChangeRequired(domainHostname, domainPort); + emit possibleDomainChangeRequired(domainHostname, domainPort, domainID); } else { QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString(); @@ -309,7 +328,10 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString(); if (!overridePath.isEmpty()) { - handlePath(overridePath, trigger); + // make sure we don't re-handle an overriden path if this was a refresh of info from API + if (trigger != LookupTrigger::AttemptedRefresh) { + handlePath(overridePath, trigger); + } } else { // take the path that came back const QString PLACE_PATH_KEY = "path"; @@ -586,7 +608,7 @@ bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, Lookup DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::HandleAddress); - emit possibleDomainChangeRequired(hostname, port); + emit possibleDomainChangeRequired(hostname, port, QUuid()); return hostChanged; } @@ -606,6 +628,13 @@ void AddressManager::goToUser(const QString& username) { QByteArray(), nullptr, requestParams); } +void AddressManager::refreshPreviousLookup() { + // if we have a non-empty previous lookup, fire it again now (but don't re-store it in the history) + if (!_previousLookup.isEmpty()) { + handleUrl(_previousLookup, LookupTrigger::AttemptedRefresh); + } +} + void AddressManager::copyAddress() { QApplication::clipboard()->setText(currentAddress().toString()); } @@ -617,7 +646,10 @@ void AddressManager::copyPath() { void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) { // if we're cold starting and this is called for the first address (from settings) we don't do anything - if (trigger != LookupTrigger::StartupFromSettings && trigger != LookupTrigger::DomainPathResponse) { + if (trigger != LookupTrigger::StartupFromSettings + && trigger != LookupTrigger::DomainPathResponse + && trigger != LookupTrigger::AttemptedRefresh) { + if (trigger == LookupTrigger::Back) { // we're about to push to the forward stack // if it's currently empty emit our signal to say that going forward is now possible diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index dd0dbd9f38..0780cfb2c2 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -48,7 +48,8 @@ public: Forward, StartupFromSettings, DomainPathResponse, - Internal + Internal, + AttemptedRefresh }; bool isConnected(); @@ -88,6 +89,8 @@ public slots: void goToUser(const QString& username); + void refreshPreviousLookup(); + void storeCurrentAddress(); void copyAddress(); @@ -98,7 +101,7 @@ signals: void lookupResultIsOffline(); void lookupResultIsNotFound(); - void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort); + void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort, const QUuid& domainID); void possibleDomainChangeRequiredViaICEForID(const QString& iceServerHostname, const QUuid& domainID); void locationChangeRequired(const glm::vec3& newPosition, @@ -150,6 +153,8 @@ private: quint64 _lastBackPush = 0; QString _newHostLookupPath; + + QUrl _previousLookup; }; #endif // hifi_AddressManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 44ce63e6c6..b3c3a28829 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -28,16 +28,8 @@ DomainHandler::DomainHandler(QObject* parent) : QObject(parent), - _uuid(), _sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)), - _assignmentUUID(), - _connectionToken(), - _iceDomainID(), - _iceClientID(), - _iceServerSockAddr(), _icePeer(this), - _isConnected(false), - _settingsObject(), _settingsTimer(this) { _sockAddr.setObjectName("DomainServer"); @@ -105,7 +97,7 @@ void DomainHandler::hardReset() { softReset(); qCDebug(networking) << "Hard reset in NodeList DomainHandler."; - _iceDomainID = QUuid(); + _pendingDomainID = QUuid(); _iceServerSockAddr = HifiSockAddr(); _hostname = QString(); _sockAddr.clear(); @@ -140,7 +132,7 @@ void DomainHandler::setUUID(const QUuid& uuid) { } } -void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { +void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) { if (hostname != _hostname || _sockAddr.getPort() != port) { // re-set the domain info so that auth information is reloaded @@ -169,6 +161,8 @@ void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { // grab the port by reading the string after the colon _sockAddr.setPort(port); } + + _pendingDomainID = domainID; } void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) { @@ -179,7 +173,7 @@ void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, // refresh our ICE client UUID to something new _iceClientID = QUuid::createUuid(); - _iceDomainID = id; + _pendingDomainID = id; HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr; replaceableSockAddr->~HifiSockAddr(); @@ -341,7 +335,7 @@ void DomainHandler::processICEResponsePacket(QSharedPointer mes DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSPeerInformation); - if (_icePeer.getUUID() != _iceDomainID) { + if (_icePeer.getUUID() != _pendingDomainID) { qCDebug(networking) << "Received a network peer with ID that does not match current domain. Will not attempt connection."; _icePeer.reset(); } else { diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 03141d8fef..c6269191d2 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -58,8 +58,8 @@ public: const QUuid& getAssignmentUUID() const { return _assignmentUUID; } void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; } - - const QUuid& getICEDomainID() const { return _iceDomainID; } + + const QUuid& getPendingDomainID() const { return _pendingDomainID; } const QUuid& getICEClientID() const { return _iceClientID; } @@ -85,7 +85,7 @@ public: void softReset(); public slots: - void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT); + void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid()); void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id); void processSettingsPacketList(QSharedPointer packetList); @@ -126,11 +126,11 @@ private: HifiSockAddr _sockAddr; QUuid _assignmentUUID; QUuid _connectionToken; - QUuid _iceDomainID; + QUuid _pendingDomainID; // ID of domain being connected to, via ICE or direct connection QUuid _iceClientID; HifiSockAddr _iceServerSockAddr; NetworkPeer _icePeer; - bool _isConnected; + bool _isConnected { false }; QJsonObject _settingsObject; QString _pendingPath; QTimer _settingsTimer; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 482d0366fd..831f0a4995 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -50,7 +50,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // handle domain change signals from AddressManager connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired, - &_domainHandler, &DomainHandler::setHostnameAndPort); + &_domainHandler, &DomainHandler::setSocketAndID); connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID, &_domainHandler, &DomainHandler::setIceServerHostnameAndID); @@ -344,6 +344,14 @@ void NodeList::sendDomainServerCheckIn() { // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; } + + if (!_publicSockAddr.isNull() && !_domainHandler.isConnected() && !_domainHandler.getPendingDomainID().isNull()) { + // if we aren't connected to the domain-server, and we have an ID + // (that we presume belongs to a domain in the HF Metaverse) + // we request connection information for the domain every so often to make sure what we have is up to date + + DependencyManager::get()->refreshPreviousLookup(); + } } void NodeList::handleDSPathQuery(const QString& newPath) { @@ -451,7 +459,7 @@ void NodeList::handleICEConnectionToDomainServer() { LimitedNodeList::sendPeerQueryToIceServer(_domainHandler.getICEServerSockAddr(), _domainHandler.getICEClientID(), - _domainHandler.getICEDomainID()); + _domainHandler.getPendingDomainID()); } } @@ -464,7 +472,7 @@ void NodeList::pingPunchForDomainServer() { if (_domainHandler.getICEPeer().getConnectionAttempts() == 0) { qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID" - << uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID()); + << uuidStringWithoutCurlyBraces(_domainHandler.getPendingDomainID()); } else { if (_domainHandler.getICEPeer().getConnectionAttempts() % NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET == 0) { // if we have then nullify the domain handler's network peer and send a fresh ICE heartbeat From d3304ee65c52dd589cc04ed194768e17f30650df Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 19 May 2016 18:01:13 -0700 Subject: [PATCH 52/77] make some overriding methods as override --- assignment-client/src/entities/EntityServer.h | 4 ++-- tests/controllers/src/main.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 1685f08e01..0486a97ede 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -60,8 +60,8 @@ public: virtual void trackViewerGone(const QUuid& sessionID) override; public slots: - virtual void nodeAdded(SharedNodePointer node); - virtual void nodeKilled(SharedNodePointer node); + virtual void nodeAdded(SharedNodePointer node) override; + virtual void nodeKilled(SharedNodePointer node) override; void pruneDeletedEntities(); protected: diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index e978dd9a38..3a5b4a4a4d 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -89,7 +89,7 @@ public: virtual GLWidget* getPrimaryWidget() override { return nullptr; } virtual MainWindow* getPrimaryWindow() override { return nullptr; } virtual QOpenGLContext* getPrimaryContext() override { return nullptr; } - virtual ui::Menu* getPrimaryMenu() { return nullptr; } + virtual ui::Menu* getPrimaryMenu() override { return nullptr; } virtual bool isForeground() override { return true; } virtual const DisplayPluginPointer getActiveDisplayPlugin() const override { return DisplayPluginPointer(); } }; From b2bbf72be2b10e0801cb281d9d96f52f2bf449bd Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 20 May 2016 18:48:51 +1200 Subject: [PATCH 53/77] Replace drive drop-down plus text field with a paths drop-down --- .../resources/qml/dialogs/FileDialog.qml | 97 ++++++++++--------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 56b7cb0505..78d5943479 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -68,16 +68,12 @@ ModalWindow { Component.onCompleted: { console.log("Helper " + helper + " drives " + drives) - drivesSelector.onCurrentTextChanged.connect(function(){ - root.dir = helper.pathToUrl(drivesSelector.currentText); - }) - // HACK: The following two lines force the model to initialize properly such that: - // - Selecting a different drive at the initial screen updates the path displayed. - // - The go-up button works properly from the initial screen. - var initialFolder = currentDirectory.lastValidFolder; - root.dir = helper.pathToUrl(drivesSelector.currentText); - root.dir = helper.pathToUrl(initialFolder); + // HACK: The following lines force the model to initialize properly such that the go-up button + // works properly from the initial screen. + var initialFolder = folderListModel.folder; + fileTableModel.folder = helper.pathToUrl(drives[0]); + fileTableModel.folder = initialFolder; iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; } @@ -97,15 +93,6 @@ ModalWindow { } spacing: hifi.dimensions.contentSpacing.x - // FIXME implement back button - //VrControls.ButtonAwesome { - // id: backButton - // text: "\uf0a8" - // size: currentDirectory.height - // enabled: d.backStack.length != 0 - // MouseArea { anchors.fill: parent; onClicked: d.navigateBack() } - //} - GlyphButton { id: upButton glyph: hifi.glyphs.levelUp @@ -124,20 +111,10 @@ ModalWindow { enabled: d.homeDestination ? true : false onClicked: d.navigateHome(); } - - ComboBox { - id: drivesSelector - width: 62 - model: drives - visible: drives.length > 1 - currentIndex: 0 - } } - TextField { - id: currentDirectory - property var lastValidFolder: helper.urlToPath(fileTableModel.folder) - height: homeButton.height + ComboBox { + id: pathSelector anchors { top: parent.top topMargin: hifi.dimensions.contentMargin.y @@ -146,23 +123,54 @@ ModalWindow { right: parent.right } - function capitalizeDrive(path) { - // Consistently capitalize drive letter for Windows. - if (/[a-zA-Z]:/.test(path)) { - return path.charAt(0).toUpperCase() + path.slice(1); + property var lastValidFolder: helper.urlToPath(fileTableModel.folder) + + function calculatePathChoices(folder) { + var folders = folder.split("/"), + choices = [], + i, length; + + if (folders[folders.length - 1] === "") { + folders.pop(); + } + + if (folders[0] !== "") { + choices.push(folders[0]); + } + + for (i = 1, length = folders.length; i < length; i++) { + choices.push(choices[i - 1] + "/" + folders[i]); + } + choices.reverse(); + + if (drives && drives.length > 1) { + choices.push("This PC"); + } + + if (choices.length > 0) { + pathSelector.model = choices; } - return path; } - onLastValidFolderChanged: text = capitalizeDrive(lastValidFolder); + onLastValidFolderChanged: { + var folder = d.capitalizeDrive(lastValidFolder); + calculatePathChoices(folder); + } - // FIXME add support auto-completion - onAccepted: { - if (!helper.validFolder(text)) { - text = lastValidFolder; - return + onCurrentTextChanged: { + var folder = currentText; + + if (/^[a-zA-z]:$/.test(folder)) { + folder = "file:///" + folder + "/"; + } else if (folder === "This PC") { + folder = "file:///"; + } else { + folder = helper.pathToUrl(folder); + } + + if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { + fileTableModel.folder = folder; } - fileTableModel.folder = helper.pathToUrl(text); } } @@ -219,12 +227,10 @@ ModalWindow { showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory - // For some reason, declaring these bindings directly in the targets doesn't - // work for setting the initial state Component.onCompleted: { - currentDirectory.lastValidFolder = helper.urlToPath(folder); showFiles = !root.selectDirectory } + onFolderChanged: { fileTableModel.update(); // Update once the data from the folder change is available. } @@ -310,7 +316,6 @@ ModalWindow { update(); } } - currentDirectory.lastValidFolder = helper.urlToPath(folder); } function isFolder(row) { From 51c16739f2a68d7eaa33344b8b385b4919f2f5e9 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 12:05:22 -0700 Subject: [PATCH 54/77] provide a getter for the place name in AddressManager --- libraries/networking/src/AddressManager.cpp | 5 +++++ libraries/networking/src/AddressManager.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index a97f4df35d..02962b636b 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -295,10 +295,13 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const // set our current root place name to the name that came back const QString PLACE_NAME_KEY = "name"; QString placeName = rootMap[PLACE_NAME_KEY].toString(); + if (!placeName.isEmpty()) { if (setHost(placeName, trigger)) { trigger = LookupTrigger::Internal; } + + _placeName = placeName; } else { if (setHost(domainIDString, trigger)) { trigger = LookupTrigger::Internal; @@ -580,7 +583,9 @@ bool AddressManager::setHost(const QString& host, LookupTrigger trigger, quint16 bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, LookupTrigger trigger) { bool hostChanged = setHost(hostname, trigger, port); + // clear any current place information _rootPlaceID = QUuid(); + _placeName.clear(); qCDebug(networking) << "Possible domain change required to connect to domain at" << hostname << "on" << port; diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index dd0dbd9f38..643924ff5c 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -58,6 +58,7 @@ public: const QString currentPath(bool withOrientation = true) const; const QUuid& getRootPlaceID() const { return _rootPlaceID; } + const QString& getPlaceName() const { return _placeName; } const QString& getHost() const { return _host; } @@ -141,6 +142,7 @@ private: QString _host; quint16 _port; + QString _placeName; QUuid _rootPlaceID; PositionGetter _positionGetter; OrientationGetter _orientationGetter; From 7110fe98eba05dd7928aef5cf259eb23dbb6ba62 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 12:13:22 -0700 Subject: [PATCH 55/77] associate incoming place name with DomainServerNodeData --- domain-server/src/DomainGatekeeper.cpp | 1 + domain-server/src/DomainServerNodeData.h | 5 +++++ domain-server/src/NodeConnectionData.cpp | 1 + domain-server/src/NodeConnectionData.h | 1 + libraries/networking/src/NodeList.cpp | 3 +++ libraries/networking/src/udt/PacketHeaders.cpp | 3 +++ 6 files changed, 14 insertions(+) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 919ac37ee9..76671461d3 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -105,6 +105,7 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer(node->getLinkedData()); nodeData->setSendingSockAddr(message->getSenderSockAddr()); nodeData->setNodeInterestSet(nodeConnection.interestList.toSet()); + nodeData->setPlaceName(nodeConnection.placeName); // signal that we just connected a node so the DomainServer can get it a list // and broadcast its presence right away diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index cd68aa3006..3ef5415cba 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -56,6 +56,9 @@ public: void addOverrideForKey(const QString& key, const QString& value, const QString& overrideValue); void removeOverrideForKey(const QString& key, const QString& value); + + const QString& getPlaceName() { return _placeName; } + void setPlaceName(const QString& placeName) { _placeName; } private: QJsonObject overrideValuesIfNeeded(const QJsonObject& newStats); @@ -75,6 +78,8 @@ private: bool _isAuthenticated = true; NodeSet _nodeInterestSet; QString _nodeVersion; + + QString _placeName; }; #endif // hifi_DomainServerNodeData_h diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index 80cb5950be..eabcfaacc6 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -19,6 +19,7 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c if (isConnectRequest) { dataStream >> newHeader.connectUUID; + dataStream >> newHeader.placeName; } dataStream >> newHeader.nodeType diff --git a/domain-server/src/NodeConnectionData.h b/domain-server/src/NodeConnectionData.h index 6b3b8eb7c1..34119ffdab 100644 --- a/domain-server/src/NodeConnectionData.h +++ b/domain-server/src/NodeConnectionData.h @@ -27,6 +27,7 @@ public: HifiSockAddr localSockAddr; HifiSockAddr senderSockAddr; QList interestList; + QString placeName; }; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 482d0366fd..cee6404755 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -312,6 +312,9 @@ void NodeList::sendDomainServerCheckIn() { // pack the connect UUID for this connect request packetStream << connectUUID; + + // pack the hostname information (so the domain-server can see which place name we came in on) + packetStream << DependencyManager::get()->getPlaceName(); } // pack our data to send to the domain-server diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e4aab94090..f6ad6b2970 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -58,6 +58,9 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AssetUpload: // Removal of extension from Asset requests return 18; + case PacketType::DomainConnectRequest: + // addition of referring hostname information + return 18; default: return 17; } From 3b2a9b7b98995298d44a316671c08331995e25b2 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 13:02:34 -0700 Subject: [PATCH 56/77] fix set of place name on DomainServerNodeData --- domain-server/src/DomainServerNodeData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index 3ef5415cba..a14d7ff768 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -58,7 +58,7 @@ public: void removeOverrideForKey(const QString& key, const QString& value); const QString& getPlaceName() { return _placeName; } - void setPlaceName(const QString& placeName) { _placeName; } + void setPlaceName(const QString& placeName) { _placeName = placeName; } private: QJsonObject overrideValuesIfNeeded(const QJsonObject& newStats); From 5884e1eadd03e62d49c92a6dd7f7a222760dda56 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 13:03:18 -0700 Subject: [PATCH 57/77] rename metaverse heartbeat methods --- domain-server/src/DomainServer.cpp | 10 +++++----- domain-server/src/DomainServer.h | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 0aab6b7e31..e8199317f0 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -462,7 +462,7 @@ void DomainServer::setupAutomaticNetworking() { nodeList->startSTUNPublicSocketUpdate(); } else { // send our heartbeat to data server so it knows what our network settings are - sendHeartbeatToDataServer(); + sendHeartbeatToMetaverse(); } } else { qDebug() << "Cannot enable domain-server automatic networking without a domain ID." @@ -471,7 +471,7 @@ void DomainServer::setupAutomaticNetworking() { return; } } else { - sendHeartbeatToDataServer(); + sendHeartbeatToMetaverse(); } qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting; @@ -480,7 +480,7 @@ void DomainServer::setupAutomaticNetworking() { const int DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS = 15 * 1000; QTimer* dataHeartbeatTimer = new QTimer(this); - connect(dataHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToDataServer())); + connect(dataHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToMetaverse())); dataHeartbeatTimer->start(DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS); } @@ -1029,11 +1029,11 @@ QJsonObject jsonForDomainSocketUpdate(const HifiSockAddr& socket) { const QString DOMAIN_UPDATE_AUTOMATIC_NETWORKING_KEY = "automatic_networking"; void DomainServer::performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr) { - sendHeartbeatToDataServer(newPublicSockAddr.getAddress().toString()); + sendHeartbeatToMetaverse(newPublicSockAddr.getAddress().toString()); } -void DomainServer::sendHeartbeatToDataServer(const QString& networkAddress) { +void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; auto nodeList = DependencyManager::get(); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 93bb5de494..fef3221b7d 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -71,7 +71,7 @@ private slots: void sendPendingTransactionsToServer(); void performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr); - void sendHeartbeatToDataServer() { sendHeartbeatToDataServer(QString()); } + void sendHeartbeatToMetaverse() { sendHeartbeatToMetaverse(QString()); } void sendHeartbeatToIceServer(); void handleConnectedNode(SharedNodePointer newNode); @@ -103,7 +103,7 @@ private: void setupAutomaticNetworking(); void setupICEHeartbeatForFullNetworking(); - void sendHeartbeatToDataServer(const QString& networkAddress); + void sendHeartbeatToMetaverse(const QString& networkAddress); void randomizeICEServerAddress(bool shouldTriggerHostLookup); From 7d2d60f2005e5f931bbcae12a1ab253360e80d8e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 13:08:24 -0700 Subject: [PATCH 58/77] split assigned and un-assigned nodes --- domain-server/src/DomainGatekeeper.cpp | 3 ++- domain-server/src/DomainServerNodeData.h | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 76671461d3..e974ab26f4 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -151,6 +151,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(it->second.getWalletUUID()); nodeData->setNodeVersion(it->second.getNodeVersion()); + nodeData->setWasAssigned(true); // cleanup the PendingAssignedNodeData for this assignment now that it's connecting _pendingAssignedNodes.erase(it); @@ -283,7 +284,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect // set the edit rights for this user newNode->setIsAllowedEditor(isAllowedEditor); newNode->setCanRez(canRez); - + // grab the linked data for our new node so we can set the username DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index a14d7ff768..f95403c779 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -59,6 +59,9 @@ public: const QString& getPlaceName() { return _placeName; } void setPlaceName(const QString& placeName) { _placeName = placeName; } + + bool wasAssigned() const { return _wasAssigned; }; + void setWasAssigned(bool wasAssigned) { _wasAssigned = wasAssigned; } private: QJsonObject overrideValuesIfNeeded(const QJsonObject& newStats); @@ -80,6 +83,8 @@ private: QString _nodeVersion; QString _placeName; + + bool _wasAssigned { false }; }; #endif // hifi_DomainServerNodeData_h From 962066c7d10fd9f5df126002f849b298477f0c06 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 13:19:38 -0700 Subject: [PATCH 59/77] send user hostname breakdown with heartbeat --- domain-server/src/DomainServer.cpp | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index e8199317f0..75f1c9f6b6 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1056,20 +1056,34 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { domainObject[RESTRICTED_ACCESS_FLAG] = _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); - // add the number of currently connected agent users - int numConnectedAuthedUsers = 0; + // figure out the breakdown of currently connected interface clients + int numConnectedUnassigned = 0; + QJsonObject userHostnames; - nodeList->eachNode([&numConnectedAuthedUsers](const SharedNodePointer& node){ - if (node->getLinkedData() && !static_cast(node->getLinkedData())->getUsername().isEmpty()) { - ++numConnectedAuthedUsers; + static const QString DEFAULT_HOSTNAME = "*"; + + nodeList->eachNode([&numConnectedUnassigned, &userHostnames](const SharedNodePointer& node) { + if (node->getLinkedData()) { + auto nodeData = static_cast(node->getLinkedData()); + + if (!nodeData->wasAssigned()) { + ++numConnectedUnassigned; + + // increment the count for this hostname (or the default if we don't have one) + auto hostname = nodeData->getPlaceName().isEmpty() ? DEFAULT_HOSTNAME : nodeData->getPlaceName(); + userHostnames[hostname] = userHostnames[hostname].toInt() + 1; + } } }); - const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; - const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; + static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; + static const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; + static const QString HEARTBEAT_USER_HOSTNAMES_KEY = "user_hostnames"; QJsonObject heartbeatObject; - heartbeatObject[HEARTBEAT_NUM_USERS_KEY] = numConnectedAuthedUsers; + heartbeatObject[HEARTBEAT_NUM_USERS_KEY] = numConnectedUnassigned; + heartbeatObject[HEARTBEAT_USER_HOSTNAMES_KEY] = userHostnames; + domainObject[DOMAIN_HEARTBEAT_KEY] = heartbeatObject; QString domainUpdateJSON = QString("{\"domain\": %1 }").arg(QString(QJsonDocument(domainObject).toJson())); From 5ab876114f86a715cc380c5214848a797dd9cf99 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 13:47:14 -0700 Subject: [PATCH 60/77] send hostname to DS with every DS packet to handle changes --- domain-server/src/DomainGatekeeper.cpp | 4 ++-- domain-server/src/DomainServer.cpp | 3 +++ domain-server/src/NodeConnectionData.cpp | 3 +-- libraries/networking/src/NodeList.cpp | 9 ++++----- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index e974ab26f4..61cc775e08 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -55,9 +55,9 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetSize() == 0) { return; } - + QDataStream packetStream(message->getMessage()); - + // read a NodeConnectionData object from the packet so we can pass around this data while we're inspecting it NodeConnectionData nodeConnection = NodeConnectionData::fromDataStream(packetStream, message->getSenderSockAddr()); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 75f1c9f6b6..cfec72a24b 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -678,6 +678,9 @@ void DomainServer::processListRequestPacket(QSharedPointer mess DomainServerNodeData* nodeData = reinterpret_cast(sendingNode->getLinkedData()); nodeData->setNodeInterestSet(nodeRequestData.interestList.toSet()); + // update the connecting hostname in case it has changed + nodeData->setPlaceName(nodeRequestData.placeName); + sendDomainListToNode(sendingNode, message->getSenderSockAddr()); } diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index eabcfaacc6..28f769298c 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -19,12 +19,11 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c if (isConnectRequest) { dataStream >> newHeader.connectUUID; - dataStream >> newHeader.placeName; } dataStream >> newHeader.nodeType >> newHeader.publicSockAddr >> newHeader.localSockAddr - >> newHeader.interestList; + >> newHeader.interestList >> newHeader.placeName; newHeader.senderSockAddr = senderSockAddr; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index cee6404755..c295ffc700 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -312,13 +312,12 @@ void NodeList::sendDomainServerCheckIn() { // pack the connect UUID for this connect request packetStream << connectUUID; - - // pack the hostname information (so the domain-server can see which place name we came in on) - packetStream << DependencyManager::get()->getPlaceName(); } - // pack our data to send to the domain-server - packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); + // pack our data to send to the domain-server including + // the hostname information (so the domain-server can see which place name we came in on) + packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList() + << DependencyManager::get()->getPlaceName(); if (!_domainHandler.isConnected()) { DataServerAccountInfo& accountInfo = accountManager->getAccountInfo(); From 33379824c8d0ad122d0e5435a9fa7c88ae59042b Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 10 May 2016 15:15:22 -0700 Subject: [PATCH 61/77] clear place name if switching to a domain ID string --- libraries/networking/src/AddressManager.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 02962b636b..36d9e621a9 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -306,6 +306,9 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const if (setHost(domainIDString, trigger)) { trigger = LookupTrigger::Internal; } + + // this isn't a place, so clear the place name + _placeName.clear(); } // check if we had a path to override the path returned From 770fab956fac0502b0a95d68f624d20d50bce533 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 May 2016 12:23:06 -0700 Subject: [PATCH 62/77] remove dead code --- interface/src/Application.cpp | 4 ---- interface/src/Menu.cpp | 3 --- interface/src/Menu.h | 1 - libraries/entities/src/EntityEditPacketSender.cpp | 4 +--- libraries/entities/src/EntityEditPacketSender.h | 4 ---- 5 files changed, 1 insertion(+), 15 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6190de5875..5ce869e5c9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3594,10 +3594,6 @@ void Application::update(float deltaTime) { int Application::sendNackPackets() { - if (Menu::getInstance()->isOptionChecked(MenuOption::DisableNackPackets)) { - return 0; - } - // iterates through all nodes in NodeList auto nodeList = DependencyManager::get(); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8b69bb8022..883c30a3b8 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -531,9 +531,6 @@ Menu::Menu() { // Developer > Network >>> MenuWrapper* networkMenu = developerMenu->addMenu("Network"); addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); - addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableNackPackets, 0, false, - qApp->getEntityEditPacketSender(), - SLOT(toggleNackPackets())); addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, 0, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 484be9f346..a58a103504 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -84,7 +84,6 @@ namespace MenuOption { const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; const QString DisableLightEntities = "Disable Light Entities"; - const QString DisableNackPackets = "Disable Entity NACK Packets"; const QString DiskCacheEditor = "Disk Cache Editor"; const QString DisplayCrashOptions = "Display Crash Options"; const QString DisplayHandTargets = "Show Hand Targets"; diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 1e38c32964..ad936cc890 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -24,9 +24,7 @@ EntityEditPacketSender::EntityEditPacketSender() { } void EntityEditPacketSender::processEntityEditNackPacket(QSharedPointer message, SharedNodePointer sendingNode) { - if (_shouldProcessNack) { - processNackPacket(*message, sendingNode); - } + processNackPacket(*message, sendingNode); } void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByteArray& buffer, qint64 clockSkew) { diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 26e4dd83ff..0492fc66f4 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -36,9 +36,5 @@ public: public slots: void processEntityEditNackPacket(QSharedPointer message, SharedNodePointer sendingNode); - void toggleNackPackets() { _shouldProcessNack = !_shouldProcessNack; } - -private: - bool _shouldProcessNack = true; }; #endif // hifi_EntityEditPacketSender_h From 00bd3b2e8a60577f25d081078f4bfe1d98ea7dee Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 20 May 2016 13:43:08 -0700 Subject: [PATCH 63/77] remove more dead code --- interface/resources/qml/hifi/MenuOption.qml | 165 -------------------- 1 file changed, 165 deletions(-) delete mode 100644 interface/resources/qml/hifi/MenuOption.qml diff --git a/interface/resources/qml/hifi/MenuOption.qml b/interface/resources/qml/hifi/MenuOption.qml deleted file mode 100644 index 46cf5d9662..0000000000 --- a/interface/resources/qml/hifi/MenuOption.qml +++ /dev/null @@ -1,165 +0,0 @@ -import QtQuick 2.5 - -QtObject { - readonly property string aboutApp: "About Interface"; - readonly property string addRemoveFriends: "Add/Remove Friends..."; - readonly property string addressBar: "Show Address Bar"; - readonly property string animations: "Animations..."; - readonly property string animDebugDrawAnimPose: "Debug Draw Animation"; - readonly property string animDebugDrawDefaultPose: "Debug Draw Default Pose"; - readonly property string animDebugDrawPosition: "Debug Draw Position"; - readonly property string antialiasing: "Antialiasing"; - readonly property string assetMigration: "ATP Asset Migration"; - readonly property string assetServer: "Asset Server"; - readonly property string atmosphere: "Atmosphere"; - readonly property string attachments: "Attachments..."; - readonly property string audioNetworkStats: "Audio Network Stats"; - readonly property string audioNoiseReduction: "Audio Noise Reduction"; - readonly property string audioScope: "Show Scope"; - readonly property string audioScopeFiftyFrames: "Fifty"; - readonly property string audioScopeFiveFrames: "Five"; - readonly property string audioScopeFrames: "Display Frames"; - readonly property string audioScopePause: "Pause Scope"; - readonly property string audioScopeTwentyFrames: "Twenty"; - readonly property string audioStatsShowInjectedStreams: "Audio Stats Show Injected Streams"; - readonly property string audioTools: "Show Level Meter"; - readonly property string autoMuteAudio: "Auto Mute Microphone"; - readonly property string avatarReceiveStats: "Show Receive Stats"; - readonly property string back: "Back"; - readonly property string bandwidthDetails: "Bandwidth Details"; - readonly property string binaryEyelidControl: "Binary Eyelid Control"; - readonly property string bookmarkLocation: "Bookmark Location"; - readonly property string bookmarks: "Bookmarks"; - readonly property string cachesSize: "RAM Caches Size"; - readonly property string calibrateCamera: "Calibrate Camera"; - readonly property string cameraEntityMode: "Entity Mode"; - readonly property string centerPlayerInView: "Center Player In View"; - readonly property string chat: "Chat..."; - readonly property string collisions: "Collisions"; - readonly property string connexion: "Activate 3D Connexion Devices"; - readonly property string console_: "Console..."; - readonly property string controlWithSpeech: "Control With Speech"; - readonly property string copyAddress: "Copy Address to Clipboard"; - readonly property string copyPath: "Copy Path to Clipboard"; - readonly property string coupleEyelids: "Couple Eyelids"; - readonly property string crashInterface: "Crash Interface"; - readonly property string debugAmbientOcclusion: "Debug Ambient Occlusion"; - readonly property string decreaseAvatarSize: "Decrease Avatar Size"; - readonly property string deleteBookmark: "Delete Bookmark..."; - readonly property string disableActivityLogger: "Disable Activity Logger"; - readonly property string disableEyelidAdjustment: "Disable Eyelid Adjustment"; - readonly property string disableLightEntities: "Disable Light Entities"; - readonly property string disableNackPackets: "Disable Entity NACK Packets"; - readonly property string diskCacheEditor: "Disk Cache Editor"; - readonly property string displayCrashOptions: "Display Crash Options"; - readonly property string displayHandTargets: "Show Hand Targets"; - readonly property string displayModelBounds: "Display Model Bounds"; - readonly property string displayModelTriangles: "Display Model Triangles"; - readonly property string displayModelElementChildProxies: "Display Model Element Children"; - readonly property string displayModelElementProxy: "Display Model Element Bounds"; - readonly property string displayDebugTimingDetails: "Display Timing Details"; - readonly property string dontDoPrecisionPicking: "Don't Do Precision Picking"; - readonly property string dontRenderEntitiesAsScene: "Don't Render Entities as Scene"; - readonly property string echoLocalAudio: "Echo Local Audio"; - readonly property string echoServerAudio: "Echo Server Audio"; - readonly property string enable3DTVMode: "Enable 3DTV Mode"; - readonly property string enableCharacterController: "Enable avatar collisions"; - readonly property string expandMyAvatarSimulateTiming: "Expand /myAvatar/simulation"; - readonly property string expandMyAvatarTiming: "Expand /myAvatar"; - readonly property string expandOtherAvatarTiming: "Expand /otherAvatar"; - readonly property string expandPaintGLTiming: "Expand /paintGL"; - readonly property string expandUpdateTiming: "Expand /update"; - readonly property string faceshift: "Faceshift"; - readonly property string firstPerson: "First Person"; - readonly property string fivePointCalibration: "5 Point Calibration"; - readonly property string fixGaze: "Fix Gaze (no saccade)"; - readonly property string forward: "Forward"; - readonly property string frameTimer: "Show Timer"; - readonly property string fullscreenMirror: "Mirror"; - readonly property string help: "Help..."; - readonly property string increaseAvatarSize: "Increase Avatar Size"; - readonly property string independentMode: "Independent Mode"; - readonly property string inputMenu: "Avatar>Input Devices"; - readonly property string keyboardMotorControl: "Enable Keyboard Motor Control"; - readonly property string leapMotionOnHMD: "Leap Motion on HMD"; - readonly property string loadScript: "Open and Run Script File..."; - readonly property string loadScriptURL: "Open and Run Script from URL..."; - readonly property string lodTools: "LOD Tools"; - readonly property string login: "Login"; - readonly property string log: "Log"; - readonly property string logExtraTimings: "Log Extra Timing Details"; - readonly property string lowVelocityFilter: "Low Velocity Filter"; - readonly property string meshVisible: "Draw Mesh"; - readonly property string miniMirror: "Mini Mirror"; - readonly property string muteAudio: "Mute Microphone"; - readonly property string muteEnvironment: "Mute Environment"; - readonly property string muteFaceTracking: "Mute Face Tracking"; - readonly property string namesAboveHeads: "Names Above Heads"; - readonly property string noFaceTracking: "None"; - readonly property string octreeStats: "Entity Statistics"; - readonly property string onePointCalibration: "1 Point Calibration"; - readonly property string onlyDisplayTopTen: "Only Display Top Ten"; - readonly property string outputMenu: "Display"; - readonly property string packageModel: "Package Model..."; - readonly property string pair: "Pair"; - readonly property string physicsShowOwned: "Highlight Simulation Ownership"; - readonly property string physicsShowHulls: "Draw Collision Hulls"; - readonly property string pipelineWarnings: "Log Render Pipeline Warnings"; - readonly property string preferences: "General..."; - readonly property string quit: "Quit"; - readonly property string reloadAllScripts: "Reload All Scripts"; - readonly property string reloadContent: "Reload Content (Clears all caches)"; - readonly property string renderBoundingCollisionShapes: "Show Bounding Collision Shapes"; - readonly property string renderFocusIndicator: "Show Eye Focus"; - readonly property string renderLookAtTargets: "Show Look-at Targets"; - readonly property string renderLookAtVectors: "Show Look-at Vectors"; - readonly property string renderResolution: "Scale Resolution"; - readonly property string renderResolutionOne: "1"; - readonly property string renderResolutionTwoThird: "2/3"; - readonly property string renderResolutionHalf: "1/2"; - readonly property string renderResolutionThird: "1/3"; - readonly property string renderResolutionQuarter: "1/4"; - readonly property string renderAmbientLight: "Ambient Light"; - readonly property string renderAmbientLightGlobal: "Global"; - readonly property string renderAmbientLight0: "OLD_TOWN_SQUARE"; - readonly property string renderAmbientLight1: "GRACE_CATHEDRAL"; - readonly property string renderAmbientLight2: "EUCALYPTUS_GROVE"; - readonly property string renderAmbientLight3: "ST_PETERS_BASILICA"; - readonly property string renderAmbientLight4: "UFFIZI_GALLERY"; - readonly property string renderAmbientLight5: "GALILEOS_TOMB"; - readonly property string renderAmbientLight6: "VINE_STREET_KITCHEN"; - readonly property string renderAmbientLight7: "BREEZEWAY"; - readonly property string renderAmbientLight8: "CAMPUS_SUNSET"; - readonly property string renderAmbientLight9: "FUNSTON_BEACH_SUNSET"; - readonly property string resetAvatarSize: "Reset Avatar Size"; - readonly property string resetSensors: "Reset Sensors"; - readonly property string runningScripts: "Running Scripts..."; - readonly property string runTimingTests: "Run Timing Tests"; - readonly property string scriptEditor: "Script Editor..."; - readonly property string scriptedMotorControl: "Enable Scripted Motor Control"; - readonly property string showDSConnectTable: "Show Domain Connection Timing"; - readonly property string showBordersEntityNodes: "Show Entity Nodes"; - readonly property string showRealtimeEntityStats: "Show Realtime Entity Stats"; - readonly property string showWhosLookingAtMe: "Show Who's Looking at Me"; - readonly property string standingHMDSensorMode: "Standing HMD Sensor Mode"; - readonly property string simulateEyeTracking: "Simulate"; - readonly property string sMIEyeTracking: "SMI Eye Tracking"; - readonly property string stars: "Stars"; - readonly property string stats: "Stats"; - readonly property string stopAllScripts: "Stop All Scripts"; - readonly property string suppressShortTimings: "Suppress Timings Less than 10ms"; - readonly property string thirdPerson: "Third Person"; - readonly property string threePointCalibration: "3 Point Calibration"; - readonly property string throttleFPSIfNotFocus: "Throttle FPS If Not Focus"; // FIXME - this value duplicated in Basic2DWindowOpenGLDisplayPlugin.cpp - readonly property string toolWindow: "Tool Window"; - readonly property string transmitterDrive: "Transmitter Drive"; - readonly property string turnWithHead: "Turn using Head"; - readonly property string useAudioForMouth: "Use Audio for Mouth"; - readonly property string useCamera: "Use Camera"; - readonly property string velocityFilter: "Velocity Filter"; - readonly property string visibleToEveryone: "Everyone"; - readonly property string visibleToFriends: "Friends"; - readonly property string visibleToNoOne: "No one"; - readonly property string worldAxes: "World Axes"; -} - From d95d3ff3ac61c722a109fe50fe99859b31eee585 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 20 May 2016 14:56:47 -0700 Subject: [PATCH 64/77] clean up debugging prints --- interface/src/avatar/Avatar.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index efaed5b83a..d12306a122 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -197,19 +197,16 @@ void Avatar::updateAvatarEntities() { QScriptEngine scriptEngine; entityTree->withWriteLock([&] { AvatarEntityMap avatarEntities = getAvatarEntityData(); - qDebug() << "---------------"; for (auto entityID : avatarEntities.keys()) { // see EntityEditPacketSender::queueEditEntityMessage for the other end of this. unpack properties // and either add or update the entity. QByteArray jsonByteArray = avatarEntities.value(entityID); QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(jsonByteArray); if (!jsonProperties.isObject()) { - qDebug() << "got bad avatarEntity json"; + qCDebug(interfaceapp) << "got bad avatarEntity json" << QString(jsonByteArray.toHex()); continue; } - qDebug() << jsonProperties.toJson(); - QVariant variantProperties = jsonProperties.toVariant(); QVariantMap asMap = variantProperties.toMap(); QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); @@ -229,19 +226,14 @@ void Avatar::updateAvatarEntities() { EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { - qDebug() << "avatar-entities existing entity, element =" << entity->getElement().get(); if (entityTree->updateEntity(entityID, properties)) { entity->updateLastEditedFromRemote(); - qDebug() << "avatar-entities after entityTree->updateEntity(), element =" << entity->getElement().get(); } else { - qDebug() << "AVATAR-ENTITIES -- updateEntity failed: " << properties.getType(); success = false; } } else { - qDebug() << "avatar-entities new entity"; entity = entityTree->addEntity(entityID, properties); if (!entity) { - qDebug() << "AVATAR-ENTITIES -- addEntity failed: " << properties.getType(); success = false; } } @@ -1194,7 +1186,7 @@ void Avatar::setParentID(const QUuid& parentID) { if (success) { setTransform(beforeChangeTransform, success); if (!success) { - qDebug() << "Avatar::setParentID failed to reset avatar's location."; + qCDebug(interfaceapp) << "Avatar::setParentID failed to reset avatar's location."; } } } @@ -1209,7 +1201,7 @@ void Avatar::setParentJointIndex(quint16 parentJointIndex) { if (success) { setTransform(beforeChangeTransform, success); if (!success) { - qDebug() << "Avatar::setParentJointIndex failed to reset avatar's location."; + qCDebug(interfaceapp) << "Avatar::setParentJointIndex failed to reset avatar's location."; } } } From a24d63a39c3974b05839499524a05c07ca86d1b7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 20 May 2016 15:45:00 -0700 Subject: [PATCH 65/77] make distance-grab work better when avatar is walking --- .../system/controllers/handControllerGrab.js | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index ed4ac219c0..06549a38b5 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1112,8 +1112,8 @@ function MyController(hand) { Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); // transform it into world frame - var controllerPosition = Vec3.sum(MyAvatar.position, - Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation)); + var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation); + var controllerPosition = Vec3.sum(MyAvatar.position, controllerPositionVSAvatar); var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); @@ -1161,7 +1161,7 @@ function MyController(hand) { this.turnOffVisualizations(); - this.previousControllerPosition = controllerPosition; + this.previousControllerPositionVSAvatar = controllerPositionVSAvatar; this.previousControllerRotation = controllerRotation; }; @@ -1179,8 +1179,8 @@ function MyController(hand) { Controller.Standard.RightHand : Controller.Standard.LeftHand); // transform it into world frame - var controllerPosition = Vec3.sum(MyAvatar.position, - Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation)); + var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation); + var controllerPosition = Vec3.sum(MyAvatar.position, controllerPositionVSAvatar); var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); @@ -1197,7 +1197,8 @@ function MyController(hand) { } // scale delta controller hand movement by radius. - var handMoved = Vec3.multiply(Vec3.subtract(controllerPosition, this.previousControllerPosition), radius); + var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar), + radius); // double delta controller rotation var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation, @@ -1218,7 +1219,7 @@ function MyController(hand) { var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData); // Update radialVelocity - var lastVelocity = Vec3.subtract(controllerPosition, this.previousControllerPosition); + var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaTime); var newRadialVelocity = Vec3.dot(lastVelocity, Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); @@ -1266,7 +1267,9 @@ function MyController(hand) { var clampedVector; var targetPosition; if (constraintData.axisStart !== false) { - clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, constraintData.axisStart, constraintData.axisEnd); + clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, + constraintData.axisStart, + constraintData.axisEnd); targetPosition = clampedVector; } else { targetPosition = { @@ -1309,7 +1312,7 @@ function MyController(hand) { print("continueDistanceHolding -- updateAction failed"); } - this.previousControllerPosition = controllerPosition; + this.previousControllerPositionVSAvatar = controllerPositionVSAvatar; this.previousControllerRotation = controllerRotation; }; From 5a27d656c52d6524940d38a84d55799259c56e4b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 21 May 2016 13:03:58 +1200 Subject: [PATCH 66/77] Potential OSX fix plus extra logging --- interface/resources/qml/dialogs/FileDialog.qml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 78d5943479..b00ee76b5e 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -130,17 +130,23 @@ ModalWindow { choices = [], i, length; + console.log("####### folder parts: " + JSON.stringify(folders)); + if (folders[folders.length - 1] === "") { folders.pop(); } - if (folders[0] !== "") { - choices.push(folders[0]); - } + choices.push(folders[0]); for (i = 1, length = folders.length; i < length; i++) { choices.push(choices[i - 1] + "/" + folders[i]); } + + if (folders[0] === "") { + // Special handling for OSX root dir. + choices[0] = "/"; + } + choices.reverse(); if (drives && drives.length > 1) { @@ -154,6 +160,9 @@ ModalWindow { onLastValidFolderChanged: { var folder = d.capitalizeDrive(lastValidFolder); + + console.log("####### lastValidFolder: " + folder); + calculatePathChoices(folder); } From 637735bbc3e6099ed6648d31812a027d866ffcfb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 20 May 2016 18:07:38 -0700 Subject: [PATCH 67/77] unmangle merge --- libraries/entities/src/EntityItemProperties.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 03dc4a0557..99285b4986 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -690,13 +690,11 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslationsSet, qVectorBool, setJointTranslationsSet); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslations, qVectorVec3, setJointTranslations); -<<<<<<< HEAD - COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly); - COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID); -======= COPY_PROPERTY_FROM_QSCRIPTVALUE(flyingAllowed, bool, setFlyingAllowed); COPY_PROPERTY_FROM_QSCRIPTVALUE(ghostingAllowed, bool, setGhostingAllowed); ->>>>>>> e012630db23d8aba81fae0721f69322220b21be2 + + COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly); + COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID); _lastEdited = usecTimestampNow(); } @@ -1592,13 +1590,11 @@ void EntityItemProperties::markAllChanged() { _queryAACubeChanged = true; -<<<<<<< HEAD - _clientOnlyChanged = true; - _owningAvatarIDChanged = true; -======= _flyingAllowedChanged = true; _ghostingAllowedChanged = true; ->>>>>>> e012630db23d8aba81fae0721f69322220b21be2 + + _clientOnlyChanged = true; + _owningAvatarIDChanged = true; } // The minimum bounding box for the entity. From a294170c74271ed1cb4b40a6e809ed2067bb5ce9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 21 May 2016 15:50:06 +1200 Subject: [PATCH 68/77] Fix not being able to teleport to user when users window moved right --- scripts/system/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/users.js b/scripts/system/users.js index d935dd23ca..c010b7ea24 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -716,7 +716,7 @@ var usersWindow = (function () { if (clickedOverlay === windowPane) { - overlayX = event.x - WINDOW_MARGIN; + overlayX = event.x - windowPosition.x - WINDOW_MARGIN; overlayY = event.y - windowPosition.y + windowHeight - WINDOW_MARGIN - windowLineHeight; numLinesBefore = Math.round(overlayY / windowLineHeight); From 40e862cf9e624b50f91f267c87148ceeb7fef505 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 21 May 2016 16:53:01 -0700 Subject: [PATCH 69/77] quiet gcc 5 warnings --- .../octree/OctreeInboundPacketProcessor.cpp | 6 ++--- interface/src/Camera.h | 4 ++++ .../AssetMappingsScriptingInterface.cpp | 2 +- libraries/animation/src/AnimationCache.cpp | 2 +- libraries/audio/src/SoundCache.cpp | 2 +- .../src/controllers/UserInputMapper.cpp | 8 +++---- libraries/entities/src/EntityTypes.h | 2 +- libraries/fbx/src/FBXReader.cpp | 8 +++---- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 4 ++-- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 2 +- libraries/gpu/src/gpu/Texture.cpp | 2 +- libraries/networking/src/HifiSockAddr.cpp | 2 +- libraries/networking/src/Node.cpp | 6 ++--- libraries/networking/src/ReceivedMessage.cpp | 4 ++-- libraries/networking/src/udt/Packet.cpp | 2 +- .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketList.cpp | 2 +- libraries/render-utils/src/Model.cpp | 6 ++--- libraries/script-engine/src/ScriptEngine.cpp | 2 +- libraries/shared/src/RegisteredMetaTypes.cpp | 22 +++++++++---------- libraries/shared/src/SimpleMovingAverage.h | 2 +- tests/render-utils/src/main.cpp | 4 ---- 22 files changed, 48 insertions(+), 48 deletions(-) diff --git a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp index c5d010871c..32f3de2ff6 100644 --- a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp +++ b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp @@ -153,7 +153,7 @@ void OctreeInboundPacketProcessor::processPacket(QSharedPointer qDebug() << " maxSize=" << maxSize; qDebug("OctreeInboundPacketProcessor::processPacket() %hhu " "payload=%p payloadLength=%lld editData=%p payloadPosition=%lld maxSize=%d", - packetType, message->getRawMessage(), message->getSize(), editData, + (int)packetType, message->getRawMessage(), message->getSize(), editData, message->getPosition(), maxSize); } @@ -191,7 +191,7 @@ void OctreeInboundPacketProcessor::processPacket(QSharedPointer if (debugProcessPacket) { qDebug("OctreeInboundPacketProcessor::processPacket() DONE LOOPING FOR %hhu " "payload=%p payloadLength=%lld editData=%p payloadPosition=%lld", - packetType, message->getRawMessage(), message->getSize(), editData, message->getPosition()); + (int)packetType, message->getRawMessage(), message->getSize(), editData, message->getPosition()); } // Make sure our Node and NodeList knows we've heard from this node. @@ -208,7 +208,7 @@ void OctreeInboundPacketProcessor::processPacket(QSharedPointer } trackInboundPacket(nodeUUID, sequence, transitTime, editsInPacket, processTime, lockWaitTime); } else { - qDebug("unknown packet ignored... packetType=%hhu", packetType); + qDebug("unknown packet ignored... packetType=%hhu", (int)packetType); } } diff --git a/interface/src/Camera.h b/interface/src/Camera.h index 017bd742a4..486b98c100 100644 --- a/interface/src/Camera.h +++ b/interface/src/Camera.h @@ -29,6 +29,10 @@ enum CameraMode }; Q_DECLARE_METATYPE(CameraMode); + +#if defined(__GNUC__) && !defined(__clang__) +__attribute__((unused)) +#endif static int cameraModeId = qRegisterMetaType(); class Camera : public QObject { diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index 965b3a9e0c..f1198c9d5b 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -196,7 +196,7 @@ bool AssetMappingModel::isKnownFolder(QString path) const { return false; } -static int assetMappingModelMetatypeId = qRegisterMetaType("AssetMappingModel*"); +int assetMappingModelMetatypeId = qRegisterMetaType("AssetMappingModel*"); void AssetMappingModel::refresh() { qDebug() << "Refreshing asset mapping model"; diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index 482c4211cb..7601fbc782 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -15,7 +15,7 @@ #include "AnimationCache.h" #include "AnimationLogging.h" -static int animationPointerMetaTypeId = qRegisterMetaType(); +int animationPointerMetaTypeId = qRegisterMetaType(); AnimationCache::AnimationCache(QObject* parent) : ResourceCache(parent) diff --git a/libraries/audio/src/SoundCache.cpp b/libraries/audio/src/SoundCache.cpp index 96a2cee204..6b34c68959 100644 --- a/libraries/audio/src/SoundCache.cpp +++ b/libraries/audio/src/SoundCache.cpp @@ -14,7 +14,7 @@ #include "AudioLogging.h" #include "SoundCache.h" -static int soundPointerMetaTypeId = qRegisterMetaType(); +int soundPointerMetaTypeId = qRegisterMetaType(); SoundCache::SoundCache(QObject* parent) : ResourceCache(parent) diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index c0d3ff40c0..c1ee3ce36c 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -336,10 +336,10 @@ QVector UserInputMapper::getActionNames() const { return result; } -static int actionMetaTypeId = qRegisterMetaType(); -static int inputMetaTypeId = qRegisterMetaType
+
+ M +
+ +
+ + +
+
+ + +
+ -
M
diff --git a/tests/entities/src/main.cpp b/tests/entities/src/main.cpp index c0e21276d8..792ef7d9c6 100644 --- a/tests/entities/src/main.cpp +++ b/tests/entities/src/main.cpp @@ -16,7 +16,7 @@ #include #include -#include +#include #include #include #include @@ -155,7 +155,7 @@ int main(int argc, char** argv) { QFile file(getTestResourceDir() + "packet.bin"); if (!file.open(QIODevice::ReadOnly)) return -1; QByteArray packet = file.readAll(); - EntityItemPointer item = BoxEntityItem::factory(EntityItemID(), EntityItemProperties()); + EntityItemPointer item = ShapeEntityItem::boxFactory(EntityItemID(), EntityItemProperties()); ReadBitstreamToTreeParams params; params.bitstreamVersion = 33; From 2c703e963cdb9780c241381948806416deffdd6a Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 22 May 2016 17:41:50 -0700 Subject: [PATCH 72/77] More shapes --- .../src/RenderableShapeEntityItem.cpp | 4 +- libraries/entities/src/ShapeEntityItem.cpp | 2 +- libraries/entities/src/ShapeEntityItem.h | 2 +- libraries/render-utils/src/GeometryCache.cpp | 651 +++++++++--------- libraries/render-utils/src/GeometryCache.h | 4 +- scripts/system/html/entityProperties.html | 6 +- tests/gpu-test/CMakeLists.txt | 4 +- tests/gpu-test/src/TestHelpers.cpp | 20 + tests/gpu-test/src/TestHelpers.h | 33 + tests/gpu-test/src/TestInstancedShapes.cpp | 84 +++ tests/gpu-test/src/TestInstancedShapes.h | 23 + tests/gpu-test/src/TestShapes.cpp | 48 ++ tests/gpu-test/src/TestShapes.h | 22 + tests/gpu-test/src/TestWindow.cpp | 180 +++++ tests/gpu-test/src/TestWindow.h | 53 ++ tests/gpu-test/src/main.cpp | 539 +++------------ 16 files changed, 895 insertions(+), 780 deletions(-) create mode 100644 tests/gpu-test/src/TestHelpers.cpp create mode 100644 tests/gpu-test/src/TestHelpers.h create mode 100644 tests/gpu-test/src/TestInstancedShapes.cpp create mode 100644 tests/gpu-test/src/TestInstancedShapes.h create mode 100644 tests/gpu-test/src/TestShapes.cpp create mode 100644 tests/gpu-test/src/TestShapes.h create mode 100644 tests/gpu-test/src/TestWindow.cpp create mode 100644 tests/gpu-test/src/TestWindow.h diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 7d30b7a47c..c93ae252e3 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -30,7 +30,7 @@ static GeometryCache::Shape MAPPING[entity::NUM_SHAPES] = { GeometryCache::Cube, GeometryCache::Sphere, GeometryCache::Tetrahedron, - GeometryCache::Octahetron, + GeometryCache::Octahedron, GeometryCache::Dodecahedron, GeometryCache::Icosahedron, GeometryCache::Torus, @@ -93,7 +93,7 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { if (!success) { return; } - if (_shape != entity::Cube) { + if (_shape == entity::Sphere) { modelTransform.postScale(SPHERE_ENTITY_SCALE); } batch.setModelTransform(modelTransform); // use a transform with scale, rotation, registration point and translation diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index a24c7e1df5..2527dedab2 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -27,7 +27,7 @@ namespace entity { "Cube", "Sphere", "Tetrahedron", - "Octahetron", + "Octahedron", "Dodecahedron", "Icosahedron", "Torus", diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index f3cb2abd66..2ae4ae2ca1 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -19,7 +19,7 @@ namespace entity { Cube, Sphere, Tetrahedron, - Octahetron, + Octahedron, Dodecahedron, Icosahedron, Torus, diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 6852d17882..02aca4216e 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -51,8 +51,8 @@ static gpu::Stream::FormatPointer INSTANCED_SOLID_STREAM_FORMAT; static const uint SHAPE_VERTEX_STRIDE = sizeof(glm::vec3) * 2; // vertices and normals static const uint SHAPE_NORMALS_OFFSET = sizeof(glm::vec3); -static const gpu::Type SHAPE_INDEX_TYPE = gpu::UINT16; -static const uint SHAPE_INDEX_SIZE = sizeof(gpu::uint16); +static const gpu::Type SHAPE_INDEX_TYPE = gpu::UINT32; +static const uint SHAPE_INDEX_SIZE = sizeof(gpu::uint32); void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const VertexVector& vertices) { vertexBuffer->append(vertices); @@ -112,102 +112,7 @@ void GeometryCache::ShapeData::drawWireInstances(gpu::Batch& batch, size_t count } } -// The golden ratio -static const float PHI = 1.61803398874f; - -const VertexVector& icosahedronVertices() { - static const float a = 1; - static const float b = PHI / 2.0f; - - static const VertexVector vertices{ // - vec3(0, b, -a), vec3(-b, a, 0), vec3(b, a, 0), // - vec3(0, b, a), vec3(b, a, 0), vec3(-b, a, 0), // - vec3(0, b, a), vec3(-a, 0, b), vec3(0, -b, a), // - vec3(0, b, a), vec3(0, -b, a), vec3(a, 0, b), // - vec3(0, b, -a), vec3(a, 0, -b), vec3(0, -b, -a),// - vec3(0, b, -a), vec3(0, -b, -a), vec3(-a, 0, -b), // - vec3(0, -b, a), vec3(-b, -a, 0), vec3(b, -a, 0), // - vec3(0, -b, -a), vec3(b, -a, 0), vec3(-b, -a, 0), // - vec3(-b, a, 0), vec3(-a, 0, -b), vec3(-a, 0, b), // - vec3(-b, -a, 0), vec3(-a, 0, b), vec3(-a, 0, -b), // - vec3(b, a, 0), vec3(a, 0, b), vec3(a, 0, -b), // - vec3(b, -a, 0), vec3(a, 0, -b), vec3(a, 0, b), // - vec3(0, b, a), vec3(-b, a, 0), vec3(-a, 0, b), // - vec3(0, b, a), vec3(a, 0, b), vec3(b, a, 0), // - vec3(0, b, -a), vec3(-a, 0, -b), vec3(-b, a, 0), // - vec3(0, b, -a), vec3(b, a, 0), vec3(a, 0, -b), // - vec3(0, -b, -a), vec3(-b, -a, 0), vec3(-a, 0, -b), // - vec3(0, -b, -a), vec3(a, 0, -b), vec3(b, -a, 0), // - vec3(0, -b, a), vec3(-a, 0, b), vec3(-b, -a, 0), // - vec3(0, -b, a), vec3(b, -a, 0), vec3(a, 0, b) - }; // - return vertices; -} - -const VertexVector& tetrahedronVertices() { - static const auto A = vec3(1, 1, 1); - static const auto B = vec3(1, -1, -1); - static const auto C = vec3(-1, 1, -1); - static const auto D = vec3(-1, -1, 1); - static const VertexVector vertices{ - A, B, C, - D, B, A, - C, D, A, - C, B, D, - }; - return vertices; -} - -static const size_t TESSELTATION_MULTIPLIER = 4; static const size_t ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT = 3; -static const size_t VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER = 2; - - -VertexVector tesselate(const VertexVector& startingTriangles, int count) { - VertexVector triangles = startingTriangles; - if (0 != (triangles.size() % 3)) { - throw std::runtime_error("Bad number of vertices for tesselation"); - } - - for (size_t i = 0; i < triangles.size(); ++i) { - triangles[i] = glm::normalize(triangles[i]); - } - - VertexVector newTriangles; - while (count) { - newTriangles.clear(); - // Tesselation takes one triangle and makes it into 4 triangles - // See https://en.wikipedia.org/wiki/Space-filling_tree#/media/File:Space_Filling_Tree_Tri_iter_1_2_3.png - newTriangles.reserve(triangles.size() * TESSELTATION_MULTIPLIER); - for (size_t i = 0; i < triangles.size(); i += VERTICES_PER_TRIANGLE) { - const vec3& a = triangles[i]; - const vec3& b = triangles[i + 1]; - const vec3& c = triangles[i + 2]; - vec3 ab = glm::normalize(a + b); - vec3 bc = glm::normalize(b + c); - vec3 ca = glm::normalize(c + a); - - newTriangles.push_back(a); - newTriangles.push_back(ab); - newTriangles.push_back(ca); - - newTriangles.push_back(b); - newTriangles.push_back(bc); - newTriangles.push_back(ab); - - newTriangles.push_back(c); - newTriangles.push_back(ca); - newTriangles.push_back(bc); - - newTriangles.push_back(ab); - newTriangles.push_back(bc); - newTriangles.push_back(ca); - } - triangles.swap(newTriangles); - --count; - } - return triangles; -} size_t GeometryCache::getShapeTriangleCount(Shape shape) { return _shapes[shape]._indexCount / VERTICES_PER_TRIANGLE; @@ -221,6 +126,324 @@ size_t GeometryCache::getCubeTriangleCount() { return getShapeTriangleCount(Cube); } +using Index = uint32_t; +using IndexPair = uint64_t; +using IndexPairs = std::unordered_set; + +template +using Face = std::array; + +template +using FaceVector = std::vector>; + +template +struct Solid { + VertexVector vertices; + FaceVector faces; + + Solid& fitDimension(float newMaxDimension) { + float maxDimension = 0; + for (const auto& vertex : vertices) { + maxDimension = std::max(maxDimension, std::max(std::max(vertex.x, vertex.y), vertex.z)); + } + float multiplier = newMaxDimension / maxDimension; + for (auto& vertex : vertices) { + vertex *= multiplier; + } + return *this; + } + + vec3 getFaceNormal(size_t faceIndex) const { + vec3 result; + const auto& face = faces[faceIndex]; + for (size_t i = 0; i < N; ++i) { + result += vertices[face[i]]; + } + result /= N; + return glm::normalize(result); + } +}; + +template +static size_t triangulatedFaceTriangleCount() { + return N - 2; +} + +template +static size_t triangulatedFaceIndexCount() { + return triangulatedFaceTriangleCount() * VERTICES_PER_TRIANGLE; +} + +static IndexPair indexToken(Index a, Index b) { + if (a > b) { + std::swap(a, b); + } + return (((IndexPair)a) << 32) | ((IndexPair)b); +} + +static Solid<3> tesselate(Solid<3> solid, int count) { + float length = glm::length(solid.vertices[0]); + for (int i = 0; i < count; ++i) { + Solid<3> result { solid.vertices, {} }; + result.vertices.reserve(solid.vertices.size() + solid.faces.size() * 3); + for (size_t f = 0; f < solid.faces.size(); ++f) { + Index baseVertex = (Index)result.vertices.size(); + const Face<3>& oldFace = solid.faces[f]; + const vec3& a = solid.vertices[oldFace[0]]; + const vec3& b = solid.vertices[oldFace[1]]; + const vec3& c = solid.vertices[oldFace[2]]; + vec3 ab = glm::normalize(a + b) * length; + vec3 bc = glm::normalize(b + c) * length; + vec3 ca = glm::normalize(c + a) * length; + result.vertices.push_back(ab); + result.vertices.push_back(bc); + result.vertices.push_back(ca); + result.faces.push_back(Face<3>{ { oldFace[0], baseVertex, baseVertex + 2 } }); + result.faces.push_back(Face<3>{ { baseVertex, oldFace[1], baseVertex + 1 } }); + result.faces.push_back(Face<3>{ { baseVertex + 1, oldFace[2], baseVertex + 2 } }); + result.faces.push_back(Face<3>{ { baseVertex, baseVertex + 1, baseVertex + 2 } }); + } + solid = result; + } + return solid; +} + +template +void setupFlatShape(GeometryCache::ShapeData& shapeData, const Solid& shape, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { + Index baseVertex = (Index)(vertexBuffer->getSize() / SHAPE_VERTEX_STRIDE); + VertexVector vertices; + IndexVector solidIndices, wireIndices; + IndexPairs wireSeenIndices; + + size_t faceCount = shape.faces.size(); + size_t faceIndexCount = triangulatedFaceIndexCount(); + + vertices.reserve(N * faceCount * 2); + solidIndices.reserve(faceIndexCount * faceCount); + + for (size_t f = 0; f < faceCount; ++f) { + const Face& face = shape.faces[f]; + // Compute the face normal + vec3 faceNormal = shape.getFaceNormal(f); + + // Create the vertices for the face + for (Index i = 0; i < N; ++i) { + Index originalIndex = face[i]; + vertices.push_back(shape.vertices[originalIndex]); + vertices.push_back(faceNormal); + } + + // Create the wire indices for unseen edges + for (Index i = 0; i < N; ++i) { + Index a = i; + Index b = (i + 1) % N; + auto token = indexToken(face[a], face[b]); + if (0 == wireSeenIndices.count(token)) { + wireSeenIndices.insert(token); + wireIndices.push_back(a + baseVertex); + wireIndices.push_back(b + baseVertex); + } + } + + // Create the solid face indices + for (Index i = 0; i < N - 2; ++i) { + solidIndices.push_back(0 + baseVertex); + solidIndices.push_back(i + 1 + baseVertex); + solidIndices.push_back(i + 2 + baseVertex); + } + baseVertex += (Index)N; + } + + shapeData.setupVertices(vertexBuffer, vertices); + shapeData.setupIndices(indexBuffer, solidIndices, wireIndices); +} + +template +void setupSmoothShape(GeometryCache::ShapeData& shapeData, const Solid& shape, gpu::BufferPointer& vertexBuffer, gpu::BufferPointer& indexBuffer) { + Index baseVertex = (Index)(vertexBuffer->getSize() / SHAPE_VERTEX_STRIDE); + + VertexVector vertices; + vertices.reserve(shape.vertices.size() * 2); + for (const auto& vertex : shape.vertices) { + vertices.push_back(vertex); + vertices.push_back(vertex); + } + + IndexVector solidIndices, wireIndices; + IndexPairs wireSeenIndices; + + size_t faceCount = shape.faces.size(); + size_t faceIndexCount = triangulatedFaceIndexCount(); + + solidIndices.reserve(faceIndexCount * faceCount); + + for (size_t f = 0; f < faceCount; ++f) { + const Face& face = shape.faces[f]; + // Create the wire indices for unseen edges + for (Index i = 0; i < N; ++i) { + Index a = face[i]; + Index b = face[(i + 1) % N]; + auto token = indexToken(a, b); + if (0 == wireSeenIndices.count(token)) { + wireSeenIndices.insert(token); + wireIndices.push_back(a + baseVertex); + wireIndices.push_back(b + baseVertex); + } + } + + // Create the solid face indices + for (Index i = 0; i < N - 2; ++i) { + solidIndices.push_back(face[i] + baseVertex); + solidIndices.push_back(face[i + 1] + baseVertex); + solidIndices.push_back(face[i + 2] + baseVertex); + } + } + + shapeData.setupVertices(vertexBuffer, vertices); + shapeData.setupIndices(indexBuffer, solidIndices, wireIndices); +} + +// The golden ratio +static const float PHI = 1.61803398874f; + +static const Solid<3>& tetrahedron() { + static const auto A = vec3(1, 1, 1); + static const auto B = vec3(1, -1, -1); + static const auto C = vec3(-1, 1, -1); + static const auto D = vec3(-1, -1, 1); + static const Solid<3> TETRAHEDRON = Solid<3>{ + { A, B, C, D }, + FaceVector<3>{ + Face<3> { { 0, 1, 2 } }, + Face<3> { { 3, 1, 0 } }, + Face<3> { { 2, 3, 0 } }, + Face<3> { { 2, 1, 3 } }, + } + }.fitDimension(0.5f); + return TETRAHEDRON; +} + +static const Solid<4>& cube() { + static const auto A = vec3(1, 1, 1); + static const auto B = vec3(-1, 1, 1); + static const auto C = vec3(-1, 1, -1); + static const auto D = vec3(1, 1, -1); + static const Solid<4> CUBE = Solid<4>{ + { A, B, C, D, -A, -B, -C, -D }, + FaceVector<4>{ + Face<4> { { 3, 2, 1, 0 } }, + Face<4> { { 0, 1, 7, 6 } }, + Face<4> { { 1, 2, 4, 7 } }, + Face<4> { { 2, 3, 5, 4 } }, + Face<4> { { 3, 0, 6, 5 } }, + Face<4> { { 4, 5, 6, 7 } }, + } + }.fitDimension(0.5f); + return CUBE; +} + +static const Solid<3>& octahedron() { + static const auto A = vec3(0, 1, 0); + static const auto B = vec3(0, -1, 0); + static const auto C = vec3(0, 0, 1); + static const auto D = vec3(0, 0, -1); + static const auto E = vec3(1, 0, 0); + static const auto F = vec3(-1, 0, 0); + static const Solid<3> OCTAHEDRON = Solid<3>{ + { A, B, C, D, E, F}, + FaceVector<3> { + Face<3> { { 0, 2, 4, } }, + Face<3> { { 0, 4, 3, } }, + Face<3> { { 0, 3, 5, } }, + Face<3> { { 0, 5, 2, } }, + Face<3> { { 1, 4, 2, } }, + Face<3> { { 1, 3, 4, } }, + Face<3> { { 1, 5, 3, } }, + Face<3> { { 1, 2, 5, } }, + } + }.fitDimension(0.5f); + return OCTAHEDRON; +} + +static const Solid<5>& dodecahedron() { + static const float P = PHI; + static const float IP = 1.0f / PHI; + static const vec3 A = vec3(IP, P, 0); + static const vec3 B = vec3(-IP, P, 0); + static const vec3 C = vec3(-1, 1, 1); + static const vec3 D = vec3(0, IP, P); + static const vec3 E = vec3(1, 1, 1); + static const vec3 F = vec3(1, 1, -1); + static const vec3 G = vec3(-1, 1, -1); + static const vec3 H = vec3(-P, 0, IP); + static const vec3 I = vec3(0, -IP, P); + static const vec3 J = vec3(P, 0, IP); + + static const Solid<5> DODECAHEDRON = Solid<5>{ + { + A, B, C, D, E, F, G, H, I, J, + -A, -B, -C, -D, -E, -F, -G, -H, -I, -J, + }, + FaceVector<5> { + Face<5> { { 0, 1, 2, 3, 4 } }, + Face<5> { { 0, 5, 18, 6, 1 } }, + Face<5> { { 1, 6, 19, 7, 2 } }, + Face<5> { { 2, 7, 15, 8, 3 } }, + Face<5> { { 3, 8, 16, 9, 4 } }, + Face<5> { { 4, 9, 17, 5, 0 } }, + Face<5> { { 14, 13, 12, 11, 10 } }, + Face<5> { { 11, 16, 8, 15, 10 } }, + Face<5> { { 12, 17, 9, 16, 11 } }, + Face<5> { { 13, 18, 5, 17, 12 } }, + Face<5> { { 14, 19, 6, 18, 13 } }, + Face<5> { { 10, 15, 7, 19, 14 } }, + } + }.fitDimension(0.5f); + return DODECAHEDRON; +} + +static const Solid<3>& icosahedron() { + static const float N = 1.0f / PHI; + static const float P = 1.0f; + static const auto A = vec3(N, P, 0); + static const auto B = vec3(-N, P, 0); + static const auto C = vec3(0, N, P); + static const auto D = vec3(P, 0, N); + static const auto E = vec3(P, 0, -N); + static const auto F = vec3(0, N, -P); + + static const Solid<3> ICOSAHEDRON = Solid<3> { + { + A, B, C, D, E, F, + -A, -B, -C, -D, -E, -F, + }, + FaceVector<3> { + Face<3> { { 1, 2, 0 } }, + Face<3> { { 2, 3, 0 } }, + Face<3> { { 3, 4, 0 } }, + Face<3> { { 4, 5, 0 } }, + Face<3> { { 5, 1, 0 } }, + + Face<3> { { 1, 10, 2 } }, + Face<3> { { 11, 2, 10 } }, + Face<3> { { 2, 11, 3 } }, + Face<3> { { 7, 3, 11 } }, + Face<3> { { 3, 7, 4 } }, + Face<3> { { 8, 4, 7 } }, + Face<3> { { 4, 8, 5 } }, + Face<3> { { 9, 5, 8 } }, + Face<3> { { 5, 9, 1 } }, + Face<3> { { 10, 1, 9 } }, + + Face<3> { { 8, 7, 6 } }, + Face<3> { { 9, 8, 6 } }, + Face<3> { { 10, 9, 6 } }, + Face<3> { { 11, 10, 6 } }, + Face<3> { { 7, 11, 6 } }, + } + }.fitDimension(0.5f); + return ICOSAHEDRON; +} // FIXME solids need per-face vertices, but smooth shaded // components do not. Find a way to support using draw elements @@ -230,229 +453,28 @@ size_t GeometryCache::getCubeTriangleCount() { void GeometryCache::buildShapes() { auto vertexBuffer = std::make_shared(); auto indexBuffer = std::make_shared(); - size_t startingIndex = 0; - // Cube - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; - { - ShapeData& shapeData = _shapes[Cube]; - VertexVector vertices; - // front - vertices.push_back(vec3(1, 1, 1)); - vertices.push_back(vec3(0, 0, 1)); - vertices.push_back(vec3(-1, 1, 1)); - vertices.push_back(vec3(0, 0, 1)); - vertices.push_back(vec3(-1, -1, 1)); - vertices.push_back(vec3(0, 0, 1)); - vertices.push_back(vec3(1, -1, 1)); - vertices.push_back(vec3(0, 0, 1)); - - // right - vertices.push_back(vec3(1, 1, 1)); - vertices.push_back(vec3(1, 0, 0)); - vertices.push_back(vec3(1, -1, 1)); - vertices.push_back(vec3(1, 0, 0)); - vertices.push_back(vec3(1, -1, -1)); - vertices.push_back(vec3(1, 0, 0)); - vertices.push_back(vec3(1, 1, -1)); - vertices.push_back(vec3(1, 0, 0)); - - // top - vertices.push_back(vec3(1, 1, 1)); - vertices.push_back(vec3(0, 1, 0)); - vertices.push_back(vec3(1, 1, -1)); - vertices.push_back(vec3(0, 1, 0)); - vertices.push_back(vec3(-1, 1, -1)); - vertices.push_back(vec3(0, 1, 0)); - vertices.push_back(vec3(-1, 1, 1)); - vertices.push_back(vec3(0, 1, 0)); - - // left - vertices.push_back(vec3(-1, 1, 1)); - vertices.push_back(vec3(-1, 0, 0)); - vertices.push_back(vec3(-1, 1, -1)); - vertices.push_back(vec3(-1, 0, 0)); - vertices.push_back(vec3(-1, -1, -1)); - vertices.push_back(vec3(-1, 0, 0)); - vertices.push_back(vec3(-1, -1, 1)); - vertices.push_back(vec3(-1, 0, 0)); - - // bottom - vertices.push_back(vec3(-1, -1, -1)); - vertices.push_back(vec3(0, -1, 0)); - vertices.push_back(vec3(1, -1, -1)); - vertices.push_back(vec3(0, -1, 0)); - vertices.push_back(vec3(1, -1, 1)); - vertices.push_back(vec3(0, -1, 0)); - vertices.push_back(vec3(-1, -1, 1)); - vertices.push_back(vec3(0, -1, 0)); - - // back - vertices.push_back(vec3(1, -1, -1)); - vertices.push_back(vec3(0, 0, -1)); - vertices.push_back(vec3(-1, -1, -1)); - vertices.push_back(vec3(0, 0, -1)); - vertices.push_back(vec3(-1, 1, -1)); - vertices.push_back(vec3(0, 0, -1)); - vertices.push_back(vec3(1, 1, -1)); - vertices.push_back(vec3(0, 0, -1)); - - static const size_t VERTEX_FORMAT_SIZE = 2; - static const size_t VERTEX_OFFSET = 0; - - for (size_t i = 0; i < vertices.size(); ++i) { - auto vertexIndex = i; - // Make a unit cube by having the vertices (at index N) - // while leaving the normals (at index N + 1) alone - if (VERTEX_OFFSET == vertexIndex % VERTEX_FORMAT_SIZE) { - vertices[vertexIndex] *= 0.5f; - } - } - shapeData.setupVertices(_shapeVertices, vertices); - - IndexVector indices{ - 0, 1, 2, 2, 3, 0, // front - 4, 5, 6, 6, 7, 4, // right - 8, 9, 10, 10, 11, 8, // top - 12, 13, 14, 14, 15, 12, // left - 16, 17, 18, 18, 19, 16, // bottom - 20, 21, 22, 22, 23, 20 // back - }; - for (auto& index : indices) { - index += (uint16_t)startingIndex; - } - - IndexVector wireIndices{ - 0, 1, 1, 2, 2, 3, 3, 0, // front - 20, 21, 21, 22, 22, 23, 23, 20, // back - 0, 23, 1, 22, 2, 21, 3, 20 // sides - }; - - for (size_t i = 0; i < wireIndices.size(); ++i) { - indices[i] += (uint16_t)startingIndex; - } - - shapeData.setupIndices(_shapeIndices, indices, wireIndices); - } - + setupFlatShape(_shapes[Cube], cube(), _shapeVertices, _shapeIndices); // Tetrahedron - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; - { - ShapeData& shapeData = _shapes[Tetrahedron]; - size_t vertexCount = 4; - VertexVector vertices; - { - VertexVector originalVertices = tetrahedronVertices(); - vertexCount = originalVertices.size(); - vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER); - for (size_t i = 0; i < originalVertices.size(); i += VERTICES_PER_TRIANGLE) { - auto triangleStartIndex = i; - vec3 faceNormal; - for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - faceNormal += originalVertices[vertexIndex]; - } - faceNormal = glm::normalize(faceNormal); - for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - vertices.push_back(originalVertices[vertexIndex]); - vertices.push_back(faceNormal); - } - } - } - shapeData.setupVertices(_shapeVertices, vertices); - - IndexVector indices; - for (size_t i = 0; i < vertexCount; i += VERTICES_PER_TRIANGLE) { - auto triangleStartIndex = i; - for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - indices.push_back((uint16_t)(vertexIndex + startingIndex)); - } - } - - IndexVector wireIndices{ - 0, 1, 1, 2, 2, 0, - 0, 3, 1, 3, 2, 3, - }; - - for (size_t i = 0; i < wireIndices.size(); ++i) { - wireIndices[i] += (uint16_t)startingIndex; - } - - shapeData.setupIndices(_shapeIndices, indices, wireIndices); - } - + setupFlatShape(_shapes[Tetrahedron], tetrahedron(), _shapeVertices, _shapeIndices); + // Icosahedron + setupFlatShape(_shapes[Icosahedron], icosahedron(), _shapeVertices, _shapeIndices); + // Octahedron + setupFlatShape(_shapes[Octahedron], octahedron(), _shapeVertices, _shapeIndices); + // Dodecahedron + setupFlatShape(_shapes[Dodecahedron], dodecahedron(), _shapeVertices, _shapeIndices); + // Sphere // FIXME this uses way more vertices than required. Should find a way to calculate the indices // using shared vertices for better vertex caching - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; - { - ShapeData& shapeData = _shapes[Sphere]; - VertexVector vertices; - IndexVector indices; - { - VertexVector originalVertices = tesselate(icosahedronVertices(), ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT); - vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER); - for (size_t i = 0; i < originalVertices.size(); i += VERTICES_PER_TRIANGLE) { - auto triangleStartIndex = i; - for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - const auto& vertex = originalVertices[i + j]; - // Spheres use the same values for vertices and normals - vertices.push_back(vertex); - vertices.push_back(vertex); - indices.push_back((uint16_t)(vertexIndex + startingIndex)); - } - } - } - - shapeData.setupVertices(_shapeVertices, vertices); - // FIXME don't use solid indices for wire drawing. - shapeData.setupIndices(_shapeIndices, indices, indices); - } - - // Icosahedron - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; - { - ShapeData& shapeData = _shapes[Icosahedron]; - - VertexVector vertices; - IndexVector indices; - { - const VertexVector& originalVertices = icosahedronVertices(); - vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER); - for (size_t i = 0; i < originalVertices.size(); i += 3) { - auto triangleStartIndex = i; - vec3 faceNormal; - for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - faceNormal += originalVertices[vertexIndex]; - } - faceNormal = glm::normalize(faceNormal); - for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) { - auto triangleVertexIndex = j; - auto vertexIndex = triangleStartIndex + triangleVertexIndex; - vertices.push_back(originalVertices[vertexIndex]); - vertices.push_back(faceNormal); - indices.push_back((uint16_t)(vertexIndex + startingIndex)); - } - } - } - - shapeData.setupVertices(_shapeVertices, vertices); - // FIXME don't use solid indices for wire drawing. - shapeData.setupIndices(_shapeIndices, indices, indices); - } + Solid<3> sphere = icosahedron(); + sphere = tesselate(sphere, ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT); + sphere.fitDimension(1.0f); + setupSmoothShape(_shapes[Sphere], sphere, _shapeVertices, _shapeIndices); // Line - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; { + Index baseVertex = (Index)(_shapeVertices->getSize() / SHAPE_VERTEX_STRIDE); ShapeData& shapeData = _shapes[Line]; shapeData.setupVertices(_shapeVertices, VertexVector{ vec3(-0.5, 0, 0), vec3(-0.5f, 0, 0), @@ -460,9 +482,8 @@ void GeometryCache::buildShapes() { }); IndexVector wireIndices; // Only two indices - wireIndices.push_back(0 + (uint16_t)startingIndex); - wireIndices.push_back(1 + (uint16_t)startingIndex); - + wireIndices.push_back(0 + baseVertex); + wireIndices.push_back(1 + baseVertex); shapeData.setupIndices(_shapeIndices, IndexVector(), wireIndices); } diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 7fa543abe2..a2f79de029 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -121,8 +121,8 @@ inline uint qHash(const Vec4PairVec4Pair& v, uint seed) { seed); } +using IndexVector = std::vector; using VertexVector = std::vector; -using IndexVector = std::vector; /// Stores cached geometry. class GeometryCache : public Dependency { @@ -137,7 +137,7 @@ public: Cube, Sphere, Tetrahedron, - Octahetron, + Octahedron, Dodecahedron, Icosahedron, Torus, diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 4a3b5a14a4..c611fc5b38 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1368,10 +1368,12 @@
diff --git a/tests/gpu-test/CMakeLists.txt b/tests/gpu-test/CMakeLists.txt index 4fc6143ff5..21ae9c5a99 100644 --- a/tests/gpu-test/CMakeLists.txt +++ b/tests/gpu-test/CMakeLists.txt @@ -4,4 +4,6 @@ AUTOSCRIBE_SHADER_LIB(gpu model render-utils) setup_hifi_project(Quick Gui OpenGL Script Widgets) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries(networking gl gpu gpu-gl procedural shared fbx model model-networking animation script-engine render render-utils ) -package_libraries_for_deployment() \ No newline at end of file +package_libraries_for_deployment() + +target_nsight() diff --git a/tests/gpu-test/src/TestHelpers.cpp b/tests/gpu-test/src/TestHelpers.cpp new file mode 100644 index 0000000000..75586da904 --- /dev/null +++ b/tests/gpu-test/src/TestHelpers.cpp @@ -0,0 +1,20 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestHelpers.h" + +gpu::ShaderPointer makeShader(const std::string & vertexShaderSrc, const std::string & fragmentShaderSrc, const gpu::Shader::BindingSet & bindings) { + auto vs = gpu::Shader::createVertex(vertexShaderSrc); + auto fs = gpu::Shader::createPixel(fragmentShaderSrc); + auto shader = gpu::Shader::createProgram(vs, fs); + if (!gpu::Shader::makeProgram(*shader, bindings)) { + printf("Could not compile shader\n"); + exit(-1); + } + return shader; +} diff --git a/tests/gpu-test/src/TestHelpers.h b/tests/gpu-test/src/TestHelpers.h new file mode 100644 index 0000000000..fd8989f628 --- /dev/null +++ b/tests/gpu-test/src/TestHelpers.h @@ -0,0 +1,33 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +class GpuTestBase { +public: + virtual ~GpuTestBase() {} + virtual bool isReady() const { return true; } + virtual size_t getTestCount() const { return 1; } + virtual void renderTest(size_t test, RenderArgs* args) = 0; +}; + +uint32_t toCompactColor(const glm::vec4& color); +gpu::ShaderPointer makeShader(const std::string & vertexShaderSrc, const std::string & fragmentShaderSrc, const gpu::Shader::BindingSet & bindings); + diff --git a/tests/gpu-test/src/TestInstancedShapes.cpp b/tests/gpu-test/src/TestInstancedShapes.cpp new file mode 100644 index 0000000000..6a98ee58b9 --- /dev/null +++ b/tests/gpu-test/src/TestInstancedShapes.cpp @@ -0,0 +1,84 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestInstancedShapes.h" + +gpu::Stream::FormatPointer& getInstancedSolidStreamFormat(); + +static const size_t TYPE_COUNT = 4; +static const size_t ITEM_COUNT = 50; +static const float SHAPE_INTERVAL = (PI * 2.0f) / ITEM_COUNT; +static const float ITEM_INTERVAL = SHAPE_INTERVAL / TYPE_COUNT; + +static GeometryCache::Shape SHAPE[TYPE_COUNT] = { + GeometryCache::Icosahedron, + GeometryCache::Cube, + GeometryCache::Sphere, + GeometryCache::Tetrahedron, +}; + +const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +const gpu::Element NORMAL_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +const gpu::Element COLOR_ELEMENT { gpu::VEC4, gpu::NUINT8, gpu::RGBA }; + + +TestInstancedShapes::TestInstancedShapes() { + auto geometryCache = DependencyManager::get(); + colorBuffer = std::make_shared(); + + static const float ITEM_RADIUS = 20; + static const vec3 ITEM_TRANSLATION { 0, 0, -ITEM_RADIUS }; + for (size_t i = 0; i < TYPE_COUNT; ++i) { + GeometryCache::Shape shape = SHAPE[i]; + GeometryCache::ShapeData shapeData = geometryCache->_shapes[shape]; + //indirectCommand._count + float startingInterval = ITEM_INTERVAL * i; + std::vector typeTransforms; + for (size_t j = 0; j < ITEM_COUNT; ++j) { + float theta = j * SHAPE_INTERVAL + startingInterval; + auto transform = glm::rotate(mat4(), theta, Vectors::UP); + transform = glm::rotate(transform, (randFloat() - 0.5f) * PI / 4.0f, Vectors::UNIT_X); + transform = glm::translate(transform, ITEM_TRANSLATION); + transform = glm::scale(transform, vec3(randFloat() / 2.0f + 0.5f)); + typeTransforms.push_back(transform); + auto color = vec4 { randomColorValue(64), randomColorValue(64), randomColorValue(64), 255 }; + color /= 255.0f; + colors.push_back(color); + colorBuffer->append(toCompactColor(color)); + } + transforms.push_back(typeTransforms); + } +} + +void TestInstancedShapes::renderTest(size_t testId, RenderArgs* args) { + gpu::Batch& batch = *(args->_batch); + auto geometryCache = DependencyManager::get(); + geometryCache->bindSimpleProgram(batch); + batch.setInputFormat(getInstancedSolidStreamFormat()); + for (size_t i = 0; i < TYPE_COUNT; ++i) { + GeometryCache::Shape shape = SHAPE[i]; + GeometryCache::ShapeData shapeData = geometryCache->_shapes[shape]; + + std::string namedCall = __FUNCTION__ + std::to_string(i); + + //batch.addInstanceModelTransforms(transforms[i]); + for (size_t j = 0; j < ITEM_COUNT; ++j) { + batch.setModelTransform(transforms[i][j]); + batch.setupNamedCalls(namedCall, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData&) { + batch.setInputBuffer(gpu::Stream::COLOR, gpu::BufferView(colorBuffer, i * ITEM_COUNT * 4, colorBuffer->getSize(), COLOR_ELEMENT)); + shapeData.drawInstances(batch, ITEM_COUNT); + }); + } + + //for (size_t j = 0; j < ITEM_COUNT; ++j) { + // batch.setModelTransform(transforms[j + i * ITEM_COUNT]); + // shapeData.draw(batch); + //} + } +} + diff --git a/tests/gpu-test/src/TestInstancedShapes.h b/tests/gpu-test/src/TestInstancedShapes.h new file mode 100644 index 0000000000..b509a13e60 --- /dev/null +++ b/tests/gpu-test/src/TestInstancedShapes.h @@ -0,0 +1,23 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include "TestHelpers.h" + +class TestInstancedShapes : public GpuTestBase { + + std::vector> transforms; + std::vector colors; + gpu::BufferPointer colorBuffer; + gpu::BufferView instanceXfmView; +public: + TestInstancedShapes(); + void renderTest(size_t testId, RenderArgs* args) override; +}; + diff --git a/tests/gpu-test/src/TestShapes.cpp b/tests/gpu-test/src/TestShapes.cpp new file mode 100644 index 0000000000..253d89cf61 --- /dev/null +++ b/tests/gpu-test/src/TestShapes.cpp @@ -0,0 +1,48 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestShapes.h" + +static const size_t TYPE_COUNT = 6; + +static GeometryCache::Shape SHAPE[TYPE_COUNT] = { + GeometryCache::Cube, + GeometryCache::Tetrahedron, + GeometryCache::Octahedron, + GeometryCache::Dodecahedron, + GeometryCache::Icosahedron, + GeometryCache::Sphere, +}; + +void TestShapes::renderTest(size_t testId, RenderArgs* args) { + gpu::Batch& batch = *(args->_batch); + auto geometryCache = DependencyManager::get(); + geometryCache->bindSimpleProgram(batch); + + // Render unlit cube + sphere + static auto startSecs = secTimestampNow(); + float seconds = secTimestampNow() - startSecs; + seconds /= 4.0f; + batch.setModelTransform(Transform()); + batch._glColor4f(0.8f, 0.25f, 0.25f, 1.0f); + + bool wire = (seconds - floorf(seconds) > 0.5f); + int shapeIndex = ((int)seconds) % TYPE_COUNT; + if (wire) { + geometryCache->renderWireShape(batch, SHAPE[shapeIndex]); + } else { + geometryCache->renderShape(batch, SHAPE[shapeIndex]); + } + + batch.setModelTransform(Transform().setScale(1.01f)); + batch._glColor4f(1, 1, 1, 1); + geometryCache->renderWireCube(batch); +} + + + diff --git a/tests/gpu-test/src/TestShapes.h b/tests/gpu-test/src/TestShapes.h new file mode 100644 index 0000000000..606d3a45f7 --- /dev/null +++ b/tests/gpu-test/src/TestShapes.h @@ -0,0 +1,22 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include "TestHelpers.h" + +class TestShapes : public GpuTestBase { + + std::vector> transforms; + std::vector colors; + gpu::BufferPointer colorBuffer; + gpu::BufferView instanceXfmView; +public: + void renderTest(size_t testId, RenderArgs* args) override; +}; + diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp new file mode 100644 index 0000000000..4fe25e989d --- /dev/null +++ b/tests/gpu-test/src/TestWindow.cpp @@ -0,0 +1,180 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TestWindow.h" + +#include +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#ifdef DEFERRED_LIGHTING +extern void initDeferredPipelines(render::ShapePlumber& plumber); +extern void initStencilPipeline(gpu::PipelinePointer& pipeline); +#endif + +TestWindow::TestWindow() { + setSurfaceType(QSurface::OpenGLSurface); + + + auto timer = new QTimer(this); + timer->setInterval(5); + connect(timer, &QTimer::timeout, [&] { draw(); }); + timer->start(); + + connect(qApp, &QCoreApplication::aboutToQuit, [this, timer] { + timer->stop(); + _aboutToQuit = true; + }); + +#ifdef DEFERRED_LIGHTING + _light->setType(model::Light::SUN); + _light->setAmbientSpherePreset(gpu::SphericalHarmonics::Preset::OLD_TOWN_SQUARE); + _light->setIntensity(1.0f); + _light->setAmbientIntensity(0.5f); + _light->setColor(vec3(1.0f)); + _light->setPosition(vec3(1, 1, 1)); + _renderContext->args = _renderArgs; +#endif + + QSurfaceFormat format = getDefaultOpenGLSurfaceFormat(); + format.setOption(QSurfaceFormat::DebugContext); + //format.setSwapInterval(0); + setFormat(format); + _glContext.setFormat(format); + _glContext.create(); + _glContext.makeCurrent(this); + show(); +} + +void TestWindow::initGl() { + _glContext.makeCurrent(this); + gpu::Context::init(); + _renderArgs->_context = std::make_shared(); + _glContext.makeCurrent(this); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + resize(QSize(800, 600)); + + setupDebugLogger(this); +#ifdef DEFERRED_LIGHTING + auto deferredLightingEffect = DependencyManager::get(); + deferredLightingEffect->init(); + deferredLightingEffect->setGlobalLight(_light); + initDeferredPipelines(*_shapePlumber); + initStencilPipeline(_opaquePipeline); +#endif +} + +void TestWindow::resizeWindow(const QSize& size) { + _size = size; + _renderArgs->_viewport = ivec4(0, 0, _size.width(), _size.height()); + auto fboCache = DependencyManager::get(); + if (fboCache) { + fboCache->setFrameBufferSize(_size); + } +} + +void TestWindow::beginFrame() { + _renderArgs->_context->syncCache(); + +#ifdef DEFERRED_LIGHTING + auto deferredLightingEffect = DependencyManager::get(); + deferredLightingEffect->prepare(_renderArgs); +#else + gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 0.0f, 0.1f, 0.2f, 1.0f }); + batch.clearDepthFramebuffer(1e4); + batch.setViewportTransform({ 0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio() }); + }); +#endif + + gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { + batch.setViewportTransform(_renderArgs->_viewport); + batch.setStateScissorRect(_renderArgs->_viewport); + batch.setProjectionTransform(_projectionMatrix); + }); +} + +void TestWindow::endFrame() { +#ifdef DEFERRED_LIGHTING + RenderArgs* args = _renderContext->args; + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + args->_batch = &batch; + auto deferredFboColorDepthStencil = DependencyManager::get()->getDeferredFramebufferDepthColor(); + batch.setViewportTransform(args->_viewport); + batch.setStateScissorRect(args->_viewport); + batch.setFramebuffer(deferredFboColorDepthStencil); + batch.setPipeline(_opaquePipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.setResourceTexture(0, nullptr); + }); + + auto deferredLightingEffect = DependencyManager::get(); + deferredLightingEffect->render(_renderContext); + + gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { + PROFILE_RANGE_BATCH(batch, "blit"); + // Blit to screen + auto framebufferCache = DependencyManager::get(); + auto framebuffer = framebufferCache->getLightingFramebuffer(); + batch.blit(framebuffer, _renderArgs->_viewport, nullptr, _renderArgs->_viewport); + }); +#endif + + gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { + batch.resetStages(); + }); + _glContext.swapBuffers(this); +} + +void TestWindow::draw() { + if (_aboutToQuit) { + return; + } + + // Attempting to draw before we're visible and have a valid size will + // produce GL errors. + if (!isVisible() || _size.width() <= 0 || _size.height() <= 0) { + return; + } + + if (!_glContext.makeCurrent(this)) { + return; + } + + static std::once_flag once; + std::call_once(once, [&] { initGl(); }); + beginFrame(); + + renderFrame(); + + endFrame(); +} + +void TestWindow::resizeEvent(QResizeEvent* ev) { + resizeWindow(ev->size()); + float fov_degrees = 60.0f; + float aspect_ratio = (float)_size.width() / _size.height(); + float near_clip = 0.1f; + float far_clip = 1000.0f; + _projectionMatrix = glm::perspective(glm::radians(fov_degrees), aspect_ratio, near_clip, far_clip); +} diff --git a/tests/gpu-test/src/TestWindow.h b/tests/gpu-test/src/TestWindow.h new file mode 100644 index 0000000000..b7f8df48f5 --- /dev/null +++ b/tests/gpu-test/src/TestWindow.h @@ -0,0 +1,53 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#define DEFERRED_LIGHTING + +class TestWindow : public QWindow { +protected: + QOpenGLContextWrapper _glContext; + QSize _size; + glm::mat4 _projectionMatrix; + bool _aboutToQuit { false }; + +#ifdef DEFERRED_LIGHTING + // Prepare the ShapePipelines + render::ShapePlumberPointer _shapePlumber { std::make_shared() }; + render::RenderContextPointer _renderContext { std::make_shared() }; + gpu::PipelinePointer _opaquePipeline; + model::LightPointer _light { std::make_shared() }; +#endif + + RenderArgs* _renderArgs { new RenderArgs() }; + + TestWindow(); + virtual void initGl(); + virtual void renderFrame() = 0; + +private: + void resizeWindow(const QSize& size); + + void beginFrame(); + void endFrame(); + void draw(); + void resizeEvent(QResizeEvent* ev) override; +}; + diff --git a/tests/gpu-test/src/main.cpp b/tests/gpu-test/src/main.cpp index b80539b33a..e672fe3c86 100644 --- a/tests/gpu-test/src/main.cpp +++ b/tests/gpu-test/src/main.cpp @@ -42,238 +42,64 @@ #include #include +#include #include +#include +#include +#include +#include -#include "unlit_frag.h" -#include "unlit_vert.h" +#include +#include -class RateCounter { - std::vector times; - QElapsedTimer timer; -public: - RateCounter() { - timer.start(); - } +#include +#include +#include +#include +#include +#include +#include +#include - void reset() { - times.clear(); - } +#include "TestWindow.h" +#include "TestInstancedShapes.h" +#include "TestShapes.h" - unsigned int count() const { - return (unsigned int)times.size() - 1; - } - - float elapsed() const { - if (times.size() < 1) { - return 0.0f; - } - float elapsed = *times.rbegin() - *times.begin(); - return elapsed; - } - - void increment() { - times.push_back(timer.elapsed() / 1000.0f); - } - - float rate() const { - if (elapsed() == 0.0f) { - return NAN; - } - return (float) count() / elapsed(); - } -}; - -uint32_t toCompactColor(const glm::vec4& color); +using namespace render; -const char* VERTEX_SHADER = R"SHADER( - -layout(location = 0) in vec4 inPosition; -layout(location = 3) in vec2 inTexCoord0; - -struct TransformObject { - mat4 _model; - mat4 _modelInverse; -}; - -layout(location=15) in ivec2 _drawCallInfo; - -uniform samplerBuffer transformObjectBuffer; - -TransformObject getTransformObject() { - int offset = 8 * _drawCallInfo.x; - TransformObject object; - object._model[0] = texelFetch(transformObjectBuffer, offset); - object._model[1] = texelFetch(transformObjectBuffer, offset + 1); - object._model[2] = texelFetch(transformObjectBuffer, offset + 2); - object._model[3] = texelFetch(transformObjectBuffer, offset + 3); - - object._modelInverse[0] = texelFetch(transformObjectBuffer, offset + 4); - object._modelInverse[1] = texelFetch(transformObjectBuffer, offset + 5); - object._modelInverse[2] = texelFetch(transformObjectBuffer, offset + 6); - object._modelInverse[3] = texelFetch(transformObjectBuffer, offset + 7); - - return object; -} - -struct TransformCamera { - mat4 _view; - mat4 _viewInverse; - mat4 _projectionViewUntranslated; - mat4 _projection; - mat4 _projectionInverse; - vec4 _viewport; -}; - -layout(std140) uniform transformCameraBuffer { - TransformCamera _camera; -}; - -TransformCamera getTransformCamera() { - return _camera; -} - -// the interpolated normal -out vec2 _texCoord0; - -void main(void) { - _texCoord0 = inTexCoord0.st; - - // standard transform - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - { // transformModelToClipPos - vec4 eyeWAPos; - { // _transformModelToEyeWorldAlignedPos - highp mat4 _mv = obj._model; - _mv[3].xyz -= cam._viewInverse[3].xyz; - highp vec4 _eyeWApos = (_mv * inPosition); - eyeWAPos = _eyeWApos; - } - gl_Position = cam._projectionViewUntranslated * eyeWAPos; - } - -})SHADER"; - -const char* FRAGMENT_SHADER = R"SHADER( - -uniform sampler2D originalTexture; - -in vec2 _texCoord0; - -layout(location = 0) out vec4 _fragColor0; - -void main(void) { - //_fragColor0 = vec4(_texCoord0, 0.0, 1.0); - _fragColor0 = texture(originalTexture, _texCoord0); -} -)SHADER"; +using TestBuilder = std::function; +using TestBuilders = std::list; -gpu::ShaderPointer makeShader(const std::string & vertexShaderSrc, const std::string & fragmentShaderSrc, const gpu::Shader::BindingSet & bindings) { - auto vs = gpu::Shader::createVertex(vertexShaderSrc); - auto fs = gpu::Shader::createPixel(fragmentShaderSrc); - auto shader = gpu::Shader::createProgram(vs, fs); - if (!gpu::Shader::makeProgram(*shader, bindings)) { - printf("Could not compile shader\n"); - exit(-1); - } - return shader; -} +#define INTERACTIVE -float getSeconds(quint64 start = 0) { - auto usecs = usecTimestampNow() - start; - auto msecs = usecs / USECS_PER_MSEC; - float seconds = (float)msecs / MSECS_PER_SECOND; - return seconds; -} - -static const size_t TYPE_COUNT = 4; -static GeometryCache::Shape SHAPE[TYPE_COUNT] = { - GeometryCache::Icosahedron, - GeometryCache::Cube, - GeometryCache::Sphere, - GeometryCache::Tetrahedron, - //GeometryCache::Line, -}; - -gpu::Stream::FormatPointer& getInstancedSolidStreamFormat(); - -// Creates an OpenGL window that renders a simple unlit scene using the gpu library and GeometryCache -// Should eventually get refactored into something that supports multiple gpu backends. -class QTestWindow : public QWindow { - Q_OBJECT - - QOpenGLContextWrapper _qGlContext; - QSize _size; - - gpu::ContextPointer _context; - gpu::PipelinePointer _pipeline; - glm::mat4 _projectionMatrix; - RateCounter fps; - QTime _time; +class MyTestWindow : public TestWindow { + using Parent = TestWindow; + TestBuilders _testBuilders; + GpuTestBase* _currentTest { nullptr }; + size_t _currentTestId { 0 }; + size_t _currentMaxTests { 0 }; glm::mat4 _camera; + QTime _time; -protected: - void renderText(); - -private: - void resizeWindow(const QSize& size) { - _size = size; - } - -public: - QTestWindow() { - setSurfaceType(QSurface::OpenGLSurface); - - QSurfaceFormat format; - // Qt Quick may need a depth and stencil buffer. Always make sure these are available. - format.setDepthBufferSize(16); - format.setStencilBufferSize(8); - setGLFormatVersion(format); - format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); - format.setOption(QSurfaceFormat::DebugContext); - //format.setSwapInterval(0); - - setFormat(format); - - _qGlContext.setFormat(format); - _qGlContext.create(); - - show(); - makeCurrent(); - setupDebugLogger(this); - - gpu::Context::init(); - _context = std::make_shared(); - makeCurrent(); - auto shader = makeShader(unlit_vert, unlit_frag, gpu::Shader::BindingSet{}); - auto state = std::make_shared(); - state->setMultisampleEnable(true); - state->setDepthTest(gpu::State::DepthTest { true }); - _pipeline = gpu::Pipeline::create(shader, state); - - - - // Clear screen - gpu::Batch batch; - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 1.0, 0.0, 0.5, 1.0 }); - _context->render(batch); - - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - - resize(QSize(800, 600)); - + void initGl() override { + Parent::initGl(); +#ifdef INTERACTIVE _time.start(); - } - - virtual ~QTestWindow() { +#endif + updateCamera(); + _testBuilders = TestBuilders({ + //[this] { return new TestFbx(_shapePlumber); }, + [] { return new TestShapes(); }, + }); } void updateCamera() { - float t = _time.elapsed() * 1e-4f; + float t = 0; +#ifdef INTERACTIVE + t = _time.elapsed() * 1e-3f; +#endif glm::vec3 unitscale { 1.0f }; glm::vec3 up { 0.0f, 1.0f, 0.0f }; @@ -283,263 +109,64 @@ public: static const vec3 camera_focus(0); static const vec3 camera_up(0, 1, 0); _camera = glm::inverse(glm::lookAt(camera_position, camera_focus, up)); + + ViewFrustum frustum; + frustum.setPosition(camera_position); + frustum.setOrientation(glm::quat_cast(_camera)); + frustum.setProjection(_projectionMatrix); + _renderArgs->setViewFrustum(frustum); } + void renderFrame() override { + updateCamera(); - void drawFloorGrid(gpu::Batch& batch) { - auto geometryCache = DependencyManager::get(); - // Render grid on xz plane (not the optimal way to do things, but w/e) - // Note: GeometryCache::renderGrid will *not* work, as it is apparenly unaffected by batch rotations and renders xy only - static const std::string GRID_INSTANCE = "Grid"; - static auto compactColor1 = toCompactColor(vec4 { 0.35f, 0.25f, 0.15f, 1.0f }); - static auto compactColor2 = toCompactColor(vec4 { 0.15f, 0.25f, 0.35f, 1.0f }); - static std::vector transforms; - static gpu::BufferPointer colorBuffer; - if (!transforms.empty()) { - transforms.reserve(200); - colorBuffer = std::make_shared(); - for (int i = 0; i < 100; ++i) { - { - glm::mat4 transform = glm::translate(mat4(), vec3(0, -1, -50 + i)); - transform = glm::scale(transform, vec3(100, 1, 1)); - transforms.push_back(transform); - colorBuffer->append(compactColor1); - } + while ((!_currentTest || (_currentTestId >= _currentMaxTests)) && !_testBuilders.empty()) { + if (_currentTest) { + delete _currentTest; + _currentTest = nullptr; + } - { - glm::mat4 transform = glm::mat4_cast(quat(vec3(0, PI / 2.0f, 0))); - transform = glm::translate(transform, vec3(0, -1, -50 + i)); - transform = glm::scale(transform, vec3(100, 1, 1)); - transforms.push_back(transform); - colorBuffer->append(compactColor2); - } + _currentTest = _testBuilders.front()(); + _testBuilders.pop_front(); + + if (_currentTest) { + _currentMaxTests = _currentTest->getTestCount(); + _currentTestId = 0; } } - auto pipeline = geometryCache->getSimplePipeline(); - for (auto& transform : transforms) { - batch.setModelTransform(transform); - batch.setupNamedCalls(GRID_INSTANCE, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { - batch.setViewTransform(_camera); - batch.setPipeline(_pipeline); - geometryCache->renderWireShapeInstances(batch, GeometryCache::Line, data.count(), colorBuffer); - }); - } - } - void drawSimpleShapes(gpu::Batch& batch) { - auto geometryCache = DependencyManager::get(); - static const size_t ITEM_COUNT = 1000; - static const float SHAPE_INTERVAL = (PI * 2.0f) / ITEM_COUNT; - static const float ITEM_INTERVAL = SHAPE_INTERVAL / TYPE_COUNT; - - static const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; - static const gpu::Element NORMAL_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; - static const gpu::Element COLOR_ELEMENT { gpu::VEC4, gpu::NUINT8, gpu::RGBA }; - - static std::vector transforms; - static std::vector colors; - static gpu::BufferPointer colorBuffer; - static gpu::BufferView colorView; - static gpu::BufferView instanceXfmView; - if (!colorBuffer) { - colorBuffer = std::make_shared(); - - static const float ITEM_RADIUS = 20; - static const vec3 ITEM_TRANSLATION { 0, 0, -ITEM_RADIUS }; - for (size_t i = 0; i < TYPE_COUNT; ++i) { - GeometryCache::Shape shape = SHAPE[i]; - GeometryCache::ShapeData shapeData = geometryCache->_shapes[shape]; - //indirectCommand._count - float startingInterval = ITEM_INTERVAL * i; - for (size_t j = 0; j < ITEM_COUNT; ++j) { - float theta = j * SHAPE_INTERVAL + startingInterval; - auto transform = glm::rotate(mat4(), theta, Vectors::UP); - transform = glm::rotate(transform, (randFloat() - 0.5f) * PI / 4.0f, Vectors::UNIT_X); - transform = glm::translate(transform, ITEM_TRANSLATION); - transform = glm::scale(transform, vec3(randFloat() / 2.0f + 0.5f)); - transforms.push_back(transform); - auto color = vec4 { randomColorValue(64), randomColorValue(64), randomColorValue(64), 255 }; - color /= 255.0f; - colors.push_back(color); - colorBuffer->append(toCompactColor(color)); - } - } - colorView = gpu::BufferView(colorBuffer, COLOR_ELEMENT); - } - - batch.setViewTransform(_camera); - batch.setPipeline(_pipeline); - batch.setInputFormat(getInstancedSolidStreamFormat()); - for (size_t i = 0; i < TYPE_COUNT; ++i) { - GeometryCache::Shape shape = SHAPE[i]; - GeometryCache::ShapeData shapeData = geometryCache->_shapes[shape]; - batch.setInputBuffer(gpu::Stream::COLOR, colorView); - for (size_t j = 0; j < ITEM_COUNT; ++j) { - batch.setModelTransform(transforms[j]); - shapeData.draw(batch); - } - } - } - - void drawCenterShape(gpu::Batch& batch) { - // Render unlit cube + sphere - static auto startUsecs = usecTimestampNow(); - float seconds = getSeconds(startUsecs); - seconds /= 4.0f; - batch.setModelTransform(Transform()); - batch._glColor4f(0.8f, 0.25f, 0.25f, 1.0f); - - bool wire = (seconds - floorf(seconds) > 0.5f); - auto geometryCache = DependencyManager::get(); - int shapeIndex = ((int)seconds) % TYPE_COUNT; - if (wire) { - geometryCache->renderWireShape(batch, SHAPE[shapeIndex]); - } else { - geometryCache->renderShape(batch, SHAPE[shapeIndex]); - } - - batch.setModelTransform(Transform().setScale(2.05f)); - batch._glColor4f(1, 1, 1, 1); - geometryCache->renderWireCube(batch); - } - - void drawTerrain(gpu::Batch& batch) { - auto geometryCache = DependencyManager::get(); - static std::once_flag once; - static gpu::BufferPointer vertexBuffer { std::make_shared() }; - static gpu::BufferPointer indexBuffer { std::make_shared() }; - - static gpu::BufferView positionView; - static gpu::BufferView textureView; - static gpu::Stream::FormatPointer vertexFormat { std::make_shared() }; - - static gpu::TexturePointer texture; - static gpu::PipelinePointer pipeline; - std::call_once(once, [&] { - static const uint SHAPE_VERTEX_STRIDE = sizeof(glm::vec4) * 2; // position, normals, textures - static const uint SHAPE_TEXTURES_OFFSET = sizeof(glm::vec4); - static const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; - static const gpu::Element TEXTURE_ELEMENT { gpu::VEC2, gpu::FLOAT, gpu::UV }; - std::vector vertices; - const int MINX = -1000; - const int MAXX = 1000; - - // top - vertices.push_back(vec4(MAXX, 0, MAXX, 1)); - vertices.push_back(vec4(MAXX, MAXX, 0, 0)); - - vertices.push_back(vec4(MAXX, 0, MINX, 1)); - vertices.push_back(vec4(MAXX, 0, 0, 0)); - - vertices.push_back(vec4(MINX, 0, MINX, 1)); - vertices.push_back(vec4(0, 0, 0, 0)); - - vertices.push_back(vec4(MINX, 0, MAXX, 1)); - vertices.push_back(vec4(0, MAXX, 0, 0)); - - vertexBuffer->append(vertices); - indexBuffer->append(std::vector({ 0, 1, 2, 2, 3, 0 })); - - positionView = gpu::BufferView(vertexBuffer, 0, vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, POSITION_ELEMENT); - textureView = gpu::BufferView(vertexBuffer, SHAPE_TEXTURES_OFFSET, vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, TEXTURE_ELEMENT); - texture = DependencyManager::get()->getImageTexture("C:/Users/bdavis/Git/openvr/samples/bin/cube_texture.png"); - // texture = DependencyManager::get()->getImageTexture("H:/test.png"); - //texture = DependencyManager::get()->getImageTexture("H:/crate_blue.fbm/lambert8SG_Normal_OpenGL.png"); - - auto shader = makeShader(VERTEX_SHADER, FRAGMENT_SHADER, gpu::Shader::BindingSet {}); - auto state = std::make_shared(); - state->setMultisampleEnable(false); - state->setDepthTest(gpu::State::DepthTest { true }); - pipeline = gpu::Pipeline::create(shader, state); - vertexFormat->setAttribute(gpu::Stream::POSITION); - vertexFormat->setAttribute(gpu::Stream::TEXCOORD); - }); - - static auto start = usecTimestampNow(); - auto now = usecTimestampNow(); - if ((now - start) > USECS_PER_SECOND * 1) { - start = now; - texture->incremementMinMip(); - } - - batch.setPipeline(pipeline); - batch.setInputBuffer(gpu::Stream::POSITION, positionView); - batch.setInputBuffer(gpu::Stream::TEXCOORD, textureView); - batch.setIndexBuffer(gpu::UINT16, indexBuffer, 0); - batch.setInputFormat(vertexFormat); - - batch.setResourceTexture(0, texture); - batch.setModelTransform(glm::translate(glm::mat4(), vec3(0, -0.1, 0))); - batch.drawIndexed(gpu::TRIANGLES, 6, 0); - - batch.setResourceTexture(0, DependencyManager::get()->getBlueTexture()); - batch.setModelTransform(glm::translate(glm::mat4(), vec3(0, -0.2, 0))); - batch.drawIndexed(gpu::TRIANGLES, 6, 0); - } - - void draw() { - // Attempting to draw before we're visible and have a valid size will - // produce GL errors. - if (!isVisible() || _size.width() <= 0 || _size.height() <= 0) { + if (!_currentTest && _testBuilders.empty()) { + qApp->quit(); return; } - updateCamera(); - makeCurrent(); - - gpu::Batch batch; - batch.resetStages(); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 0.0f, 0.1f, 0.2f, 1.0f }); - batch.clearDepthFramebuffer(1e4); - batch.setViewportTransform({ 0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio() }); - batch.setProjectionTransform(_projectionMatrix); - - batch.setViewTransform(_camera); - batch.setPipeline(_pipeline); - batch.setModelTransform(Transform()); - //drawFloorGrid(batch); - //drawSimpleShapes(batch); - //drawCenterShape(batch); - drawTerrain(batch); - - _context->render(batch); - _qGlContext.swapBuffers(this); - - fps.increment(); - if (fps.elapsed() >= 0.5f) { - qDebug() << "FPS: " << fps.rate(); - fps.reset(); + // Tests might need to wait for resources to download + if (!_currentTest->isReady()) { + return; } - } - - void makeCurrent() { - _qGlContext.makeCurrent(this); - } -protected: - void resizeEvent(QResizeEvent* ev) override { - resizeWindow(ev->size()); - - float fov_degrees = 60.0f; - float aspect_ratio = (float)_size.width() / _size.height(); - float near_clip = 0.1f; - float far_clip = 1000.0f; - _projectionMatrix = glm::perspective(glm::radians(fov_degrees), aspect_ratio, near_clip, far_clip); - } + gpu::doInBatch(_renderArgs->_context, [&](gpu::Batch& batch) { + batch.setViewTransform(_camera); + _renderArgs->_batch = &batch; + _currentTest->renderTest(_currentTestId, _renderArgs); + _renderArgs->_batch = nullptr; + }); + +#ifdef INTERACTIVE + +#else + // TODO Capture the current rendered framebuffer and save + // Increment the test ID + ++_currentTestId; +#endif + } }; + int main(int argc, char** argv) { QGuiApplication app(argc, argv); - QTestWindow window; - auto timer = new QTimer(&app); - timer->setInterval(0); - app.connect(timer, &QTimer::timeout, &app, [&] { - window.draw(); - }); - timer->start(); + MyTestWindow window; app.exec(); return 0; } -#include "main.moc" - From 11de8a52b268b723e36ea9d88767e7f8dea2d755 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 22 May 2016 22:52:56 -0700 Subject: [PATCH 73/77] fix broken procedural skybox when leaving and re-entering --- libraries/procedural/src/procedural/Procedural.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index cedf76b37a..781b508bc7 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -100,10 +100,6 @@ bool Procedural::parseUrl(const QUrl& shaderUrl) { return false; } - if (_shaderUrl == shaderUrl) { - return true; - } - _shaderUrl = shaderUrl; _shaderDirty = true; From 1f2f9da01911001914953503bfae11eda6d9e3d7 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 22 May 2016 23:34:45 -0700 Subject: [PATCH 74/77] fix keys getting stuck --- libraries/ui/src/OffscreenUi.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 4fb25e3e3f..dfd9056703 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -590,6 +590,7 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { // let the parent class do it's work bool result = OffscreenQmlSurface::eventFilter(originalDestination, event); + // Check if this is a key press/release event that might need special attention auto type = event->type(); if (type != QEvent::KeyPress && type != QEvent::KeyRelease) { @@ -597,7 +598,8 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { } QKeyEvent* keyEvent = dynamic_cast(event); - bool& pressed = _pressedKeys[keyEvent->key()]; + auto key = keyEvent->key(); + bool& pressed = _pressedKeys[key]; // Keep track of which key press events the QML has accepted if (result && QEvent::KeyPress == type) { @@ -607,7 +609,7 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { // QML input elements absorb key press, but apparently not key release. // therefore we want to ensure that key release events for key presses that were // accepted by the QML layer are suppressed - if (!result && type == QEvent::KeyRelease && pressed) { + if (type == QEvent::KeyRelease && pressed) { pressed = false; return true; } From 099a675a186e4f91c88829d806ea38963bba5604 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 24 May 2016 08:03:29 +1200 Subject: [PATCH 75/77] Remove extra logging --- interface/resources/qml/dialogs/FileDialog.qml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index b00ee76b5e..5cd972a38f 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -130,8 +130,6 @@ ModalWindow { choices = [], i, length; - console.log("####### folder parts: " + JSON.stringify(folders)); - if (folders[folders.length - 1] === "") { folders.pop(); } @@ -160,9 +158,6 @@ ModalWindow { onLastValidFolderChanged: { var folder = d.capitalizeDrive(lastValidFolder); - - console.log("####### lastValidFolder: " + folder); - calculatePathChoices(folder); } From 7dabce9cff02f74db3a9b737ca98b06708d57580 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 23 May 2016 19:04:35 -0700 Subject: [PATCH 76/77] Check throttle before idling/painting --- interface/src/Application.cpp | 45 +++++++++++++++++++---------------- interface/src/Application.h | 3 ++- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b0e4880011..7d1610c78e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -174,7 +174,6 @@ static const QString FBX_EXTENSION = ".fbx"; static const QString OBJ_EXTENSION = ".obj"; static const QString AVA_JSON_EXTENSION = ".ava.json"; -static const int MSECS_PER_SEC = 1000; static const int MIRROR_VIEW_TOP_PADDING = 5; static const int MIRROR_VIEW_LEFT_PADDING = 10; static const int MIRROR_VIEW_WIDTH = 265; @@ -633,7 +632,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one - const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SEC; + const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SECOND; auto discoverabilityManager = DependencyManager::get(); connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); @@ -1828,9 +1827,9 @@ bool Application::event(QEvent* event) { // Presentation/painting logic // TODO: Decouple presentation and painting loops - static bool isPainting = false; + static bool isPaintingThrottled = false; if ((int)event->type() == (int)Present) { - if (isPainting) { + if (isPaintingThrottled) { // If painting (triggered by presentation) is hogging the main thread, // repost as low priority to avoid hanging the GUI. // This has the effect of allowing presentation to exceed the paint budget by X times and @@ -1838,14 +1837,17 @@ bool Application::event(QEvent* event) { // (e.g. at a 60FPS target, painting for 17us would fall to 58.82FPS instead of 30FPS). removePostedEvents(this, Present); postEvent(this, new QEvent(static_cast(Present)), Qt::LowEventPriority); - isPainting = false; + isPaintingThrottled = false; return true; } - idle(); - - postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); - isPainting = true; + float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); + if (shouldPaint(nsecsElapsed)) { + _lastTimeUpdated.start(); + idle(nsecsElapsed); + postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); + } + isPaintingThrottled = true; return true; } else if ((int)event->type() == (int)Paint) { @@ -1855,7 +1857,7 @@ bool Application::event(QEvent* event) { paintGL(); - isPainting = false; + isPaintingThrottled = false; return true; } @@ -2639,10 +2641,9 @@ bool Application::acceptSnapshot(const QString& urlString) { static uint32_t _renderedFrameIndex { INVALID_FRAME }; -void Application::idle() { - // idle is called on a queued connection, so make sure we should be here. - if (_inPaint || _aboutToQuit) { - return; +bool Application::shouldPaint(float nsecsElapsed) { + if (_aboutToQuit) { + return false; } auto displayPlugin = getActiveDisplayPlugin(); @@ -2661,16 +2662,21 @@ void Application::idle() { } #endif - float msecondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_USEC / USECS_PER_MSEC; + float msecondsSinceLastUpdate = nsecsElapsed / NSECS_PER_USEC / USECS_PER_MSEC; // Throttle if requested if (displayPlugin->isThrottled() && (msecondsSinceLastUpdate < THROTTLED_SIM_FRAME_PERIOD_MS)) { - return; + return false; } // Sync up the _renderedFrameIndex _renderedFrameIndex = displayPlugin->presentCount(); + return true; +} + +void Application::idle(float nsecsElapsed) { + // Update the deadlock watchdog updateHeartbeat(); @@ -2687,7 +2693,7 @@ void Application::idle() { PROFILE_RANGE(__FUNCTION__); - float secondsSinceLastUpdate = msecondsSinceLastUpdate / MSECS_PER_SECOND; + float secondsSinceLastUpdate = nsecsElapsed / NSECS_PER_MSEC / MSECS_PER_SECOND; // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. if (_keyboardDeviceHasFocus && offscreenUi && offscreenUi->getWindow()->activeFocusItem() != offscreenUi->getRootItem()) { @@ -2697,9 +2703,6 @@ void Application::idle() { _keyboardDeviceHasFocus = true; } - // We're going to execute idle processing, so restart the last idle timer - _lastTimeUpdated.start(); - checkChangeCursor(); Stats::getInstance()->updateStats(); @@ -2926,7 +2929,7 @@ void Application::loadSettings() { } void Application::saveSettings() const { - sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SEC); + sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SECOND); DependencyManager::get()->saveSettings(); DependencyManager::get()->saveSettings(); diff --git a/interface/src/Application.h b/interface/src/Application.h index 28dbcead47..11a591776e 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -329,7 +329,8 @@ private: void cleanupBeforeQuit(); - void idle(); + bool shouldPaint(float nsecsElapsed); + void idle(float nsecsElapsed); void update(float deltaTime); // Various helper functions called during update() From 3391430f0a2f061c95a9bdf7f74bbfdbc0ab62ec Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 24 May 2016 20:28:49 +1200 Subject: [PATCH 77/77] Tidy zone flying and ghosting entities editor options Move options to top of section from under "skybox" subsection Fix capitalization of labels --- scripts/system/html/entityProperties.html | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index efe7e6cc65..67d168481b 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1717,6 +1717,14 @@
+
+ + +
+
+ + +
@@ -1801,15 +1809,6 @@
-
- - -
-
- - -
-
M