From 50a1e07ed275fec230f771663bea8536b81317bb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 18 Feb 2019 18:32:49 +1300 Subject: [PATCH 001/135] Stub missing MyAvatar, Avatar, and Agent functions and properties JSDoc --- .../src/avatars/ScriptableAvatar.h | 3 + interface/src/avatar/MyAvatar.h | 76 ++++++++++++++----- .../src/avatars-renderer/Avatar.h | 6 ++ 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index e93be897d5..4562ad6134 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -213,6 +213,9 @@ public: Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override; public slots: + /**jsdoc + * @function MyAvatar.update + */ void update(float deltatime); /**jsdoc diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 984d7b297b..4e75c93403 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -69,6 +69,7 @@ class MyAvatar : public Avatar { * @hifi-avatar * * @property {Vec3} qmlPosition - A synonym for position for use by QML. + * * @property {boolean} shouldRenderLocally=true - If true then your avatar is rendered for you in Interface, * otherwise it is not rendered for you (but it is still rendered for other users). * @property {Vec3} motorVelocity=Vec3.ZERO - The target velocity of your avatar to be achieved by a scripted motor. @@ -90,29 +91,38 @@ class MyAvatar : public Avatar { * @property {number} audioListenerModeCamera=1 - The audio listening position is at the camera. Read-only. * @property {number} audioListenerModeCustom=2 - The audio listening position is at a the position specified by set by the * customListenPosition and customListenOrientation property values. Read-only. + * @property {Vec3} customListenPosition=Vec3.ZERO - The listening position used when the audioListenerMode + * property value is audioListenerModeCustom. + * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the + * audioListenerMode property value is audioListenerModeCustom. * @property {boolean} hasScriptedBlendshapes=false - Blendshapes will be transmitted over the network if set to true. * @property {boolean} hasProceduralBlinkFaceMovement=true - procedural blinking will be turned on if set to true. * @property {boolean} hasProceduralEyeFaceMovement=true - procedural eye movement will be turned on if set to true. * @property {boolean} hasAudioEnabledFaceMovement=true - If set to true, voice audio will move the mouth Blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. - * @property {Vec3} customListenPosition=Vec3.ZERO - The listening position used when the audioListenerMode - * property value is audioListenerModeCustom. - * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the - * audioListenerMode property value is audioListenerModeCustom. + * @property {number} rotationRecenterFilterLength + * @property {number} rotationThreshold + * @property {boolean} enableStepResetRotation + * @property {boolean} enableDrawAverageFacing + * * @property {Vec3} leftHandPosition - The position of the left hand in avatar coordinates if it's being positioned by * controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. * @property {Vec3} rightHandPosition - The position of the right hand in avatar coordinates if it's being positioned by * controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. - * @property {Vec3} leftHandTipPosition - The position 30cm offset from the left hand in avatar coordinates if it's being * positioned by controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. * @property {Vec3} rightHandTipPosition - The position 30cm offset from the right hand in avatar coordinates if it's being * positioned by controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. + * * @property {Pose} leftHandPose - The pose of the left hand as determined by the hand controllers. Read-only. * @property {Pose} rightHandPose - The pose right hand position as determined by the hand controllers. Read-only. * @property {Pose} leftHandTipPose - The pose of the left hand as determined by the hand controllers, with the position * by 30cm. Read-only. * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, with the position * by 30cm. Read-only. + * + * @property {number} energy + * @property {boolean} isAway + * * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to the center of * gravity model that balance the center of gravity over the base of support of the feet. Setting the value false * will result in the default behaviour where the hips are placed under the head. @@ -122,30 +132,38 @@ class MyAvatar : public Avatar { * @property {boolean} collisionsEnabled - Set to true to enable collisions for the avatar, false * to disable collisions. May return true even though the value was set false because the * zone may disallow collisionless avatars. + * @property {boolean} otherAvatarsCollisionsEnabled * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled. * Deprecated: Use collisionsEnabled instead. * @property {boolean} useAdvancedMovementControls - Returns and sets the value of the Interface setting, Settings > * Walking and teleporting. Note: Setting the value has no effect unless Interface is restarted. * @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Show room boundaries * while teleporting. Note: Setting the value has no effect unless Interface is restarted. + * * @property {number} yawSpeed=75 * @property {number} pitchSpeed=50 + * * @property {boolean} hmdRollControlEnabled=true - If true, the roll angle of your HMD turns your avatar * while flying. * @property {number} hmdRollControlDeadZone=8 - The amount of HMD roll, in degrees, required before your avatar turns if * hmdRollControlEnabled is enabled. * @property {number} hmdRollControlRate If hmdRollControlEnabled is true, this value determines the maximum turn rate of * your avatar when rolling your HMD in degrees per second. + * * @property {number} userHeight=1.75 - The height of the user in sensor space. * @property {number} userEyeHeight=1.65 - The estimated height of the user's eyes in sensor space. Read-only. + * * @property {Uuid} SELF_ID - UUID representing "my avatar". Only use for local-only entities in situations * where MyAvatar.sessionUUID is not available (e.g., if not connected to a domain). Note: Likely to be deprecated. * Read-only. + * * @property {number} walkSpeed * @property {number} walkBackwardSpeed * @property {number} sprintSpeed * @property {number} isInSittingState - * @property {number} userRecenterModel + * @property {MyAvatar.SitStandModelType} userRecenterModel + * @property {boolean} isSitStandStateLocked + * @property {boolean} allowTeleporting * * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the * registration point of the 3D model. @@ -160,6 +178,7 @@ class MyAvatar : public Avatar { * sometimes called "elevation". * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is * sometimes called "bank". + * * @property {Quat} orientation * @property {Quat} headOrientation - The orientation of the avatar's head. * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is @@ -168,18 +187,24 @@ class MyAvatar : public Avatar { * head. Yaw is sometimes called "heading". * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is * sometimes called "bank". + * * @property {Vec3} velocity * @property {Vec3} angularVelocity + * * @property {number} audioLoudness * @property {number} audioAverageLoudness + * * @property {string} displayName * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer * rather than by Interface clients. The result is unique among all avatars present at the time. * @property {boolean} lookAtSnappingEnabled * @property {string} skeletonModelURL * @property {AttachmentData[]} attachmentData + * * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. + * * @property {Uuid} sessionUUID Read-only. + * * @property {Mat4} sensorToWorldMatrix Read-only. * @property {Mat4} controllerLeftHandMatrix Read-only. * @property {Mat4} controllerRightHandMatrix Read-only. @@ -196,11 +221,11 @@ class MyAvatar : public Avatar { Q_PROPERTY(QString motorMode READ getScriptedMotorMode WRITE setScriptedMotorMode) Q_PROPERTY(QString collisionSoundURL READ getCollisionSoundURL WRITE setCollisionSoundURL) Q_PROPERTY(AudioListenerMode audioListenerMode READ getAudioListenerMode WRITE setAudioListenerMode) - Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) - Q_PROPERTY(glm::quat customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation) Q_PROPERTY(AudioListenerMode audioListenerModeHead READ getAudioListenerModeHead) Q_PROPERTY(AudioListenerMode audioListenerModeCamera READ getAudioListenerModeCamera) Q_PROPERTY(AudioListenerMode audioListenerModeCustom READ getAudioListenerModeCustom) + Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) + Q_PROPERTY(glm::quat customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation) Q_PROPERTY(bool hasScriptedBlendshapes READ getHasScriptedBlendshapes WRITE setHasScriptedBlendshapes) Q_PROPERTY(bool hasProceduralBlinkFaceMovement READ getHasProceduralBlinkFaceMovement WRITE setHasProceduralBlinkFaceMovement) Q_PROPERTY(bool hasProceduralEyeFaceMovement READ getHasProceduralEyeFaceMovement WRITE setHasProceduralEyeFaceMovement) @@ -277,6 +302,9 @@ public: }; Q_ENUM(DriveKeys) + /**jsdoc + * @typedef {number} MyAvatar.SitStandModelType + */ enum SitStandModelType { ForceSit = 0, ForceStand, @@ -491,6 +519,9 @@ public: // adding one of the other handlers. While any handler may change a value in animStateDictionaryIn (or supply different values in animStateDictionaryOut) // a handler must not remove properties from animStateDictionaryIn, nor change property values that it does not intend to change. // It is not specified in what order multiple handlers are called. + /**jsdoc + * @function MyAvatar.addAnimationStateHandler + */ Q_INVOKABLE QScriptValue addAnimationStateHandler(QScriptValue handler, QScriptValue propertiesList) { return _skeletonModel->getRig().addAnimationStateHandler(handler, propertiesList); } /**jsdoc @@ -530,7 +561,7 @@ public: */ Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& hand); /**jsdoc - * @function MyAvatar.setHmdAvatarAlignmentType + * @function MyAvatar.getHmdAvatarAlignmentType * @returns {string} */ Q_INVOKABLE QString getHmdAvatarAlignmentType() const; @@ -649,7 +680,7 @@ public: /**jsdoc * Recenter the avatar in the horizontal direction, if {@link MyAvatar|MyAvatar.hmdLeanRecenterEnabled} is * false. - * @ function MyAvatar.triggerHorizontalRecenter + * @function MyAvatar.triggerHorizontalRecenter */ Q_INVOKABLE void triggerHorizontalRecenter(); @@ -935,7 +966,7 @@ public: /**jsdoc * Function returns list of avatar entities - * @function MyAvatar.getAvatarEntitiesVariant() + * @function MyAvatar.getAvatarEntitiesVariant * @returns {object[]} */ Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); @@ -1244,7 +1275,6 @@ public slots: * @param {boolean} [shouldFaceLocation=false] - Set to true to position the avatar a short distance away from * the new position and orientate the avatar to face the position. */ - void goToFeetLocation(const glm::vec3& newPosition, bool hasOrientation, const glm::quat& newOrientation, bool shouldFaceLocation); @@ -1382,8 +1412,20 @@ public slots: */ bool getEnableMeshVisible() const override; + /**jsdoc + * @function MyAvatar.storeAvatarEntityDataPayload + */ void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; + + /**jsdoc + * @function MyAvatar.clearAvatarEntity + * @param {Uuid} entityID + * @param {boolean} requiresRemovalFromTree + */ void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true) override; + /**jsdoc + * @function MyAvatar.sanitizeAvatarEntityProperties + */ void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const; /**jsdoc @@ -1489,11 +1531,11 @@ signals: void collisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when collisions with other avatars enabled or disabled - * @function MyAvatar.otherAvatarsCollisionsEnabledChanged - * @param {boolean} enabled - * @returns {Signal} - */ + * Triggered when collisions with other avatars enabled or disabled + * @function MyAvatar.otherAvatarsCollisionsEnabledChanged + * @param {boolean} enabled + * @returns {Signal} + */ void otherAvatarsCollisionsEnabledChanged(bool enabled); /**jsdoc diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 06942a13d8..7ebb8cad01 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -499,6 +499,9 @@ public: void tearDownGrabs(); signals: + /**jsdoc + * @function MyAvatar.targetScaleChanged + */ void targetScaleChanged(float targetScale); public slots: @@ -541,6 +544,9 @@ public slots: */ glm::quat getRightPalmRotation() const; + /**jsdoc + * @function MyAvatar.setModelURLFinished + */ // hooked up to Model::setURLFinished signal void setModelURLFinished(bool success); From f58a5db0b0931ce0244007a748ed545d51196194 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 19 Feb 2019 09:47:51 +1300 Subject: [PATCH 002/135] Reorganize JSDoc inheritance for MyAvatar and Avatar --- .../src/avatars/ScriptableAvatar.h | 109 +++------- interface/src/avatar/MyAvatar.h | 146 +++++++++----- .../src/avatars-renderer/Avatar.h | 7 +- libraries/avatars/src/AvatarData.h | 189 ++++++++++++------ 4 files changed, 262 insertions(+), 189 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 4562ad6134..b2ad4527b0 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -22,17 +22,16 @@ * The Avatar API is used to manipulate scriptable avatars on the domain. This API is a subset of the * {@link MyAvatar} API. * - *

Note: In the examples, use "Avatar" instead of "MyAvatar".

- * * @namespace Avatar * * @hifi-assignment-client * + * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. * @property {Vec3} position - * @property {number} scale + * @property {number} scale - Returns the clamped scale of the avatar. * @property {number} density Read-only. * @property {Vec3} handPosition - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". @@ -63,62 +62,6 @@ * @property {Mat4} controllerRightHandMatrix Read-only. * @property {number} sensorToWorldScale Read-only. * - * @borrows MyAvatar.getDomainMinScale as getDomainMinScale - * @borrows MyAvatar.getDomainMaxScale as getDomainMaxScale - * @borrows MyAvatar.canMeasureEyeHeight as canMeasureEyeHeight - * @borrows MyAvatar.getEyeHeight as getEyeHeight - * @borrows MyAvatar.getHeight as getHeight - * @borrows MyAvatar.setHandState as setHandState - * @borrows MyAvatar.getHandState as getHandState - * @borrows MyAvatar.setRawJointData as setRawJointData - * @borrows MyAvatar.setJointData as setJointData - * @borrows MyAvatar.setJointRotation as setJointRotation - * @borrows MyAvatar.setJointTranslation as setJointTranslation - * @borrows MyAvatar.clearJointData as clearJointData - * @borrows MyAvatar.isJointDataValid as isJointDataValid - * @borrows MyAvatar.getJointRotation as getJointRotation - * @borrows MyAvatar.getJointTranslation as getJointTranslation - * @borrows MyAvatar.getJointRotations as getJointRotations - * @borrows MyAvatar.getJointTranslations as getJointTranslations - * @borrows MyAvatar.setJointRotations as setJointRotations - * @borrows MyAvatar.setJointTranslations as setJointTranslations - * @borrows MyAvatar.clearJointsData as clearJointsData - * @borrows MyAvatar.getJointIndex as getJointIndex - * @borrows MyAvatar.getJointNames as getJointNames - * @borrows MyAvatar.setBlendshape as setBlendshape - * @borrows MyAvatar.getAttachmentsVariant as getAttachmentsVariant - * @borrows MyAvatar.setAttachmentsVariant as setAttachmentsVariant - * @borrows MyAvatar.updateAvatarEntity as updateAvatarEntity - * @borrows MyAvatar.clearAvatarEntity as clearAvatarEntity - * @borrows MyAvatar.setForceFaceTrackerConnected as setForceFaceTrackerConnected - * @borrows MyAvatar.getAttachmentData as getAttachmentData - * @borrows MyAvatar.setAttachmentData as setAttachmentData - * @borrows MyAvatar.attach as attach - * @borrows MyAvatar.detachOne as detachOne - * @borrows MyAvatar.detachAll as detachAll - * @borrows MyAvatar.getAvatarEntityData as getAvatarEntityData - * @borrows MyAvatar.setAvatarEntityData as setAvatarEntityData - * @borrows MyAvatar.getSensorToWorldMatrix as getSensorToWorldMatrix - * @borrows MyAvatar.getSensorToWorldScale as getSensorToWorldScale - * @borrows MyAvatar.getControllerLeftHandMatrix as getControllerLeftHandMatrix - * @borrows MyAvatar.getControllerRightHandMatrix as getControllerRightHandMatrix - * @borrows MyAvatar.getDataRate as getDataRate - * @borrows MyAvatar.getUpdateRate as getUpdateRate - * @borrows MyAvatar.displayNameChanged as displayNameChanged - * @borrows MyAvatar.sessionDisplayNameChanged as sessionDisplayNameChanged - * @borrows MyAvatar.skeletonModelURLChanged as skeletonModelURLChanged - * @borrows MyAvatar.lookAtSnappingChanged as lookAtSnappingChanged - * @borrows MyAvatar.sessionUUIDChanged as sessionUUIDChanged - * @borrows MyAvatar.sendAvatarDataPacket as sendAvatarDataPacket - * @borrows MyAvatar.sendIdentityPacket as sendIdentityPacket - * @borrows MyAvatar.setJointMappingsFromNetworkReply as setJointMappingsFromNetworkReply - * @borrows MyAvatar.setSessionUUID as setSessionUUID - * @borrows MyAvatar.getAbsoluteJointRotationInObjectFrame as getAbsoluteJointRotationInObjectFrame - * @borrows MyAvatar.getAbsoluteJointTranslationInObjectFrame as getAbsoluteJointTranslationInObjectFrame - * @borrows MyAvatar.setAbsoluteJointRotationInObjectFrame as setAbsoluteJointRotationInObjectFrame - * @borrows MyAvatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame - * @borrows MyAvatar.getTargetScale as getTargetScale - * @borrows MyAvatar.resetLastSent as resetLastSent */ class ScriptableAvatar : public AvatarData, public Dependency { @@ -159,23 +102,25 @@ public: Q_INVOKABLE AnimationDetails getAnimationDetails(); /**jsdoc - * Get the names of all the joints in the current avatar. - * @function MyAvatar.getJointNames - * @returns {string[]} The joint names. - * @example Report the names of all the joints in your current avatar. - * print(JSON.stringify(MyAvatar.getJointNames())); - */ + * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
+ * Get the names of all the joints in the current avatar. + * @function Avatar.getJointNames + * @returns {string[]} The joint names. + * @example Report the names of all the joints in your current avatar. + * print(JSON.stringify(Avatar.getJointNames())); + */ Q_INVOKABLE virtual QStringList getJointNames() const override; /**jsdoc - * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by - * {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. - * @function MyAvatar.getJointIndex - * @param {string} name - The name of the joint. - * @returns {number} The index of the joint. - * @example Report the index of your avatar's left arm joint. - * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm")); - */ + * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
+ * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by + * {@link Avatar.getJointNames}. + * @function Avatar.getJointIndex + * @param {string} name - The name of the joint. + * @returns {number} The index of the joint. + * @example Report the index of your avatar's left arm joint. + * print(JSON.stringify(Avatar.getJointIndex("LeftArm")); + */ /// Returns the index of the joint with the specified name, or -1 if not found/unknown. Q_INVOKABLE virtual int getJointIndex(const QString& name) const override; @@ -193,6 +138,8 @@ public: bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } /**jsdoc + * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
+ * ####### Also need to resolve with MyAvatar.
* Potentially Very Expensive. Do not use. * @function Avatar.getAvatarEntityData * @returns {object} @@ -200,13 +147,15 @@ public: Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * @function MyAvatar.setAvatarEntityData - * @param {object} avatarEntityData - */ + * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc. + * @function Avatar.setAvatarEntityData + * @param {object} avatarEntityData + */ Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; /**jsdoc - * @function MyAvatar.updateAvatarEntity + * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc. + * @function Avatar.updateAvatarEntity * @param {Uuid} entityID * @param {string} entityData */ @@ -214,12 +163,12 @@ public: public slots: /**jsdoc - * @function MyAvatar.update + * @function Avatar.update */ void update(float deltatime); /**jsdoc - * @function MyAvatar.setJointMappingsFromNetworkReply + * @function Avatar.setJointMappingsFromNetworkReply */ void setJointMappingsFromNetworkReply(); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 4e75c93403..d689d2f215 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -68,6 +68,46 @@ class MyAvatar : public Avatar { * @hifi-client-entity * @hifi-avatar * + * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. + * @property {Vec3} position + * @property {number} scale - Returns the clamped scale of the avatar. + * @property {number} density Read-only. + * @property {Vec3} handPosition + * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * Yaw is sometimes called "heading". + * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is + * sometimes called "elevation". + * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is + * sometimes called "bank". + * @property {Quat} orientation + * @property {Quat} headOrientation - The orientation of the avatar's head. + * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is + * sometimes called "elevation". + * @property {number} headYaw - The rotation left or right about an axis running from the base to the crown of the avatar's + * head. Yaw is sometimes called "heading". + * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is + * sometimes called "bank". + * @property {Vec3} velocity + * @property {Vec3} angularVelocity + * @property {number} audioLoudness + * @property {number} audioAverageLoudness + * @property {string} displayName + * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer + * rather than by Interface clients. The result is unique among all avatars present at the time. + * @property {boolean} lookAtSnappingEnabled + * @property {string} skeletonModelURL + * @property {AttachmentData[]} attachmentData + * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. + * @property {Uuid} sessionUUID Read-only. + * @property {Mat4} sensorToWorldMatrix Read-only. + * @property {Mat4} controllerLeftHandMatrix Read-only. + * @property {Mat4} controllerRightHandMatrix Read-only. + * @property {number} sensorToWorldScale Read-only. + * + * @comment IMPORTANT: This group of properties is copied from Avatar.h; they should NOT be edited here. + * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the + * registration point of the 3D model. + * * @property {Vec3} qmlPosition - A synonym for position for use by QML. * * @property {boolean} shouldRenderLocally=true - If true then your avatar is rendered for you in Interface, @@ -165,50 +205,60 @@ class MyAvatar : public Avatar { * @property {boolean} isSitStandStateLocked * @property {boolean} allowTeleporting * - * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the - * registration point of the 3D model. - * - * @property {Vec3} position - * @property {number} scale - Returns the clamped scale of the avatar. - * @property {number} density Read-only. - * @property {Vec3} handPosition - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. - * Yaw is sometimes called "heading". - * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is - * sometimes called "elevation". - * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is - * sometimes called "bank". - * - * @property {Quat} orientation - * @property {Quat} headOrientation - The orientation of the avatar's head. - * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is - * sometimes called "elevation". - * @property {number} headYaw - The rotation left or right about an axis running from the base to the crown of the avatar's - * head. Yaw is sometimes called "heading". - * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is - * sometimes called "bank". - * - * @property {Vec3} velocity - * @property {Vec3} angularVelocity - * - * @property {number} audioLoudness - * @property {number} audioAverageLoudness - * - * @property {string} displayName - * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer - * rather than by Interface clients. The result is unique among all avatars present at the time. - * @property {boolean} lookAtSnappingEnabled - * @property {string} skeletonModelURL - * @property {AttachmentData[]} attachmentData - * - * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. - * - * @property {Uuid} sessionUUID Read-only. - * - * @property {Mat4} sensorToWorldMatrix Read-only. - * @property {Mat4} controllerLeftHandMatrix Read-only. - * @property {Mat4} controllerRightHandMatrix Read-only. - * @property {number} sensorToWorldScale Read-only. + * @borrows Avatar.getDomainMinScale as getDomainMinScale + * @borrows Avatar.getDomainMaxScale as getDomainMaxScale + * @borrows Avatar.getEyeHeight as getEyeHeight + * @borrows Avatar.getHeight as getHeight + * @borrows Avatar.setHandState as setHandState + * @borrows Avatar.getHandState as getHandState + * @borrows Avatar.setRawJointData as setRawJointData + * @borrows Avatar.setJointData as setJointData + * @borrows Avatar.setJointRotation as setJointRotation + * @borrows Avatar.setJointTranslation as setJointTranslation + * @borrows Avatar.clearJointData as clearJointData + * @borrows Avatar.isJointDataValid as isJointDataValid + * @borrows Avatar.getJointRotation as getJointRotation + * @borrows Avatar.getJointTranslation as getJointTranslation + * @borrows Avatar.getJointRotations as getJointRotations + * @borrows Avatar.getJointTranslations as getJointTranslations + * @borrows Avatar.setJointRotations as setJointRotations + * @borrows Avatar.setJointTranslations as setJointTranslations + * @borrows Avatar.clearJointsData as clearJointsData + * @borrows Avatar.getJointIndex as getJointIndex + * @borrows Avatar.getJointNames as getJointNames + * @borrows Avatar.setBlendshape as setBlendshape + * @borrows Avatar.getAttachmentsVariant as getAttachmentsVariant + * @borrows Avatar.setAttachmentsVariant as setAttachmentsVariant + * @borrows Avatar.updateAvatarEntity as updateAvatarEntity + * @borrows Avatar.clearAvatarEntity as clearAvatarEntity + * @borrows Avatar.setForceFaceTrackerConnected as setForceFaceTrackerConnected + * @borrows Avatar.getAttachmentData as getAttachmentData + * @borrows Avatar.setAttachmentData as setAttachmentData + * @borrows Avatar.attach as attach + * @borrows Avatar.detachOne as detachOne + * @borrows Avatar.detachAll as detachAll + * @borrows Avatar.getAvatarEntityData as getAvatarEntityData + * @borrows Avatar.setAvatarEntityData as setAvatarEntityData + * @borrows Avatar.getSensorToWorldMatrix as getSensorToWorldMatrix + * @borrows Avatar.getSensorToWorldScale as getSensorToWorldScale + * @borrows Avatar.getControllerLeftHandMatrix as getControllerLeftHandMatrix + * @borrows Avatar.getControllerRightHandMatrix as getControllerRightHandMatrix + * @borrows Avatar.getDataRate as getDataRate + * @borrows Avatar.getUpdateRate as getUpdateRate + * @borrows Avatar.displayNameChanged as displayNameChanged + * @borrows Avatar.sessionDisplayNameChanged as sessionDisplayNameChanged + * @borrows Avatar.skeletonModelURLChanged as skeletonModelURLChanged + * @borrows Avatar.lookAtSnappingChanged as lookAtSnappingChanged + * @borrows Avatar.sessionUUIDChanged as sessionUUIDChanged + * @borrows Avatar.sendAvatarDataPacket as sendAvatarDataPacket + * @borrows Avatar.sendIdentityPacket as sendIdentityPacket + * @borrows Avatar.setSessionUUID as setSessionUUID + * @borrows Avatar.getAbsoluteJointRotationInObjectFrame as getAbsoluteJointRotationInObjectFrame + * @borrows Avatar.getAbsoluteJointTranslationInObjectFrame as getAbsoluteJointTranslationInObjectFrame + * @borrows Avatar.setAbsoluteJointRotationInObjectFrame as setAbsoluteJointRotationInObjectFrame + * @borrows Avatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame + * @borrows Avatar.getTargetScale as getTargetScale + * @borrows Avatar.resetLastSent as resetLastSent */ // FIXME: `glm::vec3 position` is not accessible from QML, so this exposes position in a QML-native type Q_PROPERTY(QVector3D qmlPosition READ getQmlPosition) @@ -1314,7 +1364,7 @@ public slots: /**jsdoc * @function MyAvatar.restrictScaleFromDomainSettings - * @param {objecct} domainSettingsObject + * @param {object} domainSettingsObject */ void restrictScaleFromDomainSettings(const QJsonObject& domainSettingsObject); @@ -1345,6 +1395,7 @@ public slots: /**jsdoc + * ####### Why Q_INVOKABLE? * @function MyAvatar.updateMotionBehaviorFromMenu */ Q_INVOKABLE void updateMotionBehaviorFromMenu(); @@ -1413,16 +1464,19 @@ public slots: bool getEnableMeshVisible() const override; /**jsdoc + * ####### TODO; Should this really be exposed in the API? * @function MyAvatar.storeAvatarEntityDataPayload */ void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; /**jsdoc + * ####### Does override change functionality? If so, document here and don't borrow; if not, borrow and don't document here. * @function MyAvatar.clearAvatarEntity * @param {Uuid} entityID - * @param {boolean} requiresRemovalFromTree + * @param {boolean} [requiresRemovalFromTree] */ void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true) override; + /**jsdoc * @function MyAvatar.sanitizeAvatarEntityProperties */ diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 7ebb8cad01..1aa6829160 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -127,7 +127,12 @@ private: class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaModelPayload { Q_OBJECT - // This property has JSDoc in MyAvatar.h. + /*jsdoc + * @comment IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h. + * + * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the + * registration point of the 3D model. + */ Q_PROPERTY(glm::vec3 skeletonOffset READ getSkeletonOffset WRITE setSkeletonOffset) public: diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 63396a59ac..7a4d235565 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -411,7 +411,43 @@ class ClientTraitsHandler; class AvatarData : public QObject, public SpatiallyNestable { Q_OBJECT - // The following properties have JSDoc in MyAvatar.h and ScriptableAvatar.h + // IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h and ScriptableAvatar.h. + /* + * @property {Vec3} position + * @property {number} scale - Returns the clamped scale of the avatar. + * @property {number} density Read-only. + * @property {Vec3} handPosition + * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * Yaw is sometimes called "heading". + * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is + * sometimes called "elevation". + * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is + * sometimes called "bank". + * @property {Quat} orientation + * @property {Quat} headOrientation - The orientation of the avatar's head. + * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is + * sometimes called "elevation". + * @property {number} headYaw - The rotation left or right about an axis running from the base to the crown of the avatar's + * head. Yaw is sometimes called "heading". + * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is + * sometimes called "bank". + * @property {Vec3} velocity + * @property {Vec3} angularVelocity + * @property {number} audioLoudness + * @property {number} audioAverageLoudness + * @property {string} displayName + * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer + * rather than by Interface clients. The result is unique among all avatars present at the time. + * @property {boolean} lookAtSnappingEnabled + * @property {string} skeletonModelURL + * @property {AttachmentData[]} attachmentData + * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. + * @property {Uuid} sessionUUID Read-only. + * @property {Mat4} sensorToWorldMatrix Read-only. + * @property {Mat4} controllerLeftHandMatrix Read-only. + * @property {Mat4} controllerRightHandMatrix Read-only. + * @property {number} sensorToWorldScale Read-only. + */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) Q_PROPERTY(float density READ getDensity) @@ -567,7 +603,7 @@ public: /**jsdoc * Returns the minimum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. - * @function MyAvatar.getDomainMinScale + * @function Avatar.getDomainMinScale * @returns {number} minimum scale allowed for this avatar in the current domain. */ Q_INVOKABLE float getDomainMinScale() const; @@ -575,7 +611,7 @@ public: /**jsdoc * Returns the maximum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. - * @function MyAvatar.getDomainMaxScale + * @function Avatar.getDomainMaxScale * @returns {number} maximum scale allowed for this avatar in the current domain. */ Q_INVOKABLE float getDomainMaxScale() const; @@ -591,7 +627,7 @@ public: /**jsdoc * Provides read only access to the current eye height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. - * @function MyAvatar.getEyeHeight + * @function Avatar.getEyeHeight * @returns {number} Eye height of avatar in meters. */ Q_INVOKABLE virtual float getEyeHeight() const { return _targetScale * getUnscaledEyeHeight(); } @@ -599,7 +635,7 @@ public: /**jsdoc * Provides read only access to the current height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. - * @function MyAvatar.getHeight + * @function Avatar.getHeight * @returns {number} Height of avatar in meters. */ Q_INVOKABLE virtual float getHeight() const; @@ -610,13 +646,13 @@ public: void setDomainMaximumHeight(float domainMaximumHeight); /**jsdoc - * @function MyAvatar.setHandState + * @function Avatar.setHandState * @param {string} state */ Q_INVOKABLE void setHandState(char s) { _handState = s; } /**jsdoc - * @function MyAvatar.getHandState + * @function Avatar.getHandState * @returns {string} */ Q_INVOKABLE char getHandState() const { return _handState; } @@ -624,7 +660,7 @@ public: const QVector& getRawJointData() const { return _jointData; } /**jsdoc - * @function MyAvatar.setRawJointData + * @function Avatar.setRawJointData * @param {JointData[]} data */ Q_INVOKABLE void setRawJointData(QVector data); @@ -636,7 +672,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointData + * @function Avatar.setJointData * @param {number} index - The index of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. * @param {Vec3} translation - The translation of the joint relative to its parent. @@ -654,6 +690,8 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointData(int index, const glm::quat& rotation, const glm::vec3& translation); @@ -664,7 +702,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointRotation + * @function Avatar.setJointRotation * @param {number} index - The index of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. */ @@ -677,7 +715,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointTranslation + * @function Avatar.setJointTranslation * @param {number} index - The index of the joint. * @param {Vec3} translation - The translation of the joint relative to its parent. */ @@ -687,13 +725,13 @@ public: * Clear joint translations and rotations set by script for a specific joint. This restores all motion from the default * animation system including inverse kinematics for that joint. *

Note: This is slightly faster than the function variation that specifies the joint name.

- * @function MyAvatar.clearJointData + * @function Avatar.clearJointData * @param {number} index - The index of the joint. */ Q_INVOKABLE virtual void clearJointData(int index); /**jsdoc - * @function MyAvatar.isJointDataValid + * @function Avatar.isJointDataValid * @param {number} index * @returns {boolean} */ @@ -702,7 +740,7 @@ public: /**jsdoc * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. - * @function MyAvatar.getJointRotation + * @function Avatar.getJointRotation * @param {number} index - The index of the joint. * @returns {Quat} The rotation of the joint relative to its parent. */ @@ -711,7 +749,7 @@ public: /**jsdoc * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. - * @function MyAvatar.getJointTranslation + * @function Avatar.getJointTranslation * @param {number} index - The index of the joint. * @returns {Vec3} The translation of the joint relative to its parent. */ @@ -724,7 +762,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointData + * @function Avatar.setJointData * @param {string} name - The name of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. * @param {Vec3} translation - The translation of the joint relative to its parent. @@ -738,7 +776,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointRotation + * @function Avatar.setJointRotation * @param {string} name - The name of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. * @example Set your avatar to its default T-pose then rotate its right arm.
@@ -759,6 +797,8 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointRotation(const QString& name, const glm::quat& rotation); @@ -769,7 +809,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointTranslation + * @function Avatar.setJointTranslation * @param {string} name - The name of the joint. * @param {Vec3} translation - The translation of the joint relative to its parent. * @example Stretch your avatar's neck. Depending on the avatar you are using, you will either see a gap between @@ -782,6 +822,8 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointData("Neck"); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointTranslation(const QString& name, const glm::vec3& translation); @@ -789,7 +831,7 @@ public: * Clear joint translations and rotations set by script for a specific joint. This restores all motion from the default * animation system including inverse kinematics for that joint. *

Note: This is slightly slower than the function variation that specifies the joint index.

- * @function MyAvatar.clearJointData + * @function Avatar.clearJointData * @param {string} name - The name of the joint. * @example Offset and restore the position of your avatar's head. * // Move your avatar's head up by 25cm from where it should be. @@ -799,11 +841,13 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointData("Neck"); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void clearJointData(const QString& name); /**jsdoc - * @function MyAvatar.isJointDataValid + * @function Avatar.isJointDataValid * @param {string} name * @returns {boolean} */ @@ -812,37 +856,43 @@ public: /**jsdoc * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. - * @function MyAvatar.getJointRotation + * @function Avatar.getJointRotation * @param {string} name - The name of the joint. * @returns {Quat} The rotation of the joint relative to its parent. * @example Report the rotation of your avatar's hips joint. * print(JSON.stringify(MyAvatar.getJointRotation("Hips"))); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual glm::quat getJointRotation(const QString& name) const; /**jsdoc * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. - * @function MyAvatar.getJointTranslation + * @function Avatar.getJointTranslation * @param {number} name - The name of the joint. * @returns {Vec3} The translation of the joint relative to its parent. * @example Report the translation of your avatar's hips joint. * print(JSON.stringify(MyAvatar.getJointRotation("Hips"))); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual glm::vec3 getJointTranslation(const QString& name) const; /**jsdoc * Get the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. - * @function MyAvatar.getJointRotations + * @function Avatar.getJointRotations * @returns {Quat[]} The rotations of all joints relative to each's parent. The values are in the same order as the array * returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. * @example Report the rotations of all your avatar's joints. * print(JSON.stringify(MyAvatar.getJointRotations())); + * + * // Note: If using from the Avatar API, replace all "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual QVector getJointRotations() const; /**jsdoc - * @function MyAvatar.getJointTranslations + * @function Avatar.getJointTranslations * @returns {Vec3[]} */ Q_INVOKABLE virtual QVector getJointTranslations() const; @@ -854,7 +904,7 @@ public: * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointRotations + * @function Avatar.setJointRotations * @param {Quat[]} jointRotations - The rotations for all joints in the avatar. The values are in the same order as the * array returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. * @example Set your avatar to its default T-pose then rotate its right arm.
@@ -880,11 +930,13 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointRotations(const QVector& jointRotations); /**jsdoc - * @function MyAvatar.setJointTranslations + * @function Avatar.setJointTranslations * @param {Vec3[]} translations */ Q_INVOKABLE virtual void setJointTranslations(const QVector& jointTranslations); @@ -892,7 +944,7 @@ public: /**jsdoc * Clear all joint translations and rotations that have been set by script. This restores all motion from the default * animation system including inverse kinematics for all joints. - * @function MyAvatar.clearJointsData + * @function Avatar.clearJointsData * @example Set your avatar to it's default T-pose for a while. * // Set all joint translations and rotations to defaults. * var i, length, rotation, translation; @@ -906,33 +958,39 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void clearJointsData(); /**jsdoc * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by * {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. - * @function MyAvatar.getJointIndex + * @function Avatar.getJointIndex * @param {string} name - The name of the joint. * @returns {number} The index of the joint. * @example Report the index of your avatar's left arm joint. * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm")); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ /// Returns the index of the joint with the specified name, or -1 if not found/unknown. Q_INVOKABLE virtual int getJointIndex(const QString& name) const; /**jsdoc * Get the names of all the joints in the current avatar. - * @function MyAvatar.getJointNames + * @function Avatar.getJointNames * @returns {string[]} The joint names. * @example Report the names of all the joints in your current avatar. * print(JSON.stringify(MyAvatar.getJointNames())); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual QStringList getJointNames() const; /**jsdoc - * @function MyAvatar.setBlendshape + * @function Avatar.setBlendshape * @param {string} name * @param {number} value */ @@ -940,14 +998,14 @@ public: /**jsdoc - * @function MyAvatar.getAttachmentsVariant + * @function Avatar.getAttachmentsVariant * @returns {object} */ // FIXME: Can this name be improved? Can it be deprecated? Q_INVOKABLE virtual QVariantList getAttachmentsVariant() const; /**jsdoc - * @function MyAvatar.setAttachmentsVariant + * @function Avatar.setAttachmentsVariant * @param {object} variant */ // FIXME: Can this name be improved? Can it be deprecated? @@ -956,21 +1014,22 @@ public: virtual void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload); /**jsdoc - * @function MyAvatar.updateAvatarEntity + * @function Avatar.updateAvatarEntity * @param {Uuid} entityID * @param {string} entityData */ Q_INVOKABLE virtual void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); /**jsdoc - * @function MyAvatar.clearAvatarEntity + * @function Avatar.clearAvatarEntity * @param {Uuid} entityID + * @param {boolean} [requiresRemovalFromTree] */ Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true); /**jsdoc - * @function MyAvatar.setForceFaceTrackerConnected + * @function Avatar.setForceFaceTrackerConnected * @param {boolean} connected */ Q_INVOKABLE void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; } @@ -1023,13 +1082,15 @@ public: /**jsdoc * Get information about all models currently attached to your avatar. - * @function MyAvatar.getAttachmentData + * @function Avatar.getAttachmentData * @returns {AttachmentData[]} Information about all models attached to your avatar. * @example Report the URLs of all current attachments. * var attachments = MyAvatar.getaAttachmentData(); * for (var i = 0; i < attachments.length; i++) { * print (attachments[i].modelURL); * } + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual QVector getAttachmentData() const; @@ -1037,7 +1098,7 @@ public: * Set all models currently attached to your avatar. For example, if you retrieve attachment data using * {@link MyAvatar.getAttachmentData} or {@link Avatar.getAttachmentData}, make changes to it, and then want to update your avatar's attachments per the * changed data. You can also remove all attachments by using setting attachmentData to null. - * @function MyAvatar.setAttachmentData + * @function Avatar.setAttachmentData * @param {AttachmentData[]} attachmentData - The attachment data defining the models to have attached to your avatar. Use * null to remove all attachments. * @example Remove a hat attachment if your avatar is wearing it. @@ -1051,6 +1112,8 @@ public: * break; * } * } + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setAttachmentData(const QVector& attachmentData); @@ -1059,7 +1122,7 @@ public: * stand on. *

Note: Attached models are models only; they are not entities and can not be manipulated using the {@link Entities} API. * Nor can you use this function to attach an entity (such as a sphere or a box) to your avatar.

- * @function MyAvatar.attach + * @function Avatar.attach * @param {string} modelURL - The URL of the model to attach. Models can be .FBX or .OBJ format. * @param {string} [jointName=""] - The name of the avatar joint (see {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}) to attach the model * to. @@ -1089,6 +1152,8 @@ public: * attachment.rotation, * attachment.scale, * attachment.isSoft); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void attach(const QString& modelURL, const QString& jointName = QString(), const glm::vec3& translation = glm::vec3(), const glm::quat& rotation = glm::quat(), @@ -1097,7 +1162,7 @@ public: /**jsdoc * Detach the most recently attached instance of a particular model from either a specific joint or any joint. - * @function MyAvatar.detachOne + * @function Avatar.detachOne * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the most * recently attached model is removed from which ever joint it was attached to. @@ -1106,7 +1171,7 @@ public: /**jsdoc * Detach all instances of a particular model from either a specific joint or all joints. - * @function MyAvatar.detachAll + * @function Avatar.detachAll * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the model is * detached from all joints. @@ -1136,13 +1201,13 @@ public: AABox getDefaultBubbleBox() const; /**jsdoc - * @function MyAvatar.getAvatarEntityData + * @function Avatar.getAvatarEntityData * @returns {object} */ Q_INVOKABLE virtual AvatarEntityMap getAvatarEntityData() const; /**jsdoc - * @function MyAvatar.setAvatarEntityData + * @function Avatar.setAvatarEntityData * @param {object} avatarEntityData */ Q_INVOKABLE virtual void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); @@ -1151,28 +1216,28 @@ public: AvatarEntityIDs getAndClearRecentlyRemovedIDs(); /**jsdoc - * @function MyAvatar.getSensorToWorldMatrix + * @function Avatar.getSensorToWorldMatrix * @returns {Mat4} */ // thread safe Q_INVOKABLE glm::mat4 getSensorToWorldMatrix() const; /**jsdoc - * @function MyAvatar.getSensorToWorldScale + * @function Avatar.getSensorToWorldScale * @returns {number} */ // thread safe Q_INVOKABLE float getSensorToWorldScale() const; /**jsdoc - * @function MyAvatar.getControllerLeftHandMatrix + * @function Avatar.getControllerLeftHandMatrix * @returns {Mat4} */ // thread safe Q_INVOKABLE glm::mat4 getControllerLeftHandMatrix() const; /**jsdoc - * @function MyAvatar.getControllerRightHandMatrix + * @function Avatar.getControllerRightHandMatrix * @returns {Mat4} */ // thread safe @@ -1180,14 +1245,14 @@ public: /**jsdoc - * @function MyAvatar.getDataRate + * @function Avatar.getDataRate * @param {string} [rateName=""] * @returns {number} */ Q_INVOKABLE float getDataRate(const QString& rateName = QString("")) const; /**jsdoc - * @function MyAvatar.getUpdateRate + * @function Avatar.getUpdateRate * @param {string} [rateName=""] * @returns {number} */ @@ -1235,32 +1300,32 @@ public: signals: /**jsdoc - * @function MyAvatar.displayNameChanged + * @function Avatar.displayNameChanged * @returns {Signal} */ void displayNameChanged(); /**jsdoc - * @function MyAvatar.sessionDisplayNameChanged + * @function Avatar.sessionDisplayNameChanged * @returns {Signal} */ void sessionDisplayNameChanged(); /**jsdoc - * @function MyAvatar.skeletonModelURLChanged + * @function Avatar.skeletonModelURLChanged * @returns {Signal} */ void skeletonModelURLChanged(); /**jsdoc - * @function MyAvatar.lookAtSnappingChanged + * @function Avatar.lookAtSnappingChanged * @param {boolean} enabled * @returns {Signal} */ void lookAtSnappingChanged(bool enabled); /**jsdoc - * @function MyAvatar.sessionUUIDChanged + * @function Avatar.sessionUUIDChanged * @returns {Signal} */ void sessionUUIDChanged(); @@ -1268,18 +1333,18 @@ signals: public slots: /**jsdoc - * @function MyAvatar.sendAvatarDataPacket + * @function Avatar.sendAvatarDataPacket * @param {boolean} [sendAll=false] */ virtual int sendAvatarDataPacket(bool sendAll = false); /**jsdoc - * @function MyAvatar.sendIdentityPacket + * @function Avatar.sendIdentityPacket */ int sendIdentityPacket(); /**jsdoc - * @function MyAvatar.setSessionUUID + * @function Avatar.setSessionUUID * @param {Uuid} sessionUUID */ virtual void setSessionUUID(const QUuid& sessionUUID) { @@ -1294,21 +1359,21 @@ public slots: } /**jsdoc - * @function MyAvatar.getAbsoluteJointRotationInObjectFrame + * @function Avatar.getAbsoluteJointRotationInObjectFrame * @param {number} index * @returns {Quat} */ virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; /**jsdoc - * @function MyAvatar.getAbsoluteJointTranslationInObjectFrame + * @function Avatar.getAbsoluteJointTranslationInObjectFrame * @param {number} index * @returns {Vec3} */ virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; /**jsdoc - * @function MyAvatar.setAbsoluteJointRotationInObjectFrame + * @function Avatar.setAbsoluteJointRotationInObjectFrame * @param {number} index * @param {Quat} rotation * @returns {boolean} @@ -1316,7 +1381,7 @@ public slots: virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } /**jsdoc - * @function MyAvatar.setAbsoluteJointTranslationInObjectFrame + * @function Avatar.setAbsoluteJointTranslationInObjectFrame * @param {number} index * @param {Vec3} translation * @returns {boolean} @@ -1324,13 +1389,13 @@ public slots: virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } /**jsdoc - * @function MyAvatar.getTargetScale + * @function Avatar.getTargetScale * @returns {number} */ float getTargetScale() const { return _targetScale; } // why is this a slot? /**jsdoc - * @function MyAvatar.resetLastSent + * @function Avatar.resetLastSent */ void resetLastSent() { _lastToByteArray = 0; } From 01119a5b5d980be426bcb20bb773d2be6591ed9e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 21 Feb 2019 11:19:29 +1300 Subject: [PATCH 003/135] Fill in Agent API JSDoc --- .../src/AgentScriptingInterface.h | 31 +++++++++++++------ .../src/avatars/ScriptableAvatar.h | 2 +- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/assignment-client/src/AgentScriptingInterface.h b/assignment-client/src/AgentScriptingInterface.h index 9fa7688778..1f592a9f18 100644 --- a/assignment-client/src/AgentScriptingInterface.h +++ b/assignment-client/src/AgentScriptingInterface.h @@ -18,16 +18,25 @@ #include "Agent.h" /**jsdoc + * The Agent API enables an assignment client to emulate an avatar. In particular, setting isAvatar = + * true connects the assignment client to the avatar and audio mixers and enables the {@link Avatar} API to be used. + * * @namespace Agent * * @hifi-assignment-client * - * @property {boolean} isAvatar - * @property {boolean} isPlayingAvatarSound Read-only. - * @property {boolean} isListeningToAudioStream - * @property {boolean} isNoiseGateEnabled - * @property {number} lastReceivedAudioLoudness Read-only. - * @property {Uuid} sessionUUID Read-only. + * @property {boolean} isAvatar - true if the assignment client script is emulating an avatar, otherwise + * false. + * @property {boolean} isPlayingAvatarSound - true if the script has a sound to play, otherwise false. + * Sounds are played when isAvatar is true, from the position and with the orientation of the + * scripted avatar's head.Read-only. + * @property {boolean} isListeningToAudioStream - true if the agent is "listening" to the audio stream from the + * domain, otherwise false. + * @property {boolean} isNoiseGateEnabled - true if the noise gate is enabled, otherwise false. When + * enabled, the input audio stream is blocked (fully attenuated) if it falls below an adaptive threshold. + * @property {number} lastReceivedAudioLoudness - The current loudness of the audio input, nominal range 0.0 (no + * sound) – 1.0 (the onset of clipping). Read-only. + * @property {Uuid} sessionUUID - The unique ID associated with the agent's current session in the domain. Read-only. */ class AgentScriptingInterface : public QObject { Q_OBJECT @@ -54,20 +63,24 @@ public: public slots: /**jsdoc + * Set whether or not the script should emulate an avatar. * @function Agent.setIsAvatar - * @param {boolean} isAvatar + * @param {boolean} isAvatar - true if the script should act as if an avatar, otherwise false. */ void setIsAvatar(bool isAvatar) const { _agent->setIsAvatar(isAvatar); } /**jsdoc + * Check whether or not the script is emulating an avatar. * @function Agent.isAvatar - * @returns {boolean} + * @returns {boolean} true if the script is acting as if an avatar, otherwise false. */ bool isAvatar() const { return _agent->isAvatar(); } /**jsdoc + * Play a sound from the position and with the orientation of the emulated avatar's head. No sound is played unless + * isAvatar == true. * @function Agent.playAvatarSound - * @param {object} avatarSound + * @param {SoundObject} avatarSound - The sound to play. */ void playAvatarSound(SharedSoundPointer avatarSound) const { _agent->playAvatarSound(avatarSound); } diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index b2ad4527b0..6b78f666e1 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -20,7 +20,7 @@ /**jsdoc * The Avatar API is used to manipulate scriptable avatars on the domain. This API is a subset of the - * {@link MyAvatar} API. + * {@link MyAvatar} API. To enable this API, set {@link Agent|Agent.isAvatatr} to true. * * @namespace Avatar * From 7f1ae634391eb8dce84f086ad60835b109c93b2f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 21 Feb 2019 14:30:38 +1300 Subject: [PATCH 004/135] Revise current Avatar API JSDoc --- .../src/avatars/ScriptableAvatar.h | 48 +++++++---------- libraries/avatars/src/AvatarData.h | 54 ++++++++++--------- 2 files changed, 47 insertions(+), 55 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 6b78f666e1..3cf10cc129 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -28,7 +28,8 @@ * * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. * @property {Vec3} position - * @property {number} scale - Returns the clamped scale of the avatar. + * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 + * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. * @property {number} density Read-only. * @property {Vec3} handPosition * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. @@ -50,8 +51,9 @@ * @property {number} audioLoudness * @property {number} audioAverageLoudness * @property {string} displayName - * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer - * rather than by Interface clients. The result is unique among all avatars present at the time. + * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the + * avatar mixer rather than by Interface clients. The result is unique among all avatars present on the domain at the + * time. * @property {boolean} lookAtSnappingEnabled * @property {string} skeletonModelURL * @property {AttachmentData[]} attachmentData @@ -102,24 +104,12 @@ public: Q_INVOKABLE AnimationDetails getAnimationDetails(); /**jsdoc - * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
- * Get the names of all the joints in the current avatar. - * @function Avatar.getJointNames - * @returns {string[]} The joint names. - * @example Report the names of all the joints in your current avatar. - * print(JSON.stringify(Avatar.getJointNames())); + * @comment Uses the base class's JSDoc. */ Q_INVOKABLE virtual QStringList getJointNames() const override; /**jsdoc - * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
- * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by - * {@link Avatar.getJointNames}. - * @function Avatar.getJointIndex - * @param {string} name - The name of the joint. - * @returns {number} The index of the joint. - * @example Report the index of your avatar's left arm joint. - * print(JSON.stringify(Avatar.getJointIndex("LeftArm")); + * @comment Uses the base class's JSDoc. */ /// Returns the index of the joint with the specified name, or -1 if not found/unknown. Q_INVOKABLE virtual int getJointIndex(const QString& name) const override; @@ -137,39 +127,37 @@ public: void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement); bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } - /**jsdoc - * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc.
- * ####### Also need to resolve with MyAvatar.
- * Potentially Very Expensive. Do not use. + /**jsdoc + * Get the avatar entities as binary data. + *

Warning: Potentially a very expensive call. Do not use if possible.

* @function Avatar.getAvatarEntityData - * @returns {object} + * @returns {AvatarEntityMap} */ Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc. + * Set the avatar entities from binary data. + *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.setAvatarEntityData - * @param {object} avatarEntityData + * @param {AvatarEntityMap} avatarEntityData */ Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; /**jsdoc - * ####### TODO: If this override changes the function use @override and do JSDoc here, otherwise @comment that uses base class's JSDoc. - * @function Avatar.updateAvatarEntity - * @param {Uuid} entityID - * @param {string} entityData + * @comment Uses the base class's JSDoc. */ Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override; public slots: /**jsdoc * @function Avatar.update + * @param {number} deltaTime */ void update(float deltatime); /**jsdoc - * @function Avatar.setJointMappingsFromNetworkReply - */ + * @function Avatar.setJointMappingsFromNetworkReply + */ void setJointMappingsFromNetworkReply(); private: diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 7a4d235565..0a5509f46c 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -61,6 +61,10 @@ using AvatarSharedPointer = std::shared_ptr; using AvatarWeakPointer = std::weak_ptr; using AvatarHash = QHash; +/**jsdoc + * An object with the UUIDs of avatar entities as keys and binary blobs, being the entity properties, as values. + * @typedef {Object.>} AvatarEntityMap + */ using AvatarEntityMap = QMap; using PackedAvatarEntityMap = QMap; // similar to AvatarEntityMap, but different internal format using AvatarEntityIDs = QSet; @@ -414,7 +418,8 @@ class AvatarData : public QObject, public SpatiallyNestable { // IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h and ScriptableAvatar.h. /* * @property {Vec3} position - * @property {number} scale - Returns the clamped scale of the avatar. + * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 + * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. * @property {number} density Read-only. * @property {Vec3} handPosition * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. @@ -436,8 +441,9 @@ class AvatarData : public QObject, public SpatiallyNestable { * @property {number} audioLoudness * @property {number} audioAverageLoudness * @property {string} displayName - * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer - * rather than by Interface clients. The result is unique among all avatars present at the time. + * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the + * avatar mixer rather than by Interface clients. The result is unique among all avatars present on the domain at the + * time. * @property {boolean} lookAtSnappingEnabled * @property {string} skeletonModelURL * @property {AttachmentData[]} attachmentData @@ -601,18 +607,18 @@ public: virtual bool getHasAudioEnabledFaceMovement() const { return false; } /**jsdoc - * Returns the minimum scale allowed for this avatar in the current domain. + * Get the minimum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. * @function Avatar.getDomainMinScale - * @returns {number} minimum scale allowed for this avatar in the current domain. + * @returns {number} The minimum scale allowed for this avatar in the current domain. */ Q_INVOKABLE float getDomainMinScale() const; /**jsdoc - * Returns the maximum scale allowed for this avatar in the current domain. + * Get the maximum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. * @function Avatar.getDomainMaxScale - * @returns {number} maximum scale allowed for this avatar in the current domain. + * @returns {number} The maximum scale allowed for this avatar in the current domain. */ Q_INVOKABLE float getDomainMaxScale() const; @@ -625,18 +631,18 @@ public: virtual bool canMeasureEyeHeight() const { return false; } /**jsdoc - * Provides read only access to the current eye height of the avatar. + * Get the current eye height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. * @function Avatar.getEyeHeight - * @returns {number} Eye height of avatar in meters. + * @returns {number} The eye height of the avatar. */ Q_INVOKABLE virtual float getEyeHeight() const { return _targetScale * getUnscaledEyeHeight(); } /**jsdoc - * Provides read only access to the current height of the avatar. + * Get the current height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. * @function Avatar.getHeight - * @returns {number} Height of avatar in meters. + * @returns {number} The height of the avatar. */ Q_INVOKABLE virtual float getHeight() const; @@ -739,7 +745,7 @@ public: /**jsdoc * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointRotation * @param {number} index - The index of the joint. * @returns {Quat} The rotation of the joint relative to its parent. @@ -748,7 +754,7 @@ public: /**jsdoc * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointTranslation * @param {number} index - The index of the joint. * @returns {Vec3} The translation of the joint relative to its parent. @@ -855,7 +861,7 @@ public: /**jsdoc * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointRotation * @param {string} name - The name of the joint. * @returns {Quat} The rotation of the joint relative to its parent. @@ -868,7 +874,7 @@ public: /**jsdoc * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointTranslation * @param {number} name - The name of the joint. * @returns {Vec3} The translation of the joint relative to its parent. @@ -883,7 +889,7 @@ public: * Get the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. * @function Avatar.getJointRotations * @returns {Quat[]} The rotations of all joints relative to each's parent. The values are in the same order as the array - * returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. + * returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. * @example Report the rotations of all your avatar's joints. * print(JSON.stringify(MyAvatar.getJointRotations())); * @@ -906,7 +912,7 @@ public: * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

* @function Avatar.setJointRotations * @param {Quat[]} jointRotations - The rotations for all joints in the avatar. The values are in the same order as the - * array returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. + * array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. * @example Set your avatar to its default T-pose then rotate its right arm.
* Avatar in T-pose * // Set all joint translations and rotations to defaults. @@ -965,10 +971,10 @@ public: /**jsdoc * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by - * {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. + * {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. * @function Avatar.getJointIndex * @param {string} name - The name of the joint. - * @returns {number} The index of the joint. + * @returns {number} The index of the joint if valid, otherwise -1. * @example Report the index of your avatar's left arm joint. * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm")); * @@ -1016,14 +1022,14 @@ public: /**jsdoc * @function Avatar.updateAvatarEntity * @param {Uuid} entityID - * @param {string} entityData + * @param {Array.} entityData */ Q_INVOKABLE virtual void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); /**jsdoc * @function Avatar.clearAvatarEntity * @param {Uuid} entityID - * @param {boolean} [requiresRemovalFromTree] + * @param {boolean} [requiresRemovalFromTree=true] */ Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true); @@ -1201,14 +1207,12 @@ public: AABox getDefaultBubbleBox() const; /**jsdoc - * @function Avatar.getAvatarEntityData - * @returns {object} + * @comment Documented in derived classes' JSDoc. */ Q_INVOKABLE virtual AvatarEntityMap getAvatarEntityData() const; /**jsdoc - * @function Avatar.setAvatarEntityData - * @param {object} avatarEntityData + * @comment Documented in derived classes' JSDoc. */ Q_INVOKABLE virtual void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); From 0c59f983daacdd96ab12939e90361ce2a3186756 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Feb 2019 09:34:05 +1300 Subject: [PATCH 005/135] Agent JSDoc tidying --- assignment-client/src/AgentScriptingInterface.h | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/AgentScriptingInterface.h b/assignment-client/src/AgentScriptingInterface.h index 1f592a9f18..1242634dd5 100644 --- a/assignment-client/src/AgentScriptingInterface.h +++ b/assignment-client/src/AgentScriptingInterface.h @@ -63,21 +63,27 @@ public: public slots: /**jsdoc - * Set whether or not the script should emulate an avatar. + * Sets whether or not the script should emulate an avatar. * @function Agent.setIsAvatar * @param {boolean} isAvatar - true if the script should act as if an avatar, otherwise false. + * @example Make an assignment client script emulate an avatar. + * (function () { + * Agent.setIsAvatar(true); + * Avatar.displayName = "AC avatar"; + * print("Position: " + JSON.stringify(Avatar.position)); // 0, 0, 0 + * }()); */ void setIsAvatar(bool isAvatar) const { _agent->setIsAvatar(isAvatar); } /**jsdoc - * Check whether or not the script is emulating an avatar. + * Checks whether or not the script is emulating an avatar. * @function Agent.isAvatar * @returns {boolean} true if the script is acting as if an avatar, otherwise false. */ bool isAvatar() const { return _agent->isAvatar(); } /**jsdoc - * Play a sound from the position and with the orientation of the emulated avatar's head. No sound is played unless + * Plays a sound from the position and with the orientation of the emulated avatar's head. No sound is played unless * isAvatar == true. * @function Agent.playAvatarSound * @param {SoundObject} avatarSound - The sound to play. From e79594ef53458f33f63ea5c135b48ce2d9698152 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Feb 2019 09:53:11 +1300 Subject: [PATCH 006/135] Fill in and tidy Avatar JSDoc --- .../src/avatars/ScriptableAvatar.h | 82 ++++-- .../src/avatars-renderer/Avatar.h | 22 +- libraries/avatars/src/AvatarData.cpp | 85 +++++- libraries/avatars/src/AvatarData.h | 277 ++++++++++++------ libraries/shared/src/RegisteredMetaTypes.cpp | 26 +- 5 files changed, 352 insertions(+), 140 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 3cf10cc129..2d8dce23de 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -22,23 +22,27 @@ * The Avatar API is used to manipulate scriptable avatars on the domain. This API is a subset of the * {@link MyAvatar} API. To enable this API, set {@link Agent|Agent.isAvatatr} to true. * + *

For Interface, client entity, and avatar scripts, see {@link MyAvatar}.

+ * * @namespace Avatar * * @hifi-assignment-client * * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. - * @property {Vec3} position + * @property {Vec3} position - The position of the avatar. * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. - * @property {number} density Read-only. - * @property {Vec3} handPosition + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * the application of physics. Read-only. + * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar + * but is otherwise not used or changed by Interface. * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is * sometimes called "bank". - * @property {Quat} orientation + * @property {Quat} orientation - The orientation of the avatar. * @property {Quat} headOrientation - The orientation of the avatar's head. * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is * sometimes called "elevation". @@ -46,24 +50,31 @@ * head. Yaw is sometimes called "heading". * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is * sometimes called "bank". - * @property {Vec3} velocity - * @property {Vec3} angularVelocity - * @property {number} audioLoudness - * @property {number} audioAverageLoudness - * @property {string} displayName + * @property {Vec3} velocity - The current velocity of the avatar. + * @property {Vec3} angularVelocity - The current angular velocity of the avatar. + * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the + * domain. + * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting + * into the domain. + * @property {string} displayName - The avatar's display name. * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the - * avatar mixer rather than by Interface clients. The result is unique among all avatars present on the domain at the + * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the * time. - * @property {boolean} lookAtSnappingEnabled - * @property {string} skeletonModelURL - * @property {AttachmentData[]} attachmentData + * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's + * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. + * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. - * @property {Uuid} sessionUUID Read-only. - * @property {Mat4} sensorToWorldMatrix Read-only. - * @property {Mat4} controllerLeftHandMatrix Read-only. - * @property {Mat4} controllerRightHandMatrix Read-only. - * @property {number} sensorToWorldScale Read-only. - * + * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. + * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the + * avatar's size, orientation, and position in the virtual world. Read-only. + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * avatar. Read-only. + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * avatar. Read-only. + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * size in the virtual world. Read-only. */ class ScriptableAvatar : public AvatarData, public Dependency { @@ -77,15 +88,17 @@ public: ScriptableAvatar(); /**jsdoc + * Starts playing an animation on the avatar. * @function Avatar.startAnimation - * @param {string} url - * @param {number} [fps=30] - * @param {number} [priority=1] - * @param {boolean} [loop=false] - * @param {boolean} [hold=false] - * @param {number} [firstFrame=0] - * @param {number} [lastFrame=3.403e+38] - * @param {string[]} [maskedJoints=[]] + * @param {string} url - The URL to the animation file. Animation files need to be .FBX format but only need to contain + * the avatar skeleton and animation data. + * @param {number} [fps=30] - The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. + * @param {number} [priority=1] - Not used. + * @param {boolean} [loop=false] - true if the animation should loop, false if it shouldn't. + * @param {boolean} [hold=false] - Not used. + * @param {number} [firstFrame=0] - The frame the animation should start at. + * @param {number} [lastFrame=3.403e+38] - The frame the animation should stop at. + * @param {string[]} [maskedJoints=[]] - The names of joints that should not be animated. */ /// Allows scripts to run animations. Q_INVOKABLE void startAnimation(const QString& url, float fps = 30.0f, float priority = 1.0f, bool loop = false, @@ -93,13 +106,15 @@ public: const QStringList& maskedJoints = QStringList()); /**jsdoc + * Stops playing the current animation. * @function Avatar.stopAnimation */ Q_INVOKABLE void stopAnimation(); /**jsdoc + * Gets the details of the current avatar animation that is being or was recently played. * @function Avatar.getAnimationDetails - * @returns {Avatar.AnimationDetails} + * @returns {Avatar.AnimationDetails} The current or recent avatar animation. */ Q_INVOKABLE AnimationDetails getAnimationDetails(); @@ -116,6 +131,9 @@ public: virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; + /**jsdoc + * @comment Uses the base class's JSDoc. + */ int sendAvatarDataPacket(bool sendAll = false) override; virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override; @@ -128,7 +146,7 @@ public: bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } /**jsdoc - * Get the avatar entities as binary data. + * Gets the avatar entities as binary data. *

Warning: Potentially a very expensive call. Do not use if possible.

* @function Avatar.getAvatarEntityData * @returns {AvatarEntityMap} @@ -136,7 +154,7 @@ public: Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * Set the avatar entities from binary data. + * Sets the avatar entities from binary data. *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.setAvatarEntityData * @param {AvatarEntityMap} avatarEntityData @@ -151,12 +169,14 @@ public: public slots: /**jsdoc * @function Avatar.update - * @param {number} deltaTime + * @param {number} deltaTime - Delta time. + * @deprecated This function is deprecated and will be removed. */ void update(float deltatime); /**jsdoc * @function Avatar.setJointMappingsFromNetworkReply + * @deprecated This function is deprecated and will be removed. */ void setJointMappingsFromNetworkReply(); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 1aa6829160..98aa255641 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -219,7 +219,7 @@ public: * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame - * @param index {number} index number + * @param index {number} -The joint index. * @returns {Quat} The rotation of this joint in avatar coordinates. */ Q_INVOKABLE virtual glm::quat getAbsoluteDefaultJointRotationInObjectFrame(int index) const; @@ -229,7 +229,7 @@ public: * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame - * @param index {number} index number + * @param index {number} - The joint index. * @returns {Vec3} The position of this joint in avatar coordinates. */ Q_INVOKABLE virtual glm::vec3 getAbsoluteDefaultJointTranslationInObjectFrame(int index) const; @@ -238,7 +238,25 @@ public: virtual glm::vec3 getAbsoluteJointScaleInObjectFrame(int index) const override; virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; + + /**jsdoc + * Sets the rotation of a joint relative to the avatar. + *

Warning: Not able to be used in the MyAvatar API.

+ * @function MyAvatar.setAbsoluteJointRotationInObjectFrame + * @param {number} index - The index of the joint. Not used. + * @param {Quat} rotation - The rotation of the joint relative to the avatar. Not used. + * @returns {boolean} false. + */ virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } + + /**jsdoc + * Sets the translation of a joint relative to the avatar. + *

Warning: Not able to be used in the MyAvatar API.

+ * @function MyAvatar.setAbsoluteJointTranslationInObjectFrame + * @param {number} index - The index of the joint. Not used. + * @param {Vec3} translation - The translation of the joint relative to the avatar. Not used. + * @returns {boolean} false. + */ virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } // world-space to avatar-space rigconversion functions diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c733cfa291..88949900ce 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1425,6 +1425,47 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { return numBytesRead; } +/**jsdoc + * The avatar mixer data comprises different types of data, with the data rates of each being tracked in kbps. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Rate NameDescription
"globalPosition"Incoming global position.
"localPosition"Incoming local position.
"avatarBoundingBox"Incoming avatar bounding box.
"avatarOrientation"Incoming avatar orientation.
"avatarScale"Incoming avatar scale.
"lookAtPosition"Incoming look-at position.
"audioLoudness"Incoming audio loudness.
"sensorToWorkMatrix"Incoming sensor-to-world matrix.
"additionalFlags"Incoming additional avatar flags.
"parentInfo"Incoming parent information.
"faceTracker"Incoming face tracker data.
"jointData"Incoming joint data.
"jointDefaultPoseFlagsRate"Incoming joint default pose flags.
"farGrabJointRate"Incoming far grab joint.
"globalPositionOutbound"Outgoing global position.
"localPositionOutbound"Outgoing local position.
"avatarBoundingBoxOutbound"Outgoing avatar bounding box.
"avatarOrientationOutbound"Outgoing avatar orientation.
"avatarScaleOutbound"Outgoing avatar scale.
"lookAtPositionOutbound"Outgoing look-at position.
"audioLoudnessOutbound"Outgoing audio loudness.
"sensorToWorkMatrixOutbound"Outgoing sensor-to-world matrix.
"additionalFlagsOutbound"Outgoing additional avatar flags.
"parentInfoOutbound"Outgoing parent information.
"faceTrackerOutbound"Outgoing face tracker data.
"jointDataOutbound"Outgoing joint data.
"jointDefaultPoseFlagsOutbound"Outgoing joint default pose flags.
""When no rate name is specified, the total incoming data rate is provided.
+ * + * @typedef {string} AvatarDataRate + */ float AvatarData::getDataRate(const QString& rateName) const { if (rateName == "") { return _parseBufferRate.rate() / BYTES_PER_KILOBIT; @@ -1486,6 +1527,35 @@ float AvatarData::getDataRate(const QString& rateName) const { return 0.0f; } +/**jsdoc + * The avatar mixer data comprises different types of data updated at different rates, in Hz. + * + * + * + * + * + * + + * + * + * + * + * + * + * + * + * + * + * + * + * + + * + * + *
Rate NameDescription
"globalPosition"Global position.
"localPosition"Local position.
"avatarBoundingBox"Avatar bounding box.
"avatarOrientation"Avatar orientation.
"avatarScale"Avatar scale.
"lookAtPosition"Look-at position.
"audioLoudness"Audio loudness.
"sensorToWorkMatrix"Sensor-to-world matrix.
"additionalFlags"Additional avatar flags.
"parentInfo"Parent information.
"faceTracker"Face tracker data.
"jointData"Joint data.
"farGrabJointData"Far grab joint data.
""When no rate name is specified, the overall update rate is provided.
+ * + * @typedef {string} AvatarUpdateRate + */ float AvatarData::getUpdateRate(const QString& rateName) const { if (rateName == "") { return _parseBufferUpdateRate.rate(); @@ -2721,13 +2791,16 @@ glm::vec3 AvatarData::getAbsoluteJointTranslationInObjectFrame(int index) const } /**jsdoc + * Information on an attachment worn by the avatar. * @typedef {object} AttachmentData - * @property {string} modelUrl - * @property {string} jointName - * @property {Vec3} translation - * @property {Vec3} rotation - * @property {number} scale - * @property {boolean} soft + * @property {string} modelUrl - The URL of the model file. Models can be .FBX or .OBJ format. + * @property {string} jointName - The offset to apply to the model relative to the joint position. + * @property {Vec3} translation - The offset from the joint that the attachment is positioned at. + * @property {Vec3} rotation - The rotation applied to the model relative to the joint orientation. + * @property {number} scale - The scale applied to the attachment model. + * @property {boolean} soft - If true and the model has a skeleton, the bones of the attached model's skeleton are + * rotated to fit the avatar's current pose. If true, the translation, rotation, and + * scale parameters are ignored. */ QVariant AttachmentData::toVariant() const { QVariantMap result; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 0a5509f46c..a20518076f 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -116,7 +116,24 @@ const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit - +/**jsdoc + *

The pointing state of the hands is specified by the following values: +

+ * + * + * + * + * + * + * + * + * + * + *
ValueDescription
0No hand is pointing.
1The left hand is pointing.
2The right hand is pointing.
4It is the index finger that is pointing.
+ *

The values for the hand states are added together to give the HandState value. For example, if the left + * hand's finger is pointing, the value is 1 + 4 == 5. + * @typedef {number} HandState + */ const char HAND_STATE_NULL = 0; const char LEFT_HAND_POINTING_FLAG = 1; const char RIGHT_HAND_POINTING_FLAG = 2; @@ -417,18 +434,20 @@ class AvatarData : public QObject, public SpatiallyNestable { // IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h and ScriptableAvatar.h. /* - * @property {Vec3} position + * @property {Vec3} position - The position of the avatar. * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. - * @property {number} density Read-only. - * @property {Vec3} handPosition + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * the application of physics. Read-only. + * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar + * but is otherwise not used or changed by Interface. * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is * sometimes called "bank". - * @property {Quat} orientation + * @property {Quat} orientation - The orientation of the avatar. * @property {Quat} headOrientation - The orientation of the avatar's head. * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is * sometimes called "elevation". @@ -436,23 +455,31 @@ class AvatarData : public QObject, public SpatiallyNestable { * head. Yaw is sometimes called "heading". * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is * sometimes called "bank". - * @property {Vec3} velocity - * @property {Vec3} angularVelocity - * @property {number} audioLoudness - * @property {number} audioAverageLoudness - * @property {string} displayName + * @property {Vec3} velocity - The current velocity of the avatar. + * @property {Vec3} angularVelocity - The current angular velocity of the avatar. + * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the + * domain. + * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting + * into the domain. + * @property {string} displayName - The avatar's display name. * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the - * avatar mixer rather than by Interface clients. The result is unique among all avatars present on the domain at the + * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the * time. - * @property {boolean} lookAtSnappingEnabled - * @property {string} skeletonModelURL - * @property {AttachmentData[]} attachmentData + * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's + * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. + * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. - * @property {Uuid} sessionUUID Read-only. - * @property {Mat4} sensorToWorldMatrix Read-only. - * @property {Mat4} controllerLeftHandMatrix Read-only. - * @property {Mat4} controllerRightHandMatrix Read-only. - * @property {number} sensorToWorldScale Read-only. + * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. + * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the + * avatar's size, orientation, and position in the virtual world. Read-only. + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * avatar. Read-only. + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * avatar. Read-only. + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * size in the virtual world. Read-only. */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) @@ -607,7 +634,7 @@ public: virtual bool getHasAudioEnabledFaceMovement() const { return false; } /**jsdoc - * Get the minimum scale allowed for this avatar in the current domain. + * Gets the minimum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. * @function Avatar.getDomainMinScale * @returns {number} The minimum scale allowed for this avatar in the current domain. @@ -615,7 +642,7 @@ public: Q_INVOKABLE float getDomainMinScale() const; /**jsdoc - * Get the maximum scale allowed for this avatar in the current domain. + * Gets the maximum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. * @function Avatar.getDomainMaxScale * @returns {number} The maximum scale allowed for this avatar in the current domain. @@ -631,7 +658,7 @@ public: virtual bool canMeasureEyeHeight() const { return false; } /**jsdoc - * Get the current eye height of the avatar. + * Gets the current eye height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. * @function Avatar.getEyeHeight * @returns {number} The eye height of the avatar. @@ -639,7 +666,7 @@ public: Q_INVOKABLE virtual float getEyeHeight() const { return _targetScale * getUnscaledEyeHeight(); } /**jsdoc - * Get the current height of the avatar. + * Gets the current height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. * @function Avatar.getHeight * @returns {number} The height of the avatar. @@ -652,27 +679,33 @@ public: void setDomainMaximumHeight(float domainMaximumHeight); /**jsdoc + * Sets the pointing state of the hands to control where the laser emanates from. If the right index finger is pointing, the + * laser emanates from the tip of that finger, otherwise it emanates from the palm. * @function Avatar.setHandState - * @param {string} state + * @param {HandState} state - The pointing state of the hand. */ Q_INVOKABLE void setHandState(char s) { _handState = s; } /**jsdoc + * Gets the pointing state of the hands to control where the laser emanates from. If the right index finger is pointing, the + * laser emanates from the tip of that finger, otherwise it emanates from the palm. * @function Avatar.getHandState - * @returns {string} + * @returns {HandState} The pointing state of the hand. */ Q_INVOKABLE char getHandState() const { return _handState; } const QVector& getRawJointData() const { return _jointData; } /**jsdoc + * Sets joint translations and rotations from raw joint data. * @function Avatar.setRawJointData - * @param {JointData[]} data + * @param {JointData[]} data - The raw joint data. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void setRawJointData(QVector data); /**jsdoc - * Set a specific joint's rotation and position relative to its parent. + * Sets a specific joint's rotation and position relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -702,7 +735,7 @@ public: Q_INVOKABLE virtual void setJointData(int index, const glm::quat& rotation, const glm::vec3& translation); /**jsdoc - * Set a specific joint's rotation relative to its parent. + * Sets a specific joint's rotation relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -715,7 +748,7 @@ public: Q_INVOKABLE virtual void setJointRotation(int index, const glm::quat& rotation); /**jsdoc - * Set a specific joint's translation relative to its parent. + * Sets a specific joint's translation relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -728,7 +761,7 @@ public: Q_INVOKABLE virtual void setJointTranslation(int index, const glm::vec3& translation); /**jsdoc - * Clear joint translations and rotations set by script for a specific joint. This restores all motion from the default + * Clears joint translations and rotations set by script for a specific joint. This restores all motion from the default * animation system including inverse kinematics for that joint. *

Note: This is slightly faster than the function variation that specifies the joint name.

* @function Avatar.clearJointData @@ -737,14 +770,15 @@ public: Q_INVOKABLE virtual void clearJointData(int index); /**jsdoc + * Checks that the data for a joint are valid. * @function Avatar.isJointDataValid - * @param {number} index - * @returns {boolean} + * @param {number} index - The index of the joint. + * @returns {boolean} true if the joint data is valid, false if not. */ Q_INVOKABLE bool isJointDataValid(int index) const; /**jsdoc - * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see + * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. * @function Avatar.getJointRotation * @param {number} index - The index of the joint. @@ -753,7 +787,7 @@ public: Q_INVOKABLE virtual glm::quat getJointRotation(int index) const; /**jsdoc - * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see + * Gets the translation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. * @function Avatar.getJointTranslation * @param {number} index - The index of the joint. @@ -762,7 +796,7 @@ public: Q_INVOKABLE virtual glm::vec3 getJointTranslation(int index) const; /**jsdoc - * Set a specific joint's rotation and position relative to its parent. + * Sets a specific joint's rotation and position relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -776,7 +810,7 @@ public: Q_INVOKABLE virtual void setJointData(const QString& name, const glm::quat& rotation, const glm::vec3& translation); /**jsdoc - * Set a specific joint's rotation relative to its parent. + * Sets a specific joint's rotation relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -809,7 +843,7 @@ public: Q_INVOKABLE virtual void setJointRotation(const QString& name, const glm::quat& rotation); /**jsdoc - * Set a specific joint's translation relative to its parent. + * Sets a specific joint's translation relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -834,7 +868,7 @@ public: Q_INVOKABLE virtual void setJointTranslation(const QString& name, const glm::vec3& translation); /**jsdoc - * Clear joint translations and rotations set by script for a specific joint. This restores all motion from the default + * Clears joint translations and rotations set by script for a specific joint. This restores all motion from the default * animation system including inverse kinematics for that joint. *

Note: This is slightly slower than the function variation that specifies the joint index.

* @function Avatar.clearJointData @@ -853,14 +887,15 @@ public: Q_INVOKABLE virtual void clearJointData(const QString& name); /**jsdoc + * Checks that the data for a joint are valid. * @function Avatar.isJointDataValid - * @param {string} name - * @returns {boolean} + * @param {string} name - The name of the joint. + * @returns {boolean} true if the joint data is valid, false if not. */ Q_INVOKABLE virtual bool isJointDataValid(const QString& name) const; /**jsdoc - * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see + * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. * @function Avatar.getJointRotation * @param {string} name - The name of the joint. @@ -873,7 +908,7 @@ public: Q_INVOKABLE virtual glm::quat getJointRotation(const QString& name) const; /**jsdoc - * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see + * Gets the translation of a joint relative to its parent. For information on the joint hierarchy used, see * Avatar Standards. * @function Avatar.getJointTranslation * @param {number} name - The name of the joint. @@ -886,7 +921,7 @@ public: Q_INVOKABLE virtual glm::vec3 getJointTranslation(const QString& name) const; /**jsdoc - * Get the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. + * Gets the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. * @function Avatar.getJointRotations * @returns {Quat[]} The rotations of all joints relative to each's parent. The values are in the same order as the array * returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. @@ -898,13 +933,15 @@ public: Q_INVOKABLE virtual QVector getJointRotations() const; /**jsdoc + * Gets the translations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. * @function Avatar.getJointTranslations - * @returns {Vec3[]} + * @returns {Vec3[]} The translations of all joints relative to each's parent. The values are in the same order as the array + * returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. */ Q_INVOKABLE virtual QVector getJointTranslations() const; /**jsdoc - * Set the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. + * Sets the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate @@ -942,13 +979,20 @@ public: Q_INVOKABLE virtual void setJointRotations(const QVector& jointRotations); /**jsdoc + * Sets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint. + *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, + * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate + * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set + * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

* @function Avatar.setJointTranslations - * @param {Vec3[]} translations + * @param {Vec3[]} translations - The translations for all joints in the avatar. The values are in the same order as the + * array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. */ Q_INVOKABLE virtual void setJointTranslations(const QVector& jointTranslations); /**jsdoc - * Clear all joint translations and rotations that have been set by script. This restores all motion from the default + * Clears all joint translations and rotations that have been set by script. This restores all motion from the default * animation system including inverse kinematics for all joints. * @function Avatar.clearJointsData * @example Set your avatar to it's default T-pose for a while. @@ -970,7 +1014,7 @@ public: Q_INVOKABLE virtual void clearJointsData(); /**jsdoc - * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by + * Gets the joint index for a named joint. The joint index value is the position of the joint in the array returned by * {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. * @function Avatar.getJointIndex * @param {string} name - The name of the joint. @@ -984,7 +1028,7 @@ public: Q_INVOKABLE virtual int getJointIndex(const QString& name) const; /**jsdoc - * Get the names of all the joints in the current avatar. + * Gets the names of all the joints in the current avatar. * @function Avatar.getJointNames * @returns {string[]} The joint names. * @example Report the names of all the joints in your current avatar. @@ -996,23 +1040,32 @@ public: /**jsdoc + * Sets the value of a blendshape to animate your avatar's face. To enable other users to see the resulting animation of + * your avatar's face, use {@link Avatar.setForceFaceTrackerConnected} or {@link MyAvatar.setForceFaceTrackerConnected}. * @function Avatar.setBlendshape - * @param {string} name - * @param {number} value + * @param {string} name - The name of the blendshape, per the + * {@link https://docs.highfidelity.com/create/avatars/create-avatars/avatar-standards.html#blendshapes Avatar Standards}. + * @param {number} value - A value between 0.0 and 1.0. */ Q_INVOKABLE void setBlendshape(QString name, float val) { _headData->setBlendshape(name, val); } /**jsdoc + * Gets information about the models currently attached to your avatar. * @function Avatar.getAttachmentsVariant - * @returns {object} + * @returns {AttachmentData[]} Information about all models attached to your avatar. + * @deprecated Use avatar entities instead. */ // FIXME: Can this name be improved? Can it be deprecated? Q_INVOKABLE virtual QVariantList getAttachmentsVariant() const; /**jsdoc + * Sets all models currently attached to your avatar. For example, if you retrieve attachment data using + * {@link MyAvatar.getAttachmentsVariant} or {@link Avatar.getAttachmentsVariant}, make changes to it, and then want to + * update your avatar's attachments per the changed data. * @function Avatar.setAttachmentsVariant - * @param {object} variant + * @param {AttachmentData[]} variant - The attachment data defining the models to have attached to your avatar. + * @deprecated Use avatar entities instead. */ // FIXME: Can this name be improved? Can it be deprecated? Q_INVOKABLE virtual void setAttachmentsVariant(const QVariantList& variant); @@ -1021,22 +1074,27 @@ public: /**jsdoc * @function Avatar.updateAvatarEntity - * @param {Uuid} entityID - * @param {Array.} entityData + * @param {Uuid} entityID - The entity ID. + * @param {Array.} entityData - Entity data. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE virtual void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); /**jsdoc * @function Avatar.clearAvatarEntity - * @param {Uuid} entityID - * @param {boolean} [requiresRemovalFromTree=true] + * @param {Uuid} entityID - The entity ID. + * @param {boolean} [requiresRemovalFromTree=true] - Requires removal from tree. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true); /**jsdoc + * Enables blendshapes set using {@link Avatar.setBlendshape} or {@link MyAvatar.setBlendshape} to be transmitted to other + * users so that they can see the animation of your avatar's face. * @function Avatar.setForceFaceTrackerConnected - * @param {boolean} connected + * @param {boolean} connected - true to enable blendshape changes to be transmitted to other users, + * false to disable. */ Q_INVOKABLE void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; } @@ -1087,9 +1145,10 @@ public: } /**jsdoc - * Get information about all models currently attached to your avatar. + * Gets information about the models currently attached to your avatar. * @function Avatar.getAttachmentData * @returns {AttachmentData[]} Information about all models attached to your avatar. + * @deprecated Use avatar entities instead. * @example Report the URLs of all current attachments. * var attachments = MyAvatar.getaAttachmentData(); * for (var i = 0; i < attachments.length; i++) { @@ -1101,12 +1160,13 @@ public: Q_INVOKABLE virtual QVector getAttachmentData() const; /**jsdoc - * Set all models currently attached to your avatar. For example, if you retrieve attachment data using + * Sets all models currently attached to your avatar. For example, if you retrieve attachment data using * {@link MyAvatar.getAttachmentData} or {@link Avatar.getAttachmentData}, make changes to it, and then want to update your avatar's attachments per the * changed data. You can also remove all attachments by using setting attachmentData to null. * @function Avatar.setAttachmentData - * @param {AttachmentData[]} attachmentData - The attachment data defining the models to have attached to your avatar. Use + * @param {AttachmentData[]} attachmentData - The attachment data defining the models to have attached to your avatar. Use * null to remove all attachments. + * @deprecated Use avatar entities instead. * @example Remove a hat attachment if your avatar is wearing it. * var hatURL = "https://s3.amazonaws.com/hifi-public/tony/cowboy-hat.fbx"; * var attachments = MyAvatar.getAttachmentData(); @@ -1124,7 +1184,7 @@ public: Q_INVOKABLE virtual void setAttachmentData(const QVector& attachmentData); /**jsdoc - * Attach a model to your avatar. For example, you can give your avatar a hat to wear, a guitar to hold, or a surfboard to + * Attaches a model to your avatar. For example, you can give your avatar a hat to wear, a guitar to hold, or a surfboard to * stand on. *

Note: Attached models are models only; they are not entities and can not be manipulated using the {@link Entities} API. * Nor can you use this function to attach an entity (such as a sphere or a box) to your avatar.

@@ -1136,12 +1196,14 @@ public: * @param {Quat} [rotation=Quat.IDENTITY] - The rotation to apply to the model relative to the joint orientation. * @param {number} [scale=1.0] - The scale to apply to the model. * @param {boolean} [isSoft=false] - If the model has a skeleton, set this to true so that the bones of the - * attached model's skeleton are be rotated to fit the avatar's current pose. isSoft is used, for example, + * attached model's skeleton are rotated to fit the avatar's current pose. isSoft is used, for example, * to have clothing that moves with the avatar.
* If true, the translation, rotation, and scale parameters are * ignored. - * @param {boolean} [allowDuplicates=false] - * @param {boolean} [useSaved=true] + * @param {boolean} [allowDuplicates=false] - If true then more than one copy of any particular model may be + * attached to the same joint; if false then the same model cannot be attached to the same joint. + * @param {boolean} [useSaved=true] - Not used. + * @deprecated Use avatar entities instead. * @example Attach a cowboy hat to your avatar's head. * var attachment = { * modelURL: "https://s3.amazonaws.com/hifi-public/tony/cowboy-hat.fbx", @@ -1167,20 +1229,22 @@ public: bool allowDuplicates = false, bool useSaved = true); /**jsdoc - * Detach the most recently attached instance of a particular model from either a specific joint or any joint. + * Detaches the most recently attached instance of a particular model from either a specific joint or any joint. * @function Avatar.detachOne * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the most * recently attached model is removed from which ever joint it was attached to. + * @deprecated Use avatar entities instead. */ Q_INVOKABLE virtual void detachOne(const QString& modelURL, const QString& jointName = QString()); /**jsdoc - * Detach all instances of a particular model from either a specific joint or all joints. + * Detaches all instances of a particular model from either a specific joint or all joints. * @function Avatar.detachAll * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the model is * detached from all joints. + * @deprecated Use avatar entities instead. */ Q_INVOKABLE virtual void detachAll(const QString& modelURL, const QString& jointName = QString()); @@ -1220,45 +1284,53 @@ public: AvatarEntityIDs getAndClearRecentlyRemovedIDs(); /**jsdoc + * Gets the transform from the user's real world to the avatar's size, orientation, and position in the virtual world. * @function Avatar.getSensorToWorldMatrix - * @returns {Mat4} + * @returns {Mat4} The scale, rotation, and translation transform from the user's real world to the avatar's size, + * orientation, and position in the virtual world. */ // thread safe Q_INVOKABLE glm::mat4 getSensorToWorldMatrix() const; /**jsdoc + * Gets the scale that transforms dimensions in the user's real world to the avatar's size in the virtual world. * @function Avatar.getSensorToWorldScale - * @returns {number} + * @returns {number} The scale that transforms dimensions in the user's real world to the avatar's size in the virtual + * world. */ // thread safe Q_INVOKABLE float getSensorToWorldScale() const; /**jsdoc + * Gets the rotation and translation of the left hand controller relative to the avatar. * @function Avatar.getControllerLeftHandMatrix - * @returns {Mat4} + * @returns {Mat4} The rotation and translation of the left hand controller relative to the avatar. */ // thread safe Q_INVOKABLE glm::mat4 getControllerLeftHandMatrix() const; /**jsdoc + * Gets the rotation and translation of the right hand controller relative to the avatar. * @function Avatar.getControllerRightHandMatrix - * @returns {Mat4} + * @returns {Mat4} The rotation and translation of the right hand controller relative to the avatar. */ // thread safe Q_INVOKABLE glm::mat4 getControllerRightHandMatrix() const; /**jsdoc + * Gets the amount of avatar mixer data being generated by the avatar. * @function Avatar.getDataRate - * @param {string} [rateName=""] - * @returns {number} + * @param {AvatarDataRate} [rateName=""] - The type of avatar mixer data to get the data rate of. + * @returns {number} The data rate in kbps. */ Q_INVOKABLE float getDataRate(const QString& rateName = QString("")) const; /**jsdoc + * Gets the update rate of avatar mixer data being generated by the avatar. * @function Avatar.getUpdateRate - * @param {string} [rateName=""] - * @returns {number} + * @param {AvatarUpdateRate} [rateName=""] - The type of avatar mixer data to get the update rate of. + * @returns {number} The update rate in Hz. */ Q_INVOKABLE float getUpdateRate(const QString& rateName = QString("")) const; @@ -1304,31 +1376,36 @@ public: signals: /**jsdoc + * Triggered when the avatar's displayName property value changes. * @function Avatar.displayNameChanged * @returns {Signal} */ void displayNameChanged(); /**jsdoc + * Triggered when the avattr's sessionDisplayName property value changes. * @function Avatar.sessionDisplayNameChanged * @returns {Signal} */ void sessionDisplayNameChanged(); /**jsdoc + * Triggered when the avatar's skeletonModelURL property value changes. * @function Avatar.skeletonModelURLChanged * @returns {Signal} */ void skeletonModelURLChanged(); /**jsdoc + * Triggered when the avatar's lookAtSnappingEnabled property value changes. * @function Avatar.lookAtSnappingChanged - * @param {boolean} enabled + * @param {boolean} enabled - true if look-at snapping is enabled, false if not. * @returns {Signal} */ void lookAtSnappingChanged(bool enabled); /**jsdoc + * Triggered when the avatar's sessionUUID property value changes. * @function Avatar.sessionUUIDChanged * @returns {Signal} */ @@ -1338,18 +1415,23 @@ public slots: /**jsdoc * @function Avatar.sendAvatarDataPacket - * @param {boolean} [sendAll=false] + * @param {boolean} [sendAll=false] - Send all. + * @returns {number} + * @deprecated This function is deprecated and will be removed. */ virtual int sendAvatarDataPacket(bool sendAll = false); /**jsdoc * @function Avatar.sendIdentityPacket + * @returns {number} + * @deprecated This function is deprecated and will be removed. */ int sendIdentityPacket(); /**jsdoc * @function Avatar.setSessionUUID - * @param {Uuid} sessionUUID + * @param {Uuid} sessionUUID - Session UUID. + * @deprecated This function is deprecated and will be removed. */ virtual void setSessionUUID(const QUuid& sessionUUID) { if (sessionUUID != getID()) { @@ -1362,44 +1444,61 @@ public slots: } } + /**jsdoc + * Gets the rotation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

* @function Avatar.getAbsoluteJointRotationInObjectFrame - * @param {number} index - * @returns {Quat} + * @param {number} index - The index of the joint. Not used. + * @returns {Quat} Quat.IDENTITY. */ virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; /**jsdoc + * Gets the translation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

* @function Avatar.getAbsoluteJointTranslationInObjectFrame - * @param {number} index - * @returns {Vec3} + * @param {number} index - The index of the joint. Not used. + * @returns {Vec3} Vec3.ZERO. */ virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; /**jsdoc + * Sets the rotation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

* @function Avatar.setAbsoluteJointRotationInObjectFrame - * @param {number} index - * @param {Quat} rotation - * @returns {boolean} + * @param {number} index - The index of the joint. Not used. + * @param {Quat} rotation - The rotation of the joint relative to the avatar. Not used. + * @returns {boolean} false. */ virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } /**jsdoc + * Sets the translation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

* @function Avatar.setAbsoluteJointTranslationInObjectFrame - * @param {number} index - * @param {Vec3} translation - * @returns {boolean} + * @param {number} index - The index of the joint. Not used. + * @param {Vec3} translation - The translation of the joint relative to the avatar. Not used. + * @returns {boolean} false. */ virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } /**jsdoc + * Gets the target scale of the avatar without any restrictions on permissible values imposed by the domain. In contrast, the + * scale property's value may be limited by the domain's settings. * @function Avatar.getTargetScale - * @returns {number} + * @returns {number} The target scale of the avatar. + * @example Compare the target and current avatar scales. + * print("Current avatar scale: " + MyAvatar.scale); + * print("Target avatar scale: " + MyAvatar.getTargetScale()); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ float getTargetScale() const { return _targetScale; } // why is this a slot? /**jsdoc * @function Avatar.resetLastSent + * @deprecated This function is deprecated and will be removed. */ void resetLastSent() { _lastToByteArray = 0; } diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index ec1126c92f..614858a77d 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -1147,19 +1147,21 @@ AnimationDetails::AnimationDetails(QString role, QUrl url, float fps, float prio } /**jsdoc + * The details of an animation that is playing. * @typedef {object} Avatar.AnimationDetails - * @property {string} role - * @property {string} url - * @property {number} fps - * @property {number} priority - * @property {boolean} loop - * @property {boolean} hold - * @property {boolean} startAutomatically - * @property {number} firstFrame - * @property {number} lastFrame - * @property {boolean} running - * @property {number} currentFrame - * @property {boolean} allowTranslation + * @property {string} role - Not used. + * @property {string} url - The URL to the animation file. Animation files need to be in .FBX format but only need to contain +* the avatar skeleton and animation data. + * @property {number} fps - The frames per second(FPS) rate for the animation playback. 30 FPS is normal speed. + * @property {number} priority - Not used. + * @property {boolean} loop - true if the animation should loop, false if it shouldn't. + * @property {boolean} hold - Not used. + * @property {number} firstFrame - The frame the animation should start at. + * @property {number} lastFrame - The frame the animation should stop at. + * @property {boolean} running - Not used. + * @property {number} currentFrame - The current frame being played. + * @property {boolean} startAutomatically - Not used. + * @property {boolean} allowTranslation - Not used. */ QScriptValue animationDetailsToScriptValue(QScriptEngine* engine, const AnimationDetails& details) { QScriptValue obj = engine->newObject(); From aa53ab7492b15f183754fc11f7886539364ae29a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 27 Feb 2019 09:54:11 +1300 Subject: [PATCH 007/135] Distinguish between Mat4 type and Mat4 API in JSDoc --- libraries/script-engine/src/Mat4.h | 33 +++++++++++----------- libraries/shared/src/RegisteredMetaTypes.h | 21 ++++++++++++++ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 7ad77b9b24..0cdc70e79c 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -23,6 +23,7 @@ /**jsdoc * @namespace Mat4 + * @variation 0 * * @hifi-interface * @hifi-client-entity @@ -38,7 +39,7 @@ class Mat4 : public QObject, protected QScriptable { public slots: /**jsdoc - * @function Mat4.multiply + * @function Mat4(0).multiply * @param {Mat4} m1 * @param {Mat4} m2 * @returns {Mat4} @@ -47,7 +48,7 @@ public slots: /**jsdoc - * @function Mat4.createFromRotAndTrans + * @function Mat4(0).createFromRotAndTrans * @param {Quat} rot * @param {Vec3} trans * @returns {Mat4} @@ -55,7 +56,7 @@ public slots: glm::mat4 createFromRotAndTrans(const glm::quat& rot, const glm::vec3& trans) const; /**jsdoc - * @function Mat4.createFromScaleRotAndTrans + * @function Mat4(0).createFromScaleRotAndTrans * @param {Vec3} scale * @param {Quat} rot * @param {Vec3} trans @@ -64,7 +65,7 @@ public slots: glm::mat4 createFromScaleRotAndTrans(const glm::vec3& scale, const glm::quat& rot, const glm::vec3& trans) const; /**jsdoc - * @function Mat4.createFromColumns + * @function Mat4(0).createFromColumns * @param {Vec4} col0 * @param {Vec4} col1 * @param {Vec4} col2 @@ -74,7 +75,7 @@ public slots: glm::mat4 createFromColumns(const glm::vec4& col0, const glm::vec4& col1, const glm::vec4& col2, const glm::vec4& col3) const; /**jsdoc - * @function Mat4.createFromArray + * @function Mat4(0).createFromArray * @param {number[]} numbers * @returns {Mat4} */ @@ -82,21 +83,21 @@ public slots: /**jsdoc - * @function Mat4.extractTranslation + * @function Mat4(0).extractTranslation * @param {Mat4} m * @returns {Vec3} */ glm::vec3 extractTranslation(const glm::mat4& m) const; /**jsdoc - * @function Mat4.extractRotation + * @function Mat4(0).extractRotation * @param {Mat4} m * @returns {Vec3} */ glm::quat extractRotation(const glm::mat4& m) const; /**jsdoc - * @function Mat4.extractScale + * @function Mat4(0).extractScale * @param {Mat4} m * @returns {Vec3} */ @@ -104,7 +105,7 @@ public slots: /**jsdoc - * @function Mat4.transformPoint + * @function Mat4(0).transformPoint * @param {Mat4} m * @param {Vec3} point * @returns {Vec3} @@ -112,7 +113,7 @@ public slots: glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& point) const; /**jsdoc - * @function Mat4.transformVector + * @function Mat4(0).transformVector * @param {Mat4} m * @param {Vec3} vector * @returns {Vec3} @@ -121,7 +122,7 @@ public slots: /**jsdoc - * @function Mat4.inverse + * @function Mat4(0).inverse * @param {Mat4} m * @returns {Mat4} */ @@ -129,7 +130,7 @@ public slots: /**jsdoc - * @function Mat4.getFront + * @function Mat4(0).getFront * @param {Mat4} m * @returns {Vec3} */ @@ -137,28 +138,28 @@ public slots: glm::vec3 getFront(const glm::mat4& m) const { return getForward(m); } /**jsdoc - * @function Mat4.getForward + * @function Mat4(0).getForward * @param {Mat4} m * @returns {Vec3} */ glm::vec3 getForward(const glm::mat4& m) const; /**jsdoc - * @function Mat4.getRight + * @function Mat4(0).getRight * @param {Mat4} m * @returns {Vec3} */ glm::vec3 getRight(const glm::mat4& m) const; /**jsdoc - * @function Mat4.getUp + * @function Mat4(0).getUp * @param {Mat4} m * @returns {Vec3} */ glm::vec3 getUp(const glm::mat4& m) const; /**jsdoc - * @function Mat4.print + * @function Mat4(0).print * @param {string} label * @param {Mat4} m * @param {boolean} [transpose=false] diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 1edb303455..ea2c5b8354 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -43,6 +43,27 @@ Q_DECLARE_METATYPE(std::function); void registerMetaTypes(QScriptEngine* engine); // Mat4 +/**jsdoc + * A 4 x 4 matrix, typically containing a scale, rotation, and translation transform. See also the {@link Mat4(0)|Mat4} object. + * + * @typedef {object} Mat4 + * @property {number} r0c0 - Row 0, column 0 value. + * @property {number} r1c0 - Row 1, column 0 value. + * @property {number} r2c0 - Row 2, column 0 value. + * @property {number} r3c0 - Row 3, column 0 value. + * @property {number} r0c1 - Row 0, column 1 value. + * @property {number} r1c1 - Row 1, column 1 value. + * @property {number} r2c1 - Row 2, column 1 value. + * @property {number} r3c1 - Row 3, column 1 value. + * @property {number} r0c2 - Row 0, column 2 value. + * @property {number} r1c2 - Row 1, column 2 value. + * @property {number} r2c2 - Row 2, column 2 value. + * @property {number} r3c2 - Row 3, column 2 value. + * @property {number} r0c3 - Row 0, column 3 value. + * @property {number} r1c3 - Row 1, column 3 value. + * @property {number} r2c3 - Row 2, column 3 value. + * @property {number} r3c3 - Row 3, column 3 value. + */ QScriptValue mat4toScriptValue(QScriptEngine* engine, const glm::mat4& mat4); void mat4FromScriptValue(const QScriptValue& object, glm::mat4& mat4); From d73ff2e8558b27b861d1602c63b05cfbffa89587 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 28 Feb 2019 10:40:10 +1300 Subject: [PATCH 008/135] Revise current MyAvatar API JSDoc --- interface/src/avatar/MyAvatar.cpp | 14 +- interface/src/avatar/MyAvatar.h | 425 +++++++++++------- .../src/avatars-renderer/Avatar.h | 99 ++-- 3 files changed, 326 insertions(+), 212 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index afb7a218f6..30e8733a42 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2381,7 +2381,19 @@ void MyAvatar::clearWornAvatarEntities() { } } - +/**jsdoc + * Information about an avatar entity. + * + * + * + * + * + * + * + * + *
PropertyTypeDescription
idUuidEntity ID.
properties{@link Entities.EntityProperties}Entity properties.
+ * @typedef {object} MyAvatar.AvatarEntityData + */ QVariantList MyAvatar::getAvatarEntitiesVariant() { // NOTE: this method is NOT efficient QVariantList avatarEntitiesData; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d689d2f215..a0f1531e64 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -60,7 +60,9 @@ class MyAvatar : public Avatar { /**jsdoc * Your avatar is your in-world representation of you. The MyAvatar API is used to manipulate the avatar. * For example, you can customize the avatar's appearance, run custom avatar animations, - * change the avatar's position within the domain, or manage the avatar's collisions with other objects. + * change the avatar's position within the domain, or manage the avatar's collisions with the environment and other avatars. + * + *

For assignment client scripts, see {@link Avatar}.

* * @namespace MyAvatar * @@ -69,17 +71,20 @@ class MyAvatar : public Avatar { * @hifi-avatar * * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. - * @property {Vec3} position - * @property {number} scale - Returns the clamped scale of the avatar. - * @property {number} density Read-only. - * @property {Vec3} handPosition + * @property {Vec3} position - The position of the avatar. + * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 + * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * the application of physics. Read-only. + * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar + * but is otherwise not used or changed by Interface. * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is * sometimes called "bank". - * @property {Quat} orientation + * @property {Quat} orientation - The orientation of the avatar. * @property {Quat} headOrientation - The orientation of the avatar's head. * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is * sometimes called "elevation". @@ -87,22 +92,31 @@ class MyAvatar : public Avatar { * head. Yaw is sometimes called "heading". * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is * sometimes called "bank". - * @property {Vec3} velocity - * @property {Vec3} angularVelocity - * @property {number} audioLoudness - * @property {number} audioAverageLoudness - * @property {string} displayName - * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer - * rather than by Interface clients. The result is unique among all avatars present at the time. - * @property {boolean} lookAtSnappingEnabled - * @property {string} skeletonModelURL - * @property {AttachmentData[]} attachmentData + * @property {Vec3} velocity - The current velocity of the avatar. + * @property {Vec3} angularVelocity - The current angular velocity of the avatar. + * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the + * domain. + * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting + * into the domain. + * @property {string} displayName - The avatar's display name. + * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the + * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the + * time. + * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's + * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. + * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. - * @property {Uuid} sessionUUID Read-only. - * @property {Mat4} sensorToWorldMatrix Read-only. - * @property {Mat4} controllerLeftHandMatrix Read-only. - * @property {Mat4} controllerRightHandMatrix Read-only. - * @property {number} sensorToWorldScale Read-only. + * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. + * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the + * avatar's size, orientation, and position in the virtual world. Read-only. + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * avatar. Read-only. + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * avatar. Read-only. + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * size in the virtual world. Read-only. * * @comment IMPORTANT: This group of properties is copied from Avatar.h; they should NOT be edited here. * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the @@ -124,9 +138,9 @@ class MyAvatar : public Avatar { * by the audio mixer, so all audio effectively plays back at a 24khz. 48kHz RAW files are also supported. * @property {number} audioListenerMode=0 - Specifies the listening position when hearing spatialized audio. Must be one * of the following property values:
- * audioListenerModeHead
- * audioListenerModeCamera
- * audioListenerModeCustom + * Myavatar.audioListenerModeHead
+ * Myavatar.audioListenerModeCamera
+ * Myavatar.audioListenerModeCustom * @property {number} audioListenerModeHead=0 - The audio listening position is at the avatar's head. Read-only. * @property {number} audioListenerModeCamera=1 - The audio listening position is at the camera. Read-only. * @property {number} audioListenerModeCustom=2 - The audio listening position is at a the position specified by set by the @@ -135,10 +149,12 @@ class MyAvatar : public Avatar { * property value is audioListenerModeCustom. * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the * audioListenerMode property value is audioListenerModeCustom. - * @property {boolean} hasScriptedBlendshapes=false - Blendshapes will be transmitted over the network if set to true. - * @property {boolean} hasProceduralBlinkFaceMovement=true - procedural blinking will be turned on if set to true. - * @property {boolean} hasProceduralEyeFaceMovement=true - procedural eye movement will be turned on if set to true. - * @property {boolean} hasAudioEnabledFaceMovement=true - If set to true, voice audio will move the mouth Blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. + * @property {boolean} hasScriptedBlendshapes=false - Blendshapes will be transmitted over the network if set to true.
+ * Note: Currently doesn't work. Use {@link MyAvatar.setForceFaceTrackerConnected} instead. + * @property {boolean} hasProceduralBlinkFaceMovement=true - If true then procedural blinking is turned on. + * @property {boolean} hasProceduralEyeFaceMovement=true - If true then procedural eye movement is turned on. + * @property {boolean} hasAudioEnabledFaceMovement=true - If true then voice audio will move the mouth + * blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. * @property {number} rotationRecenterFilterLength * @property {number} rotationThreshold * @property {boolean} enableStepResetRotation @@ -169,16 +185,18 @@ class MyAvatar : public Avatar { * @property {boolean} hmdLeanRecenterEnabled=true - If true then the avatar is re-centered to be under the * head's position. In room-scale VR, this behavior is what causes your avatar to follow your HMD as you walk around * the room. Setting the value false is useful if you want to pin the avatar to a fixed position. - * @property {boolean} collisionsEnabled - Set to true to enable collisions for the avatar, false - * to disable collisions. May return true even though the value was set false because the - * zone may disallow collisionless avatars. - * @property {boolean} otherAvatarsCollisionsEnabled - * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled. + * @property {boolean} collisionsEnabled - Set to true to enable the avatar to collide with the environment, + * false to disable collisions with the environment. May return true even though the value + * was set false because the zone may disallow collisionless avatars. + * @property {boolean} otherAvatarsCollisionsEnabled - Set to true to enable the avatar to collide with other + * avatars, false to disable collisions with other avatars. + * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled.
* Deprecated: Use collisionsEnabled instead. * @property {boolean} useAdvancedMovementControls - Returns and sets the value of the Interface setting, Settings > - * Walking and teleporting. Note: Setting the value has no effect unless Interface is restarted. - * @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Show room boundaries - * while teleporting. Note: Setting the value has no effect unless Interface is restarted. + * Controls > Walking. Note: Setting the value has no effect unless Interface is restarted. + * @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Controls > Show room + * boundaries while teleporting.
+ * Note: Setting the value has no effect unless Interface is restarted. * * @property {number} yawSpeed=75 * @property {number} pitchSpeed=50 @@ -187,8 +205,8 @@ class MyAvatar : public Avatar { * while flying. * @property {number} hmdRollControlDeadZone=8 - The amount of HMD roll, in degrees, required before your avatar turns if * hmdRollControlEnabled is enabled. - * @property {number} hmdRollControlRate If hmdRollControlEnabled is true, this value determines the maximum turn rate of - * your avatar when rolling your HMD in degrees per second. + * @property {number} hmdRollControlRate If MyAvatar.hmdRollControlEnabled is true, this value determines the + * maximum turn rate of your avatar when rolling your HMD in degrees per second. * * @property {number} userHeight=1.75 - The height of the user in sensor space. * @property {number} userEyeHeight=1.65 - The estimated height of the user's eyes in sensor space. Read-only. @@ -237,8 +255,8 @@ class MyAvatar : public Avatar { * @borrows Avatar.attach as attach * @borrows Avatar.detachOne as detachOne * @borrows Avatar.detachAll as detachAll - * @borrows Avatar.getAvatarEntityData as getAvatarEntityData - * @borrows Avatar.setAvatarEntityData as setAvatarEntityData + * @comment Avatar.getAvatarEntityData as getAvatarEntityData - Don't borrow because implementation is different. + * @comment Avatar.setAvatarEntityData as setAvatarEntityData - Don't borrow because implementation is different. * @borrows Avatar.getSensorToWorldMatrix as getSensorToWorldMatrix * @borrows Avatar.getSensorToWorldScale as getSensorToWorldScale * @borrows Avatar.getControllerLeftHandMatrix as getControllerLeftHandMatrix @@ -253,10 +271,10 @@ class MyAvatar : public Avatar { * @borrows Avatar.sendAvatarDataPacket as sendAvatarDataPacket * @borrows Avatar.sendIdentityPacket as sendIdentityPacket * @borrows Avatar.setSessionUUID as setSessionUUID - * @borrows Avatar.getAbsoluteJointRotationInObjectFrame as getAbsoluteJointRotationInObjectFrame - * @borrows Avatar.getAbsoluteJointTranslationInObjectFrame as getAbsoluteJointTranslationInObjectFrame - * @borrows Avatar.setAbsoluteJointRotationInObjectFrame as setAbsoluteJointRotationInObjectFrame - * @borrows Avatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame + * @comment Avatar.getAbsoluteJointRotationInObjectFrame as getAbsoluteJointRotationInObjectFrame - Don't borrow because implementation is different. + * @comment Avatar.getAbsoluteJointTranslationInObjectFrame as getAbsoluteJointTranslationInObjectFrame - Don't borrow because implementation is different. + * @comment Avatar.setAbsoluteJointRotationInObjectFrame as setAbsoluteJointRotationInObjectFrame - Don't borrow because implementation is different. + * @comment Avatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame - Don't borrow because implementation is different. * @borrows Avatar.getTargetScale as getTargetScale * @borrows Avatar.resetLastSent as resetLastSent */ @@ -393,8 +411,9 @@ public: /**jsdoc - * The internal inverse-kinematics system maintains a record of which joints are "locked". Sometimes it is useful to forget this history, to prevent - * contorted joints. + * Clears inverse kinematics joint limit history. + *

The internal inverse-kinematics system maintains a record of which joints are "locked". Sometimes it is useful to + * forget this history, to prevent contorted joints.

* @function MyAvatar.clearIKJointLimitHistory */ Q_INVOKABLE void clearIKJointLimitHistory(); // thread-safe @@ -441,7 +460,7 @@ public: void setRealWorldFieldOfView(float realWorldFov) { _realWorldFieldOfView.set(realWorldFov); } /**jsdoc - * Get the position in world coordinates of the point directly between your avatar's eyes assuming your avatar was in its + * Gets the position in world coordinates of the point directly between your avatar's eyes assuming your avatar was in its * default pose. This is a reference position; it does not change as your avatar's head moves relative to the avatar * position. * @function MyAvatar.getDefaultEyePosition @@ -455,15 +474,16 @@ public: float getRealWorldFieldOfView() { return _realWorldFieldOfView.get(); } /**jsdoc - * The avatar animation system includes a set of default animations along with rules for how those animations are blended - * together with procedural data (such as look at vectors, hand sensors etc.). overrideAnimation() is used to completely - * override all motion from the default animation system (including inverse kinematics for hand and head controllers) and - * play a set of specified animations. To end these animations and restore the default animations, use - * {@link MyAvatar.restoreAnimation}.
+ * Overrides the default avatar animations. + *

The avatar animation system includes a set of default animations along with rules for how those animations are blended + * together with procedural data (such as look at vectors, hand sensors etc.). overrideAnimation() is used to + * completely override all motion from the default animation system (including inverse kinematics for hand and head + * controllers) and play a set of specified animations. To end these animations and restore the default animations, use + * {@link MyAvatar.restoreAnimation}.

*

Note: When using pre-built animation data, it's critical that the joint orientation of the source animation and target * rig are equivalent, since the animation data applies absolute values onto the joints. If the orientations are different, * the avatar will move in unpredictable ways. For more information about avatar joint orientation standards, see - * Avatar Standards.

+ * Avatar Standards.

* @function MyAvatar.overrideAnimation * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the * avatar skeleton and animation data. @@ -482,10 +502,12 @@ public: Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); /**jsdoc - * The avatar animation system includes a set of default animations along with rules for how those animations are blended together with - * procedural data (such as look at vectors, hand sensors etc.). Playing your own custom animations will override the default animations. - * restoreAnimation() is used to restore all motion from the default animation system including inverse kinematics for hand and head - * controllers. If you aren't currently playing an override animation, this function will have no effect. + * Restores the default animations. + *

The avatar animation system includes a set of default animations along with rules for how those animations are blended + * together with procedural data (such as look at vectors, hand sensors etc.). Playing your own custom animations will + * override the default animations. restoreAnimation() is used to restore all motion from the default + * animation system including inverse kinematics for hand and head controllers. If you aren't currently playing an override + * animation, this function has no effect.

* @function MyAvatar.restoreAnimation * @example Play a clapping animation on your avatar for three seconds. * // Clap your hands for 3 seconds then restore animation back to the avatar. @@ -498,10 +520,12 @@ public: Q_INVOKABLE void restoreAnimation(); /**jsdoc - * Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together with procedural data - * (such as look at vectors, hand sensors etc.). Each animation specified in the avatar-animation.json file is known as an animation role. - * Animation roles map to easily understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd." - * getAnimationRoles() is used get the list of animation roles defined in the avatar-animation.json. + * Gets the current animation roles. + *

Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together + * with procedural data (such as look at vectors, hand sensors etc.). Each animation specified in the avatar-animation.json + * file is known as an animation role. Animation roles map to easily understandable actions that the avatar can perform, + * such as "idleStand", "idleTalk", or "walkFwd". getAnimationRoles() + * is used get the list of animation roles defined in the avatar-animation.json.

* @function MyAvatar.getAnimationRoles * @returns {string[]} Array of role strings. * @example Print the list of animation roles defined in the avatar's avatar-animation.json file to the debug log. @@ -514,17 +538,19 @@ public: Q_INVOKABLE QStringList getAnimationRoles(); /**jsdoc - * Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily understandable actions - * that the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd". To get the full list of roles, use getAnimationRoles(). - * For each role, the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are blended - * together with procedural data (such as look at vectors, hand sensors etc.). - * overrideRoleAnimation() is used to change the animation clip (.FBX) associated with a specified animation role. To end - * the animations and restore the default animations, use {@link MyAvatar.restoreRoleAnimation}.
+ * Overrides a specific animation role. + *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily + * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or + * "walkFwd". To get the full list of roles, use {@ link MyAvatar.getAnimationRoles}. + * For each role, the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how + * animations are blended together with procedural data (such as look at vectors, hand sensors etc.). + * overrideRoleAnimation() is used to change the animation clip (.FBX) associated with a specified animation + * role. To end the role animation and restore the default, use {@link MyAvatar.restoreRoleAnimation}.

*

Note: Hand roles only affect the hand. Other 'main' roles, like 'idleStand', 'idleTalk', 'takeoffStand' are full body.

*

Note: When using pre-built animation data, it's critical that the joint orientation of the source animation and target * rig are equivalent, since the animation data applies absolute values onto the joints. If the orientations are different, * the avatar will move in unpredictable ways. For more information about avatar joint orientation standards, see - * Avatar Standards. + * Avatar Standards. * @function MyAvatar.overrideRoleAnimation * @param role {string} The animation role to override * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the avatar skeleton and animation data. @@ -548,13 +574,15 @@ public: Q_INVOKABLE void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); /**jsdoc - * Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily understandable actions that - * the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd". To get the full list of roles, use getAnimationRoles(). For each role, - * the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are blended together with - * procedural data (such as look at vectors, hand sensors etc.). You can change the animation clip (.FBX) associated with a specified animation - * role using overrideRoleAnimation(). - * restoreRoleAnimation() is used to restore a specified animation role's default animation clip. If you have not specified an override animation - * for the specified role, this function will have no effect. + * Restores a default role animation. + *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily + * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or + * "walkFwd". To get the full list of roles, use {#link MyAvatar.getAnimationRoles}. For each role, + * the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are + * blended together with procedural data (such as look-at vectors, hand sensors etc.). You can change the animation clip + * (.FBX) associated with a specified animation role using {@link MyAvatar.overrideRoleAnimation}. + * restoreRoleAnimation() is used to restore a specified animation role's default animation clip. If you have + * not specified an override animation for the specified role, this function has no effect. * @function MyAvatar.restoreRoleAnimation * @param role {string} The animation role clip to restore. */ @@ -599,6 +627,7 @@ public: * @param {string} hand */ Q_INVOKABLE void setDominantHand(const QString& hand); + /**jsdoc * @function MyAvatar.getDominantHand * @returns {string} @@ -610,6 +639,7 @@ public: * @param {string} hand */ Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& hand); + /**jsdoc * @function MyAvatar.getHmdAvatarAlignmentType * @returns {string} @@ -617,45 +647,61 @@ public: Q_INVOKABLE QString getHmdAvatarAlignmentType() const; /**jsdoc - * @function MyAvatar.setCenterOfGravityModelEnabled - * @param {boolean} enabled - */ + * @function MyAvatar.setCenterOfGravityModelEnabled + * @param {boolean} enabled + */ Q_INVOKABLE void setCenterOfGravityModelEnabled(bool value) { _centerOfGravityModelEnabled = value; } + /**jsdoc - * @function MyAvatar.getCenterOfGravityModelEnabled - * @returns {boolean} - */ + * @function MyAvatar.getCenterOfGravityModelEnabled + * @returns {boolean} + */ Q_INVOKABLE bool getCenterOfGravityModelEnabled() const { return _centerOfGravityModelEnabled; } /**jsdoc * @function MyAvatar.setHMDLeanRecenterEnabled * @param {boolean} enabled */ Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } + /**jsdoc * @function MyAvatar.getHMDLeanRecenterEnabled * @returns {boolean} */ Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } + /**jsdoc - * Request to enable hand touch effect globally + * Requests that the hand touch effect is disabled for your avatar. Any resulting change in the status of the hand touch + * effect will be signaled by {@link MyAvatar.shouldDisableHandTouchChanged}. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.requestEnableHandTouch */ Q_INVOKABLE void requestEnableHandTouch(); + /**jsdoc - * Request to disable hand touch effect globally + * Requests that the hand touch effect is enabled for your avatar. Any resulting change in the status of the hand touch + * effect will be signaled by {@link MyAvatar.shouldDisableHandTouchChanged}. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.requestDisableHandTouch */ Q_INVOKABLE void requestDisableHandTouch(); + /**jsdoc - * Disables hand touch effect on a specific entity + * Disables the hand touch effect on a specific entity. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.disableHandTouchForID - * @param {Uuid} entityID - ID of the entity that will disable hand touch effect + * @param {Uuid} entityID - The entity that the hand touch effect will be disabled for. */ Q_INVOKABLE void disableHandTouchForID(const QUuid& entityID); + /**jsdoc - * Enables hand touch effect on a specific entity + * Enables the hand touch effect on a specific entity. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.enableHandTouchForID - * @param {Uuid} entityID - ID of the entity that will enable hand touch effect + * @param {Uuid} entityID - The entity that the hand touch effect will be enabled for. */ Q_INVOKABLE void enableHandTouchForID(const QUuid& entityID); @@ -712,6 +758,7 @@ public: * @param {DriveKeys} key */ Q_INVOKABLE void enableDriveKey(DriveKeys key); + /**jsdoc * @function MyAvatar.isDriveKeyDisabled * @param {DriveKeys} key @@ -741,10 +788,10 @@ public: Q_INVOKABLE void triggerRotationRecenter(); /**jsdoc - *The isRecenteringHorizontally function returns true if MyAvatar - *is translating the root of the Avatar to keep the center of gravity under the head. - *isActive(Horizontal) is returned. - *@function MyAvatar.isRecenteringHorizontally + * Gets whether or not the avatar is configured to keep its center of gravity under its head. + * @function MyAvatar.isRecenteringHorizontally + * @returns {boolean} true if the avatar is keeping its center of gravity under its head position, + * false if not. */ Q_INVOKABLE bool isRecenteringHorizontally() const; @@ -753,7 +800,7 @@ public: const MyHead* getMyHead() const; /**jsdoc - * Get the current position of the avatar's "Head" joint. + * Gets the current position of the avatar's "Head" joint. * @function MyAvatar.getHeadPosition * @returns {Vec3} The current position of the avatar's "Head" joint. * @example Report the current position of your avatar's head. @@ -786,7 +833,7 @@ public: Q_INVOKABLE float getHeadDeltaPitch() const { return getHead()->getDeltaPitch(); } /**jsdoc - * Get the current position of the point directly between the avatar's eyes. + * Gets the current position of the point directly between the avatar's eyes. * @function MyAvatar.getEyePosition * @returns {Vec3} The current position of the point directly between the avatar's eyes. * @example Report your avatar's current eye position. @@ -796,8 +843,9 @@ public: Q_INVOKABLE glm::vec3 getEyePosition() const { return getHead()->getEyePosition(); } /**jsdoc + * Gets the position of the avatar your avatar is currently looking at. * @function MyAvatar.getTargetAvatarPosition - * @returns {Vec3} The position of the avatar you're currently looking at. + * @returns {Vec3} The position of the avatar your avatar is currently looking at. * @example Report the position of the avatar you're currently looking at. * print(JSON.stringify(MyAvatar.getTargetAvatarPosition())); */ @@ -811,7 +859,7 @@ public: /**jsdoc - * Get the position of the avatar's left hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
+ * Gets the position of the avatar's left hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
*

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.)

* @function MyAvatar.getLeftHandPosition @@ -823,7 +871,7 @@ public: Q_INVOKABLE glm::vec3 getLeftHandPosition() const; /**jsdoc - * Get the position of the avatar's right hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
+ * Gets the position of the avatar's right hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
*

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.)

* @function MyAvatar.getRightHandPosition @@ -848,7 +896,7 @@ public: /**jsdoc - * Get the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a + * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a * hand controller (e.g., Oculus Touch or Vive).
*

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.) If you are using the Leap Motion, the return value's valid property will be @@ -861,7 +909,7 @@ public: Q_INVOKABLE controller::Pose getLeftHandPose() const; /**jsdoc - * Get the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a + * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a * hand controller (e.g., Oculus Touch or Vive).
*

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.) If you are using the Leap Motion, the return value's valid property will be @@ -935,7 +983,7 @@ public: Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); /**jsdoc - * Get the complete URL for the current avatar. + * Gets the complete URL for the current avatar. * @function MyAvatar.getFullAvatarURLFromPreferences * @returns {string} The full avatar model name. * @example Report the URL for the current avatar. @@ -944,7 +992,7 @@ public: Q_INVOKABLE QUrl getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } /**jsdoc - * Get the full avatar model name for the current avatar. + * Gets the full avatar model name for the current avatar. * @function MyAvatar.getFullAvatarModelName * @returns {string} The full avatar model name. * @example Report the current full avatar model name. @@ -1015,24 +1063,24 @@ public: bool hasDriveInput() const; /**jsdoc - * Function returns list of avatar entities - * @function MyAvatar.getAvatarEntitiesVariant - * @returns {object[]} - */ + * Gets the list of avatar entities and their properties. + * @function MyAvatar.getAvatarEntitiesVariant + * @returns {MyAvatar.AvatarEntityData[]} + */ Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); + void removeWornAvatarEntity(const EntityItemID& entityID); void clearWornAvatarEntities(); /**jsdoc - * Check whether your avatar is flying or not. + * Checks whether your avatar is flying or not. * @function MyAvatar.isFlying - * @returns {boolean} true if your avatar is flying and not taking off or falling, otherwise - * false. + * @returns {boolean} true if your avatar is flying and not taking off or falling, false if not. */ Q_INVOKABLE bool isFlying(); /**jsdoc - * Check whether your avatar is in the air or not. + * Checks whether your avatar is in the air or not. * @function MyAvatar.isInAir * @returns {boolean} true if your avatar is taking off, flying, or falling, otherwise false * because your avatar is on the ground. @@ -1040,7 +1088,7 @@ public: Q_INVOKABLE bool isInAir(); /**jsdoc - * Set your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends + * Sets your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends * on whether the domain you're in allows you to fly. * @function MyAvatar.setFlyingEnabled * @param {boolean} enabled - Set true if you want to enable flying in your current desktop or HMD display @@ -1049,7 +1097,7 @@ public: Q_INVOKABLE void setFlyingEnabled(bool enabled); /**jsdoc - * Get your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends + * Gets your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends * on whether the domain you're in allows you to fly. * @function MyAvatar.getFlyingEnabled * @returns {boolean} true if your preference is to enable flying in your current desktop or HMD display mode, @@ -1058,7 +1106,7 @@ public: Q_INVOKABLE bool getFlyingEnabled(); /**jsdoc - * Set your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain + * Sets your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.setFlyingDesktopPref * @param {boolean} enabled - Set true if you want to enable flying in desktop display mode, otherwise set @@ -1067,7 +1115,7 @@ public: Q_INVOKABLE void setFlyingDesktopPref(bool enabled); /**jsdoc - * Get your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain + * Gets your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.getFlyingDesktopPref * @returns {boolean} true if your preference is to enable flying in desktop display mode, otherwise @@ -1076,7 +1124,7 @@ public: Q_INVOKABLE bool getFlyingDesktopPref(); /**jsdoc - * Set your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain + * Sets your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.setFlyingHMDPref * @param {boolean} enabled - Set true if you want to enable flying in HMD display mode, otherwise set @@ -1085,7 +1133,7 @@ public: Q_INVOKABLE void setFlyingHMDPref(bool enabled); /**jsdoc - * Get your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain + * Gets your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.getFlyingHMDPref * @returns {boolean} true if your preference is to enable flying in HMD display mode, otherwise @@ -1120,38 +1168,53 @@ public: Q_INVOKABLE bool getCollisionsEnabled(); /**jsdoc - * @function MyAvatar.setOtherAvatarsCollisionsEnabled - * @param {boolean} enabled - */ + * @function MyAvatar.setOtherAvatarsCollisionsEnabled + * @param {boolean} enabled + */ Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled); /**jsdoc - * @function MyAvatar.getOtherAvatarsCollisionsEnabled - * @returns {boolean} - */ + * @function MyAvatar.getOtherAvatarsCollisionsEnabled + * @returns {boolean} + */ Q_INVOKABLE bool getOtherAvatarsCollisionsEnabled(); /**jsdoc - * @function MyAvatar.getCollisionCapsule - * @returns {object} - */ + * @function MyAvatar.getCollisionCapsule + * @returns {object} + */ Q_INVOKABLE QVariantMap getCollisionCapsule() const; /**jsdoc * @function MyAvatar.setCharacterControllerEnabled * @param {boolean} enabled - * @deprecated + * @deprecated Use {@link MyAvatar.setCollisionsEnabled} instead. */ Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); // deprecated /**jsdoc * @function MyAvatar.getCharacterControllerEnabled * @returns {boolean} - * @deprecated + * @deprecated Use {@link MyAvatar.getCollisionsEnabled} instead. */ Q_INVOKABLE bool getCharacterControllerEnabled(); // deprecated + /**jsdoc + * @comment Different behavior to the Avatar version of this method. + * Gets the rotation of a joint relative to the avatar. + * @function MyAvatar.getAbsoluteJointRotationInObjectFrame + * @param {number} index - The index of the joint. + * @returns {Quat} The rotation of the joint relative to the avatar. + */ virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; + + /**jsdoc + * @comment Different behavior to the Avatar version of this method. + * Gets the translation of a joint relative to the avatar. + * @function MyAvatar.getAbsoluteJointTranslationInObjectFrame + * @param {number} index - The index of the joint. + * @returns {Vec3} The translation of the joint relative to the avatar. + */ virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; // all calibration matrices are in absolute sensor space. @@ -1241,34 +1304,56 @@ public: void prepareAvatarEntityDataForReload(); /**jsdoc - * Create a new grab. + * Creates a new grab, that grabs an entity. * @function MyAvatar.grab - * @param {Uuid} targetID - id of grabbed thing - * @param {number} parentJointIndex - avatar joint being used to grab - * @param {Vec3} offset - target's positional offset from joint - * @param {Quat} rotationalOffset - target's rotational offset from joint - * @returns {Uuid} id of the new grab + * @param {Uuid} targetID - The ID of the entity to grab. + * @param {number} parentJointIndex - The avatar joint to use to grab the entity. + * @param {Vec3} offset - The target's local positional relative to the joint. + * @param {Quat} rotationalOffset - The target's local rotation relative to the joint. + * @returns {Uuid} The ID of the new grab. */ Q_INVOKABLE const QUuid grab(const QUuid& targetID, int parentJointIndex, glm::vec3 positionalOffset, glm::quat rotationalOffset); /**jsdoc - * Release (delete) a grab. + * Releases (deletes) a grab, to stop grabbing an entity. * @function MyAvatar.releaseGrab - * @param {Uuid} grabID - id of grabbed thing + * @param {Uuid} grabID - The ID of the grab to release. */ Q_INVOKABLE void releaseGrab(const QUuid& grabID); + /**jsdoc + * Gets the avatar entities as binary data. + * @function MyAvatar.getAvatarEntityData + * @override + * @returns {AvatarEntityMap} + */ AvatarEntityMap getAvatarEntityData() const override; + + /**jsdoc + * Sets the avatar entities from binary data. + * @function MyAvatar.setAvatarEntityData + * @param {AvatarEntityMap} avatarEntityData + */ void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; + + /**jsdoc + * @comment Uses the base class's JSDoc. + */ void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override; + void avatarEntityDataToJson(QJsonObject& root) const override; + + /**jsdoc + * @function MyAvatar.sendAvatarDataPacket + * @param {boolean} sendAll + */ int sendAvatarDataPacket(bool sendAll = false) override; public slots: /**jsdoc - * Increase the avatar's scale by five percent, up to a minimum scale of 1000. + * Increases the avatar's scale by five percent, up to a minimum scale of 1000. * @function MyAvatar.increaseSize * @example Reset your avatar's size to default then grow it 5 times. * MyAvatar.resetSize(); @@ -1281,7 +1366,7 @@ public slots: void increaseSize(); /**jsdoc - * Decrease the avatar's scale by five percent, down to a minimum scale of 0.25. + * Decreases the avatar's scale by five percent, down to a minimum scale of 0.25. * @function MyAvatar.decreaseSize * @example Reset your avatar's size to default then shrink it 5 times. * MyAvatar.resetSize(); @@ -1294,7 +1379,7 @@ public slots: void decreaseSize(); /**jsdoc - * Reset the avatar's scale back to the default scale of 1.0. + * Resets the avatar's scale back to the default scale of 1.0. * @function MyAvatar.resetSize */ void resetSize(); @@ -1317,7 +1402,7 @@ public slots: float getGravity(); /**jsdoc - * Move the avatar to a new position and/or orientation in the domain, while taking into account Avatar leg-length. + * Moves the avatar to a new position and/or orientation in the domain, while taking into account Avatar leg-length. * @function MyAvatar.goToFeetLocation * @param {Vec3} position - The new position for the avatar, in world coordinates. * @param {boolean} [hasOrientation=false] - Set to true to set the orientation of the avatar. @@ -1330,7 +1415,7 @@ public slots: bool shouldFaceLocation); /**jsdoc - * Move the avatar to a new position and/or orientation in the domain. + * Moves the avatar to a new position and/or orientation in the domain. * @function MyAvatar.goToLocation * @param {Vec3} position - The new position for the avatar, in world coordinates. * @param {boolean} [hasOrientation=false] - Set to true to set the orientation of the avatar. @@ -1395,61 +1480,70 @@ public slots: /**jsdoc - * ####### Why Q_INVOKABLE? * @function MyAvatar.updateMotionBehaviorFromMenu */ Q_INVOKABLE void updateMotionBehaviorFromMenu(); /**jsdoc - * @function MyAvatar.setToggleHips - * @param {boolean} enabled - */ + * @function MyAvatar.setToggleHips + * @param {boolean} enabled + */ void setToggleHips(bool followHead); + /**jsdoc - * @function MyAvatar.setEnableDebugDrawBaseOfSupport - * @param {boolean} enabled - */ + * @function MyAvatar.setEnableDebugDrawBaseOfSupport + * @param {boolean} enabled + */ void setEnableDebugDrawBaseOfSupport(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawDefaultPose * @param {boolean} enabled */ void setEnableDebugDrawDefaultPose(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawAnimPose * @param {boolean} enabled */ void setEnableDebugDrawAnimPose(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawPosition * @param {boolean} enabled */ void setEnableDebugDrawPosition(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawHandControllers * @param {boolean} enabled */ void setEnableDebugDrawHandControllers(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawSensorToWorldMatrix * @param {boolean} enabled */ void setEnableDebugDrawSensorToWorldMatrix(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawIKTargets * @param {boolean} enabled */ void setEnableDebugDrawIKTargets(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawIKConstraints * @param {boolean} enabled */ void setEnableDebugDrawIKConstraints(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawIKChains * @param {boolean} enabled */ void setEnableDebugDrawIKChains(bool isEnabled); + /**jsdoc * @function MyAvatar.setEnableDebugDrawDetailedCollision * @param {boolean} enabled @@ -1457,23 +1551,20 @@ public slots: void setEnableDebugDrawDetailedCollision(bool isEnabled); /**jsdoc - * Get whether or not your avatar mesh is visible. + * Gets whether or not your avatar mesh is visible. * @function MyAvatar.getEnableMeshVisible * @returns {boolean} true if your avatar's mesh is visible, otherwise false. */ bool getEnableMeshVisible() const override; /**jsdoc - * ####### TODO; Should this really be exposed in the API? * @function MyAvatar.storeAvatarEntityDataPayload + * @deprecated This function is deprecated and will be removed. */ void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; /**jsdoc - * ####### Does override change functionality? If so, document here and don't borrow; if not, borrow and don't document here. - * @function MyAvatar.clearAvatarEntity - * @param {Uuid} entityID - * @param {boolean} [requiresRemovalFromTree] + * @comment Uses the base class's JSDoc. */ void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true) override; @@ -1483,7 +1574,7 @@ public slots: void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const; /**jsdoc - * Set whether or not your avatar mesh is visible. + * Sets whether or not your avatar mesh is visible. * @function MyAvatar.setEnableMeshVisible * @param {boolean} visible - true to set your avatar mesh visible; false to set it invisible. * @example Make your avatar invisible for 10s. @@ -1577,25 +1668,27 @@ signals: void collisionWithEntity(const Collision& collision); /**jsdoc - * Triggered when collisions with avatar enabled or disabled + * Triggered when collisions with the environment are enabled or disabled. * @function MyAvatar.collisionsEnabledChanged - * @param {boolean} enabled + * @param {boolean} enabled - true if collisions with the environment are enabled, false if + * they're not. * @returns {Signal} */ void collisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when collisions with other avatars enabled or disabled + * Triggered when collisions with other avatars are enabled or disabled. * @function MyAvatar.otherAvatarsCollisionsEnabledChanged - * @param {boolean} enabled + * @param {boolean} enabled - true if collisions with other avatars are enabled, false if they're + * not. * @returns {Signal} */ void otherAvatarsCollisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when avatar's animation url changes + * Triggered when the avatar's animation changes. * @function MyAvatar.animGraphUrlChanged - * @param {url} url + * @param {url} url - The URL of the new animation. * @returns {Signal} */ void animGraphUrlChanged(const QUrl& url); @@ -1672,18 +1765,24 @@ signals: void scaleChanged(); /**jsdoc - * Triggered when hand touch is globally enabled or disabled + * Triggered when the hand touch effect is enabled or disabled for the avatar. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.shouldDisableHandTouchChanged - * @param {boolean} shouldDisable + * @param {boolean} disabled - true if the hand touch effect is disabled for the avatar, + * false if it isn't disabled. * @returns {Signal} */ void shouldDisableHandTouchChanged(bool shouldDisable); /**jsdoc - * Triggered when hand touch is enabled or disabled for an specific entity + * Triggered when the hand touch is enabled or disabled on a specific entity. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.disableHandTouchForIDChanged - * @param {Uuid} entityID - ID of the entity that will enable hand touch effect - * @param {boolean} disable + * @param {Uuid} entityID - The entity that the hand touch effect has been enabled or disabled for. + * @param {boolean} disabled - true if the hand touch effect is disabled for the entity, + * false if it isn't disabled. * @returns {Signal} */ void disableHandTouchForIDChanged(const QUuid& entityID, bool disable); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 98aa255641..11940ad76a 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -215,17 +215,17 @@ public: Q_INVOKABLE virtual glm::vec3 getDefaultJointTranslation(int index) const; /**jsdoc - * Provides read only access to the default joint rotations in avatar coordinates. + * Provides read-only access to the default joint rotations in avatar coordinates. * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame - * @param index {number} -The joint index. + * @param index {number} - The joint index. * @returns {Quat} The rotation of this joint in avatar coordinates. */ Q_INVOKABLE virtual glm::quat getAbsoluteDefaultJointRotationInObjectFrame(int index) const; /**jsdoc - * Provides read only access to the default joint translations in avatar coordinates. + * Provides read-only access to the default joint translations in avatar coordinates. * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame @@ -261,52 +261,51 @@ public: // world-space to avatar-space rigconversion functions /**jsdoc - * @function MyAvatar.worldToJointPoint - * @param {Vec3} position - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * @function MyAvatar.worldToJointPoint + * @param {Vec3} position + * @param {number} [jointIndex=-1] + * @returns {Vec3} + */ Q_INVOKABLE glm::vec3 worldToJointPoint(const glm::vec3& position, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.worldToJointDirection - * @param {Vec3} direction - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * @function MyAvatar.worldToJointDirection + * @param {Vec3} direction + * @param {number} [jointIndex=-1] + * @returns {Vec3} + */ Q_INVOKABLE glm::vec3 worldToJointDirection(const glm::vec3& direction, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.worldToJointRotation - * @param {Quat} rotation - * @param {number} [jointIndex=-1] - * @returns {Quat} + * @function MyAvatar.worldToJointRotation + * @param {Quat} rotation + * @param {number} [jointIndex=-1] + * @returns {Quat} */ Q_INVOKABLE glm::quat worldToJointRotation(const glm::quat& rotation, const int jointIndex = -1) const; - /**jsdoc - * @function MyAvatar.jointToWorldPoint - * @param {vec3} position - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * @function MyAvatar.jointToWorldPoint + * @param {vec3} position + * @param {number} [jointIndex=-1] + * @returns {Vec3} + */ Q_INVOKABLE glm::vec3 jointToWorldPoint(const glm::vec3& position, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.jointToWorldDirection - * @param {Vec3} direction - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * @function MyAvatar.jointToWorldDirection + * @param {Vec3} direction + * @param {number} [jointIndex=-1] + * @returns {Vec3} + */ Q_INVOKABLE glm::vec3 jointToWorldDirection(const glm::vec3& direction, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.jointToWorldRotation - * @param {Quat} rotation - * @param {number} [jointIndex=-1] - * @returns {Quat} - */ + * @function MyAvatar.jointToWorldRotation + * @param {Quat} rotation + * @param {number} [jointIndex=-1] + * @returns {Quat} + */ Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; @@ -321,7 +320,7 @@ public: float radius1, float radius2, const glm::vec4& color); /**jsdoc - * Set the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, + * Sets the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, * with an offset of { x: 0, y: 0.1, z: 0 }, your avatar will appear to be raised off the ground slightly. * @function MyAvatar.setSkeletonOffset * @param {Vec3} offset - The skeleton offset to set. @@ -337,7 +336,7 @@ public: Q_INVOKABLE void setSkeletonOffset(const glm::vec3& offset); /**jsdoc - * Get the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, + * Gets the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, * with an offset of { x: 0, y: 0.1, z: 0 }, your avatar will appear to be raised off the ground slightly. * @function MyAvatar.getSkeletonOffset * @returns {Vec3} The current skeleton offset. @@ -349,7 +348,7 @@ public: virtual glm::vec3 getSkeletonPosition() const; /**jsdoc - * Get the position of a joint in the current avatar. + * Gets the position of a joint in the current avatar. * @function MyAvatar.getJointPosition * @param {number} index - The index of the joint. * @returns {Vec3} The position of the joint in world coordinates. @@ -357,7 +356,7 @@ public: Q_INVOKABLE glm::vec3 getJointPosition(int index) const; /**jsdoc - * Get the position of a joint in the current avatar. + * Gets the position of a joint in the current avatar. * @function MyAvatar.getJointPosition * @param {string} name - The name of the joint. * @returns {Vec3} The position of the joint in world coordinates. @@ -367,7 +366,7 @@ public: Q_INVOKABLE glm::vec3 getJointPosition(const QString& name) const; /**jsdoc - * Get the position of the current avatar's neck in world coordinates. + * Gets the position of the current avatar's neck in world coordinates. * @function MyAvatar.getNeckPosition * @returns {Vec3} The position of the neck in world coordinates. * @example Report the position of your avatar's neck. @@ -401,10 +400,10 @@ public: void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); float computeMass(); /**jsdoc - * Get the position of the current avatar's feet (or rather, bottom of its collision capsule) in world coordinates. + * Gets the position of the current avatar's feet (or rather, bottom of its collision capsule) in world coordinates. * @function MyAvatar.getWorldFeetPosition * @returns {Vec3} The position of the avatar's feet in world coordinates. - */ + */ Q_INVOKABLE glm::vec3 getWorldFeetPosition(); void setPositionViaScript(const glm::vec3& position) override; @@ -439,9 +438,9 @@ public: Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex) override; /**jsdoc - * Returns an array of joints, where each joint is an object containing name, index, and parentIndex fields. + * Gets information on all the joints in the avatar's skeleton. * @function MyAvatar.getSkeleton - * @returns {MyAvatar.SkeletonJoint[]} A list of information about each joint in this avatar's skeleton. + * @returns {MyAvatar.SkeletonJoint[]} Information about each joint in the avatar's skeleton. */ /**jsdoc * Information about a single joint in an Avatar's skeleton hierarchy. @@ -524,6 +523,8 @@ public: signals: /**jsdoc * @function MyAvatar.targetScaleChanged + * @param {number} targetScale + * @returns Signal */ void targetScaleChanged(float targetScale); @@ -533,7 +534,7 @@ public slots: // thread safe, will return last valid palm from cache /**jsdoc - * Get the position of the left palm in world coordinates. + * Gets the position of the left palm in world coordinates. * @function MyAvatar.getLeftPalmPosition * @returns {Vec3} The position of the left palm in world coordinates. * @example Report the position of your avatar's left palm. @@ -542,15 +543,16 @@ public slots: glm::vec3 getLeftPalmPosition() const; /**jsdoc - * Get the rotation of the left palm in world coordinates. + * Gets the rotation of the left palm in world coordinates. * @function MyAvatar.getLeftPalmRotation * @returns {Quat} The rotation of the left palm in world coordinates. * @example Report the rotation of your avatar's left palm. * print(JSON.stringify(MyAvatar.getLeftPalmRotation())); */ glm::quat getLeftPalmRotation() const; + /**jsdoc - * Get the position of the right palm in world coordinates. + * Gets the position of the right palm in world coordinates. * @function MyAvatar.getRightPalmPosition * @returns {Vec3} The position of the right palm in world coordinates. * @example Report the position of your avatar's right palm. @@ -569,22 +571,23 @@ public slots: /**jsdoc * @function MyAvatar.setModelURLFinished + * @param {boolean} success */ // hooked up to Model::setURLFinished signal void setModelURLFinished(bool success); /**jsdoc * @function MyAvatar.rigReady - * @returns {Signal} + * @deprecated This function is deprecated and will be removed. */ // Hooked up to Model::rigReady signal void rigReady(); /**jsdoc * @function MyAvatar.rigReset - * @returns {Signal} + * @deprecated This function is deprecated and will be removed. */ - // Jooked up to Model::rigReset signal + // Hooked up to Model::rigReset signal void rigReset(); protected: From 0b7cddb886ffd24f7e150b9150fe7a582626be77 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Mar 2019 13:49:10 +1300 Subject: [PATCH 009/135] Fill in and tidy MyAvatar JSDoc --- interface/src/avatar/MyAvatar.cpp | 14 + interface/src/avatar/MyAvatar.h | 533 +++++++++++++----- .../src/avatars-renderer/Avatar.h | 93 ++- libraries/shared/src/RegisteredMetaTypes.cpp | 9 +- 4 files changed, 483 insertions(+), 166 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 30e8733a42..f62896772d 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3531,6 +3531,12 @@ void MyAvatar::clearScaleRestriction() { _haveReceivedHeightLimitsFromDomain = false; } +/**jsdoc + * A teleport target. + * @typedef {object} MyAvatar.GoToProperties + * @property {Vec3} position - The new position for the avatar, in world coordinates. + * @property {Quat} [orientation] - The new orientation for the avatar. + */ void MyAvatar::goToLocation(const QVariant& propertiesVar) { qCDebug(interfaceapp, "MyAvatar QML goToLocation"); auto properties = propertiesVar.toMap(); @@ -3887,6 +3893,14 @@ void MyAvatar::setCollisionWithOtherAvatarsFlags() { _characterController.setPendingFlagsUpdateCollisionMask(); } +/**jsdoc + * A collision capsule is a cylinder with hemispherical ends. It is used, in particular, to approximate the extents of an + * avatar. + * @typedef {object} MyAvatar.CollisionCapsule + * @property {Vec3} start - The bottom end of the cylinder, excluding the bottom hemisphere. + * @property {Vec3} end - The top end of the cylinder, excluding the top hemisphere. + * @property {number} radius - The radius of the cylinder and the hemispheres. + */ void MyAvatar::updateCollisionCapsuleCache() { glm::vec3 start, end; float radius; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index a0f1531e64..f05bdd5184 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -155,33 +155,45 @@ class MyAvatar : public Avatar { * @property {boolean} hasProceduralEyeFaceMovement=true - If true then procedural eye movement is turned on. * @property {boolean} hasAudioEnabledFaceMovement=true - If true then voice audio will move the mouth * blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. - * @property {number} rotationRecenterFilterLength - * @property {number} rotationThreshold - * @property {boolean} enableStepResetRotation - * @property {boolean} enableDrawAverageFacing + * @property {number} rotationRecenterFilterLength - Configures how quickly the avatar root rotates to recenter its facing + * direction to match that of the user's torso based on head and hands orientation. A smaller value makes the + * recentering happen more quickly. The minimum value is 0.01. + * @property {number} rotationThreshold - The angle in radians that the user's torso facing direction (based on head and + * hands orientation) can differ from that of the avatar before the avatar's root is rotated to match the user's torso. + * @property {boolean} enableStepResetRotation - If true then after the user's avatar takes a step, the + * avatar's root immediately rotates to recenter its facing direction to match that of the user's torso based on head + * and hands orientation. + * @property {boolean} enableDrawAverageFacing - If true, debug graphics are drawn that show the average + * facing direction of the user's torso (based on head and hands orientation). This can be useful if you want to try + * out different filter lengths and thresholds. * * @property {Vec3} leftHandPosition - The position of the left hand in avatar coordinates if it's being positioned by * controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. * @property {Vec3} rightHandPosition - The position of the right hand in avatar coordinates if it's being positioned by * controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. - * @property {Vec3} leftHandTipPosition - The position 30cm offset from the left hand in avatar coordinates if it's being - * positioned by controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. - * @property {Vec3} rightHandTipPosition - The position 30cm offset from the right hand in avatar coordinates if it's being - * positioned by controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. + * @property {Vec3} leftHandTipPosition - The position 0.3m in front of the left hand's position, in the direction along the + * palm, in avatar coordinates. If the hand isn't being positioned by a controller, the value is + * {@link Vec3(0)|Vec3.ZERO}. Read-only. + * @property {Vec3} rightHandTipPosition - The position 0.3m in front of the right hand's position, in the direction along + * the palm, in avatar coordinates. If the hand isn't being positioned by a controller, the value is + * {@link Vec3(0)|Vec3.ZERO}. Read-only. * - * @property {Pose} leftHandPose - The pose of the left hand as determined by the hand controllers. Read-only. - * @property {Pose} rightHandPose - The pose right hand position as determined by the hand controllers. Read-only. - * @property {Pose} leftHandTipPose - The pose of the left hand as determined by the hand controllers, with the position - * by 30cm. Read-only. - * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, with the position - * by 30cm. Read-only. + * @property {Pose} leftHandPose - The pose of the left hand as determined by the hand controllers, relative to the avatar. + * Read-only. + * @property {Pose} rightHandPose - The pose right hand position as determined by the hand controllers, relative to the + * avatar. Read-only. + * @property {Pose} leftHandTipPose - The pose of the left hand as determined by the hand controllers, relative to the + * avatar, with the position adjusted to be 0.3m along the direction of the palm. Read-only. + * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, relative to the + * avatar, with the position adjusted by 0.3m along the direction of the palm. Read-only. * - * @property {number} energy - * @property {boolean} isAway + * @property {number} energy - Deprecated: This property will be removed from the API. + * @property {boolean} isAway - true if your avatar is away (i.e., inactive), false if it is + * active. * - * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to the center of - * gravity model that balance the center of gravity over the base of support of the feet. Setting the value false - * will result in the default behaviour where the hips are placed under the head. + * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to + * the center of gravity model that balances the center of gravity over the base of support of the feet. Setting the + * value false results in the default behavior where the hips are positioned under the head. * @property {boolean} hmdLeanRecenterEnabled=true - If true then the avatar is re-centered to be under the * head's position. In room-scale VR, this behavior is what causes your avatar to follow your HMD as you walk around * the room. Setting the value false is useful if you want to pin the avatar to a fixed position. @@ -198,8 +210,8 @@ class MyAvatar : public Avatar { * boundaries while teleporting.
* Note: Setting the value has no effect unless Interface is restarted. * - * @property {number} yawSpeed=75 - * @property {number} pitchSpeed=50 + * @property {number} yawSpeed=75 - The mouse X sensitivity value in Settings > General. Read-only. + * @property {number} pitchSpeed=50 - The mouse Y sensitivity value in Settings > General. Read-only. * * @property {boolean} hmdRollControlEnabled=true - If true, the roll angle of your HMD turns your avatar * while flying. @@ -215,13 +227,20 @@ class MyAvatar : public Avatar { * where MyAvatar.sessionUUID is not available (e.g., if not connected to a domain). Note: Likely to be deprecated. * Read-only. * - * @property {number} walkSpeed - * @property {number} walkBackwardSpeed - * @property {number} sprintSpeed - * @property {number} isInSittingState - * @property {MyAvatar.SitStandModelType} userRecenterModel - * @property {boolean} isSitStandStateLocked - * @property {boolean} allowTeleporting + * @property {number} walkSpeed - Adjusts the walk speed of your avatar. + * @property {number} walkBackwardSpeed - Adjusts the walk backward speed of your avatar. + * @property {number} sprintSpeed - Adjusts the sprint speed of your avatar. + * @property {MyAvatar.SitStandModelType} userRecenterModel - Controls avatar leaning and recentering behavior. + * @property {number} isInSittingState - true if your avatar is sitting (avatar leaning is disabled, + * recenntering is enabled), false if it is standing (avatar leaning is enabled, and avatar recenters if it + * leans too far). If userRecenterModel == 2 (i.e., auto) the property value automatically updates as the + * user sits or stands, unless isSitStandStateLocked == true. Setting the property value overrides the + * current siting / standing state, which is updated when the user next sits or stands unless + * isSitStandStateLocked == true. + * @property {boolean} isSitStandStateLocked - true locks the avatar sitting / standing state, i.e., disables + * automatically changing it based on the user sitting or standing. + * @property {boolean} allowTeleporting - true if teleporting is enabled in the Interface settings, + * false if it isn't. Read-only. * * @borrows Avatar.getDomainMinScale as getDomainMinScale * @borrows Avatar.getDomainMaxScale as getDomainMaxScale @@ -353,6 +372,40 @@ class MyAvatar : public Avatar { using TimePoint = Clock::time_point; public: + + /**jsdoc + *

Logical keys that drive your avatar and camera.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ValueNameDescription
0TRANSLATE_XMove the user's avatar in the direction of its x-axis, if the + * camera isn't in independent or mirror modes.
1TRANSLATE_YMove the user's avatar in the direction of its y-axis, if the + * camera isn't in independent or mirror modes.
2TRANSLATE_ZMove the user's avatar in the direction of its z-axis, if the + * camera isn't in independent or mirror modes
3YAWRotate the user's avatar about its y-axis at a rate proportional to the + * control value, if the camera isn't in independent or mirror modes.
4STEP_TRANSLATE_XNo action.
5STEP_TRANSLATE_YNo action.
6STEP_TRANSLATE_ZNo action.
7STEP_YAWRotate the user's avatar about its y-axis in a step increment, if + * the camera isn't in independent or mirror modes.
8PITCHRotate the user's avatar head and attached camera about its negative + * x-axis (i.e., positive values pitch down) at a rate proportional to the control value, if the camera isn't in HMD, + * independent, or mirror modes.
9ZOOMZooms the camera in or out.
10DELTA_YAWRotate the user's avatar about its y-axis by an amount proportional + * to the control value, if the camera isn't in independent or mirror modes.
11DELTA_PITCHRotate the user's avatar head and attached camera about its + * negative x-axis (i.e., positive values pitch down) by an amount proportional to the control value, if the camera + * isn't in HMD, independent, or mirror modes.
+ * @typedef {number} MyAvatar.DriveKeys + */ enum DriveKeys { TRANSLATE_X = 0, TRANSLATE_Y, @@ -371,6 +424,25 @@ public: Q_ENUM(DriveKeys) /**jsdoc + *

Specifies different avatar leaning and recentering behaviors.

+ * + * + * + * + * + * + * + * + * + * + *
ValueNameDescription
0ForceSitAssumes the user is seated in the real world. Disables avatar + * leaning regardless of what the avatar is doing in the virtual world (i.e., avatar always recenters).
1ForceStandAssumes the user is standing in the real world. Enables avatar + * leaning regardless of what the avatar is doing in the virtual world (i.e. avatar leans, then if leans too far it + * recenters).
2AutoInterface detects when the user is standing or seated in the real world. + * Avatar leaning is disabled when the user is sitting (i.e., avatar always recenters), and avatar leaning is enabled + * when the user is standing (i.e., avatar leans, then if leans too far it recenters).
3DisableHMDLeanBoth avatar leaning and recentering are disabled regardless of + * what the user is doing in the real world and no matter what their avatar is doing in the virtual world. Enables + * the avatar to sit on the floor when the user sits on the floor.
Note: Experimental.
* @typedef {number} MyAvatar.SitStandModelType */ enum SitStandModelType { @@ -399,6 +471,7 @@ public: void setCollisionWithOtherAvatarsFlags() override; /**jsdoc + * Resets the sensor positioning of your HMD (if used) and recenters your avatar body and head. * @function MyAvatar.resetSensorsAndBody */ Q_INVOKABLE void resetSensorsAndBody(); @@ -427,14 +500,16 @@ public: const glm::quat& getHMDSensorOrientation() const { return _hmdSensorOrientation; } /**jsdoc + * Gets the avatar orientation. Suitable for use in QML. * @function MyAvatar.setOrientationVar - * @param {object} newOrientationVar + * @param {object} newOrientationVar - The avatar orientation. */ Q_INVOKABLE void setOrientationVar(const QVariant& newOrientationVar); /**jsdoc + * Gets the avatar orientation. Suitable for use in QML. * @function MyAvatar.getOrientationVar - * @returns {object} + * @returns {object} The avatar orientation. */ Q_INVOKABLE QVariant getOrientationVar() const; @@ -598,74 +673,124 @@ public: // a handler must not remove properties from animStateDictionaryIn, nor change property values that it does not intend to change. // It is not specified in what order multiple handlers are called. /**jsdoc + * Adds an animation state handler function that is invoked just before each animation graph update. More than one + * animation state handler function may be added by calling addAnimationStateHandler multiple times. It is not + * specified in what order multiple handlers are called. + *

The animation state handler function is called with an {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} + * "animStateDictionaryIn" parameter and is expected to return an + * {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} "animStateDictionaryOut" object. The + * animStateDictionaryOut object can be the same object as animStateDictionaryIn, or it can be a + * different object. The animStateDictionaryIn may be shared among multiple handlers and thus may contain + * additional properties specified when adding the different handlers.

+ *

A handler may change a value from animStateDictionaryIn or add different values in the + * animStateDictionaryOut returned. Any property values set in animStateDictionaryOut will + * override those of the internal animation machinery.|null} propertiesList - The list of {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} + * properties that should be included the in parameter that the handler function is called with. If null + * then all properties are included in the call parameter. + * @returns {number} The ID of the animation state handler function if successfully added, undefined if not + * successfully added. + * @example Log all the animation state dictionary parameters for a short while. + * function animStateHandler(dictionary) { + * print("Anim state dictionary: " + JSON.stringify(dictionary)); + * } + * + * var handler = MyAvatar.addAnimationStateHandler(animStateHandler, null); + * + * Script.setTimeout(function () { + * MyAvatar.removeAnimationStateHandler(handler); + * }, 100); */ Q_INVOKABLE QScriptValue addAnimationStateHandler(QScriptValue handler, QScriptValue propertiesList) { return _skeletonModel->getRig().addAnimationStateHandler(handler, propertiesList); } /**jsdoc + * Removes an animation state handler function. * @function MyAvatar.removeAnimationStateHandler - * @param {number} handler + * @param {number} handler - The ID of the animation state handler function to remove. */ // Removes a handler previously added by addAnimationStateHandler. Q_INVOKABLE void removeAnimationStateHandler(QScriptValue handler) { _skeletonModel->getRig().removeAnimationStateHandler(handler); } /**jsdoc + * Gets whether or not you do snap turns in HMD mode. * @function MyAvatar.getSnapTurn - * @returns {boolean} + * @returns {boolean} true if you do snap turns in HMD mode; false if you do smooth turns in HMD + * mode. */ Q_INVOKABLE bool getSnapTurn() const { return _useSnapTurn; } /**jsdoc + * Sets whether your should do snap turns or smooth turns in HMD mode. * @function MyAvatar.setSnapTurn - * @param {boolean} on + * @param {boolean} on - true to do snap turns in HMD mode; false to do smooth turns in HMD mode. */ Q_INVOKABLE void setSnapTurn(bool on) { _useSnapTurn = on; } /**jsdoc + * Sets the avatar's dominant hand. * @function MyAvatar.setDominantHand - * @param {string} hand + * @param {string} hand - The dominant hand: "left" for the left hand or "right" for the right + * hand. Any other value has no effect. */ Q_INVOKABLE void setDominantHand(const QString& hand); /**jsdoc + * Gets the avatar's dominant hand. * @function MyAvatar.getDominantHand - * @returns {string} + * @returns {string} "left" for the left hand, "right" for the right hand. */ Q_INVOKABLE QString getDominantHand() const; /**jsdoc * @function MyAvatar.setHmdAvatarAlignmentType - * @param {string} hand + * @param {string} type - "head" to align your head and your avatar's head, "eyes" to align your + * eyes and your avatar's eyes. + * */ - Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& hand); + Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& type); /**jsdoc + * Gets the HMD alignment for your avatar. * @function MyAvatar.getHmdAvatarAlignmentType - * @returns {string} + * @returns {string} "head" if aligning your head and your avatar's head, "eyes" if aligning your + * eyes and your avatar's eyes. */ Q_INVOKABLE QString getHmdAvatarAlignmentType() const; /**jsdoc + * Sets whether the avatar hips are balanced over the feet or positioned under the head. * @function MyAvatar.setCenterOfGravityModelEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to balance the hips over the feet, false to position the hips + * under the head. */ Q_INVOKABLE void setCenterOfGravityModelEnabled(bool value) { _centerOfGravityModelEnabled = value; } /**jsdoc + * Gets whether the avatar hips are being balanced over the feet or placed under the head. * @function MyAvatar.getCenterOfGravityModelEnabled - * @returns {boolean} + * @returns {boolean} true if the hips are being balanced over the feet, false if the hips are + * being positioned under the head. */ Q_INVOKABLE bool getCenterOfGravityModelEnabled() const { return _centerOfGravityModelEnabled; } + /**jsdoc + * Sets whether or not the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering + * causes your avatar to follow your HMD as you walk around the room. Disabling recentering is useful if you want to pin + * the avatar to a fixed position. * @function MyAvatar.setHMDLeanRecenterEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to recenter the avatar under the head as it moves, false to + * disable recentering. */ Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } /**jsdoc + * Gets whether or not the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering + * causes your avatar to follow your HMD as you walk around the room. * @function MyAvatar.getHMDLeanRecenterEnabled - * @returns {boolean} + * @returns {boolean} true if recentering is enabled, false if not. */ Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } @@ -739,30 +864,42 @@ public: float getDriveKey(DriveKeys key) const; /**jsdoc + * Gets the value of a drive key, regardless of whether or not it is disabled. * @function MyAvatar.getRawDriveKey - * @param {DriveKeys} key - * @returns {number} + * @param {MyAvatar.DriveKeys} key - The drive key. + * @returns {number} The value of the drive key. */ Q_INVOKABLE float getRawDriveKey(DriveKeys key) const; void relayDriveKeysToCharacterController(); /**jsdoc + * Disables the action associated with a drive key. * @function MyAvatar.disableDriveKey - * @param {DriveKeys} key + * @param {MyAvatar.DriveKeys} key - The drive key to disable. + * @example Disable rotating your avatar using the keyboard for a couple of seconds. + * var YAW = 3; + * print("Disable"); + * MyAvatar.disableDriveKey(YAW); + * Script.setTimeout(function () { + * print("Enable"); + * MyAvatar.enableDriveKey(YAW); + * }, 5000); */ Q_INVOKABLE void disableDriveKey(DriveKeys key); /**jsdoc + * Enables the acction associated with a drive key. * @function MyAvatar.enableDriveKey - * @param {DriveKeys} key + * @param {MyAvatar.DriveKeys} key - The drive key to enable. */ Q_INVOKABLE void enableDriveKey(DriveKeys key); /**jsdoc + * Checks whether or not a drive key is disabled. * @function MyAvatar.isDriveKeyDisabled - * @param {DriveKeys} key - * @returns {boolean} + * @param {DriveKeys} key - The drive key to check. + * @returns {boolean} true if the drive key is disabled, false if it isn't. */ Q_INVOKABLE bool isDriveKeyDisabled(DriveKeys key) const; @@ -809,26 +946,32 @@ public: Q_INVOKABLE glm::vec3 getHeadPosition() const { return getHead()->getPosition(); } /**jsdoc + * Gets the yaw of the avatar's head relative to its body. * @function MyAvatar.getHeadFinalYaw - * @returns {number} + * @returns {number} The yaw of the avatar's head, in degrees. */ Q_INVOKABLE float getHeadFinalYaw() const { return getHead()->getFinalYaw(); } /**jsdoc + * Gets the roll of the avatar's head relative to its body. * @function MyAvatar.getHeadFinalRoll - * @returns {number} + * @returns {number} The roll of the avatar's head, in degrees. */ Q_INVOKABLE float getHeadFinalRoll() const { return getHead()->getFinalRoll(); } /**jsdoc + * Gets the pitch of the avatar's head relative to its body. * @function MyAvatar.getHeadFinalPitch - * @returns {number} + * @returns {number} The pitch of the avatar's head, in degrees. */ Q_INVOKABLE float getHeadFinalPitch() const { return getHead()->getFinalPitch(); } /**jsdoc + * If a face tracker is connected and being used, gets the estimated pitch of the user's head scaled such that the avatar + * looks at the edge of the view frustum when the user looks at the edge of their screen. * @function MyAvatar.getHeadDeltaPitch - * @returns {number} + * @returns {number} The pitch that the avatar's head should be if a face tracker is connected and being used, otherwise + * 0, in degrees. */ Q_INVOKABLE float getHeadDeltaPitch() const { return getHead()->getDeltaPitch(); } @@ -845,21 +988,27 @@ public: /**jsdoc * Gets the position of the avatar your avatar is currently looking at. * @function MyAvatar.getTargetAvatarPosition - * @returns {Vec3} The position of the avatar your avatar is currently looking at. + * @returns {Vec3} The position of the avatar beeing looked at. * @example Report the position of the avatar you're currently looking at. * print(JSON.stringify(MyAvatar.getTargetAvatarPosition())); */ + // FIXME: If not looking at an avatar, the most recently looked-at position is returned. This should be fixed to return + // undefined or {NaN, NaN, NaN} or similar. Q_INVOKABLE glm::vec3 getTargetAvatarPosition() const { return _targetAvatarPosition; } /**jsdoc + * Gets information on the avatar your avatar is currently looking at. * @function MyAvatar.getTargetAvatar - * @returns {AvatarData} + * @returns {AvatarData} Information on the avatar being looked at. */ + // FIXME: The return type doesn't have a conversion to a script value so the function always returns undefined in + // JavaScript. Note: When fixed, JSDoc is needed for the return type. Q_INVOKABLE ScriptAvatarData* getTargetAvatar() const; /**jsdoc - * Gets the position of the avatar's left hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
+ * Gets the position of the avatar's left hand, relative to the avatar, as positioned by a hand controller (e.g., Oculus + * Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.)

* @function MyAvatar.getLeftHandPosition @@ -871,7 +1020,8 @@ public: Q_INVOKABLE glm::vec3 getLeftHandPosition() const; /**jsdoc - * Gets the position of the avatar's right hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
+ * Gets the position of the avatar's right hand, relative to the avatar, as positioned by a hand controller (e.g., Oculus + * Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.)

* @function MyAvatar.getRightHandPosition @@ -883,26 +1033,32 @@ public: Q_INVOKABLE glm::vec3 getRightHandPosition() const; /**jsdoc + * Gets the position 0.3m in front of the left hand's position in the direction along the palm, in avatar coordinates, as + * positioned by a hand controller. * @function MyAvatar.getLeftHandTipPosition - * @returns {Vec3} + * @returns {Vec3} The position 0.3m in front of the left hand's position in the direction along the palm, in avatar + * coordinates. If the hand isn't being positioned by a controller, {@link Vec3(0)|Vec3.ZERO} is returned. */ Q_INVOKABLE glm::vec3 getLeftHandTipPosition() const; /**jsdoc + * Gets the position 0.3m in front of the right hand's position in the direction along the palm, in avatar coordinates, as + * positioned by a hand controller. * @function MyAvatar.getRightHandTipPosition - * @returns {Vec3} + * @returns {Vec3} The position 0.3m in front of the right hand's position in the direction along the palm, in avatar + * coordinates. If the hand isn't being positioned by a controller, {@link Vec3(0)|Vec3.ZERO} is returned. */ Q_INVOKABLE glm::vec3 getRightHandTipPosition() const; /**jsdoc * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a - * hand controller (e.g., Oculus Touch or Vive).
+ * hand controller (e.g., Oculus Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.) If you are using the Leap Motion, the return value's valid property will be * false and any pose values returned will not be meaningful.

* @function MyAvatar.getLeftHandPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's left hand, relative to the avatar, as positioned by a hand controller. * @example Report the pose of your avatar's left hand. * print(JSON.stringify(MyAvatar.getLeftHandPose())); */ @@ -910,26 +1066,38 @@ public: /**jsdoc * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a - * hand controller (e.g., Oculus Touch or Vive).
+ * hand controller (e.g., Oculus Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.) If you are using the Leap Motion, the return value's valid property will be * false and any pose values returned will not be meaningful.

* @function MyAvatar.getRightHandPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's right hand, relative to the avatar, as positioned by a hand controller. * @example Report the pose of your avatar's right hand. * print(JSON.stringify(MyAvatar.getRightHandPose())); */ Q_INVOKABLE controller::Pose getRightHandPose() const; /**jsdoc + * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand, relative to the avatar, as + * positioned by a hand controller (e.g., Oculus Touch or Vive), and translated 0.3m along the palm. + *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints + * for hand animation.) If you are using the Leap Motion, the return value's valid property will be + * false and any pose values returned will not be meaningful.

* @function MyAvatar.getLeftHandTipPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's left hand, relative to the avatar, as positioned by a hand controller, and + * translated 0.3m along the palm. */ Q_INVOKABLE controller::Pose getLeftHandTipPose() const; /**jsdoc + * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's right hand, relative to the avatar, as + * positioned by a hand controller (e.g., Oculus Touch or Vive), and translated 0.3m along the palm. + *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints + * for hand animation.) If you are using the Leap Motion, the return value's valid property will be + * false and any pose values returned will not be meaningful.

* @function MyAvatar.getRightHandTipPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's right hand, relative to the avatar, as positioned by a hand controller, and + * translated 0.3m along the palm. */ Q_INVOKABLE controller::Pose getRightHandTipPose() const; @@ -952,33 +1120,39 @@ public: virtual void clearJointsData() override; /**jsdoc + * Sets and locks a joint's position and orientation. + *

Note: Only works on the hips joint.

* @function MyAvatar.pinJoint - * @param {number} index - * @param {Vec3} position - * @param {Quat} orientation - * @returns {boolean} + * @param {number} index - The index of the joint. + * @param {Vec3} position - The position of the joint in world coordinates. + * @param {Quat} orientation - The orientation of the joint in world coordinates. + * @returns {boolean} true if the joint was pinned, false if it wasn't. */ Q_INVOKABLE bool pinJoint(int index, const glm::vec3& position, const glm::quat& orientation); bool isJointPinned(int index); /**jsdoc + * Clears a lock on a joint's position and orientation, as set by {@link MyAvatar.pinJoint|pinJoint}. + *

Note: Only works on the hips joint.

* @function MyAvatar.clearPinOnJoint - * @param {number} index - * @returns {boolean} + * @param {number} index - The index of the joint. + * @returns {boolean} true if the joint was unpinned, false if it wasn't. */ Q_INVOKABLE bool clearPinOnJoint(int index); /**jsdoc + * Gets the maximum error distance from the most recent inverse kinematics (IK) solution. * @function MyAvatar.getIKErrorOnLastSolve - * @returns {number} + * @returns {number} The maximum IK error distance. */ Q_INVOKABLE float getIKErrorOnLastSolve() const; /**jsdoc + * Changes the user's avatar and associated descriptive name. * @function MyAvatar.useFullAvatarURL - * @param {string} fullAvatarURL - * @param {string} [modelName=""] + * @param {string} fullAvatarURL - The URL of the avatar's .fst file. + * @param {string} [modelName=""] - Descriptive name of the avatar. */ Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); @@ -1065,7 +1239,7 @@ public: /**jsdoc * Gets the list of avatar entities and their properties. * @function MyAvatar.getAvatarEntitiesVariant - * @returns {MyAvatar.AvatarEntityData[]} + * @returns {MyAvatar.AvatarEntityData[]} The list of avatar entities and their properties. */ Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); @@ -1141,60 +1315,76 @@ public: */ Q_INVOKABLE bool getFlyingHMDPref(); - /**jsdoc + * Gets the target scale of the avatar. The target scale is the desired scale of the avatar without any restrictions on + * permissible scale values imposed by the domain. * @function MyAvatar.getAvatarScale - * @returns {number} + * @returns {number} The target scale for the avatar, range 0.0051000.0. */ Q_INVOKABLE float getAvatarScale(); /**jsdoc + * Sets the target scale of the avatar. The target scale is the desired scale of the avatar without any restrictions on + * permissible scale values imposed by the domain. * @function MyAvatar.setAvatarScale - * @param {number} scale + * @param {number} scale - The target scale for the avatar, range 0.0051000.0. */ Q_INVOKABLE void setAvatarScale(float scale); - /**jsdoc + * Sets whether or not the avatar should collide with entities. + *

Note: A false value won't disable collisions if the avatar is in a zone that disallows + * collisionless avatars, however the false value will be set so that collisions are disabled as soon as the + * avatar moves to a position where collisionless avatars are allowed. * @function MyAvatar.setCollisionsEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to enable the avatar to collide with entities, false to + * disable. */ Q_INVOKABLE void setCollisionsEnabled(bool enabled); /**jsdoc + * Gets whether or not the avatar will currently collide with entities. + *

Note: The avatar will always collide with entities if in a zone that disallows collisionless avatars. * @function MyAvatar.getCollisionsEnabled - * @returns {boolean} + * @returns {boolean} true if the avatar will currently collide with entities, false if it won't. */ Q_INVOKABLE bool getCollisionsEnabled(); /**jsdoc + * Sets whether or not the avatar should collide with other avatars. * @function MyAvatar.setOtherAvatarsCollisionsEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to enable the avatar to collide with other avatars, false + * to disable. */ Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled); /**jsdoc + * Gets whether or not the avatar will collide with other avatars. * @function MyAvatar.getOtherAvatarsCollisionsEnabled - * @returns {boolean} + * @returns {boolean} true if the avatar will collide with other avatars, false if it won't. */ Q_INVOKABLE bool getOtherAvatarsCollisionsEnabled(); /**jsdoc + * Gets the avatar's collision capsule: a cylinder with hemispherical ends that approximates the extents or the avatar. + *

Warning: The values returned are in world coordinates but aren't necessarily up to date with the + * avatar's current position.

* @function MyAvatar.getCollisionCapsule - * @returns {object} + * @returns {MyAvatar.CollisionCapsule} The avatar's collision capsule. */ Q_INVOKABLE QVariantMap getCollisionCapsule() const; /**jsdoc * @function MyAvatar.setCharacterControllerEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to enable the avatar to collide with entities, false to + * disable. * @deprecated Use {@link MyAvatar.setCollisionsEnabled} instead. */ Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); // deprecated /**jsdoc * @function MyAvatar.getCharacterControllerEnabled - * @returns {boolean} + * @returns {boolean} true if the avatar will currently collide with entities, false if it won't. * @deprecated Use {@link MyAvatar.getCollisionsEnabled} instead. */ Q_INVOKABLE bool getCharacterControllerEnabled(); // deprecated @@ -1248,16 +1438,22 @@ public: glm::mat4 deriveBodyUsingCgModel(); /**jsdoc + * Tests whether a vector is pointing in the general direction of the avatar's "up" direction (i.e., dot product of vectors + * is > 0). * @function MyAvatar.isUp - * @param {Vec3} direction - * @returns {boolean} + * @param {Vec3} direction - The vector to test. + * @returns {boolean} true if the direction vector is pointing generally in the direction of the avatar's "up" + * direction. */ Q_INVOKABLE bool isUp(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) > 0.0f; }; // true iff direction points up wrt avatar's definition of up. /**jsdoc + * Tests whether a vector is pointing in the general direction of the avatar's "down" direction (i.e., dot product of + * vectors is < 0). * @function MyAvatar.isDown - * @param {Vec3} direction - * @returns {boolean} + * @param {Vec3} direction - The vector to test. + * @returns {boolean} true if the direction vector is pointing generally in the direction of the avatar's + * "down" direction. */ Q_INVOKABLE bool isDown(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) < 0.0f; }; @@ -1326,14 +1522,14 @@ public: * Gets the avatar entities as binary data. * @function MyAvatar.getAvatarEntityData * @override - * @returns {AvatarEntityMap} + * @returns {AvatarEntityMap} The avatar entities as binary data. */ AvatarEntityMap getAvatarEntityData() const override; /**jsdoc * Sets the avatar entities from binary data. * @function MyAvatar.setAvatarEntityData - * @param {AvatarEntityMap} avatarEntityData + * @param {AvatarEntityMap} avatarEntityData - The avatar entities as binary data. */ void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; @@ -1345,8 +1541,7 @@ public: void avatarEntityDataToJson(QJsonObject& root) const override; /**jsdoc - * @function MyAvatar.sendAvatarDataPacket - * @param {boolean} sendAll + * @comment Uses the base class's JSDoc. */ int sendAvatarDataPacket(bool sendAll = false) override; @@ -1386,18 +1581,22 @@ public slots: /**jsdoc * @function MyAvatar.animGraphLoaded + * @deprecated This function is deprecated and will be removed. */ void animGraphLoaded(); /**jsdoc + * Sets the amount of gravity applied to the avatar in the y-axis direction. (Negative values are downward.) * @function MyAvatar.setGravity - * @param {number} gravity + * @param {number} gravity - The amount of gravity to be applied to the avatar, in m/s2. */ void setGravity(float gravity); /**jsdoc + * Sets the amount of gravity applied to the avatar in the y-axis direction. (Negative values are downward.) The default + * value is -5 m/s2. * @function MyAvatar.getGravity - * @returns {number} + * @returns {number} The amount of gravity currently applied to the avatar, in m/s2. */ float getGravity(); @@ -1428,125 +1627,149 @@ public slots: bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), bool shouldFaceLocation = false, bool withSafeLanding = true); /**jsdoc + * Moves the avatar to a new position and (optional) orientation in the domain. * @function MyAvatar.goToLocation - * @param {object} properties + * @param {MyAvatar.GoToProperties} target - The goto target. */ void goToLocation(const QVariant& properties); /**jsdoc + * Moves the avatar to a new position and then enables collisions. * @function MyAvatar.goToLocationAndEnableCollisions - * @param {Vec3} position + * @param {Vec3} position - The new position for the avatar, in world coordinates. */ void goToLocationAndEnableCollisions(const glm::vec3& newPosition); /**jsdoc * @function MyAvatar.safeLanding - * @param {Vec3} position - * @returns {boolean} + * @param {Vec3} position -The new position for the avatar, in world coordinates. + * @returns {boolean} true if the avatar was moved, false if it wasn't. + * @deprecated This function is deprecated and will be removed. */ bool safeLanding(const glm::vec3& position); /**jsdoc * @function MyAvatar.restrictScaleFromDomainSettings - * @param {object} domainSettingsObject + * @param {object} domainSettings - Domain settings. + * @deprecated This function is deprecated and will be removed. */ void restrictScaleFromDomainSettings(const QJsonObject& domainSettingsObject); /**jsdoc * @function MyAvatar.clearScaleRestriction + * @deprecated This function is deprecated and will be removed from the API. */ void clearScaleRestriction(); /**jsdoc + * Adds a thrust to your avatar's current thrust, to be applied for a short while. * @function MyAvatar.addThrust - * @param {Vec3} thrust + * @param {Vec3} thrust - The thrust direction and magnitude. + * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. */ // Set/Get update the thrust that will move the avatar around void addThrust(glm::vec3 newThrust) { _thrust += newThrust; }; /**jsdoc + * Gets the thrust currently being applied to your avatar. * @function MyAvatar.getThrust - * @returns {vec3} + * @returns {Vec3} The thrust currently being applied to your avatar. + * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. */ glm::vec3 getThrust() { return _thrust; }; /**jsdoc + * Sets the thrust to be applied to your avatar for a short while. * @function MyAvatar.setThrust - * @param {Vec3} thrust + * @param {Vec3} thrust - The thrust direction and magnitude. + * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. */ void setThrust(glm::vec3 newThrust) { _thrust = newThrust; } /**jsdoc + * Updates avatar motion behavior from the Developer > Avatar > Enable Default Motor Control and Enable Scripted + * Motor Control menu items. * @function MyAvatar.updateMotionBehaviorFromMenu */ Q_INVOKABLE void updateMotionBehaviorFromMenu(); /**jsdoc * @function MyAvatar.setToggleHips - * @param {boolean} enabled + * @param {boolean} enabled - Enabled. + * @deprecated This function is deprecated and will be removed. */ void setToggleHips(bool followHead); /**jsdoc + * Displays base of support of feet debug graphics. * @function MyAvatar.setEnableDebugDrawBaseOfSupport - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawBaseOfSupport(bool isEnabled); /**jsdoc + * Displays default pose debug graphics. * @function MyAvatar.setEnableDebugDrawDefaultPose - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawDefaultPose(bool isEnabled); /**jsdoc + * Displays animation debug graphics. * @function MyAvatar.setEnableDebugDrawAnimPose - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawAnimPose(bool isEnabled); /**jsdoc + * Displays position debug graphics. * @function MyAvatar.setEnableDebugDrawPosition - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawPosition(bool isEnabled); /**jsdoc + * Displays controller hand target debug graphics. * @function MyAvatar.setEnableDebugDrawHandControllers - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawHandControllers(bool isEnabled); /**jsdoc + * Displays sensor-to-world matrix debug graphics. * @function MyAvatar.setEnableDebugDrawSensorToWorldMatrix - * @param {boolean} enabled + * @param {boolean} enable - true to show the debug graphics, false to hide. */ void setEnableDebugDrawSensorToWorldMatrix(bool isEnabled); /**jsdoc + * Displays inverse kinematics targets debug graphics. * @function MyAvatar.setEnableDebugDrawIKTargets - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawIKTargets(bool isEnabled); /**jsdoc + * Displays inverse kinematics constraints debug graphics. * @function MyAvatar.setEnableDebugDrawIKConstraints - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawIKConstraints(bool isEnabled); /**jsdoc + * Displays inverse kinematics chains debug graphics. * @function MyAvatar.setEnableDebugDrawIKChains - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawIKChains(bool isEnabled); /**jsdoc + * Displays detailed collision debug graphics. * @function MyAvatar.setEnableDebugDrawDetailedCollision - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawDetailedCollision(bool isEnabled); @@ -1570,13 +1793,15 @@ public slots: /**jsdoc * @function MyAvatar.sanitizeAvatarEntityProperties + * @param {EntityItemProperties} properties - Properties. + * @deprecated This function is deprecated and will be removed. */ void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const; /**jsdoc - * Sets whether or not your avatar mesh is visible. + * Sets whether or not your avatar mesh is visible to you. * @function MyAvatar.setEnableMeshVisible - * @param {boolean} visible - true to set your avatar mesh visible; false to set it invisible. + * @param {boolean} enabled - true to show your avatar mesh, false to hide. * @example Make your avatar invisible for 10s. * MyAvatar.setEnableMeshVisible(false); * Script.setTimeout(function () { @@ -1586,57 +1811,76 @@ public slots: virtual void setEnableMeshVisible(bool isEnabled) override; /**jsdoc + * Sets whether or not inverse kinematics (IK) for your avatar. * @function MyAvatar.setEnableInverseKinematics - * @param {boolean} enabled + * @param {boolean} enabled - true to enable IK, false to disable. */ void setEnableInverseKinematics(bool isEnabled); /**jsdoc + * Gets the URL of the override animation graph. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.getAnimGraphOverrideUrl - * @returns {string} + * @returns {string} The URL of the override animation graph. "" if there is no override animation graph. */ QUrl getAnimGraphOverrideUrl() const; // thread-safe /**jsdoc + * Sets the animation graph to use in preference to the default animation graph. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.setAnimGraphOverrideUrl - * @param {string} url + * @param {string} url - The URL of the animation graph to use. Set to "" to clear an override. */ void setAnimGraphOverrideUrl(QUrl value); // thread-safe /**jsdoc + * Gets the URL of animation graph that's currently being used for avatar animations. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.getAnimGraphUrl - * @returns {string} + * @returns {string} The URL of the current animation graph. */ QUrl getAnimGraphUrl() const; // thread-safe /**jsdoc + * Sets the current animation graph to use for avatar animations and makes it the default. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.setAnimGraphUrl - * @param {string} url + * @param {string} url - The URL of the animation graph to use. */ void setAnimGraphUrl(const QUrl& url); // thread-safe /**jsdoc + * Gets your listening position for spatialized audio. The position depends on the value of the + * {@link Myavatar|audioListenerMode} property. * @function MyAvatar.getPositionForAudio - * @returns {Vec3} + * @returns {Vec3} Your listening position. */ glm::vec3 getPositionForAudio(); /**jsdoc + * Gets the orientation of your listening position for spatialized audio. The orientation depends on the value of the + * {@link Myavatar|audioListenerMode} property. * @function MyAvatar.getOrientationForAudio - * @returns {Quat} + * @returns {Quat} The orientation of your listening position. */ glm::quat getOrientationForAudio(); /**jsdoc * @function MyAvatar.setModelScale - * @param {number} scale + * @param {number} scale - The scale. + * @deprecated This function is deprecated and will be removed. */ virtual void setModelScale(float scale) override; signals: /**jsdoc + * Triggered when the {@link MyAvatar|audioListenerMode} property value changes. * @function MyAvatar.audioListenerModeChanged * @returns {Signal} */ @@ -1645,12 +1889,14 @@ signals: /**jsdoc * @function MyAvatar.transformChanged * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void transformChanged(); /**jsdoc + * Triggered when the {@link MyAvatar|collisionSoundURL} property value changes. * @function MyAvatar.newCollisionSoundURL - * @param {string} url + * @param {string} url - The URL of the new collision sound. * @returns {Signal} */ void newCollisionSoundURL(const QUrl& url); @@ -1658,7 +1904,7 @@ signals: /**jsdoc * Triggered when the avatar collides with an entity. * @function MyAvatar.collisionWithEntity - * @param {Collision} collision + * @param {Collision} collision - Details of the collision. * @returns {Signal} * @example Report each time your avatar collides with an entity. * MyAvatar.collisionWithEntity.connect(function (collision) { @@ -1686,21 +1932,23 @@ signals: void otherAvatarsCollisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when the avatar's animation changes. + * Triggered when the avatar's animation graph URL changes. * @function MyAvatar.animGraphUrlChanged - * @param {url} url - The URL of the new animation. + * @param {string} url - The URL of the new animation graph. * @returns {Signal} */ void animGraphUrlChanged(const QUrl& url); /**jsdoc * @function MyAvatar.energyChanged - * @param {number} energy + * @param {number} energy - Avatar energy. * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void energyChanged(float newEnergy); /**jsdoc + * Triggered when the avatar has been moved to a new position by one of the MyAvatar "goTo" functions. * @function MyAvatar.positionGoneTo * @returns {Signal} */ @@ -1708,57 +1956,78 @@ signals: void positionGoneTo(); /**jsdoc + * Triggered when the avatar's model finishes loading. * @function MyAvatar.onLoadComplete * @returns {Signal} */ void onLoadComplete(); /**jsdoc + * Triggered when your avatar changes from being active to being away. * @function MyAvatar.wentAway * @returns {Signal} + * @example Report when your avatar goes away. + * MyAvatar.wentAway.connect(function () { + * print("My avatar went away"); + * }); + * // In desktop mode, pres the Esc key to go away. */ void wentAway(); /**jsdoc + * Triggered when your avatar changes from being away to being active. * @function MyAvatar.wentActive * @returns {Signal} */ void wentActive(); /**jsdoc + * Triggered when the avatar's model (i.e., {@link MyAvatar|skeletonModelURL} property value) is changed. + *

Synonym of {@link MyAvatar.skeletonModelURLChanged|skeletonModelURLChanged}.

* @function MyAvatar.skeletonChanged * @returns {Signal} */ void skeletonChanged(); /**jsdoc + * Triggered when the avatar's dominant hand changes. * @function MyAvatar.dominantHandChanged - * @param {string} hand + * @param {string} hand - The dominant hand: "left" for the left hand, "right" for the right hand. * @returns {Signal} */ void dominantHandChanged(const QString& hand); /**jsdoc + * Triggered when the HMD alignment for your avatar changes. * @function MyAvatar.hmdAvatarAlignmentTypeChanged - * @param {string} type + * @param {string} type - "head" if aligning your head and your avatar's head, "eyes" if aligning + * your eyes and your avatar's eyes. * @returns {Signal} */ void hmdAvatarAlignmentTypeChanged(const QString& type); /**jsdoc + * Triggered when the avatar's sensorToWorldScale property value changes. * @function MyAvatar.sensorToWorldScaleChanged - * @param {number} scale + * @param {number} scale - The scale that transforms dimensions in the user's real world to the avatar's size in the virtual + * world. * @returns {Signal} */ void sensorToWorldScaleChanged(float sensorToWorldScale); /**jsdoc + * Triggered when the a model is attached to or detached from one of the avatar's joints using one of + * {@link MyAvatar.attach|attach}, {@link MyAvatar.detachOne|detachOne}, {@link MyAvatar.detachAll|detachAll}, or + * {@link MyAvatar.setAttachmentData|setAttachmentData}. * @function MyAvatar.attachmentsChanged * @returns {Signal} + * @deprecated Use avatar entities instead. */ void attachmentsChanged(); /**jsdoc + * Triggered when the avatar's size changes. This can be due to the user changing the size of their avatar or the domain + * limiting the size of their avatar. * @function MyAvatar.scaleChanged * @returns {Signal} */ diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 11940ad76a..aedfcedf89 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -201,16 +201,24 @@ public: virtual QStringList getJointNames() const override; /**jsdoc + * Gets the default rotation of a joint (in the current avatar) relative to its parent. + *

For information on the joint hierarchy used, see + * Avatar Standards.

* @function MyAvatar.getDefaultJointRotation - * @param {number} index - * @returns {Quat} + * @param {number} index - The joint index. + * @returns {Quat} The default rotation of the joint if the joint index is valid, otherwise {@link Quat(0)|Quat.IDENTITY}. */ Q_INVOKABLE virtual glm::quat getDefaultJointRotation(int index) const; /**jsdoc + * Gets the default translation of a joint (in the current avatar) relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

For information on the joint hierarchy used, see + * Avatar Standards.

* @function MyAvatar.getDefaultJointTranslation - * @param {number} index - * @returns {Vec3} + * @param {number} index - The joint index. + * @returns {Vec3} The default translation of the joint (in model coordinates) if the joint index is valid, otherwise + * {@link Vec3(0)|Vec3.ZERO}. */ Q_INVOKABLE virtual glm::vec3 getDefaultJointTranslation(int index) const; @@ -261,50 +269,62 @@ public: // world-space to avatar-space rigconversion functions /**jsdoc + * Transforms a position in world coordinates to a position in a joint's coordinates, or avatar coordinates if no joint is + * specified. * @function MyAvatar.worldToJointPoint - * @param {Vec3} position - * @param {number} [jointIndex=-1] - * @returns {Vec3} + * @param {Vec3} position - The position in world coordinates. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The position in the joint's coordinate system, or avatar coordinate system if no joint is specified. */ Q_INVOKABLE glm::vec3 worldToJointPoint(const glm::vec3& position, const int jointIndex = -1) const; /**jsdoc + * Transforms a direction in world coordinates to a direction in a joint's coordinates, or avatar coordinates if no joint + * is specified. * @function MyAvatar.worldToJointDirection - * @param {Vec3} direction - * @param {number} [jointIndex=-1] - * @returns {Vec3} + * @param {Vec3} direction - The direction in world coordinates. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The direction in the joint's coordinate system, or avatar coordinate system if no joint is specified. */ Q_INVOKABLE glm::vec3 worldToJointDirection(const glm::vec3& direction, const int jointIndex = -1) const; /**jsdoc + * Transforms a rotation in world coordinates to a rotation in a joint's coordinates, or avatar coordinates if no joint is + * specified. * @function MyAvatar.worldToJointRotation - * @param {Quat} rotation - * @param {number} [jointIndex=-1] - * @returns {Quat} + * @param {Quat} rotation - The rotation in world coordinates. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Quat} The rotation in the joint's coordinate system, or avatar coordinate system if no joint is specified. */ Q_INVOKABLE glm::quat worldToJointRotation(const glm::quat& rotation, const int jointIndex = -1) const; /**jsdoc + * Transforms a position in a joint's coordinates, or avatar coordinates if no joint is specified, to a position in world + * coordinates. * @function MyAvatar.jointToWorldPoint - * @param {vec3} position - * @param {number} [jointIndex=-1] - * @returns {Vec3} + * @param {Vec3} position - The position in joint coordinates, or avatar coordinates if no joint is specified. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The position in world coordinates. */ Q_INVOKABLE glm::vec3 jointToWorldPoint(const glm::vec3& position, const int jointIndex = -1) const; /**jsdoc + * Transforms a direction in a joint's coordinates, or avatar coordinates if no joint is specified, to a direction in world + * coordinates. * @function MyAvatar.jointToWorldDirection - * @param {Vec3} direction - * @param {number} [jointIndex=-1] - * @returns {Vec3} + * @param {Vec3} direction - The direction in joint coordinates, or avatar coordinates if no joint is specified. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The direction in world coordinates. */ Q_INVOKABLE glm::vec3 jointToWorldDirection(const glm::vec3& direction, const int jointIndex = -1) const; /**jsdoc + * Transforms a rotation in a joint's coordinates, or avatar coordinates if no joint is specified, to a rotation in world + * coordinates. * @function MyAvatar.jointToWorldRotation - * @param {Quat} rotation - * @param {number} [jointIndex=-1] - * @returns {Quat} + * @param {Quat} rotation - The rotation in joint coordinates, or avatar coordinates if no joint is specified. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Quat} The rotation in world coordinates. */ Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; @@ -375,8 +395,9 @@ public: Q_INVOKABLE glm::vec3 getNeckPosition() const; /**jsdoc + * Gets the current acceleration of the avatar. * @function MyAvatar.getAcceleration - * @returns {Vec3} + * @returns {Vec3} The current acceleration of the avatar. */ Q_INVOKABLE glm::vec3 getAcceleration() const { return _acceleration; } @@ -410,29 +431,37 @@ public: void setOrientationViaScript(const glm::quat& orientation) override; /**jsdoc + * Gets the ID of the entity of avatar that the avatar is parented to. * @function MyAvatar.getParentID - * @returns {Uuid} + * @returns {Uuid} The ID of the entity or avatar that the avatar is parented to. {@link Uuid|Uuid.NULL} if not parented. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual const QUuid getParentID() const override { return SpatiallyNestable::getParentID(); } /**jsdoc + * Sets the ID of the entity of avatar that the avatar is parented to. * @function MyAvatar.setParentID - * @param {Uuid} parentID + * @param {Uuid} parentID - The ID of the entity or avatar that the avatar should be parented to. Set to + * {@link Uuid|Uuid.NULL} to unparent. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual void setParentID(const QUuid& parentID) override; /**jsdoc + * Gets the joint of the entity or avatar that the avatar is parented to. * @function MyAvatar.getParentJointIndex - * @returns {number} + * @returns {number} The joint of the entity or avatar that the avatar is parented to. 65535 or + * -1 if parented to the entity or avatar's position and orientation rather than a joint. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual quint16 getParentJointIndex() const override { return SpatiallyNestable::getParentJointIndex(); } /**jsdoc + * sets the joint of the entity or avatar that the avatar is parented to. * @function MyAvatar.setParentJointIndex - * @param {number} parentJointIndex + * @param {number} parentJointIndex - he joint of the entity or avatar that the avatar should be parented to. Use + * 65535 or -1 to parent to the entity or avatar's position and orientation rather than a + * joint. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex) override; @@ -466,8 +495,9 @@ public: /**jsdoc * @function MyAvatar.getSimulationRate - * @param {string} [rateName=""] - * @returns {number} + * @param {string} [rateName=""] - Rate name. + * @returns {number} Simulation rate. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE float getSimulationRate(const QString& rateName = QString("")) const; @@ -522,8 +552,10 @@ public: signals: /**jsdoc + * Triggered when the avatar's target scale is changed. The target scale is the desired scale of the avatar without any + * restrictions on permissible scale values imposed by the domain. * @function MyAvatar.targetScaleChanged - * @param {number} targetScale + * @param {number} targetScale - The avatar's target scale. * @returns Signal */ void targetScaleChanged(float targetScale); @@ -572,6 +604,7 @@ public slots: /**jsdoc * @function MyAvatar.setModelURLFinished * @param {boolean} success + * @deprecated This function is deprecated and will be removed. */ // hooked up to Model::setURLFinished signal void setModelURLFinished(bool success); diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 614858a77d..79085e05ba 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -1069,13 +1069,14 @@ void pickRayFromScriptValue(const QScriptValue& object, PickRay& pickRay) { } /**jsdoc + * Details of a collision between avatars and entities. * @typedef {object} Collision * @property {ContactEventType} type - The contact type of the collision event. - * @property {Uuid} idA - The ID of one of the entities in the collision. - * @property {Uuid} idB - The ID of the other of the entities in the collision. - * @property {Vec3} penetration - The amount of penetration between the two entities. + * @property {Uuid} idA - The ID of one of the avatars or entities in the collision. + * @property {Uuid} idB - The ID of the other of the avatars or entities in the collision. + * @property {Vec3} penetration - The amount of penetration between the two items. * @property {Vec3} contactPoint - The point of contact. - * @property {Vec3} velocityChange - The change in relative velocity of the two entities, in m/s. + * @property {Vec3} velocityChange - The change in relative velocity of the two items, in m/s. */ QScriptValue collisionToScriptValue(QScriptEngine* engine, const Collision& collision) { QScriptValue obj = engine->newObject(); From eba89e8a80ff0d49a3ef3350b079ee486ec42f69 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 8 Mar 2019 13:50:03 +1300 Subject: [PATCH 010/135] Revise Avatar JSDoc as a result of MyAvatar JSDoc work --- .../src/avatars/ScriptableAvatar.h | 4 +- libraries/avatars/src/AvatarData.h | 84 +++++++++++-------- 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 2d8dce23de..37e82947ae 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -149,7 +149,7 @@ public: * Gets the avatar entities as binary data. *

Warning: Potentially a very expensive call. Do not use if possible.

* @function Avatar.getAvatarEntityData - * @returns {AvatarEntityMap} + * @returns {AvatarEntityMap} The avatar entities as binary data. */ Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; @@ -157,7 +157,7 @@ public: * Sets the avatar entities from binary data. *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.setAvatarEntityData - * @param {AvatarEntityMap} avatarEntityData + * @param {AvatarEntityMap} avatarEntityData - The avatar entities as binary data. */ Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index a20518076f..858af7ba3c 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -131,7 +131,7 @@ const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit * * *

The values for the hand states are added together to give the HandState value. For example, if the left - * hand's finger is pointing, the value is 1 + 4 == 5. + * hand's finger is pointing, the value is 1 + 4 == 5. * @typedef {number} HandState */ const char HAND_STATE_NULL = 0; @@ -705,8 +705,9 @@ public: Q_INVOKABLE void setRawJointData(QVector data); /**jsdoc - * Sets a specific joint's rotation and position relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's rotation and position relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set @@ -714,7 +715,7 @@ public: * @function Avatar.setJointData * @param {number} index - The index of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. * @example Set your avatar to it's default T-pose for a while.
* Avatar in T-pose * // Set all joint translations and rotations to defaults. @@ -748,15 +749,16 @@ public: Q_INVOKABLE virtual void setJointRotation(int index, const glm::quat& rotation); /**jsdoc - * Sets a specific joint's translation relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's translation relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

* @function Avatar.setJointTranslation * @param {number} index - The index of the joint. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. */ Q_INVOKABLE virtual void setJointTranslation(int index, const glm::vec3& translation); @@ -773,7 +775,7 @@ public: * Checks that the data for a joint are valid. * @function Avatar.isJointDataValid * @param {number} index - The index of the joint. - * @returns {boolean} true if the joint data is valid, false if not. + * @returns {boolean} true if the joint data are valid, false if not. */ Q_INVOKABLE bool isJointDataValid(int index) const; @@ -787,17 +789,20 @@ public: Q_INVOKABLE virtual glm::quat getJointRotation(int index) const; /**jsdoc - * Gets the translation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Gets the translation of a joint relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

For information on the joint hierarchy used, see + * Avatar Standards.

* @function Avatar.getJointTranslation * @param {number} index - The index of the joint. - * @returns {Vec3} The translation of the joint relative to its parent. + * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. */ Q_INVOKABLE virtual glm::vec3 getJointTranslation(int index) const; /**jsdoc - * Sets a specific joint's rotation and position relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's rotation and position relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set @@ -805,7 +810,7 @@ public: * @function Avatar.setJointData * @param {string} name - The name of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. */ Q_INVOKABLE virtual void setJointData(const QString& name, const glm::quat& rotation, const glm::vec3& translation); @@ -843,20 +848,21 @@ public: Q_INVOKABLE virtual void setJointRotation(const QString& name, const glm::quat& rotation); /**jsdoc - * Sets a specific joint's translation relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's translation relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

* @function Avatar.setJointTranslation * @param {string} name - The name of the joint. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. * @example Stretch your avatar's neck. Depending on the avatar you are using, you will either see a gap between * the head and body or you will see the neck stretched.
* Avatar with neck stretched * // Stretch your avatar's neck. - * MyAvatar.setJointTranslation("Neck", { x: 0, y: 25, z: 0 }); + * MyAvatar.setJointTranslation("Neck", Vec3.multiply(2, MyAvatar.getJointTranslation("Neck"))); * * // Restore your avatar's neck after 5s. * Script.setTimeout(function () { @@ -874,10 +880,10 @@ public: * @function Avatar.clearJointData * @param {string} name - The name of the joint. * @example Offset and restore the position of your avatar's head. - * // Move your avatar's head up by 25cm from where it should be. - * MyAvatar.setJointTranslation("Neck", { x: 0, y: 0.25, z: 0 }); + * // Stretch your avatar's neck. + * MyAvatar.setJointTranslation("Neck", Vec3.multiply(2, MyAvatar.getJointTranslation("Neck"))); * - * // Restore your avatar's head to its default position after 5s. + * // Restore your avatar's neck after 5s. * Script.setTimeout(function () { * MyAvatar.clearJointData("Neck"); * }, 5000); @@ -890,7 +896,7 @@ public: * Checks that the data for a joint are valid. * @function Avatar.isJointDataValid * @param {string} name - The name of the joint. - * @returns {boolean} true if the joint data is valid, false if not. + * @returns {boolean} true if the joint data are valid, false if not. */ Q_INVOKABLE virtual bool isJointDataValid(const QString& name) const; @@ -908,11 +914,13 @@ public: Q_INVOKABLE virtual glm::quat getJointRotation(const QString& name) const; /**jsdoc - * Gets the translation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Gets the translation of a joint relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

For information on the joint hierarchy used, see + * Avatar Standards.

* @function Avatar.getJointTranslation * @param {number} name - The name of the joint. - * @returns {Vec3} The translation of the joint relative to its parent. + * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. * @example Report the translation of your avatar's hips joint. * print(JSON.stringify(MyAvatar.getJointRotation("Hips"))); * @@ -933,10 +941,13 @@ public: Q_INVOKABLE virtual QVector getJointRotations() const; /**jsdoc - * Gets the translations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. + * Gets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint, in + * model coordinates. + *

Warning: These coordinates are not necessarily in meters.

* @function Avatar.getJointTranslations - * @returns {Vec3[]} The translations of all joints relative to each's parent. The values are in the same order as the array - * returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. + * @returns {Vec3[]} The translations of all joints relative to each's parent, in model coordinates. The values are in the + * same order as the array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the + * Avatar API. */ Q_INVOKABLE virtual QVector getJointTranslations() const; @@ -979,15 +990,18 @@ public: Q_INVOKABLE virtual void setJointRotations(const QVector& jointRotations); /**jsdoc - * Sets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint. + * Sets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint, in + * model coordinates. + *

Warning: These coordinates are not necessarily in meters.

*

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

* @function Avatar.setJointTranslations - * @param {Vec3[]} translations - The translations for all joints in the avatar. The values are in the same order as the - * array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. + * @param {Vec3[]} translations - The translations for all joints in the avatar, in model coordinates. The values are in + * the same order as the array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the + * Avatar API. */ Q_INVOKABLE virtual void setJointTranslations(const QVector& jointTranslations); @@ -1271,12 +1285,12 @@ public: AABox getDefaultBubbleBox() const; /**jsdoc - * @comment Documented in derived classes' JSDoc. + * @comment Documented in derived classes' JSDoc because implementations are different. */ Q_INVOKABLE virtual AvatarEntityMap getAvatarEntityData() const; /**jsdoc - * @comment Documented in derived classes' JSDoc. + * @comment Documented in derived classes' JSDoc because implementations are different. */ Q_INVOKABLE virtual void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); @@ -1383,14 +1397,14 @@ signals: void displayNameChanged(); /**jsdoc - * Triggered when the avattr's sessionDisplayName property value changes. + * Triggered when the avatar's sessionDisplayName property value changes. * @function Avatar.sessionDisplayNameChanged * @returns {Signal} */ void sessionDisplayNameChanged(); /**jsdoc - * Triggered when the avatar's skeletonModelURL property value changes. + * Triggered when the avatar's model (i.e., skeletonModelURL property value) is changed. * @function Avatar.skeletonModelURLChanged * @returns {Signal} */ From 5068075645c8c1511d2cc99c94c78dec68f0f325 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 9 Mar 2019 07:42:55 +1300 Subject: [PATCH 011/135] Fill in MyAvatar animation JSDoc --- .../animation/src/AnimInverseKinematics.h | 41 ++++ libraries/animation/src/AnimOverlay.h | 27 +++ libraries/animation/src/IKTarget.h | 21 ++ libraries/animation/src/Rig.cpp | 211 ++++++++++++++++++ libraries/script-engine/src/ScriptEngine.h | 11 +- 5 files changed, 306 insertions(+), 5 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index 0136b7d125..d5a110ea76 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -59,6 +59,47 @@ public: float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; } + /**jsdoc + *

Specifies the initial conditions of the IK solver.

+ * + * + * + * + * + * + * + * + * + * + * + *
ValueName

Description
0RelaxToUnderPosesThis is a blend between PreviousSolution and + * UnderPoses: it is 15/16 PreviousSolution and 1/16 UnderPoses. This + * provides some of the benefits of using UnderPoses so that the underlying animation is still visible, + * while at the same time converging faster then using the UnderPoses only initial solution.
1RelaxToLimitCenterPosesThis is a blend between + * Previous Solution and LimitCenterPoses: it is 15/16 PreviousSolution and + * 1/16 LimitCenterPoses. This should converge quickly because it is close to the previous solution, but + * still provides the benefits of avoiding limb locking.
2PreviousSolutionThe IK system will begin to solve from the same position and + * orientations for each joint that was the result from the previous frame.
+ * Pros: because the end effectors typically do not move much from frame to frame, this is likely to converge quickly + * to a valid solution.
+ * Cons: If the previous solution resulted in an awkward or uncomfortable posture, the next frame will also be + * awkward and uncomfortable. It can also result in locked elbows and knees.
3UnderPosesThe IK occurs at one of the top-most layers, it has access to the + * full posture that was computed via canned animations and blends. We call this animated set of poses the "under + * pose". The under poses are what would be visible if IK was completely disabled. Using the under poses as the + * initial conditions of the CCD solve will cause some of the animated motion to be blended in to the result of the + * IK. This can result in very natural results, especially if there are only a few IK targets enabled. On the other + * hand, because the under poses might be quite far from the desired end effector, it can converge slowly in some + * cases, causing it to never reach the IK target in the allotted number of iterations. Also, in situations where all + * of the IK targets are being controlled by external sensors, sometimes starting from the under poses can cause + * awkward motions from the underlying animations to leak into the IK result.
4LimitCenterPosesThis pose is taken to be the center of all the joint + * constraints. This can prevent the IK solution from getting locked or stuck at a particular constraint. For + * example, if the arm is pointing straight outward from the body, as the end effector moves towards the body, at + * some point the elbow should bend to accommodate. However, because the CCD solver is stuck at a local maximum, it + * will not rotate the elbow, unless the initial conditions already has the elbow bent, which is the case for + * LimitCenterPoses. When all the IK targets are enabled, this result will provide a consistent starting + * point for each IK solve, hopefully resulting in a consistent, natural result.
+ * @typedef {number} MyAvatar.AnimIKSolutionSource + */ enum class SolutionSource { RelaxToUnderPoses = 0, RelaxToLimitCenterPoses, diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h index 70929bd4e4..623672143e 100644 --- a/libraries/animation/src/AnimOverlay.h +++ b/libraries/animation/src/AnimOverlay.h @@ -24,6 +24,33 @@ class AnimOverlay : public AnimNode { public: friend class AnimTests; + /**jsdoc + *

Specifies sets of joints.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ValueName

Description
0FullBodyBoneSetAll joints.
1UpperBodyBoneSetOnly the "Spine" joint and its children.
2LowerBodyBoneSetOnly the leg joints and their children.
3LeftArmBoneSetJoints that are children of the "LeftShoulder" joint.
4RightArmBoneSetJoints that are children of the "RightShoulder" + * joint.
5AboveTheHeadBoneSetJoints that are children of the "Head" joint.
6BelowTheHeadBoneSetJoints that are NOT children of the "head" + * joint.
7HeadOnlyBoneSetThe "Head" joint.
8SpineOnlyBoneSetThe "Spine" joint.
9EmptyBoneSetNo joints.
10LeftHandBoneSetjoints that are children of the "LeftHand" joint.
11RightHandBoneSetJoints that are children of the "RightHand" joint.
12HipsOnlyBoneSetThe "Hips" joint.
13BothFeetBoneSetThe "LeftFoot" and "RightFoot" joints.
+ * @typedef {number} MyAvatar.AnimOverlayBoneSet + */ enum BoneSet { FullBodyBoneSet = 0, UpperBodyBoneSet, diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 325a1b40b6..7e53e6a7ea 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -16,6 +16,27 @@ const float HACK_HMD_TARGET_WEIGHT = 8.0f; class IKTarget { public: + /**jsdoc + *

An IK target type.

+ * + * + * + * + * + * + * + * + * + * + * + * + *
ValueName

Description
0RotationAndPositionAttempt to reach the rotation and position end + * effector.
1RotationOnlyAttempt to reach the end effector rotation only.
2HmdHeadDeprecated: A special mode of IK that would attempt + * to prevent unnecessary bending of the spine.
3HipsRelativeRotationAndPositionAttempt to reach a rotation and position end + * effector that is not in absolute rig coordinates but is offset by the avatar hips translation.
4SplineUse a cubic Hermite spline to model the human spine. This prevents + * kinks in the spine and allows for a small amount of stretch and squash.
5UnknownIK is disabled.
+ * @typedef {number} MyAvatar.IKTargetType + */ enum class Type { RotationAndPosition, RotationOnly, diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index a9c57a4a15..1ab680fba2 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -75,6 +75,217 @@ static const QString MAIN_STATE_MACHINE_RIGHT_FOOT_ROTATION("mainStateMachineRig static const QString MAIN_STATE_MACHINE_RIGHT_FOOT_POSITION("mainStateMachineRightFootPosition"); +/**jsdoc + *

An AnimStateDictionary object may have the following properties. It may also have other properties, set by + * scripts.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + + * + * + * + * + * + * + * + * + * + + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
NameType

Description
userAnimNonebooleantrue when no user overrideAnimation is + * playing.
userAnimAbooleantrue when a user overrideAnimation is + * playing.
userAnimBbooleantrue when a user overrideAnimation is + * playing.
sinenumberOscillating sine wave.
moveForwardSpeednumberControls the blend between the various forward walking + * & running animations.
moveBackwardSpeednumberControls the blend between the various backward walking + * & running animations.
moveLateralSpeednumberControls the blend between the various sidestep walking + * & running animations.
isMovingForwardbooleantrue if the avatar is moving + * forward.
isMovingBackwardbooleantrue if the avatar is moving + * backward.
isMovingRightbooleantrue if the avatar is moving to the + * right.
isMovingLeftbooleantrue if the avatar is moving to the + * left.
isMovingRightHmdbooleantrue if the avatar is moving to the right + * while the user is in HMD mode.
isMovingLeftHmdbooleantrue if the avatar is moving to the left while + * the user is in HMD mode.
isNotMovingbooleantrue if the avatar is stationary.
isTurningRightbooleantrue if the avatar is turning + * clockwise.
isTurningLeftbooleantrue if the avatar is turning + * counter-clockwise.
isNotTurningbooleantrue if the avatar is not turning.
isFlyingbooleantrue if the avatar is flying.
isNotFlyingbooleantrue if the avatar is not flying.
isTakeoffStandbooleantrue if the avatar is about to execute a + * standing jump.
isTakeoffRunbooleantrue if the avatar is about to execute a running + * jump.
isNotTakeoffbooleantrue if the avatar is not jumping.
isInAirStandbooleantrue if the avatar is in the air after a standing + * jump.
isInAirRunbooleantrue if the avatar is in the air after a running + * jump.
isNotInAirbooleantrue if the avatar on the ground.
inAirAlphanumberUsed to interpolate between the up, apex, and down in-air + * animations.
ikOverlayAlphanumberThe blend between upper body and spline IK versus the + * underlying animation
headPosition{@link Vec3}The desired position of the Head joint in + * rig coordinates.
headRotation{@link Quat}The desired orientation of the Head joint in + * rig coordinates.
headType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * head.
headWeightnumberHow strongly the head chain blends with the other IK + * chains.
leftHandPosition{@link Vec3}The desired position of the LeftHand + * joint in rig coordinates.
leftHandRotation{@link Quat}The desired orientation of the LeftHand + * joint in rig coordinates.
leftHandType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * left arm.
leftHandPoleVectorEnabledbooleanWhen true, the elbow angle is + * controlled by the rightHandPoleVector property value. Otherwise the elbow direction comes from the + * underlying animation.
leftHandPoleReferenceVector{@link Vec3}The direction of the elbow in the local + * coordinate system of the elbow.
leftHandPoleVector{@link Vec3}The direction the elbow should point in rig + * coordinates.
rightHandPosition{@link Vec3}The desired position of the RightHand + * joint in rig coordinates.
rightHandRotation{@link Quat}The desired orientation of the + * RightHand joint in rig coordinates.
rightHandType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for + * the right arm.
rightHandPoleVectorEnabledbooleanWhen true, the elbow angle is + * controlled by the rightHandPoleVector property value. Otherwise the elbow direction comes from the + * underlying animation.
rightHandPoleReferenceVector{@link Vec3}The direction of the elbow in the local + * coordinate system of the elbow.
rightHandPoleVector{@link Vec3}The direction the elbow should point in rig + * coordinates.
leftFootIKEnabledbooleantrue if IK is enabled for the left + * foot.
rightFootIKEnabledbooleantrue if IK is enabled for the right + * foot.
leftFootIKPositionVarstringThe name of the source for the desired position + * of the LeftFoot joint. If not set, the foot rotation of the underlying animation will be used.
leftFootIKRotationVarstringThe name of the source for the desired rotation + * of the LeftFoot joint. If not set, the foot rotation of the underlying animation will be used.
leftFootPoleVectorEnabledbooleanWhen true, the knee angle is + * controlled by the leftFootPoleVector property value. Otherwise the knee direction comes from the + * underlying animation.
leftFootPoleVector{@link Vec3}The direction the knee should face in rig + * coordinates.
rightFootIKPositionVarstringThe name of the source for the desired position + * of the RightFoot joint. If not set, the foot rotation of the underlying animation will be used.
rightFootIKRotationVarstringThe name of the source for the desired rotation + * of the RightFoot joint. If not set, the foot rotation of the underlying animation will be used.
rightFootPoleVectorEnabledbooleanWhen true, the knee angle is + * controlled by the rightFootPoleVector property value. Otherwise the knee direction comes from the + * underlying animation.
rightFootPoleVector{@link Vec3}The direction the knee should face in rig + * coordinates.
isTalkingbooleantrue if the avatar is talking.
notIsTalkingbooleantrue if the avatar is not talking.
solutionSource{@link MyAvatar.AnimIKSolutionSource|AnimIKSolutionSource}Determines the initial conditions of the IK solver.
defaultPoseOverlayAlphanumberControls the blend between the main animation state + * machine and the default pose. Mostly used during full body tracking so that walking & jumping animations do not + * affect the IK of the figure.
defaultPoseOverlayBoneSet{@link MyAvatar.AnimOverlayBoneSet|AnimOverlayBoneSet}Specifies which bones will be replace by the source overlay.
hipsType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * hips.
hipsPosition{@link Vec3}The desired position of Hips joint in rig + * coordinates.
hipsRotation{@link Quat}the desired orientation of the Hips joint in + * rig coordinates.
spine2Type{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * Spine2 joint.
spine2Position{@link Vec3}The desired position of the Spine2 joint + * in rig coordinates.
spine2Rotation{@link Quat}The desired orientation of the Spine2 + * joint in rig coordinates.
leftFootIKAlphanumberBlends between full IK for the leg and the underlying + * animation.
rightFootIKAlphanumberBlends between full IK for the leg and the underlying + * animation.
hipsWeightnumberHow strongly the hips target blends with the IK solution for + * other IK chains.
leftHandWeightnumberHow strongly the left hand blends with IK solution of other + * IK chains.
rightHandWeightnumberHow strongly the right hand blends with IK solution of other + * IK chains.
spine2WeightnumberHow strongly the spine2 chain blends with the rest of the IK + * solution.
leftHandOverlayAlphanumberUsed to blend in the animated hand gesture poses, such + * as point and thumbs up.
leftHandGraspAlphanumberUsed to blend between an open hand and a closed hand. + * Usually changed as you squeeze the trigger of the hand controller.
rightHandOverlayAlphanumberUsed to blend in the animated hand gesture poses, + * such as point and thumbs up.
rightHandGraspAlphanumberUsed to blend between an open hand and a closed hand. + * Usually changed as you squeeze the trigger of the hand controller.
isLeftIndexPointbooleantrue if the left hand should be + * pointing.
isLeftThumbRaisebooleantrue if the left hand should be + * thumbs-up.
isLeftIndexPointAndThumbRaisebooleantrue if the left hand should be + * pointing and thumbs-up.
isLeftHandGraspbooleantrue if the left hand should be at rest, + * grasping the controller.
isRightIndexPointbooleantrue if the right hand should be + * pointing.
isRightThumbRaisebooleantrue if the right hand should be + * thumbs-up.
isRightIndexPointAndThumbRaisebooleantrue if the right hand should + * be pointing and thumbs-up.
isRightHandGraspbooleantrue if the right hand should be at rest, + * grasping the controller.
+ *

Note: Rig coordinates are +z forward and +y up.

+ * @typedef {object} MyAvatar.AnimStateDictionary + */ +// Note: The following animVars are intentionally not documented: +// - leftFootPosition +// - leftFootRotation +// - rightFooKPosition +// - rightFooKRotation +// Note: The following items aren't set in the code below but are still intentionally documented: +// - leftFootIKAlpha +// - rightFootIKAlpha +// - hipsWeight +// - leftHandWeight +// - rightHandWeight +// - spine2Weight +// - rightHandOverlayAlpha +// - rightHandGraspAlpha +// - leftHandOverlayAlpha +// - leftHandGraspAlpha +// - isRightIndexPoint +// - isRightThumbRaise +// - isRightIndexPointAndThumbRaise +// - isRightHandGrasp +// - isLeftIndexPoint +// - isLeftThumbRaise +// - isLeftIndexPointAndThumbRaise +// - isLeftHandGrasp Rig::Rig() { // Ensure thread-safe access to the rigRegistry. std::lock_guard guard(rigRegistryMutex); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 48fb4f0b83..3549578ed5 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -573,11 +573,12 @@ public slots: /**jsdoc * @function Script.callAnimationStateHandler - * @param {function} callback - * @param {object} parameters - * @param {string[]} names - * @param {boolean} useNames - * @param {object} resultHandler + * @param {function} callback - Callback. + * @param {object} parameters - Parameters. + * @param {string[]} names - Names. + * @param {boolean} useNames - Use names. + * @param {function} resultHandler - Result handler. + * @deprecated This function is deprecated and will be removed. */ void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler); From 24c7c8be190cd605f2531a6f2353e44aefb747e6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 9 Mar 2019 12:03:59 +1300 Subject: [PATCH 012/135] Update JSDoc per merge from master --- interface/src/avatar/MyAvatar.cpp | 18 ++++++++++++++++++ interface/src/avatar/MyAvatar.h | 21 ++++++++++++--------- libraries/animation/src/Rig.cpp | 15 +++++++++++---- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3db1228796..710a0550c5 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5358,6 +5358,24 @@ void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) } } +/**jsdoc + * Physics options to use in the flow simulation of a joint. + * @typedef {object} MyAvatar.FlowPhysicsOptions + * @property {boolean} [active=true] - true to enable flow on the joint, false if it isn't., + * @property {number} [radius=0.01] - The thickness of segments and knots. (Needed for collisions.) + * @property {number} [gravity=-0.0096] - Y-value of the gravity vector. + * @property {number} [inertia=0.8] - Rotational inertia multiplier. + * @property {number} [damping=0.85] - The amount of damping on joint oscillation. + * @property {number} [stiffness=0.0] - How stiff each thread is. + * @property {number} [delta=0.55] - Delta time for every integration step. + */ +/**jsdoc + * Collision options to use in the flow simulation of a joint. + * @typedef {object} MyAvatar.FlowCollisionsOptions + * @property {string} [type="sphere"] - Currently, only "sphere" is supported. + * @property {number} [radius=0.05] - Collision sphere radius. + * @property {number} [offset=Vec3.ZERO] - Offset of the collision sphere from the joint. + */ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig, const QVariantMap& collisionsConfig) { if (_skeletonModel->isLoaded()) { _skeletonModel->getRig().initFlow(isActive); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 5e049c7a02..1c44db703f 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1548,15 +1548,18 @@ public: void addAvatarHandsToFlow(const std::shared_ptr& otherAvatar); /**jsdoc - * Init flow simulation on avatar. - * @function MyAvatar.useFlow - * @param {boolean} - Set to true to activate flow simulation. - * @param {boolean} - Set to true to activate collisions. - * @param {Object} physicsConfig - object with the customized physic parameters - * i.e. {"hair": {"active": true, "stiffness": 0.0, "radius": 0.04, "gravity": -0.035, "damping": 0.8, "inertia": 0.8, "delta": 0.35}} - * @param {Object} collisionsConfig - object with the customized collision parameters - * i.e. {"Spine2": {"type": "sphere", "radius": 0.14, "offset": {"x": 0.0, "y": 0.2, "z": 0.0}}} - */ + * Enables and disables flow simulation of physics on the avatar's hair, clothes, and body parts. See + * {@link https://docs.highfidelity.com/create/avatars/create-avatars/add-flow.html|Add Flow to Your Avatar} for more + * information. + * @function MyAvatar.useFlow + * @param {boolean} isActive - true if flow simulation is enabled on the joint, false if it isn't. + * @param {boolean} isCollidable - true to enable collisions in the flow simulation, false to + * disable. + * @param {Object} [physicsConfig>] - Physic configurations for particular entity + * and avatar joints. + * @param {Object} [collisionsConfig] - Collision configurations for particular + * entity and avatar joints. + */ Q_INVOKABLE void useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig = QVariantMap(), const QVariantMap& collisionsConfig = QVariantMap()); public slots: diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 2a4c2326db..e1ee134530 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -170,8 +170,13 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig * coordinate system of the elbow. * leftHandPoleVector{@link Vec3}The direction the elbow should point in rig * coordinates. - - * rightHandPosition{@link Vec3}The desired position of the RightHand + * + * leftHandIKEnabledbooleantrue if IK is enabled for the left + * hand. + * rightHandIKEnabledbooleantrue if IK is enabled for the right + * hand. + * + * rightHandPosition{@link Vec3}The desired position of the RightHand * joint in rig coordinates. * rightHandRotation{@link Quat}The desired orientation of the * RightHand joint in rig coordinates. @@ -189,7 +194,7 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig * foot. * rightFootIKEnabledbooleantrue if IK is enabled for the right * foot. - + * * leftFootIKPositionVarstringThe name of the source for the desired position * of the LeftFoot joint. If not set, the foot rotation of the underlying animation will be used. * leftFootIKRotationVarstringThe name of the source for the desired rotation @@ -199,7 +204,7 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig * underlying animation. * leftFootPoleVector{@link Vec3}The direction the knee should face in rig * coordinates. - * rightFootIKPositionVarstringThe name of the source for the desired position + * rightFootIKPositionVarstringThe name of the source for the desired position * of the RightFoot joint. If not set, the foot rotation of the underlying animation will be used. * rightFootIKRotationVarstringThe name of the source for the desired rotation * of the RightFoot joint. If not set, the foot rotation of the underlying animation will be used. @@ -209,6 +214,8 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig * rightFootPoleVector{@link Vec3}The direction the knee should face in rig * coordinates. * + * splineIKEnabledbooleantrue if IK is enabled for the spline. + * * isTalkingbooleantrue if the avatar is talking. * notIsTalkingbooleantrue if the avatar is not talking. * From 24286273b402d83009fa7387900514d68e6e73ef Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 9 Mar 2019 19:22:07 +1300 Subject: [PATCH 013/135] Miscellaneous JSDoc fixes --- interface/src/ui/overlays/Overlays.cpp | 48 +++++++++++++------------- libraries/avatars/src/AvatarData.h | 2 +- libraries/shared/src/BillboardMode.h | 8 +++-- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 1bcb040a77..eec6eddf44 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -1711,9 +1711,9 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if * parentID is an avatar skeleton. A value of 65535 means "no joint". * - * @property {boolean} isFacingAvatar - If true< / code>, the overlay is rotated to face the user's camera about an axis + * @property {boolean} isFacingAvatar - If true, the overlay is rotated to face the user's camera about an axis * parallel to the user's avatar's "up" direction. - * @property {string} text="" - The text to display.Text does not automatically wrap; use \n< / code> for a line break. + * @property {string} text="" - The text to display.Text does not automatically wrap; use \n for a line break. * @property {number} textAlpha=1 - The text alpha value. * @property {Color} backgroundColor=0,0,0 - The background color. * @property {number} backgroundAlpha=0.7 - The background alpha value. @@ -1929,39 +1929,39 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * * @property {number} startAt = 0 - The counter - clockwise angle from the overlay's x-axis that drawing starts at, in degrees. * @property {number} endAt = 360 - The counter - clockwise angle from the overlay's x-axis that drawing ends at, in degrees. - * @property {number} outerRadius = 1 - The outer radius of the overlay, in meters.Synonym: radius< / code>. + * @property {number} outerRadius = 1 - The outer radius of the overlay, in meters.Synonym: radius. * @property {number} innerRadius = 0 - The inner radius of the overlay, in meters. * @property {Color} color = 255, 255, 255 - The color of the overlay.Setting this value also sets the values of - * innerStartColor< / code>, innerEndColor< / code>, outerStartColor< / code>, and outerEndColor< / code>. - * @property {Color} startColor - Sets the values of innerStartColor< / code> and outerStartColor< / code>. - * Write - only.< / em> - * @property {Color} endColor - Sets the values of innerEndColor< / code> and outerEndColor< / code>. - * Write - only.< / em> - * @property {Color} innerColor - Sets the values of innerStartColor< / code> and innerEndColor< / code>. - * Write - only.< / em> - * @property {Color} outerColor - Sets the values of outerStartColor< / code> and outerEndColor< / code>. - * Write - only.< / em> + * innerStartColor, innerEndColor, outerStartColor, and outerEndColor. + * @property {Color} startColor - Sets the values of innerStartColor and outerStartColor. + * Write - only. + * @property {Color} endColor - Sets the values of innerEndColor and outerEndColor. + * Write - only. + * @property {Color} innerColor - Sets the values of innerStartColor and innerEndColor. + * Write - only. + * @property {Color} outerColor - Sets the values of outerStartColor and outerEndColor. + * Write - only. * @property {Color} innerStartcolor - The color at the inner start point of the overlay. * @property {Color} innerEndColor - The color at the inner end point of the overlay. * @property {Color} outerStartColor - The color at the outer start point of the overlay. * @property {Color} outerEndColor - The color at the outer end point of the overlay. - * @property {number} alpha = 0.5 - The opacity of the overlay, 0.0< / code> -1.0< / code>.Setting this value also sets - * the values of innerStartAlpha< / code>, innerEndAlpha< / code>, outerStartAlpha< / code>, and - * outerEndAlpha< / code>.Synonym: Alpha< / code>; write - only< / em>. - * @property {number} startAlpha - Sets the values of innerStartAlpha< / code> and outerStartAlpha< / code>. - * Write - only.< / em> - * @property {number} endAlpha - Sets the values of innerEndAlpha< / code> and outerEndAlpha< / code>. - * Write - only.< / em> - * @property {number} innerAlpha - Sets the values of innerStartAlpha< / code> and innerEndAlpha< / code>. - * Write - only.< / em> - * @property {number} outerAlpha - Sets the values of outerStartAlpha< / code> and outerEndAlpha< / code>. - * Write - only.< / em> + * @property {number} alpha = 0.5 - The opacity of the overlay, 0.0 -1.0.Setting this value also sets + * the values of innerStartAlpha, innerEndAlpha, outerStartAlpha, and + * outerEndAlpha.Synonym: Alpha; write - only. + * @property {number} startAlpha - Sets the values of innerStartAlpha and outerStartAlpha. + * Write - only. + * @property {number} endAlpha - Sets the values of innerEndAlpha and outerEndAlpha. + * Write - only. + * @property {number} innerAlpha - Sets the values of innerStartAlpha and innerEndAlpha. + * Write - only. + * @property {number} outerAlpha - Sets the values of outerStartAlpha and outerEndAlpha. + * Write - only. * @property {number} innerStartAlpha = 0 - The alpha at the inner start point of the overlay. * @property {number} innerEndAlpha = 0 - The alpha at the inner end point of the overlay. * @property {number} outerStartAlpha = 0 - The alpha at the outer start point of the overlay. * @property {number} outerEndAlpha = 0 - The alpha at the outer end point of the overlay. * - * @property {boolean} hasTickMarks = false - If true< / code>, tick marks are drawn. + * @property {boolean} hasTickMarks = false - If true, tick marks are drawn. * @property {number} majorTickMarksAngle = 0 - The angle between major tick marks, in degrees. * @property {number} minorTickMarksAngle = 0 - The angle between minor tick marks, in degrees. * @property {number} majorTickMarksLength = 0 - The length of the major tick marks, in meters.A positive value draws tick marks diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 858af7ba3c..e0437cfeb1 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1034,7 +1034,7 @@ public: * @param {string} name - The name of the joint. * @returns {number} The index of the joint if valid, otherwise -1. * @example Report the index of your avatar's left arm joint. - * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm")); + * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm"))); * * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ diff --git a/libraries/shared/src/BillboardMode.h b/libraries/shared/src/BillboardMode.h index 050f939941..700127aff1 100644 --- a/libraries/shared/src/BillboardMode.h +++ b/libraries/shared/src/BillboardMode.h @@ -18,9 +18,11 @@ * ValueDescription * * - * noneThe entity will not be billboarded. - * yawThe entity will yaw, but not pitch, to face the camera. Its actual rotation will be ignored. - * fullThe entity will be billboarded to face the camera. Its actual rotation will be ignored. + * "none"The entity will not be billboarded. + * "yaw"The entity will yaw, but not pitch, to face the camera. Its actual rotation will be + * ignored. + * "full"The entity will be billboarded to face the camera. Its actual rotation will be + * ignored. * * * @typedef {string} BillboardMode From 6032fde5e55fb6688f43f701fefcb20b132052d2 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 12:43:34 -0700 Subject: [PATCH 014/135] Unique command line to run Interface on Quest. --- tools/nitpick/src/Nitpick.cpp | 2 +- tools/nitpick/src/TestRunnerMobile.cpp | 28 +++++++++++++++++--------- tools/nitpick/src/TestRunnerMobile.h | 2 ++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index cf50774617..e72de9d1ad 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.2"); + setWindowTitle("Nitpick - v3.1.3"); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 4d0d18ef3d..284b4de15c 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -98,12 +98,12 @@ void TestRunnerMobile::connectDevice() { QString deviceID = tokens[0]; QString modelID = tokens[3].split(':')[1]; - QString modelName = "UNKNOWN"; + _modelName = "UNKNOWN"; if (modelNames.count(modelID) == 1) { - modelName = modelNames[modelID]; + _modelName = modelNames[modelID]; } - _detectedDeviceLabel->setText(modelName + " [" + deviceID + "]"); + _detectedDeviceLabel->setText(_modelName + " [" + deviceID + "]"); _pullFolderButton->setEnabled(true); _folderLineEdit->setEnabled(true); _downloadAPKPushbutton->setEnabled(true); @@ -198,14 +198,22 @@ void TestRunnerMobile::runInterface() { ? QString("https://raw.githubusercontent.com/") + nitpick->getSelectedUser() + "/hifi_tests/" + nitpick->getSelectedBranch() + "/tests/testRecursive.js" : _scriptURL->text(); + // Quest and Android have different commands to run interface + QString startCommand; + if (_modelName == "Quest") { + startCommand = "io.highfidelity.questInterface/.PermissionsChecker"; + } else { + startCommand = "io.highfidelity.hifiinterface/.PermissionChecker"; + } + QString command = _adbInterface->getAdbCommand() + - " shell am start -n io.highfidelity.hifiinterface/.PermissionChecker" + - " --es args \\\"" + - " --url file:///~/serverless/tutorial.json" + - " --no-updater" + - " --no-login-suggestion" + - " --testScript " + testScript + " quitWhenFinished" + - " --testResultsLocation /sdcard/snapshots" + + " shell am start -n " + startCommand + + " --es args \\\"" + + " --url file:///~/serverless/tutorial.json" + + " --no-updater" + + " --no-login-suggestion" + + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation /sdcard/snapshots" + "\\\""; appendLog(command); diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index f7b16da6f8..7dbf5456b3 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -75,5 +75,7 @@ private: std::map modelNames; AdbInterface* _adbInterface; + + QString _modelName; }; #endif From 577ff9695fb1e59cec27e96b16ca89a0bb9e9433 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 13:53:19 -0700 Subject: [PATCH 015/135] TEST!!! --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6d9a1823a1..3d6a26d7e3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1795,7 +1795,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) +#if !defined(DISABLE_QML) // Do not show login dialog if requested not to on the command line QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); int index = arguments().indexOf(hifiNoLoginCommandLineKey); From 7c7e632589eb25608c82a830ebaf9931672d32e4 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 11 Mar 2019 17:50:20 -0700 Subject: [PATCH 016/135] debug statements to find the node parsing error --- libraries/animation/src/AnimClip.cpp | 2 +- libraries/fbx/src/FBXSerializer.cpp | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 4fe02e9307..5d846a8f84 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -154,7 +154,7 @@ void AnimClip::copyFromNetworkAnim() { const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); - + qCDebug(animation) << " avatar unit scale " << avatarUnitScale << " animation unit scale " << animationUnitScale << " avatar height " << avatarHeightInMeters << " animation height " << animHeightInMeters << " avatar scale " << avatarHipsParentScale << " animation scale " << animHipsParentScale; boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; } diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 5246242a1e..e6e3d73815 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -142,6 +142,7 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen visitedNodes.append(nodeID); // Append each node we visit const FBXModel& fbxModel = fbxModels.value(nodeID); + qCDebug(modelformat) << "this fbx model name is " << fbxModel.name; globalTransform = glm::translate(fbxModel.translation) * fbxModel.preTransform * glm::mat4_cast(fbxModel.preRotation * fbxModel.rotation * fbxModel.postRotation) * fbxModel.postTransform * globalTransform; if (fbxModel.hasGeometricOffset) { @@ -201,13 +202,23 @@ public: void appendModelIDs(const QString& parentID, const QMultiMap& connectionChildMap, QHash& fbxModels, QSet& remainingModels, QVector& modelIDs, bool isRootNode = false) { if (remainingModels.contains(parentID)) { + qCDebug(modelformat) << " remaining models contains parent " << parentID; modelIDs.append(parentID); remainingModels.remove(parentID); } - int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; + int parentIndex = 1000; + if (isRootNode) { + qCDebug(modelformat) << " found a root node " << parentID; + parentIndex = -1; + } else { + parentIndex = modelIDs.size() - 1; + } + //int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; foreach (const QString& childID, connectionChildMap.values(parentID)) { + qCDebug(modelformat) << " searching children, parent id " << parentID; if (remainingModels.contains(childID)) { FBXModel& fbxModel = fbxModels[childID]; + qCDebug(modelformat) << " child id " << fbxModel.name; if (fbxModel.parentIndex == -1) { fbxModel.parentIndex = parentIndex; appendModelIDs(childID, connectionChildMap, fbxModels, remainingModels, modelIDs); @@ -441,6 +452,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; + int nodeParentId = -1; foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { @@ -497,6 +509,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (child.name == "Objects") { foreach (const FBXNode& object, child.children) { + nodeParentId++; if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { meshes.insert(getID(object.properties), extractMesh(object, meshIndex)); @@ -505,6 +518,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr blendshapes.append(extracted); } } else if (object.name == "Model") { + qCDebug(modelformat) << "model name from object properties " << getName(object.properties) << " node parentID " << nodeParentId; QString name = getName(object.properties); QString id = getID(object.properties); modelIDsToNames.insert(id, name); @@ -1181,6 +1195,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr #endif } + // TODO: check if is code is needed if (!lights.empty()) { if (hifiGlobalNodeID.isEmpty()) { @@ -1211,6 +1226,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr for (QHash::const_iterator fbxModel = fbxModels.constBegin(); fbxModel != fbxModels.constEnd(); fbxModel++) { // models with clusters must be parented to the cluster top // Unless the model is a root node. + qCDebug(modelformat) << "fbx model name " << fbxModel.key(); bool isARootNode = !modelIDs.contains(_connectionParentMap.value(fbxModel.key())); if (!isARootNode) { foreach(const QString& deformerID, _connectionChildMap.values(fbxModel.key())) { @@ -1266,6 +1282,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.parentIndex = fbxModel.parentIndex; int jointIndex = hfmModel.joints.size(); + qCDebug(modelformat) << "fbx joint name " << fbxModel.name << " joint index " << jointIndex << " parent index " << joint.parentIndex; + joint.translation = fbxModel.translation; // these are usually in centimeters joint.preTransform = fbxModel.preTransform; joint.preRotation = fbxModel.preRotation; From 065897a8f341ee38058c155a1626daa8982c9132 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 18:56:23 -0700 Subject: [PATCH 017/135] TEST!!! --- interface/src/Application.cpp | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3d6a26d7e3..852c4eb695 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1795,19 +1795,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(DISABLE_QML) - // Do not show login dialog if requested not to on the command line - QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); - int index = arguments().indexOf(hifiNoLoginCommandLineKey); - if (index != -1) { - resumeAfterLoginDialogActionTaken(); - return; - } - - showLoginScreen(); -#else - resumeAfterLoginDialogActionTaken(); -#endif + resumeAfterLoginDialogActionTaken(); }); // Make sure we don't time out during slow operations at startup From 80168058f7a427b49a87a0bdbbd68e595e429629 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 20:22:07 -0700 Subject: [PATCH 018/135] Correct APK installation. --- tools/nitpick/src/TestRunnerMobile.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 284b4de15c..eaebb6ca5a 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -164,22 +164,20 @@ void TestRunnerMobile::installAPK() { _adbInterface = new AdbInterface(); } - if (_installerFilename.isNull()) { - QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, - "Available APKs (*.apk)" - ); + QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, + "Available APKs (*.apk)" + ); - if (installerPathname.isNull()) { - return; - } - - // Remove the path - QStringList parts = installerPathname.split('/'); - _installerFilename = parts[parts.length() - 1]; + if (installerPathname.isNull()) { + return; } + // Remove the path + QStringList parts = installerPathname.split('/'); + _installerFilename = parts[parts.length() - 1]; + _statusLabel->setText("Installing"); - QString command = _adbInterface->getAdbCommand() + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; + QString command = _adbInterface->getAdbCommand() + " install -r -d " + installerPathname + " >" + _workingFolder + "/installOutput.txt"; appendLog(command); system(command.toStdString().c_str()); _statusLabel->setText("Installation complete"); From 1a9f33ef660228e0e947cc8bc443b9655640d046 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 20:24:34 -0700 Subject: [PATCH 019/135] Removed test. --- interface/src/Application - Copy.cpp | 9191 ++++++++++++++++++++++++++ interface/src/Application.cpp | 14 +- 2 files changed, 9204 insertions(+), 1 deletion(-) create mode 100644 interface/src/Application - Copy.cpp diff --git a/interface/src/Application - Copy.cpp b/interface/src/Application - Copy.cpp new file mode 100644 index 0000000000..ca8883f660 --- /dev/null +++ b/interface/src/Application - Copy.cpp @@ -0,0 +1,9191 @@ +// +// Application.cpp +// interface/src +// +// Created by Andrzej Kapolka on 5/10/13. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#include "Application.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ui/overlays/ContextOverlayInterface.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "LocationBookmarks.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "recording/ClipCache.h" + +#include "AudioClient.h" +#include "audio/AudioScope.h" +#include "avatar/AvatarManager.h" +#include "avatar/MyHead.h" +#include "avatar/AvatarPackager.h" +#include "avatar/MyCharacterController.h" +#include "CrashRecoveryHandler.h" +#include "CrashHandler.h" +#include "devices/DdeFaceTracker.h" +#include "DiscoverabilityManager.h" +#include "GLCanvas.h" +#include "InterfaceDynamicFactory.h" +#include "InterfaceLogging.h" +#include "LODManager.h" +#include "ModelPackager.h" +#include "scripting/Audio.h" +#include "networking/CloseEventSender.h" +#include "scripting/TestScriptingInterface.h" +#include "scripting/PlatformInfoScriptingInterface.h" +#include "scripting/AssetMappingsScriptingInterface.h" +#include "scripting/ClipboardScriptingInterface.h" +#include "scripting/DesktopScriptingInterface.h" +#include "scripting/AccountServicesScriptingInterface.h" +#include "scripting/HMDScriptingInterface.h" +#include "scripting/MenuScriptingInterface.h" +#include "graphics-scripting/GraphicsScriptingInterface.h" +#include "scripting/SettingsScriptingInterface.h" +#include "scripting/WindowScriptingInterface.h" +#include "scripting/ControllerScriptingInterface.h" +#include "scripting/RatesScriptingInterface.h" +#include "scripting/SelectionScriptingInterface.h" +#include "scripting/WalletScriptingInterface.h" +#include "scripting/TTSScriptingInterface.h" +#include "scripting/KeyboardScriptingInterface.h" + + + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#include "SpeechRecognizer.h" +#endif +#include "ui/ResourceImageItem.h" +#include "ui/AddressBarDialog.h" +#include "ui/AvatarInputs.h" +#include "ui/DialogsManager.h" +#include "ui/LoginDialog.h" +#include "ui/Snapshot.h" +#include "ui/SnapshotAnimated.h" +#include "ui/StandAloneJSConsole.h" +#include "ui/Stats.h" +#include "ui/AnimStats.h" +#include "ui/UpdateDialog.h" +#include "ui/DomainConnectionModel.h" +#include "ui/Keyboard.h" +#include "Util.h" +#include "InterfaceParentFinder.h" +#include "ui/OctreeStatsProvider.h" + +#include "avatar/GrabManager.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "commerce/Ledger.h" +#include "commerce/Wallet.h" +#include "commerce/QmlCommerce.h" +#include "commerce/QmlMarketplace.h" +#include "ResourceRequestObserver.h" + +#include "webbrowser/WebBrowserSuggestionsEngine.h" +#include + + +#include "AboutUtil.h" + +#if defined(Q_OS_WIN) +#include + +#ifdef DEBUG_EVENT_QUEUE +// This is a HACK that uses private headers included with the qt source distrubution. +// To use this feature you need to add these directores to your include path: +// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1/QtCore +// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1 +#define QT_BOOTSTRAPPED +#include +#include +#undef QT_BOOTSTRAPPED +#endif + +// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU +// FIXME seems to be broken. +extern "C" { + _declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; +} +#endif + +#if defined(Q_OS_ANDROID) +#include +#include "AndroidHelper.h" +#endif + +#include "graphics/RenderEventHandler.h" + +Q_LOGGING_CATEGORY(trace_app_input_mouse, "trace.app.input.mouse") + +using namespace std; + +static QTimer locationUpdateTimer; +static QTimer identityPacketTimer; +static QTimer pingTimer; + +#if defined(Q_OS_ANDROID) +static bool DISABLE_WATCHDOG = true; +#else +static const QString DISABLE_WATCHDOG_FLAG{ "HIFI_DISABLE_WATCHDOG" }; +static bool DISABLE_WATCHDOG = nsightActive() || QProcessEnvironment::systemEnvironment().contains(DISABLE_WATCHDOG_FLAG); +#endif + +#if defined(USE_GLES) +static bool DISABLE_DEFERRED = true; +#else +static const QString RENDER_FORWARD{ "HIFI_RENDER_FORWARD" }; +static bool DISABLE_DEFERRED = QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); +#endif + +#if !defined(Q_OS_ANDROID) +static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; +#else +static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 4; +#endif + +// For processing on QThreadPool, we target a number of threads after reserving some +// based on how many are being consumed by the application and the display plugin. However, +// we will never drop below the 'min' value +static const int MIN_PROCESSING_THREAD_POOL_SIZE = 1; + +static const QString SNAPSHOT_EXTENSION = ".jpg"; +static const QString JPG_EXTENSION = ".jpg"; +static const QString PNG_EXTENSION = ".png"; +static const QString SVO_EXTENSION = ".svo"; +static const QString SVO_JSON_EXTENSION = ".svo.json"; +static const QString JSON_GZ_EXTENSION = ".json.gz"; +static const QString JSON_EXTENSION = ".json"; +static const QString JS_EXTENSION = ".js"; +static const QString FST_EXTENSION = ".fst"; +static const QString FBX_EXTENSION = ".fbx"; +static const QString OBJ_EXTENSION = ".obj"; +static const QString AVA_JSON_EXTENSION = ".ava.json"; +static const QString WEB_VIEW_TAG = "noDownload=true"; +static const QString ZIP_EXTENSION = ".zip"; +static const QString CONTENT_ZIP_EXTENSION = ".content.zip"; + +static const float MIRROR_FULLSCREEN_DISTANCE = 0.789f; + +static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; + +static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; +static const QString INFO_HELP_PATH = "html/tabletHelp.html"; + +static const unsigned int THROTTLED_SIM_FRAMERATE = 15; +static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; +static const int ENTITY_SERVER_ADDED_TIMEOUT = 5000; +static const int ENTITY_SERVER_CONNECTION_TIMEOUT = 5000; + +static const float INITIAL_QUERY_RADIUS = 10.0f; // priority radius for entities before physics enabled + +static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + +Setting::Handle maxOctreePacketsPerSecond{"maxOctreePPS", DEFAULT_MAX_OCTREE_PPS}; + +Setting::Handle loginDialogPoppedUp{"loginDialogPoppedUp", false}; + +static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; +static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; +static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; + +static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; +static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds +static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; +static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; +static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; +static const QString KEEP_ME_LOGGED_IN_SETTING_NAME = "keepMeLoggedIn"; + +static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f; + +#if defined(Q_OS_ANDROID) +static const QString TESTER_FILE = "/sdcard/_hifi_test_device.txt"; +#endif +const std::vector> Application::_acceptedExtensions { + { SVO_EXTENSION, &Application::importSVOFromURL }, + { SVO_JSON_EXTENSION, &Application::importSVOFromURL }, + { AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl }, + { JSON_EXTENSION, &Application::importJSONFromURL }, + { JS_EXTENSION, &Application::askToLoadScript }, + { FST_EXTENSION, &Application::askToSetAvatarUrl }, + { JSON_GZ_EXTENSION, &Application::askToReplaceDomainContent }, + { CONTENT_ZIP_EXTENSION, &Application::askToReplaceDomainContent }, + { ZIP_EXTENSION, &Application::importFromZIP }, + { JPG_EXTENSION, &Application::importImage }, + { PNG_EXTENSION, &Application::importImage } +}; + +class DeadlockWatchdogThread : public QThread { +public: + static const unsigned long HEARTBEAT_UPDATE_INTERVAL_SECS = 1; + static const unsigned long MAX_HEARTBEAT_AGE_USECS = 120 * USECS_PER_SECOND; // 2 mins with no checkin probably a deadlock + static const int WARNING_ELAPSED_HEARTBEAT = 500 * USECS_PER_MSEC; // warn if elapsed heartbeat average is large + static const int HEARTBEAT_SAMPLES = 100000; // ~5 seconds worth of samples + + // Set the heartbeat on launch + DeadlockWatchdogThread() { + setObjectName("Deadlock Watchdog"); + // Give the heartbeat an initial value + _heartbeat = usecTimestampNow(); + _paused = false; + connect(qApp, &QCoreApplication::aboutToQuit, [this] { + _quit = true; + }); + } + + void setMainThreadID(Qt::HANDLE threadID) { + _mainThreadID = threadID; + } + + static void updateHeartbeat() { + auto now = usecTimestampNow(); + auto elapsed = now - _heartbeat; + _movingAverage.addSample(elapsed); + _heartbeat = now; + } + + void deadlockDetectionCrash() { + setCrashAnnotation("_mod_faulting_tid", std::to_string((uint64_t)_mainThreadID)); + setCrashAnnotation("deadlock", "1"); + uint32_t* crashTrigger = nullptr; + *crashTrigger = 0xDEAD10CC; + } + + static void withPause(const std::function& lambda) { + pause(); + lambda(); + resume(); + } + static void pause() { + _paused = true; + } + + static void resume() { + // Update the heartbeat BEFORE resuming the checks + updateHeartbeat(); + _paused = false; + } + + void run() override { + while (!_quit) { + QThread::sleep(HEARTBEAT_UPDATE_INTERVAL_SECS); + // Don't do heartbeat detection under nsight + if (_paused) { + continue; + } + uint64_t lastHeartbeat = _heartbeat; // sample atomic _heartbeat, because we could context switch away and have it updated on us + uint64_t now = usecTimestampNow(); + auto lastHeartbeatAge = (now > lastHeartbeat) ? now - lastHeartbeat : 0; + auto elapsedMovingAverage = _movingAverage.getAverage(); + + if (elapsedMovingAverage > _maxElapsedAverage) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" + << "lastHeartbeatAge:" << lastHeartbeatAge + << "elapsedMovingAverage:" << elapsedMovingAverage + << "maxElapsed:" << _maxElapsed + << "PREVIOUS maxElapsedAverage:" << _maxElapsedAverage + << "NEW maxElapsedAverage:" << elapsedMovingAverage << "** NEW MAX ELAPSED AVERAGE **" + << "samples:" << _movingAverage.getSamples(); + _maxElapsedAverage = elapsedMovingAverage; + } + if (lastHeartbeatAge > _maxElapsed) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" + << "lastHeartbeatAge:" << lastHeartbeatAge + << "elapsedMovingAverage:" << elapsedMovingAverage + << "PREVIOUS maxElapsed:" << _maxElapsed + << "NEW maxElapsed:" << lastHeartbeatAge << "** NEW MAX ELAPSED **" + << "maxElapsedAverage:" << _maxElapsedAverage + << "samples:" << _movingAverage.getSamples(); + _maxElapsed = lastHeartbeatAge; + } + if (elapsedMovingAverage > WARNING_ELAPSED_HEARTBEAT) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" + << "lastHeartbeatAge:" << lastHeartbeatAge + << "elapsedMovingAverage:" << elapsedMovingAverage << "** OVER EXPECTED VALUE **" + << "maxElapsed:" << _maxElapsed + << "maxElapsedAverage:" << _maxElapsedAverage + << "samples:" << _movingAverage.getSamples(); + } + + if (lastHeartbeatAge > MAX_HEARTBEAT_AGE_USECS) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK DETECTED -- " + << "lastHeartbeatAge:" << lastHeartbeatAge + << "[ lastHeartbeat :" << lastHeartbeat + << "now:" << now << " ]" + << "elapsedMovingAverage:" << elapsedMovingAverage + << "maxElapsed:" << _maxElapsed + << "maxElapsedAverage:" << _maxElapsedAverage + << "samples:" << _movingAverage.getSamples(); + + // Don't actually crash in debug builds, in case this apparent deadlock is simply from + // the developer actively debugging code + #ifdef NDEBUG + deadlockDetectionCrash(); + #endif + } + } + } + + static std::atomic _paused; + static std::atomic _heartbeat; + static std::atomic _maxElapsed; + static std::atomic _maxElapsedAverage; + static ThreadSafeMovingAverage _movingAverage; + + bool _quit { false }; + + Qt::HANDLE _mainThreadID = nullptr; +}; + +std::atomic DeadlockWatchdogThread::_paused; +std::atomic DeadlockWatchdogThread::_heartbeat; +std::atomic DeadlockWatchdogThread::_maxElapsed; +std::atomic DeadlockWatchdogThread::_maxElapsedAverage; +ThreadSafeMovingAverage DeadlockWatchdogThread::_movingAverage; + +bool isDomainURL(QUrl url) { + if (!url.isValid()) { + return false; + } + if (url.scheme() == URL_SCHEME_HIFI) { + return true; + } + if (url.scheme() != HIFI_URL_SCHEME_FILE) { + // TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can + // be loaded over http(s) + // && url.scheme() != HIFI_URL_SCHEME_HTTP && + // url.scheme() != HIFI_URL_SCHEME_HTTPS + return false; + } + if (url.path().endsWith(".json", Qt::CaseInsensitive) || + url.path().endsWith(".json.gz", Qt::CaseInsensitive)) { + return true; + } + return false; +} + +#ifdef Q_OS_WIN +class MyNativeEventFilter : public QAbstractNativeEventFilter { +public: + static MyNativeEventFilter& getInstance() { + static MyNativeEventFilter staticInstance; + return staticInstance; + } + + bool nativeEventFilter(const QByteArray &eventType, void* msg, long* result) Q_DECL_OVERRIDE { + if (eventType == "windows_generic_MSG") { + MSG* message = (MSG*)msg; + + if (message->message == UWM_IDENTIFY_INSTANCES) { + *result = UWM_IDENTIFY_INSTANCES; + return true; + } + + if (message->message == UWM_SHOW_APPLICATION) { + MainWindow* applicationWindow = qApp->getWindow(); + if (applicationWindow->isMinimized()) { + applicationWindow->showNormal(); // Restores to windowed or maximized state appropriately. + } + qApp->setActiveWindow(applicationWindow); // Flashes the taskbar icon if not focus. + return true; + } + + if (message->message == WM_COPYDATA) { + COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)(message->lParam); + QUrl url = QUrl((const char*)(pcds->lpData)); + if (isDomainURL(url)) { + DependencyManager::get()->handleLookupString(url.toString()); + return true; + } + } + + if (message->message == WM_DEVICECHANGE) { + const float MIN_DELTA_SECONDS = 2.0f; // de-bounce signal + static float lastTriggerTime = 0.0f; + const float deltaSeconds = secTimestampNow() - lastTriggerTime; + lastTriggerTime = secTimestampNow(); + if (deltaSeconds > MIN_DELTA_SECONDS) { + Midi::USBchanged(); // re-scan the MIDI bus + } + } + } + return false; + } +}; +#endif + +class LambdaEvent : public QEvent { + std::function _fun; +public: + LambdaEvent(const std::function & fun) : + QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { + } + LambdaEvent(std::function && fun) : + QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { + } + void call() const { _fun(); } +}; + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + QString logMessage = LogHandler::getInstance().printMessage((LogMsgType) type, context, message); + + if (!logMessage.isEmpty()) { +#ifdef Q_OS_ANDROID + const char * local=logMessage.toStdString().c_str(); + switch (type) { + case QtDebugMsg: + __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); + break; + case QtInfoMsg: + __android_log_write(ANDROID_LOG_INFO,"Interface",local); + break; + case QtWarningMsg: + __android_log_write(ANDROID_LOG_WARN,"Interface",local); + break; + case QtCriticalMsg: + __android_log_write(ANDROID_LOG_ERROR,"Interface",local); + break; + case QtFatalMsg: + default: + __android_log_write(ANDROID_LOG_FATAL,"Interface",local); + abort(); + } +#else + qApp->getLogger()->addMessage(qPrintable(logMessage)); +#endif + } +} + + +class ApplicationMeshProvider : public scriptable::ModelProviderFactory { +public: + virtual scriptable::ModelProviderPointer lookupModelProvider(const QUuid& uuid) override { + bool success; + if (auto nestable = DependencyManager::get()->find(uuid, success).lock()) { + auto type = nestable->getNestableType(); +#ifdef SCRIPTABLE_MESH_DEBUG + qCDebug(interfaceapp) << "ApplicationMeshProvider::lookupModelProvider" << uuid << SpatiallyNestable::nestableTypeToString(type); +#endif + switch (type) { + case NestableType::Entity: + return getEntityModelProvider(static_cast(uuid)); + case NestableType::Avatar: + return getAvatarModelProvider(uuid); + } + } + return nullptr; + } + +private: + scriptable::ModelProviderPointer getEntityModelProvider(EntityItemID entityID) { + scriptable::ModelProviderPointer provider; + auto entityTreeRenderer = qApp->getEntities(); + auto entityTree = entityTreeRenderer->getTree(); + if (auto entity = entityTree->findEntityByID(entityID)) { + if (auto renderer = entityTreeRenderer->renderableForEntityId(entityID)) { + provider = std::dynamic_pointer_cast(renderer); + provider->modelProviderType = NestableType::Entity; + } else { + qCWarning(interfaceapp) << "no renderer for entity ID" << entityID.toString(); + } + } + return provider; + } + + scriptable::ModelProviderPointer getAvatarModelProvider(QUuid sessionUUID) { + scriptable::ModelProviderPointer provider; + auto avatarManager = DependencyManager::get(); + if (auto avatar = avatarManager->getAvatarBySessionID(sessionUUID)) { + provider = std::dynamic_pointer_cast(avatar); + provider->modelProviderType = NestableType::Avatar; + } + return provider; + } +}; + +/**jsdoc + *

The Controller.Hardware.Application object has properties representing Interface's state. The property + * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or + * Controller.Standard items in a {@link RouteObject} mapping (e.g., using the {@link RouteObject#when} method). + * Each data value is either 1.0 for "true" or 0.0 for "false".

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PropertyTypeDataDescription
CameraFirstPersonnumbernumberThe camera is in first-person mode. + *
CameraThirdPersonnumbernumberThe camera is in third-person mode. + *
CameraFSMnumbernumberThe camera is in full screen mirror mode.
CameraIndependentnumbernumberThe camera is in independent mode.
CameraEntitynumbernumberThe camera is in entity mode.
InHMDnumbernumberThe user is in HMD mode.
AdvancedMovementnumbernumberAdvanced movement controls are enabled. + *
SnapTurnnumbernumberSnap turn is enabled.
GroundednumbernumberThe user's avatar is on the ground.
NavigationFocusednumbernumberNot used.
+ * @typedef {object} Controller.Hardware-Application + */ + +static const QString STATE_IN_HMD = "InHMD"; +static const QString STATE_CAMERA_FULL_SCREEN_MIRROR = "CameraFSM"; +static const QString STATE_CAMERA_FIRST_PERSON = "CameraFirstPerson"; +static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; +static const QString STATE_CAMERA_ENTITY = "CameraEntity"; +static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; +static const QString STATE_SNAP_TURN = "SnapTurn"; +static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; +static const QString STATE_GROUNDED = "Grounded"; +static const QString STATE_NAV_FOCUSED = "NavigationFocused"; +static const QString STATE_PLATFORM_WINDOWS = "PlatformWindows"; +static const QString STATE_PLATFORM_MAC = "PlatformMac"; +static const QString STATE_PLATFORM_ANDROID = "PlatformAndroid"; + +// Statically provided display and input plugins +extern DisplayPluginList getDisplayPlugins(); +extern InputPluginList getInputPlugins(); +extern void saveInputPluginSettings(const InputPluginList& plugins); + +// Parameters used for running tests from teh command line +const QString TEST_SCRIPT_COMMAND{ "--testScript" }; +const QString TEST_QUIT_WHEN_FINISHED_OPTION{ "quitWhenFinished" }; +const QString TEST_RESULTS_LOCATION_COMMAND{ "--testResultsLocation" }; + +bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { + const char** constArgv = const_cast(argv); + + qInstallMessageHandler(messageHandler); + + // HRS: I could not figure out how to move these any earlier in startup, so when using this option, be sure to also supply + // --allowMultipleInstances + auto reportAndQuit = [&](const char* commandSwitch, std::function report) { + const char* reportfile = getCmdOption(argc, constArgv, commandSwitch); + // Reports to the specified file, because stdout is set up to be captured for logging. + if (reportfile) { + FILE* fp = fopen(reportfile, "w"); + if (fp) { + report(fp); + fclose(fp); + if (!runningMarkerExisted) { // don't leave ours around + RunningMarker runingMarker(RUNNING_MARKER_FILENAME); + runingMarker.deleteRunningMarkerFile(); // happens in deleter, but making the side-effect explicit. + } + _exit(0); + } + } + }; + reportAndQuit("--protocolVersion", [&](FILE* fp) { + auto version = protocolVersionsSignatureBase64(); + fputs(version.toLatin1().data(), fp); + }); + reportAndQuit("--version", [&](FILE* fp) { + fputs(BuildInfo::VERSION.toLatin1().data(), fp); + }); + + const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); + const int listenPort = portStr ? atoi(portStr) : INVALID_PORT; + + static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; + bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); + + // set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store + static const auto OCULUS_STORE_ARG = "--oculus-store"; + bool isStore = cmdOptionExists(argc, const_cast(argv), OCULUS_STORE_ARG); + qApp->setProperty(hifi::properties::OCULUS_STORE, isStore); + + // Ignore any previous crashes if running from command line with a test script. + bool inTestMode { false }; + for (int i = 0; i < argc; ++i) { + QString parameter(argv[i]); + if (parameter == TEST_SCRIPT_COMMAND) { + inTestMode = true; + break; + } + } + + bool previousSessionCrashed { false }; + if (!inTestMode) { + previousSessionCrashed = CrashRecoveryHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt); + } + + // get dir to use for cache + static const auto CACHE_SWITCH = "--cache"; + QString cacheDir = getCmdOption(argc, const_cast(argv), CACHE_SWITCH); + if (!cacheDir.isEmpty()) { + qApp->setProperty(hifi::properties::APP_LOCAL_DATA_PATH, cacheDir); + } + + { + const QString resourcesBinaryFile = PathUtils::getRccPath(); + if (!QFile::exists(resourcesBinaryFile)) { + throw std::runtime_error("Unable to find primary resources"); + } + if (!QResource::registerResource(resourcesBinaryFile)) { + throw std::runtime_error("Unable to load primary resources"); + } + } + + // Tell the plugin manager about our statically linked plugins + DependencyManager::set(); + auto pluginManager = PluginManager::getInstance(); + pluginManager->setInputPluginProvider([] { return getInputPlugins(); }); + pluginManager->setDisplayPluginProvider([] { return getDisplayPlugins(); }); + pluginManager->setInputPluginSettingsPersister([](const InputPluginList& plugins) { saveInputPluginSettings(plugins); }); + if (auto steamClient = pluginManager->getSteamClientPlugin()) { + steamClient->init(); + } + if (auto oculusPlatform = pluginManager->getOculusPlatformPlugin()) { + oculusPlatform->init(); + } + + PROFILE_SET_THREAD_NAME("Main Thread"); + +#if defined(Q_OS_WIN) + // Select appropriate audio DLL + QString audioDLLPath = QCoreApplication::applicationDirPath(); + if (IsWindows8OrGreater()) { + audioDLLPath += "/audioWin8"; + } else { + audioDLLPath += "/audioWin7"; + } + QCoreApplication::addLibraryPath(audioDLLPath); +#endif + + DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); + + // Set dependencies + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); +#if defined(Q_OS_ANDROID) + DependencyManager::set(); // use the default user agent getter +#else + DependencyManager::set(std::bind(&Application::getUserAgent, qApp)); +#endif + DependencyManager::set(); + DependencyManager::set(ScriptEngine::CLIENT_SCRIPT); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(NodeType::Agent, listenPort); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache constructor. + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(true); + DependencyManager::set(); + DependencyManager::registerInheritance(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + DependencyManager::set(); +#endif + DependencyManager::set(); + DependencyManager::set(); +#if !defined(DISABLE_QML) + DependencyManager::set(); +#endif + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, + STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, + STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED, + STATE_PLATFORM_WINDOWS, STATE_PLATFORM_MAC, STATE_PLATFORM_ANDROID } }); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(true, qApp, qApp); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + + return previousSessionCrashed; +} + +// FIXME move to header, or better yet, design some kind of UI manager +// to take care of highlighting keyboard focused items, rather than +// continuing to overburden Application.cpp +QUuid _keyboardFocusHighlightID; + +OffscreenGLCanvas* _qmlShareContext { nullptr }; + +// FIXME hack access to the internal share context for the Chromium helper +// Normally we'd want to use QWebEngine::initialize(), but we can't because +// our primary context is a QGLWidget, which can't easily be initialized to share +// from a QOpenGLContext. +// +// So instead we create a new offscreen context to share with the QGLWidget, +// and manually set THAT to be the shared context for the Chromium helper +#if !defined(DISABLE_QML) +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +#endif + +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); +Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); + +Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; + +const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f; +const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; +const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; +const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; +const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false; +const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; +const QString DEFAULT_CURSOR_NAME = "DEFAULT"; +const bool DEFAULT_MINI_TABLET_ENABLED = true; + +QSharedPointer getOffscreenUI() { +#if !defined(DISABLE_QML) + return DependencyManager::get(); +#else + return nullptr; +#endif +} + +Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runningMarkerExisted) : + QApplication(argc, argv), + _window(new MainWindow(desktop())), + _sessionRunTimer(startupTimer), +#ifndef Q_OS_ANDROID + _logger(new FileLogger(this)), +#endif + _previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)), + _entitySimulation(new PhysicalEntitySimulation()), + _physicsEngine(new PhysicsEngine(Vectors::ZERO)), + _entityClipboard(new EntityTree()), + _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), + _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), + _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), + _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), + _firstRun(Settings::firstRun, true), + _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), + _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), + _preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER), + _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), + _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), + _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), + _miniTabletEnabledSetting("miniTabletEnabled", DEFAULT_MINI_TABLET_ENABLED), + _scaleMirror(1.0f), + _mirrorYawOffset(0.0f), + _raiseMirror(0.0f), + _enableProcessOctreeThread(true), + _lastNackTime(usecTimestampNow()), + _lastSendDownstreamAudioStats(usecTimestampNow()), + _notifiedPacketVersionMismatchThisDomain(false), + _maxOctreePPS(maxOctreePacketsPerSecond.get()), + _lastFaceTrackerUpdate(0), + _snapshotSound(nullptr), + _sampleSound(nullptr) +{ + + auto steamClient = PluginManager::getInstance()->getSteamClientPlugin(); + setProperty(hifi::properties::STEAM, (steamClient && steamClient->isRunning())); + setProperty(hifi::properties::CRASHED, _previousSessionCrashed); + + { + const QStringList args = arguments(); + + for (int i = 0; i < args.size() - 1; ++i) { + if (args.at(i) == TEST_SCRIPT_COMMAND && (i + 1) < args.size()) { + QString testScriptPath = args.at(i + 1); + + // If the URL scheme is http(s) or ftp, then use as is, else - treat it as a local file + // This is done so as not break previous command line scripts + if (testScriptPath.left(HIFI_URL_SCHEME_HTTP.length()) == HIFI_URL_SCHEME_HTTP || + testScriptPath.left(HIFI_URL_SCHEME_FTP.length()) == HIFI_URL_SCHEME_FTP) { + + setProperty(hifi::properties::TEST, QUrl::fromUserInput(testScriptPath)); + } else if (QFileInfo(testScriptPath).exists()) { + setProperty(hifi::properties::TEST, QUrl::fromLocalFile(testScriptPath)); + } + + // quite when finished parameter must directly follow the test script + if ((i + 2) < args.size() && args.at(i + 2) == TEST_QUIT_WHEN_FINISHED_OPTION) { + quitWhenFinished = true; + } + } else if (args.at(i) == TEST_RESULTS_LOCATION_COMMAND) { + // Set test snapshot location only if it is a writeable directory + QString path(args.at(i + 1)); + + QFileInfo fileInfo(path); + if (fileInfo.isDir() && fileInfo.isWritable()) { + TestScriptingInterface::getInstance()->setTestResultsLocation(path); + } + } + } + } + + // make sure the debug draw singleton is initialized on the main thread. + DebugDraw::getInstance().removeMarker(""); + + PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care + PluginManager::getInstance()->setContainer(pluginContainer); + + QThreadPool::globalInstance()->setMaxThreadCount(MIN_PROCESSING_THREAD_POOL_SIZE); + thread()->setPriority(QThread::HighPriority); + thread()->setObjectName("Main Thread"); + + setInstance(this); + + auto controllerScriptingInterface = DependencyManager::get().data(); + _controllerScriptingInterface = dynamic_cast(controllerScriptingInterface); + connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged, + controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices); + + EntityTree::setEntityClicksCapturedOperator([this] { + return _controllerScriptingInterface->areEntityClicksCaptured(); + }); + + _entityClipboard->createRootElement(); + +#ifdef Q_OS_WIN + installNativeEventFilter(&MyNativeEventFilter::getInstance()); +#endif + + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/fontawesome-webfont.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/hifi-glyphs.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/AnonymousPro-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-SemiBold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Light.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/rawline-500.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf"); + _window->setWindowTitle("High Fidelity Interface"); + + Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us + + auto nodeList = DependencyManager::get(); + nodeList->startThread(); + nodeList->setFlagTimeForConnectionStep(true); + + // move the AddressManager to the NodeList thread so that domain resets due to domain changes always occur + // before we tell MyAvatar to go to a new location in the new domain + auto addressManager = DependencyManager::get(); + addressManager->moveToThread(nodeList->thread()); + + const char** constArgv = const_cast(argv); + if (cmdOptionExists(argc, constArgv, "--disableWatchdog")) { + DISABLE_WATCHDOG = true; + } + // Set up a watchdog thread to intentionally crash the application on deadlocks + if (!DISABLE_WATCHDOG) { + auto deadlockWatchdogThread = new DeadlockWatchdogThread(); + deadlockWatchdogThread->setMainThreadID(QThread::currentThreadId()); + deadlockWatchdogThread->start(); + } + + // Set File Logger Session UUID + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + if (avatarManager) { + workload::SpacePointer space = getEntities()->getWorkloadSpace(); + avatarManager->setSpace(space); + } + auto accountManager = DependencyManager::get(); + +#ifndef Q_OS_ANDROID + _logger->setSessionID(accountManager->getSessionID()); +#endif + + setCrashAnnotation("metaverse_session_id", accountManager->getSessionID().toString().toStdString()); + setCrashAnnotation("main_thread_id", std::to_string((size_t)QThread::currentThreadId())); + + if (steamClient) { + qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << steamClient->getSteamVRBuildID(); + } + setCrashAnnotation("steam", property(hifi::properties::STEAM).toBool() ? "1" : "0"); + + qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); + qCDebug(interfaceapp) << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; + qCDebug(interfaceapp) << "[VERSION] VERSION:" << BuildInfo::VERSION; + qCDebug(interfaceapp) << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING; + qCDebug(interfaceapp) << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES; +#if USE_STABLE_GLOBAL_SERVICES + qCDebug(interfaceapp) << "[VERSION] We will use STABLE global services."; +#else + qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services."; +#endif + + bool isStore = property(hifi::properties::OCULUS_STORE).toBool(); + + DependencyManager::get()->setLimitedCommerce(isStore); // Or we could make it a separate arg, or if either arg is set, etc. And should this instead by a hifi::properties? + + updateHeartbeat(); + + // setup a timer for domain-server check ins + QTimer* domainCheckInTimer = new QTimer(this); + QWeakPointer nodeListWeak = nodeList; + connect(domainCheckInTimer, &QTimer::timeout, [this, nodeListWeak] { + auto nodeList = nodeListWeak.lock(); + if (!isServerlessMode() && nodeList) { + nodeList->sendDomainServerCheckIn(); + } + }); + domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); + connect(this, &QCoreApplication::aboutToQuit, [domainCheckInTimer] { + domainCheckInTimer->stop(); + domainCheckInTimer->deleteLater(); + }); + + { + auto audioIO = DependencyManager::get().data(); + audioIO->setPositionGetter([] { + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + + return myAvatar ? myAvatar->getPositionForAudio() : Vectors::ZERO; + }); + audioIO->setOrientationGetter([] { + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + + return myAvatar ? myAvatar->getOrientationForAudio() : Quaternions::IDENTITY; + }); + + recording::Frame::registerFrameHandler(AudioConstants::getAudioFrameName(), [&audioIO](recording::Frame::ConstPointer frame) { + audioIO->handleRecordedAudioInput(frame->data); + }); + + connect(audioIO, &AudioClient::inputReceived, [](const QByteArray& audio) { + static auto recorder = DependencyManager::get(); + if (recorder->isRecording()) { + static const recording::FrameType AUDIO_FRAME_TYPE = recording::Frame::registerFrameType(AudioConstants::getAudioFrameName()); + recorder->recordFrame(AUDIO_FRAME_TYPE, audio); + } + }); + audioIO->startThread(); + } + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + // Setup MessagesClient + DependencyManager::get()->startThread(); + + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + + connect(&domainHandler, SIGNAL(domainURLChanged(QUrl)), SLOT(domainURLChanged(QUrl))); + connect(&domainHandler, SIGNAL(redirectToErrorDomainURL(QUrl)), SLOT(goToErrorDomainURL(QUrl))); + connect(&domainHandler, &DomainHandler::domainURLChanged, [](QUrl domainURL){ + setCrashAnnotation("domain", domainURL.toString().toStdString()); + }); + connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); + connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); + connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { + auto tabletScriptingInterface = DependencyManager::get(); + if (tabletScriptingInterface) { + tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); + } + auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->deleteEntity(getTabletScreenID()); + entityScriptingInterface->deleteEntity(getTabletHomeButtonID()); + entityScriptingInterface->deleteEntity(getTabletFrameID()); + _failedToConnectToEntityServer = false; + }); + + _entityServerConnectionTimer.setSingleShot(true); + connect(&_entityServerConnectionTimer, &QTimer::timeout, this, &Application::setFailedToConnectToEntityServer); + + connect(&domainHandler, &DomainHandler::connectedToDomain, this, [this]() { + if (!isServerlessMode()) { + _entityServerConnectionTimer.setInterval(ENTITY_SERVER_ADDED_TIMEOUT); + _entityServerConnectionTimer.start(); + _failedToConnectToEntityServer = false; + } + }); + connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); + + nodeList->getDomainHandler().setErrorDomainURL(QUrl(REDIRECT_HIFI_ADDRESS)); + + // We could clear ATP assets only when changing domains, but it's possible that the domain you are connected + // to has gone down and switched to a new content set, so when you reconnect the cached ATP assets will no longer be valid. + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, DependencyManager::get().data(), &ScriptCache::clearATPScriptsFromCache); + + // 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_SECOND; + + auto discoverabilityManager = DependencyManager::get(); + connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); + connect(&locationUpdateTimer, &QTimer::timeout, + DependencyManager::get().data(), &AddressManager::storeCurrentAddress); + locationUpdateTimer.start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS); + + // if we get a domain change, immediately attempt update location in metaverse server + connect(&nodeList->getDomainHandler(), &DomainHandler::connectedToDomain, + discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); + + // send a location update immediately + discoverabilityManager->updateLocation(); + + connect(nodeList.data(), &NodeList::nodeAdded, this, &Application::nodeAdded); + connect(nodeList.data(), &NodeList::nodeKilled, this, &Application::nodeKilled); + connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); + connect(nodeList.data(), &NodeList::uuidChanged, myAvatar.get(), &MyAvatar::setSessionUUID); + connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); + connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); + + // you might think we could just do this in NodeList but we only want this connection for Interface + connect(&nodeList->getDomainHandler(), SIGNAL(limitOfSilentDomainCheckInsReached()), + nodeList.data(), SLOT(reset())); + + auto dialogsManager = DependencyManager::get(); +#if defined(Q_OS_ANDROID) + connect(accountManager.data(), &AccountManager::authRequired, this, []() { + auto addressManager = DependencyManager::get(); + AndroidHelper::instance().showLoginDialog(addressManager->currentAddress()); + }); +#else + connect(accountManager.data(), &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); +#endif + connect(accountManager.data(), &AccountManager::usernameChanged, this, &Application::updateWindowTitle); + + // set the account manager's root URL and trigger a login request if we don't have the access token + accountManager->setIsAgent(true); + accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); + + // use our MyAvatar position and quat for address manager path + addressManager->setPositionGetter([this]{ return getMyAvatar()->getWorldFeetPosition(); }); + addressManager->setOrientationGetter([this]{ return getMyAvatar()->getWorldOrientation(); }); + + connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle); + connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); + + connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateThreadPoolCount); + connect(this, &Application::activeDisplayPluginChanged, this, [](){ + qApp->setProperty(hifi::properties::HMD, qApp->isHMDMode()); + auto displayPlugin = qApp->getActiveDisplayPlugin(); + setCrashAnnotation("display_plugin", displayPlugin->getName().toStdString()); + setCrashAnnotation("hmd", displayPlugin->isHmd() ? "1" : "0"); + }); + connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); + connect(this, &Application::activeDisplayPluginChanged, this, [&](){ + if (getLoginDialogPoppedUp()) { + auto dialogsManager = DependencyManager::get(); + auto keyboard = DependencyManager::get(); + if (_firstRun.get()) { + // display mode changed. Don't allow auto-switch to work after this session. + _firstRun.set(false); + } + if (isHMDMode()) { + emit loginDialogFocusDisabled(); + dialogsManager->hideLoginDialog(); + createLoginDialog(); + } else { + DependencyManager::get()->deleteEntity(_loginDialogID); + _loginDialogID = QUuid(); + _loginStateManager.tearDown(); + dialogsManager->showLoginDialog(); + emit loginDialogFocusEnabled(); + } + } + }); + + // Save avatar location immediately after a teleport. + connect(myAvatar.get(), &MyAvatar::positionGoneTo, + DependencyManager::get().data(), &AddressManager::storeCurrentAddress); + + connect(myAvatar.get(), &MyAvatar::skeletonModelURLChanged, [](){ + QUrl avatarURL = qApp->getMyAvatar()->getSkeletonModelURL(); + setCrashAnnotation("avatar", avatarURL.toString().toStdString()); + }); + + + // Inititalize sample before registering + _sampleSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/sample.wav")); + + { + auto scriptEngines = DependencyManager::get().data(); + scriptEngines->registerScriptInitializer([this](ScriptEnginePointer engine) { + registerScriptEngineWithApplicationServices(engine); + }); + + connect(scriptEngines, &ScriptEngines::scriptCountChanged, this, [this] { + auto scriptEngines = DependencyManager::get(); + if (scriptEngines->getRunningScripts().isEmpty()) { + getMyAvatar()->clearScriptableSettings(); + } + }, Qt::QueuedConnection); + + connect(scriptEngines, &ScriptEngines::scriptsReloading, this, [this] { + getEntities()->reloadEntityScripts(); + loadAvatarScripts(getMyAvatar()->getScriptUrls()); + }, Qt::QueuedConnection); + + connect(scriptEngines, &ScriptEngines::scriptLoadError, + this, [](const QString& filename, const QString& error) { + OffscreenUi::asyncWarning(nullptr, "Error Loading Script", filename + " failed to load."); + }, Qt::QueuedConnection); + } + +#ifdef _WIN32 + WSADATA WsaData; + int wsaresult = WSAStartup(MAKEWORD(2, 2), &WsaData); +#endif + + // tell the NodeList instance who to tell the domain server we care about + nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer + << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer); + + // connect to the packet sent signal of the _entityEditSender + connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent); + connect(&_entityEditSender, &EntityEditPacketSender::addingEntityWithCertificate, this, &Application::addingEntityWithCertificate); + + QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads"); + bool success; + uint32_t concurrentDownloads = concurrentDownloadsStr.toUInt(&success); + if (!success) { + concurrentDownloads = MAX_CONCURRENT_RESOURCE_DOWNLOADS; + } + ResourceCache::setRequestLimit(concurrentDownloads); + + // perhaps override the avatar url. Since we will test later for validity + // we don't need to do so here. + QString avatarURL = getCmdOption(argc, constArgv, "--avatarURL"); + _avatarOverrideUrl = QUrl::fromUserInput(avatarURL); + + // If someone specifies both --avatarURL and --replaceAvatarURL, + // the replaceAvatarURL wins. So only set the _overrideUrl if this + // does have a non-empty string. + QString replaceURL = getCmdOption(argc, constArgv, "--replaceAvatarURL"); + if (!replaceURL.isEmpty()) { + _avatarOverrideUrl = QUrl::fromUserInput(replaceURL); + _saveAvatarOverrideUrl = true; + } + + _glWidget = new GLCanvas(); + getApplicationCompositor().setRenderingWidget(_glWidget); + _window->setCentralWidget(_glWidget); + + _window->restoreGeometry(); + _window->setVisible(true); + + _glWidget->setFocusPolicy(Qt::StrongFocus); + _glWidget->setFocus(); + + if (cmdOptionExists(argc, constArgv, "--system-cursor")) { + _preferredCursor.set(Cursor::Manager::getIconName(Cursor::Icon::SYSTEM)); + } + showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); + + // enable mouse tracking; otherwise, we only get drag events + _glWidget->setMouseTracking(true); + // Make sure the window is set to the correct size by processing the pending events + QCoreApplication::processEvents(); + + // Create the main thread context, the GPU backend + initializeGL(); + qCDebug(interfaceapp, "Initialized GL"); + + // Initialize the display plugin architecture + initializeDisplayPlugins(); + qCDebug(interfaceapp, "Initialized Display"); + + // An audio device changed signal received before the display plugins are set up will cause a crash, + // so we defer the setup of the `scripting::Audio` class until this point + { + auto audioScriptingInterface = DependencyManager::set(); + auto audioIO = DependencyManager::get().data(); + connect(audioIO, &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); + connect(audioIO, &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); + connect(audioIO, &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); + connect(audioIO, &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { + auto audioClient = DependencyManager::get(); + auto audioScriptingInterface = DependencyManager::get(); + auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getWorldPosition(); + float distance = glm::distance(myAvatarPosition, position); + + if (distance < radius) { + audioClient->setMuted(true); + audioScriptingInterface->environmentMuted(); + } + }); + connect(this, &Application::activeDisplayPluginChanged, + reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); + } + + // Create the rendering engine. This can be slow on some machines due to lots of + // GPU pipeline creation. + initializeRenderEngine(); + qCDebug(interfaceapp, "Initialized Render Engine."); + + // Overlays need to exist before we set the ContextOverlayInterface dependency + _overlays.init(); // do this before scripts load + DependencyManager::set(); + + // Initialize the user interface and menu system + // Needs to happen AFTER the render engine initialization to access its configuration + initializeUi(); + + init(); + qCDebug(interfaceapp, "init() complete."); + + // create thread for parsing of octree data independent of the main network and rendering threads + _octreeProcessor.initialize(_enableProcessOctreeThread); + connect(&_octreeProcessor, &OctreePacketProcessor::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); + _entityEditSender.initialize(_enableProcessOctreeThread); + + _idleLoopStdev.reset(); + + // update before the first render + update(0); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + static const QString TESTER = "HIFI_TESTER"; + bool isTester = false; +#if defined (Q_OS_ANDROID) + // Since we cannot set environment variables in Android we use a file presence + // to denote that this is a testing device + QFileInfo check_tester_file(TESTER_FILE); + isTester = check_tester_file.exists() && check_tester_file.isFile(); +#endif + + constexpr auto INSTALLER_INI_NAME = "installer.ini"; + auto iniPath = QDir(applicationDirPath()).filePath(INSTALLER_INI_NAME); + QFile installerFile { iniPath }; + std::unordered_map installerKeyValues; + if (installerFile.open(QIODevice::ReadOnly)) { + while (!installerFile.atEnd()) { + auto line = installerFile.readLine(); + if (!line.isEmpty()) { + auto index = line.indexOf("="); + if (index >= 0) { + installerKeyValues[line.mid(0, index).trimmed()] = line.mid(index + 1).trimmed(); + } + } + } + } + + // In practice we shouldn't run across installs that don't have a known installer type. + // Client or Client+Server installs should always have the installer.ini next to their + // respective interface.exe, and Steam installs will be detected as such. If a user were + // to delete the installer.ini, though, and as an example, we won't know the context of the + // original install. + constexpr auto INSTALLER_KEY_TYPE = "type"; + constexpr auto INSTALLER_KEY_CAMPAIGN = "campaign"; + constexpr auto INSTALLER_TYPE_UNKNOWN = "unknown"; + constexpr auto INSTALLER_TYPE_STEAM = "steam"; + + auto typeIt = installerKeyValues.find(INSTALLER_KEY_TYPE); + QString installerType = INSTALLER_TYPE_UNKNOWN; + if (typeIt == installerKeyValues.end()) { + if (property(hifi::properties::STEAM).toBool()) { + installerType = INSTALLER_TYPE_STEAM; + } + } else { + installerType = typeIt->second; + } + + auto campaignIt = installerKeyValues.find(INSTALLER_KEY_CAMPAIGN); + QString installerCampaign = campaignIt != installerKeyValues.end() ? campaignIt->second : ""; + + qDebug() << "Detected installer type:" << installerType; + qDebug() << "Detected installer campaign:" << installerCampaign; + + auto& userActivityLogger = UserActivityLogger::getInstance(); + if (userActivityLogger.isEnabled()) { + // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. + // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. + static const QString TESTER = "HIFI_TESTER"; + auto gpuIdent = GPUIdent::getInstance(); + auto glContextData = getGLContextData(); + QJsonObject properties = { + { "version", applicationVersion() }, + { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) || isTester }, + { "installer_campaign", installerCampaign }, + { "installer_type", installerType }, + { "build_type", BuildInfo::BUILD_TYPE_STRING }, + { "previousSessionCrashed", _previousSessionCrashed }, + { "previousSessionRuntime", sessionRunTime.get() }, + { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, + { "kernel_type", QSysInfo::kernelType() }, + { "kernel_version", QSysInfo::kernelVersion() }, + { "os_type", QSysInfo::productType() }, + { "os_version", QSysInfo::productVersion() }, + { "gpu_name", gpuIdent->getName() }, + { "gpu_driver", gpuIdent->getDriver() }, + { "gpu_memory", static_cast(gpuIdent->getMemory()) }, + { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, + { "gl_version", glContextData["version"] }, + { "gl_vender", glContextData["vendor"] }, + { "gl_sl_version", glContextData["sl_version"] }, + { "gl_renderer", glContextData["renderer"] }, + { "ideal_thread_count", QThread::idealThreadCount() } + }; + auto macVersion = QSysInfo::macVersion(); + if (macVersion != QSysInfo::MV_None) { + properties["os_osx_version"] = QSysInfo::macVersion(); + } + auto windowsVersion = QSysInfo::windowsVersion(); + if (windowsVersion != QSysInfo::WV_None) { + properties["os_win_version"] = QSysInfo::windowsVersion(); + } + + ProcessorInfo procInfo; + if (getProcessorInfo(procInfo)) { + properties["processor_core_count"] = procInfo.numProcessorCores; + properties["logical_processor_count"] = procInfo.numLogicalProcessors; + properties["processor_l1_cache_count"] = procInfo.numProcessorCachesL1; + properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; + properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; + } + + properties["first_run"] = _firstRun.get(); + + // add the user's machine ID to the launch event + QString machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); + properties["machine_fingerprint"] = machineFingerPrint; + + userActivityLogger.logAction("launch", properties); + } + + _entityEditSender.setMyAvatar(myAvatar.get()); + + // The entity octree will have to know about MyAvatar for the parentJointName import + getEntities()->getTree()->setMyAvatar(myAvatar); + _entityClipboard->setMyAvatar(myAvatar); + + // For now we're going to set the PPS for outbound packets to be super high, this is + // probably not the right long term solution. But for now, we're going to do this to + // allow you to move an entity around in your hand + _entityEditSender.setPacketsPerSecond(3000); // super high!! + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + connect(this, SIGNAL(aboutToQuit()), this, SLOT(onAboutToQuit())); + + // FIXME -- I'm a little concerned about this. + connect(myAvatar->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded, + this, &Application::checkSkeleton, Qt::QueuedConnection); + + // Setup the userInputMapper with the actions + auto userInputMapper = DependencyManager::get(); + connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { + using namespace controller; + auto tabletScriptingInterface = DependencyManager::get(); + { + auto actionEnum = static_cast(action); + int key = Qt::Key_unknown; + static int lastKey = Qt::Key_unknown; + bool navAxis = false; + switch (actionEnum) { + case Action::UI_NAV_VERTICAL: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Up; + } else if (state < 0.0f) { + key = Qt::Key_Down; + } + break; + + case Action::UI_NAV_LATERAL: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Right; + } else if (state < 0.0f) { + key = Qt::Key_Left; + } + break; + + case Action::UI_NAV_GROUP: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Tab; + } else if (state < 0.0f) { + key = Qt::Key_Backtab; + } + break; + + case Action::UI_NAV_BACK: + key = Qt::Key_Escape; + break; + + case Action::UI_NAV_SELECT: + key = Qt::Key_Return; + break; + default: + break; + } + + auto window = tabletScriptingInterface->getTabletWindow(); + if (navAxis && window) { + if (lastKey != Qt::Key_unknown) { + QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); + sendEvent(window, &event); + lastKey = Qt::Key_unknown; + } + + if (key != Qt::Key_unknown) { + QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); + sendEvent(window, &event); + tabletScriptingInterface->processEvent(&event); + lastKey = key; + } + } else if (key != Qt::Key_unknown && window) { + if (state) { + QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); + sendEvent(window, &event); + tabletScriptingInterface->processEvent(&event); + } else { + QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); + sendEvent(window, &event); + } + return; + } + } + + if (action == controller::toInt(controller::Action::RETICLE_CLICK)) { + auto reticlePos = getApplicationCompositor().getReticlePosition(); + QPoint localPos(reticlePos.x, reticlePos.y); // both hmd and desktop already handle this in our coordinates. + if (state) { + QMouseEvent mousePress(QEvent::MouseButtonPress, localPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + sendEvent(_glWidget, &mousePress); + _reticleClickPressed = true; + } else { + QMouseEvent mouseRelease(QEvent::MouseButtonRelease, localPos, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); + sendEvent(_glWidget, &mouseRelease); + _reticleClickPressed = false; + } + return; // nothing else to do + } + + if (state) { + if (action == controller::toInt(controller::Action::TOGGLE_MUTE)) { + auto audioClient = DependencyManager::get(); + audioClient->setMuted(!audioClient->isMuted()); + } else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) { + cycleCamera(); + } else if (action == controller::toInt(controller::Action::CONTEXT_MENU) && !isInterstitialMode()) { + toggleTabletUI(); + } else if (action == controller::toInt(controller::Action::RETICLE_X)) { + auto oldPos = getApplicationCompositor().getReticlePosition(); + getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); + } else if (action == controller::toInt(controller::Action::RETICLE_Y)) { + auto oldPos = getApplicationCompositor().getReticlePosition(); + getApplicationCompositor().setReticlePosition({ oldPos.x, oldPos.y + state }); + } else if (action == controller::toInt(controller::Action::TOGGLE_OVERLAY)) { + toggleOverlays(); + } + } + }); + + _applicationStateDevice = userInputMapper->getStateDevice(); + + _applicationStateDevice->setInputVariant(STATE_IN_HMD, []() -> float { + return qApp->isHMDMode() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_FULL_SCREEN_MIRROR, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_MIRROR ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_FIRST_PERSON, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_FIRST_PERSON ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_THIRD_PERSON, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_THIRD_PERSON ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_ENTITY, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_ENTITY ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_INDEPENDENT, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_INDEPENDENT ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { + return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { + return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; + }); + + _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { + return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_NAV_FOCUSED, []() -> float { + auto offscreenUi = getOffscreenUI(); + return offscreenUi ? (offscreenUi->navigationFocused() ? 1 : 0) : 0; + }); + _applicationStateDevice->setInputVariant(STATE_PLATFORM_WINDOWS, []() -> float { +#if defined(Q_OS_WIN) + return 1; +#else + return 0; +#endif + }); + _applicationStateDevice->setInputVariant(STATE_PLATFORM_MAC, []() -> float { +#if defined(Q_OS_MAC) + return 1; +#else + return 0; +#endif + }); + _applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float { +#if defined(Q_OS_ANDROID) + return 1 ; +#else + return 0; +#endif + }); + + + // Setup the _keyboardMouseDevice, _touchscreenDevice, _touchscreenVirtualPadDevice and the user input mapper with the default bindings + userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); + // if the _touchscreenDevice is not supported it will not be registered + if (_touchscreenDevice) { + userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); + } + if (_touchscreenVirtualPadDevice) { + userInputMapper->registerDevice(_touchscreenVirtualPadDevice->getInputDevice()); + } + + QString scriptsSwitch = QString("--").append(SCRIPTS_SWITCH); + _defaultScriptsLocation = getCmdOption(argc, constArgv, scriptsSwitch.toStdString().c_str()); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + loadSettings(); + + updateVerboseLogging(); + + // Now that we've loaded the menu and thus switched to the previous display plugin + // we can unlock the desktop repositioning code, since all the positions will be + // relative to the desktop size for this plugin + auto offscreenUi = getOffscreenUI(); + connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() { + auto offscreenUi = getOffscreenUI(); + auto desktop = offscreenUi->getDesktop(); + if (desktop) { + desktop->setProperty("repositionLocked", false); + } + }); + + connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) + // Do not show login dialog if requested not to on the command line + QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); + int index = arguments().indexOf(hifiNoLoginCommandLineKey); + if (index != -1) { + resumeAfterLoginDialogActionTaken(); + return; + } + + showLoginScreen(); +#else + resumeAfterLoginDialogActionTaken(); +#endif + }); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + QTimer* settingsTimer = new QTimer(); + moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{ + // This needs to run on the settings thread, so we need to pass the `settingsTimer` as the + // receiver object, otherwise it will run on the application thread and trigger a warning + // about trying to kill the timer on the main thread. + connect(qApp, &Application::beforeAboutToQuit, settingsTimer, [this, settingsTimer]{ + // Disconnect the signal from the save settings + QObject::disconnect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); + // Stop the settings timer + settingsTimer->stop(); + // Delete it (this will trigger the thread destruction + settingsTimer->deleteLater(); + // Mark the settings thread as finished, so we know we can safely save in the main application + // shutdown code + _settingsGuard.trigger(); + }); + + int SAVE_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; // Let's save every seconds for now + settingsTimer->setSingleShot(false); + settingsTimer->setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable + QObject::connect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); + settingsTimer->start(); + }, QThread::LowestPriority); + + if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person. + } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + cameraMenuChanged(); + } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + cameraMenuChanged(); + } + + { + auto audioIO = DependencyManager::get().data(); + // set the local loopback interface for local sounds + AudioInjector::setLocalAudioInterface(audioIO); + auto audioScriptingInterface = DependencyManager::get(); + audioScriptingInterface->setLocalAudioInterface(audioIO); + connect(audioIO, &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); + connect(audioIO, &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); + connect(audioIO, &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); + } + + this->installEventFilter(this); + + + +#ifdef HAVE_DDE + auto ddeTracker = DependencyManager::get(); + ddeTracker->init(); + connect(ddeTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); +#endif + +#ifdef HAVE_IVIEWHMD + auto eyeTracker = DependencyManager::get(); + eyeTracker->init(); + setActiveEyeTracker(); +#endif + + // If launched from Steam, let it handle updates + const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater"; + bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1; + bool buildCanUpdate = BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable + || BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Master; + if (!noUpdater && buildCanUpdate) { + constexpr auto INSTALLER_TYPE_CLIENT_ONLY = "client_only"; + + auto applicationUpdater = DependencyManager::set(); + + AutoUpdater::InstallerType type = installerType == INSTALLER_TYPE_CLIENT_ONLY + ? AutoUpdater::InstallerType::CLIENT_ONLY : AutoUpdater::InstallerType::FULL; + + applicationUpdater->setInstallerType(type); + applicationUpdater->setInstallerCampaign(installerCampaign); + connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog); + applicationUpdater->checkForUpdate(); + } + + Menu::getInstance()->setIsOptionChecked(MenuOption::ActionMotorControl, true); + +// FIXME spacemouse code still needs cleanup +#if 0 + // the 3Dconnexion device wants to be initialized after a window is displayed. + SpacemouseManager::getInstance().init(); +#endif + + // If the user clicks on an object, we will check that it's a web surface, and if so, set the focus to it + auto pointerManager = DependencyManager::get(); + auto keyboardFocusOperator = [this](const QUuid& id, const PointerEvent& event) { + if (event.shouldFocus()) { + auto keyboard = DependencyManager::get(); + if (getEntities()->wantsKeyboardFocus(id)) { + setKeyboardFocusEntity(id); + } else if (!keyboard->containsID(id)) { // FIXME: this is a hack to make the keyboard work for now, since the keys would otherwise steal focus + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + } + } + }; + connect(pointerManager.data(), &PointerManager::triggerBeginEntity, keyboardFocusOperator); + connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, keyboardFocusOperator); + + auto entityScriptingInterface = DependencyManager::get(); + connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityItemID) { + if (entityItemID == _keyboardFocusedEntity.get()) { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + } + }, Qt::QueuedConnection); + + EntityTreeRenderer::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) { + if (_aboutToQuit) { + return false; + } + + auto renderable = getEntities()->renderableForEntityId(entityID); + if (renderable) { + renderable->addMaterial(material, parentMaterialName); + return true; + } + + return false; + }); + EntityTreeRenderer::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + if (_aboutToQuit) { + return false; + } + + auto renderable = getEntities()->renderableForEntityId(entityID); + if (renderable) { + renderable->removeMaterial(material, parentMaterialName); + return true; + } + + return false; + }); + + EntityTreeRenderer::setAddMaterialToAvatarOperator([](const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) { + auto avatarManager = DependencyManager::get(); + auto avatar = avatarManager->getAvatarBySessionID(avatarID); + if (avatar) { + avatar->addMaterial(material, parentMaterialName); + return true; + } + return false; + }); + EntityTreeRenderer::setRemoveMaterialFromAvatarOperator([](const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + auto avatarManager = DependencyManager::get(); + auto avatar = avatarManager->getAvatarBySessionID(avatarID); + if (avatar) { + avatar->removeMaterial(material, parentMaterialName); + return true; + } + return false; + }); + + EntityTree::setGetEntityObjectOperator([this](const QUuid& id) -> QObject* { + auto entities = getEntities(); + if (auto entity = entities->renderableForEntityId(id)) { + return qobject_cast(entity.get()); + } + return nullptr; + }); + + EntityTree::setTextSizeOperator([this](const QUuid& id, const QString& text) { + auto entities = getEntities(); + if (auto entity = entities->renderableForEntityId(id)) { + if (auto renderable = std::dynamic_pointer_cast(entity)) { + return renderable->textSize(text); + } + } + return QSizeF(0.0f, 0.0f); + }); + + connect(this, &Application::aboutToQuit, [this]() { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + }); + + // Add periodic checks to send user activity data + static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; + static int NEARBY_AVATAR_RADIUS_METERS = 10; + + // setup the stats interval depending on if the 1s faster hearbeat was requested + static const QString FAST_STATS_ARG = "--fast-heartbeat"; + static int SEND_STATS_INTERVAL_MS = arguments().indexOf(FAST_STATS_ARG) != -1 ? 1000 : 10000; + + static glm::vec3 lastAvatarPosition = myAvatar->getWorldPosition(); + static glm::mat4 lastHMDHeadPose = getHMDSensorPose(); + static controller::Pose lastLeftHandPose = myAvatar->getLeftHandPose(); + static controller::Pose lastRightHandPose = myAvatar->getRightHandPose(); + + // Periodically send fps as a user activity event + QTimer* sendStatsTimer = new QTimer(this); + sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); // 10s, Qt::CoarseTimer acceptable + connect(sendStatsTimer, &QTimer::timeout, this, [this]() { + + QJsonObject properties = {}; + MemoryInfo memInfo; + if (getMemoryInfo(memInfo)) { + properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); + properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); + properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); + } + + // content location and build info - useful for filtering stats + auto addressManager = DependencyManager::get(); + auto currentDomain = addressManager->currentShareableAddress(true).toString(); // domain only + auto currentPath = addressManager->currentPath(true); // with orientation + properties["current_domain"] = currentDomain; + properties["current_path"] = currentPath; + properties["build_version"] = BuildInfo::VERSION; + + auto displayPlugin = qApp->getActiveDisplayPlugin(); + + properties["render_rate"] = getRenderLoopRate(); + properties["target_render_rate"] = getTargetRenderFrameRate(); + properties["present_rate"] = displayPlugin->presentRate(); + properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); + properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); + properties["stutter_rate"] = displayPlugin->stutterRate(); + properties["game_rate"] = getGameLoopRate(); + properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); + properties["hardware_stats"] = displayPlugin->getHardwareStats(); + + // deadlock watchdog related stats + properties["deadlock_watchdog_maxElapsed"] = (int)DeadlockWatchdogThread::_maxElapsed; + properties["deadlock_watchdog_maxElapsedAverage"] = (int)DeadlockWatchdogThread::_maxElapsedAverage; + + auto nodeList = DependencyManager::get(); + properties["packet_rate_in"] = nodeList->getInboundPPS(); + properties["packet_rate_out"] = nodeList->getOutboundPPS(); + properties["kbps_in"] = nodeList->getInboundKbps(); + properties["kbps_out"] = nodeList->getOutboundKbps(); + + SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); + SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); + SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); + SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); + SharedNodePointer messagesMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); + properties["entity_ping"] = entityServerNode ? entityServerNode->getPingMs() : -1; + properties["audio_ping"] = audioMixerNode ? audioMixerNode->getPingMs() : -1; + properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; + properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1; + properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; + properties["atp_in_kbps"] = assetServerNode ? assetServerNode->getInboundKbps() : 0.0f; + + auto loadingRequests = ResourceCache::getLoadingRequests(); + + QJsonArray loadingRequestsStats; + for (const auto& request : loadingRequests) { + QJsonObject requestStats; + requestStats["filename"] = request->getURL().fileName(); + requestStats["received"] = request->getBytesReceived(); + requestStats["total"] = request->getBytesTotal(); + requestStats["attempts"] = (int)request->getDownloadAttempts(); + loadingRequestsStats.append(requestStats); + } + + properties["active_downloads"] = loadingRequests.size(); + properties["pending_downloads"] = (int)ResourceCache::getPendingRequestCount(); + properties["active_downloads_details"] = loadingRequestsStats; + + auto statTracker = DependencyManager::get(); + + properties["processing_resources"] = statTracker->getStat("Processing").toInt(); + properties["pending_processing_resources"] = statTracker->getStat("PendingProcessing").toInt(); + + QJsonObject startedRequests; + startedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_STARTED).toInt(); + startedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_STARTED).toInt(); + startedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_STARTED).toInt(); + startedRequests["total"] = startedRequests["atp"].toInt() + startedRequests["http"].toInt() + + startedRequests["file"].toInt(); + properties["started_requests"] = startedRequests; + + QJsonObject successfulRequests; + successfulRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_SUCCESS).toInt(); + successfulRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_SUCCESS).toInt(); + successfulRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_SUCCESS).toInt(); + successfulRequests["total"] = successfulRequests["atp"].toInt() + successfulRequests["http"].toInt() + + successfulRequests["file"].toInt(); + properties["successful_requests"] = successfulRequests; + + QJsonObject failedRequests; + failedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_FAILED).toInt(); + failedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_FAILED).toInt(); + failedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_FAILED).toInt(); + failedRequests["total"] = failedRequests["atp"].toInt() + failedRequests["http"].toInt() + + failedRequests["file"].toInt(); + properties["failed_requests"] = failedRequests; + + QJsonObject cacheRequests; + cacheRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_CACHE).toInt(); + cacheRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_CACHE).toInt(); + cacheRequests["total"] = cacheRequests["atp"].toInt() + cacheRequests["http"].toInt(); + properties["cache_requests"] = cacheRequests; + + QJsonObject atpMappingRequests; + atpMappingRequests["started"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_STARTED).toInt(); + atpMappingRequests["failed"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_FAILED).toInt(); + atpMappingRequests["successful"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_SUCCESS).toInt(); + properties["atp_mapping_requests"] = atpMappingRequests; + + properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; + + QJsonObject bytesDownloaded; + auto atpBytes = statTracker->getStat(STAT_ATP_RESOURCE_TOTAL_BYTES).toLongLong(); + auto httpBytes = statTracker->getStat(STAT_HTTP_RESOURCE_TOTAL_BYTES).toLongLong(); + auto fileBytes = statTracker->getStat(STAT_FILE_RESOURCE_TOTAL_BYTES).toLongLong(); + bytesDownloaded["atp"] = atpBytes; + bytesDownloaded["http"] = httpBytes; + bytesDownloaded["file"] = fileBytes; + bytesDownloaded["total"] = atpBytes + httpBytes + fileBytes; + properties["bytes_downloaded"] = bytesDownloaded; + + auto myAvatar = getMyAvatar(); + glm::vec3 avatarPosition = myAvatar->getWorldPosition(); + properties["avatar_has_moved"] = lastAvatarPosition != avatarPosition; + lastAvatarPosition = avatarPosition; + + auto entityScriptingInterface = DependencyManager::get(); + auto entityActivityTracking = entityScriptingInterface->getActivityTracking(); + entityScriptingInterface->resetActivityTracking(); + properties["added_entity_cnt"] = entityActivityTracking.addedEntityCount; + properties["deleted_entity_cnt"] = entityActivityTracking.deletedEntityCount; + properties["edited_entity_cnt"] = entityActivityTracking.editedEntityCount; + + NodeToOctreeSceneStats* octreeServerSceneStats = getOcteeSceneStats(); + unsigned long totalServerOctreeElements = 0; + for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) { + totalServerOctreeElements += i->second.getTotalElements(); + } + + properties["local_octree_elements"] = (qint64) OctreeElement::getInternalNodeCount(); + properties["server_octree_elements"] = (qint64) totalServerOctreeElements; + + properties["active_display_plugin"] = getActiveDisplayPlugin()->getName(); + properties["using_hmd"] = isHMDMode(); + + _autoSwitchDisplayModeSupportedHMDPlugin = nullptr; + foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { + if (displayPlugin->isHmd() && + displayPlugin->getSupportsAutoSwitch()) { + _autoSwitchDisplayModeSupportedHMDPlugin = displayPlugin; + _autoSwitchDisplayModeSupportedHMDPluginName = + _autoSwitchDisplayModeSupportedHMDPlugin->getName(); + _previousHMDWornStatus = + _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); + break; + } + } + + if (_autoSwitchDisplayModeSupportedHMDPlugin) { + if (getActiveDisplayPlugin() != _autoSwitchDisplayModeSupportedHMDPlugin && + !_autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { + startHMDStandBySession(); + } + // Poll periodically to check whether the user has worn HMD or not. Switch Display mode accordingly. + // If the user wears HMD then switch to VR mode. If the user removes HMD then switch to Desktop mode. + QTimer* autoSwitchDisplayModeTimer = new QTimer(this); + connect(autoSwitchDisplayModeTimer, SIGNAL(timeout()), this, SLOT(switchDisplayMode())); + autoSwitchDisplayModeTimer->start(INTERVAL_TO_CHECK_HMD_WORN_STATUS); + } + + auto glInfo = getGLContextData(); + properties["gl_info"] = glInfo; + properties["gpu_used_memory"] = (int)BYTES_TO_MB(gpu::Context::getUsedGPUMemSize()); + properties["gpu_free_memory"] = (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemSize()); + properties["gpu_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerGPUAverage()); + properties["batch_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerBatchAverage()); + properties["ideal_thread_count"] = QThread::idealThreadCount(); + + auto hmdHeadPose = getHMDSensorPose(); + properties["hmd_head_pose_changed"] = isHMDMode() && (hmdHeadPose != lastHMDHeadPose); + lastHMDHeadPose = hmdHeadPose; + + auto leftHandPose = myAvatar->getLeftHandPose(); + auto rightHandPose = myAvatar->getRightHandPose(); + // controller::Pose considers two poses to be different if either are invalid. In our case, we actually + // want to consider the pose to be unchanged if it was invalid and still is invalid, so we check that first. + properties["hand_pose_changed"] = + ((leftHandPose.valid || lastLeftHandPose.valid) && (leftHandPose != lastLeftHandPose)) + || ((rightHandPose.valid || lastRightHandPose.valid) && (rightHandPose != lastRightHandPose)); + lastLeftHandPose = leftHandPose; + lastRightHandPose = rightHandPose; + + UserActivityLogger::getInstance().logAction("stats", properties); + }); + sendStatsTimer->start(); + + // Periodically check for count of nearby avatars + static int lastCountOfNearbyAvatars = -1; + QTimer* checkNearbyAvatarsTimer = new QTimer(this); + checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); // 10 seconds, Qt::CoarseTimer ok + connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, []() { + auto avatarManager = DependencyManager::get(); + int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getWorldPosition(), + NEARBY_AVATAR_RADIUS_METERS) - 1; + if (nearbyAvatars != lastCountOfNearbyAvatars) { + lastCountOfNearbyAvatars = nearbyAvatars; + UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); + } + }); + checkNearbyAvatarsTimer->start(); + + // Track user activity event when we receive a mute packet + auto onMutedByMixer = []() { + UserActivityLogger::getInstance().logAction("received_mute_packet"); + }; + connect(DependencyManager::get().data(), &AudioClient::mutedByMixer, this, onMutedByMixer); + + // Track when the address bar is opened + auto onAddressBarShown = [this]() { + // Record time + UserActivityLogger::getInstance().logAction("opened_address_bar", { { "uptime_ms", _sessionRunTimer.elapsed() } }); + }; + connect(DependencyManager::get().data(), &DialogsManager::addressBarShown, this, onAddressBarShown); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender(); + EntityEditPacketSender* entityPacketSender = static_cast(packetSender); + entityPacketSender->setMyAvatar(myAvatar.get()); + + connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); + connect(_window, SIGNAL(windowMinimizedChanged(bool)), this, SLOT(windowMinimizedChanged(bool))); + qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); + + EntityTreeRenderer::setEntitiesShouldFadeFunction([this]() { + SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); + return entityServerNode && !isPhysicsEnabled(); + }); + + _snapshotSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/snapshot/snap.wav")); + + // Monitor model assets (e.g., from Clara.io) added to the world that may need resizing. + static const int ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS = 1000; + _addAssetToWorldResizeTimer.setInterval(ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable + connect(&_addAssetToWorldResizeTimer, &QTimer::timeout, this, &Application::addAssetToWorldCheckModelSize); + + // Auto-update and close adding asset to world info message box. + static const int ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS = 5000; + _addAssetToWorldInfoTimer.setInterval(ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS); // 5s, Qt::CoarseTimer acceptable + _addAssetToWorldInfoTimer.setSingleShot(true); + connect(&_addAssetToWorldInfoTimer, &QTimer::timeout, this, &Application::addAssetToWorldInfoTimeout); + static const int ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS = 8000; + _addAssetToWorldErrorTimer.setInterval(ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS); // 8s, Qt::CoarseTimer acceptable + _addAssetToWorldErrorTimer.setSingleShot(true); + connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout); + + connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose); + connect(&domainHandler, &DomainHandler::domainURLChanged, this, &Application::addAssetToWorldMessageClose); + connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &Application::addAssetToWorldMessageClose); + + updateSystemTabletMode(); + + connect(&_myCamera, &Camera::modeUpdated, this, &Application::cameraModeChanged); + + DependencyManager::get()->setShouldPickHUDOperator([]() { return DependencyManager::get()->isHMDMode(); }); + DependencyManager::get()->setCalculatePos2DFromHUDOperator([this](const glm::vec3& intersection) { + const glm::vec2 MARGIN(25.0f); + glm::vec2 maxPos = _controllerScriptingInterface->getViewportDimensions() - MARGIN; + glm::vec2 pos2D = DependencyManager::get()->overlayFromWorldPoint(intersection); + return glm::max(MARGIN, glm::min(pos2D, maxPos)); + }); + + // Setup the mouse ray pick and related operators + { + auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_LOCAL_ENTITIES()), 0.0f, true); + mouseRayPick->parentTransform = std::make_shared(); + mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE); + auto mouseRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, mouseRayPick); + DependencyManager::get()->setMouseRayPickID(mouseRayPickID); + } + DependencyManager::get()->setMouseRayPickResultOperator([](unsigned int rayPickID) { + RayToEntityIntersectionResult entityResult; + entityResult.intersects = false; + auto pickResult = DependencyManager::get()->getPrevPickResultTyped(rayPickID); + if (pickResult) { + entityResult.intersects = pickResult->type != IntersectionType::NONE; + if (entityResult.intersects) { + entityResult.intersection = pickResult->intersection; + entityResult.distance = pickResult->distance; + entityResult.surfaceNormal = pickResult->surfaceNormal; + entityResult.entityID = pickResult->objectID; + entityResult.extraInfo = pickResult->extraInfo; + } + } + return entityResult; + }); + DependencyManager::get()->setSetPrecisionPickingOperator([](unsigned int rayPickID, bool value) { + DependencyManager::get()->setPrecisionPicking(rayPickID, value); + }); + + EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { + if (billboardMode == BillboardMode::YAW) { + //rotate about vertical to face the camera + glm::vec3 dPosition = frustumPos - position; + // If x and z are 0, atan(x, z) is undefined, so default to 0 degrees + float yawRotation = dPosition.x == 0.0f && dPosition.z == 0.0f ? 0.0f : glm::atan(dPosition.x, dPosition.z); + return glm::quat(glm::vec3(0.0f, yawRotation, 0.0f)); + } else if (billboardMode == BillboardMode::FULL) { + // use the referencial from the avatar, y isn't always up + glm::vec3 avatarUP = DependencyManager::get()->getMyAvatar()->getWorldOrientation() * Vectors::UP; + // check to see if glm::lookAt will work / using glm::lookAt variable name + glm::highp_vec3 s(glm::cross(position - frustumPos, avatarUP)); + + // make sure s is not NaN for any component + if (glm::length2(s) > 0.0f) { + return glm::conjugate(glm::toQuat(glm::lookAt(frustumPos, position, avatarUP))); + } + } + return rotation; + }); + EntityItem::setPrimaryViewFrustumPositionOperator([this]() { + ViewFrustum viewFrustum; + copyViewFrustum(viewFrustum); + return viewFrustum.getPosition(); + }); + + render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { + bool isTablet = url == TabletScriptingInterface::QML; + if (htmlContent) { + webSurface = DependencyManager::get()->acquire(render::entities::WebEntityRenderer::QML); + cachedWebSurface = true; + auto rootItemLoadedFunctor = [url, webSurface] { + webSurface->getRootItem()->setProperty(render::entities::WebEntityRenderer::URL_PROPERTY, url); + }; + if (webSurface->getRootItem()) { + rootItemLoadedFunctor(); + } else { + QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); + } + auto surfaceContext = webSurface->getSurfaceContext(); + surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); + } else { + // FIXME: the tablet should use the OffscreenQmlSurfaceCache + webSurface = QSharedPointer(new OffscreenQmlSurface(), [](OffscreenQmlSurface* webSurface) { + AbstractViewStateInterface::instance()->sendLambdaEvent([webSurface] { + // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown + // if the application has already stopped its event loop, delete must be explicit + delete webSurface; + }); + }); + auto rootItemLoadedFunctor = [webSurface, url, isTablet] { + Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString()); + }; + if (webSurface->getRootItem()) { + rootItemLoadedFunctor(); + } else { + QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); + } + webSurface->load(url); + cachedWebSurface = false; + } + const uint8_t DEFAULT_MAX_FPS = 10; + const uint8_t TABLET_FPS = 90; + webSurface->setMaxFps(isTablet ? TABLET_FPS : DEFAULT_MAX_FPS); + }); + render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([this](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { + QQuickItem* rootItem = webSurface->getRootItem(); + + // Fix for crash in QtWebEngineCore when rapidly switching domains + // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. + if (rootItem && !cachedWebSurface) { + // stop loading + QMetaObject::invokeMethod(rootItem, "stop"); + } + + webSurface->pause(); + + for (auto& connection : connections) { + QObject::disconnect(connection); + } + connections.clear(); + + // If the web surface was fetched out of the cache, release it back into the cache + if (cachedWebSurface) { + // If it's going back into the cache make sure to explicitly set the URL to a blank page + // in order to stop any resource consumption or audio related to the page. + if (rootItem) { + rootItem->setProperty("url", "about:blank"); + } + auto offscreenCache = DependencyManager::get(); + if (offscreenCache) { + offscreenCache->release(render::entities::WebEntityRenderer::QML, webSurface); + } + cachedWebSurface = false; + } + webSurface.reset(); + }); + + // Preload Tablet sounds + DependencyManager::get()->setEntityTree(qApp->getEntities()->getTree()); + DependencyManager::get()->preloadSounds(); + DependencyManager::get()->createKeyboard(); + + _pendingIdleEvent = false; + _graphicsEngine.startup(); + + qCDebug(interfaceapp) << "Metaverse session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID()); + +#if defined(Q_OS_ANDROID) + connect(&AndroidHelper::instance(), &AndroidHelper::beforeEnterBackground, this, &Application::beforeEnterBackground); + connect(&AndroidHelper::instance(), &AndroidHelper::enterBackground, this, &Application::enterBackground); + connect(&AndroidHelper::instance(), &AndroidHelper::enterForeground, this, &Application::enterForeground); + connect(&AndroidHelper::instance(), &AndroidHelper::toggleAwayMode, this, &Application::toggleAwayMode); + AndroidHelper::instance().notifyLoadComplete(); +#endif + pauseUntilLoginDetermined(); +} + +void Application::updateVerboseLogging() { + auto menu = Menu::getInstance(); + if (!menu) { + return; + } + bool enable = menu->isOptionChecked(MenuOption::VerboseLogging); + + QString rules = + "hifi.*.info=%1\n" + "hifi.audio-stream.debug=false\n" + "hifi.audio-stream.info=false"; + rules = rules.arg(enable ? "true" : "false"); + QLoggingCategory::setFilterRules(rules); +} + +void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { + DomainHandler::ConnectionRefusedReason reasonCode = static_cast(reasonCodeInt); + + if (reasonCode == DomainHandler::ConnectionRefusedReason::TooManyUsers && !extraInfo.isEmpty()) { + DependencyManager::get()->handleLookupString(extraInfo); + return; + } + + switch (reasonCode) { + case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: + case DomainHandler::ConnectionRefusedReason::TooManyUsers: + case DomainHandler::ConnectionRefusedReason::Unknown: { + QString message = "Unable to connect to the location you are visiting.\n"; + message += reasonMessage; + OffscreenUi::asyncWarning("", message); + getMyAvatar()->setWorldVelocity(glm::vec3(0.0f)); + break; + } + default: + // nothing to do. + break; + } +} + +QString Application::getUserAgent() { + if (QThread::currentThread() != thread()) { + QString userAgent; + + BLOCKING_INVOKE_METHOD(this, "getUserAgent", Q_RETURN_ARG(QString, userAgent)); + + return userAgent; + } + + QString userAgent = "Mozilla/5.0 (HighFidelityInterface/" + BuildInfo::VERSION + "; " + + QSysInfo::productType() + " " + QSysInfo::productVersion() + ")"; + + auto formatPluginName = [](QString name) -> QString { return name.trimmed().replace(" ", "-"); }; + + // For each plugin, add to userAgent + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + for (auto& dp : displayPlugins) { + if (dp->isActive() && dp->isHmd()) { + userAgent += " " + formatPluginName(dp->getName()); + } + } + auto inputPlugins= PluginManager::getInstance()->getInputPlugins(); + for (auto& ip : inputPlugins) { + if (ip->isActive()) { + userAgent += " " + formatPluginName(ip->getName()); + } + } + // for codecs, we include all of them, even if not active + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + for (auto& cp : codecPlugins) { + userAgent += " " + formatPluginName(cp->getName()); + } + + return userAgent; +} + +void Application::toggleTabletUI(bool shouldOpen) const { + auto hmd = DependencyManager::get(); + if (!(shouldOpen && hmd->getShouldShowTablet())) { + auto HMD = DependencyManager::get(); + HMD->toggleShouldShowTablet(); + + if (!HMD->getShouldShowTablet()) { + DependencyManager::get()->setRaised(false); + _window->activateWindow(); + auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); + tablet->unfocus(); + } + } +} + +void Application::checkChangeCursor() { + QMutexLocker locker(&_changeCursorLock); + if (_cursorNeedsChanging) { +#ifdef Q_OS_MAC + auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget +#else + // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the + // window menu, which is a pain, so only hide it for the GL surface + auto cursorTarget = _glWidget; +#endif + cursorTarget->setCursor(_desiredCursor); + + _cursorNeedsChanging = false; + } +} + +void Application::showCursor(const Cursor::Icon& cursor) { + QMutexLocker locker(&_changeCursorLock); + + auto managedCursor = Cursor::Manager::instance().getCursor(); + auto curIcon = managedCursor->getIcon(); + if (curIcon != cursor) { + managedCursor->setIcon(cursor); + curIcon = cursor; + } + _desiredCursor = cursor == Cursor::Icon::SYSTEM ? Qt::ArrowCursor : Qt::BlankCursor; + _cursorNeedsChanging = true; +} + +void Application::updateHeartbeat() const { + DeadlockWatchdogThread::updateHeartbeat(); +} + +void Application::onAboutToQuit() { + // quickly save AvatarEntityData before the EntityTree is dismantled + getMyAvatar()->saveAvatarEntityDataToSettings(); + + emit beforeAboutToQuit(); + + if (getLoginDialogPoppedUp() && _firstRun.get()) { + _firstRun.set(false); + } + + foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { + if (inputPlugin->isActive()) { + inputPlugin->deactivate(); + } + } + + // The active display plugin needs to be loaded before the menu system is active, + // so its persisted explicitly here + Setting::Handle{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME }.set(getActiveDisplayPlugin()->getName()); + + loginDialogPoppedUp.set(false); + + getActiveDisplayPlugin()->deactivate(); + if (_autoSwitchDisplayModeSupportedHMDPlugin + && _autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { + _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); + } + // use the CloseEventSender via a QThread to send an event that says the user asked for the app to close + DependencyManager::get()->startThread(); + + // Hide Running Scripts dialog so that it gets destroyed in an orderly manner; prevents warnings at shutdown. +#if !defined(DISABLE_QML) + getOffscreenUI()->hide("RunningScripts"); +#endif + + _aboutToQuit = true; + + cleanupBeforeQuit(); +} + +void Application::cleanupBeforeQuit() { + // add a logline indicating if QTWEBENGINE_REMOTE_DEBUGGING is set or not + QString webengineRemoteDebugging = QProcessEnvironment::systemEnvironment().value("QTWEBENGINE_REMOTE_DEBUGGING", "false"); + qCDebug(interfaceapp) << "QTWEBENGINE_REMOTE_DEBUGGING =" << webengineRemoteDebugging; + + DependencyManager::prepareToExit(); + + if (tracing::enabled()) { + auto tracer = DependencyManager::get(); + tracer->stopTracing(); + auto outputFile = property(hifi::properties::TRACING).toString(); + tracer->serialize(outputFile); + } + + // Stop third party processes so that they're not left running in the event of a subsequent shutdown crash. +#ifdef HAVE_DDE + DependencyManager::get()->setEnabled(false); +#endif +#ifdef HAVE_IVIEWHMD + DependencyManager::get()->setEnabled(false, true); +#endif + AnimDebugDraw::getInstance().shutdown(); + + // FIXME: once we move to shared pointer for the INputDevice we shoud remove this naked delete: + _applicationStateDevice.reset(); + + { + if (_keyboardFocusHighlightID != UNKNOWN_ENTITY_ID) { + DependencyManager::get()->deleteEntity(_keyboardFocusHighlightID); + _keyboardFocusHighlightID = UNKNOWN_ENTITY_ID; + } + } + + { + auto nodeList = DependencyManager::get(); + + // send the domain a disconnect packet, force stoppage of domain-server check-ins + nodeList->getDomainHandler().disconnect(); + nodeList->setIsShuttingDown(true); + + // tell the packet receiver we're shutting down, so it can drop packets + nodeList->getPacketReceiver().setShouldDropPackets(true); + } + + getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts + + // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) + QThreadPool::globalInstance()->clear(); + + DependencyManager::destroy(); + + // FIXME: Something is still holding on to the ScriptEnginePointers contained in ScriptEngines, and they hold backpointers to ScriptEngines, + // so this doesn't shut down properly + DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts + // These classes hold ScriptEnginePointers, so they must be destroyed before ScriptEngines + // Must be done after shutdownScripting in case any scripts try to access these things + { + DependencyManager::destroy(); + EntityTreePointer tree = getEntities()->getTree(); + tree->setSimulation(nullptr); + DependencyManager::destroy(); + } + DependencyManager::destroy(); + + bool keepMeLoggedIn = Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, false).get(); + if (!keepMeLoggedIn) { + DependencyManager::get()->removeAccountFromFile(); + } + + _displayPlugin.reset(); + PluginManager::getInstance()->shutdown(); + + // Cleanup all overlays after the scripts, as scripts might add more + _overlays.cleanupAllOverlays(); + + // first stop all timers directly or by invokeMethod + // depending on what thread they run in + locationUpdateTimer.stop(); + identityPacketTimer.stop(); + pingTimer.stop(); + + // Wait for the settings thread to shut down, and save the settings one last time when it's safe + if (_settingsGuard.wait()) { + // save state + saveSettings(); + } + + _window->saveGeometry(); + + // Destroy third party processes after scripts have finished using them. +#ifdef HAVE_DDE + DependencyManager::destroy(); +#endif +#ifdef HAVE_IVIEWHMD + DependencyManager::destroy(); +#endif + + DependencyManager::destroy(); // Must be destroyed before TabletScriptingInterface + + // stop QML + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + DependencyManager::destroy(); + + if (_snapshotSoundInjector != nullptr) { + _snapshotSoundInjector->stop(); + } + + // destroy Audio so it and its threads have a chance to go down safely + // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine + QMetaObject::invokeMethod(DependencyManager::get().data(), "stop"); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + // The PointerManager must be destroyed before the PickManager because when a Pointer is deleted, + // it accesses the PickManager to delete its associated Pick + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; +} + +Application::~Application() { + // remove avatars from physics engine + auto avatarManager = DependencyManager::get(); + avatarManager->clearOtherAvatars(); + + PhysicsEngine::Transaction transaction; + avatarManager->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + avatarManager->handleProcessedPhysicsTransaction(transaction); + + avatarManager->deleteAllAvatars(); + + auto myCharacterController = getMyAvatar()->getCharacterController(); + myCharacterController->clearDetailedMotionStates(); + + myCharacterController->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + myCharacterController->handleProcessedPhysicsTransaction(transaction); + + _physicsEngine->setCharacterController(nullptr); + + // the _shapeManager should have zero references + _shapeManager.collectGarbage(); + assert(_shapeManager.getNumShapes() == 0); + + // shutdown graphics engine + _graphicsEngine.shutdown(); + + _gameWorkload.shutdown(); + + DependencyManager::destroy(); + + _entityClipboard->eraseAllOctreeElements(); + _entityClipboard.reset(); + + _octreeProcessor.terminate(); + _entityEditSender.terminate(); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + steamClient->shutdown(); + } + + if (auto oculusPlatform = PluginManager::getInstance()->getOculusPlatformPlugin()) { + oculusPlatform->shutdown(); + } + + DependencyManager::destroy(); + + DependencyManager::destroy(); // must be destroyed before the FramebufferCache + + DependencyManager::destroy(); + + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + DependencyManager::get()->cleanup(); + + // remove the NodeList from the DependencyManager + DependencyManager::destroy(); + +#if 0 + ConnexionClient::getInstance().destroy(); +#endif + // The window takes ownership of the menu, so this has the side effect of destroying it. + _window->setMenuBar(nullptr); + + _window->deleteLater(); + + // make sure that the quit event has finished sending before we take the application down + auto closeEventSender = DependencyManager::get(); + while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) { + // sleep a little so we're not spinning at 100% + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + // quit the thread used by the closure event sender + closeEventSender->thread()->quit(); + + // Can't log to file past this point, FileLogger about to be deleted + qInstallMessageHandler(LogHandler::verboseMessageHandler); +} + +void Application::initializeGL() { + qCDebug(interfaceapp) << "Created Display Window."; + +#ifdef DISABLE_QML + setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); +#endif + + // initialize glut for shape drawing; Qt apparently initializes it on OS X + if (_isGLInitialized) { + return; + } else { + _isGLInitialized = true; + } + + _glWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); + + // When loading QtWebEngineWidgets, it creates a global share context on startup. + // We have to account for this possibility by checking here for an existing + // global share context + auto globalShareContext = qt_gl_global_share_context(); + +#if !defined(DISABLE_QML) + // Build a shared canvas / context for the Chromium processes + if (!globalShareContext) { + // Chromium rendering uses some GL functions that prevent nSight from capturing + // frames, so we only create the shared context if nsight is NOT active. + if (!nsightActive()) { + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->setObjectName("ChromiumShareContext"); + auto format =QSurfaceFormat::defaultFormat(); +#ifdef Q_OS_MAC + // On mac, the primary shared OpenGL context must be a 3.2 core context, + // or chromium flips out and spews error spam (but renders fine) + format.setMajorVersion(3); + format.setMinorVersion(2); +#endif + _chromiumShareContext->setFormat(format); + _chromiumShareContext->create(); + if (!_chromiumShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make chromium shared context current"); + } + globalShareContext = _chromiumShareContext->getContext(); + qt_gl_set_global_share_context(globalShareContext); + _chromiumShareContext->doneCurrent(); + } + } +#endif + + + _glWidget->createContext(globalShareContext); + + if (!_glWidget->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make window context current"); + } + +#if !defined(DISABLE_QML) + // Disable signed distance field font rendering on ATI/AMD GPUs, due to + // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app + std::string vendor{ (const char*)glGetString(GL_VENDOR) }; + if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { + qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); + } +#endif + + if (!globalShareContext) { + globalShareContext = _glWidget->qglContext(); + qt_gl_set_global_share_context(globalShareContext); + } + + // Build a shared canvas / context for the QML rendering +#if !defined(DISABLE_QML) + { + _qmlShareContext = new OffscreenGLCanvas(); + _qmlShareContext->setObjectName("QmlShareContext"); + _qmlShareContext->create(globalShareContext); + if (!_qmlShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make QML shared context current"); + } + OffscreenQmlSurface::setSharedContext(_qmlShareContext->getContext()); + _qmlShareContext->doneCurrent(); + if (!_glWidget->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make window context current"); + } + } +#endif + + + // Build an offscreen GL context for the main thread. + _glWidget->makeCurrent(); + glClearColor(0.2f, 0.2f, 0.2f, 1); + glClear(GL_COLOR_BUFFER_BIT); + _glWidget->swapBuffers(); + + _graphicsEngine.initializeGPU(_glWidget); +} + +void Application::initializeDisplayPlugins() { + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + Setting::Handle activeDisplayPluginSetting{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME, displayPlugins.at(0)->getName() }; + auto lastActiveDisplayPluginName = activeDisplayPluginSetting.get(); + + auto defaultDisplayPlugin = displayPlugins.at(0); + // Once time initialization code + DisplayPluginPointer targetDisplayPlugin; + foreach(auto displayPlugin, displayPlugins) { + displayPlugin->setContext(_graphicsEngine.getGPUContext()); + if (displayPlugin->getName() == lastActiveDisplayPluginName) { + targetDisplayPlugin = displayPlugin; + } + QObject::connect(displayPlugin.get(), &DisplayPlugin::recommendedFramebufferSizeChanged, + [this](const QSize& size) { resizeGL(); }); + QObject::connect(displayPlugin.get(), &DisplayPlugin::resetSensorsRequested, this, &Application::requestReset); + if (displayPlugin->isHmd()) { + auto hmdDisplayPlugin = dynamic_cast(displayPlugin.get()); + QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdMountedChanged, + DependencyManager::get().data(), &HMDScriptingInterface::mountedChanged); + QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdVisibleChanged, this, &Application::hmdVisibleChanged); + } + } + + // The default display plugin needs to be activated first, otherwise the display plugin thread + // may be launched by an external plugin, which is bad + setDisplayPlugin(defaultDisplayPlugin); + + // Now set the desired plugin if it's not the same as the default plugin + if (targetDisplayPlugin && (targetDisplayPlugin != defaultDisplayPlugin)) { + setDisplayPlugin(targetDisplayPlugin); + } + + // Submit a default frame to render until the engine starts up + updateRenderArgs(0.0f); +} + +void Application::initializeRenderEngine() { + // FIXME: on low end systems os the shaders take up to 1 minute to compile, so we pause the deadlock watchdog thread. + DeadlockWatchdogThread::withPause([&] { + _graphicsEngine.initializeRender(DISABLE_DEFERRED); + DependencyManager::get()->registerKeyboardHighlighting(); + }); +} + +extern void setupPreferences(); +#if !defined(DISABLE_QML) +static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active = false); +#endif + +void Application::showLoginScreen() { +#if !defined(DISABLE_QML) + auto accountManager = DependencyManager::get(); + auto dialogsManager = DependencyManager::get(); + if (!accountManager->isLoggedIn()) { + if (!isHMDMode()) { + auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); + toolbar->writeProperty("visible", false); + } + _loginDialogPoppedUp = true; + dialogsManager->showLoginDialog(); + emit loginDialogFocusEnabled(); + QJsonObject loginData = {}; + loginData["action"] = "login dialog popped up"; + UserActivityLogger::getInstance().logAction("encourageLoginDialog", loginData); + _window->setWindowTitle("High Fidelity Interface"); + } else { + resumeAfterLoginDialogActionTaken(); + } + _loginDialogPoppedUp = !accountManager->isLoggedIn(); + loginDialogPoppedUp.set(_loginDialogPoppedUp); +#else + resumeAfterLoginDialogActionTaken(); +#endif +} + +void Application::initializeUi() { + AddressBarDialog::registerType(); + ErrorDialog::registerType(); + LoginDialog::registerType(); + Tooltip::registerType(); + UpdateDialog::registerType(); + QmlContextCallback commerceCallback = [](QQmlContext* context) { + context->setContextProperty("Commerce", DependencyManager::get().data()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/checkout/Checkout.qml" }, + QUrl{ "hifi/commerce/common/CommerceLightbox.qml" }, + QUrl{ "hifi/commerce/common/EmulatedMarketplaceHeader.qml" }, + QUrl{ "hifi/commerce/common/FirstUseTutorial.qml" }, + QUrl{ "hifi/commerce/common/sendAsset/SendAsset.qml" }, + QUrl{ "hifi/commerce/common/SortableListModel.qml" }, + QUrl{ "hifi/commerce/inspectionCertificate/InspectionCertificate.qml" }, + QUrl{ "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"}, + QUrl{ "hifi/commerce/purchases/PurchasedItem.qml" }, + QUrl{ "hifi/commerce/purchases/Purchases.qml" }, + QUrl{ "hifi/commerce/wallet/Help.qml" }, + QUrl{ "hifi/commerce/wallet/NeedsLogIn.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseChange.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseModal.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseSelection.qml" }, + QUrl{ "hifi/commerce/wallet/Wallet.qml" }, + QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, + QUrl{ "hifi/commerce/wallet/WalletSetup.qml" }, + QUrl{ "hifi/dialogs/security/Security.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageChange.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, + QUrl{ "hifi/tablet/TabletMenu.qml" }, + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, commerceCallback); + + QmlContextCallback marketplaceCallback = [](QQmlContext* context) { + context->setContextProperty("MarketplaceScriptingInterface", new QmlMarketplace()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, marketplaceCallback); + + QmlContextCallback platformInfoCallback = [](QQmlContext* context) { + context->setContextProperty("PlatformInfo", new PlatformInfoScriptingInterface()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, platformInfoCallback); + + QmlContextCallback ttsCallback = [](QQmlContext* context) { + context->setContextProperty("TextToSpeech", DependencyManager::get().data()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/tts/TTS.qml" } + }, ttsCallback); + qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); + qmlRegisterType("Hifi", 1, 0, "Preference"); + qmlRegisterType("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine"); + + { + auto tabletScriptingInterface = DependencyManager::get(); + tabletScriptingInterface->getTablet(SYSTEM_TABLET); + } + + auto offscreenUi = getOffscreenUI(); + connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootContextCreated, + this, &Application::onDesktopRootContextCreated); + connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootItemCreated, + this, &Application::onDesktopRootItemCreated); + +#if !defined(DISABLE_QML) + offscreenUi->setProxyWindow(_window->windowHandle()); + // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to + // support the window management and scripting proxies for VR use + DeadlockWatchdogThread::withPause([&] { + offscreenUi->createDesktop(PathUtils::qmlUrl("hifi/Desktop.qml")); + }); + // FIXME either expose so that dialogs can set this themselves or + // do better detection in the offscreen UI of what has focus + offscreenUi->setNavigationFocused(false); +#else + _window->setMenuBar(new Menu()); +#endif + + setupPreferences(); + +#if !defined(DISABLE_QML) + _glWidget->installEventFilter(offscreenUi.data()); + offscreenUi->setMouseTranslator([=](const QPointF& pt) { + QPointF result = pt; + auto displayPlugin = getActiveDisplayPlugin(); + if (displayPlugin->isHmd()) { + getApplicationCompositor().handleRealMouseMoveEvent(false); + auto resultVec = getApplicationCompositor().getReticlePosition(); + result = QPointF(resultVec.x, resultVec.y); + } + return result.toPoint(); + }); + offscreenUi->resume(); +#endif + connect(_window, &MainWindow::windowGeometryChanged, [this](const QRect& r){ + resizeGL(); + if (_touchscreenVirtualPadDevice) { + _touchscreenVirtualPadDevice->resize(); + } + }); + + // This will set up the input plugins UI + _activeInputPlugins.clear(); + foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { + if (KeyboardMouseDevice::NAME == inputPlugin->getName()) { + _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); + } + if (TouchscreenDevice::NAME == inputPlugin->getName()) { + _touchscreenDevice = std::dynamic_pointer_cast(inputPlugin); + } + if (TouchscreenVirtualPadDevice::NAME == inputPlugin->getName()) { + _touchscreenVirtualPadDevice = std::dynamic_pointer_cast(inputPlugin); +#if defined(ANDROID_APP_INTERFACE) + auto& virtualPadManager = VirtualPad::Manager::instance(); + connect(&virtualPadManager, &VirtualPad::Manager::hapticFeedbackRequested, + this, [](int duration) { + AndroidHelper::instance().performHapticFeedback(duration); + }); +#endif + } + } + + auto compositorHelper = DependencyManager::get(); + connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, this, [=] { + if (isHMDMode()) { + auto compositorHelper = DependencyManager::get(); // don't capture outer smartpointer + showCursor(compositorHelper->getAllowMouseCapture() ? + Cursor::Manager::lookupIcon(_preferredCursor.get()) : + Cursor::Icon::SYSTEM); + } + }); + +#if !defined(DISABLE_QML) + // Pre-create a couple of offscreen surfaces to speed up tablet UI + auto offscreenSurfaceCache = DependencyManager::get(); + offscreenSurfaceCache->setOnRootContextCreated([&](const QString& rootObject, QQmlContext* surfaceContext) { + if (rootObject == TabletScriptingInterface::QML) { + // in Qt 5.10.0 there is already an "Audio" object in the QML context + // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" + surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + } + }); + + offscreenSurfaceCache->reserve(TabletScriptingInterface::QML, 1); + offscreenSurfaceCache->reserve(render::entities::WebEntityRenderer::QML, 2); +#endif + + flushMenuUpdates(); + +#if !defined(DISABLE_QML) + // Now that the menu is instantiated, ensure the display plugin menu is properly updated + { + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + // first sort the plugins into groupings: standard, advanced, developer + std::stable_sort(displayPlugins.begin(), displayPlugins.end(), + [](const DisplayPluginPointer& a, const DisplayPluginPointer& b) -> bool { return a->getGrouping() < b->getGrouping(); }); + int dpIndex = 1; + // concatenate the groupings into a single list in the order: standard, advanced, developer + for(const auto& displayPlugin : displayPlugins) { + addDisplayPluginToMenu(displayPlugin, dpIndex, _displayPlugin == displayPlugin); + dpIndex++; + } + + // after all plugins have been added to the menu, add a separator to the menu + auto parent = getPrimaryMenu()->getMenu(MenuOption::OutputMenu); + parent->addSeparator(); + } +#endif + + + // The display plugins are created before the menu now, so we need to do this here to hide the menu bar + // now that it exists + if (_window && _window->isFullScreen()) { + setFullscreen(nullptr, true); + } + + + setIsInterstitialMode(true); +} + + +void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { + auto engine = surfaceContext->engine(); + // in Qt 5.10.0 there is already an "Audio" object in the QML context + // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" + surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); + + surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); + surfaceContext->setContextProperty("AudioScope", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); + surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); + _fileDownload = new FileScriptingInterface(engine); + surfaceContext->setContextProperty("File", _fileDownload); + connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip); + surfaceContext->setContextProperty("MyAvatar", getMyAvatar().get()); + surfaceContext->setContextProperty("Messages", DependencyManager::get().data()); + surfaceContext->setContextProperty("Recording", DependencyManager::get().data()); + surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); + surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("FrameTimings", &_graphicsEngine._frameTimingsScriptingInterface); + surfaceContext->setContextProperty("Rates", new RatesScriptingInterface(this)); + + surfaceContext->setContextProperty("TREE_SCALE", TREE_SCALE); + // FIXME Quat and Vec3 won't work with QJSEngine used by QML + surfaceContext->setContextProperty("Quat", new Quat()); + surfaceContext->setContextProperty("Vec3", new Vec3()); + surfaceContext->setContextProperty("Uuid", new ScriptUUID()); + surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); + surfaceContext->setContextProperty("Keyboard", DependencyManager::get().data()); + + surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); + surfaceContext->setContextProperty("Users", DependencyManager::get().data()); + + surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Camera", &_myCamera); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + surfaceContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); +#endif + + surfaceContext->setContextProperty("Overlays", &_overlays); + surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); + surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); + + // Caches + surfaceContext->setContextProperty("AnimationCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("TextureCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("ModelCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); + + surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); + + surfaceContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); + surfaceContext->setContextProperty("FaceTracker", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); + surfaceContext->setContextProperty("Scene", DependencyManager::get().data()); + surfaceContext->setContextProperty("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); + surfaceContext->setContextProperty("Workload", _gameWorkload._engine->getConfiguration().get()); + surfaceContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); + surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); + + surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); + + surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("Selection", DependencyManager::get().data()); + surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); + surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); + } + + _window->setMenuBar(new Menu()); +} + +void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { + Stats::show(); + AnimStats::show(); + auto surfaceContext = getOffscreenUI()->getSurfaceContext(); + surfaceContext->setContextProperty("Stats", Stats::getInstance()); + surfaceContext->setContextProperty("AnimStats", AnimStats::getInstance()); + +#if !defined(Q_OS_ANDROID) + auto offscreenUi = getOffscreenUI(); + auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml"); + offscreenUi->show(qml, "AvatarInputsBar"); +#endif +} + +void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) { + surfaceContext->setContextProperty("Users", DependencyManager::get().data()); + surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); + surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); + surfaceContext->setContextProperty("Vec3", new Vec3()); + surfaceContext->setContextProperty("Quat", new Quat()); + surfaceContext->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); + surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); + surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); + + if (setAdditionalContextProperties) { + auto tabletScriptingInterface = DependencyManager::get(); + auto flags = tabletScriptingInterface->getFlags(); + + surfaceContext->setContextProperty("offscreenFlags", flags); + surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + + surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); + + // in Qt 5.10.0 there is already an "Audio" object in the QML context + // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" + surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); + + surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); + surfaceContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); + surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); + surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("OctreeStats", DependencyManager::get().data()); + surfaceContext->setContextProperty("DCModel", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); + surfaceContext->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); + surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); + surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); + surfaceContext->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); + surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); + surfaceContext->setContextProperty("Pointers", DependencyManager::get().data()); + surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); + surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); + } +} + +void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { + PROFILE_RANGE(render, __FUNCTION__); + PerformanceTimer perfTimer("updateCamera"); + + glm::vec3 boomOffset; + auto myAvatar = getMyAvatar(); + boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; + + // The render mode is default or mirror if the camera is in mirror mode, assigned further below + renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; + + // Always use the default eye position, not the actual head eye position. + // Using the latter will cause the camera to wobble with idle animations, + // or with changes from the face tracker + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { + _thirdPersonHMDCameraBoomValid= false; + if (isHMDMode()) { + mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setPosition(extractTranslation(camMat)); + _myCamera.setOrientation(glmExtractRotation(camMat)); + } + else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition()); + _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); + } + } + else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { + if (isHMDMode()) { + + if (!_thirdPersonHMDCameraBoomValid) { + const glm::vec3 CAMERA_OFFSET = glm::vec3(0.0f, 0.0f, 0.7f); + _thirdPersonHMDCameraBoom = cancelOutRollAndPitch(myAvatar->getHMDSensorOrientation()) * CAMERA_OFFSET; + _thirdPersonHMDCameraBoomValid = true; + } + + glm::mat4 thirdPersonCameraSensorToWorldMatrix = myAvatar->getSensorToWorldMatrix(); + + const glm::vec3 cameraPos = myAvatar->getHMDSensorPosition() + _thirdPersonHMDCameraBoom * myAvatar->getBoomLength(); + glm::mat4 sensorCameraMat = createMatFromQuatAndPos(myAvatar->getHMDSensorOrientation(), cameraPos); + glm::mat4 worldCameraMat = thirdPersonCameraSensorToWorldMatrix * sensorCameraMat; + + _myCamera.setOrientation(glm::normalize(glmExtractRotation(worldCameraMat))); + _myCamera.setPosition(extractTranslation(worldCameraMat)); + } + else { + _thirdPersonHMDCameraBoomValid = false; + + _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); + if (isOptionChecked(MenuOption::CenterPlayerInView)) { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + _myCamera.getOrientation() * boomOffset); + } + else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + myAvatar->getWorldOrientation() * boomOffset); + } + } + } + else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + _thirdPersonHMDCameraBoomValid= false; + + if (isHMDMode()) { + auto mirrorBodyOrientation = myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f)); + + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD yaw and roll + glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); + mirrorHmdEulers.y = -mirrorHmdEulers.y; + mirrorHmdEulers.z = -mirrorHmdEulers.z; + glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); + + glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; + + _myCamera.setOrientation(worldMirrorRotation); + + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD lateral offsets + hmdOffset.x = -hmdOffset.x; + + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) + + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + + mirrorBodyOrientation * hmdOffset); + } + else { + auto userInputMapper = DependencyManager::get(); + const float YAW_SPEED = TWO_PI / 5.0f; + float deltaYaw = userInputMapper->getActionState(controller::Action::YAW) * YAW_SPEED * deltaTime; + _mirrorYawOffset += deltaYaw; + _myCamera.setOrientation(myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f))); + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) + + (myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, _mirrorYawOffset, 0.0f))) * + glm::vec3(0.0f, 0.0f, -1.0f) * myAvatar->getBoomLength() * _scaleMirror); + } + renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; + } + else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { + _thirdPersonHMDCameraBoomValid= false; + EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); + if (cameraEntity != nullptr) { + if (isHMDMode()) { + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + _myCamera.setOrientation(cameraEntity->getWorldOrientation() * hmdRotation); + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + _myCamera.setPosition(cameraEntity->getWorldPosition() + (hmdRotation * hmdOffset)); + } + else { + _myCamera.setOrientation(cameraEntity->getWorldOrientation()); + _myCamera.setPosition(cameraEntity->getWorldPosition()); + } + } + } + // Update camera position + if (!isHMDMode()) { + _myCamera.update(); + } + + renderArgs._cameraMode = (int8_t)_myCamera.getMode(); +} + +void Application::runTests() { + runTimingTests(); + runUnitTests(); +} + +void Application::faceTrackerMuteToggled() { + + QAction* muteAction = Menu::getInstance()->getActionForOption(MenuOption::MuteFaceTracking); + Q_CHECK_PTR(muteAction); + bool isMuted = getSelectedFaceTracker()->isMuted(); + muteAction->setChecked(isMuted); + getSelectedFaceTracker()->setEnabled(!isMuted); + Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setEnabled(!isMuted); +} + +void Application::setFieldOfView(float fov) { + if (fov != _fieldOfView.get()) { + _fieldOfView.set(fov); + resizeGL(); + } +} + +void Application::setHMDTabletScale(float hmdTabletScale) { + _hmdTabletScale.set(hmdTabletScale); +} + +void Application::setDesktopTabletScale(float desktopTabletScale) { + _desktopTabletScale.set(desktopTabletScale); +} + +void Application::setDesktopTabletBecomesToolbarSetting(bool value) { + _desktopTabletBecomesToolbarSetting.set(value); + updateSystemTabletMode(); +} + +void Application::setHmdTabletBecomesToolbarSetting(bool value) { + _hmdTabletBecomesToolbarSetting.set(value); + updateSystemTabletMode(); +} + +void Application::setPreferStylusOverLaser(bool value) { + _preferStylusOverLaserSetting.set(value); +} + +void Application::setPreferAvatarFingerOverStylus(bool value) { + _preferAvatarFingerOverStylusSetting.set(value); +} + +void Application::setPreferredCursor(const QString& cursorName) { + qCDebug(interfaceapp) << "setPreferredCursor" << cursorName; + _preferredCursor.set(cursorName.isEmpty() ? DEFAULT_CURSOR_NAME : cursorName); + showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); +} + +void Application::setSettingConstrainToolbarPosition(bool setting) { + _constrainToolbarPosition.set(setting); + getOffscreenUI()->setConstrainToolbarToCenterX(setting); +} + +void Application::setMiniTabletEnabled(bool enabled) { + _miniTabletEnabledSetting.set(enabled); + emit miniTabletEnabledChanged(enabled); +} + +void Application::showHelp() { + static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; + static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; + static const QString HAND_CONTROLLER_NAME_WINDOWS_MR = "windowsMR"; + + static const QString VIVE_PLUGIN_NAME = "HTC Vive"; + static const QString OCULUS_RIFT_PLUGIN_NAME = "Oculus Rift"; + static const QString WINDOWS_MR_PLUGIN_NAME = "WindowsMR"; + + static const QString TAB_KEYBOARD_MOUSE = "kbm"; + static const QString TAB_GAMEPAD = "gamepad"; + static const QString TAB_HAND_CONTROLLERS = "handControllers"; + + QString handControllerName; + QString defaultTab = TAB_KEYBOARD_MOUSE; + + if (PluginUtils::isHMDAvailable(WINDOWS_MR_PLUGIN_NAME)) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_WINDOWS_MR; + } else if (PluginUtils::isHMDAvailable(VIVE_PLUGIN_NAME)) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_VIVE; + } else if (PluginUtils::isHMDAvailable(OCULUS_RIFT_PLUGIN_NAME)) { + if (PluginUtils::isOculusTouchControllerAvailable()) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_OCULUS_TOUCH; + } else if (PluginUtils::isXboxControllerAvailable()) { + defaultTab = TAB_GAMEPAD; + } else { + defaultTab = TAB_KEYBOARD_MOUSE; + } + } else if (PluginUtils::isXboxControllerAvailable()) { + defaultTab = TAB_GAMEPAD; + } else { + defaultTab = TAB_KEYBOARD_MOUSE; + } + + QUrlQuery queryString; + queryString.addQueryItem("handControllerName", handControllerName); + queryString.addQueryItem("defaultTab", defaultTab); + TabletProxy* tablet = dynamic_cast(DependencyManager::get()->getTablet(SYSTEM_TABLET)); + tablet->gotoWebScreen(PathUtils::resourcesUrl() + INFO_HELP_PATH + "?" + queryString.toString()); + DependencyManager::get()->openTablet(); + //InfoView::show(INFO_HELP_PATH, false, queryString.toString()); +} + +void Application::resizeEvent(QResizeEvent* event) { + resizeGL(); +} + +void Application::resizeGL() { + PROFILE_RANGE(render, __FUNCTION__); + if (nullptr == _displayPlugin) { + return; + } + + auto displayPlugin = getActiveDisplayPlugin(); + // Set the desired FBO texture size. If it hasn't changed, this does nothing. + // Otherwise, it must rebuild the FBOs + uvec2 framebufferSize = displayPlugin->getRecommendedRenderSize(); + uvec2 renderSize = uvec2(framebufferSize); + if (_renderResolution != renderSize) { + _renderResolution = renderSize; + DependencyManager::get()->setFrameBufferSize(fromGlm(renderSize)); + } + + auto renderResolutionScale = getRenderResolutionScale(); + if (displayPlugin->getRenderResolutionScale() != renderResolutionScale) { + auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); + assert(renderConfig); + auto mainView = renderConfig->getConfig("RenderMainView.RenderDeferredTask"); + // mainView can be null if we're rendering in forward mode + if (mainView) { + mainView->setProperty("resolutionScale", renderResolutionScale); + } + displayPlugin->setRenderResolutionScale(renderResolutionScale); + } + + // FIXME the aspect ratio for stereo displays is incorrect based on this. + float aspectRatio = displayPlugin->getRecommendedAspectRatio(); + _myCamera.setProjection(glm::perspective(glm::radians(_fieldOfView.get()), aspectRatio, + DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); + // Possible change in aspect ratio + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_viewFrustum); + } + +#if !defined(DISABLE_QML) + getOffscreenUI()->resize(fromGlm(displayPlugin->getRecommendedUiSize())); +#endif +} + +void Application::handleSandboxStatus(QNetworkReply* reply) { + PROFILE_RANGE(render, __FUNCTION__); + + bool sandboxIsRunning = SandboxUtils::readStatus(reply->readAll()); + + enum HandControllerType { + Vive, + Oculus + }; + static const std::map MIN_CONTENT_VERSION = { + { Vive, 1 }, + { Oculus, 27 } + }; + + // Get sandbox content set version + auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; + auto contentVersionPath = acDirPath + "content-version.txt"; + qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; + int contentVersion = 0; + QFile contentVersionFile(contentVersionPath); + if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QString line = contentVersionFile.readAll(); + contentVersion = line.toInt(); // returns 0 if conversion fails + } + + // Get controller availability +#ifdef ANDROID_APP_QUEST_INTERFACE + bool hasHandControllers = true; +#else + bool hasHandControllers = false; + if (PluginUtils::isViveControllerAvailable() || PluginUtils::isOculusTouchControllerAvailable()) { + hasHandControllers = true; + } +#endif + + // Check HMD use (may be technically available without being in use) + bool hasHMD = PluginUtils::isHMDAvailable(); + bool isUsingHMD = _displayPlugin->isHmd(); + bool isUsingHMDAndHandControllers = hasHMD && hasHandControllers && isUsingHMD; + + qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMDAndHandControllers; + + // when --url in command line, teleport to location + const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; + int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); + QString addressLookupString; + if (urlIndex != -1) { + QUrl url(arguments().value(urlIndex + 1)); + if (url.scheme() == URL_SCHEME_HIFIAPP) { + Setting::Handle("startUpApp").set(url.path()); + } else { + addressLookupString = url.toString(); + } + } + + static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location"; + static const QString SENT_TO_ENTRY = "entry"; + + QString sentTo; + + // If this is a first run we short-circuit the address passed in + if (_firstRun.get()) { +#if !defined(Q_OS_ANDROID) + DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; +#endif + _firstRun.set(false); + + } else { +#if !defined(Q_OS_ANDROID) + QString goingTo = ""; + if (addressLookupString.isEmpty()) { + if (Menu::getInstance()->isOptionChecked(MenuOption::HomeLocation)) { + auto locationBookmarks = DependencyManager::get(); + addressLookupString = locationBookmarks->addressForBookmark(LocationBookmarks::HOME_BOOKMARK); + goingTo = "home location"; + } else { + goingTo = "previous location"; + } + } + qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(!goingTo.isEmpty() ? goingTo : addressLookupString); + DependencyManager::get()->loadSettings(addressLookupString); + sentTo = SENT_TO_PREVIOUS_LOCATION; +#endif + } + + UserActivityLogger::getInstance().logAction("startup_sent_to", { + { "sent_to", sentTo }, + { "sandbox_is_running", sandboxIsRunning }, + { "has_hmd", hasHMD }, + { "has_hand_controllers", hasHandControllers }, + { "is_using_hmd", isUsingHMD }, + { "is_using_hmd_and_hand_controllers", isUsingHMDAndHandControllers }, + { "content_version", contentVersion } + }); + + _connectionMonitor.init(); +} + +bool Application::importJSONFromURL(const QString& urlString) { + // we only load files that terminate in just .json (not .svo.json and not .ava.json) + QUrl jsonURL { urlString }; + + emit svoImportRequested(urlString); + return true; +} + +bool Application::importSVOFromURL(const QString& urlString) { + emit svoImportRequested(urlString); + return true; +} + +bool Application::importFromZIP(const QString& filePath) { + qDebug() << "A zip file has been dropped in: " << filePath; + QUrl empty; + // handle Blocks download from Marketplace + if (filePath.contains("poly.google.com/downloads")) { + addAssetToWorldFromURL(filePath); + } else { + qApp->getFileDownloadInterface()->runUnzip(filePath, empty, true, true, false); + } + return true; +} + +bool Application::isServerlessMode() const { + auto tree = getEntities()->getTree(); + if (tree) { + return tree->isServerlessMode(); + } + return false; +} + +void Application::setIsInterstitialMode(bool interstitialMode) { + bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + if (enableInterstitial) { + if (_interstitialMode != interstitialMode) { + _interstitialMode = interstitialMode; + emit interstitialModeChanged(_interstitialMode); + + DependencyManager::get()->setAudioPaused(_interstitialMode); + DependencyManager::get()->setMyAvatarDataPacketsPaused(_interstitialMode); + } + } +} + +void Application::setIsServerlessMode(bool serverlessDomain) { + auto tree = getEntities()->getTree(); + if (tree) { + tree->setIsServerlessMode(serverlessDomain); + } +} + +std::map Application::prepareServerlessDomainContents(QUrl domainURL) { + QUuid serverlessSessionID = QUuid::createUuid(); + getMyAvatar()->setSessionUUID(serverlessSessionID); + auto nodeList = DependencyManager::get(); + nodeList->setSessionUUID(serverlessSessionID); + + // there is no domain-server to tell us our permissions, so enable all + NodePermissions permissions; + permissions.setAll(true); + nodeList->setPermissions(permissions); + + // we can't import directly into the main tree because we would need to lock it, and + // Octree::readFromURL calls loop.exec which can run code which will also attempt to lock the tree. + EntityTreePointer tmpTree(new EntityTree()); + tmpTree->setIsServerlessMode(true); + tmpTree->createRootElement(); + auto myAvatar = getMyAvatar(); + tmpTree->setMyAvatar(myAvatar); + bool success = tmpTree->readFromURL(domainURL.toString()); + if (success) { + tmpTree->reaverageOctreeElements(); + tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), 0, 0, 0); + } + std::map namedPaths = tmpTree->getNamedPaths(); + + // we must manually eraseAllOctreeElements(false) else the tmpTree will mem-leak + tmpTree->eraseAllOctreeElements(false); + + return namedPaths; +} + +void Application::loadServerlessDomain(QUrl domainURL) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_ARG(QUrl, domainURL)); + return; + } + + if (domainURL.isEmpty()) { + return; + } + + auto namedPaths = prepareServerlessDomainContents(domainURL); + auto nodeList = DependencyManager::get(); + + nodeList->getDomainHandler().connectedToServerless(namedPaths); + + _fullSceneReceivedCounter++; +} + +void Application::loadErrorDomain(QUrl domainURL) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadErrorDomain", Q_ARG(QUrl, domainURL)); + return; + } + + if (domainURL.isEmpty()) { + return; + } + + auto namedPaths = prepareServerlessDomainContents(domainURL); + auto nodeList = DependencyManager::get(); + + nodeList->getDomainHandler().loadedErrorDomain(namedPaths); + + _fullSceneReceivedCounter++; +} + +bool Application::importImage(const QString& urlString) { + qCDebug(interfaceapp) << "An image file has been dropped in"; + QString filepath(urlString); + filepath.remove("file:///"); + addAssetToWorld(filepath, "", false, false); + return true; +} + +// thread-safe +void Application::onPresent(quint32 frameCount) { + bool expected = false; + if (_pendingIdleEvent.compare_exchange_strong(expected, true)) { + postEvent(this, new QEvent((QEvent::Type)ApplicationEvent::Idle), Qt::HighEventPriority); + } + expected = false; + if (_graphicsEngine.checkPendingRenderEvent() && !isAboutToQuit()) { + postEvent(_graphicsEngine._renderEventHandler, new QEvent((QEvent::Type)ApplicationEvent::Render)); + } +} + +static inline bool isKeyEvent(QEvent::Type type) { + return type == QEvent::KeyPress || type == QEvent::KeyRelease; +} + +bool Application::handleKeyEventForFocusedEntity(QEvent* event) { + if (_keyboardFocusedEntity.get() != UNKNOWN_ENTITY_ID) { + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: + { + auto eventHandler = getEntities()->getEventHandler(_keyboardFocusedEntity.get()); + if (eventHandler) { + event->setAccepted(false); + QCoreApplication::sendEvent(eventHandler, event); + if (event->isAccepted()) { + _lastAcceptedKeyPress = usecTimestampNow(); + return true; + } + } + break; + } + default: + break; + } + } + + return false; +} + +bool Application::handleFileOpenEvent(QFileOpenEvent* fileEvent) { + QUrl url = fileEvent->url(); + if (!url.isEmpty()) { + QString urlString = url.toString(); + if (canAcceptURL(urlString)) { + return acceptURL(urlString); + } + } + return false; +} + +#ifdef DEBUG_EVENT_QUEUE +static int getEventQueueSize(QThread* thread) { + auto threadData = QThreadData::get2(thread); + QMutexLocker locker(&threadData->postEventList.mutex); + return threadData->postEventList.size(); +} + +static void dumpEventQueue(QThread* thread) { + auto threadData = QThreadData::get2(thread); + QMutexLocker locker(&threadData->postEventList.mutex); + qDebug() << "Event list, size =" << threadData->postEventList.size(); + for (auto& postEvent : threadData->postEventList) { + QEvent::Type type = (postEvent.event ? postEvent.event->type() : QEvent::None); + qDebug() << " " << type; + } +} +#endif // DEBUG_EVENT_QUEUE + +bool Application::event(QEvent* event) { + + if (_aboutToQuit) { + return false; + } + + if (!Menu::getInstance()) { + return false; + } + + // Allow focused Entities to handle keyboard input + if (isKeyEvent(event->type()) && handleKeyEventForFocusedEntity(event)) { + return true; + } + + int type = event->type(); + switch (type) { + case ApplicationEvent::Lambda: + static_cast(event)->call(); + return true; + + // Explicit idle keeps the idle running at a lower interval, but without any rendering + // see (windowMinimizedChanged) + case ApplicationEvent::Idle: + idle(); + +#ifdef DEBUG_EVENT_QUEUE + { + int count = getEventQueueSize(QThread::currentThread()); + if (count > 400) { + dumpEventQueue(QThread::currentThread()); + } + } +#endif // DEBUG_EVENT_QUEUE + + _pendingIdleEvent.store(false); + + return true; + + case QEvent::MouseMove: + mouseMoveEvent(static_cast(event)); + return true; + case QEvent::MouseButtonPress: + mousePressEvent(static_cast(event)); + return true; + case QEvent::MouseButtonDblClick: + mouseDoublePressEvent(static_cast(event)); + return true; + case QEvent::MouseButtonRelease: + mouseReleaseEvent(static_cast(event)); + return true; + case QEvent::KeyPress: + keyPressEvent(static_cast(event)); + return true; + case QEvent::KeyRelease: + keyReleaseEvent(static_cast(event)); + return true; + case QEvent::FocusOut: + focusOutEvent(static_cast(event)); + return true; + case QEvent::TouchBegin: + touchBeginEvent(static_cast(event)); + event->accept(); + return true; + case QEvent::TouchEnd: + touchEndEvent(static_cast(event)); + return true; + case QEvent::TouchUpdate: + touchUpdateEvent(static_cast(event)); + return true; + case QEvent::Gesture: + touchGestureEvent((QGestureEvent*)event); + return true; + case QEvent::Wheel: + wheelEvent(static_cast(event)); + return true; + case QEvent::Drop: + dropEvent(static_cast(event)); + return true; + + case QEvent::FileOpen: + if (handleFileOpenEvent(static_cast(event))) { + return true; + } + break; + + default: + break; + } + + return QApplication::event(event); +} + +bool Application::eventFilter(QObject* object, QEvent* event) { + + if (_aboutToQuit && event->type() != QEvent::DeferredDelete && event->type() != QEvent::Destroy) { + return true; + } + + if (event->type() == QEvent::Leave) { + getApplicationCompositor().handleLeaveEvent(); + } + + if (event->type() == QEvent::ShortcutOverride) { +#if !defined(DISABLE_QML) + if (getOffscreenUI()->shouldSwallowShortcut(event)) { + event->accept(); + return true; + } +#endif + + // Filter out captured keys before they're used for shortcut actions. + if (_controllerScriptingInterface->isKeyCaptured(static_cast(event))) { + event->accept(); + return true; + } + } + + return false; +} + +static bool _altPressed{ false }; + +void Application::keyPressEvent(QKeyEvent* event) { + _altPressed = event->key() == Qt::Key_Alt; + + if (!event->isAutoRepeat()) { + _keysPressed.insert(event->key(), *event); + } + + _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isKeyCaptured(event) || isInterstitialMode()) { + return; + } + + if (hasFocus() && getLoginDialogPoppedUp()) { + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->keyReleaseEvent(event); + } + + bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isOption = event->modifiers().testFlag(Qt::AltModifier); + switch (event->key()) { + case Qt::Key_4: + case Qt::Key_5: + case Qt::Key_6: + case Qt::Key_7: + if (isMeta || isOption) { + unsigned int index = static_cast(event->key() - Qt::Key_1); + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + if (index < displayPlugins.size()) { + auto targetPlugin = displayPlugins.at(index); + QString targetName = targetPlugin->getName(); + auto menu = Menu::getInstance(); + QAction* action = menu->getActionForOption(targetName); + if (action && !action->isChecked()) { + action->trigger(); + } + } + } + break; + } + } else if (hasFocus()) { + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->keyPressEvent(event); + } + + bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); + bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isOption = event->modifiers().testFlag(Qt::AltModifier); + switch (event->key()) { + case Qt::Key_Enter: + case Qt::Key_Return: + if (isOption) { + if (_window->isFullScreen()) { + unsetFullscreen(); + } else { + setFullscreen(nullptr); + } + } + break; + + case Qt::Key_1: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::FirstPerson); + break; + } + case Qt::Key_2: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::FullscreenMirror); + break; + } + case Qt::Key_3: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::ThirdPerson); + break; + } + case Qt::Key_4: + case Qt::Key_5: + case Qt::Key_6: + case Qt::Key_7: + if (isMeta || isOption) { + unsigned int index = static_cast(event->key() - Qt::Key_1); + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + if (index < displayPlugins.size()) { + auto targetPlugin = displayPlugins.at(index); + QString targetName = targetPlugin->getName(); + auto menu = Menu::getInstance(); + QAction* action = menu->getActionForOption(targetName); + if (action && !action->isChecked()) { + action->trigger(); + } + } + } + break; + + case Qt::Key_G: + if (isShifted && isMeta && Menu::getInstance() && Menu::getInstance()->getMenu("Developer")->isVisible()) { + static const QString HIFI_FRAMES_FOLDER_VAR = "HIFI_FRAMES_FOLDER"; + static const QString GPU_FRAME_FOLDER = QProcessEnvironment::systemEnvironment().contains(HIFI_FRAMES_FOLDER_VAR) + ? QProcessEnvironment::systemEnvironment().value(HIFI_FRAMES_FOLDER_VAR) + : "hifiFrames"; + static QString GPU_FRAME_TEMPLATE = GPU_FRAME_FOLDER + "/{DATE}_{TIME}"; + QString fullPath = FileUtils::computeDocumentPath(FileUtils::replaceDateTimeTokens(GPU_FRAME_TEMPLATE)); + if (FileUtils::canCreateFile(fullPath)) { + getActiveDisplayPlugin()->captureFrame(fullPath.toStdString()); + } + } + break; + case Qt::Key_X: + if (isShifted && isMeta) { + auto offscreenUi = getOffscreenUI(); + offscreenUi->togglePinned(); + //offscreenUi->getSurfaceContext()->engine()->clearComponentCache(); + //OffscreenUi::information("Debugging", "Component cache cleared"); + // placeholder for dialogs being converted to QML. + } + break; + + case Qt::Key_Y: + if (isShifted && isMeta) { + getActiveDisplayPlugin()->cycleDebugOutput(); + } + break; + + case Qt::Key_B: + if (isMeta) { + auto offscreenUi = getOffscreenUI(); + offscreenUi->load("Browser.qml"); + } else if (isOption) { + controller::InputRecorder* inputRecorder = controller::InputRecorder::getInstance(); + inputRecorder->stopPlayback(); + } + break; + + case Qt::Key_L: + if (isShifted && isMeta) { + Menu::getInstance()->triggerOption(MenuOption::Log); + } else if (isMeta) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->toggleAddressBar(); + } else if (isShifted) { + Menu::getInstance()->triggerOption(MenuOption::LodTools); + } + break; + + case Qt::Key_R: + if (isMeta && !event->isAutoRepeat()) { + DependencyManager::get()->reloadAllScripts(); + getOffscreenUI()->clearCache(); + } + break; + + case Qt::Key_Asterisk: + Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox); + break; + + case Qt::Key_M: + if (isMeta) { + auto audioClient = DependencyManager::get(); + audioClient->setMuted(!audioClient->isMuted()); + } + break; + + case Qt::Key_N: + if (!isOption && !isShifted && isMeta) { + DependencyManager::get()->toggleIgnoreRadius(); + } + break; + + case Qt::Key_S: + if (isShifted && isMeta && !isOption) { + Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); + } + break; + + case Qt::Key_P: { + if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { + AudioInjectorOptions options; + options.localOnly = true; + options.positionSet = false; // system sound + options.stereo = true; + + Setting::Handle notificationSounds{ MenuOption::NotificationSounds, true }; + Setting::Handle notificationSoundSnapshot{ MenuOption::NotificationSoundsSnapshot, true }; + if (notificationSounds.get() && notificationSoundSnapshot.get()) { + if (_snapshotSoundInjector) { + _snapshotSoundInjector->setOptions(options); + _snapshotSoundInjector->restart(); + } else { + _snapshotSoundInjector = AudioInjector::playSound(_snapshotSound, options); + } + } + takeSnapshot(true); + } + break; + } + + case Qt::Key_Apostrophe: { + if (isMeta) { + auto cursor = Cursor::Manager::instance().getCursor(); + auto curIcon = cursor->getIcon(); + if (curIcon == Cursor::Icon::DEFAULT) { + showCursor(Cursor::Icon::RETICLE); + } else if (curIcon == Cursor::Icon::RETICLE) { + showCursor(Cursor::Icon::SYSTEM); + } else if (curIcon == Cursor::Icon::SYSTEM) { + showCursor(Cursor::Icon::LINK); + } else { + showCursor(Cursor::Icon::DEFAULT); + } + } else if (!event->isAutoRepeat()){ + resetSensors(true); + } + break; + } + + case Qt::Key_Backslash: + Menu::getInstance()->triggerOption(MenuOption::Chat); + break; + + case Qt::Key_Slash: + Menu::getInstance()->triggerOption(MenuOption::Stats); + break; + + case Qt::Key_Plus: { + if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { + auto& cursorManager = Cursor::Manager::instance(); + cursorManager.setScale(cursorManager.getScale() * 1.1f); + } else { + getMyAvatar()->increaseSize(); + } + break; + } + + case Qt::Key_Minus: { + if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { + auto& cursorManager = Cursor::Manager::instance(); + cursorManager.setScale(cursorManager.getScale() / 1.1f); + } else { + getMyAvatar()->decreaseSize(); + } + break; + } + + case Qt::Key_Equal: + getMyAvatar()->resetSize(); + break; + case Qt::Key_Escape: { + getActiveDisplayPlugin()->abandonCalibration(); + break; + } + + default: + event->ignore(); + break; + } + } +} + +void Application::keyReleaseEvent(QKeyEvent* event) { + if (!event->isAutoRepeat()) { + _keysPressed.remove(event->key()); + } + +#if defined(Q_OS_ANDROID) + if (event->key() == Qt::Key_Back) { + event->accept(); + AndroidHelper::instance().requestActivity("Home", false); + } +#endif + _controllerScriptingInterface->emitKeyReleaseEvent(event); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isKeyCaptured(event)) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->keyReleaseEvent(event); + } +} + +void Application::focusOutEvent(QFocusEvent* event) { + auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); + foreach(auto inputPlugin, inputPlugins) { + if (inputPlugin->isActive()) { + inputPlugin->pluginFocusOutEvent(); + } + } + +// FIXME spacemouse code still needs cleanup +#if 0 + //SpacemouseDevice::getInstance().focusOutEvent(); + //SpacemouseManager::getInstance().getDevice()->focusOutEvent(); + SpacemouseManager::getInstance().ManagerFocusOutEvent(); +#endif + + synthesizeKeyReleasEvents(); +} + +void Application::synthesizeKeyReleasEvents() { + // synthesize events for keys currently pressed, since we may not get their release events + // Because our key event handlers may manipulate _keysPressed, lets swap the keys pressed into a local copy, + // clearing the existing list. + QHash keysPressed; + std::swap(keysPressed, _keysPressed); + for (auto& ev : keysPressed) { + QKeyEvent synthesizedEvent { QKeyEvent::KeyRelease, ev.key(), Qt::NoModifier, ev.text() }; + keyReleaseEvent(&synthesizedEvent); + } +} + +void Application::maybeToggleMenuVisible(QMouseEvent* event) const { +#ifndef Q_OS_MAC + // If in full screen, and our main windows menu bar is hidden, and we're close to the top of the QMainWindow + // then show the menubar. + if (_window->isFullScreen()) { + QMenuBar* menuBar = _window->menuBar(); + if (menuBar) { + static const int MENU_TOGGLE_AREA = 10; + if (!menuBar->isVisible()) { + if (event->pos().y() <= MENU_TOGGLE_AREA) { + menuBar->setVisible(true); + } + } else { + if (event->pos().y() > MENU_TOGGLE_AREA) { + menuBar->setVisible(false); + } + } + } + } +#endif +} + +void Application::mouseMoveEvent(QMouseEvent* event) { + PROFILE_RANGE(app_input_mouse, __FUNCTION__); + + maybeToggleMenuVisible(event); + + auto& compositor = getApplicationCompositor(); + // if this is a real mouse event, and we're in HMD mode, then we should use it to move the + // compositor reticle + // handleRealMouseMoveEvent() will return true, if we shouldn't process the event further + if (!compositor.fakeEventActive() && compositor.handleRealMouseMoveEvent()) { + return; // bail + } + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto eventPosition = compositor.getMouseEventPosition(event); + QPointF transformedPos = offscreenUi ? offscreenUi->mapToVirtualScreen(eventPosition) : QPointF(); +#else + QPointF transformedPos; +#endif + auto button = event->button(); + auto buttons = event->buttons(); + // Determine if the ReticleClick Action is 1 and if so, fake include the LeftMouseButton + if (_reticleClickPressed) { + if (button == Qt::NoButton) { + button = Qt::LeftButton; + } + buttons |= Qt::LeftButton; + } + + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->screenPos(), button, + buttons, event->modifiers()); + + if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || + getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) { + getEntities()->mouseMoveEvent(&mappedEvent); + } + + _controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->mouseMoveEvent(event); + } +} + +void Application::mousePressEvent(QMouseEvent* event) { + // Inhibit the menu if the user is using alt-mouse dragging + _altPressed = false; + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + // If we get a mouse press event it means it wasn't consumed by the offscreen UI, + // hence, we should defocus all of the offscreen UI windows, in order to allow + // keyboard shortcuts not to be swallowed by them. In particular, WebEngineViews + // will consume all keyboard events. + offscreenUi->unfocusWindows(); + + auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); + QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); +#else + QPointF transformedPos; +#endif + + QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); + QUuid result = getEntities()->mousePressEvent(&mappedEvent); + setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); + + _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + +#if defined(Q_OS_MAC) + // Fix for OSX right click dragging on window when coming from a native window + bool isFocussed = hasFocus(); + if (!isFocussed && event->button() == Qt::MouseButton::RightButton) { + setFocus(); + isFocussed = true; + } + + if (isFocussed) { +#else + if (hasFocus()) { +#endif + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->mousePressEvent(event); + } + } +} + +void Application::mouseDoublePressEvent(QMouseEvent* event) { +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); + QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); +#else + QPointF transformedPos; +#endif + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->screenPos(), event->button(), + event->buttons(), event->modifiers()); + getEntities()->mouseDoublePressEvent(&mappedEvent); + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + + _controllerScriptingInterface->emitMouseDoublePressEvent(event); +} + +void Application::mouseReleaseEvent(QMouseEvent* event) { + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); + QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); +#else + QPointF transformedPos; +#endif + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->screenPos(), event->button(), + event->buttons(), event->modifiers()); + + getEntities()->mouseReleaseEvent(&mappedEvent); + + _controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + + if (hasFocus()) { + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->mouseReleaseEvent(event); + } + } +} + +void Application::touchUpdateEvent(QTouchEvent* event) { + _altPressed = false; + + if (event->type() == QEvent::TouchUpdate) { + TouchEvent thisEvent(*event, _lastTouchEvent); + _controllerScriptingInterface->emitTouchUpdateEvent(thisEvent); // send events to any registered scripts + _lastTouchEvent = thisEvent; + } + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isTouchCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->touchUpdateEvent(event); + } + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchUpdateEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchUpdateEvent(event); + } +} + +void Application::touchBeginEvent(QTouchEvent* event) { + _altPressed = false; + TouchEvent thisEvent(*event); // on touch begin, we don't compare to last event + _controllerScriptingInterface->emitTouchBeginEvent(thisEvent); // send events to any registered scripts + + _lastTouchEvent = thisEvent; // and we reset our last event to this event before we call our update + touchUpdateEvent(event); + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isTouchCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->touchBeginEvent(event); + } + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchBeginEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchBeginEvent(event); + } + +} + +void Application::touchEndEvent(QTouchEvent* event) { + _altPressed = false; + TouchEvent thisEvent(*event, _lastTouchEvent); + _controllerScriptingInterface->emitTouchEndEvent(thisEvent); // send events to any registered scripts + _lastTouchEvent = thisEvent; + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isTouchCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->touchEndEvent(event); + } + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchEndEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchEndEvent(event); + } + // put any application specific touch behavior below here.. +} + +void Application::touchGestureEvent(QGestureEvent* event) { + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchGestureEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchGestureEvent(event); + } +} + +void Application::wheelEvent(QWheelEvent* event) const { + _altPressed = false; + _controllerScriptingInterface->emitWheelEvent(event); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isWheelCaptured() || getLoginDialogPoppedUp()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->wheelEvent(event); + } +} + +void Application::dropEvent(QDropEvent *event) { + const QMimeData* mimeData = event->mimeData(); + for (auto& url : mimeData->urls()) { + QString urlString = url.toString(); + if (acceptURL(urlString, true)) { + event->acceptProposedAction(); + } + } +} + +void Application::dragEnterEvent(QDragEnterEvent* event) { + event->acceptProposedAction(); +} + +// This is currently not used, but could be invoked if the user wants to go to the place embedded in an +// Interface-taken snapshot. (It was developed for drag and drop, before we had asset-server loading or in-world browsers.) +bool Application::acceptSnapshot(const QString& urlString) { + QUrl url(urlString); + QString snapshotPath = url.toLocalFile(); + + SnapshotMetaData* snapshotData = DependencyManager::get()->parseSnapshotData(snapshotPath); + if (snapshotData) { + if (!snapshotData->getURL().toString().isEmpty()) { + DependencyManager::get()->handleLookupString(snapshotData->getURL().toString()); + } + } else { + OffscreenUi::asyncWarning("", "No location details were found in the file\n" + + snapshotPath + "\nTry dragging in an authentic Hifi snapshot."); + } + return true; +} + +#ifdef Q_OS_WIN +#include +#include +#include +#pragma comment(lib, "pdh.lib") +#pragma comment(lib, "ntdll.lib") + +extern "C" { + enum SYSTEM_INFORMATION_CLASS { + SystemBasicInformation = 0, + SystemProcessorPerformanceInformation = 8, + }; + + struct SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION { + LARGE_INTEGER IdleTime; + LARGE_INTEGER KernelTime; + LARGE_INTEGER UserTime; + LARGE_INTEGER DpcTime; + LARGE_INTEGER InterruptTime; + ULONG InterruptCount; + }; + + struct SYSTEM_BASIC_INFORMATION { + ULONG Reserved; + ULONG TimerResolution; + ULONG PageSize; + ULONG NumberOfPhysicalPages; + ULONG LowestPhysicalPageNumber; + ULONG HighestPhysicalPageNumber; + ULONG AllocationGranularity; + ULONG_PTR MinimumUserModeAddress; + ULONG_PTR MaximumUserModeAddress; + ULONG_PTR ActiveProcessorsAffinityMask; + CCHAR NumberOfProcessors; + }; + + NTSYSCALLAPI NTSTATUS NTAPI NtQuerySystemInformation( + _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass, + _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation, + _In_ ULONG SystemInformationLength, + _Out_opt_ PULONG ReturnLength + ); + +} +template +NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, T& t) { + return NtQuerySystemInformation(SystemInformationClass, &t, (ULONG)sizeof(T), nullptr); +} + +template +NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, std::vector& t) { + return NtQuerySystemInformation(SystemInformationClass, t.data(), (ULONG)(sizeof(T) * t.size()), nullptr); +} + + +template +void updateValueAndDelta(std::pair& pair, T newValue) { + auto& value = pair.first; + auto& delta = pair.second; + delta = (value != 0) ? newValue - value : 0; + value = newValue; +} + +struct MyCpuInfo { + using ValueAndDelta = std::pair; + std::string name; + ValueAndDelta kernel { 0, 0 }; + ValueAndDelta user { 0, 0 }; + ValueAndDelta idle { 0, 0 }; + float kernelUsage { 0.0f }; + float userUsage { 0.0f }; + + void update(const SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION& cpuInfo) { + updateValueAndDelta(kernel, cpuInfo.KernelTime.QuadPart); + updateValueAndDelta(user, cpuInfo.UserTime.QuadPart); + updateValueAndDelta(idle, cpuInfo.IdleTime.QuadPart); + auto totalTime = kernel.second + user.second + idle.second; + if (totalTime != 0) { + kernelUsage = (FLOAT)kernel.second / totalTime; + userUsage = (FLOAT)user.second / totalTime; + } else { + kernelUsage = userUsage = 0.0f; + } + } +}; + +void updateCpuInformation() { + static std::once_flag once; + static SYSTEM_BASIC_INFORMATION systemInfo {}; + static SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION cpuTotals; + static std::vector cpuInfos; + static std::vector myCpuInfos; + static MyCpuInfo myCpuTotals; + std::call_once(once, [&] { + NtQuerySystemInformation( SystemBasicInformation, systemInfo); + cpuInfos.resize(systemInfo.NumberOfProcessors); + myCpuInfos.resize(systemInfo.NumberOfProcessors); + for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { + myCpuInfos[i].name = "cpu." + std::to_string(i); + } + myCpuTotals.name = "cpu.total"; + }); + NtQuerySystemInformation(SystemProcessorPerformanceInformation, cpuInfos); + + // Zero the CPU totals. + memset(&cpuTotals, 0, sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { + auto& cpuInfo = cpuInfos[i]; + // KernelTime includes IdleTime. + cpuInfo.KernelTime.QuadPart -= cpuInfo.IdleTime.QuadPart; + + // Update totals + cpuTotals.IdleTime.QuadPart += cpuInfo.IdleTime.QuadPart; + cpuTotals.KernelTime.QuadPart += cpuInfo.KernelTime.QuadPart; + cpuTotals.UserTime.QuadPart += cpuInfo.UserTime.QuadPart; + + // Update friendly structure + auto& myCpuInfo = myCpuInfos[i]; + myCpuInfo.update(cpuInfo); + PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { + { "kernel", myCpuInfo.kernelUsage }, + { "user", myCpuInfo.userUsage } + }); + } + + myCpuTotals.update(cpuTotals); + PROFILE_COUNTER(app, myCpuTotals.name.c_str(), { + { "kernel", myCpuTotals.kernelUsage }, + { "user", myCpuTotals.userUsage } + }); +} + + +static ULARGE_INTEGER lastCPU, lastSysCPU, lastUserCPU; +static int numProcessors; +static HANDLE self; +static PDH_HQUERY cpuQuery; +static PDH_HCOUNTER cpuTotal; + +void initCpuUsage() { + SYSTEM_INFO sysInfo; + FILETIME ftime, fsys, fuser; + + GetSystemInfo(&sysInfo); + numProcessors = sysInfo.dwNumberOfProcessors; + + GetSystemTimeAsFileTime(&ftime); + memcpy(&lastCPU, &ftime, sizeof(FILETIME)); + + self = GetCurrentProcess(); + GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); + memcpy(&lastSysCPU, &fsys, sizeof(FILETIME)); + memcpy(&lastUserCPU, &fuser, sizeof(FILETIME)); + + PdhOpenQuery(NULL, NULL, &cpuQuery); + PdhAddCounter(cpuQuery, "\\Processor(_Total)\\% Processor Time", NULL, &cpuTotal); + PdhCollectQueryData(cpuQuery); +} + +void getCpuUsage(vec3& systemAndUser) { + FILETIME ftime, fsys, fuser; + ULARGE_INTEGER now, sys, user; + + GetSystemTimeAsFileTime(&ftime); + memcpy(&now, &ftime, sizeof(FILETIME)); + + GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); + memcpy(&sys, &fsys, sizeof(FILETIME)); + memcpy(&user, &fuser, sizeof(FILETIME)); + systemAndUser.x = (sys.QuadPart - lastSysCPU.QuadPart); + systemAndUser.y = (user.QuadPart - lastUserCPU.QuadPart); + systemAndUser /= (float)(now.QuadPart - lastCPU.QuadPart); + systemAndUser /= (float)numProcessors; + systemAndUser *= 100.0f; + lastCPU = now; + lastUserCPU = user; + lastSysCPU = sys; + + PDH_FMT_COUNTERVALUE counterVal; + PdhCollectQueryData(cpuQuery); + PdhGetFormattedCounterValue(cpuTotal, PDH_FMT_DOUBLE, NULL, &counterVal); + systemAndUser.z = (float)counterVal.doubleValue; +} + +void setupCpuMonitorThread() { + initCpuUsage(); + auto cpuMonitorThread = QThread::currentThread(); + + QTimer* timer = new QTimer(); + timer->setInterval(50); + QObject::connect(timer, &QTimer::timeout, [] { + updateCpuInformation(); + vec3 kernelUserAndSystem; + getCpuUsage(kernelUserAndSystem); + PROFILE_COUNTER(app, "cpuProcess", { { "system", kernelUserAndSystem.x }, { "user", kernelUserAndSystem.y } }); + PROFILE_COUNTER(app, "cpuSystem", { { "system", kernelUserAndSystem.z } }); + }); + QObject::connect(cpuMonitorThread, &QThread::finished, [=] { + timer->deleteLater(); + cpuMonitorThread->deleteLater(); + }); + timer->start(); +} + +#endif + +void Application::idle() { + PerformanceTimer perfTimer("idle"); + + // Update the deadlock watchdog + updateHeartbeat(); + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + + // These tasks need to be done on our first idle, because we don't want the showing of + // overlay subwindows to do a showDesktop() until after the first time through + static bool firstIdle = true; + if (firstIdle) { + firstIdle = false; + connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); + } +#endif + +#ifdef Q_OS_WIN + // If tracing is enabled then monitor the CPU in a separate thread + static std::once_flag once; + std::call_once(once, [&] { + if (trace_app().isDebugEnabled()) { + QThread* cpuMonitorThread = new QThread(qApp); + cpuMonitorThread->setObjectName("cpuMonitorThread"); + QObject::connect(cpuMonitorThread, &QThread::started, [this] { setupCpuMonitorThread(); }); + QObject::connect(qApp, &QCoreApplication::aboutToQuit, cpuMonitorThread, &QThread::quit); + cpuMonitorThread->start(); + } + }); +#endif + + auto displayPlugin = getActiveDisplayPlugin(); +#if !defined(DISABLE_QML) + if (displayPlugin) { + auto uiSize = displayPlugin->getRecommendedUiSize(); + // Bit of a hack since there's no device pixel ratio change event I can find. + if (offscreenUi->size() != fromGlm(uiSize)) { + qCDebug(interfaceapp) << "Device pixel ratio changed, triggering resize to " << uiSize; + offscreenUi->resize(fromGlm(uiSize)); + } + } +#endif + + if (displayPlugin) { + PROFILE_COUNTER_IF_CHANGED(app, "present", float, displayPlugin->presentRate()); + } + PROFILE_COUNTER_IF_CHANGED(app, "renderLoopRate", float, getRenderLoopRate()); + PROFILE_COUNTER_IF_CHANGED(app, "currentDownloads", uint32_t, ResourceCache::getLoadingRequests().length()); + PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", uint32_t, ResourceCache::getPendingRequestCount()); + PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get()->getStat("Processing").toInt()); + PROFILE_COUNTER_IF_CHANGED(app, "pendingProcessing", int, DependencyManager::get()->getStat("PendingProcessing").toInt()); + auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); + PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_graphicsEngine.getGPUContext()->getFrameTimerGPUAverage()); + + PROFILE_RANGE(app, __FUNCTION__); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + steamClient->runCallbacks(); + } + + if (auto oculusPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { + oculusPlugin->handleOVREvents(); + } + + float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND; + _lastTimeUpdated.start(); + +#if !defined(DISABLE_QML) + // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. + if (offscreenUi && offscreenUi->getWindow()) { + auto activeFocusItem = offscreenUi->getWindow()->activeFocusItem(); + if (_keyboardDeviceHasFocus && activeFocusItem != offscreenUi->getRootItem()) { + _keyboardMouseDevice->pluginFocusOutEvent(); + _keyboardDeviceHasFocus = false; + synthesizeKeyReleasEvents(); + } else if (activeFocusItem == offscreenUi->getRootItem()) { + _keyboardDeviceHasFocus = true; + } + } +#endif + + checkChangeCursor(); + +#if !defined(DISABLE_QML) + auto stats = Stats::getInstance(); + if (stats) { + stats->updateStats(); + } + auto animStats = AnimStats::getInstance(); + if (animStats) { + animStats->updateStats(); + } +#endif + + // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing + // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing + // details normally. +#ifdef Q_OS_ANDROID + bool showWarnings = false; +#else + bool showWarnings = getLogger()->extraDebugging(); +#endif + PerformanceWarning warn(showWarnings, "idle()"); + + { + _gameWorkload.updateViews(_viewFrustum, getMyAvatar()->getHeadPosition()); + _gameWorkload._engine->run(); + } + { + PerformanceTimer perfTimer("update"); + PerformanceWarning warn(showWarnings, "Application::idle()... update()"); + static const float BIGGEST_DELTA_TIME_SECS = 0.25f; + update(glm::clamp(secondsSinceLastUpdate, 0.0f, BIGGEST_DELTA_TIME_SECS)); + } + + { // Update keyboard focus highlight + if (!_keyboardFocusedEntity.get().isInvalidID()) { + const quint64 LOSE_FOCUS_AFTER_ELAPSED_TIME = 30 * USECS_PER_SECOND; // if idle for 30 seconds, drop focus + quint64 elapsedSinceAcceptedKeyPress = usecTimestampNow() - _lastAcceptedKeyPress; + if (elapsedSinceAcceptedKeyPress > LOSE_FOCUS_AFTER_ELAPSED_TIME) { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + } else { + if (auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get())) { + EntityItemProperties properties; + properties.setPosition(entity->getWorldPosition()); + properties.setRotation(entity->getWorldOrientation()); + properties.setDimensions(entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); + DependencyManager::get()->editEntity(_keyboardFocusHighlightID, properties); + } + } + } + } + + { + if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) { + QUuid entityId = _keyboardFocusedEntity.get(); + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + _keyboardFocusWaitingOnRenderable = false; + setKeyboardFocusEntity(entityId); + } + } + + { + PerformanceTimer perfTimer("pluginIdle"); + PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()"); + getActiveDisplayPlugin()->idle(); + auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); + foreach(auto inputPlugin, inputPlugins) { + if (inputPlugin->isActive()) { + inputPlugin->idle(); + } + } + } + { + PerformanceTimer perfTimer("rest"); + PerformanceWarning warn(showWarnings, "Application::idle()... rest of it"); + _idleLoopStdev.addValue(secondsSinceLastUpdate); + + // Record standard deviation and reset counter if needed + const int STDEV_SAMPLES = 500; + if (_idleLoopStdev.getSamples() > STDEV_SAMPLES) { + _idleLoopMeasuredJitter = _idleLoopStdev.getStDev(); + _idleLoopStdev.reset(); + } + } + + _overlayConductor.update(secondsSinceLastUpdate); + + _gameLoopCounter.increment(); +} + +ivec2 Application::getMouse() const { + return getApplicationCompositor().getReticlePosition(); +} + +FaceTracker* Application::getActiveFaceTracker() { + auto dde = DependencyManager::get(); + + return dde->isActive() ? static_cast(dde.data()) : nullptr; +} + +FaceTracker* Application::getSelectedFaceTracker() { + FaceTracker* faceTracker = nullptr; +#ifdef HAVE_DDE + if (Menu::getInstance()->isOptionChecked(MenuOption::UseCamera)) { + faceTracker = DependencyManager::get().data(); + } +#endif + return faceTracker; +} + +void Application::setActiveFaceTracker() const { +#ifdef HAVE_DDE + bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); + bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); + Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::UseAudioForMouth)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::VelocityFilter)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setVisible(isUsingDDE); + auto ddeTracker = DependencyManager::get(); + ddeTracker->setIsMuted(isMuted); + ddeTracker->setEnabled(isUsingDDE && !isMuted); +#endif +} + +#ifdef HAVE_IVIEWHMD +void Application::setActiveEyeTracker() { + auto eyeTracker = DependencyManager::get(); + if (!eyeTracker->isInitialized()) { + return; + } + + bool isEyeTracking = Menu::getInstance()->isOptionChecked(MenuOption::SMIEyeTracking); + bool isSimulating = Menu::getInstance()->isOptionChecked(MenuOption::SimulateEyeTracking); + eyeTracker->setEnabled(isEyeTracking, isSimulating); + + Menu::getInstance()->getActionForOption(MenuOption::OnePointCalibration)->setEnabled(isEyeTracking && !isSimulating); + Menu::getInstance()->getActionForOption(MenuOption::ThreePointCalibration)->setEnabled(isEyeTracking && !isSimulating); + Menu::getInstance()->getActionForOption(MenuOption::FivePointCalibration)->setEnabled(isEyeTracking && !isSimulating); +} + +void Application::calibrateEyeTracker1Point() { + DependencyManager::get()->calibrate(1); +} + +void Application::calibrateEyeTracker3Points() { + DependencyManager::get()->calibrate(3); +} + +void Application::calibrateEyeTracker5Points() { + DependencyManager::get()->calibrate(5); +} +#endif + +bool Application::exportEntities(const QString& filename, + const QVector& entityIDs, + const glm::vec3* givenOffset) { + QHash entities; + + auto nodeList = DependencyManager::get(); + const QUuid myAvatarID = nodeList->getSessionUUID(); + + auto entityTree = getEntities()->getTree(); + auto exportTree = std::make_shared(); + exportTree->setMyAvatar(getMyAvatar()); + exportTree->createRootElement(); + glm::vec3 root(TREE_SCALE, TREE_SCALE, TREE_SCALE); + bool success = true; + entityTree->withReadLock([entityIDs, entityTree, givenOffset, myAvatarID, &root, &entities, &success, &exportTree] { + for (auto entityID : entityIDs) { // Gather entities and properties. + auto entityItem = entityTree->findEntityByEntityItemID(entityID); + if (!entityItem) { + qCWarning(interfaceapp) << "Skipping export of" << entityID << "that is not in scene."; + continue; + } + + if (!givenOffset) { + EntityItemID parentID = entityItem->getParentID(); + bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); + if (!parentIsAvatar && (parentID.isInvalidID() || + !entityIDs.contains(parentID) || + !entityTree->findEntityByEntityItemID(parentID))) { + // If parent wasn't selected, we want absolute position, which isn't in properties. + auto position = entityItem->getWorldPosition(); + root.x = glm::min(root.x, position.x); + root.y = glm::min(root.y, position.y); + root.z = glm::min(root.z, position.z); + } + } + entities[entityID] = entityItem; + } + + if (entities.size() == 0) { + success = false; + return; + } + + if (givenOffset) { + root = *givenOffset; + } + for (EntityItemPointer& entityDatum : entities) { + auto properties = entityDatum->getProperties(); + EntityItemID parentID = properties.getParentID(); + bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); + if (parentIsAvatar) { + properties.setParentID(AVATAR_SELF_ID); + } else { + if (parentID.isInvalidID()) { + properties.setPosition(properties.getPosition() - root); + } else if (!entities.contains(parentID)) { + entityDatum->globalizeProperties(properties, "Parent %3 of %2 %1 is not selected for export.", -root); + } // else valid parent -- don't offset + } + exportTree->addEntity(entityDatum->getEntityItemID(), properties); + } + }); + if (success) { + success = exportTree->writeToJSONFile(filename.toLocal8Bit().constData()); + + // restore the main window's active state + _window->activateWindow(); + } + return success; +} + +bool Application::exportEntities(const QString& filename, float x, float y, float z, float scale) { + glm::vec3 center(x, y, z); + glm::vec3 minCorner = center - vec3(scale); + float cubeSize = scale * 2; + AACube boundingCube(minCorner, cubeSize); + QVector entities; + auto entityTree = getEntities()->getTree(); + entityTree->withReadLock([&] { + entityTree->evalEntitiesInCube(boundingCube, PickFilter(), entities); + }); + return exportEntities(filename, entities, ¢er); +} + +void Application::loadSettings() { + + sessionRunTime.set(0); // Just clean living. We're about to saveSettings, which will update value. + DependencyManager::get()->loadSettings(); + DependencyManager::get()->loadSettings(); + + // DONT CHECK IN + //DependencyManager::get()->setAutomaticLODAdjust(false); + + auto menu = Menu::getInstance(); + menu->loadSettings(); + + // override the menu option show overlays to always be true on startup + menu->setIsOptionChecked(MenuOption::Overlays, true); + + // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. + auto pluginManager = PluginManager::getInstance(); + auto plugins = pluginManager->getPreferredDisplayPlugins(); + if (plugins.size() > 0) { + for (auto plugin : plugins) { + if (auto action = menu->getActionForOption(plugin->getName())) { + action->setChecked(true); + action->trigger(); + // Find and activated highest priority plugin, bail for the rest + break; + } + } + } + + bool isFirstPerson = false; + if (_firstRun.get()) { + // If this is our first run, and no preferred devices were set, default to + // an HMD device if available. + auto displayPlugins = pluginManager->getDisplayPlugins(); + for (auto& plugin : displayPlugins) { + if (plugin->isHmd()) { + if (auto action = menu->getActionForOption(plugin->getName())) { + action->setChecked(true); + action->trigger(); + break; + } + } + } + + isFirstPerson = (qApp->isHMDMode()); + + } else { + // if this is not the first run, the camera will be initialized differently depending on user settings + + if (qApp->isHMDMode()) { + // if the HMD is active, use first-person camera, unless the appropriate setting is checked + isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonHMD); + } else { + // if HMD is not active, only use first person if the menu option is checked + isFirstPerson = menu->isOptionChecked(MenuOption::FirstPerson); + } + } + + // finish initializing the camera, based on everything we checked above. Third person camera will be used if no settings + // dictated that we should be in first person + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, isFirstPerson); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !isFirstPerson); + _myCamera.setMode((isFirstPerson) ? CAMERA_MODE_FIRST_PERSON : CAMERA_MODE_THIRD_PERSON); + cameraMenuChanged(); + + auto inputs = pluginManager->getInputPlugins(); + for (auto plugin : inputs) { + if (!plugin->isActive()) { + plugin->activate(); + } + } + + getMyAvatar()->loadData(); + _settingsLoaded = true; +} + +void Application::saveSettings() const { + sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SECOND); + DependencyManager::get()->saveSettings(); + DependencyManager::get()->saveSettings(); + + Menu::getInstance()->saveSettings(); + getMyAvatar()->saveData(); + PluginManager::getInstance()->saveSettings(); +} + +bool Application::importEntities(const QString& urlOrFilename, const bool isObservable, const qint64 callerId) { + bool success = false; + _entityClipboard->withWriteLock([&] { + _entityClipboard->eraseAllOctreeElements(); + + success = _entityClipboard->readFromURL(urlOrFilename, isObservable, callerId); + if (success) { + _entityClipboard->reaverageOctreeElements(); + } + }); + return success; +} + +QVector Application::pasteEntities(float x, float y, float z) { + return _entityClipboard->sendEntities(&_entityEditSender, getEntities()->getTree(), x, y, z); +} + +void Application::init() { + // Make sure Login state is up to date +#if !defined(DISABLE_QML) + DependencyManager::get()->toggleLoginDialog(); +#endif + DependencyManager::get()->init(); + + _timerStart.start(); + _lastTimeUpdated.start(); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + // when +connect_lobby in command line, join steam lobby + const QString STEAM_LOBBY_COMMAND_LINE_KEY = "+connect_lobby"; + int lobbyIndex = arguments().indexOf(STEAM_LOBBY_COMMAND_LINE_KEY); + if (lobbyIndex != -1) { + QString lobbyId = arguments().value(lobbyIndex + 1); + steamClient->joinLobby(lobbyId); + } + } + + + qCDebug(interfaceapp) << "Loaded settings"; + + // fire off an immediate domain-server check in now that settings are loaded + if (!isServerlessMode()) { + DependencyManager::get()->sendDomainServerCheckIn(); + } + + // This allows collision to be set up properly for shape entities supported by GeometryCache. + // This is before entity setup to ensure that it's ready for whenever instance collision is initialized. + ShapeEntityItem::setShapeInfoCalulator(ShapeEntityItem::ShapeInfoCalculator(&shapeInfoCalculator)); + + getEntities()->init(); + getEntities()->setEntityLoadingPriorityFunction([this](const EntityItem& item) { + auto dims = item.getScaledDimensions(); + auto maxSize = glm::compMax(dims); + + if (maxSize <= 0.0f) { + return 0.0f; + } + + auto distance = glm::distance(getMyAvatar()->getWorldPosition(), item.getWorldPosition()); + return atan2(maxSize, distance); + }); + + ObjectMotionState::setShapeManager(&_shapeManager); + _physicsEngine->init(); + + EntityTreePointer tree = getEntities()->getTree(); + _entitySimulation->init(tree, _physicsEngine, &_entityEditSender); + tree->setSimulation(_entitySimulation); + + auto entityScriptingInterface = DependencyManager::get(); + + // connect the _entityCollisionSystem to our EntityTreeRenderer since that's what handles running entity scripts + connect(_entitySimulation.get(), &PhysicalEntitySimulation::entityCollisionWithEntity, + getEntities().data(), &EntityTreeRenderer::entityCollisionWithEntity); + + // connect the _entities (EntityTreeRenderer) to our script engine's EntityScriptingInterface for firing + // of events related clicking, hovering over, and entering entities + getEntities()->connectSignalsToSlots(entityScriptingInterface.data()); + + // Make sure any new sounds are loaded as soon as know about them. + connect(tree.get(), &EntityTree::newCollisionSoundURL, this, [this](QUrl newURL, EntityItemID id) { + getEntities()->setCollisionSound(id, DependencyManager::get()->getSound(newURL)); + }, Qt::QueuedConnection); + connect(getMyAvatar().get(), &MyAvatar::newCollisionSoundURL, this, [this](QUrl newURL) { + if (auto avatar = getMyAvatar()) { + auto sound = DependencyManager::get()->getSound(newURL); + avatar->setCollisionSound(sound); + } + }, Qt::QueuedConnection); + + _gameWorkload.startup(getEntities()->getWorkloadSpace(), _graphicsEngine.getRenderScene(), _entitySimulation); + _entitySimulation->setWorkloadSpace(getEntities()->getWorkloadSpace()); +} + +void Application::pauseUntilLoginDetermined() { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "pauseUntilLoginDetermined"); + return; + } + + auto myAvatar = getMyAvatar(); + _previousAvatarTargetScale = myAvatar->getTargetScale(); + _previousAvatarSkeletonModel = myAvatar->getSkeletonModelURL().toString(); + myAvatar->setTargetScale(1.0f); + myAvatar->setSkeletonModelURLFromScript(myAvatar->defaultFullAvatarModelUrl().toString()); + myAvatar->setEnableMeshVisible(false); + + _controllerScriptingInterface->disableMapping(STANDARD_TO_ACTION_MAPPING_NAME); + + { + auto userInputMapper = DependencyManager::get(); + if (userInputMapper->loadMapping(NO_MOVEMENT_MAPPING_JSON)) { + _controllerScriptingInterface->enableMapping(NO_MOVEMENT_MAPPING_NAME); + } + } + + const auto& nodeList = DependencyManager::get(); + // save interstitial mode setting until resuming. + _interstitialModeEnabled = nodeList->getDomainHandler().getInterstitialModeEnabled(); + nodeList->getDomainHandler().setInterstitialModeEnabled(false); + + auto menu = Menu::getInstance(); + menu->getMenu("Edit")->setVisible(false); + menu->getMenu("View")->setVisible(false); + menu->getMenu("Navigate")->setVisible(false); + menu->getMenu("Settings")->setVisible(false); + _developerMenuVisible = menu->getMenu("Developer")->isVisible(); + menu->setIsOptionChecked(MenuOption::Stats, false); + if (_developerMenuVisible) { + menu->getMenu("Developer")->setVisible(false); + } + _previousCameraMode = _myCamera.getMode(); + _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); + cameraModeChanged(); + + // disconnect domain handler. + nodeList->getDomainHandler().disconnect(); + +} + +void Application::resumeAfterLoginDialogActionTaken() { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "resumeAfterLoginDialogActionTaken"); + return; + } + + if (!isHMDMode() && getDesktopTabletBecomesToolbarSetting()) { + auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); + toolbar->writeProperty("visible", true); + } else { + getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(true); + getApplicationCompositor().getReticleInterface()->setVisible(true); + } + + updateSystemTabletMode(); + + { + auto userInputMapper = DependencyManager::get(); + userInputMapper->unloadMapping(NO_MOVEMENT_MAPPING_JSON); + _controllerScriptingInterface->disableMapping(NO_MOVEMENT_MAPPING_NAME); + } + + auto myAvatar = getMyAvatar(); + myAvatar->setTargetScale(_previousAvatarTargetScale); + myAvatar->setSkeletonModelURLFromScript(_previousAvatarSkeletonModel); + myAvatar->setEnableMeshVisible(true); + + _controllerScriptingInterface->enableMapping(STANDARD_TO_ACTION_MAPPING_NAME); + + const auto& nodeList = DependencyManager::get(); + nodeList->getDomainHandler().setInterstitialModeEnabled(_interstitialModeEnabled); + { + auto scriptEngines = DependencyManager::get().data(); + // this will force the model the look at the correct directory (weird order of operations issue) + scriptEngines->reloadLocalFiles(); + + // if the --scripts command-line argument was used. + if (!_defaultScriptsLocation.exists() && (arguments().indexOf(QString("--").append(SCRIPTS_SWITCH))) != -1) { + scriptEngines->loadDefaultScripts(); + scriptEngines->defaultScriptsLocationOverridden(true); + } else { + scriptEngines->loadScripts(); + } + } + + auto accountManager = DependencyManager::get(); + auto addressManager = DependencyManager::get(); + + // restart domain handler. + nodeList->getDomainHandler().resetting(); + + QVariant testProperty = property(hifi::properties::TEST); + if (testProperty.isValid()) { + const auto testScript = property(hifi::properties::TEST).toUrl(); + // Set last parameter to exit interface when the test script finishes, if so requested + DependencyManager::get()->loadScript(testScript, false, false, false, false, quitWhenFinished); + // This is done so we don't get a "connection time-out" message when we haven't passed in a URL. + if (arguments().contains("--url")) { + auto reply = SandboxUtils::getStatus(); + connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); + } + } else { + auto reply = SandboxUtils::getStatus(); + connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); + } + + auto menu = Menu::getInstance(); + menu->getMenu("Edit")->setVisible(true); + menu->getMenu("View")->setVisible(true); + menu->getMenu("Navigate")->setVisible(true); + menu->getMenu("Settings")->setVisible(true); + menu->getMenu("Developer")->setVisible(_developerMenuVisible); + _myCamera.setMode(_previousCameraMode); + cameraModeChanged(); +} + +void Application::loadAvatarScripts(const QVector& urls) { + auto scriptEngines = DependencyManager::get(); + auto runningScripts = scriptEngines->getRunningScripts(); + for (auto url : urls) { + int index = runningScripts.indexOf(url); + if (index < 0) { + auto scriptEnginePointer = scriptEngines->loadScript(url, false); + if (scriptEnginePointer) { + scriptEnginePointer->setType(ScriptEngine::Type::AVATAR); + } + } + } +} + +void Application::unloadAvatarScripts() { + auto scriptEngines = DependencyManager::get(); + auto urls = scriptEngines->getRunningScripts(); + for (auto url : urls) { + auto scriptEngine = scriptEngines->getScriptEngine(url); + if (scriptEngine->getType() == ScriptEngine::Type::AVATAR) { + scriptEngines->stopScript(url, false); + } + } +} + +void Application::updateLOD(float deltaTime) const { + PerformanceTimer perfTimer("LOD"); + // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode + if (!isThrottleRendering()) { + float presentTime = getActiveDisplayPlugin()->getAveragePresentTime(); + float engineRunTime = (float)(_graphicsEngine.getRenderEngine()->getConfiguration().get()->getCPURunTime()); + float gpuTime = getGPUContext()->getFrameTimerGPUAverage(); + float batchTime = getGPUContext()->getFrameTimerBatchAverage(); + auto lodManager = DependencyManager::get(); + lodManager->setRenderTimes(presentTime, engineRunTime, batchTime, gpuTime); + lodManager->autoAdjustLOD(deltaTime); + } else { + DependencyManager::get()->resetLODAdjust(); + } +} + +void Application::pushPostUpdateLambda(void* key, const std::function& func) { + std::unique_lock guard(_postUpdateLambdasLock); + _postUpdateLambdas[key] = func; +} + +// Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent to everyone. +// (Maybe this code should be moved there?) +// The principal result is to call updateLookAtTargetAvatar() and then setLookAtPosition(). +// Note that it is called BEFORE we update position or joints based on sensors, etc. +void Application::updateMyAvatarLookAtPosition() { + PerformanceTimer perfTimer("lookAt"); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); + + auto myAvatar = getMyAvatar(); + myAvatar->updateLookAtTargetAvatar(); + FaceTracker* faceTracker = getActiveFaceTracker(); + auto eyeTracker = DependencyManager::get(); + + bool isLookingAtSomeone = false; + bool isHMD = qApp->isHMDMode(); + glm::vec3 lookAtSpot; + if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { + // Look at the point that the user is looking at. + glm::vec3 lookAtPosition = eyeTracker->getLookAtPosition(); + if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + lookAtPosition.x = -lookAtPosition.x; + } + if (isHMD) { + // TODO -- this code is probably wrong, getHeadPose() returns something in sensor frame, not avatar + glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); + glm::quat hmdRotation = glm::quat_cast(headPose); + lookAtSpot = _myCamera.getPosition() + myAvatar->getWorldOrientation() * (hmdRotation * lookAtPosition); + } else { + lookAtSpot = myAvatar->getHead()->getEyePosition() + + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * lookAtPosition); + } + } else { + AvatarSharedPointer lookingAt = myAvatar->getLookAtTargetAvatar().lock(); + bool haveLookAtCandidate = lookingAt && myAvatar.get() != lookingAt.get(); + auto avatar = static_pointer_cast(lookingAt); + bool mutualLookAtSnappingEnabled = avatar && avatar->getLookAtSnappingEnabled() && myAvatar->getLookAtSnappingEnabled(); + if (haveLookAtCandidate && mutualLookAtSnappingEnabled) { + // If I am looking at someone else, look directly at one of their eyes + isLookingAtSomeone = true; + auto lookingAtHead = avatar->getHead(); + + const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; + glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; + glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() + - lookingAtHead->getEyePosition()); + float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); + + if (faceAngle < MAXIMUM_FACE_ANGLE) { + // Randomly look back and forth between look targets + eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? + LEFT_EYE : myAvatar->getEyeContactTarget(); + switch (target) { + case LEFT_EYE: + lookAtSpot = lookingAtHead->getLeftEyePosition(); + break; + case RIGHT_EYE: + lookAtSpot = lookingAtHead->getRightEyePosition(); + break; + case MOUTH: + lookAtSpot = lookingAtHead->getMouthPosition(); + break; + } + } else { + // Just look at their head (mid point between eyes) + lookAtSpot = lookingAtHead->getEyePosition(); + } + } else { + // I am not looking at anyone else, so just look forward + auto headPose = myAvatar->getControllerPoseInWorldFrame(controller::Action::HEAD); + if (headPose.isValid()) { + lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE)); + } else { + lookAtSpot = myAvatar->getHead()->getEyePosition() + + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); + } + } + + // Deflect the eyes a bit to match the detected gaze from the face tracker if active. + if (faceTracker && !faceTracker->isMuted()) { + float eyePitch = faceTracker->getEstimatedEyePitch(); + float eyeYaw = faceTracker->getEstimatedEyeYaw(); + const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; + glm::vec3 origin = myAvatar->getHead()->getEyePosition(); + float deflection = faceTracker->getEyeDeflection(); + if (isLookingAtSomeone) { + deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; + } + lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( + eyePitch * deflection, eyeYaw * deflection, 0.0f))) * + glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); + } + } + + myAvatar->getHead()->setLookAtPosition(lookAtSpot); +} + +void Application::updateThreads(float deltaTime) { + PerformanceTimer perfTimer("updateThreads"); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::updateThreads()"); + + // parse voxel packets + if (!_enableProcessOctreeThread) { + _octreeProcessor.threadRoutine(); + _entityEditSender.threadRoutine(); + } +} + +void Application::toggleOverlays() { + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, !menu->isOptionChecked(MenuOption::Overlays)); +} + +void Application::setOverlaysVisible(bool visible) { + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, visible); +} + +void Application::centerUI() { + _overlayConductor.centerUI(); +} + +void Application::cycleCamera() { + auto menu = Menu::getInstance(); + if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { + + menu->setIsOptionChecked(MenuOption::FullscreenMirror, false); + menu->setIsOptionChecked(MenuOption::FirstPerson, true); + + } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { + + menu->setIsOptionChecked(MenuOption::FirstPerson, false); + menu->setIsOptionChecked(MenuOption::ThirdPerson, true); + + } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { + + menu->setIsOptionChecked(MenuOption::ThirdPerson, false); + menu->setIsOptionChecked(MenuOption::FullscreenMirror, true); + + } else if (menu->isOptionChecked(MenuOption::IndependentMode) || menu->isOptionChecked(MenuOption::CameraEntityMode)) { + // do nothing if in independent or camera entity modes + return; + } + cameraMenuChanged(); // handle the menu change +} + +void Application::cameraModeChanged() { + switch (_myCamera.getMode()) { + case CAMERA_MODE_FIRST_PERSON: + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); + break; + case CAMERA_MODE_THIRD_PERSON: + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + break; + case CAMERA_MODE_MIRROR: + Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, true); + break; + case CAMERA_MODE_INDEPENDENT: + Menu::getInstance()->setIsOptionChecked(MenuOption::IndependentMode, true); + break; + case CAMERA_MODE_ENTITY: + Menu::getInstance()->setIsOptionChecked(MenuOption::CameraEntityMode, true); + break; + default: + break; + } + cameraMenuChanged(); +} + +void Application::changeViewAsNeeded(float boomLength) { + // Switch between first and third person views as needed + // This is called when the boom length has changed + bool boomLengthGreaterThanMinimum = (boomLength > MyAvatar::ZOOM_MIN); + + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON && boomLengthGreaterThanMinimum) { + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, false); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + cameraMenuChanged(); + } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON && !boomLengthGreaterThanMinimum) { + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, false); + cameraMenuChanged(); + } +} + +void Application::cameraMenuChanged() { + auto menu = Menu::getInstance(); + if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { + if (!isHMDMode() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { + _mirrorYawOffset = 0.0f; + _myCamera.setMode(CAMERA_MODE_MIRROR); + getMyAvatar()->reset(false, false, false); // to reset any active MyAvatar::FollowHelpers + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); + } + } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { + if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) { + _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); + } + } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { + if (_myCamera.getMode() != CAMERA_MODE_THIRD_PERSON) { + _myCamera.setMode(CAMERA_MODE_THIRD_PERSON); + if (getMyAvatar()->getBoomLength() == MyAvatar::ZOOM_MIN) { + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); + } + } + } else if (menu->isOptionChecked(MenuOption::IndependentMode)) { + if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { + _myCamera.setMode(CAMERA_MODE_INDEPENDENT); + } + } else if (menu->isOptionChecked(MenuOption::CameraEntityMode)) { + if (_myCamera.getMode() != CAMERA_MODE_ENTITY) { + _myCamera.setMode(CAMERA_MODE_ENTITY); + } + } +} + +void Application::resetPhysicsReadyInformation() { + // we've changed domains or cleared out caches or something. we no longer know enough about the + // collision information of nearby entities to make running bullet be safe. + _fullSceneReceivedCounter = 0; + _fullSceneCounterAtLastPhysicsCheck = 0; + _gpuTextureMemSizeStabilityCount = 0; + _gpuTextureMemSizeAtLastCheck = 0; + _physicsEnabled = false; + _octreeProcessor.startEntitySequence(); +} + + +void Application::reloadResourceCaches() { + resetPhysicsReadyInformation(); + + // Query the octree to refresh everything in view + _queryExpiry = SteadyClock::now(); + _octreeQuery.incrementConnectionID(); + + queryOctree(NodeType::EntityServer, PacketType::EntityQuery); + + // Clear the entities and their renderables + getEntities()->clear(); + + DependencyManager::get()->clearCache(); + DependencyManager::get()->clearCache(); + + // Clear all the resource caches + DependencyManager::get()->clear(); + DependencyManager::get()->refreshAll(); + DependencyManager::get()->refreshAll(); + MaterialCache::instance().refreshAll(); + DependencyManager::get()->refreshAll(); + ShaderCache::instance().refreshAll(); + DependencyManager::get()->refreshAll(); + DependencyManager::get()->refreshAll(); + + DependencyManager::get()->reset(); // Force redownload of .fst models + + DependencyManager::get()->reloadAllScripts(); + getOffscreenUI()->clearCache(); + + DependencyManager::get()->createKeyboard(); + + getMyAvatar()->resetFullAvatarURL(); +} + +void Application::rotationModeChanged() const { + if (!Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { + getMyAvatar()->setHeadPitch(0); + } +} + +void Application::setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions) { + if (qApp->getLoginDialogPoppedUp()) { + return; + } + + auto entityScriptingInterface = DependencyManager::get(); + if (_keyboardFocusHighlightID == UNKNOWN_ENTITY_ID || !entityScriptingInterface->isAddedEntity(_keyboardFocusHighlightID)) { + EntityItemProperties properties; + properties.setType(EntityTypes::Box); + properties.setAlpha(1.0f); + properties.setColor({ 0xFF, 0xEF, 0x00 }); + properties.setPrimitiveMode(PrimitiveMode::LINES); + properties.getPulse().setMin(0.5); + properties.getPulse().setMax(1.0f); + properties.getPulse().setColorMode(PulseMode::IN_PHASE); + properties.setIgnorePickIntersection(true); + _keyboardFocusHighlightID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); + } + + // Position focus + EntityItemProperties properties; + properties.setPosition(position); + properties.setRotation(rotation); + properties.setDimensions(dimensions); + properties.setVisible(true); + entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); +} + +QUuid Application::getKeyboardFocusEntity() const { + return _keyboardFocusedEntity.get(); +} + +void Application::setKeyboardFocusEntity(const QUuid& id) { + if (_keyboardFocusedEntity.get() != id) { + if (qApp->getLoginDialogPoppedUp() && !_loginDialogID.isNull()) { + if (id == _loginDialogID) { + emit loginDialogFocusEnabled(); + } else if (!_keyboardFocusWaitingOnRenderable) { + // that's the only entity we want in focus; + return; + } + } + + _keyboardFocusedEntity.set(id); + + auto entityScriptingInterface = DependencyManager::get(); + if (id != UNKNOWN_ENTITY_ID) { + EntityPropertyFlags desiredProperties; + desiredProperties += PROP_VISIBLE; + desiredProperties += PROP_SHOW_KEYBOARD_FOCUS_HIGHLIGHT; + auto properties = entityScriptingInterface->getEntityProperties(id); + if (properties.getVisible()) { + auto entities = getEntities(); + auto entityId = _keyboardFocusedEntity.get(); + auto entityItemRenderable = entities->renderableForEntityId(entityId); + if (!entityItemRenderable) { + _keyboardFocusWaitingOnRenderable = true; + } else if (entityItemRenderable->wantsKeyboardFocus()) { + entities->setProxyWindow(entityId, _window->windowHandle()); + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->pluginFocusOutEvent(); + } + _lastAcceptedKeyPress = usecTimestampNow(); + + if (properties.getShowKeyboardFocusHighlight()) { + if (auto entity = entities->getEntity(entityId)) { + setKeyboardFocusHighlight(entity->getWorldPosition(), entity->getWorldOrientation(), + entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); + return; + } + } + } + } + } + + EntityItemProperties properties; + properties.setVisible(false); + entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); + } +} + +void Application::updateDialogs(float deltaTime) const { + PerformanceTimer perfTimer("updateDialogs"); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); + auto dialogsManager = DependencyManager::get(); + + QPointer octreeStatsDialog = dialogsManager->getOctreeStatsDialog(); + if (octreeStatsDialog) { + octreeStatsDialog->update(); + } +} + +void Application::updateSecondaryCameraViewFrustum() { + // TODO: Fix this by modeling the way the secondary camera works on how the main camera works + // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary + // camera should be. + + // Code based on SecondaryCameraJob + auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); + assert(renderConfig); + auto camera = dynamic_cast(renderConfig->getConfig("SecondaryCamera")); + + if (!camera || !camera->isEnabled()) { + return; + } + + ViewFrustum secondaryViewFrustum; + if (camera->portalProjection && !camera->attachedEntityId.isNull() && !camera->portalEntranceEntityId.isNull()) { + auto entityScriptingInterface = DependencyManager::get(); + EntityItemPointer portalEntrance = qApp->getEntities()->getTree()->findEntityByID(camera->portalEntranceEntityId); + EntityItemPointer portalExit = qApp->getEntities()->getTree()->findEntityByID(camera->attachedEntityId); + + glm::vec3 portalEntrancePropertiesPosition = portalEntrance->getWorldPosition(); + glm::quat portalEntrancePropertiesRotation = portalEntrance->getWorldOrientation(); + glm::mat4 worldFromPortalEntranceRotation = glm::mat4_cast(portalEntrancePropertiesRotation); + glm::mat4 worldFromPortalEntranceTranslation = glm::translate(portalEntrancePropertiesPosition); + glm::mat4 worldFromPortalEntrance = worldFromPortalEntranceTranslation * worldFromPortalEntranceRotation; + glm::mat4 portalEntranceFromWorld = glm::inverse(worldFromPortalEntrance); + + glm::vec3 portalExitPropertiesPosition = portalExit->getWorldPosition(); + glm::quat portalExitPropertiesRotation = portalExit->getWorldOrientation(); + glm::vec3 portalExitPropertiesDimensions = portalExit->getScaledDimensions(); + glm::vec3 halfPortalExitPropertiesDimensions = 0.5f * portalExitPropertiesDimensions; + + glm::mat4 worldFromPortalExitRotation = glm::mat4_cast(portalExitPropertiesRotation); + glm::mat4 worldFromPortalExitTranslation = glm::translate(portalExitPropertiesPosition); + glm::mat4 worldFromPortalExit = worldFromPortalExitTranslation * worldFromPortalExitRotation; + + glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); + glm::vec3 mainCameraPositionPortalEntrance = vec3(portalEntranceFromWorld * vec4(mainCameraPositionWorld, 1.0f)); + mainCameraPositionPortalEntrance = vec3(-mainCameraPositionPortalEntrance.x, mainCameraPositionPortalEntrance.y, + -mainCameraPositionPortalEntrance.z); + glm::vec3 portalExitCameraPositionWorld = vec3(worldFromPortalExit * vec4(mainCameraPositionPortalEntrance, 1.0f)); + + secondaryViewFrustum.setPosition(portalExitCameraPositionWorld); + secondaryViewFrustum.setOrientation(portalExitPropertiesRotation); + + float nearClip = mainCameraPositionPortalEntrance.z + portalExitPropertiesDimensions.z * 2.0f; + // `mainCameraPositionPortalEntrance` should technically be `mainCameraPositionPortalExit`, + // but the values are the same. + glm::vec3 upperRight = halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; + glm::vec3 bottomLeft = -halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; + glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); + secondaryViewFrustum.setProjection(frustum); + } else if (camera->mirrorProjection && !camera->attachedEntityId.isNull()) { + auto entityScriptingInterface = DependencyManager::get(); + auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); + glm::vec3 mirrorPropertiesPosition = entityProperties.getPosition(); + glm::quat mirrorPropertiesRotation = entityProperties.getRotation(); + glm::vec3 mirrorPropertiesDimensions = entityProperties.getDimensions(); + glm::vec3 halfMirrorPropertiesDimensions = 0.5f * mirrorPropertiesDimensions; + + // setup mirror from world as inverse of world from mirror transformation using inverted x and z for mirrored image + // TODO: we are assuming here that UP is world y-axis + glm::mat4 worldFromMirrorRotation = glm::mat4_cast(mirrorPropertiesRotation) * glm::scale(vec3(-1.0f, 1.0f, -1.0f)); + glm::mat4 worldFromMirrorTranslation = glm::translate(mirrorPropertiesPosition); + glm::mat4 worldFromMirror = worldFromMirrorTranslation * worldFromMirrorRotation; + glm::mat4 mirrorFromWorld = glm::inverse(worldFromMirror); + + // get mirror camera position by reflecting main camera position's z coordinate in mirror space + glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); + glm::vec3 mainCameraPositionMirror = vec3(mirrorFromWorld * vec4(mainCameraPositionWorld, 1.0f)); + glm::vec3 mirrorCameraPositionMirror = vec3(mainCameraPositionMirror.x, mainCameraPositionMirror.y, + -mainCameraPositionMirror.z); + glm::vec3 mirrorCameraPositionWorld = vec3(worldFromMirror * vec4(mirrorCameraPositionMirror, 1.0f)); + + // set frustum position to be mirrored camera and set orientation to mirror's adjusted rotation + glm::quat mirrorCameraOrientation = glm::quat_cast(worldFromMirrorRotation); + secondaryViewFrustum.setPosition(mirrorCameraPositionWorld); + secondaryViewFrustum.setOrientation(mirrorCameraOrientation); + + // build frustum using mirror space translation of mirrored camera + float nearClip = mirrorCameraPositionMirror.z + mirrorPropertiesDimensions.z * 2.0f; + glm::vec3 upperRight = halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; + glm::vec3 bottomLeft = -halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; + glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); + secondaryViewFrustum.setProjection(frustum); + } else { + if (!camera->attachedEntityId.isNull()) { + auto entityScriptingInterface = DependencyManager::get(); + auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); + secondaryViewFrustum.setPosition(entityProperties.getPosition()); + secondaryViewFrustum.setOrientation(entityProperties.getRotation()); + } else { + secondaryViewFrustum.setPosition(camera->position); + secondaryViewFrustum.setOrientation(camera->orientation); + } + + float aspectRatio = (float)camera->textureWidth / (float)camera->textureHeight; + secondaryViewFrustum.setProjection(camera->vFoV, + aspectRatio, + camera->nearClipPlaneDistance, + camera->farClipPlaneDistance); + } + // Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera, + // which is not what we want here. + secondaryViewFrustum.calculate(); + + _conicalViews.push_back(secondaryViewFrustum); +} + +static bool domainLoadingInProgress = false; + +void Application::update(float deltaTime) { + PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_graphicsEngine._renderFrameCount + 1); + + if (_aboutToQuit) { + return; + } + + + if (!_physicsEnabled) { + if (!domainLoadingInProgress) { + PROFILE_ASYNC_BEGIN(app, "Scene Loading", ""); + domainLoadingInProgress = true; + } + + // we haven't yet enabled physics. we wait until we think we have all the collision information + // for nearby entities before starting bullet up. + quint64 now = usecTimestampNow(); + if (isServerlessMode() || _octreeProcessor.isLoadSequenceComplete()) { + bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + + if (gpuTextureMemSizeStable() || !enableInterstitial) { + // we've received a new full-scene octree stats packet, or it's been long enough to try again anyway + _lastPhysicsCheckTime = now; + _fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter; + _lastQueriedViews.clear(); // Force new view. + + // process octree stats packets are sent in between full sends of a scene (this isn't currently true). + // We keep physics disabled until we've received a full scene and everything near the avatar in that + // scene is ready to compute its collision shape. + if (getMyAvatar()->isReadyForPhysics()) { + _physicsEnabled = true; + setIsInterstitialMode(false); + getMyAvatar()->updateMotionBehaviorFromMenu(); + } + } + } + } else if (domainLoadingInProgress) { + domainLoadingInProgress = false; + PROFILE_ASYNC_END(app, "Scene Loading", ""); + } + + auto myAvatar = getMyAvatar(); + { + PerformanceTimer perfTimer("devices"); + + FaceTracker* tracker = getSelectedFaceTracker(); + if (tracker && Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking) != tracker->isMuted()) { + tracker->toggleMute(); + } + + tracker = getActiveFaceTracker(); + if (tracker && !tracker->isMuted()) { + tracker->update(deltaTime); + + // Auto-mute microphone after losing face tracking? + if (tracker->isTracking()) { + _lastFaceTrackerUpdate = usecTimestampNow(); + } else { + const quint64 MUTE_MICROPHONE_AFTER_USECS = 5000000; //5 secs + Menu* menu = Menu::getInstance(); + auto audioClient = DependencyManager::get(); + if (menu->isOptionChecked(MenuOption::AutoMuteAudio) && !audioClient->isMuted()) { + if (_lastFaceTrackerUpdate > 0 + && ((usecTimestampNow() - _lastFaceTrackerUpdate) > MUTE_MICROPHONE_AFTER_USECS)) { + audioClient->setMuted(true); + _lastFaceTrackerUpdate = 0; + } + } else { + _lastFaceTrackerUpdate = 0; + } + } + } else { + _lastFaceTrackerUpdate = 0; + } + + auto userInputMapper = DependencyManager::get(); + + controller::HmdAvatarAlignmentType hmdAvatarAlignmentType; + if (myAvatar->getHmdAvatarAlignmentType() == "eyes") { + hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Eyes; + } else { + hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Head; + } + + controller::InputCalibrationData calibrationData = { + myAvatar->getSensorToWorldMatrix(), + createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()), + myAvatar->getHMDSensorMatrix(), + myAvatar->getCenterEyeCalibrationMat(), + myAvatar->getHeadCalibrationMat(), + myAvatar->getSpine2CalibrationMat(), + myAvatar->getHipsCalibrationMat(), + myAvatar->getLeftFootCalibrationMat(), + myAvatar->getRightFootCalibrationMat(), + myAvatar->getRightArmCalibrationMat(), + myAvatar->getLeftArmCalibrationMat(), + myAvatar->getRightHandCalibrationMat(), + myAvatar->getLeftHandCalibrationMat(), + hmdAvatarAlignmentType + }; + + InputPluginPointer keyboardMousePlugin; + for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { + if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { + keyboardMousePlugin = inputPlugin; + } else if (inputPlugin->isActive()) { + inputPlugin->pluginUpdate(deltaTime, calibrationData); + } + } + + userInputMapper->setInputCalibrationData(calibrationData); + userInputMapper->update(deltaTime); + + if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { + keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); + } + // Transfer the user inputs to the driveKeys + // FIXME can we drop drive keys and just have the avatar read the action states directly? + myAvatar->clearDriveKeys(); + if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT && !isInterstitialMode()) { + if (!_controllerScriptingInterface->areActionsCaptured() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); + if (deltaTime > FLT_EPSILON) { + myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + myAvatar->setDriveKey(MyAvatar::DELTA_PITCH, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_PITCH)); + myAvatar->setDriveKey(MyAvatar::DELTA_YAW, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_YAW)); + myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + } + } + myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + } + + myAvatar->setSprintMode((bool)userInputMapper->getActionState(controller::Action::SPRINT)); + static const std::vector avatarControllerActions = { + controller::Action::LEFT_HAND, + controller::Action::RIGHT_HAND, + controller::Action::LEFT_FOOT, + controller::Action::RIGHT_FOOT, + controller::Action::HIPS, + controller::Action::SPINE2, + controller::Action::HEAD, + controller::Action::LEFT_HAND_THUMB1, + controller::Action::LEFT_HAND_THUMB2, + controller::Action::LEFT_HAND_THUMB3, + controller::Action::LEFT_HAND_THUMB4, + controller::Action::LEFT_HAND_INDEX1, + controller::Action::LEFT_HAND_INDEX2, + controller::Action::LEFT_HAND_INDEX3, + controller::Action::LEFT_HAND_INDEX4, + controller::Action::LEFT_HAND_MIDDLE1, + controller::Action::LEFT_HAND_MIDDLE2, + controller::Action::LEFT_HAND_MIDDLE3, + controller::Action::LEFT_HAND_MIDDLE4, + controller::Action::LEFT_HAND_RING1, + controller::Action::LEFT_HAND_RING2, + controller::Action::LEFT_HAND_RING3, + controller::Action::LEFT_HAND_RING4, + controller::Action::LEFT_HAND_PINKY1, + controller::Action::LEFT_HAND_PINKY2, + controller::Action::LEFT_HAND_PINKY3, + controller::Action::LEFT_HAND_PINKY4, + controller::Action::RIGHT_HAND_THUMB1, + controller::Action::RIGHT_HAND_THUMB2, + controller::Action::RIGHT_HAND_THUMB3, + controller::Action::RIGHT_HAND_THUMB4, + controller::Action::RIGHT_HAND_INDEX1, + controller::Action::RIGHT_HAND_INDEX2, + controller::Action::RIGHT_HAND_INDEX3, + controller::Action::RIGHT_HAND_INDEX4, + controller::Action::RIGHT_HAND_MIDDLE1, + controller::Action::RIGHT_HAND_MIDDLE2, + controller::Action::RIGHT_HAND_MIDDLE3, + controller::Action::RIGHT_HAND_MIDDLE4, + controller::Action::RIGHT_HAND_RING1, + controller::Action::RIGHT_HAND_RING2, + controller::Action::RIGHT_HAND_RING3, + controller::Action::RIGHT_HAND_RING4, + controller::Action::RIGHT_HAND_PINKY1, + controller::Action::RIGHT_HAND_PINKY2, + controller::Action::RIGHT_HAND_PINKY3, + controller::Action::RIGHT_HAND_PINKY4, + controller::Action::LEFT_ARM, + controller::Action::RIGHT_ARM, + controller::Action::LEFT_SHOULDER, + controller::Action::RIGHT_SHOULDER, + controller::Action::LEFT_FORE_ARM, + controller::Action::RIGHT_FORE_ARM, + controller::Action::LEFT_LEG, + controller::Action::RIGHT_LEG, + controller::Action::LEFT_UP_LEG, + controller::Action::RIGHT_UP_LEG, + controller::Action::LEFT_TOE_BASE, + controller::Action::RIGHT_TOE_BASE + }; + + // copy controller poses from userInputMapper to myAvatar. + glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()); + glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); + glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; + for (auto& action : avatarControllerActions) { + controller::Pose pose = userInputMapper->getPoseState(action); + myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); + } + + static const std::vector trackedObjectStringLiterals = { + QStringLiteral("_TrackedObject00"), QStringLiteral("_TrackedObject01"), QStringLiteral("_TrackedObject02"), QStringLiteral("_TrackedObject03"), + QStringLiteral("_TrackedObject04"), QStringLiteral("_TrackedObject05"), QStringLiteral("_TrackedObject06"), QStringLiteral("_TrackedObject07"), + QStringLiteral("_TrackedObject08"), QStringLiteral("_TrackedObject09"), QStringLiteral("_TrackedObject10"), QStringLiteral("_TrackedObject11"), + QStringLiteral("_TrackedObject12"), QStringLiteral("_TrackedObject13"), QStringLiteral("_TrackedObject14"), QStringLiteral("_TrackedObject15") + }; + + // Controlled by the Developer > Avatar > Show Tracked Objects menu. + if (_showTrackedObjects) { + static const std::vector trackedObjectActions = { + controller::Action::TRACKED_OBJECT_00, controller::Action::TRACKED_OBJECT_01, controller::Action::TRACKED_OBJECT_02, controller::Action::TRACKED_OBJECT_03, + controller::Action::TRACKED_OBJECT_04, controller::Action::TRACKED_OBJECT_05, controller::Action::TRACKED_OBJECT_06, controller::Action::TRACKED_OBJECT_07, + controller::Action::TRACKED_OBJECT_08, controller::Action::TRACKED_OBJECT_09, controller::Action::TRACKED_OBJECT_10, controller::Action::TRACKED_OBJECT_11, + controller::Action::TRACKED_OBJECT_12, controller::Action::TRACKED_OBJECT_13, controller::Action::TRACKED_OBJECT_14, controller::Action::TRACKED_OBJECT_15 + }; + + int i = 0; + glm::vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); + for (auto& action : trackedObjectActions) { + controller::Pose pose = userInputMapper->getPoseState(action); + if (pose.valid) { + glm::vec3 pos = transformPoint(myAvatarMatrix, pose.translation); + glm::quat rot = glmExtractRotation(myAvatarMatrix) * pose.rotation; + DebugDraw::getInstance().addMarker(trackedObjectStringLiterals[i], rot, pos, BLUE); + } else { + DebugDraw::getInstance().removeMarker(trackedObjectStringLiterals[i]); + } + i++; + } + } else if (_prevShowTrackedObjects) { + for (auto& key : trackedObjectStringLiterals) { + DebugDraw::getInstance().removeMarker(key); + } + } + _prevShowTrackedObjects = _showTrackedObjects; + } + + updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... + updateDialogs(deltaTime); // update various stats dialogs if present + + auto grabManager = DependencyManager::get(); + grabManager->simulateGrabs(); + + // TODO: break these out into distinct perfTimers when they prove interesting + { + PROFILE_RANGE(app, "PickManager"); + PerformanceTimer perfTimer("pickManager"); + DependencyManager::get()->update(); + } + + { + PROFILE_RANGE(app, "PointerManager"); + PerformanceTimer perfTimer("pointerManager"); + DependencyManager::get()->update(); + } + + QSharedPointer avatarManager = DependencyManager::get(); + + { + PROFILE_RANGE(simulation_physics, "Simulation"); + PerformanceTimer perfTimer("simulation"); + + if (_physicsEnabled) { + auto t0 = std::chrono::high_resolution_clock::now(); + auto t1 = t0; + { + PROFILE_RANGE(simulation_physics, "PrePhysics"); + PerformanceTimer perfTimer("prePhysics)"); + { + PROFILE_RANGE(simulation_physics, "RemoveEntities"); + const VectorOfMotionStates& motionStates = _entitySimulation->getObjectsToRemoveFromPhysics(); + { + PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); + _physicsEngine->removeObjects(motionStates); + } + _entitySimulation->deleteObjectsRemovedFromPhysics(); + } + + { + PROFILE_RANGE(simulation_physics, "AddEntities"); + VectorOfMotionStates motionStates; + getEntities()->getTree()->withReadLock([&] { + _entitySimulation->getObjectsToAddToPhysics(motionStates); + PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); + _physicsEngine->addObjects(motionStates); + }); + } + { + VectorOfMotionStates motionStates; + PROFILE_RANGE(simulation_physics, "ChangeEntities"); + getEntities()->getTree()->withReadLock([&] { + _entitySimulation->getObjectsToChange(motionStates); + VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); + _entitySimulation->setObjectsToChange(stillNeedChange); + }); + } + + _entitySimulation->applyDynamicChanges(); + + t1 = std::chrono::high_resolution_clock::now(); + + { + PROFILE_RANGE(simulation_physics, "Avatars"); + PhysicsEngine::Transaction transaction; + avatarManager->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + avatarManager->handleProcessedPhysicsTransaction(transaction); + myAvatar->getCharacterController()->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + myAvatar->getCharacterController()->handleProcessedPhysicsTransaction(transaction); + myAvatar->prepareForPhysicsSimulation(); + _physicsEngine->enableGlobalContactAddedCallback(myAvatar->isFlying()); + } + + { + PROFILE_RANGE(simulation_physics, "PrepareActions"); + _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { + dynamic->prepareForPhysicsSimulation(); + }); + } + } + auto t2 = std::chrono::high_resolution_clock::now(); + { + PROFILE_RANGE(simulation_physics, "StepPhysics"); + PerformanceTimer perfTimer("stepPhysics"); + getEntities()->getTree()->withWriteLock([&] { + _physicsEngine->stepSimulation(); + }); + } + auto t3 = std::chrono::high_resolution_clock::now(); + { + if (_physicsEngine->hasOutgoingChanges()) { + { + PROFILE_RANGE(simulation_physics, "PostPhysics"); + PerformanceTimer perfTimer("postPhysics"); + // grab the collision events BEFORE handleChangedMotionStates() because at this point + // we have a better idea of which objects we own or should own. + auto& collisionEvents = _physicsEngine->getCollisionEvents(); + + getEntities()->getTree()->withWriteLock([&] { + PROFILE_RANGE(simulation_physics, "HandleChanges"); + PerformanceTimer perfTimer("handleChanges"); + + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); + _entitySimulation->handleChangedMotionStates(outgoingChanges); + avatarManager->handleChangedMotionStates(outgoingChanges); + + const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); + _entitySimulation->handleDeactivatedMotionStates(deactivations); + }); + + // handleCollisionEvents() AFTER handleChangedMotionStates() + { + PROFILE_RANGE(simulation_physics, "CollisionEvents"); + avatarManager->handleCollisionEvents(collisionEvents); + // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk + // deadlock.) + _entitySimulation->handleCollisionEvents(collisionEvents); + } + + { + PROFILE_RANGE(simulation_physics, "MyAvatar"); + myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); + } + + if (PerformanceTimer::isActive() && + Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && + Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsTiming)) { + _physicsEngine->harvestPerformanceStats(); + } + // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework + _physicsEngine->dumpStatsIfNecessary(); + } + auto t4 = std::chrono::high_resolution_clock::now(); + + // NOTE: the getEntities()->update() call below will wait for lock + // and will provide non-physical entity motion + getEntities()->update(true); // update the models... + + auto t5 = std::chrono::high_resolution_clock::now(); + + workload::Timings timings(6); + timings[0] = t1 - t0; // prePhysics entities + timings[1] = t2 - t1; // prePhysics avatars + timings[2] = t3 - t2; // stepPhysics + timings[3] = t4 - t3; // postPhysics + timings[4] = t5 - t4; // non-physical kinematics + timings[5] = workload::Timing_ns((int32_t)(NSECS_PER_SECOND * deltaTime)); // game loop duration + _gameWorkload.updateSimulationTimings(timings); + } + } + } else { + // update the rendering without any simulation + getEntities()->update(false); + } + // remove recently dead avatarEntities + SetOfEntities deadAvatarEntities; + _entitySimulation->takeDeadAvatarEntities(deadAvatarEntities); + avatarManager->removeDeadAvatarEntities(deadAvatarEntities); + } + + // AvatarManager update + { + { + PROFILE_RANGE(simulation, "OtherAvatars"); + PerformanceTimer perfTimer("otherAvatars"); + avatarManager->updateOtherAvatars(deltaTime); + } + + { + PROFILE_RANGE(simulation, "MyAvatar"); + PerformanceTimer perfTimer("MyAvatar"); + qApp->updateMyAvatarLookAtPosition(); + avatarManager->updateMyAvatar(deltaTime); + } + } + + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::update()"); + + updateLOD(deltaTime); + + if (!_loginDialogID.isNull()) { + _loginStateManager.update(getMyAvatar()->getDominantHand(), _loginDialogID); + updateLoginDialogPosition(); + } + + { + PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("overlays"); + _overlays.update(deltaTime); + } + + // Update _viewFrustum with latest camera and view frustum data... + // NOTE: we get this from the view frustum, to make it simpler, since the + // loadViewFrumstum() method will get the correct details from the camera + // We could optimize this to not actually load the viewFrustum, since we don't + // actually need to calculate the view frustum planes to send these details + // to the server. + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_viewFrustum); + + _conicalViews.clear(); + _conicalViews.push_back(_viewFrustum); + // TODO: Fix this by modeling the way the secondary camera works on how the main camera works + // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary + // camera should be. + updateSecondaryCameraViewFrustum(); + } + + quint64 now = usecTimestampNow(); + + // Update my voxel servers with my current voxel query... + { + PROFILE_RANGE_EX(app, "QueryOctree", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("queryOctree"); + QMutexLocker viewLocker(&_viewMutex); + + bool viewIsDifferentEnough = false; + if (_conicalViews.size() == _lastQueriedViews.size()) { + for (size_t i = 0; i < _conicalViews.size(); ++i) { + if (!_conicalViews[i].isVerySimilar(_lastQueriedViews[i])) { + viewIsDifferentEnough = true; + break; + } + } + } else { + viewIsDifferentEnough = true; + } + + + // if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it + static const std::chrono::seconds MIN_PERIOD_BETWEEN_QUERIES { 3 }; + auto now = SteadyClock::now(); + if (now > _queryExpiry || viewIsDifferentEnough) { + if (DependencyManager::get()->shouldRenderEntities()) { + queryOctree(NodeType::EntityServer, PacketType::EntityQuery); + } + queryAvatars(); + + _lastQueriedViews = _conicalViews; + _queryExpiry = now + MIN_PERIOD_BETWEEN_QUERIES; + } + } + + // sent nack packets containing missing sequence numbers of received packets from nodes + { + quint64 sinceLastNack = now - _lastNackTime; + const quint64 TOO_LONG_SINCE_LAST_NACK = 1 * USECS_PER_SECOND; + if (sinceLastNack > TOO_LONG_SINCE_LAST_NACK) { + _lastNackTime = now; + sendNackPackets(); + } + } + + // send packet containing downstream audio stats to the AudioMixer + { + quint64 sinceLastNack = now - _lastSendDownstreamAudioStats; + if (sinceLastNack > TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS && !isInterstitialMode()) { + _lastSendDownstreamAudioStats = now; + + QMetaObject::invokeMethod(DependencyManager::get().data(), "sendDownstreamAudioStatsPacket", Qt::QueuedConnection); + } + } + + { + PerformanceTimer perfTimer("avatarManager/postUpdate"); + avatarManager->postUpdate(deltaTime, getMain3DScene()); + } + + { + PROFILE_RANGE_EX(app, "PostUpdateLambdas", 0xffff0000, (uint64_t)0); + PerformanceTimer perfTimer("postUpdateLambdas"); + std::unique_lock guard(_postUpdateLambdasLock); + for (auto& iter : _postUpdateLambdas) { + iter.second(); + } + _postUpdateLambdas.clear(); + } + + + updateRenderArgs(deltaTime); + + { + PerformanceTimer perfTimer("AnimDebugDraw"); + AnimDebugDraw::getInstance().update(); + } + + + { // Game loop is done, mark the end of the frame for the scene transactions and the render loop to take over + PerformanceTimer perfTimer("enqueueFrame"); + getMain3DScene()->enqueueFrame(); + } + + // If the display plugin is inactive then the frames won't be processed so process them here. + if (!getActiveDisplayPlugin()->isActive()) { + getMain3DScene()->processTransactionQueue(); + } +} + +void Application::updateRenderArgs(float deltaTime) { + _graphicsEngine.editRenderArgs([this, deltaTime](AppRenderArgs& appRenderArgs) { + PerformanceTimer perfTimer("editRenderArgs"); + appRenderArgs._headPose = getHMDSensorPose(); + + auto myAvatar = getMyAvatar(); + + // update the avatar with a fresh HMD pose + { + PROFILE_RANGE(render, "/updateAvatar"); + myAvatar->updateFromHMDSensorMatrix(appRenderArgs._headPose); + } + + auto lodManager = DependencyManager::get(); + + float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); + appRenderArgs._sensorToWorldScale = sensorToWorldScale; + appRenderArgs._sensorToWorld = getMyAvatar()->getSensorToWorldMatrix(); + { + PROFILE_RANGE(render, "/buildFrustrumAndArgs"); + { + QMutexLocker viewLocker(&_viewMutex); + // adjust near clip plane to account for sensor scaling. + auto adjustedProjection = glm::perspective(glm::radians(_fieldOfView.get()), + getActiveDisplayPlugin()->getRecommendedAspectRatio(), + DEFAULT_NEAR_CLIP * sensorToWorldScale, + DEFAULT_FAR_CLIP); + _viewFrustum.setProjection(adjustedProjection); + _viewFrustum.calculate(); + } + appRenderArgs._renderArgs = RenderArgs(_graphicsEngine.getGPUContext(), lodManager->getOctreeSizeScale(), + lodManager->getBoundaryLevelAdjust(), lodManager->getLODAngleHalfTan(), RenderArgs::DEFAULT_RENDER_MODE, + RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); + appRenderArgs._renderArgs._scene = getMain3DScene(); + + { + QMutexLocker viewLocker(&_viewMutex); + appRenderArgs._renderArgs.setViewFrustum(_viewFrustum); + } + } + { + PROFILE_RANGE(render, "/resizeGL"); + bool showWarnings = false; + bool suppressShortTimings = false; + auto menu = Menu::getInstance(); + if (menu) { + suppressShortTimings = menu->isOptionChecked(MenuOption::SuppressShortTimings); + showWarnings = menu->isOptionChecked(MenuOption::PipelineWarnings); + } + PerformanceWarning::setSuppressShortTimings(suppressShortTimings); + PerformanceWarning warn(showWarnings, "Application::paintGL()"); + resizeGL(); + } + + this->updateCamera(appRenderArgs._renderArgs, deltaTime); + appRenderArgs._eyeToWorld = _myCamera.getTransform(); + appRenderArgs._isStereo = false; + + { + auto hmdInterface = DependencyManager::get(); + float ipdScale = hmdInterface->getIPDScale(); + + // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. + ipdScale *= sensorToWorldScale; + + auto baseProjection = appRenderArgs._renderArgs.getViewFrustum().getProjection(); + + if (getActiveDisplayPlugin()->isStereo()) { + // Stereo modes will typically have a larger projection matrix overall, + // so we ask for the 'mono' projection matrix, which for stereo and HMD + // plugins will imply the combined projection for both eyes. + // + // This is properly implemented for the Oculus plugins, but for OpenVR + // and Stereo displays I'm not sure how to get / calculate it, so we're + // just relying on the left FOV in each case and hoping that the + // overall culling margin of error doesn't cause popping in the + // right eye. There are FIXMEs in the relevant plugins + _myCamera.setProjection(getActiveDisplayPlugin()->getCullingProjection(baseProjection)); + appRenderArgs._isStereo = true; + + auto& eyeOffsets = appRenderArgs._eyeOffsets; + auto& eyeProjections = appRenderArgs._eyeProjections; + + // FIXME we probably don't need to set the projection matrix every frame, + // only when the display plugin changes (or in non-HMD modes when the user + // changes the FOV manually, which right now I don't think they can. + for_each_eye([&](Eye eye) { + // For providing the stereo eye views, the HMD head pose has already been + // applied to the avatar, so we need to get the difference between the head + // pose applied to the avatar and the per eye pose, and use THAT as + // the per-eye stereo matrix adjustment. + mat4 eyeToHead = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); + // Grab the translation + vec3 eyeOffset = glm::vec3(eyeToHead[3]); + // Apply IPD scaling + mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); + eyeOffsets[eye] = eyeOffsetTransform; + eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); + }); + + // Configure the type of display / stereo + appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); + } + } + + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_displayViewFrustum); + appRenderArgs._view = glm::inverse(_displayViewFrustum.getView()); + } + + { + QMutexLocker viewLocker(&_viewMutex); + appRenderArgs._renderArgs.setViewFrustum(_displayViewFrustum); + } + + + // HACK + // load the view frustum + // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. + // Then we can move this logic into the Avatar::simulate call. + myAvatar->preDisplaySide(&appRenderArgs._renderArgs); + }); +} + +void Application::queryAvatars() { + if (!isInterstitialMode()) { + auto avatarPacket = NLPacket::create(PacketType::AvatarQuery); + auto destinationBuffer = reinterpret_cast(avatarPacket->getPayload()); + unsigned char* bufferStart = destinationBuffer; + + uint8_t numFrustums = (uint8_t)_conicalViews.size(); + memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums)); + destinationBuffer += sizeof(numFrustums); + + for (const auto& view : _conicalViews) { + destinationBuffer += view.serialize(destinationBuffer); + } + + avatarPacket->setPayloadSize(destinationBuffer - bufferStart); + + DependencyManager::get()->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); + } +} + + +int Application::sendNackPackets() { + + // iterates through all nodes in NodeList + auto nodeList = DependencyManager::get(); + + int packetsSent = 0; + + nodeList->eachNode([&](const SharedNodePointer& node){ + + if (node->getActiveSocket() && node->getType() == NodeType::EntityServer) { + + auto nackPacketList = NLPacketList::create(PacketType::OctreeDataNack); + + QUuid nodeUUID = node->getUUID(); + + // if there are octree packets from this node that are waiting to be processed, + // don't send a NACK since the missing packets may be among those waiting packets. + if (_octreeProcessor.hasPacketsToProcessFrom(nodeUUID)) { + return; + } + + QSet missingSequenceNumbers; + _octreeServerSceneStats.withReadLock([&] { + // retrieve octree scene stats of this node + if (_octreeServerSceneStats.find(nodeUUID) == _octreeServerSceneStats.end()) { + return; + } + // get sequence number stats of node, prune its missing set, and make a copy of the missing set + SequenceNumberStats& sequenceNumberStats = _octreeServerSceneStats[nodeUUID].getIncomingOctreeSequenceNumberStats(); + sequenceNumberStats.pruneMissingSet(); + missingSequenceNumbers = sequenceNumberStats.getMissingSet(); + }); + + _isMissingSequenceNumbers = (missingSequenceNumbers.size() != 0); + + // construct nack packet(s) for this node + foreach(const OCTREE_PACKET_SEQUENCE& missingNumber, missingSequenceNumbers) { + nackPacketList->writePrimitive(missingNumber); + } + + if (nackPacketList->getNumPackets()) { + packetsSent += (int)nackPacketList->getNumPackets(); + + // send the packet list + nodeList->sendPacketList(std::move(nackPacketList), *node); + } + } + }); + + return packetsSent; +} + +void Application::queryOctree(NodeType_t serverType, PacketType packetType) { + + if (!_settingsLoaded) { + return; // bail early if settings are not loaded + } + + const bool isModifiedQuery = !_physicsEnabled; + if (isModifiedQuery) { + // Create modified view that is a simple sphere. + bool interstitialModeEnabled = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + + ConicalViewFrustum sphericalView; + sphericalView.setSimpleRadius(INITIAL_QUERY_RADIUS); + + if (interstitialModeEnabled) { + ConicalViewFrustum farView; + farView.set(_viewFrustum); + _octreeQuery.setConicalViews({ sphericalView, farView }); + } else { + _octreeQuery.setConicalViews({ sphericalView }); + } + + _octreeQuery.setOctreeSizeScale(DEFAULT_OCTREE_SIZE_SCALE); + static constexpr float MIN_LOD_ADJUST = -20.0f; + _octreeQuery.setBoundaryLevelAdjust(MIN_LOD_ADJUST); + } else { + _octreeQuery.setConicalViews(_conicalViews); + auto lodManager = DependencyManager::get(); + _octreeQuery.setOctreeSizeScale(lodManager->getOctreeSizeScale()); + _octreeQuery.setBoundaryLevelAdjust(lodManager->getBoundaryLevelAdjust()); + } + _octreeQuery.setReportInitialCompletion(isModifiedQuery); + + + auto nodeList = DependencyManager::get(); + + auto node = nodeList->soloNodeOfType(serverType); + if (node && node->getActiveSocket()) { + _octreeQuery.setMaxQueryPacketsPerSecond(getMaxOctreePacketsPerSecond()); + + auto queryPacket = NLPacket::create(packetType); + + // encode the query data + auto packetData = reinterpret_cast(queryPacket->getPayload()); + int packetSize = _octreeQuery.getBroadcastData(packetData); + queryPacket->setPayloadSize(packetSize); + + // make sure we still have an active socket + nodeList->sendUnreliablePacket(*queryPacket, *node); + } +} + + +bool Application::isHMDMode() const { + return getActiveDisplayPlugin()->isHmd(); +} + +float Application::getNumCollisionObjects() const { + return _physicsEngine ? _physicsEngine->getNumCollisionObjects() : 0; +} + +float Application::getTargetRenderFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } + +QRect Application::getDesirableApplicationGeometry() const { + QRect applicationGeometry = getWindow()->geometry(); + + // If our parent window is on the HMD, then don't use its geometry, instead use + // the "main screen" geometry. + HMDToolsDialog* hmdTools = DependencyManager::get()->getHMDToolsDialog(); + if (hmdTools && hmdTools->hasHMDScreen()) { + QScreen* hmdScreen = hmdTools->getHMDScreen(); + QWindow* appWindow = getWindow()->windowHandle(); + QScreen* appScreen = appWindow->screen(); + + // if our app's screen is the hmd screen, we don't want to place the + // running scripts widget on it. So we need to pick a better screen. + // we will use the screen for the HMDTools since it's a guaranteed + // better screen. + if (appScreen == hmdScreen) { + QScreen* betterScreen = hmdTools->windowHandle()->screen(); + applicationGeometry = betterScreen->geometry(); + } + } + return applicationGeometry; +} + +PickRay Application::computePickRay(float x, float y) const { + vec2 pickPoint { x, y }; + PickRay result; + if (isHMDMode()) { + getApplicationCompositor().computeHmdPickRay(pickPoint, result.origin, result.direction); + } else { + pickPoint /= getCanvasSize(); + if (_myCamera.getMode() == CameraMode::CAMERA_MODE_MIRROR) { + pickPoint.x = 1.0f - pickPoint.x; + } + QMutexLocker viewLocker(&_viewMutex); + _viewFrustum.computePickRay(pickPoint.x, pickPoint.y, result.origin, result.direction); + } + return result; +} + +std::shared_ptr Application::getMyAvatar() const { + return DependencyManager::get()->getMyAvatar(); +} + +glm::vec3 Application::getAvatarPosition() const { + return getMyAvatar()->getWorldPosition(); +} + +void Application::copyViewFrustum(ViewFrustum& viewOut) const { + QMutexLocker viewLocker(&_viewMutex); + viewOut = _viewFrustum; +} + +void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const { + QMutexLocker viewLocker(&_viewMutex); + viewOut = _displayViewFrustum; +} + +// resentSensors() is a bit of vestigial feature. It used to be used for Oculus DK2 to recenter the view around +// the current head orientation. With the introduction of "room scale" tracking we no longer need that particular +// feature. However, we still use this to reset face trackers, eye trackers, audio and to optionally re-load the avatar +// rig and animations from scratch. +void Application::resetSensors(bool andReload) { + DependencyManager::get()->reset(); + DependencyManager::get()->reset(); + _overlayConductor.centerUI(); + getActiveDisplayPlugin()->resetSensors(); + getMyAvatar()->reset(true, andReload); + QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); +} + +void Application::hmdVisibleChanged(bool visible) { + // TODO + // calling start and stop will change audio input and ouput to default audio devices. + // we need to add a pause/unpause functionality to AudioClient for this to work properly +#if 0 + if (visible) { + QMetaObject::invokeMethod(DependencyManager::get().data(), "start", Qt::QueuedConnection); + } else { + QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::QueuedConnection); + } +#endif +} + +void Application::updateWindowTitle() const { + + auto nodeList = DependencyManager::get(); + auto accountManager = DependencyManager::get(); + auto isInErrorState = nodeList->getDomainHandler().isInErrorState(); + + QString buildVersion = " - " + + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) + + " " + applicationVersion(); + + QString loginStatus = accountManager->isLoggedIn() ? "" : " (NOT LOGGED IN)"; + + QString connectionStatus = isInErrorState ? " (ERROR CONNECTING)" : + nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED)"; + QString username = accountManager->getAccountInfo().getUsername(); + + setCrashAnnotation("username", username.toStdString()); + + QString currentPlaceName; + if (isServerlessMode()) { + if (isInErrorState) { + currentPlaceName = "serverless: " + nodeList->getDomainHandler().getErrorDomainURL().toString(); + } else { + currentPlaceName = "serverless: " + DependencyManager::get()->getDomainURL().toString(); + } + } else { + currentPlaceName = DependencyManager::get()->getDomainURL().host(); + if (currentPlaceName.isEmpty()) { + currentPlaceName = nodeList->getDomainHandler().getHostname(); + } + } + + QString title = QString() + (!username.isEmpty() ? username + " @ " : QString()) + + currentPlaceName + connectionStatus + loginStatus + buildVersion; + +#ifndef WIN32 + // crashes with vs2013/win32 + qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str()); +#endif + _window->setWindowTitle(title); + + // updateTitleWindow gets called whenever there's a change regarding the domain, so rather + // than placing this within domainURLChanged, it's placed here to cover the other potential cases. + DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", ""); +} + +void Application::clearDomainOctreeDetails(bool clearAll) { + // before we delete all entities get MyAvatar's AvatarEntityData ready + getMyAvatar()->prepareAvatarEntityDataForReload(); + + // if we're about to quit, we really don't need to do the rest of these things... + if (_aboutToQuit) { + return; + } + + qCDebug(interfaceapp) << "Clearing domain octree details..."; + + resetPhysicsReadyInformation(); + setIsInterstitialMode(true); + + _octreeServerSceneStats.withWriteLock([&] { + _octreeServerSceneStats.clear(); + }); + + // reset the model renderer + clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities(); + + auto skyStage = DependencyManager::get()->getSkyStage(); + + skyStage->setBackgroundMode(graphics::SunSkyStage::SKY_DEFAULT); + + DependencyManager::get()->clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + MaterialCache::instance().clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + ShaderCache::instance().clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); +} + +void Application::domainURLChanged(QUrl domainURL) { + // disable physics until we have enough information about our new location to not cause craziness. + resetPhysicsReadyInformation(); + setIsServerlessMode(domainURL.scheme() != URL_SCHEME_HIFI); + if (isServerlessMode()) { + loadServerlessDomain(domainURL); + } + updateWindowTitle(); +} + +void Application::goToErrorDomainURL(QUrl errorDomainURL) { + // disable physics until we have enough information about our new location to not cause craziness. + resetPhysicsReadyInformation(); + setIsServerlessMode(errorDomainURL.scheme() != URL_SCHEME_HIFI); + if (isServerlessMode()) { + loadErrorDomain(errorDomainURL); + } + updateWindowTitle(); +} + +void Application::resettingDomain() { + _notifiedPacketVersionMismatchThisDomain = false; + + clearDomainOctreeDetails(false); +} + +void Application::nodeAdded(SharedNodePointer node) const { + if (node->getType() == NodeType::EntityServer) { + if (!_failedToConnectToEntityServer) { + _entityServerConnectionTimer.stop(); + _entityServerConnectionTimer.setInterval(ENTITY_SERVER_CONNECTION_TIMEOUT); + _entityServerConnectionTimer.start(); + } + } +} + +void Application::nodeActivated(SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + // asset server just connected - check if we have the asset browser showing + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + + if (offscreenUi) { + auto nodeList = DependencyManager::get(); + + if (nodeList->getThisNodeCanWriteAssets()) { + // call reload on the shown asset browser dialog to get the mappings (if permissions allow) + auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; + if (assetDialog) { + QMetaObject::invokeMethod(assetDialog, "reload"); + } + } else { + // we switched to an Asset Server that we can't modify, hide the Asset Browser + offscreenUi->hide("AssetServer"); + } + } +#endif + } + + // If we get a new EntityServer activated, reset lastQueried time + // so we will do a proper query during update + if (node->getType() == NodeType::EntityServer) { + _queryExpiry = SteadyClock::now(); + _octreeQuery.incrementConnectionID(); + + if (!_failedToConnectToEntityServer) { + _entityServerConnectionTimer.stop(); + } + } + + if (node->getType() == NodeType::AudioMixer && !isInterstitialMode()) { + DependencyManager::get()->negotiateAudioFormat(); + } + + if (node->getType() == NodeType::AvatarMixer) { + _queryExpiry = SteadyClock::now(); + + // new avatar mixer, send off our identity packet on next update loop + // Reset skeletonModelUrl if the last server modified our choice. + // Override the avatar url (but not model name) here too. + if (_avatarOverrideUrl.isValid()) { + getMyAvatar()->useFullAvatarURL(_avatarOverrideUrl); + } + + if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->getSkeletonModelURL()) { + getMyAvatar()->resetFullAvatarURL(); + } + getMyAvatar()->markIdentityDataChanged(); + getMyAvatar()->resetLastSent(); + + if (!isInterstitialMode()) { + // transmit a "sendAll" packet to the AvatarMixer we just connected to. + getMyAvatar()->sendAvatarDataPacket(true); + } + } +} + +void Application::nodeKilled(SharedNodePointer node) { + // These are here because connecting NodeList::nodeKilled to OctreePacketProcessor::nodeKilled doesn't work: + // OctreePacketProcessor::nodeKilled is not being called when NodeList::nodeKilled is emitted. + // This may have to do with GenericThread::threadRoutine() blocking the QThread event loop + + _octreeProcessor.nodeKilled(node); + + _entityEditSender.nodeKilled(node); + + if (node->getType() == NodeType::AudioMixer) { + QMetaObject::invokeMethod(DependencyManager::get().data(), "audioMixerKilled"); + } else if (node->getType() == NodeType::EntityServer) { + // we lost an entity server, clear all of the domain octree details + clearDomainOctreeDetails(false); + } else if (node->getType() == NodeType::AssetServer) { + // asset server going away - check if we have the asset browser showing + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; + + if (assetDialog) { + // call reload on the shown asset browser dialog + QMetaObject::invokeMethod(assetDialog, "clear"); + } +#endif + } +} + +void Application::trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket) { + // Attempt to identify the sender from its address. + if (sendingNode) { + const QUuid& nodeUUID = sendingNode->getUUID(); + + // now that we know the node ID, let's add these stats to the stats for that node... + _octreeServerSceneStats.withWriteLock([&] { + if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { + OctreeSceneStats& stats = _octreeServerSceneStats[nodeUUID]; + stats.trackIncomingOctreePacket(message, wasStatsPacket, sendingNode->getClockSkewUsec()); + } + }); + } +} + +bool Application::gpuTextureMemSizeStable() { + auto renderConfig = qApp->getRenderEngine()->getConfiguration(); + auto renderStats = renderConfig->getConfig("Stats"); + + qint64 textureResourceGPUMemSize = renderStats->textureResourceGPUMemSize; + qint64 texturePopulatedGPUMemSize = renderStats->textureResourcePopulatedGPUMemSize; + qint64 textureTransferSize = renderStats->texturePendingGPUTransferSize; + + if (_gpuTextureMemSizeAtLastCheck == textureResourceGPUMemSize) { + _gpuTextureMemSizeStabilityCount++; + } else { + _gpuTextureMemSizeStabilityCount = 0; + } + _gpuTextureMemSizeAtLastCheck = textureResourceGPUMemSize; + + if (_gpuTextureMemSizeStabilityCount >= _minimumGPUTextureMemSizeStabilityCount) { + return (textureResourceGPUMemSize == texturePopulatedGPUMemSize) && (textureTransferSize == 0); + } + return false; +} + +int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode) { + // parse the incoming stats datas stick it in a temporary object for now, while we + // determine which server it belongs to + int statsMessageLength = 0; + + const QUuid& nodeUUID = sendingNode->getUUID(); + + // now that we know the node ID, let's add these stats to the stats for that node... + _octreeServerSceneStats.withWriteLock([&] { + OctreeSceneStats& octreeStats = _octreeServerSceneStats[nodeUUID]; + statsMessageLength = octreeStats.unpackFromPacket(message); + + if (octreeStats.isFullScene()) { + _fullSceneReceivedCounter++; + } + }); + + return statsMessageLength; +} + +void Application::packetSent(quint64 length) { +} + +void Application::addingEntityWithCertificate(const QString& certificateID, const QString& placeName) { + auto ledger = DependencyManager::get(); + ledger->updateLocation(certificateID, placeName); +} + +void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointer scriptEngine) { + + scriptEngine->setEmitScriptUpdatesFunction([this]() { + SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); + return !entityServerNode || isPhysicsEnabled(); + }); + + // setup the packet sender of the script engine's scripting interfaces so + // we can use the same ones from the application. + auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->setPacketSender(&_entityEditSender); + entityScriptingInterface->setEntityTree(getEntities()->getTree()); + + if (property(hifi::properties::TEST).isValid()) { + scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance()); + } + + scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); + + // hook our avatar and avatar hash map object into this script engine + getMyAvatar()->registerMetaTypes(scriptEngine); + + scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Camera", &_myCamera); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get().data()); +#endif + + ClipboardScriptingInterface* clipboardScriptable = new ClipboardScriptingInterface(); + scriptEngine->registerGlobalObject("Clipboard", clipboardScriptable); + connect(scriptEngine.data(), &ScriptEngine::finished, clipboardScriptable, &ClipboardScriptingInterface::deleteLater); + + scriptEngine->registerGlobalObject("Overlays", &_overlays); + qScriptRegisterMetaType(scriptEngine.data(), RayToOverlayIntersectionResultToScriptValue, + RayToOverlayIntersectionResultFromScriptValue); + +#if !defined(DISABLE_QML) + scriptEngine->registerGlobalObject("OffscreenFlags", getOffscreenUI()->getFlags()); + scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); +#endif + + qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); + qScriptRegisterMetaType(scriptEngine.data(), + wrapperToScriptValue, wrapperFromScriptValue); + scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); + + qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); + qScriptRegisterMetaType(scriptEngine.data(), + wrapperToScriptValue, wrapperFromScriptValue); + scriptEngine->registerGlobalObject("Tablet", DependencyManager::get().data()); + // FIXME remove these deprecated names for the tablet scripting interface + scriptEngine->registerGlobalObject("tabletInterface", DependencyManager::get().data()); + + auto toolbarScriptingInterface = DependencyManager::get().data(); + DependencyManager::get().data()->setToolbarScriptingInterface(toolbarScriptingInterface); + + scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); + scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, + LocationScriptingInterface::locationSetter, "Window"); + // register `location` on the global object. + scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, + LocationScriptingInterface::locationSetter); + + bool clientScript = scriptEngine->isClientScript(); + scriptEngine->registerFunction("OverlayWindow", clientScript ? QmlWindowClass::constructor : QmlWindowClass::restricted_constructor); +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) + scriptEngine->registerFunction("OverlayWebWindow", clientScript ? QmlWebWindowClass::constructor : QmlWebWindowClass::restricted_constructor); +#endif + scriptEngine->registerFunction("QmlFragment", clientScript ? QmlFragmentClass::constructor : QmlFragmentClass::restricted_constructor); + + scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("DesktopPreviewProvider", DependencyManager::get().data()); +#if !defined(DISABLE_QML) + scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); +#endif + scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); + scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AvatarBookmarks", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("LocationBookmarks", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("RayPick", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("LaserPointers", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Picks", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Pointers", DependencyManager::get().data()); + + // Caches + scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("TextureCache", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("ModelCache", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("DialogsManager", _dialogsManagerScriptingInterface); + + scriptEngine->registerGlobalObject("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + scriptEngine->registerGlobalObject("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + scriptEngine->registerGlobalObject("AccountServices", AccountServicesScriptingInterface::getInstance()); + qScriptRegisterMetaType(scriptEngine.data(), DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue); + + scriptEngine->registerGlobalObject("FaceTracker", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("HMD", DependencyManager::get().data()); + scriptEngine->registerFunction("HMD", "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0); + scriptEngine->registerFunction("HMD", "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0); + + scriptEngine->registerGlobalObject("Scene", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); + scriptEngine->registerGlobalObject("Workload", _gameWorkload._engine->getConfiguration().get()); + + GraphicsScriptingInterface::registerMetaTypes(scriptEngine.data()); + scriptEngine->registerGlobalObject("Graphics", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); + + scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("GooglePoly", DependencyManager::get().data()); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptEngine.data(), steamClient.get())); + } + auto scriptingInterface = DependencyManager::get(); + scriptEngine->registerGlobalObject("Controller", scriptingInterface.data()); + UserInputMapper::registerControllerTypes(scriptEngine.data()); + + auto recordingInterface = DependencyManager::get(); + scriptEngine->registerGlobalObject("Recording", recordingInterface.data()); + + auto entityScriptServerLog = DependencyManager::get(); + scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); + scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); + scriptEngine->registerGlobalObject("Selection", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("WalletScriptingInterface", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); + scriptEngine->registerGlobalObject("ResourceRequestObserver", DependencyManager::get().data()); + + registerInteractiveWindowMetaType(scriptEngine.data()); + + auto pickScriptingInterface = DependencyManager::get(); + pickScriptingInterface->registerMetaTypes(scriptEngine.data()); + + // connect this script engines printedMessage signal to the global ScriptEngines these various messages + auto scriptEngines = DependencyManager::get().data(); + connect(scriptEngine.data(), &ScriptEngine::printedMessage, scriptEngines, &ScriptEngines::onPrintedMessage); + connect(scriptEngine.data(), &ScriptEngine::errorMessage, scriptEngines, &ScriptEngines::onErrorMessage); + connect(scriptEngine.data(), &ScriptEngine::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage); + connect(scriptEngine.data(), &ScriptEngine::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage); + connect(scriptEngine.data(), &ScriptEngine::clearDebugWindow, scriptEngines, &ScriptEngines::onClearDebugWindow); + +} + +bool Application::canAcceptURL(const QString& urlString) const { + QUrl url(urlString); + if (url.query().contains(WEB_VIEW_TAG)) { + return false; + } else if (urlString.startsWith(URL_SCHEME_HIFI)) { + return true; + } + QString lowerPath = url.path().toLower(); + for (auto& pair : _acceptedExtensions) { + if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { + return true; + } + } + return false; +} + +bool Application::acceptURL(const QString& urlString, bool defaultUpload) { + QUrl url(urlString); + + if (url.scheme() == URL_SCHEME_HIFI) { + // this is a hifi URL - have the AddressManager handle it + QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", + Qt::AutoConnection, Q_ARG(const QString&, urlString)); + return true; + } + + QString lowerPath = url.path().toLower(); + for (auto& pair : _acceptedExtensions) { + if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { + AcceptURLMethod method = pair.second; + return (this->*method)(urlString); + } + } + + if (defaultUpload && !url.fileName().isEmpty() && url.isLocalFile()) { + showAssetServerWidget(urlString); + } + return defaultUpload; +} + +void Application::setSessionUUID(const QUuid& sessionUUID) const { + Physics::setSessionUUID(sessionUUID); +} + +bool Application::askToSetAvatarUrl(const QString& url) { + QUrl realUrl(url); + if (realUrl.isLocalFile()) { + OffscreenUi::asyncWarning("", "You can not use local files for avatar components."); + return false; + } + + // Download the FST file, to attempt to determine its model type + QVariantHash fstMapping = FSTReader::downloadMapping(url); + + FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); + + QString modelName = fstMapping["name"].toString(); + QString modelLicense = fstMapping["license"].toString(); + + bool agreeToLicense = true; // assume true + //create set avatar callback + auto setAvatar = [=] (QString url, QString modelName) { + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Set Avatar", + "Would you like to use '" + modelName + "' for your avatar?", + QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + bool ok = (QMessageBox::Ok == static_cast(answer.toInt())); + if (ok) { + getMyAvatar()->useFullAvatarURL(url, modelName); + emit fullAvatarURLChanged(url, modelName); + } else { + qCDebug(interfaceapp) << "Declined to use the avatar"; + } + }); + }; + + if (!modelLicense.isEmpty()) { + // word wrap the license text to fit in a reasonable shaped message box. + const int MAX_CHARACTERS_PER_LINE = 90; + modelLicense = simpleWordWrap(modelLicense, MAX_CHARACTERS_PER_LINE); + + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Avatar Usage License", + modelLicense + "\nDo you agree to these terms?", + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + QObject::connect(dlg, &ModalDialogListener::response, this, [=, &agreeToLicense] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + agreeToLicense = (static_cast(answer.toInt()) == QMessageBox::Yes); + if (agreeToLicense) { + switch (modelType) { + case FSTReader::HEAD_AND_BODY_MODEL: { + setAvatar(url, modelName); + break; + } + default: + OffscreenUi::asyncWarning("", modelName + "Does not support a head and body as required."); + break; + } + } else { + qCDebug(interfaceapp) << "Declined to agree to avatar license"; + } + + //auto offscreenUi = getOffscreenUI(); + }); + } else { + setAvatar(url, modelName); + } + + return true; +} + + +bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { + QString shortName = scriptFilenameOrURL; + + QUrl scriptURL { scriptFilenameOrURL }; + + if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + int startIndex = shortName.lastIndexOf('/') + 1; + int endIndex = shortName.lastIndexOf('?'); + shortName = shortName.mid(startIndex, endIndex - startIndex); + } + +#ifdef DISABLE_QML + DependencyManager::get()->loadScript(scriptFilenameOrURL); +#else + QString message = "Would you like to run this script:\n" + shortName; + ModalDialogListener* dlg = OffscreenUi::asyncQuestion(getWindow(), "Run Script", message, + QMessageBox::Yes | QMessageBox::No); + + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + const QString& fileName = scriptFilenameOrURL; + if (static_cast(answer.toInt()) == QMessageBox::Yes) { + qCDebug(interfaceapp) << "Chose to run the script: " << fileName; + DependencyManager::get()->loadScript(fileName); + } else { + qCDebug(interfaceapp) << "Declined to run the script"; + } + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + }); +#endif + return true; +} + +bool Application::askToWearAvatarAttachmentUrl(const QString& url) { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest = QNetworkRequest(url); + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + QNetworkReply* reply = networkAccessManager.get(networkRequest); + int requestNumber = ++_avatarAttachmentRequest; + connect(reply, &QNetworkReply::finished, [this, reply, url, requestNumber]() { + + if (requestNumber != _avatarAttachmentRequest) { + // this request has been superseded by another more recent request + reply->deleteLater(); + return; + } + + QNetworkReply::NetworkError networkError = reply->error(); + if (networkError == QNetworkReply::NoError) { + // download success + QByteArray contents = reply->readAll(); + + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(contents, &jsonError); + if (jsonError.error == QJsonParseError::NoError) { + + auto jsonObject = doc.object(); + + // retrieve optional name field from JSON + QString name = tr("Unnamed Attachment"); + auto nameValue = jsonObject.value("name"); + if (nameValue.isString()) { + name = nameValue.toString(); + } + + auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); + auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); + ModalDialogListener* dlg = OffscreenUi::asyncQuestion(avatarAttachmentConfirmationTitle, + avatarAttachmentConfirmationMessage, + QMessageBox::Ok | QMessageBox::Cancel); + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + if (static_cast(answer.toInt()) == QMessageBox::Yes) { + // add attachment to avatar + auto myAvatar = getMyAvatar(); + assert(myAvatar); + auto attachmentDataVec = myAvatar->getAttachmentData(); + AttachmentData attachmentData; + attachmentData.fromJson(jsonObject); + attachmentDataVec.push_back(attachmentData); + myAvatar->setAttachmentData(attachmentDataVec); + } else { + qCDebug(interfaceapp) << "User declined to wear the avatar attachment"; + } + }); + } else { + // json parse error + auto avatarAttachmentParseErrorString = tr("Error parsing attachment JSON from url: \"%1\""); + displayAvatarAttachmentWarning(avatarAttachmentParseErrorString.arg(url)); + } + } else { + // download failure + auto avatarAttachmentDownloadErrorString = tr("Error downloading attachment JSON from url: \"%1\""); + displayAvatarAttachmentWarning(avatarAttachmentDownloadErrorString.arg(url)); + } + reply->deleteLater(); + }); + return true; +} + +void Application::replaceDomainContent(const QString& url) { + qCDebug(interfaceapp) << "Attempting to replace domain content"; + QByteArray urlData(url.toUtf8()); + auto limitedNodeList = DependencyManager::get(); + const auto& domainHandler = limitedNodeList->getDomainHandler(); + + auto octreeFilePacket = NLPacket::create(PacketType::DomainContentReplacementFromUrl, urlData.size(), true); + octreeFilePacket->write(urlData); + limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); + + auto addressManager = DependencyManager::get(); + addressManager->handleLookupString(DOMAIN_SPAWNING_POINT); + QString newHomeAddress = addressManager->getHost() + DOMAIN_SPAWNING_POINT; + qCDebug(interfaceapp) << "Setting new home bookmark to: " << newHomeAddress; + DependencyManager::get()->setHomeLocationToAddress(newHomeAddress); +} + +bool Application::askToReplaceDomainContent(const QString& url) { + QString methodDetails; + const int MAX_CHARACTERS_PER_LINE = 90; + if (DependencyManager::get()->getThisNodeCanReplaceContent()) { + QUrl originURL { url }; + if (originURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + // Create a confirmation dialog when this call is made + static const QString infoText = simpleWordWrap("Your domain's content will be replaced with a new content set. " + "If you want to save what you have now, create a backup before proceeding. For more information about backing up " + "and restoring content, visit the documentation page at: ", MAX_CHARACTERS_PER_LINE) + + "\nhttps://docs.highfidelity.com/create-and-explore/start-working-in-your-sandbox/restoring-sandbox-content"; + + ModalDialogListener* dig = OffscreenUi::asyncQuestion("Are you sure you want to replace this domain's content set?", + infoText, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + QObject::connect(dig, &ModalDialogListener::response, this, [=] (QVariant answer) { + QString details; + if (static_cast(answer.toInt()) == QMessageBox::Yes) { + // Given confirmation, send request to domain server to replace content + replaceDomainContent(url); + details = "SuccessfulRequestToReplaceContent"; + } else { + details = "UserDeclinedToReplaceContent"; + } + QJsonObject messageProperties = { + { "status", details }, + { "content_set_url", url } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + QObject::disconnect(dig, &ModalDialogListener::response, this, nullptr); + }); + } else { + methodDetails = "ContentSetDidNotOriginateFromMarketplace"; + QJsonObject messageProperties = { + { "status", methodDetails }, + { "content_set_url", url } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + } + } else { + methodDetails = "UserDoesNotHavePermissionToReplaceContent"; + static const QString warningMessage = simpleWordWrap("The domain owner must enable 'Replace Content' " + "permissions for you in this domain's server settings before you can continue.", MAX_CHARACTERS_PER_LINE); + OffscreenUi::asyncWarning("You do not have permissions to replace domain content", warningMessage, + QMessageBox::Ok, QMessageBox::Ok); + + QJsonObject messageProperties = { + { "status", methodDetails }, + { "content_set_url", url } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + } + return true; +} + +void Application::displayAvatarAttachmentWarning(const QString& message) const { + auto avatarAttachmentWarningTitle = tr("Avatar Attachment Failure"); + OffscreenUi::asyncWarning(avatarAttachmentWarningTitle, message); +} + +void Application::showDialog(const QUrl& widgetUrl, const QUrl& tabletUrl, const QString& name) const { + auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); + auto hmd = DependencyManager::get(); + bool onTablet = false; + + if (!tablet->getToolbarMode()) { + onTablet = tablet->pushOntoStack(tabletUrl); + if (onTablet) { + toggleTabletUI(true); + } + } else { +#if !defined(DISABLE_QML) + getOffscreenUI()->show(widgetUrl, name); +#endif + } +} + +void Application::showScriptLogs() { + QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); + DependencyManager::get()->loadScript(defaultScriptsLoc.toString()); +} + +void Application::showAssetServerWidget(QString filePath) { + if (!DependencyManager::get()->getThisNodeCanWriteAssets() || getLoginDialogPoppedUp()) { + return; + } + static const QUrl url { "hifi/AssetServer.qml" }; + + auto startUpload = [=](QQmlContext* context, QObject* newObject){ + if (!filePath.isEmpty()) { + emit uploadRequest(filePath); + } + }; + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + auto hmd = DependencyManager::get(); + if (tablet->getToolbarMode()) { + getOffscreenUI()->show(url, "AssetServer", startUpload); + } else { + if (!hmd->getShouldShowTablet() && !isHMDMode()) { + getOffscreenUI()->show(url, "AssetServer", startUpload); + } else { + static const QUrl url("hifi/dialogs/TabletAssetServer.qml"); + if (!tablet->isPathLoaded(url)) { + tablet->pushOntoStack(url); + } + } + } + + startUpload(nullptr, nullptr); +} + +void Application::addAssetToWorldFromURL(QString url) { + + QString filename; + if (url.contains("filename")) { + filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. + } + if (url.contains("poly.google.com/downloads")) { + filename = url.section('/', -1); + if (url.contains("noDownload")) { + filename.remove(".zip?noDownload=false"); + } else { + filename.remove(".zip"); + } + + } + + if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { + QString errorInfo = "You do not have permissions to write to the Asset Server."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filename, errorInfo); + return; + } + + addAssetToWorldInfo(filename, "Downloading model file " + filename + "."); + + auto request = DependencyManager::get()->createResourceRequest( + nullptr, QUrl(url), true, -1, "Application::addAssetToWorldFromURL"); + connect(request, &ResourceRequest::finished, this, &Application::addAssetToWorldFromURLRequestFinished); + request->send(); +} + +void Application::addAssetToWorldFromURLRequestFinished() { + auto request = qobject_cast(sender()); + auto url = request->getUrl().toString(); + auto result = request->getResult(); + + QString filename; + bool isBlocks = false; + + if (url.contains("filename")) { + filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. + } + if (url.contains("poly.google.com/downloads")) { + filename = url.section('/', -1); + if (url.contains("noDownload")) { + filename.remove(".zip?noDownload=false"); + } else { + filename.remove(".zip"); + } + isBlocks = true; + } + + if (result == ResourceRequest::Success) { + QTemporaryDir temporaryDir; + temporaryDir.setAutoRemove(false); + if (temporaryDir.isValid()) { + QString temporaryDirPath = temporaryDir.path(); + QString downloadPath = temporaryDirPath + "/" + filename; + + QFile tempFile(downloadPath); + if (tempFile.open(QIODevice::WriteOnly)) { + tempFile.write(request->getData()); + addAssetToWorldInfoClear(filename); // Remove message from list; next one added will have a different key. + tempFile.close(); + qApp->getFileDownloadInterface()->runUnzip(downloadPath, url, true, false, isBlocks); + } else { + QString errorInfo = "Couldn't open temporary file for download"; + qWarning(interfaceapp) << errorInfo; + addAssetToWorldError(filename, errorInfo); + } + } else { + QString errorInfo = "Couldn't create temporary directory for download"; + qWarning(interfaceapp) << errorInfo; + addAssetToWorldError(filename, errorInfo); + } + } else { + qWarning(interfaceapp) << "Error downloading" << url << ":" << request->getResultString(); + addAssetToWorldError(filename, "Error downloading " + filename + " : " + request->getResultString()); + } + + request->deleteLater(); +} + + +QString filenameFromPath(QString filePath) { + return filePath.right(filePath.length() - filePath.lastIndexOf("/") - 1); +} + +void Application::addAssetToWorldUnzipFailure(QString filePath) { + QString filename = filenameFromPath(QUrl(filePath).toLocalFile()); + qWarning(interfaceapp) << "Couldn't unzip file" << filePath; + addAssetToWorldError(filename, "Couldn't unzip file " + filename + "."); +} + +void Application::addAssetToWorld(QString path, QString zipFile, bool isZip, bool isBlocks) { + // Automatically upload and add asset to world as an alternative manual process initiated by showAssetServerWidget(). + QString mapping; + QString filename = filenameFromPath(path); + if (isZip || isBlocks) { + QString assetName = zipFile.section("/", -1).remove(QRegExp("[.]zip(.*)$")); + QString assetFolder = path.section("model_repo/", -1); + mapping = "/" + assetName + "/" + assetFolder; + } else { + mapping = "/" + filename; + } + + // Test repeated because possibly different code paths. + if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { + QString errorInfo = "You do not have permissions to write to the Asset Server."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filename, errorInfo); + return; + } + + addAssetToWorldInfo(filename, "Adding " + mapping.mid(1) + " to the Asset Server."); + + addAssetToWorldWithNewMapping(path, mapping, 0, isZip, isBlocks); +} + +void Application::addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy, bool isZip, bool isBlocks) { + auto request = DependencyManager::get()->createGetMappingRequest(mapping); + + QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { + const int MAX_COPY_COUNT = 100; // Limit number of duplicate assets; recursion guard. + auto result = request->getError(); + if (result == GetMappingRequest::NotFound) { + addAssetToWorldUpload(filePath, mapping, isZip, isBlocks); + } else if (result != GetMappingRequest::NoError) { + QString errorInfo = "Could not map asset name: " + + mapping.left(mapping.length() - QString::number(copy).length() - 1); + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else if (copy < MAX_COPY_COUNT - 1) { + if (copy > 0) { + mapping = mapping.remove(mapping.lastIndexOf("-"), QString::number(copy).length() + 1); + } + copy++; + mapping = mapping.insert(mapping.lastIndexOf("."), "-" + QString::number(copy)); + addAssetToWorldWithNewMapping(filePath, mapping, copy, isZip, isBlocks); + } else { + QString errorInfo = "Too many copies of asset name: " + + mapping.left(mapping.length() - QString::number(copy).length() - 1); + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } + request->deleteLater(); + }); + + request->start(); +} + +void Application::addAssetToWorldUpload(QString filePath, QString mapping, bool isZip, bool isBlocks) { + qInfo(interfaceapp) << "Uploading" << filePath << "to Asset Server as" << mapping; + auto upload = DependencyManager::get()->createUpload(filePath); + QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { + if (upload->getError() != AssetUpload::NoError) { + QString errorInfo = "Could not upload model to the Asset Server."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else { + addAssetToWorldSetMapping(filePath, mapping, hash, isZip, isBlocks); + } + + // Remove temporary directory created by Clara.io market place download. + int index = filePath.lastIndexOf("/model_repo/"); + if (index > 0) { + QString tempDir = filePath.left(index); + qCDebug(interfaceapp) << "Removing temporary directory at: " + tempDir; + QDir(tempDir).removeRecursively(); + } + + upload->deleteLater(); + }); + + upload->start(); +} + +void Application::addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash, bool isZip, bool isBlocks) { + auto request = DependencyManager::get()->createSetMappingRequest(mapping, hash); + connect(request, &SetMappingRequest::finished, this, [=](SetMappingRequest* request) mutable { + if (request->getError() != SetMappingRequest::NoError) { + QString errorInfo = "Could not set asset mapping."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else { + // to prevent files that aren't models or texture files from being loaded into world automatically + if ((filePath.toLower().endsWith(OBJ_EXTENSION) || filePath.toLower().endsWith(FBX_EXTENSION)) || + ((filePath.toLower().endsWith(JPG_EXTENSION) || filePath.toLower().endsWith(PNG_EXTENSION)) && + ((!isBlocks) && (!isZip)))) { + addAssetToWorldAddEntity(filePath, mapping); + } else { + qCDebug(interfaceapp) << "Zipped contents are not supported entity files"; + addAssetToWorldInfoDone(filenameFromPath(filePath)); + } + } + request->deleteLater(); + }); + + request->start(); +} + +void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) { + EntityItemProperties properties; + properties.setName(mapping.right(mapping.length() - 1)); + if (filePath.toLower().endsWith(PNG_EXTENSION) || filePath.toLower().endsWith(JPG_EXTENSION)) { + properties.setType(EntityTypes::Image); + properties.setImageURL(QString("atp:" + mapping)); + properties.setKeepAspectRatio(false); + } else { + properties.setType(EntityTypes::Model); + properties.setModelURL("atp:" + mapping); + properties.setShapeType(SHAPE_TYPE_SIMPLE_COMPOUND); + } + properties.setCollisionless(true); // Temporarily set so that doesn't collide with avatar. + properties.setVisible(false); // Temporarily set so that don't see at large unresized dimensions. + bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); + properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); + glm::vec3 positionOffset = getMyAvatar()->getWorldOrientation() * (getMyAvatar()->getSensorToWorldScale() * glm::vec3(0.0f, 0.0f, -2.0f)); + properties.setPosition(getMyAvatar()->getWorldPosition() + positionOffset); + properties.setRotation(getMyAvatar()->getWorldOrientation()); + properties.setGravity(glm::vec3(0.0f, 0.0f, 0.0f)); + auto entityID = DependencyManager::get()->addEntity(properties); + + // Note: Model dimensions are not available here; model is scaled per FBX mesh in RenderableModelEntityItem::update() later + // on. But FBX dimensions may be in cm, so we monitor for the dimension change and rescale again if warranted. + + if (entityID == QUuid()) { + QString errorInfo = "Could not add model " + mapping + " to world."; + qWarning(interfaceapp) << "Could not add model to world: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else { + // Monitor when asset is rendered in world so that can resize if necessary. + _addAssetToWorldResizeList.insert(entityID, 0); // List value is count of checks performed. + if (!_addAssetToWorldResizeTimer.isActive()) { + _addAssetToWorldResizeTimer.start(); + } + + // Close progress message box. + addAssetToWorldInfoDone(filenameFromPath(filePath)); + } +} + +void Application::addAssetToWorldCheckModelSize() { + if (_addAssetToWorldResizeList.size() == 0) { + return; + } + + auto item = _addAssetToWorldResizeList.begin(); + while (item != _addAssetToWorldResizeList.end()) { + auto entityID = item.key(); + + EntityPropertyFlags propertyFlags; + propertyFlags += PROP_NAME; + propertyFlags += PROP_DIMENSIONS; + auto entityScriptingInterface = DependencyManager::get(); + auto properties = entityScriptingInterface->getEntityProperties(entityID, propertyFlags); + auto name = properties.getName(); + auto dimensions = properties.getDimensions(); + + bool doResize = false; + + const glm::vec3 DEFAULT_DIMENSIONS = glm::vec3(0.1f, 0.1f, 0.1f); + if (dimensions != DEFAULT_DIMENSIONS) { + + // Scale model so that its maximum is exactly specific size. + const float MAXIMUM_DIMENSION = getMyAvatar()->getSensorToWorldScale(); + auto previousDimensions = dimensions; + auto scale = std::min(MAXIMUM_DIMENSION / dimensions.x, std::min(MAXIMUM_DIMENSION / dimensions.y, + MAXIMUM_DIMENSION / dimensions.z)); + dimensions *= scale; + qInfo(interfaceapp) << "Model" << name << "auto-resized from" << previousDimensions << " to " << dimensions; + doResize = true; + + item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. + } else { + // Increment count of checks done. + _addAssetToWorldResizeList[entityID]++; + + const int CHECK_MODEL_SIZE_MAX_CHECKS = 300; + if (_addAssetToWorldResizeList[entityID] > CHECK_MODEL_SIZE_MAX_CHECKS) { + // Have done enough checks; model was either the default size or something's gone wrong. + + // Rescale all dimensions. + const glm::vec3 UNIT_DIMENSIONS = glm::vec3(1.0f, 1.0f, 1.0f); + dimensions = UNIT_DIMENSIONS; + qInfo(interfaceapp) << "Model" << name << "auto-resize timed out; resized to " << dimensions; + doResize = true; + + item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. + } else { + // No action on this entity; advance to next. + ++item; + } + } + + if (doResize) { + EntityItemProperties properties; + properties.setDimensions(dimensions); + properties.setVisible(true); + if (!name.toLower().endsWith(PNG_EXTENSION) && !name.toLower().endsWith(JPG_EXTENSION)) { + properties.setCollisionless(false); + } + bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); + properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); + properties.setLastEdited(usecTimestampNow()); + entityScriptingInterface->editEntity(entityID, properties); + } + } + + // Stop timer if nothing in list to check. + if (_addAssetToWorldResizeList.size() == 0) { + _addAssetToWorldResizeTimer.stop(); + } +} + + +void Application::addAssetToWorldInfo(QString modelName, QString infoText) { + // Displays the most recent info message, subject to being overridden by error messages. + + if (_aboutToQuit) { + return; + } + + /* + Cancel info timer if running. + If list has an entry for modelName, delete it (just one). + Append modelName, infoText to list. + Display infoText in message box unless an error is being displayed (i.e., error timer is running). + Show message box if not already visible. + */ + + _addAssetToWorldInfoTimer.stop(); + + addAssetToWorldInfoClear(modelName); + + _addAssetToWorldInfoKeys.append(modelName); + _addAssetToWorldInfoMessages.append(infoText); + + if (!_addAssetToWorldErrorTimer.isActive()) { + if (!_addAssetToWorldMessageBox) { + _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, + "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); + connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); + } + + _addAssetToWorldMessageBox->setProperty("text", "\n" + infoText); + _addAssetToWorldMessageBox->setVisible(true); + } +} + +void Application::addAssetToWorldInfoClear(QString modelName) { + // Clears modelName entry from message list without affecting message currently displayed. + + if (_aboutToQuit) { + return; + } + + /* + Delete entry for modelName from list. + */ + + auto index = _addAssetToWorldInfoKeys.indexOf(modelName); + if (index > -1) { + _addAssetToWorldInfoKeys.removeAt(index); + _addAssetToWorldInfoMessages.removeAt(index); + } +} + +void Application::addAssetToWorldInfoDone(QString modelName) { + // Continues to display this message if the latest for a few seconds, then deletes it and displays the next latest. + + if (_aboutToQuit) { + return; + } + + /* + Delete entry for modelName from list. + (Re)start the info timer to update message box. ... onAddAssetToWorldInfoTimeout() + */ + + addAssetToWorldInfoClear(modelName); + _addAssetToWorldInfoTimer.start(); +} + +void Application::addAssetToWorldInfoTimeout() { + if (_aboutToQuit) { + return; + } + + /* + If list not empty, display last message in list (may already be displayed ) unless an error is being displayed. + If list empty, close the message box unless an error is being displayed. + */ + + if (!_addAssetToWorldErrorTimer.isActive() && _addAssetToWorldMessageBox) { + if (_addAssetToWorldInfoKeys.length() > 0) { + _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); + } else { + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox->setVisible(false); + _addAssetToWorldMessageBox->deleteLater(); + _addAssetToWorldMessageBox = nullptr; + } + } +} + +void Application::addAssetToWorldError(QString modelName, QString errorText) { + // Displays the most recent error message for a few seconds. + + if (_aboutToQuit) { + return; + } + + /* + If list has an entry for modelName, delete it. + Display errorText in message box. + Show message box if not already visible. + (Re)start error timer. ... onAddAssetToWorldErrorTimeout() + */ + + addAssetToWorldInfoClear(modelName); + + if (!_addAssetToWorldMessageBox) { + _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, + "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); + connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); + } + + _addAssetToWorldMessageBox->setProperty("text", "\n" + errorText); + _addAssetToWorldMessageBox->setVisible(true); + + _addAssetToWorldErrorTimer.start(); +} + +void Application::addAssetToWorldErrorTimeout() { + if (_aboutToQuit) { + return; + } + + /* + If list is not empty, display message from last entry. + If list is empty, close the message box. + */ + + if (_addAssetToWorldMessageBox) { + if (_addAssetToWorldInfoKeys.length() > 0) { + _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); + } else { + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox->setVisible(false); + _addAssetToWorldMessageBox->deleteLater(); + _addAssetToWorldMessageBox = nullptr; + } + } +} + + +void Application::addAssetToWorldMessageClose() { + // Clear messages, e.g., if Interface is being closed or domain changes. + + /* + Call if user manually closes message box. + Call if domain changes. + Call if application is shutting down. + + Stop timers. + Close the message box if open. + Clear lists. + */ + + _addAssetToWorldInfoTimer.stop(); + _addAssetToWorldErrorTimer.stop(); + + if (_addAssetToWorldMessageBox) { + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox->setVisible(false); + _addAssetToWorldMessageBox->deleteLater(); + _addAssetToWorldMessageBox = nullptr; + } + + _addAssetToWorldInfoKeys.clear(); + _addAssetToWorldInfoMessages.clear(); +} + +void Application::onAssetToWorldMessageBoxClosed() { + if (_addAssetToWorldMessageBox) { + // User manually closed message box; perhaps because it has become stuck, so reset all messages. + qInfo(interfaceapp) << "User manually closed download status message box"; + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox = nullptr; + addAssetToWorldMessageClose(); + } +} + + +void Application::handleUnzip(QString zipFile, QStringList unzipFile, bool autoAdd, bool isZip, bool isBlocks) { + if (autoAdd) { + if (!unzipFile.isEmpty()) { + for (int i = 0; i < unzipFile.length(); i++) { + if (QFileInfo(unzipFile.at(i)).isFile()) { + qCDebug(interfaceapp) << "Preparing file for asset server: " << unzipFile.at(i); + addAssetToWorld(unzipFile.at(i), zipFile, isZip, isBlocks); + } + } + } else { + addAssetToWorldUnzipFailure(zipFile); + } + } else { + showAssetServerWidget(unzipFile.first()); + } +} + +void Application::packageModel() { + ModelPackager::package(); +} + +void Application::openUrl(const QUrl& url) const { + if (!url.isEmpty()) { + if (url.scheme() == URL_SCHEME_HIFI) { + DependencyManager::get()->handleLookupString(url.toString()); + } else if (url.scheme() == URL_SCHEME_HIFIAPP) { + DependencyManager::get()->openSystemApp(url.path()); + } else { + // address manager did not handle - ask QDesktopServices to handle + QDesktopServices::openUrl(url); + } + } +} + +void Application::loadDialog() { + ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"), + getPreviousScriptLocation(), + tr("JavaScript Files (*.js)")); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); + const QString& response = answer.toString(); + if (!response.isEmpty() && QFile(response).exists()) { + setPreviousScriptLocation(QFileInfo(response).absolutePath()); + DependencyManager::get()->loadScript(response, true, false, false, true); // Don't load from cache + } + }); +} + +QString Application::getPreviousScriptLocation() { + QString result = _previousScriptLocation.get(); + return result; +} + +void Application::setPreviousScriptLocation(const QString& location) { + _previousScriptLocation.set(location); +} + +void Application::loadScriptURLDialog() const { + ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); + const QString& newScript = response.toString(); + if (QUrl(newScript).scheme() == "atp") { + OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP"); + } else if (!newScript.isEmpty()) { + DependencyManager::get()->loadScript(newScript.trimmed()); + } + }); +} + +SharedSoundPointer Application::getSampleSound() const { + return _sampleSound; +} + +void Application::loadLODToolsDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->lodTools(); + } else { + tablet->pushOntoStack("hifi/dialogs/TabletLODTools.qml"); + } +} + +void Application::loadEntityStatisticsDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->octreeStatsDetails(); + } else { + tablet->pushOntoStack("hifi/dialogs/TabletEntityStatistics.qml"); + } +} + +void Application::loadDomainConnectionDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->showDomainConnectionDialog(); + } else { + tablet->pushOntoStack("hifi/dialogs/TabletDCDialog.qml"); + } +} + +void Application::toggleLogDialog() { +#ifndef ANDROID_APP_QUEST_INTERFACE + if (getLoginDialogPoppedUp()) { + return; + } + if (! _logDialog) { + + bool keepOnTop =_keepLogWindowOnTop.get(); +#ifdef Q_OS_WIN + _logDialog = new LogDialog(keepOnTop ? qApp->getWindow() : nullptr, getLogger()); +#elif !defined(Q_OS_ANDROID) + _logDialog = new LogDialog(nullptr, getLogger()); + + if (keepOnTop) { + Qt::WindowFlags flags = _logDialog->windowFlags() | Qt::Tool; + _logDialog->setWindowFlags(flags); + } +#endif + } + + if (_logDialog->isVisible()) { + _logDialog->hide(); + } else { + _logDialog->show(); + } +#endif +} + + void Application::recreateLogWindow(int keepOnTop) { + _keepLogWindowOnTop.set(keepOnTop != 0); + if (_logDialog) { + bool toggle = _logDialog->isVisible(); + _logDialog->close(); + _logDialog = nullptr; + + if (toggle) { + toggleLogDialog(); + } + } + } + +void Application::toggleEntityScriptServerLogDialog() { + if (! _entityScriptServerLogDialog) { + _entityScriptServerLogDialog = new EntityScriptServerLogDialog(nullptr); + } + + if (_entityScriptServerLogDialog->isVisible()) { + _entityScriptServerLogDialog->hide(); + } else { + _entityScriptServerLogDialog->show(); + } +} + +void Application::loadAddAvatarBookmarkDialog() const { + auto avatarBookmarks = DependencyManager::get(); +} + +void Application::loadAvatarBrowser() const { + auto tablet = dynamic_cast(DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + // construct the url to the marketplace item + QString url = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace?category=avatars"; + + QString MARKETPLACES_INJECT_SCRIPT_PATH = "file:///" + qApp->applicationDirPath() + "/scripts/system/html/js/marketplacesInject.js"; + tablet->gotoWebScreen(url, MARKETPLACES_INJECT_SCRIPT_PATH); + DependencyManager::get()->openTablet(); +} + +void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) { + postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] { + // Get a screenshot and save it + QString path = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, + TestScriptingInterface::getInstance()->getTestResultsLocation()); + + // If we're not doing an animated snapshot as well... + if (!includeAnimated) { + if (!path.isEmpty()) { + // Tell the dependency manager that the capture of the still snapshot has taken place. + emit DependencyManager::get()->stillSnapshotTaken(path, notify); + } + } else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) { + // Get an animated GIF snapshot and save it + SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); + } + }); +} + +void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { + postLambdaEvent([notify, filename, this] { + QString snapshotPath = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, + TestScriptingInterface::getInstance()->getTestResultsLocation()); + + emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, notify); + }); +} + +void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { + postLambdaEvent([notify, filename, cubemapOutputFormat, cameraPosition] { + DependencyManager::get()->save360Snapshot(cameraPosition, cubemapOutputFormat, notify, filename); + }); +} + +void Application::shareSnapshot(const QString& path, const QUrl& href) { + postLambdaEvent([path, href] { + // not much to do here, everything is done in snapshot code... + DependencyManager::get()->uploadSnapshot(path, href); + }); +} + +float Application::getRenderResolutionScale() const { + auto menu = Menu::getInstance(); + if (!menu) { + return 1.0f; + } + if (menu->isOptionChecked(MenuOption::RenderResolutionOne)) { + return 1.0f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionTwoThird)) { + return 0.666f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionHalf)) { + return 0.5f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionThird)) { + return 0.333f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionQuarter)) { + return 0.25f; + } else { + return 1.0f; + } +} + +void Application::notifyPacketVersionMismatch() { + if (!_notifiedPacketVersionMismatchThisDomain && !isInterstitialMode()) { + _notifiedPacketVersionMismatchThisDomain = true; + + QString message = "The location you are visiting is running an incompatible server version.\n"; + message += "Content may not display properly."; + + OffscreenUi::asyncWarning("", message); + } +} + +void Application::checkSkeleton() const { + if (getMyAvatar()->getSkeletonModel()->isActive() && !getMyAvatar()->getSkeletonModel()->hasSkeleton()) { + qCDebug(interfaceapp) << "MyAvatar model has no skeleton"; + + QString message = "Your selected avatar body has no skeleton.\n\nThe default body will be loaded..."; + OffscreenUi::asyncWarning("", message); + + getMyAvatar()->useFullAvatarURL(AvatarData::defaultFullAvatarModelUrl(), DEFAULT_FULL_AVATAR_MODEL_NAME); + } else { + _physicsEngine->setCharacterController(getMyAvatar()->getCharacterController()); + } +} + +void Application::activeChanged(Qt::ApplicationState state) { + switch (state) { + case Qt::ApplicationActive: + _isForeground = true; + break; + + case Qt::ApplicationSuspended: + case Qt::ApplicationHidden: + case Qt::ApplicationInactive: + default: + _isForeground = false; + break; + } +} + +void Application::windowMinimizedChanged(bool minimized) { + // initialize the _minimizedWindowTimer + static std::once_flag once; + std::call_once(once, [&] { + connect(&_minimizedWindowTimer, &QTimer::timeout, this, [] { + QCoreApplication::postEvent(QCoreApplication::instance(), new QEvent(static_cast(Idle)), Qt::HighEventPriority); + }); + }); + + // avoid rendering to the display plugin but continue posting Idle events, + // so that physics continues to simulate and the deadlock watchdog knows we're alive + if (!minimized && !getActiveDisplayPlugin()->isActive()) { + _minimizedWindowTimer.stop(); + getActiveDisplayPlugin()->activate(); + } else if (minimized && getActiveDisplayPlugin()->isActive()) { + getActiveDisplayPlugin()->deactivate(); + _minimizedWindowTimer.start(THROTTLED_SIM_FRAME_PERIOD_MS); + } +} + +void Application::postLambdaEvent(const std::function& f) { + if (this->thread() == QThread::currentThread()) { + f(); + } else { + QCoreApplication::postEvent(this, new LambdaEvent(f)); + } +} + +void Application::sendLambdaEvent(const std::function& f) { + if (this->thread() == QThread::currentThread()) { + f(); + } else { + LambdaEvent event(f); + QCoreApplication::sendEvent(this, &event); + } +} + +void Application::initPlugins(const QStringList& arguments) { + QCommandLineOption display("display", "Preferred displays", "displays"); + QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "displays"); + QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "inputs"); + + QCommandLineParser parser; + parser.addOption(display); + parser.addOption(disableDisplays); + parser.addOption(disableInputs); + parser.parse(arguments); + + if (parser.isSet(display)) { + auto preferredDisplays = parser.value(display).split(',', QString::SkipEmptyParts); + qInfo() << "Setting prefered display plugins:" << preferredDisplays; + PluginManager::getInstance()->setPreferredDisplayPlugins(preferredDisplays); + } + + if (parser.isSet(disableDisplays)) { + auto disabledDisplays = parser.value(disableDisplays).split(',', QString::SkipEmptyParts); + qInfo() << "Disabling following display plugins:" << disabledDisplays; + PluginManager::getInstance()->disableDisplays(disabledDisplays); + } + + if (parser.isSet(disableInputs)) { + auto disabledInputs = parser.value(disableInputs).split(',', QString::SkipEmptyParts); + qInfo() << "Disabling following input plugins:" << disabledInputs; + PluginManager::getInstance()->disableInputs(disabledInputs); + } +} + +void Application::shutdownPlugins() { +} + +glm::uvec2 Application::getCanvasSize() const { + return glm::uvec2(_glWidget->width(), _glWidget->height()); +} + +QRect Application::getRenderingGeometry() const { + auto geometry = _glWidget->geometry(); + auto topLeft = geometry.topLeft(); + auto topLeftScreen = _glWidget->mapToGlobal(topLeft); + geometry.moveTopLeft(topLeftScreen); + return geometry; +} + +glm::uvec2 Application::getUiSize() const { + static const uint MIN_SIZE = 1; + glm::uvec2 result(MIN_SIZE); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedUiSize(); + } + return result; +} + +QRect Application::getRecommendedHUDRect() const { + auto uiSize = getUiSize(); + QRect result(0, 0, uiSize.x, uiSize.y); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedHUDRect(); + } + return result; +} + +glm::vec2 Application::getDeviceSize() const { + static const int MIN_SIZE = 1; + glm::vec2 result(MIN_SIZE); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedRenderSize(); + } + return result; +} + +bool Application::isThrottleRendering() const { + if (_displayPlugin) { + return getActiveDisplayPlugin()->isThrottled(); + } + return false; +} + +bool Application::hasFocus() const { + bool result = (QApplication::activeWindow() != nullptr); +#if defined(Q_OS_WIN) + // On Windows, QWidget::activateWindow() - as called in setFocus() - makes the application's taskbar icon flash but doesn't + // take user focus away from their current window. So also check whether the application is the user's current foreground + // window. + result = result && (HWND)QApplication::activeWindow()->winId() == GetForegroundWindow(); +#endif + return result; +} + +void Application::setFocus() { + // Note: Windows doesn't allow a user focus to be taken away from another application. Instead, it changes the color of and + // flashes the taskbar icon. + auto window = qApp->getWindow(); + window->activateWindow(); +} + +void Application::raise() { + auto windowState = qApp->getWindow()->windowState(); + if (windowState & Qt::WindowMinimized) { + if (windowState & Qt::WindowMaximized) { + qApp->getWindow()->showMaximized(); + } else if (windowState & Qt::WindowFullScreen) { + qApp->getWindow()->showFullScreen(); + } else { + qApp->getWindow()->showNormal(); + } + } + qApp->getWindow()->raise(); +} + +void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { + if (maxOctreePPS != _maxOctreePPS) { + _maxOctreePPS = maxOctreePPS; + maxOctreePacketsPerSecond.set(_maxOctreePPS); + } +} + +int Application::getMaxOctreePacketsPerSecond() const { + return _maxOctreePPS; +} + +qreal Application::getDevicePixelRatio() { + return (_window && _window->windowHandle()) ? _window->windowHandle()->devicePixelRatio() : 1.0; +} + +DisplayPluginPointer Application::getActiveDisplayPlugin() const { + if (QThread::currentThread() != thread()) { + std::unique_lock lock(_displayPluginLock); + return _displayPlugin; + } + + if (!_aboutToQuit && !_displayPlugin) { + const_cast(this)->updateDisplayMode(); + Q_ASSERT(_displayPlugin); + } + return _displayPlugin; +} + + +#if !defined(DISABLE_QML) +static const char* EXCLUSION_GROUP_KEY = "exclusionGroup"; + +static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active) { + auto menu = Menu::getInstance(); + QString name = displayPlugin->getName(); + auto grouping = displayPlugin->getGrouping(); + QString groupingMenu { "" }; + Q_ASSERT(!menu->menuItemExists(MenuOption::OutputMenu, name)); + + // assign the meny grouping based on plugin grouping + switch (grouping) { + case Plugin::ADVANCED: + groupingMenu = "Advanced"; + break; + case Plugin::DEVELOPER: + groupingMenu = "Developer"; + break; + default: + groupingMenu = "Standard"; + break; + } + + static QActionGroup* displayPluginGroup = nullptr; + if (!displayPluginGroup) { + displayPluginGroup = new QActionGroup(menu); + displayPluginGroup->setExclusive(true); + } + auto parent = menu->getMenu(MenuOption::OutputMenu); + auto action = menu->addActionToQMenuAndActionHash(parent, + name, QKeySequence(Qt::CTRL + (Qt::Key_0 + index)), qApp, + SLOT(updateDisplayMode()), + QAction::NoRole, Menu::UNSPECIFIED_POSITION, groupingMenu); + + action->setCheckable(true); + action->setChecked(active); + displayPluginGroup->addAction(action); + + action->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(displayPluginGroup)); + Q_ASSERT(menu->menuItemExists(MenuOption::OutputMenu, name)); +} +#endif + +void Application::updateDisplayMode() { + // Unsafe to call this method from anything but the main thread + if (QThread::currentThread() != thread()) { + qFatal("Attempted to switch display plugins from a non-main thread"); + } + + // Once time initialization code that depends on the UI being available + auto displayPlugins = getDisplayPlugins(); + + // Default to the first item on the list, in case none of the menu items match + DisplayPluginPointer newDisplayPlugin = displayPlugins.at(0); + auto menu = getPrimaryMenu(); + if (menu) { + foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { + QString name = displayPlugin->getName(); + QAction* action = menu->getActionForOption(name); + // Menu might have been removed if the display plugin lost + if (!action) { + continue; + } + if (action->isChecked()) { + newDisplayPlugin = displayPlugin; + break; + } + } + } + + if (newDisplayPlugin == _displayPlugin) { + return; + } + + setDisplayPlugin(newDisplayPlugin); +} + +void Application::setDisplayPlugin(DisplayPluginPointer newDisplayPlugin) { + if (newDisplayPlugin == _displayPlugin) { + return; + } + + // FIXME don't have the application directly set the state of the UI, + // instead emit a signal that the display plugin is changing and let + // the desktop lock itself. Reduces coupling between the UI and display + // plugins + auto offscreenUi = getOffscreenUI(); + auto desktop = offscreenUi ? offscreenUi->getDesktop() : nullptr; + auto menu = Menu::getInstance(); + + // Make the switch atomic from the perspective of other threads + { + std::unique_lock lock(_displayPluginLock); + bool wasRepositionLocked = false; + if (desktop) { + // Tell the desktop to no reposition (which requires plugin info), until we have set the new plugin, below. + wasRepositionLocked = desktop->property("repositionLocked").toBool(); + desktop->setProperty("repositionLocked", true); + } + + if (_displayPlugin) { + disconnect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent); + _displayPlugin->deactivate(); + } + + auto oldDisplayPlugin = _displayPlugin; + bool active = newDisplayPlugin->activate(); + + if (!active) { + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + + // If the new plugin fails to activate, fallback to last display + qWarning() << "Failed to activate display: " << newDisplayPlugin->getName(); + newDisplayPlugin = oldDisplayPlugin; + + if (newDisplayPlugin) { + qWarning() << "Falling back to last display: " << newDisplayPlugin->getName(); + active = newDisplayPlugin->activate(); + } + + // If there is no last display, or + // If the last display fails to activate, fallback to desktop + if (!active) { + newDisplayPlugin = displayPlugins.at(0); + qWarning() << "Falling back to display: " << newDisplayPlugin->getName(); + active = newDisplayPlugin->activate(); + } + + if (!active) { + qFatal("Failed to activate fallback plugin"); + } + } + + if (offscreenUi) { + offscreenUi->resize(fromGlm(newDisplayPlugin->getRecommendedUiSize())); + } + getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); + _displayPlugin = newDisplayPlugin; + connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent, Qt::DirectConnection); + if (desktop) { + desktop->setProperty("repositionLocked", wasRepositionLocked); + } + } + + bool isHmd = _displayPlugin->isHmd(); + qCDebug(interfaceapp) << "Entering into" << (isHmd ? "HMD" : "Desktop") << "Mode"; + + // Only log/emit after a successful change + UserActivityLogger::getInstance().logAction("changed_display_mode", { + { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, + { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" }, + { "hmd", isHmd } + }); + emit activeDisplayPluginChanged(); + + // reset the avatar, to set head and hand palms back to a reasonable default pose. + getMyAvatar()->reset(false); + + // switch to first person if entering hmd and setting is checked + if (menu) { + QAction* action = menu->getActionForOption(newDisplayPlugin->getName()); + if (action) { + action->setChecked(true); + } + + if (isHmd && menu->isOptionChecked(MenuOption::FirstPersonHMD)) { + menu->setIsOptionChecked(MenuOption::FirstPerson, true); + cameraMenuChanged(); + } + + // Remove the mirror camera option from menu if in HMD mode + auto mirrorAction = menu->getActionForOption(MenuOption::FullscreenMirror); + mirrorAction->setVisible(!isHmd); + } + + Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); +} + +void Application::switchDisplayMode() { + if (!_autoSwitchDisplayModeSupportedHMDPlugin) { + return; + } + bool currentHMDWornStatus = _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); + if (currentHMDWornStatus != _previousHMDWornStatus) { + // Switch to respective mode as soon as currentHMDWornStatus changes + if (currentHMDWornStatus) { + qCDebug(interfaceapp) << "Switching from Desktop to HMD mode"; + endHMDSession(); + setActiveDisplayPlugin(_autoSwitchDisplayModeSupportedHMDPluginName); + } else { + qCDebug(interfaceapp) << "Switching from HMD to desktop mode"; + setActiveDisplayPlugin(DESKTOP_DISPLAY_PLUGIN_NAME); + startHMDStandBySession(); + } + } + _previousHMDWornStatus = currentHMDWornStatus; +} + +void Application::setShowBulletWireframe(bool value) { + _physicsEngine->setShowBulletWireframe(value); +} + +void Application::setShowBulletAABBs(bool value) { + _physicsEngine->setShowBulletAABBs(value); +} + +void Application::setShowBulletContactPoints(bool value) { + _physicsEngine->setShowBulletContactPoints(value); +} + +void Application::setShowBulletConstraints(bool value) { + _physicsEngine->setShowBulletConstraints(value); +} + +void Application::setShowBulletConstraintLimits(bool value) { + _physicsEngine->setShowBulletConstraintLimits(value); +} + +void Application::createLoginDialog() { + const glm::vec3 LOGIN_DIMENSIONS { 0.89f, 0.5f, 0.01f }; + const auto OFFSET = glm::vec2(0.7f, -0.1f); + auto cameraPosition = _myCamera.getPosition(); + auto cameraOrientation = _myCamera.getOrientation(); + auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; + auto headLookVec = (cameraOrientation * Vectors::FRONT); + // DEFAULT_DPI / tablet scale percentage + const float DPI = 31.0f / (75.0f / 100.0f); + auto offset = headLookVec * OFFSET.x; + auto position = (cameraPosition + offset) + (upVec * OFFSET.y); + + EntityItemProperties properties; + properties.setType(EntityTypes::Web); + properties.setName("LoginDialogEntity"); + properties.setSourceUrl(LOGIN_DIALOG.toString()); + properties.setPosition(position); + properties.setRotation(cameraOrientation); + properties.setDimensions(LOGIN_DIMENSIONS); + properties.setPrimitiveMode(PrimitiveMode::SOLID); + properties.getGrab().setGrabbable(false); + properties.setIgnorePickIntersection(false); + properties.setAlpha(1.0f); + properties.setDPI(DPI); + properties.setVisible(true); + + auto entityScriptingInterface = DependencyManager::get(); + _loginDialogID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); + + auto keyboard = DependencyManager::get().data(); + if (!keyboard->getAnchorID().isNull() && !_loginDialogID.isNull()) { + auto keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); + + EntityItemProperties properties; + properties.setPosition(position + keyboardLocalOffset); + properties.setRotation(cameraOrientation * Quaternions::Y_180); + + entityScriptingInterface->editEntity(keyboard->getAnchorID(), properties); + keyboard->setResetKeyboardPositionOnRaise(false); + } + setKeyboardFocusEntity(_loginDialogID); + emit loginDialogFocusEnabled(); + getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(false); + getApplicationCompositor().getReticleInterface()->setVisible(false); + if (!_loginStateManager.isSetUp()) { + _loginStateManager.setUp(); + } +} + +void Application::updateLoginDialogPosition() { + const float LOOK_AWAY_THRESHOLD_ANGLE = 70.0f; + const auto OFFSET = glm::vec2(0.7f, -0.1f); + + auto entityScriptingInterface = DependencyManager::get(); + EntityPropertyFlags desiredProperties; + desiredProperties += PROP_POSITION; + auto properties = entityScriptingInterface->getEntityProperties(_loginDialogID, desiredProperties); + auto positionVec = properties.getPosition(); + auto cameraPositionVec = _myCamera.getPosition(); + auto cameraOrientation = cancelOutRollAndPitch(_myCamera.getOrientation()); + auto headLookVec = (cameraOrientation * Vectors::FRONT); + auto entityToHeadVec = positionVec - cameraPositionVec; + auto pointAngle = (glm::acos(glm::dot(glm::normalize(entityToHeadVec), glm::normalize(headLookVec))) * 180.0f / PI); + auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; + auto offset = headLookVec * OFFSET.x; + auto newPositionVec = (cameraPositionVec + offset) + (upVec * OFFSET.y); + + bool outOfBounds = glm::distance(positionVec, cameraPositionVec) > 1.0f; + + if (pointAngle > LOOK_AWAY_THRESHOLD_ANGLE || outOfBounds) { + { + EntityItemProperties properties; + properties.setPosition(newPositionVec); + properties.setRotation(cameraOrientation); + entityScriptingInterface->editEntity(_loginDialogID, properties); + } + + { + glm::vec3 keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); + glm::quat keyboardOrientation = cameraOrientation * glm::quat(glm::radians(glm::vec3(-30.0f, 180.0f, 0.0f))); + + EntityItemProperties properties; + properties.setPosition(newPositionVec + keyboardLocalOffset); + properties.setRotation(keyboardOrientation); + entityScriptingInterface->editEntity(DependencyManager::get()->getAnchorID(), properties); + } + } +} + +bool Application::hasRiftControllers() { + return PluginUtils::isOculusTouchControllerAvailable(); +} + +bool Application::hasViveControllers() { + return PluginUtils::isViveControllerAvailable(); +} + +void Application::onDismissedLoginDialog() { + _loginDialogPoppedUp = false; + loginDialogPoppedUp.set(false); + auto keyboard = DependencyManager::get().data(); + keyboard->setResetKeyboardPositionOnRaise(true); + if (!_loginDialogID.isNull()) { + DependencyManager::get()->deleteEntity(_loginDialogID); + _loginDialogID = QUuid(); + _loginStateManager.tearDown(); + } + resumeAfterLoginDialogActionTaken(); +} + +void Application::setShowTrackedObjects(bool value) { + _showTrackedObjects = value; +} + +void Application::startHMDStandBySession() { + _autoSwitchDisplayModeSupportedHMDPlugin->startStandBySession(); +} + +void Application::endHMDSession() { + _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); +} + +mat4 Application::getEyeProjection(int eye) const { + QMutexLocker viewLocker(&_viewMutex); + if (isHMDMode()) { + return getActiveDisplayPlugin()->getEyeProjection((Eye)eye, _viewFrustum.getProjection()); + } + return _viewFrustum.getProjection(); +} + +mat4 Application::getEyeOffset(int eye) const { + // FIXME invert? + return getActiveDisplayPlugin()->getEyeToHeadTransform((Eye)eye); +} + +mat4 Application::getHMDSensorPose() const { + if (isHMDMode()) { + return getActiveDisplayPlugin()->getHeadPose(); + } + return mat4(); +} + +void Application::deadlockApplication() { + qCDebug(interfaceapp) << "Intentionally deadlocked Interface"; + // Using a loop that will *technically* eventually exit (in ~600 billion years) + // to avoid compiler warnings about a loop that will never exit + for (uint64_t i = 1; i != 0; ++i) { + QThread::sleep(1); + } +} + +// cause main thread to be unresponsive for 35 seconds +void Application::unresponsiveApplication() { + // to avoid compiler warnings about a loop that will never exit + uint64_t start = usecTimestampNow(); + uint64_t UNRESPONSIVE_FOR_SECONDS = 35; + uint64_t UNRESPONSIVE_FOR_USECS = UNRESPONSIVE_FOR_SECONDS * USECS_PER_SECOND; + qCDebug(interfaceapp) << "Intentionally cause Interface to be unresponsive for " << UNRESPONSIVE_FOR_SECONDS << " seconds"; + while (usecTimestampNow() - start < UNRESPONSIVE_FOR_USECS) { + QThread::sleep(1); + } +} + +void Application::setActiveDisplayPlugin(const QString& pluginName) { + DisplayPluginPointer newDisplayPlugin; + for (DisplayPluginPointer displayPlugin : PluginManager::getInstance()->getDisplayPlugins()) { + QString name = displayPlugin->getName(); + if (pluginName == name) { + newDisplayPlugin = displayPlugin; + break; + } + } + + if (newDisplayPlugin) { + setDisplayPlugin(newDisplayPlugin); + } +} + +void Application::handleLocalServerConnection() const { + auto server = qobject_cast(sender()); + + qCDebug(interfaceapp) << "Got connection on local server from additional instance - waiting for parameters"; + + auto socket = server->nextPendingConnection(); + + connect(socket, &QLocalSocket::readyRead, this, &Application::readArgumentsFromLocalSocket); + + qApp->getWindow()->raise(); + qApp->getWindow()->activateWindow(); +} + +void Application::readArgumentsFromLocalSocket() const { + auto socket = qobject_cast(sender()); + + auto message = socket->readAll(); + socket->deleteLater(); + + qCDebug(interfaceapp) << "Read from connection: " << message; + + // If we received a message, try to open it as a URL + if (message.length() > 0) { + qApp->openUrl(QString::fromUtf8(message)); + } +} + +void Application::showDesktop() { +} + +CompositorHelper& Application::getApplicationCompositor() const { + return *DependencyManager::get(); +} + + +// virtual functions required for PluginContainer +ui::Menu* Application::getPrimaryMenu() { + auto appMenu = _window->menuBar(); + auto uiMenu = dynamic_cast(appMenu); + return uiMenu; +} + +void Application::showDisplayPluginsTools(bool show) { + DependencyManager::get()->hmdTools(show); +} + +GLWidget* Application::getPrimaryWidget() { + return _glWidget; +} + +MainWindow* Application::getPrimaryWindow() { + return getWindow(); +} + +QOpenGLContext* Application::getPrimaryContext() { + return _glWidget->qglContext(); +} + +bool Application::makeRenderingContextCurrent() { + return true; +} + +bool Application::isForeground() const { + return _isForeground && !_window->isMinimized(); +} + +// FIXME? perhaps two, one for the main thread and one for the offscreen UI rendering thread? +static const int UI_RESERVED_THREADS = 1; +// Windows won't let you have all the cores +static const int OS_RESERVED_THREADS = 1; + +void Application::updateThreadPoolCount() const { + auto reservedThreads = UI_RESERVED_THREADS + OS_RESERVED_THREADS + _displayPlugin->getRequiredThreadCount(); + auto availableThreads = QThread::idealThreadCount() - reservedThreads; + auto threadPoolSize = std::max(MIN_PROCESSING_THREAD_POOL_SIZE, availableThreads); + qCDebug(interfaceapp) << "Ideal Thread Count " << QThread::idealThreadCount(); + qCDebug(interfaceapp) << "Reserved threads " << reservedThreads; + qCDebug(interfaceapp) << "Setting thread pool size to " << threadPoolSize; + QThreadPool::globalInstance()->setMaxThreadCount(threadPoolSize); +} + +void Application::updateSystemTabletMode() { + if (_settingsLoaded && !getLoginDialogPoppedUp()) { + qApp->setProperty(hifi::properties::HMD, isHMDMode()); + if (isHMDMode()) { + DependencyManager::get()->setToolbarMode(getHmdTabletBecomesToolbarSetting()); + } else { + DependencyManager::get()->setToolbarMode(getDesktopTabletBecomesToolbarSetting()); + } + } +} + +QUuid Application::getTabletScreenID() const { + auto HMD = DependencyManager::get(); + return HMD->getCurrentTabletScreenID(); +} + +QUuid Application::getTabletHomeButtonID() const { + auto HMD = DependencyManager::get(); + return HMD->getCurrentHomeButtonID(); +} + +QUuid Application::getTabletFrameID() const { + auto HMD = DependencyManager::get(); + return HMD->getCurrentTabletFrameID(); +} + +QVector Application::getTabletIDs() const { + // Most important first. + QVector result; + auto HMD = DependencyManager::get(); + result << HMD->getCurrentTabletScreenID(); + result << HMD->getCurrentHomeButtonID(); + result << HMD->getCurrentTabletFrameID(); + return result; +} + +void Application::setAvatarOverrideUrl(const QUrl& url, bool save) { + _avatarOverrideUrl = url; + _saveAvatarOverrideUrl = save; +} + +void Application::saveNextPhysicsStats(QString filename) { + _physicsEngine->saveNextPhysicsStats(filename); +} + +void Application::copyToClipboard(const QString& text) { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "copyToClipboard"); + return; + } + + // assume that the address is being copied because the user wants a shareable address + QApplication::clipboard()->setText(text); +} + +QString Application::getGraphicsCardType() { + return GPUIdent::getInstance()->getName(); +} + +#if defined(Q_OS_ANDROID) +void Application::beforeEnterBackground() { + auto nodeList = DependencyManager::get(); + nodeList->setSendDomainServerCheckInEnabled(false); + nodeList->reset(true); + clearDomainOctreeDetails(); +} + + + +void Application::enterBackground() { + QMetaObject::invokeMethod(DependencyManager::get().data(), + "stop", Qt::BlockingQueuedConnection); +// Quest only supports one plugin which can't be deactivated currently +#if !defined(ANDROID_APP_QUEST_INTERFACE) + if (getActiveDisplayPlugin()->isActive()) { + getActiveDisplayPlugin()->deactivate(); + } +#endif +} + +void Application::enterForeground() { + QMetaObject::invokeMethod(DependencyManager::get().data(), + "start", Qt::BlockingQueuedConnection); +// Quest only supports one plugin which can't be deactivated currently +#if !defined(ANDROID_APP_QUEST_INTERFACE) + if (!getActiveDisplayPlugin() || getActiveDisplayPlugin()->isActive() || !getActiveDisplayPlugin()->activate()) { + qWarning() << "Could not re-activate display plugin"; + } +#endif + auto nodeList = DependencyManager::get(); + nodeList->setSendDomainServerCheckInEnabled(true); +} + + +void Application::toggleAwayMode(){ + QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); + QCoreApplication::sendEvent (this, &event); +} + + +#endif + + +#include "Application.moc" diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 852c4eb695..6d9a1823a1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1795,7 +1795,19 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { - resumeAfterLoginDialogActionTaken(); +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) + // Do not show login dialog if requested not to on the command line + QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); + int index = arguments().indexOf(hifiNoLoginCommandLineKey); + if (index != -1) { + resumeAfterLoginDialogActionTaken(); + return; + } + + showLoginScreen(); +#else + resumeAfterLoginDialogActionTaken(); +#endif }); // Make sure we don't time out during slow operations at startup From 4c7b292e1afc2961cef5599a800b70f2d06e2c87 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 10:25:40 -0700 Subject: [PATCH 020/135] Remove unneeded wait. --- tools/nitpick/src/TestCreator.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index 089e84904a..17a191315c 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -851,10 +851,7 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << " nitpick.enableRecursive();" << endl; - textStream << " nitpick.enableAuto();" << endl << endl; - textStream << " if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << " }" << endl; + textStream << " nitpick.enableAuto();" << endl; textStream << "} else {" << endl; textStream << " depth++" << endl; textStream << "}" << endl << endl; From 858d80073faa16dac126566cfebb7f3a2bc43428 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 13:27:36 -0700 Subject: [PATCH 021/135] Change default snapshots folder. --- tools/nitpick/src/TestRunnerMobile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index eaebb6ca5a..ed48c37290 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -43,7 +43,7 @@ TestRunnerMobile::TestRunnerMobile( _installAPKPushbutton = installAPKPushbutton; _runInterfacePushbutton = runInterfacePushbutton; - folderLineEdit->setText("/sdcard/DCIM/TEST"); + folderLineEdit->setText("/sdcard/snapshots"); modelNames["SM_G955U1"] = "Samsung S8+ unlocked"; modelNames["SM_N960U1"] = "Samsung Note 9 unlocked"; From 643fbfd80538a95e3fc880c27f7b7877f7bd2829 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 14:06:45 -0700 Subject: [PATCH 022/135] TEST TEST TEST --- interface/src/Application.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..7dddaecadb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1804,7 +1804,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) // Do not show login dialog if requested not to on the command line QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); int index = arguments().indexOf(hifiNoLoginCommandLineKey); @@ -1814,9 +1813,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } showLoginScreen(); -#else - resumeAfterLoginDialogActionTaken(); -#endif }); // Make sure we don't time out during slow operations at startup From 647b508b9d005584cd2806f87d69b7ff42955b7f Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 14:53:19 -0700 Subject: [PATCH 023/135] TEST TEST TEST --- interface/src/Application - Copy.cpp | 9191 -------------------------- 1 file changed, 9191 deletions(-) delete mode 100644 interface/src/Application - Copy.cpp diff --git a/interface/src/Application - Copy.cpp b/interface/src/Application - Copy.cpp deleted file mode 100644 index ca8883f660..0000000000 --- a/interface/src/Application - Copy.cpp +++ /dev/null @@ -1,9191 +0,0 @@ -// -// Application.cpp -// interface/src -// -// Created by Andrzej Kapolka on 5/10/13. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#include "Application.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include - - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "ui/overlays/ContextOverlayInterface.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "LocationBookmarks.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "recording/ClipCache.h" - -#include "AudioClient.h" -#include "audio/AudioScope.h" -#include "avatar/AvatarManager.h" -#include "avatar/MyHead.h" -#include "avatar/AvatarPackager.h" -#include "avatar/MyCharacterController.h" -#include "CrashRecoveryHandler.h" -#include "CrashHandler.h" -#include "devices/DdeFaceTracker.h" -#include "DiscoverabilityManager.h" -#include "GLCanvas.h" -#include "InterfaceDynamicFactory.h" -#include "InterfaceLogging.h" -#include "LODManager.h" -#include "ModelPackager.h" -#include "scripting/Audio.h" -#include "networking/CloseEventSender.h" -#include "scripting/TestScriptingInterface.h" -#include "scripting/PlatformInfoScriptingInterface.h" -#include "scripting/AssetMappingsScriptingInterface.h" -#include "scripting/ClipboardScriptingInterface.h" -#include "scripting/DesktopScriptingInterface.h" -#include "scripting/AccountServicesScriptingInterface.h" -#include "scripting/HMDScriptingInterface.h" -#include "scripting/MenuScriptingInterface.h" -#include "graphics-scripting/GraphicsScriptingInterface.h" -#include "scripting/SettingsScriptingInterface.h" -#include "scripting/WindowScriptingInterface.h" -#include "scripting/ControllerScriptingInterface.h" -#include "scripting/RatesScriptingInterface.h" -#include "scripting/SelectionScriptingInterface.h" -#include "scripting/WalletScriptingInterface.h" -#include "scripting/TTSScriptingInterface.h" -#include "scripting/KeyboardScriptingInterface.h" - - - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) -#include "SpeechRecognizer.h" -#endif -#include "ui/ResourceImageItem.h" -#include "ui/AddressBarDialog.h" -#include "ui/AvatarInputs.h" -#include "ui/DialogsManager.h" -#include "ui/LoginDialog.h" -#include "ui/Snapshot.h" -#include "ui/SnapshotAnimated.h" -#include "ui/StandAloneJSConsole.h" -#include "ui/Stats.h" -#include "ui/AnimStats.h" -#include "ui/UpdateDialog.h" -#include "ui/DomainConnectionModel.h" -#include "ui/Keyboard.h" -#include "Util.h" -#include "InterfaceParentFinder.h" -#include "ui/OctreeStatsProvider.h" - -#include "avatar/GrabManager.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "commerce/Ledger.h" -#include "commerce/Wallet.h" -#include "commerce/QmlCommerce.h" -#include "commerce/QmlMarketplace.h" -#include "ResourceRequestObserver.h" - -#include "webbrowser/WebBrowserSuggestionsEngine.h" -#include - - -#include "AboutUtil.h" - -#if defined(Q_OS_WIN) -#include - -#ifdef DEBUG_EVENT_QUEUE -// This is a HACK that uses private headers included with the qt source distrubution. -// To use this feature you need to add these directores to your include path: -// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1/QtCore -// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1 -#define QT_BOOTSTRAPPED -#include -#include -#undef QT_BOOTSTRAPPED -#endif - -// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU -// FIXME seems to be broken. -extern "C" { - _declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; -} -#endif - -#if defined(Q_OS_ANDROID) -#include -#include "AndroidHelper.h" -#endif - -#include "graphics/RenderEventHandler.h" - -Q_LOGGING_CATEGORY(trace_app_input_mouse, "trace.app.input.mouse") - -using namespace std; - -static QTimer locationUpdateTimer; -static QTimer identityPacketTimer; -static QTimer pingTimer; - -#if defined(Q_OS_ANDROID) -static bool DISABLE_WATCHDOG = true; -#else -static const QString DISABLE_WATCHDOG_FLAG{ "HIFI_DISABLE_WATCHDOG" }; -static bool DISABLE_WATCHDOG = nsightActive() || QProcessEnvironment::systemEnvironment().contains(DISABLE_WATCHDOG_FLAG); -#endif - -#if defined(USE_GLES) -static bool DISABLE_DEFERRED = true; -#else -static const QString RENDER_FORWARD{ "HIFI_RENDER_FORWARD" }; -static bool DISABLE_DEFERRED = QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); -#endif - -#if !defined(Q_OS_ANDROID) -static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; -#else -static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 4; -#endif - -// For processing on QThreadPool, we target a number of threads after reserving some -// based on how many are being consumed by the application and the display plugin. However, -// we will never drop below the 'min' value -static const int MIN_PROCESSING_THREAD_POOL_SIZE = 1; - -static const QString SNAPSHOT_EXTENSION = ".jpg"; -static const QString JPG_EXTENSION = ".jpg"; -static const QString PNG_EXTENSION = ".png"; -static const QString SVO_EXTENSION = ".svo"; -static const QString SVO_JSON_EXTENSION = ".svo.json"; -static const QString JSON_GZ_EXTENSION = ".json.gz"; -static const QString JSON_EXTENSION = ".json"; -static const QString JS_EXTENSION = ".js"; -static const QString FST_EXTENSION = ".fst"; -static const QString FBX_EXTENSION = ".fbx"; -static const QString OBJ_EXTENSION = ".obj"; -static const QString AVA_JSON_EXTENSION = ".ava.json"; -static const QString WEB_VIEW_TAG = "noDownload=true"; -static const QString ZIP_EXTENSION = ".zip"; -static const QString CONTENT_ZIP_EXTENSION = ".content.zip"; - -static const float MIRROR_FULLSCREEN_DISTANCE = 0.789f; - -static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; - -static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; -static const QString INFO_HELP_PATH = "html/tabletHelp.html"; - -static const unsigned int THROTTLED_SIM_FRAMERATE = 15; -static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; -static const int ENTITY_SERVER_ADDED_TIMEOUT = 5000; -static const int ENTITY_SERVER_CONNECTION_TIMEOUT = 5000; - -static const float INITIAL_QUERY_RADIUS = 10.0f; // priority radius for entities before physics enabled - -static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - -Setting::Handle maxOctreePacketsPerSecond{"maxOctreePPS", DEFAULT_MAX_OCTREE_PPS}; - -Setting::Handle loginDialogPoppedUp{"loginDialogPoppedUp", false}; - -static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; -static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; -static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; - -static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; -static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds -static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; -static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; -static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; -static const QString KEEP_ME_LOGGED_IN_SETTING_NAME = "keepMeLoggedIn"; - -static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f; - -#if defined(Q_OS_ANDROID) -static const QString TESTER_FILE = "/sdcard/_hifi_test_device.txt"; -#endif -const std::vector> Application::_acceptedExtensions { - { SVO_EXTENSION, &Application::importSVOFromURL }, - { SVO_JSON_EXTENSION, &Application::importSVOFromURL }, - { AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl }, - { JSON_EXTENSION, &Application::importJSONFromURL }, - { JS_EXTENSION, &Application::askToLoadScript }, - { FST_EXTENSION, &Application::askToSetAvatarUrl }, - { JSON_GZ_EXTENSION, &Application::askToReplaceDomainContent }, - { CONTENT_ZIP_EXTENSION, &Application::askToReplaceDomainContent }, - { ZIP_EXTENSION, &Application::importFromZIP }, - { JPG_EXTENSION, &Application::importImage }, - { PNG_EXTENSION, &Application::importImage } -}; - -class DeadlockWatchdogThread : public QThread { -public: - static const unsigned long HEARTBEAT_UPDATE_INTERVAL_SECS = 1; - static const unsigned long MAX_HEARTBEAT_AGE_USECS = 120 * USECS_PER_SECOND; // 2 mins with no checkin probably a deadlock - static const int WARNING_ELAPSED_HEARTBEAT = 500 * USECS_PER_MSEC; // warn if elapsed heartbeat average is large - static const int HEARTBEAT_SAMPLES = 100000; // ~5 seconds worth of samples - - // Set the heartbeat on launch - DeadlockWatchdogThread() { - setObjectName("Deadlock Watchdog"); - // Give the heartbeat an initial value - _heartbeat = usecTimestampNow(); - _paused = false; - connect(qApp, &QCoreApplication::aboutToQuit, [this] { - _quit = true; - }); - } - - void setMainThreadID(Qt::HANDLE threadID) { - _mainThreadID = threadID; - } - - static void updateHeartbeat() { - auto now = usecTimestampNow(); - auto elapsed = now - _heartbeat; - _movingAverage.addSample(elapsed); - _heartbeat = now; - } - - void deadlockDetectionCrash() { - setCrashAnnotation("_mod_faulting_tid", std::to_string((uint64_t)_mainThreadID)); - setCrashAnnotation("deadlock", "1"); - uint32_t* crashTrigger = nullptr; - *crashTrigger = 0xDEAD10CC; - } - - static void withPause(const std::function& lambda) { - pause(); - lambda(); - resume(); - } - static void pause() { - _paused = true; - } - - static void resume() { - // Update the heartbeat BEFORE resuming the checks - updateHeartbeat(); - _paused = false; - } - - void run() override { - while (!_quit) { - QThread::sleep(HEARTBEAT_UPDATE_INTERVAL_SECS); - // Don't do heartbeat detection under nsight - if (_paused) { - continue; - } - uint64_t lastHeartbeat = _heartbeat; // sample atomic _heartbeat, because we could context switch away and have it updated on us - uint64_t now = usecTimestampNow(); - auto lastHeartbeatAge = (now > lastHeartbeat) ? now - lastHeartbeat : 0; - auto elapsedMovingAverage = _movingAverage.getAverage(); - - if (elapsedMovingAverage > _maxElapsedAverage) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" - << "lastHeartbeatAge:" << lastHeartbeatAge - << "elapsedMovingAverage:" << elapsedMovingAverage - << "maxElapsed:" << _maxElapsed - << "PREVIOUS maxElapsedAverage:" << _maxElapsedAverage - << "NEW maxElapsedAverage:" << elapsedMovingAverage << "** NEW MAX ELAPSED AVERAGE **" - << "samples:" << _movingAverage.getSamples(); - _maxElapsedAverage = elapsedMovingAverage; - } - if (lastHeartbeatAge > _maxElapsed) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" - << "lastHeartbeatAge:" << lastHeartbeatAge - << "elapsedMovingAverage:" << elapsedMovingAverage - << "PREVIOUS maxElapsed:" << _maxElapsed - << "NEW maxElapsed:" << lastHeartbeatAge << "** NEW MAX ELAPSED **" - << "maxElapsedAverage:" << _maxElapsedAverage - << "samples:" << _movingAverage.getSamples(); - _maxElapsed = lastHeartbeatAge; - } - if (elapsedMovingAverage > WARNING_ELAPSED_HEARTBEAT) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" - << "lastHeartbeatAge:" << lastHeartbeatAge - << "elapsedMovingAverage:" << elapsedMovingAverage << "** OVER EXPECTED VALUE **" - << "maxElapsed:" << _maxElapsed - << "maxElapsedAverage:" << _maxElapsedAverage - << "samples:" << _movingAverage.getSamples(); - } - - if (lastHeartbeatAge > MAX_HEARTBEAT_AGE_USECS) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK DETECTED -- " - << "lastHeartbeatAge:" << lastHeartbeatAge - << "[ lastHeartbeat :" << lastHeartbeat - << "now:" << now << " ]" - << "elapsedMovingAverage:" << elapsedMovingAverage - << "maxElapsed:" << _maxElapsed - << "maxElapsedAverage:" << _maxElapsedAverage - << "samples:" << _movingAverage.getSamples(); - - // Don't actually crash in debug builds, in case this apparent deadlock is simply from - // the developer actively debugging code - #ifdef NDEBUG - deadlockDetectionCrash(); - #endif - } - } - } - - static std::atomic _paused; - static std::atomic _heartbeat; - static std::atomic _maxElapsed; - static std::atomic _maxElapsedAverage; - static ThreadSafeMovingAverage _movingAverage; - - bool _quit { false }; - - Qt::HANDLE _mainThreadID = nullptr; -}; - -std::atomic DeadlockWatchdogThread::_paused; -std::atomic DeadlockWatchdogThread::_heartbeat; -std::atomic DeadlockWatchdogThread::_maxElapsed; -std::atomic DeadlockWatchdogThread::_maxElapsedAverage; -ThreadSafeMovingAverage DeadlockWatchdogThread::_movingAverage; - -bool isDomainURL(QUrl url) { - if (!url.isValid()) { - return false; - } - if (url.scheme() == URL_SCHEME_HIFI) { - return true; - } - if (url.scheme() != HIFI_URL_SCHEME_FILE) { - // TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can - // be loaded over http(s) - // && url.scheme() != HIFI_URL_SCHEME_HTTP && - // url.scheme() != HIFI_URL_SCHEME_HTTPS - return false; - } - if (url.path().endsWith(".json", Qt::CaseInsensitive) || - url.path().endsWith(".json.gz", Qt::CaseInsensitive)) { - return true; - } - return false; -} - -#ifdef Q_OS_WIN -class MyNativeEventFilter : public QAbstractNativeEventFilter { -public: - static MyNativeEventFilter& getInstance() { - static MyNativeEventFilter staticInstance; - return staticInstance; - } - - bool nativeEventFilter(const QByteArray &eventType, void* msg, long* result) Q_DECL_OVERRIDE { - if (eventType == "windows_generic_MSG") { - MSG* message = (MSG*)msg; - - if (message->message == UWM_IDENTIFY_INSTANCES) { - *result = UWM_IDENTIFY_INSTANCES; - return true; - } - - if (message->message == UWM_SHOW_APPLICATION) { - MainWindow* applicationWindow = qApp->getWindow(); - if (applicationWindow->isMinimized()) { - applicationWindow->showNormal(); // Restores to windowed or maximized state appropriately. - } - qApp->setActiveWindow(applicationWindow); // Flashes the taskbar icon if not focus. - return true; - } - - if (message->message == WM_COPYDATA) { - COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)(message->lParam); - QUrl url = QUrl((const char*)(pcds->lpData)); - if (isDomainURL(url)) { - DependencyManager::get()->handleLookupString(url.toString()); - return true; - } - } - - if (message->message == WM_DEVICECHANGE) { - const float MIN_DELTA_SECONDS = 2.0f; // de-bounce signal - static float lastTriggerTime = 0.0f; - const float deltaSeconds = secTimestampNow() - lastTriggerTime; - lastTriggerTime = secTimestampNow(); - if (deltaSeconds > MIN_DELTA_SECONDS) { - Midi::USBchanged(); // re-scan the MIDI bus - } - } - } - return false; - } -}; -#endif - -class LambdaEvent : public QEvent { - std::function _fun; -public: - LambdaEvent(const std::function & fun) : - QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { - } - LambdaEvent(std::function && fun) : - QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { - } - void call() const { _fun(); } -}; - -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - QString logMessage = LogHandler::getInstance().printMessage((LogMsgType) type, context, message); - - if (!logMessage.isEmpty()) { -#ifdef Q_OS_ANDROID - const char * local=logMessage.toStdString().c_str(); - switch (type) { - case QtDebugMsg: - __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); - break; - case QtInfoMsg: - __android_log_write(ANDROID_LOG_INFO,"Interface",local); - break; - case QtWarningMsg: - __android_log_write(ANDROID_LOG_WARN,"Interface",local); - break; - case QtCriticalMsg: - __android_log_write(ANDROID_LOG_ERROR,"Interface",local); - break; - case QtFatalMsg: - default: - __android_log_write(ANDROID_LOG_FATAL,"Interface",local); - abort(); - } -#else - qApp->getLogger()->addMessage(qPrintable(logMessage)); -#endif - } -} - - -class ApplicationMeshProvider : public scriptable::ModelProviderFactory { -public: - virtual scriptable::ModelProviderPointer lookupModelProvider(const QUuid& uuid) override { - bool success; - if (auto nestable = DependencyManager::get()->find(uuid, success).lock()) { - auto type = nestable->getNestableType(); -#ifdef SCRIPTABLE_MESH_DEBUG - qCDebug(interfaceapp) << "ApplicationMeshProvider::lookupModelProvider" << uuid << SpatiallyNestable::nestableTypeToString(type); -#endif - switch (type) { - case NestableType::Entity: - return getEntityModelProvider(static_cast(uuid)); - case NestableType::Avatar: - return getAvatarModelProvider(uuid); - } - } - return nullptr; - } - -private: - scriptable::ModelProviderPointer getEntityModelProvider(EntityItemID entityID) { - scriptable::ModelProviderPointer provider; - auto entityTreeRenderer = qApp->getEntities(); - auto entityTree = entityTreeRenderer->getTree(); - if (auto entity = entityTree->findEntityByID(entityID)) { - if (auto renderer = entityTreeRenderer->renderableForEntityId(entityID)) { - provider = std::dynamic_pointer_cast(renderer); - provider->modelProviderType = NestableType::Entity; - } else { - qCWarning(interfaceapp) << "no renderer for entity ID" << entityID.toString(); - } - } - return provider; - } - - scriptable::ModelProviderPointer getAvatarModelProvider(QUuid sessionUUID) { - scriptable::ModelProviderPointer provider; - auto avatarManager = DependencyManager::get(); - if (auto avatar = avatarManager->getAvatarBySessionID(sessionUUID)) { - provider = std::dynamic_pointer_cast(avatar); - provider->modelProviderType = NestableType::Avatar; - } - return provider; - } -}; - -/**jsdoc - *

The Controller.Hardware.Application object has properties representing Interface's state. The property - * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or - * Controller.Standard items in a {@link RouteObject} mapping (e.g., using the {@link RouteObject#when} method). - * Each data value is either 1.0 for "true" or 0.0 for "false".

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
PropertyTypeDataDescription
CameraFirstPersonnumbernumberThe camera is in first-person mode. - *
CameraThirdPersonnumbernumberThe camera is in third-person mode. - *
CameraFSMnumbernumberThe camera is in full screen mirror mode.
CameraIndependentnumbernumberThe camera is in independent mode.
CameraEntitynumbernumberThe camera is in entity mode.
InHMDnumbernumberThe user is in HMD mode.
AdvancedMovementnumbernumberAdvanced movement controls are enabled. - *
SnapTurnnumbernumberSnap turn is enabled.
GroundednumbernumberThe user's avatar is on the ground.
NavigationFocusednumbernumberNot used.
- * @typedef {object} Controller.Hardware-Application - */ - -static const QString STATE_IN_HMD = "InHMD"; -static const QString STATE_CAMERA_FULL_SCREEN_MIRROR = "CameraFSM"; -static const QString STATE_CAMERA_FIRST_PERSON = "CameraFirstPerson"; -static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; -static const QString STATE_CAMERA_ENTITY = "CameraEntity"; -static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; -static const QString STATE_SNAP_TURN = "SnapTurn"; -static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; -static const QString STATE_GROUNDED = "Grounded"; -static const QString STATE_NAV_FOCUSED = "NavigationFocused"; -static const QString STATE_PLATFORM_WINDOWS = "PlatformWindows"; -static const QString STATE_PLATFORM_MAC = "PlatformMac"; -static const QString STATE_PLATFORM_ANDROID = "PlatformAndroid"; - -// Statically provided display and input plugins -extern DisplayPluginList getDisplayPlugins(); -extern InputPluginList getInputPlugins(); -extern void saveInputPluginSettings(const InputPluginList& plugins); - -// Parameters used for running tests from teh command line -const QString TEST_SCRIPT_COMMAND{ "--testScript" }; -const QString TEST_QUIT_WHEN_FINISHED_OPTION{ "quitWhenFinished" }; -const QString TEST_RESULTS_LOCATION_COMMAND{ "--testResultsLocation" }; - -bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { - const char** constArgv = const_cast(argv); - - qInstallMessageHandler(messageHandler); - - // HRS: I could not figure out how to move these any earlier in startup, so when using this option, be sure to also supply - // --allowMultipleInstances - auto reportAndQuit = [&](const char* commandSwitch, std::function report) { - const char* reportfile = getCmdOption(argc, constArgv, commandSwitch); - // Reports to the specified file, because stdout is set up to be captured for logging. - if (reportfile) { - FILE* fp = fopen(reportfile, "w"); - if (fp) { - report(fp); - fclose(fp); - if (!runningMarkerExisted) { // don't leave ours around - RunningMarker runingMarker(RUNNING_MARKER_FILENAME); - runingMarker.deleteRunningMarkerFile(); // happens in deleter, but making the side-effect explicit. - } - _exit(0); - } - } - }; - reportAndQuit("--protocolVersion", [&](FILE* fp) { - auto version = protocolVersionsSignatureBase64(); - fputs(version.toLatin1().data(), fp); - }); - reportAndQuit("--version", [&](FILE* fp) { - fputs(BuildInfo::VERSION.toLatin1().data(), fp); - }); - - const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); - const int listenPort = portStr ? atoi(portStr) : INVALID_PORT; - - static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; - bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); - - // set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store - static const auto OCULUS_STORE_ARG = "--oculus-store"; - bool isStore = cmdOptionExists(argc, const_cast(argv), OCULUS_STORE_ARG); - qApp->setProperty(hifi::properties::OCULUS_STORE, isStore); - - // Ignore any previous crashes if running from command line with a test script. - bool inTestMode { false }; - for (int i = 0; i < argc; ++i) { - QString parameter(argv[i]); - if (parameter == TEST_SCRIPT_COMMAND) { - inTestMode = true; - break; - } - } - - bool previousSessionCrashed { false }; - if (!inTestMode) { - previousSessionCrashed = CrashRecoveryHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt); - } - - // get dir to use for cache - static const auto CACHE_SWITCH = "--cache"; - QString cacheDir = getCmdOption(argc, const_cast(argv), CACHE_SWITCH); - if (!cacheDir.isEmpty()) { - qApp->setProperty(hifi::properties::APP_LOCAL_DATA_PATH, cacheDir); - } - - { - const QString resourcesBinaryFile = PathUtils::getRccPath(); - if (!QFile::exists(resourcesBinaryFile)) { - throw std::runtime_error("Unable to find primary resources"); - } - if (!QResource::registerResource(resourcesBinaryFile)) { - throw std::runtime_error("Unable to load primary resources"); - } - } - - // Tell the plugin manager about our statically linked plugins - DependencyManager::set(); - auto pluginManager = PluginManager::getInstance(); - pluginManager->setInputPluginProvider([] { return getInputPlugins(); }); - pluginManager->setDisplayPluginProvider([] { return getDisplayPlugins(); }); - pluginManager->setInputPluginSettingsPersister([](const InputPluginList& plugins) { saveInputPluginSettings(plugins); }); - if (auto steamClient = pluginManager->getSteamClientPlugin()) { - steamClient->init(); - } - if (auto oculusPlatform = pluginManager->getOculusPlatformPlugin()) { - oculusPlatform->init(); - } - - PROFILE_SET_THREAD_NAME("Main Thread"); - -#if defined(Q_OS_WIN) - // Select appropriate audio DLL - QString audioDLLPath = QCoreApplication::applicationDirPath(); - if (IsWindows8OrGreater()) { - audioDLLPath += "/audioWin8"; - } else { - audioDLLPath += "/audioWin7"; - } - QCoreApplication::addLibraryPath(audioDLLPath); -#endif - - DependencyManager::registerInheritance(); - DependencyManager::registerInheritance(); - DependencyManager::registerInheritance(); - DependencyManager::registerInheritance(); - - // Set dependencies - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); -#if defined(Q_OS_ANDROID) - DependencyManager::set(); // use the default user agent getter -#else - DependencyManager::set(std::bind(&Application::getUserAgent, qApp)); -#endif - DependencyManager::set(); - DependencyManager::set(ScriptEngine::CLIENT_SCRIPT); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(NodeType::Agent, listenPort); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache constructor. - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(true); - DependencyManager::set(); - DependencyManager::registerInheritance(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - DependencyManager::set(); -#endif - DependencyManager::set(); - DependencyManager::set(); -#if !defined(DISABLE_QML) - DependencyManager::set(); -#endif - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, - STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, - STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED, - STATE_PLATFORM_WINDOWS, STATE_PLATFORM_MAC, STATE_PLATFORM_ANDROID } }); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(true, qApp, qApp); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - - return previousSessionCrashed; -} - -// FIXME move to header, or better yet, design some kind of UI manager -// to take care of highlighting keyboard focused items, rather than -// continuing to overburden Application.cpp -QUuid _keyboardFocusHighlightID; - -OffscreenGLCanvas* _qmlShareContext { nullptr }; - -// FIXME hack access to the internal share context for the Chromium helper -// Normally we'd want to use QWebEngine::initialize(), but we can't because -// our primary context is a QGLWidget, which can't easily be initialized to share -// from a QOpenGLContext. -// -// So instead we create a new offscreen context to share with the QGLWidget, -// and manually set THAT to be the shared context for the Chromium helper -#if !defined(DISABLE_QML) -OffscreenGLCanvas* _chromiumShareContext { nullptr }; -#endif - -Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); -Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); - -Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; - -const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f; -const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; -const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; -const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; -const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false; -const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; -const QString DEFAULT_CURSOR_NAME = "DEFAULT"; -const bool DEFAULT_MINI_TABLET_ENABLED = true; - -QSharedPointer getOffscreenUI() { -#if !defined(DISABLE_QML) - return DependencyManager::get(); -#else - return nullptr; -#endif -} - -Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runningMarkerExisted) : - QApplication(argc, argv), - _window(new MainWindow(desktop())), - _sessionRunTimer(startupTimer), -#ifndef Q_OS_ANDROID - _logger(new FileLogger(this)), -#endif - _previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)), - _entitySimulation(new PhysicalEntitySimulation()), - _physicsEngine(new PhysicsEngine(Vectors::ZERO)), - _entityClipboard(new EntityTree()), - _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), - _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), - _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), - _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), - _firstRun(Settings::firstRun, true), - _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), - _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), - _preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER), - _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), - _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), - _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), - _miniTabletEnabledSetting("miniTabletEnabled", DEFAULT_MINI_TABLET_ENABLED), - _scaleMirror(1.0f), - _mirrorYawOffset(0.0f), - _raiseMirror(0.0f), - _enableProcessOctreeThread(true), - _lastNackTime(usecTimestampNow()), - _lastSendDownstreamAudioStats(usecTimestampNow()), - _notifiedPacketVersionMismatchThisDomain(false), - _maxOctreePPS(maxOctreePacketsPerSecond.get()), - _lastFaceTrackerUpdate(0), - _snapshotSound(nullptr), - _sampleSound(nullptr) -{ - - auto steamClient = PluginManager::getInstance()->getSteamClientPlugin(); - setProperty(hifi::properties::STEAM, (steamClient && steamClient->isRunning())); - setProperty(hifi::properties::CRASHED, _previousSessionCrashed); - - { - const QStringList args = arguments(); - - for (int i = 0; i < args.size() - 1; ++i) { - if (args.at(i) == TEST_SCRIPT_COMMAND && (i + 1) < args.size()) { - QString testScriptPath = args.at(i + 1); - - // If the URL scheme is http(s) or ftp, then use as is, else - treat it as a local file - // This is done so as not break previous command line scripts - if (testScriptPath.left(HIFI_URL_SCHEME_HTTP.length()) == HIFI_URL_SCHEME_HTTP || - testScriptPath.left(HIFI_URL_SCHEME_FTP.length()) == HIFI_URL_SCHEME_FTP) { - - setProperty(hifi::properties::TEST, QUrl::fromUserInput(testScriptPath)); - } else if (QFileInfo(testScriptPath).exists()) { - setProperty(hifi::properties::TEST, QUrl::fromLocalFile(testScriptPath)); - } - - // quite when finished parameter must directly follow the test script - if ((i + 2) < args.size() && args.at(i + 2) == TEST_QUIT_WHEN_FINISHED_OPTION) { - quitWhenFinished = true; - } - } else if (args.at(i) == TEST_RESULTS_LOCATION_COMMAND) { - // Set test snapshot location only if it is a writeable directory - QString path(args.at(i + 1)); - - QFileInfo fileInfo(path); - if (fileInfo.isDir() && fileInfo.isWritable()) { - TestScriptingInterface::getInstance()->setTestResultsLocation(path); - } - } - } - } - - // make sure the debug draw singleton is initialized on the main thread. - DebugDraw::getInstance().removeMarker(""); - - PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care - PluginManager::getInstance()->setContainer(pluginContainer); - - QThreadPool::globalInstance()->setMaxThreadCount(MIN_PROCESSING_THREAD_POOL_SIZE); - thread()->setPriority(QThread::HighPriority); - thread()->setObjectName("Main Thread"); - - setInstance(this); - - auto controllerScriptingInterface = DependencyManager::get().data(); - _controllerScriptingInterface = dynamic_cast(controllerScriptingInterface); - connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged, - controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices); - - EntityTree::setEntityClicksCapturedOperator([this] { - return _controllerScriptingInterface->areEntityClicksCaptured(); - }); - - _entityClipboard->createRootElement(); - -#ifdef Q_OS_WIN - installNativeEventFilter(&MyNativeEventFilter::getInstance()); -#endif - - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/fontawesome-webfont.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/hifi-glyphs.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/AnonymousPro-Regular.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-Regular.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-SemiBold.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Light.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Regular.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/rawline-500.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf"); - _window->setWindowTitle("High Fidelity Interface"); - - Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us - - auto nodeList = DependencyManager::get(); - nodeList->startThread(); - nodeList->setFlagTimeForConnectionStep(true); - - // move the AddressManager to the NodeList thread so that domain resets due to domain changes always occur - // before we tell MyAvatar to go to a new location in the new domain - auto addressManager = DependencyManager::get(); - addressManager->moveToThread(nodeList->thread()); - - const char** constArgv = const_cast(argv); - if (cmdOptionExists(argc, constArgv, "--disableWatchdog")) { - DISABLE_WATCHDOG = true; - } - // Set up a watchdog thread to intentionally crash the application on deadlocks - if (!DISABLE_WATCHDOG) { - auto deadlockWatchdogThread = new DeadlockWatchdogThread(); - deadlockWatchdogThread->setMainThreadID(QThread::currentThreadId()); - deadlockWatchdogThread->start(); - } - - // Set File Logger Session UUID - auto avatarManager = DependencyManager::get(); - auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - if (avatarManager) { - workload::SpacePointer space = getEntities()->getWorkloadSpace(); - avatarManager->setSpace(space); - } - auto accountManager = DependencyManager::get(); - -#ifndef Q_OS_ANDROID - _logger->setSessionID(accountManager->getSessionID()); -#endif - - setCrashAnnotation("metaverse_session_id", accountManager->getSessionID().toString().toStdString()); - setCrashAnnotation("main_thread_id", std::to_string((size_t)QThread::currentThreadId())); - - if (steamClient) { - qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << steamClient->getSteamVRBuildID(); - } - setCrashAnnotation("steam", property(hifi::properties::STEAM).toBool() ? "1" : "0"); - - qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); - qCDebug(interfaceapp) << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; - qCDebug(interfaceapp) << "[VERSION] VERSION:" << BuildInfo::VERSION; - qCDebug(interfaceapp) << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING; - qCDebug(interfaceapp) << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES; -#if USE_STABLE_GLOBAL_SERVICES - qCDebug(interfaceapp) << "[VERSION] We will use STABLE global services."; -#else - qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services."; -#endif - - bool isStore = property(hifi::properties::OCULUS_STORE).toBool(); - - DependencyManager::get()->setLimitedCommerce(isStore); // Or we could make it a separate arg, or if either arg is set, etc. And should this instead by a hifi::properties? - - updateHeartbeat(); - - // setup a timer for domain-server check ins - QTimer* domainCheckInTimer = new QTimer(this); - QWeakPointer nodeListWeak = nodeList; - connect(domainCheckInTimer, &QTimer::timeout, [this, nodeListWeak] { - auto nodeList = nodeListWeak.lock(); - if (!isServerlessMode() && nodeList) { - nodeList->sendDomainServerCheckIn(); - } - }); - domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - connect(this, &QCoreApplication::aboutToQuit, [domainCheckInTimer] { - domainCheckInTimer->stop(); - domainCheckInTimer->deleteLater(); - }); - - { - auto audioIO = DependencyManager::get().data(); - audioIO->setPositionGetter([] { - auto avatarManager = DependencyManager::get(); - auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - - return myAvatar ? myAvatar->getPositionForAudio() : Vectors::ZERO; - }); - audioIO->setOrientationGetter([] { - auto avatarManager = DependencyManager::get(); - auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - - return myAvatar ? myAvatar->getOrientationForAudio() : Quaternions::IDENTITY; - }); - - recording::Frame::registerFrameHandler(AudioConstants::getAudioFrameName(), [&audioIO](recording::Frame::ConstPointer frame) { - audioIO->handleRecordedAudioInput(frame->data); - }); - - connect(audioIO, &AudioClient::inputReceived, [](const QByteArray& audio) { - static auto recorder = DependencyManager::get(); - if (recorder->isRecording()) { - static const recording::FrameType AUDIO_FRAME_TYPE = recording::Frame::registerFrameType(AudioConstants::getAudioFrameName()); - recorder->recordFrame(AUDIO_FRAME_TYPE, audio); - } - }); - audioIO->startThread(); - } - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - // Setup MessagesClient - DependencyManager::get()->startThread(); - - const DomainHandler& domainHandler = nodeList->getDomainHandler(); - - connect(&domainHandler, SIGNAL(domainURLChanged(QUrl)), SLOT(domainURLChanged(QUrl))); - connect(&domainHandler, SIGNAL(redirectToErrorDomainURL(QUrl)), SLOT(goToErrorDomainURL(QUrl))); - connect(&domainHandler, &DomainHandler::domainURLChanged, [](QUrl domainURL){ - setCrashAnnotation("domain", domainURL.toString().toStdString()); - }); - connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); - connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); - connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); - connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { - auto tabletScriptingInterface = DependencyManager::get(); - if (tabletScriptingInterface) { - tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); - } - auto entityScriptingInterface = DependencyManager::get(); - entityScriptingInterface->deleteEntity(getTabletScreenID()); - entityScriptingInterface->deleteEntity(getTabletHomeButtonID()); - entityScriptingInterface->deleteEntity(getTabletFrameID()); - _failedToConnectToEntityServer = false; - }); - - _entityServerConnectionTimer.setSingleShot(true); - connect(&_entityServerConnectionTimer, &QTimer::timeout, this, &Application::setFailedToConnectToEntityServer); - - connect(&domainHandler, &DomainHandler::connectedToDomain, this, [this]() { - if (!isServerlessMode()) { - _entityServerConnectionTimer.setInterval(ENTITY_SERVER_ADDED_TIMEOUT); - _entityServerConnectionTimer.start(); - _failedToConnectToEntityServer = false; - } - }); - connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); - - nodeList->getDomainHandler().setErrorDomainURL(QUrl(REDIRECT_HIFI_ADDRESS)); - - // We could clear ATP assets only when changing domains, but it's possible that the domain you are connected - // to has gone down and switched to a new content set, so when you reconnect the cached ATP assets will no longer be valid. - connect(&domainHandler, &DomainHandler::disconnectedFromDomain, DependencyManager::get().data(), &ScriptCache::clearATPScriptsFromCache); - - // 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_SECOND; - - auto discoverabilityManager = DependencyManager::get(); - connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); - connect(&locationUpdateTimer, &QTimer::timeout, - DependencyManager::get().data(), &AddressManager::storeCurrentAddress); - locationUpdateTimer.start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS); - - // if we get a domain change, immediately attempt update location in metaverse server - connect(&nodeList->getDomainHandler(), &DomainHandler::connectedToDomain, - discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); - - // send a location update immediately - discoverabilityManager->updateLocation(); - - connect(nodeList.data(), &NodeList::nodeAdded, this, &Application::nodeAdded); - connect(nodeList.data(), &NodeList::nodeKilled, this, &Application::nodeKilled); - connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); - connect(nodeList.data(), &NodeList::uuidChanged, myAvatar.get(), &MyAvatar::setSessionUUID); - connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); - connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); - - // you might think we could just do this in NodeList but we only want this connection for Interface - connect(&nodeList->getDomainHandler(), SIGNAL(limitOfSilentDomainCheckInsReached()), - nodeList.data(), SLOT(reset())); - - auto dialogsManager = DependencyManager::get(); -#if defined(Q_OS_ANDROID) - connect(accountManager.data(), &AccountManager::authRequired, this, []() { - auto addressManager = DependencyManager::get(); - AndroidHelper::instance().showLoginDialog(addressManager->currentAddress()); - }); -#else - connect(accountManager.data(), &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); -#endif - connect(accountManager.data(), &AccountManager::usernameChanged, this, &Application::updateWindowTitle); - - // set the account manager's root URL and trigger a login request if we don't have the access token - accountManager->setIsAgent(true); - accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); - - // use our MyAvatar position and quat for address manager path - addressManager->setPositionGetter([this]{ return getMyAvatar()->getWorldFeetPosition(); }); - addressManager->setOrientationGetter([this]{ return getMyAvatar()->getWorldOrientation(); }); - - connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle); - connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); - - connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateThreadPoolCount); - connect(this, &Application::activeDisplayPluginChanged, this, [](){ - qApp->setProperty(hifi::properties::HMD, qApp->isHMDMode()); - auto displayPlugin = qApp->getActiveDisplayPlugin(); - setCrashAnnotation("display_plugin", displayPlugin->getName().toStdString()); - setCrashAnnotation("hmd", displayPlugin->isHmd() ? "1" : "0"); - }); - connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); - connect(this, &Application::activeDisplayPluginChanged, this, [&](){ - if (getLoginDialogPoppedUp()) { - auto dialogsManager = DependencyManager::get(); - auto keyboard = DependencyManager::get(); - if (_firstRun.get()) { - // display mode changed. Don't allow auto-switch to work after this session. - _firstRun.set(false); - } - if (isHMDMode()) { - emit loginDialogFocusDisabled(); - dialogsManager->hideLoginDialog(); - createLoginDialog(); - } else { - DependencyManager::get()->deleteEntity(_loginDialogID); - _loginDialogID = QUuid(); - _loginStateManager.tearDown(); - dialogsManager->showLoginDialog(); - emit loginDialogFocusEnabled(); - } - } - }); - - // Save avatar location immediately after a teleport. - connect(myAvatar.get(), &MyAvatar::positionGoneTo, - DependencyManager::get().data(), &AddressManager::storeCurrentAddress); - - connect(myAvatar.get(), &MyAvatar::skeletonModelURLChanged, [](){ - QUrl avatarURL = qApp->getMyAvatar()->getSkeletonModelURL(); - setCrashAnnotation("avatar", avatarURL.toString().toStdString()); - }); - - - // Inititalize sample before registering - _sampleSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/sample.wav")); - - { - auto scriptEngines = DependencyManager::get().data(); - scriptEngines->registerScriptInitializer([this](ScriptEnginePointer engine) { - registerScriptEngineWithApplicationServices(engine); - }); - - connect(scriptEngines, &ScriptEngines::scriptCountChanged, this, [this] { - auto scriptEngines = DependencyManager::get(); - if (scriptEngines->getRunningScripts().isEmpty()) { - getMyAvatar()->clearScriptableSettings(); - } - }, Qt::QueuedConnection); - - connect(scriptEngines, &ScriptEngines::scriptsReloading, this, [this] { - getEntities()->reloadEntityScripts(); - loadAvatarScripts(getMyAvatar()->getScriptUrls()); - }, Qt::QueuedConnection); - - connect(scriptEngines, &ScriptEngines::scriptLoadError, - this, [](const QString& filename, const QString& error) { - OffscreenUi::asyncWarning(nullptr, "Error Loading Script", filename + " failed to load."); - }, Qt::QueuedConnection); - } - -#ifdef _WIN32 - WSADATA WsaData; - int wsaresult = WSAStartup(MAKEWORD(2, 2), &WsaData); -#endif - - // tell the NodeList instance who to tell the domain server we care about - nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer - << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer); - - // connect to the packet sent signal of the _entityEditSender - connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent); - connect(&_entityEditSender, &EntityEditPacketSender::addingEntityWithCertificate, this, &Application::addingEntityWithCertificate); - - QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads"); - bool success; - uint32_t concurrentDownloads = concurrentDownloadsStr.toUInt(&success); - if (!success) { - concurrentDownloads = MAX_CONCURRENT_RESOURCE_DOWNLOADS; - } - ResourceCache::setRequestLimit(concurrentDownloads); - - // perhaps override the avatar url. Since we will test later for validity - // we don't need to do so here. - QString avatarURL = getCmdOption(argc, constArgv, "--avatarURL"); - _avatarOverrideUrl = QUrl::fromUserInput(avatarURL); - - // If someone specifies both --avatarURL and --replaceAvatarURL, - // the replaceAvatarURL wins. So only set the _overrideUrl if this - // does have a non-empty string. - QString replaceURL = getCmdOption(argc, constArgv, "--replaceAvatarURL"); - if (!replaceURL.isEmpty()) { - _avatarOverrideUrl = QUrl::fromUserInput(replaceURL); - _saveAvatarOverrideUrl = true; - } - - _glWidget = new GLCanvas(); - getApplicationCompositor().setRenderingWidget(_glWidget); - _window->setCentralWidget(_glWidget); - - _window->restoreGeometry(); - _window->setVisible(true); - - _glWidget->setFocusPolicy(Qt::StrongFocus); - _glWidget->setFocus(); - - if (cmdOptionExists(argc, constArgv, "--system-cursor")) { - _preferredCursor.set(Cursor::Manager::getIconName(Cursor::Icon::SYSTEM)); - } - showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); - - // enable mouse tracking; otherwise, we only get drag events - _glWidget->setMouseTracking(true); - // Make sure the window is set to the correct size by processing the pending events - QCoreApplication::processEvents(); - - // Create the main thread context, the GPU backend - initializeGL(); - qCDebug(interfaceapp, "Initialized GL"); - - // Initialize the display plugin architecture - initializeDisplayPlugins(); - qCDebug(interfaceapp, "Initialized Display"); - - // An audio device changed signal received before the display plugins are set up will cause a crash, - // so we defer the setup of the `scripting::Audio` class until this point - { - auto audioScriptingInterface = DependencyManager::set(); - auto audioIO = DependencyManager::get().data(); - connect(audioIO, &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); - connect(audioIO, &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); - connect(audioIO, &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); - connect(audioIO, &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { - auto audioClient = DependencyManager::get(); - auto audioScriptingInterface = DependencyManager::get(); - auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getWorldPosition(); - float distance = glm::distance(myAvatarPosition, position); - - if (distance < radius) { - audioClient->setMuted(true); - audioScriptingInterface->environmentMuted(); - } - }); - connect(this, &Application::activeDisplayPluginChanged, - reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); - } - - // Create the rendering engine. This can be slow on some machines due to lots of - // GPU pipeline creation. - initializeRenderEngine(); - qCDebug(interfaceapp, "Initialized Render Engine."); - - // Overlays need to exist before we set the ContextOverlayInterface dependency - _overlays.init(); // do this before scripts load - DependencyManager::set(); - - // Initialize the user interface and menu system - // Needs to happen AFTER the render engine initialization to access its configuration - initializeUi(); - - init(); - qCDebug(interfaceapp, "init() complete."); - - // create thread for parsing of octree data independent of the main network and rendering threads - _octreeProcessor.initialize(_enableProcessOctreeThread); - connect(&_octreeProcessor, &OctreePacketProcessor::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); - _entityEditSender.initialize(_enableProcessOctreeThread); - - _idleLoopStdev.reset(); - - // update before the first render - update(0); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - static const QString TESTER = "HIFI_TESTER"; - bool isTester = false; -#if defined (Q_OS_ANDROID) - // Since we cannot set environment variables in Android we use a file presence - // to denote that this is a testing device - QFileInfo check_tester_file(TESTER_FILE); - isTester = check_tester_file.exists() && check_tester_file.isFile(); -#endif - - constexpr auto INSTALLER_INI_NAME = "installer.ini"; - auto iniPath = QDir(applicationDirPath()).filePath(INSTALLER_INI_NAME); - QFile installerFile { iniPath }; - std::unordered_map installerKeyValues; - if (installerFile.open(QIODevice::ReadOnly)) { - while (!installerFile.atEnd()) { - auto line = installerFile.readLine(); - if (!line.isEmpty()) { - auto index = line.indexOf("="); - if (index >= 0) { - installerKeyValues[line.mid(0, index).trimmed()] = line.mid(index + 1).trimmed(); - } - } - } - } - - // In practice we shouldn't run across installs that don't have a known installer type. - // Client or Client+Server installs should always have the installer.ini next to their - // respective interface.exe, and Steam installs will be detected as such. If a user were - // to delete the installer.ini, though, and as an example, we won't know the context of the - // original install. - constexpr auto INSTALLER_KEY_TYPE = "type"; - constexpr auto INSTALLER_KEY_CAMPAIGN = "campaign"; - constexpr auto INSTALLER_TYPE_UNKNOWN = "unknown"; - constexpr auto INSTALLER_TYPE_STEAM = "steam"; - - auto typeIt = installerKeyValues.find(INSTALLER_KEY_TYPE); - QString installerType = INSTALLER_TYPE_UNKNOWN; - if (typeIt == installerKeyValues.end()) { - if (property(hifi::properties::STEAM).toBool()) { - installerType = INSTALLER_TYPE_STEAM; - } - } else { - installerType = typeIt->second; - } - - auto campaignIt = installerKeyValues.find(INSTALLER_KEY_CAMPAIGN); - QString installerCampaign = campaignIt != installerKeyValues.end() ? campaignIt->second : ""; - - qDebug() << "Detected installer type:" << installerType; - qDebug() << "Detected installer campaign:" << installerCampaign; - - auto& userActivityLogger = UserActivityLogger::getInstance(); - if (userActivityLogger.isEnabled()) { - // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. - // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. - static const QString TESTER = "HIFI_TESTER"; - auto gpuIdent = GPUIdent::getInstance(); - auto glContextData = getGLContextData(); - QJsonObject properties = { - { "version", applicationVersion() }, - { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) || isTester }, - { "installer_campaign", installerCampaign }, - { "installer_type", installerType }, - { "build_type", BuildInfo::BUILD_TYPE_STRING }, - { "previousSessionCrashed", _previousSessionCrashed }, - { "previousSessionRuntime", sessionRunTime.get() }, - { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, - { "kernel_type", QSysInfo::kernelType() }, - { "kernel_version", QSysInfo::kernelVersion() }, - { "os_type", QSysInfo::productType() }, - { "os_version", QSysInfo::productVersion() }, - { "gpu_name", gpuIdent->getName() }, - { "gpu_driver", gpuIdent->getDriver() }, - { "gpu_memory", static_cast(gpuIdent->getMemory()) }, - { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, - { "gl_version", glContextData["version"] }, - { "gl_vender", glContextData["vendor"] }, - { "gl_sl_version", glContextData["sl_version"] }, - { "gl_renderer", glContextData["renderer"] }, - { "ideal_thread_count", QThread::idealThreadCount() } - }; - auto macVersion = QSysInfo::macVersion(); - if (macVersion != QSysInfo::MV_None) { - properties["os_osx_version"] = QSysInfo::macVersion(); - } - auto windowsVersion = QSysInfo::windowsVersion(); - if (windowsVersion != QSysInfo::WV_None) { - properties["os_win_version"] = QSysInfo::windowsVersion(); - } - - ProcessorInfo procInfo; - if (getProcessorInfo(procInfo)) { - properties["processor_core_count"] = procInfo.numProcessorCores; - properties["logical_processor_count"] = procInfo.numLogicalProcessors; - properties["processor_l1_cache_count"] = procInfo.numProcessorCachesL1; - properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; - properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; - } - - properties["first_run"] = _firstRun.get(); - - // add the user's machine ID to the launch event - QString machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); - properties["machine_fingerprint"] = machineFingerPrint; - - userActivityLogger.logAction("launch", properties); - } - - _entityEditSender.setMyAvatar(myAvatar.get()); - - // The entity octree will have to know about MyAvatar for the parentJointName import - getEntities()->getTree()->setMyAvatar(myAvatar); - _entityClipboard->setMyAvatar(myAvatar); - - // For now we're going to set the PPS for outbound packets to be super high, this is - // probably not the right long term solution. But for now, we're going to do this to - // allow you to move an entity around in your hand - _entityEditSender.setPacketsPerSecond(3000); // super high!! - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - connect(this, SIGNAL(aboutToQuit()), this, SLOT(onAboutToQuit())); - - // FIXME -- I'm a little concerned about this. - connect(myAvatar->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded, - this, &Application::checkSkeleton, Qt::QueuedConnection); - - // Setup the userInputMapper with the actions - auto userInputMapper = DependencyManager::get(); - connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { - using namespace controller; - auto tabletScriptingInterface = DependencyManager::get(); - { - auto actionEnum = static_cast(action); - int key = Qt::Key_unknown; - static int lastKey = Qt::Key_unknown; - bool navAxis = false; - switch (actionEnum) { - case Action::UI_NAV_VERTICAL: - navAxis = true; - if (state > 0.0f) { - key = Qt::Key_Up; - } else if (state < 0.0f) { - key = Qt::Key_Down; - } - break; - - case Action::UI_NAV_LATERAL: - navAxis = true; - if (state > 0.0f) { - key = Qt::Key_Right; - } else if (state < 0.0f) { - key = Qt::Key_Left; - } - break; - - case Action::UI_NAV_GROUP: - navAxis = true; - if (state > 0.0f) { - key = Qt::Key_Tab; - } else if (state < 0.0f) { - key = Qt::Key_Backtab; - } - break; - - case Action::UI_NAV_BACK: - key = Qt::Key_Escape; - break; - - case Action::UI_NAV_SELECT: - key = Qt::Key_Return; - break; - default: - break; - } - - auto window = tabletScriptingInterface->getTabletWindow(); - if (navAxis && window) { - if (lastKey != Qt::Key_unknown) { - QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); - sendEvent(window, &event); - lastKey = Qt::Key_unknown; - } - - if (key != Qt::Key_unknown) { - QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); - sendEvent(window, &event); - tabletScriptingInterface->processEvent(&event); - lastKey = key; - } - } else if (key != Qt::Key_unknown && window) { - if (state) { - QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); - sendEvent(window, &event); - tabletScriptingInterface->processEvent(&event); - } else { - QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); - sendEvent(window, &event); - } - return; - } - } - - if (action == controller::toInt(controller::Action::RETICLE_CLICK)) { - auto reticlePos = getApplicationCompositor().getReticlePosition(); - QPoint localPos(reticlePos.x, reticlePos.y); // both hmd and desktop already handle this in our coordinates. - if (state) { - QMouseEvent mousePress(QEvent::MouseButtonPress, localPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); - sendEvent(_glWidget, &mousePress); - _reticleClickPressed = true; - } else { - QMouseEvent mouseRelease(QEvent::MouseButtonRelease, localPos, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); - sendEvent(_glWidget, &mouseRelease); - _reticleClickPressed = false; - } - return; // nothing else to do - } - - if (state) { - if (action == controller::toInt(controller::Action::TOGGLE_MUTE)) { - auto audioClient = DependencyManager::get(); - audioClient->setMuted(!audioClient->isMuted()); - } else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) { - cycleCamera(); - } else if (action == controller::toInt(controller::Action::CONTEXT_MENU) && !isInterstitialMode()) { - toggleTabletUI(); - } else if (action == controller::toInt(controller::Action::RETICLE_X)) { - auto oldPos = getApplicationCompositor().getReticlePosition(); - getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); - } else if (action == controller::toInt(controller::Action::RETICLE_Y)) { - auto oldPos = getApplicationCompositor().getReticlePosition(); - getApplicationCompositor().setReticlePosition({ oldPos.x, oldPos.y + state }); - } else if (action == controller::toInt(controller::Action::TOGGLE_OVERLAY)) { - toggleOverlays(); - } - } - }); - - _applicationStateDevice = userInputMapper->getStateDevice(); - - _applicationStateDevice->setInputVariant(STATE_IN_HMD, []() -> float { - return qApp->isHMDMode() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_FULL_SCREEN_MIRROR, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_MIRROR ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_FIRST_PERSON, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_FIRST_PERSON ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_THIRD_PERSON, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_THIRD_PERSON ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_ENTITY, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_ENTITY ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_INDEPENDENT, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_INDEPENDENT ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { - return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { - return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; - }); - - _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { - return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_NAV_FOCUSED, []() -> float { - auto offscreenUi = getOffscreenUI(); - return offscreenUi ? (offscreenUi->navigationFocused() ? 1 : 0) : 0; - }); - _applicationStateDevice->setInputVariant(STATE_PLATFORM_WINDOWS, []() -> float { -#if defined(Q_OS_WIN) - return 1; -#else - return 0; -#endif - }); - _applicationStateDevice->setInputVariant(STATE_PLATFORM_MAC, []() -> float { -#if defined(Q_OS_MAC) - return 1; -#else - return 0; -#endif - }); - _applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float { -#if defined(Q_OS_ANDROID) - return 1 ; -#else - return 0; -#endif - }); - - - // Setup the _keyboardMouseDevice, _touchscreenDevice, _touchscreenVirtualPadDevice and the user input mapper with the default bindings - userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); - // if the _touchscreenDevice is not supported it will not be registered - if (_touchscreenDevice) { - userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); - } - if (_touchscreenVirtualPadDevice) { - userInputMapper->registerDevice(_touchscreenVirtualPadDevice->getInputDevice()); - } - - QString scriptsSwitch = QString("--").append(SCRIPTS_SWITCH); - _defaultScriptsLocation = getCmdOption(argc, constArgv, scriptsSwitch.toStdString().c_str()); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - loadSettings(); - - updateVerboseLogging(); - - // Now that we've loaded the menu and thus switched to the previous display plugin - // we can unlock the desktop repositioning code, since all the positions will be - // relative to the desktop size for this plugin - auto offscreenUi = getOffscreenUI(); - connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() { - auto offscreenUi = getOffscreenUI(); - auto desktop = offscreenUi->getDesktop(); - if (desktop) { - desktop->setProperty("repositionLocked", false); - } - }); - - connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) - // Do not show login dialog if requested not to on the command line - QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); - int index = arguments().indexOf(hifiNoLoginCommandLineKey); - if (index != -1) { - resumeAfterLoginDialogActionTaken(); - return; - } - - showLoginScreen(); -#else - resumeAfterLoginDialogActionTaken(); -#endif - }); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - QTimer* settingsTimer = new QTimer(); - moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{ - // This needs to run on the settings thread, so we need to pass the `settingsTimer` as the - // receiver object, otherwise it will run on the application thread and trigger a warning - // about trying to kill the timer on the main thread. - connect(qApp, &Application::beforeAboutToQuit, settingsTimer, [this, settingsTimer]{ - // Disconnect the signal from the save settings - QObject::disconnect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); - // Stop the settings timer - settingsTimer->stop(); - // Delete it (this will trigger the thread destruction - settingsTimer->deleteLater(); - // Mark the settings thread as finished, so we know we can safely save in the main application - // shutdown code - _settingsGuard.trigger(); - }); - - int SAVE_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; // Let's save every seconds for now - settingsTimer->setSingleShot(false); - settingsTimer->setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable - QObject::connect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); - settingsTimer->start(); - }, QThread::LowestPriority); - - if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person. - } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } - - { - auto audioIO = DependencyManager::get().data(); - // set the local loopback interface for local sounds - AudioInjector::setLocalAudioInterface(audioIO); - auto audioScriptingInterface = DependencyManager::get(); - audioScriptingInterface->setLocalAudioInterface(audioIO); - connect(audioIO, &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); - connect(audioIO, &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); - connect(audioIO, &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); - } - - this->installEventFilter(this); - - - -#ifdef HAVE_DDE - auto ddeTracker = DependencyManager::get(); - ddeTracker->init(); - connect(ddeTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); -#endif - -#ifdef HAVE_IVIEWHMD - auto eyeTracker = DependencyManager::get(); - eyeTracker->init(); - setActiveEyeTracker(); -#endif - - // If launched from Steam, let it handle updates - const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater"; - bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1; - bool buildCanUpdate = BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable - || BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Master; - if (!noUpdater && buildCanUpdate) { - constexpr auto INSTALLER_TYPE_CLIENT_ONLY = "client_only"; - - auto applicationUpdater = DependencyManager::set(); - - AutoUpdater::InstallerType type = installerType == INSTALLER_TYPE_CLIENT_ONLY - ? AutoUpdater::InstallerType::CLIENT_ONLY : AutoUpdater::InstallerType::FULL; - - applicationUpdater->setInstallerType(type); - applicationUpdater->setInstallerCampaign(installerCampaign); - connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog); - applicationUpdater->checkForUpdate(); - } - - Menu::getInstance()->setIsOptionChecked(MenuOption::ActionMotorControl, true); - -// FIXME spacemouse code still needs cleanup -#if 0 - // the 3Dconnexion device wants to be initialized after a window is displayed. - SpacemouseManager::getInstance().init(); -#endif - - // If the user clicks on an object, we will check that it's a web surface, and if so, set the focus to it - auto pointerManager = DependencyManager::get(); - auto keyboardFocusOperator = [this](const QUuid& id, const PointerEvent& event) { - if (event.shouldFocus()) { - auto keyboard = DependencyManager::get(); - if (getEntities()->wantsKeyboardFocus(id)) { - setKeyboardFocusEntity(id); - } else if (!keyboard->containsID(id)) { // FIXME: this is a hack to make the keyboard work for now, since the keys would otherwise steal focus - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - } - } - }; - connect(pointerManager.data(), &PointerManager::triggerBeginEntity, keyboardFocusOperator); - connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, keyboardFocusOperator); - - auto entityScriptingInterface = DependencyManager::get(); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityItemID) { - if (entityItemID == _keyboardFocusedEntity.get()) { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - } - }, Qt::QueuedConnection); - - EntityTreeRenderer::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) { - if (_aboutToQuit) { - return false; - } - - auto renderable = getEntities()->renderableForEntityId(entityID); - if (renderable) { - renderable->addMaterial(material, parentMaterialName); - return true; - } - - return false; - }); - EntityTreeRenderer::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) { - if (_aboutToQuit) { - return false; - } - - auto renderable = getEntities()->renderableForEntityId(entityID); - if (renderable) { - renderable->removeMaterial(material, parentMaterialName); - return true; - } - - return false; - }); - - EntityTreeRenderer::setAddMaterialToAvatarOperator([](const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) { - auto avatarManager = DependencyManager::get(); - auto avatar = avatarManager->getAvatarBySessionID(avatarID); - if (avatar) { - avatar->addMaterial(material, parentMaterialName); - return true; - } - return false; - }); - EntityTreeRenderer::setRemoveMaterialFromAvatarOperator([](const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) { - auto avatarManager = DependencyManager::get(); - auto avatar = avatarManager->getAvatarBySessionID(avatarID); - if (avatar) { - avatar->removeMaterial(material, parentMaterialName); - return true; - } - return false; - }); - - EntityTree::setGetEntityObjectOperator([this](const QUuid& id) -> QObject* { - auto entities = getEntities(); - if (auto entity = entities->renderableForEntityId(id)) { - return qobject_cast(entity.get()); - } - return nullptr; - }); - - EntityTree::setTextSizeOperator([this](const QUuid& id, const QString& text) { - auto entities = getEntities(); - if (auto entity = entities->renderableForEntityId(id)) { - if (auto renderable = std::dynamic_pointer_cast(entity)) { - return renderable->textSize(text); - } - } - return QSizeF(0.0f, 0.0f); - }); - - connect(this, &Application::aboutToQuit, [this]() { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - }); - - // Add periodic checks to send user activity data - static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; - static int NEARBY_AVATAR_RADIUS_METERS = 10; - - // setup the stats interval depending on if the 1s faster hearbeat was requested - static const QString FAST_STATS_ARG = "--fast-heartbeat"; - static int SEND_STATS_INTERVAL_MS = arguments().indexOf(FAST_STATS_ARG) != -1 ? 1000 : 10000; - - static glm::vec3 lastAvatarPosition = myAvatar->getWorldPosition(); - static glm::mat4 lastHMDHeadPose = getHMDSensorPose(); - static controller::Pose lastLeftHandPose = myAvatar->getLeftHandPose(); - static controller::Pose lastRightHandPose = myAvatar->getRightHandPose(); - - // Periodically send fps as a user activity event - QTimer* sendStatsTimer = new QTimer(this); - sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); // 10s, Qt::CoarseTimer acceptable - connect(sendStatsTimer, &QTimer::timeout, this, [this]() { - - QJsonObject properties = {}; - MemoryInfo memInfo; - if (getMemoryInfo(memInfo)) { - properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); - properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); - properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); - } - - // content location and build info - useful for filtering stats - auto addressManager = DependencyManager::get(); - auto currentDomain = addressManager->currentShareableAddress(true).toString(); // domain only - auto currentPath = addressManager->currentPath(true); // with orientation - properties["current_domain"] = currentDomain; - properties["current_path"] = currentPath; - properties["build_version"] = BuildInfo::VERSION; - - auto displayPlugin = qApp->getActiveDisplayPlugin(); - - properties["render_rate"] = getRenderLoopRate(); - properties["target_render_rate"] = getTargetRenderFrameRate(); - properties["present_rate"] = displayPlugin->presentRate(); - properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); - properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); - properties["stutter_rate"] = displayPlugin->stutterRate(); - properties["game_rate"] = getGameLoopRate(); - properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); - properties["hardware_stats"] = displayPlugin->getHardwareStats(); - - // deadlock watchdog related stats - properties["deadlock_watchdog_maxElapsed"] = (int)DeadlockWatchdogThread::_maxElapsed; - properties["deadlock_watchdog_maxElapsedAverage"] = (int)DeadlockWatchdogThread::_maxElapsedAverage; - - auto nodeList = DependencyManager::get(); - properties["packet_rate_in"] = nodeList->getInboundPPS(); - properties["packet_rate_out"] = nodeList->getOutboundPPS(); - properties["kbps_in"] = nodeList->getInboundKbps(); - properties["kbps_out"] = nodeList->getOutboundKbps(); - - SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); - SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); - SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); - SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); - SharedNodePointer messagesMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); - properties["entity_ping"] = entityServerNode ? entityServerNode->getPingMs() : -1; - properties["audio_ping"] = audioMixerNode ? audioMixerNode->getPingMs() : -1; - properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; - properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1; - properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; - properties["atp_in_kbps"] = assetServerNode ? assetServerNode->getInboundKbps() : 0.0f; - - auto loadingRequests = ResourceCache::getLoadingRequests(); - - QJsonArray loadingRequestsStats; - for (const auto& request : loadingRequests) { - QJsonObject requestStats; - requestStats["filename"] = request->getURL().fileName(); - requestStats["received"] = request->getBytesReceived(); - requestStats["total"] = request->getBytesTotal(); - requestStats["attempts"] = (int)request->getDownloadAttempts(); - loadingRequestsStats.append(requestStats); - } - - properties["active_downloads"] = loadingRequests.size(); - properties["pending_downloads"] = (int)ResourceCache::getPendingRequestCount(); - properties["active_downloads_details"] = loadingRequestsStats; - - auto statTracker = DependencyManager::get(); - - properties["processing_resources"] = statTracker->getStat("Processing").toInt(); - properties["pending_processing_resources"] = statTracker->getStat("PendingProcessing").toInt(); - - QJsonObject startedRequests; - startedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_STARTED).toInt(); - startedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_STARTED).toInt(); - startedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_STARTED).toInt(); - startedRequests["total"] = startedRequests["atp"].toInt() + startedRequests["http"].toInt() - + startedRequests["file"].toInt(); - properties["started_requests"] = startedRequests; - - QJsonObject successfulRequests; - successfulRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_SUCCESS).toInt(); - successfulRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_SUCCESS).toInt(); - successfulRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_SUCCESS).toInt(); - successfulRequests["total"] = successfulRequests["atp"].toInt() + successfulRequests["http"].toInt() - + successfulRequests["file"].toInt(); - properties["successful_requests"] = successfulRequests; - - QJsonObject failedRequests; - failedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_FAILED).toInt(); - failedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_FAILED).toInt(); - failedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_FAILED).toInt(); - failedRequests["total"] = failedRequests["atp"].toInt() + failedRequests["http"].toInt() - + failedRequests["file"].toInt(); - properties["failed_requests"] = failedRequests; - - QJsonObject cacheRequests; - cacheRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_CACHE).toInt(); - cacheRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_CACHE).toInt(); - cacheRequests["total"] = cacheRequests["atp"].toInt() + cacheRequests["http"].toInt(); - properties["cache_requests"] = cacheRequests; - - QJsonObject atpMappingRequests; - atpMappingRequests["started"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_STARTED).toInt(); - atpMappingRequests["failed"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_FAILED).toInt(); - atpMappingRequests["successful"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_SUCCESS).toInt(); - properties["atp_mapping_requests"] = atpMappingRequests; - - properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; - - QJsonObject bytesDownloaded; - auto atpBytes = statTracker->getStat(STAT_ATP_RESOURCE_TOTAL_BYTES).toLongLong(); - auto httpBytes = statTracker->getStat(STAT_HTTP_RESOURCE_TOTAL_BYTES).toLongLong(); - auto fileBytes = statTracker->getStat(STAT_FILE_RESOURCE_TOTAL_BYTES).toLongLong(); - bytesDownloaded["atp"] = atpBytes; - bytesDownloaded["http"] = httpBytes; - bytesDownloaded["file"] = fileBytes; - bytesDownloaded["total"] = atpBytes + httpBytes + fileBytes; - properties["bytes_downloaded"] = bytesDownloaded; - - auto myAvatar = getMyAvatar(); - glm::vec3 avatarPosition = myAvatar->getWorldPosition(); - properties["avatar_has_moved"] = lastAvatarPosition != avatarPosition; - lastAvatarPosition = avatarPosition; - - auto entityScriptingInterface = DependencyManager::get(); - auto entityActivityTracking = entityScriptingInterface->getActivityTracking(); - entityScriptingInterface->resetActivityTracking(); - properties["added_entity_cnt"] = entityActivityTracking.addedEntityCount; - properties["deleted_entity_cnt"] = entityActivityTracking.deletedEntityCount; - properties["edited_entity_cnt"] = entityActivityTracking.editedEntityCount; - - NodeToOctreeSceneStats* octreeServerSceneStats = getOcteeSceneStats(); - unsigned long totalServerOctreeElements = 0; - for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) { - totalServerOctreeElements += i->second.getTotalElements(); - } - - properties["local_octree_elements"] = (qint64) OctreeElement::getInternalNodeCount(); - properties["server_octree_elements"] = (qint64) totalServerOctreeElements; - - properties["active_display_plugin"] = getActiveDisplayPlugin()->getName(); - properties["using_hmd"] = isHMDMode(); - - _autoSwitchDisplayModeSupportedHMDPlugin = nullptr; - foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { - if (displayPlugin->isHmd() && - displayPlugin->getSupportsAutoSwitch()) { - _autoSwitchDisplayModeSupportedHMDPlugin = displayPlugin; - _autoSwitchDisplayModeSupportedHMDPluginName = - _autoSwitchDisplayModeSupportedHMDPlugin->getName(); - _previousHMDWornStatus = - _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); - break; - } - } - - if (_autoSwitchDisplayModeSupportedHMDPlugin) { - if (getActiveDisplayPlugin() != _autoSwitchDisplayModeSupportedHMDPlugin && - !_autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { - startHMDStandBySession(); - } - // Poll periodically to check whether the user has worn HMD or not. Switch Display mode accordingly. - // If the user wears HMD then switch to VR mode. If the user removes HMD then switch to Desktop mode. - QTimer* autoSwitchDisplayModeTimer = new QTimer(this); - connect(autoSwitchDisplayModeTimer, SIGNAL(timeout()), this, SLOT(switchDisplayMode())); - autoSwitchDisplayModeTimer->start(INTERVAL_TO_CHECK_HMD_WORN_STATUS); - } - - auto glInfo = getGLContextData(); - properties["gl_info"] = glInfo; - properties["gpu_used_memory"] = (int)BYTES_TO_MB(gpu::Context::getUsedGPUMemSize()); - properties["gpu_free_memory"] = (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemSize()); - properties["gpu_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerGPUAverage()); - properties["batch_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerBatchAverage()); - properties["ideal_thread_count"] = QThread::idealThreadCount(); - - auto hmdHeadPose = getHMDSensorPose(); - properties["hmd_head_pose_changed"] = isHMDMode() && (hmdHeadPose != lastHMDHeadPose); - lastHMDHeadPose = hmdHeadPose; - - auto leftHandPose = myAvatar->getLeftHandPose(); - auto rightHandPose = myAvatar->getRightHandPose(); - // controller::Pose considers two poses to be different if either are invalid. In our case, we actually - // want to consider the pose to be unchanged if it was invalid and still is invalid, so we check that first. - properties["hand_pose_changed"] = - ((leftHandPose.valid || lastLeftHandPose.valid) && (leftHandPose != lastLeftHandPose)) - || ((rightHandPose.valid || lastRightHandPose.valid) && (rightHandPose != lastRightHandPose)); - lastLeftHandPose = leftHandPose; - lastRightHandPose = rightHandPose; - - UserActivityLogger::getInstance().logAction("stats", properties); - }); - sendStatsTimer->start(); - - // Periodically check for count of nearby avatars - static int lastCountOfNearbyAvatars = -1; - QTimer* checkNearbyAvatarsTimer = new QTimer(this); - checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); // 10 seconds, Qt::CoarseTimer ok - connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, []() { - auto avatarManager = DependencyManager::get(); - int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getWorldPosition(), - NEARBY_AVATAR_RADIUS_METERS) - 1; - if (nearbyAvatars != lastCountOfNearbyAvatars) { - lastCountOfNearbyAvatars = nearbyAvatars; - UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); - } - }); - checkNearbyAvatarsTimer->start(); - - // Track user activity event when we receive a mute packet - auto onMutedByMixer = []() { - UserActivityLogger::getInstance().logAction("received_mute_packet"); - }; - connect(DependencyManager::get().data(), &AudioClient::mutedByMixer, this, onMutedByMixer); - - // Track when the address bar is opened - auto onAddressBarShown = [this]() { - // Record time - UserActivityLogger::getInstance().logAction("opened_address_bar", { { "uptime_ms", _sessionRunTimer.elapsed() } }); - }; - connect(DependencyManager::get().data(), &DialogsManager::addressBarShown, this, onAddressBarShown); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender(); - EntityEditPacketSender* entityPacketSender = static_cast(packetSender); - entityPacketSender->setMyAvatar(myAvatar.get()); - - connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); - connect(_window, SIGNAL(windowMinimizedChanged(bool)), this, SLOT(windowMinimizedChanged(bool))); - qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); - - EntityTreeRenderer::setEntitiesShouldFadeFunction([this]() { - SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); - return entityServerNode && !isPhysicsEnabled(); - }); - - _snapshotSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/snapshot/snap.wav")); - - // Monitor model assets (e.g., from Clara.io) added to the world that may need resizing. - static const int ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS = 1000; - _addAssetToWorldResizeTimer.setInterval(ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable - connect(&_addAssetToWorldResizeTimer, &QTimer::timeout, this, &Application::addAssetToWorldCheckModelSize); - - // Auto-update and close adding asset to world info message box. - static const int ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS = 5000; - _addAssetToWorldInfoTimer.setInterval(ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS); // 5s, Qt::CoarseTimer acceptable - _addAssetToWorldInfoTimer.setSingleShot(true); - connect(&_addAssetToWorldInfoTimer, &QTimer::timeout, this, &Application::addAssetToWorldInfoTimeout); - static const int ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS = 8000; - _addAssetToWorldErrorTimer.setInterval(ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS); // 8s, Qt::CoarseTimer acceptable - _addAssetToWorldErrorTimer.setSingleShot(true); - connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout); - - connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose); - connect(&domainHandler, &DomainHandler::domainURLChanged, this, &Application::addAssetToWorldMessageClose); - connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &Application::addAssetToWorldMessageClose); - - updateSystemTabletMode(); - - connect(&_myCamera, &Camera::modeUpdated, this, &Application::cameraModeChanged); - - DependencyManager::get()->setShouldPickHUDOperator([]() { return DependencyManager::get()->isHMDMode(); }); - DependencyManager::get()->setCalculatePos2DFromHUDOperator([this](const glm::vec3& intersection) { - const glm::vec2 MARGIN(25.0f); - glm::vec2 maxPos = _controllerScriptingInterface->getViewportDimensions() - MARGIN; - glm::vec2 pos2D = DependencyManager::get()->overlayFromWorldPoint(intersection); - return glm::max(MARGIN, glm::min(pos2D, maxPos)); - }); - - // Setup the mouse ray pick and related operators - { - auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_LOCAL_ENTITIES()), 0.0f, true); - mouseRayPick->parentTransform = std::make_shared(); - mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE); - auto mouseRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, mouseRayPick); - DependencyManager::get()->setMouseRayPickID(mouseRayPickID); - } - DependencyManager::get()->setMouseRayPickResultOperator([](unsigned int rayPickID) { - RayToEntityIntersectionResult entityResult; - entityResult.intersects = false; - auto pickResult = DependencyManager::get()->getPrevPickResultTyped(rayPickID); - if (pickResult) { - entityResult.intersects = pickResult->type != IntersectionType::NONE; - if (entityResult.intersects) { - entityResult.intersection = pickResult->intersection; - entityResult.distance = pickResult->distance; - entityResult.surfaceNormal = pickResult->surfaceNormal; - entityResult.entityID = pickResult->objectID; - entityResult.extraInfo = pickResult->extraInfo; - } - } - return entityResult; - }); - DependencyManager::get()->setSetPrecisionPickingOperator([](unsigned int rayPickID, bool value) { - DependencyManager::get()->setPrecisionPicking(rayPickID, value); - }); - - EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { - if (billboardMode == BillboardMode::YAW) { - //rotate about vertical to face the camera - glm::vec3 dPosition = frustumPos - position; - // If x and z are 0, atan(x, z) is undefined, so default to 0 degrees - float yawRotation = dPosition.x == 0.0f && dPosition.z == 0.0f ? 0.0f : glm::atan(dPosition.x, dPosition.z); - return glm::quat(glm::vec3(0.0f, yawRotation, 0.0f)); - } else if (billboardMode == BillboardMode::FULL) { - // use the referencial from the avatar, y isn't always up - glm::vec3 avatarUP = DependencyManager::get()->getMyAvatar()->getWorldOrientation() * Vectors::UP; - // check to see if glm::lookAt will work / using glm::lookAt variable name - glm::highp_vec3 s(glm::cross(position - frustumPos, avatarUP)); - - // make sure s is not NaN for any component - if (glm::length2(s) > 0.0f) { - return glm::conjugate(glm::toQuat(glm::lookAt(frustumPos, position, avatarUP))); - } - } - return rotation; - }); - EntityItem::setPrimaryViewFrustumPositionOperator([this]() { - ViewFrustum viewFrustum; - copyViewFrustum(viewFrustum); - return viewFrustum.getPosition(); - }); - - render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { - bool isTablet = url == TabletScriptingInterface::QML; - if (htmlContent) { - webSurface = DependencyManager::get()->acquire(render::entities::WebEntityRenderer::QML); - cachedWebSurface = true; - auto rootItemLoadedFunctor = [url, webSurface] { - webSurface->getRootItem()->setProperty(render::entities::WebEntityRenderer::URL_PROPERTY, url); - }; - if (webSurface->getRootItem()) { - rootItemLoadedFunctor(); - } else { - QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); - } - auto surfaceContext = webSurface->getSurfaceContext(); - surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); - } else { - // FIXME: the tablet should use the OffscreenQmlSurfaceCache - webSurface = QSharedPointer(new OffscreenQmlSurface(), [](OffscreenQmlSurface* webSurface) { - AbstractViewStateInterface::instance()->sendLambdaEvent([webSurface] { - // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown - // if the application has already stopped its event loop, delete must be explicit - delete webSurface; - }); - }); - auto rootItemLoadedFunctor = [webSurface, url, isTablet] { - Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString()); - }; - if (webSurface->getRootItem()) { - rootItemLoadedFunctor(); - } else { - QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); - } - webSurface->load(url); - cachedWebSurface = false; - } - const uint8_t DEFAULT_MAX_FPS = 10; - const uint8_t TABLET_FPS = 90; - webSurface->setMaxFps(isTablet ? TABLET_FPS : DEFAULT_MAX_FPS); - }); - render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([this](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { - QQuickItem* rootItem = webSurface->getRootItem(); - - // Fix for crash in QtWebEngineCore when rapidly switching domains - // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. - if (rootItem && !cachedWebSurface) { - // stop loading - QMetaObject::invokeMethod(rootItem, "stop"); - } - - webSurface->pause(); - - for (auto& connection : connections) { - QObject::disconnect(connection); - } - connections.clear(); - - // If the web surface was fetched out of the cache, release it back into the cache - if (cachedWebSurface) { - // If it's going back into the cache make sure to explicitly set the URL to a blank page - // in order to stop any resource consumption or audio related to the page. - if (rootItem) { - rootItem->setProperty("url", "about:blank"); - } - auto offscreenCache = DependencyManager::get(); - if (offscreenCache) { - offscreenCache->release(render::entities::WebEntityRenderer::QML, webSurface); - } - cachedWebSurface = false; - } - webSurface.reset(); - }); - - // Preload Tablet sounds - DependencyManager::get()->setEntityTree(qApp->getEntities()->getTree()); - DependencyManager::get()->preloadSounds(); - DependencyManager::get()->createKeyboard(); - - _pendingIdleEvent = false; - _graphicsEngine.startup(); - - qCDebug(interfaceapp) << "Metaverse session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID()); - -#if defined(Q_OS_ANDROID) - connect(&AndroidHelper::instance(), &AndroidHelper::beforeEnterBackground, this, &Application::beforeEnterBackground); - connect(&AndroidHelper::instance(), &AndroidHelper::enterBackground, this, &Application::enterBackground); - connect(&AndroidHelper::instance(), &AndroidHelper::enterForeground, this, &Application::enterForeground); - connect(&AndroidHelper::instance(), &AndroidHelper::toggleAwayMode, this, &Application::toggleAwayMode); - AndroidHelper::instance().notifyLoadComplete(); -#endif - pauseUntilLoginDetermined(); -} - -void Application::updateVerboseLogging() { - auto menu = Menu::getInstance(); - if (!menu) { - return; - } - bool enable = menu->isOptionChecked(MenuOption::VerboseLogging); - - QString rules = - "hifi.*.info=%1\n" - "hifi.audio-stream.debug=false\n" - "hifi.audio-stream.info=false"; - rules = rules.arg(enable ? "true" : "false"); - QLoggingCategory::setFilterRules(rules); -} - -void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { - DomainHandler::ConnectionRefusedReason reasonCode = static_cast(reasonCodeInt); - - if (reasonCode == DomainHandler::ConnectionRefusedReason::TooManyUsers && !extraInfo.isEmpty()) { - DependencyManager::get()->handleLookupString(extraInfo); - return; - } - - switch (reasonCode) { - case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: - case DomainHandler::ConnectionRefusedReason::TooManyUsers: - case DomainHandler::ConnectionRefusedReason::Unknown: { - QString message = "Unable to connect to the location you are visiting.\n"; - message += reasonMessage; - OffscreenUi::asyncWarning("", message); - getMyAvatar()->setWorldVelocity(glm::vec3(0.0f)); - break; - } - default: - // nothing to do. - break; - } -} - -QString Application::getUserAgent() { - if (QThread::currentThread() != thread()) { - QString userAgent; - - BLOCKING_INVOKE_METHOD(this, "getUserAgent", Q_RETURN_ARG(QString, userAgent)); - - return userAgent; - } - - QString userAgent = "Mozilla/5.0 (HighFidelityInterface/" + BuildInfo::VERSION + "; " - + QSysInfo::productType() + " " + QSysInfo::productVersion() + ")"; - - auto formatPluginName = [](QString name) -> QString { return name.trimmed().replace(" ", "-"); }; - - // For each plugin, add to userAgent - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - for (auto& dp : displayPlugins) { - if (dp->isActive() && dp->isHmd()) { - userAgent += " " + formatPluginName(dp->getName()); - } - } - auto inputPlugins= PluginManager::getInstance()->getInputPlugins(); - for (auto& ip : inputPlugins) { - if (ip->isActive()) { - userAgent += " " + formatPluginName(ip->getName()); - } - } - // for codecs, we include all of them, even if not active - auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); - for (auto& cp : codecPlugins) { - userAgent += " " + formatPluginName(cp->getName()); - } - - return userAgent; -} - -void Application::toggleTabletUI(bool shouldOpen) const { - auto hmd = DependencyManager::get(); - if (!(shouldOpen && hmd->getShouldShowTablet())) { - auto HMD = DependencyManager::get(); - HMD->toggleShouldShowTablet(); - - if (!HMD->getShouldShowTablet()) { - DependencyManager::get()->setRaised(false); - _window->activateWindow(); - auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); - tablet->unfocus(); - } - } -} - -void Application::checkChangeCursor() { - QMutexLocker locker(&_changeCursorLock); - if (_cursorNeedsChanging) { -#ifdef Q_OS_MAC - auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget -#else - // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the - // window menu, which is a pain, so only hide it for the GL surface - auto cursorTarget = _glWidget; -#endif - cursorTarget->setCursor(_desiredCursor); - - _cursorNeedsChanging = false; - } -} - -void Application::showCursor(const Cursor::Icon& cursor) { - QMutexLocker locker(&_changeCursorLock); - - auto managedCursor = Cursor::Manager::instance().getCursor(); - auto curIcon = managedCursor->getIcon(); - if (curIcon != cursor) { - managedCursor->setIcon(cursor); - curIcon = cursor; - } - _desiredCursor = cursor == Cursor::Icon::SYSTEM ? Qt::ArrowCursor : Qt::BlankCursor; - _cursorNeedsChanging = true; -} - -void Application::updateHeartbeat() const { - DeadlockWatchdogThread::updateHeartbeat(); -} - -void Application::onAboutToQuit() { - // quickly save AvatarEntityData before the EntityTree is dismantled - getMyAvatar()->saveAvatarEntityDataToSettings(); - - emit beforeAboutToQuit(); - - if (getLoginDialogPoppedUp() && _firstRun.get()) { - _firstRun.set(false); - } - - foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - if (inputPlugin->isActive()) { - inputPlugin->deactivate(); - } - } - - // The active display plugin needs to be loaded before the menu system is active, - // so its persisted explicitly here - Setting::Handle{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME }.set(getActiveDisplayPlugin()->getName()); - - loginDialogPoppedUp.set(false); - - getActiveDisplayPlugin()->deactivate(); - if (_autoSwitchDisplayModeSupportedHMDPlugin - && _autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { - _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); - } - // use the CloseEventSender via a QThread to send an event that says the user asked for the app to close - DependencyManager::get()->startThread(); - - // Hide Running Scripts dialog so that it gets destroyed in an orderly manner; prevents warnings at shutdown. -#if !defined(DISABLE_QML) - getOffscreenUI()->hide("RunningScripts"); -#endif - - _aboutToQuit = true; - - cleanupBeforeQuit(); -} - -void Application::cleanupBeforeQuit() { - // add a logline indicating if QTWEBENGINE_REMOTE_DEBUGGING is set or not - QString webengineRemoteDebugging = QProcessEnvironment::systemEnvironment().value("QTWEBENGINE_REMOTE_DEBUGGING", "false"); - qCDebug(interfaceapp) << "QTWEBENGINE_REMOTE_DEBUGGING =" << webengineRemoteDebugging; - - DependencyManager::prepareToExit(); - - if (tracing::enabled()) { - auto tracer = DependencyManager::get(); - tracer->stopTracing(); - auto outputFile = property(hifi::properties::TRACING).toString(); - tracer->serialize(outputFile); - } - - // Stop third party processes so that they're not left running in the event of a subsequent shutdown crash. -#ifdef HAVE_DDE - DependencyManager::get()->setEnabled(false); -#endif -#ifdef HAVE_IVIEWHMD - DependencyManager::get()->setEnabled(false, true); -#endif - AnimDebugDraw::getInstance().shutdown(); - - // FIXME: once we move to shared pointer for the INputDevice we shoud remove this naked delete: - _applicationStateDevice.reset(); - - { - if (_keyboardFocusHighlightID != UNKNOWN_ENTITY_ID) { - DependencyManager::get()->deleteEntity(_keyboardFocusHighlightID); - _keyboardFocusHighlightID = UNKNOWN_ENTITY_ID; - } - } - - { - auto nodeList = DependencyManager::get(); - - // send the domain a disconnect packet, force stoppage of domain-server check-ins - nodeList->getDomainHandler().disconnect(); - nodeList->setIsShuttingDown(true); - - // tell the packet receiver we're shutting down, so it can drop packets - nodeList->getPacketReceiver().setShouldDropPackets(true); - } - - getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts - - // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) - QThreadPool::globalInstance()->clear(); - - DependencyManager::destroy(); - - // FIXME: Something is still holding on to the ScriptEnginePointers contained in ScriptEngines, and they hold backpointers to ScriptEngines, - // so this doesn't shut down properly - DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts - // These classes hold ScriptEnginePointers, so they must be destroyed before ScriptEngines - // Must be done after shutdownScripting in case any scripts try to access these things - { - DependencyManager::destroy(); - EntityTreePointer tree = getEntities()->getTree(); - tree->setSimulation(nullptr); - DependencyManager::destroy(); - } - DependencyManager::destroy(); - - bool keepMeLoggedIn = Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, false).get(); - if (!keepMeLoggedIn) { - DependencyManager::get()->removeAccountFromFile(); - } - - _displayPlugin.reset(); - PluginManager::getInstance()->shutdown(); - - // Cleanup all overlays after the scripts, as scripts might add more - _overlays.cleanupAllOverlays(); - - // first stop all timers directly or by invokeMethod - // depending on what thread they run in - locationUpdateTimer.stop(); - identityPacketTimer.stop(); - pingTimer.stop(); - - // Wait for the settings thread to shut down, and save the settings one last time when it's safe - if (_settingsGuard.wait()) { - // save state - saveSettings(); - } - - _window->saveGeometry(); - - // Destroy third party processes after scripts have finished using them. -#ifdef HAVE_DDE - DependencyManager::destroy(); -#endif -#ifdef HAVE_IVIEWHMD - DependencyManager::destroy(); -#endif - - DependencyManager::destroy(); // Must be destroyed before TabletScriptingInterface - - // stop QML - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - DependencyManager::destroy(); - - if (_snapshotSoundInjector != nullptr) { - _snapshotSoundInjector->stop(); - } - - // destroy Audio so it and its threads have a chance to go down safely - // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine - QMetaObject::invokeMethod(DependencyManager::get().data(), "stop"); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - // The PointerManager must be destroyed before the PickManager because when a Pointer is deleted, - // it accesses the PickManager to delete its associated Pick - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; -} - -Application::~Application() { - // remove avatars from physics engine - auto avatarManager = DependencyManager::get(); - avatarManager->clearOtherAvatars(); - - PhysicsEngine::Transaction transaction; - avatarManager->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - avatarManager->handleProcessedPhysicsTransaction(transaction); - - avatarManager->deleteAllAvatars(); - - auto myCharacterController = getMyAvatar()->getCharacterController(); - myCharacterController->clearDetailedMotionStates(); - - myCharacterController->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - myCharacterController->handleProcessedPhysicsTransaction(transaction); - - _physicsEngine->setCharacterController(nullptr); - - // the _shapeManager should have zero references - _shapeManager.collectGarbage(); - assert(_shapeManager.getNumShapes() == 0); - - // shutdown graphics engine - _graphicsEngine.shutdown(); - - _gameWorkload.shutdown(); - - DependencyManager::destroy(); - - _entityClipboard->eraseAllOctreeElements(); - _entityClipboard.reset(); - - _octreeProcessor.terminate(); - _entityEditSender.terminate(); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - steamClient->shutdown(); - } - - if (auto oculusPlatform = PluginManager::getInstance()->getOculusPlatformPlugin()) { - oculusPlatform->shutdown(); - } - - DependencyManager::destroy(); - - DependencyManager::destroy(); // must be destroyed before the FramebufferCache - - DependencyManager::destroy(); - - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - DependencyManager::get()->cleanup(); - - // remove the NodeList from the DependencyManager - DependencyManager::destroy(); - -#if 0 - ConnexionClient::getInstance().destroy(); -#endif - // The window takes ownership of the menu, so this has the side effect of destroying it. - _window->setMenuBar(nullptr); - - _window->deleteLater(); - - // make sure that the quit event has finished sending before we take the application down - auto closeEventSender = DependencyManager::get(); - while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) { - // sleep a little so we're not spinning at 100% - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - // quit the thread used by the closure event sender - closeEventSender->thread()->quit(); - - // Can't log to file past this point, FileLogger about to be deleted - qInstallMessageHandler(LogHandler::verboseMessageHandler); -} - -void Application::initializeGL() { - qCDebug(interfaceapp) << "Created Display Window."; - -#ifdef DISABLE_QML - setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); -#endif - - // initialize glut for shape drawing; Qt apparently initializes it on OS X - if (_isGLInitialized) { - return; - } else { - _isGLInitialized = true; - } - - _glWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); - - // When loading QtWebEngineWidgets, it creates a global share context on startup. - // We have to account for this possibility by checking here for an existing - // global share context - auto globalShareContext = qt_gl_global_share_context(); - -#if !defined(DISABLE_QML) - // Build a shared canvas / context for the Chromium processes - if (!globalShareContext) { - // Chromium rendering uses some GL functions that prevent nSight from capturing - // frames, so we only create the shared context if nsight is NOT active. - if (!nsightActive()) { - _chromiumShareContext = new OffscreenGLCanvas(); - _chromiumShareContext->setObjectName("ChromiumShareContext"); - auto format =QSurfaceFormat::defaultFormat(); -#ifdef Q_OS_MAC - // On mac, the primary shared OpenGL context must be a 3.2 core context, - // or chromium flips out and spews error spam (but renders fine) - format.setMajorVersion(3); - format.setMinorVersion(2); -#endif - _chromiumShareContext->setFormat(format); - _chromiumShareContext->create(); - if (!_chromiumShareContext->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make chromium shared context current"); - } - globalShareContext = _chromiumShareContext->getContext(); - qt_gl_set_global_share_context(globalShareContext); - _chromiumShareContext->doneCurrent(); - } - } -#endif - - - _glWidget->createContext(globalShareContext); - - if (!_glWidget->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make window context current"); - } - -#if !defined(DISABLE_QML) - // Disable signed distance field font rendering on ATI/AMD GPUs, due to - // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app - std::string vendor{ (const char*)glGetString(GL_VENDOR) }; - if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { - qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); - } -#endif - - if (!globalShareContext) { - globalShareContext = _glWidget->qglContext(); - qt_gl_set_global_share_context(globalShareContext); - } - - // Build a shared canvas / context for the QML rendering -#if !defined(DISABLE_QML) - { - _qmlShareContext = new OffscreenGLCanvas(); - _qmlShareContext->setObjectName("QmlShareContext"); - _qmlShareContext->create(globalShareContext); - if (!_qmlShareContext->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make QML shared context current"); - } - OffscreenQmlSurface::setSharedContext(_qmlShareContext->getContext()); - _qmlShareContext->doneCurrent(); - if (!_glWidget->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make window context current"); - } - } -#endif - - - // Build an offscreen GL context for the main thread. - _glWidget->makeCurrent(); - glClearColor(0.2f, 0.2f, 0.2f, 1); - glClear(GL_COLOR_BUFFER_BIT); - _glWidget->swapBuffers(); - - _graphicsEngine.initializeGPU(_glWidget); -} - -void Application::initializeDisplayPlugins() { - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - Setting::Handle activeDisplayPluginSetting{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME, displayPlugins.at(0)->getName() }; - auto lastActiveDisplayPluginName = activeDisplayPluginSetting.get(); - - auto defaultDisplayPlugin = displayPlugins.at(0); - // Once time initialization code - DisplayPluginPointer targetDisplayPlugin; - foreach(auto displayPlugin, displayPlugins) { - displayPlugin->setContext(_graphicsEngine.getGPUContext()); - if (displayPlugin->getName() == lastActiveDisplayPluginName) { - targetDisplayPlugin = displayPlugin; - } - QObject::connect(displayPlugin.get(), &DisplayPlugin::recommendedFramebufferSizeChanged, - [this](const QSize& size) { resizeGL(); }); - QObject::connect(displayPlugin.get(), &DisplayPlugin::resetSensorsRequested, this, &Application::requestReset); - if (displayPlugin->isHmd()) { - auto hmdDisplayPlugin = dynamic_cast(displayPlugin.get()); - QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdMountedChanged, - DependencyManager::get().data(), &HMDScriptingInterface::mountedChanged); - QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdVisibleChanged, this, &Application::hmdVisibleChanged); - } - } - - // The default display plugin needs to be activated first, otherwise the display plugin thread - // may be launched by an external plugin, which is bad - setDisplayPlugin(defaultDisplayPlugin); - - // Now set the desired plugin if it's not the same as the default plugin - if (targetDisplayPlugin && (targetDisplayPlugin != defaultDisplayPlugin)) { - setDisplayPlugin(targetDisplayPlugin); - } - - // Submit a default frame to render until the engine starts up - updateRenderArgs(0.0f); -} - -void Application::initializeRenderEngine() { - // FIXME: on low end systems os the shaders take up to 1 minute to compile, so we pause the deadlock watchdog thread. - DeadlockWatchdogThread::withPause([&] { - _graphicsEngine.initializeRender(DISABLE_DEFERRED); - DependencyManager::get()->registerKeyboardHighlighting(); - }); -} - -extern void setupPreferences(); -#if !defined(DISABLE_QML) -static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active = false); -#endif - -void Application::showLoginScreen() { -#if !defined(DISABLE_QML) - auto accountManager = DependencyManager::get(); - auto dialogsManager = DependencyManager::get(); - if (!accountManager->isLoggedIn()) { - if (!isHMDMode()) { - auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); - toolbar->writeProperty("visible", false); - } - _loginDialogPoppedUp = true; - dialogsManager->showLoginDialog(); - emit loginDialogFocusEnabled(); - QJsonObject loginData = {}; - loginData["action"] = "login dialog popped up"; - UserActivityLogger::getInstance().logAction("encourageLoginDialog", loginData); - _window->setWindowTitle("High Fidelity Interface"); - } else { - resumeAfterLoginDialogActionTaken(); - } - _loginDialogPoppedUp = !accountManager->isLoggedIn(); - loginDialogPoppedUp.set(_loginDialogPoppedUp); -#else - resumeAfterLoginDialogActionTaken(); -#endif -} - -void Application::initializeUi() { - AddressBarDialog::registerType(); - ErrorDialog::registerType(); - LoginDialog::registerType(); - Tooltip::registerType(); - UpdateDialog::registerType(); - QmlContextCallback commerceCallback = [](QQmlContext* context) { - context->setContextProperty("Commerce", DependencyManager::get().data()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/commerce/checkout/Checkout.qml" }, - QUrl{ "hifi/commerce/common/CommerceLightbox.qml" }, - QUrl{ "hifi/commerce/common/EmulatedMarketplaceHeader.qml" }, - QUrl{ "hifi/commerce/common/FirstUseTutorial.qml" }, - QUrl{ "hifi/commerce/common/sendAsset/SendAsset.qml" }, - QUrl{ "hifi/commerce/common/SortableListModel.qml" }, - QUrl{ "hifi/commerce/inspectionCertificate/InspectionCertificate.qml" }, - QUrl{ "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"}, - QUrl{ "hifi/commerce/purchases/PurchasedItem.qml" }, - QUrl{ "hifi/commerce/purchases/Purchases.qml" }, - QUrl{ "hifi/commerce/wallet/Help.qml" }, - QUrl{ "hifi/commerce/wallet/NeedsLogIn.qml" }, - QUrl{ "hifi/commerce/wallet/PassphraseChange.qml" }, - QUrl{ "hifi/commerce/wallet/PassphraseModal.qml" }, - QUrl{ "hifi/commerce/wallet/PassphraseSelection.qml" }, - QUrl{ "hifi/commerce/wallet/Wallet.qml" }, - QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, - QUrl{ "hifi/commerce/wallet/WalletSetup.qml" }, - QUrl{ "hifi/dialogs/security/Security.qml" }, - QUrl{ "hifi/dialogs/security/SecurityImageChange.qml" }, - QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, - QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, - QUrl{ "hifi/tablet/TabletMenu.qml" }, - QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, - }, commerceCallback); - - QmlContextCallback marketplaceCallback = [](QQmlContext* context) { - context->setContextProperty("MarketplaceScriptingInterface", new QmlMarketplace()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, - }, marketplaceCallback); - - QmlContextCallback platformInfoCallback = [](QQmlContext* context) { - context->setContextProperty("PlatformInfo", new PlatformInfoScriptingInterface()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, - }, platformInfoCallback); - - QmlContextCallback ttsCallback = [](QQmlContext* context) { - context->setContextProperty("TextToSpeech", DependencyManager::get().data()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/tts/TTS.qml" } - }, ttsCallback); - qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); - qmlRegisterType("Hifi", 1, 0, "Preference"); - qmlRegisterType("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine"); - - { - auto tabletScriptingInterface = DependencyManager::get(); - tabletScriptingInterface->getTablet(SYSTEM_TABLET); - } - - auto offscreenUi = getOffscreenUI(); - connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootContextCreated, - this, &Application::onDesktopRootContextCreated); - connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootItemCreated, - this, &Application::onDesktopRootItemCreated); - -#if !defined(DISABLE_QML) - offscreenUi->setProxyWindow(_window->windowHandle()); - // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to - // support the window management and scripting proxies for VR use - DeadlockWatchdogThread::withPause([&] { - offscreenUi->createDesktop(PathUtils::qmlUrl("hifi/Desktop.qml")); - }); - // FIXME either expose so that dialogs can set this themselves or - // do better detection in the offscreen UI of what has focus - offscreenUi->setNavigationFocused(false); -#else - _window->setMenuBar(new Menu()); -#endif - - setupPreferences(); - -#if !defined(DISABLE_QML) - _glWidget->installEventFilter(offscreenUi.data()); - offscreenUi->setMouseTranslator([=](const QPointF& pt) { - QPointF result = pt; - auto displayPlugin = getActiveDisplayPlugin(); - if (displayPlugin->isHmd()) { - getApplicationCompositor().handleRealMouseMoveEvent(false); - auto resultVec = getApplicationCompositor().getReticlePosition(); - result = QPointF(resultVec.x, resultVec.y); - } - return result.toPoint(); - }); - offscreenUi->resume(); -#endif - connect(_window, &MainWindow::windowGeometryChanged, [this](const QRect& r){ - resizeGL(); - if (_touchscreenVirtualPadDevice) { - _touchscreenVirtualPadDevice->resize(); - } - }); - - // This will set up the input plugins UI - _activeInputPlugins.clear(); - foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - if (KeyboardMouseDevice::NAME == inputPlugin->getName()) { - _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); - } - if (TouchscreenDevice::NAME == inputPlugin->getName()) { - _touchscreenDevice = std::dynamic_pointer_cast(inputPlugin); - } - if (TouchscreenVirtualPadDevice::NAME == inputPlugin->getName()) { - _touchscreenVirtualPadDevice = std::dynamic_pointer_cast(inputPlugin); -#if defined(ANDROID_APP_INTERFACE) - auto& virtualPadManager = VirtualPad::Manager::instance(); - connect(&virtualPadManager, &VirtualPad::Manager::hapticFeedbackRequested, - this, [](int duration) { - AndroidHelper::instance().performHapticFeedback(duration); - }); -#endif - } - } - - auto compositorHelper = DependencyManager::get(); - connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, this, [=] { - if (isHMDMode()) { - auto compositorHelper = DependencyManager::get(); // don't capture outer smartpointer - showCursor(compositorHelper->getAllowMouseCapture() ? - Cursor::Manager::lookupIcon(_preferredCursor.get()) : - Cursor::Icon::SYSTEM); - } - }); - -#if !defined(DISABLE_QML) - // Pre-create a couple of offscreen surfaces to speed up tablet UI - auto offscreenSurfaceCache = DependencyManager::get(); - offscreenSurfaceCache->setOnRootContextCreated([&](const QString& rootObject, QQmlContext* surfaceContext) { - if (rootObject == TabletScriptingInterface::QML) { - // in Qt 5.10.0 there is already an "Audio" object in the QML context - // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" - surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); - surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - } - }); - - offscreenSurfaceCache->reserve(TabletScriptingInterface::QML, 1); - offscreenSurfaceCache->reserve(render::entities::WebEntityRenderer::QML, 2); -#endif - - flushMenuUpdates(); - -#if !defined(DISABLE_QML) - // Now that the menu is instantiated, ensure the display plugin menu is properly updated - { - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - // first sort the plugins into groupings: standard, advanced, developer - std::stable_sort(displayPlugins.begin(), displayPlugins.end(), - [](const DisplayPluginPointer& a, const DisplayPluginPointer& b) -> bool { return a->getGrouping() < b->getGrouping(); }); - int dpIndex = 1; - // concatenate the groupings into a single list in the order: standard, advanced, developer - for(const auto& displayPlugin : displayPlugins) { - addDisplayPluginToMenu(displayPlugin, dpIndex, _displayPlugin == displayPlugin); - dpIndex++; - } - - // after all plugins have been added to the menu, add a separator to the menu - auto parent = getPrimaryMenu()->getMenu(MenuOption::OutputMenu); - parent->addSeparator(); - } -#endif - - - // The display plugins are created before the menu now, so we need to do this here to hide the menu bar - // now that it exists - if (_window && _window->isFullScreen()) { - setFullscreen(nullptr, true); - } - - - setIsInterstitialMode(true); -} - - -void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { - auto engine = surfaceContext->engine(); - // in Qt 5.10.0 there is already an "Audio" object in the QML context - // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" - surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); - - surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); - surfaceContext->setContextProperty("AudioScope", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); - surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); - _fileDownload = new FileScriptingInterface(engine); - surfaceContext->setContextProperty("File", _fileDownload); - connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip); - surfaceContext->setContextProperty("MyAvatar", getMyAvatar().get()); - surfaceContext->setContextProperty("Messages", DependencyManager::get().data()); - surfaceContext->setContextProperty("Recording", DependencyManager::get().data()); - surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); - surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("FrameTimings", &_graphicsEngine._frameTimingsScriptingInterface); - surfaceContext->setContextProperty("Rates", new RatesScriptingInterface(this)); - - surfaceContext->setContextProperty("TREE_SCALE", TREE_SCALE); - // FIXME Quat and Vec3 won't work with QJSEngine used by QML - surfaceContext->setContextProperty("Quat", new Quat()); - surfaceContext->setContextProperty("Vec3", new Vec3()); - surfaceContext->setContextProperty("Uuid", new ScriptUUID()); - surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); - surfaceContext->setContextProperty("Keyboard", DependencyManager::get().data()); - - surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); - surfaceContext->setContextProperty("Users", DependencyManager::get().data()); - - surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Camera", &_myCamera); - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - surfaceContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); -#endif - - surfaceContext->setContextProperty("Overlays", &_overlays); - surfaceContext->setContextProperty("Window", DependencyManager::get().data()); - surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); - surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); - surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); - surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); - - // Caches - surfaceContext->setContextProperty("AnimationCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("TextureCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("ModelCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); - - surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); - - surfaceContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); - surfaceContext->setContextProperty("FaceTracker", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); - surfaceContext->setContextProperty("Scene", DependencyManager::get().data()); - surfaceContext->setContextProperty("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); - surfaceContext->setContextProperty("Workload", _gameWorkload._engine->getConfiguration().get()); - surfaceContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); - surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); - - surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); - - surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); - surfaceContext->setContextProperty("Selection", DependencyManager::get().data()); - surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); - surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); - surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); - surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); - } - - _window->setMenuBar(new Menu()); -} - -void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { - Stats::show(); - AnimStats::show(); - auto surfaceContext = getOffscreenUI()->getSurfaceContext(); - surfaceContext->setContextProperty("Stats", Stats::getInstance()); - surfaceContext->setContextProperty("AnimStats", AnimStats::getInstance()); - -#if !defined(Q_OS_ANDROID) - auto offscreenUi = getOffscreenUI(); - auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml"); - offscreenUi->show(qml, "AvatarInputsBar"); -#endif -} - -void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) { - surfaceContext->setContextProperty("Users", DependencyManager::get().data()); - surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); - surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); - surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); - surfaceContext->setContextProperty("Vec3", new Vec3()); - surfaceContext->setContextProperty("Quat", new Quat()); - surfaceContext->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); - surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); - surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); - surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); - - if (setAdditionalContextProperties) { - auto tabletScriptingInterface = DependencyManager::get(); - auto flags = tabletScriptingInterface->getFlags(); - - surfaceContext->setContextProperty("offscreenFlags", flags); - surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); - surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - - surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); - - // in Qt 5.10.0 there is already an "Audio" object in the QML context - // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" - surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); - - surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); - surfaceContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); - surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); - surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("OctreeStats", DependencyManager::get().data()); - surfaceContext->setContextProperty("DCModel", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); - surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); - surfaceContext->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); - surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); - surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); - surfaceContext->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); - surfaceContext->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); - surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); - surfaceContext->setContextProperty("Pointers", DependencyManager::get().data()); - surfaceContext->setContextProperty("Window", DependencyManager::get().data()); - surfaceContext->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); - surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); - surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); - surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); - } -} - -void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { - PROFILE_RANGE(render, __FUNCTION__); - PerformanceTimer perfTimer("updateCamera"); - - glm::vec3 boomOffset; - auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; - - // The render mode is default or mirror if the camera is in mirror mode, assigned further below - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - - // Always use the default eye position, not the actual head eye position. - // Using the latter will cause the camera to wobble with idle animations, - // or with changes from the face tracker - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { - _thirdPersonHMDCameraBoomValid= false; - if (isHMDMode()) { - mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setPosition(extractTranslation(camMat)); - _myCamera.setOrientation(glmExtractRotation(camMat)); - } - else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition()); - _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); - } - } - else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { - if (isHMDMode()) { - - if (!_thirdPersonHMDCameraBoomValid) { - const glm::vec3 CAMERA_OFFSET = glm::vec3(0.0f, 0.0f, 0.7f); - _thirdPersonHMDCameraBoom = cancelOutRollAndPitch(myAvatar->getHMDSensorOrientation()) * CAMERA_OFFSET; - _thirdPersonHMDCameraBoomValid = true; - } - - glm::mat4 thirdPersonCameraSensorToWorldMatrix = myAvatar->getSensorToWorldMatrix(); - - const glm::vec3 cameraPos = myAvatar->getHMDSensorPosition() + _thirdPersonHMDCameraBoom * myAvatar->getBoomLength(); - glm::mat4 sensorCameraMat = createMatFromQuatAndPos(myAvatar->getHMDSensorOrientation(), cameraPos); - glm::mat4 worldCameraMat = thirdPersonCameraSensorToWorldMatrix * sensorCameraMat; - - _myCamera.setOrientation(glm::normalize(glmExtractRotation(worldCameraMat))); - _myCamera.setPosition(extractTranslation(worldCameraMat)); - } - else { - _thirdPersonHMDCameraBoomValid = false; - - _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); - if (isOptionChecked(MenuOption::CenterPlayerInView)) { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + _myCamera.getOrientation() * boomOffset); - } - else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + myAvatar->getWorldOrientation() * boomOffset); - } - } - } - else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - _thirdPersonHMDCameraBoomValid= false; - - if (isHMDMode()) { - auto mirrorBodyOrientation = myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f)); - - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD yaw and roll - glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); - mirrorHmdEulers.y = -mirrorHmdEulers.y; - mirrorHmdEulers.z = -mirrorHmdEulers.z; - glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); - - glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; - - _myCamera.setOrientation(worldMirrorRotation); - - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD lateral offsets - hmdOffset.x = -hmdOffset.x; - - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror - + mirrorBodyOrientation * hmdOffset); - } - else { - auto userInputMapper = DependencyManager::get(); - const float YAW_SPEED = TWO_PI / 5.0f; - float deltaYaw = userInputMapper->getActionState(controller::Action::YAW) * YAW_SPEED * deltaTime; - _mirrorYawOffset += deltaYaw; - _myCamera.setOrientation(myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f))); - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + (myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, _mirrorYawOffset, 0.0f))) * - glm::vec3(0.0f, 0.0f, -1.0f) * myAvatar->getBoomLength() * _scaleMirror); - } - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - } - else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { - _thirdPersonHMDCameraBoomValid= false; - EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); - if (cameraEntity != nullptr) { - if (isHMDMode()) { - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - _myCamera.setOrientation(cameraEntity->getWorldOrientation() * hmdRotation); - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - _myCamera.setPosition(cameraEntity->getWorldPosition() + (hmdRotation * hmdOffset)); - } - else { - _myCamera.setOrientation(cameraEntity->getWorldOrientation()); - _myCamera.setPosition(cameraEntity->getWorldPosition()); - } - } - } - // Update camera position - if (!isHMDMode()) { - _myCamera.update(); - } - - renderArgs._cameraMode = (int8_t)_myCamera.getMode(); -} - -void Application::runTests() { - runTimingTests(); - runUnitTests(); -} - -void Application::faceTrackerMuteToggled() { - - QAction* muteAction = Menu::getInstance()->getActionForOption(MenuOption::MuteFaceTracking); - Q_CHECK_PTR(muteAction); - bool isMuted = getSelectedFaceTracker()->isMuted(); - muteAction->setChecked(isMuted); - getSelectedFaceTracker()->setEnabled(!isMuted); - Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setEnabled(!isMuted); -} - -void Application::setFieldOfView(float fov) { - if (fov != _fieldOfView.get()) { - _fieldOfView.set(fov); - resizeGL(); - } -} - -void Application::setHMDTabletScale(float hmdTabletScale) { - _hmdTabletScale.set(hmdTabletScale); -} - -void Application::setDesktopTabletScale(float desktopTabletScale) { - _desktopTabletScale.set(desktopTabletScale); -} - -void Application::setDesktopTabletBecomesToolbarSetting(bool value) { - _desktopTabletBecomesToolbarSetting.set(value); - updateSystemTabletMode(); -} - -void Application::setHmdTabletBecomesToolbarSetting(bool value) { - _hmdTabletBecomesToolbarSetting.set(value); - updateSystemTabletMode(); -} - -void Application::setPreferStylusOverLaser(bool value) { - _preferStylusOverLaserSetting.set(value); -} - -void Application::setPreferAvatarFingerOverStylus(bool value) { - _preferAvatarFingerOverStylusSetting.set(value); -} - -void Application::setPreferredCursor(const QString& cursorName) { - qCDebug(interfaceapp) << "setPreferredCursor" << cursorName; - _preferredCursor.set(cursorName.isEmpty() ? DEFAULT_CURSOR_NAME : cursorName); - showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); -} - -void Application::setSettingConstrainToolbarPosition(bool setting) { - _constrainToolbarPosition.set(setting); - getOffscreenUI()->setConstrainToolbarToCenterX(setting); -} - -void Application::setMiniTabletEnabled(bool enabled) { - _miniTabletEnabledSetting.set(enabled); - emit miniTabletEnabledChanged(enabled); -} - -void Application::showHelp() { - static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; - static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; - static const QString HAND_CONTROLLER_NAME_WINDOWS_MR = "windowsMR"; - - static const QString VIVE_PLUGIN_NAME = "HTC Vive"; - static const QString OCULUS_RIFT_PLUGIN_NAME = "Oculus Rift"; - static const QString WINDOWS_MR_PLUGIN_NAME = "WindowsMR"; - - static const QString TAB_KEYBOARD_MOUSE = "kbm"; - static const QString TAB_GAMEPAD = "gamepad"; - static const QString TAB_HAND_CONTROLLERS = "handControllers"; - - QString handControllerName; - QString defaultTab = TAB_KEYBOARD_MOUSE; - - if (PluginUtils::isHMDAvailable(WINDOWS_MR_PLUGIN_NAME)) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_WINDOWS_MR; - } else if (PluginUtils::isHMDAvailable(VIVE_PLUGIN_NAME)) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_VIVE; - } else if (PluginUtils::isHMDAvailable(OCULUS_RIFT_PLUGIN_NAME)) { - if (PluginUtils::isOculusTouchControllerAvailable()) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_OCULUS_TOUCH; - } else if (PluginUtils::isXboxControllerAvailable()) { - defaultTab = TAB_GAMEPAD; - } else { - defaultTab = TAB_KEYBOARD_MOUSE; - } - } else if (PluginUtils::isXboxControllerAvailable()) { - defaultTab = TAB_GAMEPAD; - } else { - defaultTab = TAB_KEYBOARD_MOUSE; - } - - QUrlQuery queryString; - queryString.addQueryItem("handControllerName", handControllerName); - queryString.addQueryItem("defaultTab", defaultTab); - TabletProxy* tablet = dynamic_cast(DependencyManager::get()->getTablet(SYSTEM_TABLET)); - tablet->gotoWebScreen(PathUtils::resourcesUrl() + INFO_HELP_PATH + "?" + queryString.toString()); - DependencyManager::get()->openTablet(); - //InfoView::show(INFO_HELP_PATH, false, queryString.toString()); -} - -void Application::resizeEvent(QResizeEvent* event) { - resizeGL(); -} - -void Application::resizeGL() { - PROFILE_RANGE(render, __FUNCTION__); - if (nullptr == _displayPlugin) { - return; - } - - auto displayPlugin = getActiveDisplayPlugin(); - // Set the desired FBO texture size. If it hasn't changed, this does nothing. - // Otherwise, it must rebuild the FBOs - uvec2 framebufferSize = displayPlugin->getRecommendedRenderSize(); - uvec2 renderSize = uvec2(framebufferSize); - if (_renderResolution != renderSize) { - _renderResolution = renderSize; - DependencyManager::get()->setFrameBufferSize(fromGlm(renderSize)); - } - - auto renderResolutionScale = getRenderResolutionScale(); - if (displayPlugin->getRenderResolutionScale() != renderResolutionScale) { - auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); - assert(renderConfig); - auto mainView = renderConfig->getConfig("RenderMainView.RenderDeferredTask"); - // mainView can be null if we're rendering in forward mode - if (mainView) { - mainView->setProperty("resolutionScale", renderResolutionScale); - } - displayPlugin->setRenderResolutionScale(renderResolutionScale); - } - - // FIXME the aspect ratio for stereo displays is incorrect based on this. - float aspectRatio = displayPlugin->getRecommendedAspectRatio(); - _myCamera.setProjection(glm::perspective(glm::radians(_fieldOfView.get()), aspectRatio, - DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); - // Possible change in aspect ratio - { - QMutexLocker viewLocker(&_viewMutex); - _myCamera.loadViewFrustum(_viewFrustum); - } - -#if !defined(DISABLE_QML) - getOffscreenUI()->resize(fromGlm(displayPlugin->getRecommendedUiSize())); -#endif -} - -void Application::handleSandboxStatus(QNetworkReply* reply) { - PROFILE_RANGE(render, __FUNCTION__); - - bool sandboxIsRunning = SandboxUtils::readStatus(reply->readAll()); - - enum HandControllerType { - Vive, - Oculus - }; - static const std::map MIN_CONTENT_VERSION = { - { Vive, 1 }, - { Oculus, 27 } - }; - - // Get sandbox content set version - auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; - auto contentVersionPath = acDirPath + "content-version.txt"; - qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; - int contentVersion = 0; - QFile contentVersionFile(contentVersionPath); - if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - QString line = contentVersionFile.readAll(); - contentVersion = line.toInt(); // returns 0 if conversion fails - } - - // Get controller availability -#ifdef ANDROID_APP_QUEST_INTERFACE - bool hasHandControllers = true; -#else - bool hasHandControllers = false; - if (PluginUtils::isViveControllerAvailable() || PluginUtils::isOculusTouchControllerAvailable()) { - hasHandControllers = true; - } -#endif - - // Check HMD use (may be technically available without being in use) - bool hasHMD = PluginUtils::isHMDAvailable(); - bool isUsingHMD = _displayPlugin->isHmd(); - bool isUsingHMDAndHandControllers = hasHMD && hasHandControllers && isUsingHMD; - - qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMDAndHandControllers; - - // when --url in command line, teleport to location - const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; - int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); - QString addressLookupString; - if (urlIndex != -1) { - QUrl url(arguments().value(urlIndex + 1)); - if (url.scheme() == URL_SCHEME_HIFIAPP) { - Setting::Handle("startUpApp").set(url.path()); - } else { - addressLookupString = url.toString(); - } - } - - static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location"; - static const QString SENT_TO_ENTRY = "entry"; - - QString sentTo; - - // If this is a first run we short-circuit the address passed in - if (_firstRun.get()) { -#if !defined(Q_OS_ANDROID) - DependencyManager::get()->goToEntry(); - sentTo = SENT_TO_ENTRY; -#endif - _firstRun.set(false); - - } else { -#if !defined(Q_OS_ANDROID) - QString goingTo = ""; - if (addressLookupString.isEmpty()) { - if (Menu::getInstance()->isOptionChecked(MenuOption::HomeLocation)) { - auto locationBookmarks = DependencyManager::get(); - addressLookupString = locationBookmarks->addressForBookmark(LocationBookmarks::HOME_BOOKMARK); - goingTo = "home location"; - } else { - goingTo = "previous location"; - } - } - qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(!goingTo.isEmpty() ? goingTo : addressLookupString); - DependencyManager::get()->loadSettings(addressLookupString); - sentTo = SENT_TO_PREVIOUS_LOCATION; -#endif - } - - UserActivityLogger::getInstance().logAction("startup_sent_to", { - { "sent_to", sentTo }, - { "sandbox_is_running", sandboxIsRunning }, - { "has_hmd", hasHMD }, - { "has_hand_controllers", hasHandControllers }, - { "is_using_hmd", isUsingHMD }, - { "is_using_hmd_and_hand_controllers", isUsingHMDAndHandControllers }, - { "content_version", contentVersion } - }); - - _connectionMonitor.init(); -} - -bool Application::importJSONFromURL(const QString& urlString) { - // we only load files that terminate in just .json (not .svo.json and not .ava.json) - QUrl jsonURL { urlString }; - - emit svoImportRequested(urlString); - return true; -} - -bool Application::importSVOFromURL(const QString& urlString) { - emit svoImportRequested(urlString); - return true; -} - -bool Application::importFromZIP(const QString& filePath) { - qDebug() << "A zip file has been dropped in: " << filePath; - QUrl empty; - // handle Blocks download from Marketplace - if (filePath.contains("poly.google.com/downloads")) { - addAssetToWorldFromURL(filePath); - } else { - qApp->getFileDownloadInterface()->runUnzip(filePath, empty, true, true, false); - } - return true; -} - -bool Application::isServerlessMode() const { - auto tree = getEntities()->getTree(); - if (tree) { - return tree->isServerlessMode(); - } - return false; -} - -void Application::setIsInterstitialMode(bool interstitialMode) { - bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); - if (enableInterstitial) { - if (_interstitialMode != interstitialMode) { - _interstitialMode = interstitialMode; - emit interstitialModeChanged(_interstitialMode); - - DependencyManager::get()->setAudioPaused(_interstitialMode); - DependencyManager::get()->setMyAvatarDataPacketsPaused(_interstitialMode); - } - } -} - -void Application::setIsServerlessMode(bool serverlessDomain) { - auto tree = getEntities()->getTree(); - if (tree) { - tree->setIsServerlessMode(serverlessDomain); - } -} - -std::map Application::prepareServerlessDomainContents(QUrl domainURL) { - QUuid serverlessSessionID = QUuid::createUuid(); - getMyAvatar()->setSessionUUID(serverlessSessionID); - auto nodeList = DependencyManager::get(); - nodeList->setSessionUUID(serverlessSessionID); - - // there is no domain-server to tell us our permissions, so enable all - NodePermissions permissions; - permissions.setAll(true); - nodeList->setPermissions(permissions); - - // we can't import directly into the main tree because we would need to lock it, and - // Octree::readFromURL calls loop.exec which can run code which will also attempt to lock the tree. - EntityTreePointer tmpTree(new EntityTree()); - tmpTree->setIsServerlessMode(true); - tmpTree->createRootElement(); - auto myAvatar = getMyAvatar(); - tmpTree->setMyAvatar(myAvatar); - bool success = tmpTree->readFromURL(domainURL.toString()); - if (success) { - tmpTree->reaverageOctreeElements(); - tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), 0, 0, 0); - } - std::map namedPaths = tmpTree->getNamedPaths(); - - // we must manually eraseAllOctreeElements(false) else the tmpTree will mem-leak - tmpTree->eraseAllOctreeElements(false); - - return namedPaths; -} - -void Application::loadServerlessDomain(QUrl domainURL) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_ARG(QUrl, domainURL)); - return; - } - - if (domainURL.isEmpty()) { - return; - } - - auto namedPaths = prepareServerlessDomainContents(domainURL); - auto nodeList = DependencyManager::get(); - - nodeList->getDomainHandler().connectedToServerless(namedPaths); - - _fullSceneReceivedCounter++; -} - -void Application::loadErrorDomain(QUrl domainURL) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadErrorDomain", Q_ARG(QUrl, domainURL)); - return; - } - - if (domainURL.isEmpty()) { - return; - } - - auto namedPaths = prepareServerlessDomainContents(domainURL); - auto nodeList = DependencyManager::get(); - - nodeList->getDomainHandler().loadedErrorDomain(namedPaths); - - _fullSceneReceivedCounter++; -} - -bool Application::importImage(const QString& urlString) { - qCDebug(interfaceapp) << "An image file has been dropped in"; - QString filepath(urlString); - filepath.remove("file:///"); - addAssetToWorld(filepath, "", false, false); - return true; -} - -// thread-safe -void Application::onPresent(quint32 frameCount) { - bool expected = false; - if (_pendingIdleEvent.compare_exchange_strong(expected, true)) { - postEvent(this, new QEvent((QEvent::Type)ApplicationEvent::Idle), Qt::HighEventPriority); - } - expected = false; - if (_graphicsEngine.checkPendingRenderEvent() && !isAboutToQuit()) { - postEvent(_graphicsEngine._renderEventHandler, new QEvent((QEvent::Type)ApplicationEvent::Render)); - } -} - -static inline bool isKeyEvent(QEvent::Type type) { - return type == QEvent::KeyPress || type == QEvent::KeyRelease; -} - -bool Application::handleKeyEventForFocusedEntity(QEvent* event) { - if (_keyboardFocusedEntity.get() != UNKNOWN_ENTITY_ID) { - switch (event->type()) { - case QEvent::KeyPress: - case QEvent::KeyRelease: - { - auto eventHandler = getEntities()->getEventHandler(_keyboardFocusedEntity.get()); - if (eventHandler) { - event->setAccepted(false); - QCoreApplication::sendEvent(eventHandler, event); - if (event->isAccepted()) { - _lastAcceptedKeyPress = usecTimestampNow(); - return true; - } - } - break; - } - default: - break; - } - } - - return false; -} - -bool Application::handleFileOpenEvent(QFileOpenEvent* fileEvent) { - QUrl url = fileEvent->url(); - if (!url.isEmpty()) { - QString urlString = url.toString(); - if (canAcceptURL(urlString)) { - return acceptURL(urlString); - } - } - return false; -} - -#ifdef DEBUG_EVENT_QUEUE -static int getEventQueueSize(QThread* thread) { - auto threadData = QThreadData::get2(thread); - QMutexLocker locker(&threadData->postEventList.mutex); - return threadData->postEventList.size(); -} - -static void dumpEventQueue(QThread* thread) { - auto threadData = QThreadData::get2(thread); - QMutexLocker locker(&threadData->postEventList.mutex); - qDebug() << "Event list, size =" << threadData->postEventList.size(); - for (auto& postEvent : threadData->postEventList) { - QEvent::Type type = (postEvent.event ? postEvent.event->type() : QEvent::None); - qDebug() << " " << type; - } -} -#endif // DEBUG_EVENT_QUEUE - -bool Application::event(QEvent* event) { - - if (_aboutToQuit) { - return false; - } - - if (!Menu::getInstance()) { - return false; - } - - // Allow focused Entities to handle keyboard input - if (isKeyEvent(event->type()) && handleKeyEventForFocusedEntity(event)) { - return true; - } - - int type = event->type(); - switch (type) { - case ApplicationEvent::Lambda: - static_cast(event)->call(); - return true; - - // Explicit idle keeps the idle running at a lower interval, but without any rendering - // see (windowMinimizedChanged) - case ApplicationEvent::Idle: - idle(); - -#ifdef DEBUG_EVENT_QUEUE - { - int count = getEventQueueSize(QThread::currentThread()); - if (count > 400) { - dumpEventQueue(QThread::currentThread()); - } - } -#endif // DEBUG_EVENT_QUEUE - - _pendingIdleEvent.store(false); - - return true; - - case QEvent::MouseMove: - mouseMoveEvent(static_cast(event)); - return true; - case QEvent::MouseButtonPress: - mousePressEvent(static_cast(event)); - return true; - case QEvent::MouseButtonDblClick: - mouseDoublePressEvent(static_cast(event)); - return true; - case QEvent::MouseButtonRelease: - mouseReleaseEvent(static_cast(event)); - return true; - case QEvent::KeyPress: - keyPressEvent(static_cast(event)); - return true; - case QEvent::KeyRelease: - keyReleaseEvent(static_cast(event)); - return true; - case QEvent::FocusOut: - focusOutEvent(static_cast(event)); - return true; - case QEvent::TouchBegin: - touchBeginEvent(static_cast(event)); - event->accept(); - return true; - case QEvent::TouchEnd: - touchEndEvent(static_cast(event)); - return true; - case QEvent::TouchUpdate: - touchUpdateEvent(static_cast(event)); - return true; - case QEvent::Gesture: - touchGestureEvent((QGestureEvent*)event); - return true; - case QEvent::Wheel: - wheelEvent(static_cast(event)); - return true; - case QEvent::Drop: - dropEvent(static_cast(event)); - return true; - - case QEvent::FileOpen: - if (handleFileOpenEvent(static_cast(event))) { - return true; - } - break; - - default: - break; - } - - return QApplication::event(event); -} - -bool Application::eventFilter(QObject* object, QEvent* event) { - - if (_aboutToQuit && event->type() != QEvent::DeferredDelete && event->type() != QEvent::Destroy) { - return true; - } - - if (event->type() == QEvent::Leave) { - getApplicationCompositor().handleLeaveEvent(); - } - - if (event->type() == QEvent::ShortcutOverride) { -#if !defined(DISABLE_QML) - if (getOffscreenUI()->shouldSwallowShortcut(event)) { - event->accept(); - return true; - } -#endif - - // Filter out captured keys before they're used for shortcut actions. - if (_controllerScriptingInterface->isKeyCaptured(static_cast(event))) { - event->accept(); - return true; - } - } - - return false; -} - -static bool _altPressed{ false }; - -void Application::keyPressEvent(QKeyEvent* event) { - _altPressed = event->key() == Qt::Key_Alt; - - if (!event->isAutoRepeat()) { - _keysPressed.insert(event->key(), *event); - } - - _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isKeyCaptured(event) || isInterstitialMode()) { - return; - } - - if (hasFocus() && getLoginDialogPoppedUp()) { - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->keyReleaseEvent(event); - } - - bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); - bool isOption = event->modifiers().testFlag(Qt::AltModifier); - switch (event->key()) { - case Qt::Key_4: - case Qt::Key_5: - case Qt::Key_6: - case Qt::Key_7: - if (isMeta || isOption) { - unsigned int index = static_cast(event->key() - Qt::Key_1); - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - if (index < displayPlugins.size()) { - auto targetPlugin = displayPlugins.at(index); - QString targetName = targetPlugin->getName(); - auto menu = Menu::getInstance(); - QAction* action = menu->getActionForOption(targetName); - if (action && !action->isChecked()) { - action->trigger(); - } - } - } - break; - } - } else if (hasFocus()) { - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->keyPressEvent(event); - } - - bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); - bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); - bool isOption = event->modifiers().testFlag(Qt::AltModifier); - switch (event->key()) { - case Qt::Key_Enter: - case Qt::Key_Return: - if (isOption) { - if (_window->isFullScreen()) { - unsetFullscreen(); - } else { - setFullscreen(nullptr); - } - } - break; - - case Qt::Key_1: { - Menu* menu = Menu::getInstance(); - menu->triggerOption(MenuOption::FirstPerson); - break; - } - case Qt::Key_2: { - Menu* menu = Menu::getInstance(); - menu->triggerOption(MenuOption::FullscreenMirror); - break; - } - case Qt::Key_3: { - Menu* menu = Menu::getInstance(); - menu->triggerOption(MenuOption::ThirdPerson); - break; - } - case Qt::Key_4: - case Qt::Key_5: - case Qt::Key_6: - case Qt::Key_7: - if (isMeta || isOption) { - unsigned int index = static_cast(event->key() - Qt::Key_1); - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - if (index < displayPlugins.size()) { - auto targetPlugin = displayPlugins.at(index); - QString targetName = targetPlugin->getName(); - auto menu = Menu::getInstance(); - QAction* action = menu->getActionForOption(targetName); - if (action && !action->isChecked()) { - action->trigger(); - } - } - } - break; - - case Qt::Key_G: - if (isShifted && isMeta && Menu::getInstance() && Menu::getInstance()->getMenu("Developer")->isVisible()) { - static const QString HIFI_FRAMES_FOLDER_VAR = "HIFI_FRAMES_FOLDER"; - static const QString GPU_FRAME_FOLDER = QProcessEnvironment::systemEnvironment().contains(HIFI_FRAMES_FOLDER_VAR) - ? QProcessEnvironment::systemEnvironment().value(HIFI_FRAMES_FOLDER_VAR) - : "hifiFrames"; - static QString GPU_FRAME_TEMPLATE = GPU_FRAME_FOLDER + "/{DATE}_{TIME}"; - QString fullPath = FileUtils::computeDocumentPath(FileUtils::replaceDateTimeTokens(GPU_FRAME_TEMPLATE)); - if (FileUtils::canCreateFile(fullPath)) { - getActiveDisplayPlugin()->captureFrame(fullPath.toStdString()); - } - } - break; - case Qt::Key_X: - if (isShifted && isMeta) { - auto offscreenUi = getOffscreenUI(); - offscreenUi->togglePinned(); - //offscreenUi->getSurfaceContext()->engine()->clearComponentCache(); - //OffscreenUi::information("Debugging", "Component cache cleared"); - // placeholder for dialogs being converted to QML. - } - break; - - case Qt::Key_Y: - if (isShifted && isMeta) { - getActiveDisplayPlugin()->cycleDebugOutput(); - } - break; - - case Qt::Key_B: - if (isMeta) { - auto offscreenUi = getOffscreenUI(); - offscreenUi->load("Browser.qml"); - } else if (isOption) { - controller::InputRecorder* inputRecorder = controller::InputRecorder::getInstance(); - inputRecorder->stopPlayback(); - } - break; - - case Qt::Key_L: - if (isShifted && isMeta) { - Menu::getInstance()->triggerOption(MenuOption::Log); - } else if (isMeta) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->toggleAddressBar(); - } else if (isShifted) { - Menu::getInstance()->triggerOption(MenuOption::LodTools); - } - break; - - case Qt::Key_R: - if (isMeta && !event->isAutoRepeat()) { - DependencyManager::get()->reloadAllScripts(); - getOffscreenUI()->clearCache(); - } - break; - - case Qt::Key_Asterisk: - Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox); - break; - - case Qt::Key_M: - if (isMeta) { - auto audioClient = DependencyManager::get(); - audioClient->setMuted(!audioClient->isMuted()); - } - break; - - case Qt::Key_N: - if (!isOption && !isShifted && isMeta) { - DependencyManager::get()->toggleIgnoreRadius(); - } - break; - - case Qt::Key_S: - if (isShifted && isMeta && !isOption) { - Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); - } - break; - - case Qt::Key_P: { - if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { - AudioInjectorOptions options; - options.localOnly = true; - options.positionSet = false; // system sound - options.stereo = true; - - Setting::Handle notificationSounds{ MenuOption::NotificationSounds, true }; - Setting::Handle notificationSoundSnapshot{ MenuOption::NotificationSoundsSnapshot, true }; - if (notificationSounds.get() && notificationSoundSnapshot.get()) { - if (_snapshotSoundInjector) { - _snapshotSoundInjector->setOptions(options); - _snapshotSoundInjector->restart(); - } else { - _snapshotSoundInjector = AudioInjector::playSound(_snapshotSound, options); - } - } - takeSnapshot(true); - } - break; - } - - case Qt::Key_Apostrophe: { - if (isMeta) { - auto cursor = Cursor::Manager::instance().getCursor(); - auto curIcon = cursor->getIcon(); - if (curIcon == Cursor::Icon::DEFAULT) { - showCursor(Cursor::Icon::RETICLE); - } else if (curIcon == Cursor::Icon::RETICLE) { - showCursor(Cursor::Icon::SYSTEM); - } else if (curIcon == Cursor::Icon::SYSTEM) { - showCursor(Cursor::Icon::LINK); - } else { - showCursor(Cursor::Icon::DEFAULT); - } - } else if (!event->isAutoRepeat()){ - resetSensors(true); - } - break; - } - - case Qt::Key_Backslash: - Menu::getInstance()->triggerOption(MenuOption::Chat); - break; - - case Qt::Key_Slash: - Menu::getInstance()->triggerOption(MenuOption::Stats); - break; - - case Qt::Key_Plus: { - if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { - auto& cursorManager = Cursor::Manager::instance(); - cursorManager.setScale(cursorManager.getScale() * 1.1f); - } else { - getMyAvatar()->increaseSize(); - } - break; - } - - case Qt::Key_Minus: { - if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { - auto& cursorManager = Cursor::Manager::instance(); - cursorManager.setScale(cursorManager.getScale() / 1.1f); - } else { - getMyAvatar()->decreaseSize(); - } - break; - } - - case Qt::Key_Equal: - getMyAvatar()->resetSize(); - break; - case Qt::Key_Escape: { - getActiveDisplayPlugin()->abandonCalibration(); - break; - } - - default: - event->ignore(); - break; - } - } -} - -void Application::keyReleaseEvent(QKeyEvent* event) { - if (!event->isAutoRepeat()) { - _keysPressed.remove(event->key()); - } - -#if defined(Q_OS_ANDROID) - if (event->key() == Qt::Key_Back) { - event->accept(); - AndroidHelper::instance().requestActivity("Home", false); - } -#endif - _controllerScriptingInterface->emitKeyReleaseEvent(event); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isKeyCaptured(event)) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->keyReleaseEvent(event); - } -} - -void Application::focusOutEvent(QFocusEvent* event) { - auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); - foreach(auto inputPlugin, inputPlugins) { - if (inputPlugin->isActive()) { - inputPlugin->pluginFocusOutEvent(); - } - } - -// FIXME spacemouse code still needs cleanup -#if 0 - //SpacemouseDevice::getInstance().focusOutEvent(); - //SpacemouseManager::getInstance().getDevice()->focusOutEvent(); - SpacemouseManager::getInstance().ManagerFocusOutEvent(); -#endif - - synthesizeKeyReleasEvents(); -} - -void Application::synthesizeKeyReleasEvents() { - // synthesize events for keys currently pressed, since we may not get their release events - // Because our key event handlers may manipulate _keysPressed, lets swap the keys pressed into a local copy, - // clearing the existing list. - QHash keysPressed; - std::swap(keysPressed, _keysPressed); - for (auto& ev : keysPressed) { - QKeyEvent synthesizedEvent { QKeyEvent::KeyRelease, ev.key(), Qt::NoModifier, ev.text() }; - keyReleaseEvent(&synthesizedEvent); - } -} - -void Application::maybeToggleMenuVisible(QMouseEvent* event) const { -#ifndef Q_OS_MAC - // If in full screen, and our main windows menu bar is hidden, and we're close to the top of the QMainWindow - // then show the menubar. - if (_window->isFullScreen()) { - QMenuBar* menuBar = _window->menuBar(); - if (menuBar) { - static const int MENU_TOGGLE_AREA = 10; - if (!menuBar->isVisible()) { - if (event->pos().y() <= MENU_TOGGLE_AREA) { - menuBar->setVisible(true); - } - } else { - if (event->pos().y() > MENU_TOGGLE_AREA) { - menuBar->setVisible(false); - } - } - } - } -#endif -} - -void Application::mouseMoveEvent(QMouseEvent* event) { - PROFILE_RANGE(app_input_mouse, __FUNCTION__); - - maybeToggleMenuVisible(event); - - auto& compositor = getApplicationCompositor(); - // if this is a real mouse event, and we're in HMD mode, then we should use it to move the - // compositor reticle - // handleRealMouseMoveEvent() will return true, if we shouldn't process the event further - if (!compositor.fakeEventActive() && compositor.handleRealMouseMoveEvent()) { - return; // bail - } - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto eventPosition = compositor.getMouseEventPosition(event); - QPointF transformedPos = offscreenUi ? offscreenUi->mapToVirtualScreen(eventPosition) : QPointF(); -#else - QPointF transformedPos; -#endif - auto button = event->button(); - auto buttons = event->buttons(); - // Determine if the ReticleClick Action is 1 and if so, fake include the LeftMouseButton - if (_reticleClickPressed) { - if (button == Qt::NoButton) { - button = Qt::LeftButton; - } - buttons |= Qt::LeftButton; - } - - QMouseEvent mappedEvent(event->type(), - transformedPos, - event->screenPos(), button, - buttons, event->modifiers()); - - if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || - getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) { - getEntities()->mouseMoveEvent(&mappedEvent); - } - - _controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->mouseMoveEvent(event); - } -} - -void Application::mousePressEvent(QMouseEvent* event) { - // Inhibit the menu if the user is using alt-mouse dragging - _altPressed = false; - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - // If we get a mouse press event it means it wasn't consumed by the offscreen UI, - // hence, we should defocus all of the offscreen UI windows, in order to allow - // keyboard shortcuts not to be swallowed by them. In particular, WebEngineViews - // will consume all keyboard events. - offscreenUi->unfocusWindows(); - - auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); - QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); -#else - QPointF transformedPos; -#endif - - QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); - QUuid result = getEntities()->mousePressEvent(&mappedEvent); - setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); - - _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - -#if defined(Q_OS_MAC) - // Fix for OSX right click dragging on window when coming from a native window - bool isFocussed = hasFocus(); - if (!isFocussed && event->button() == Qt::MouseButton::RightButton) { - setFocus(); - isFocussed = true; - } - - if (isFocussed) { -#else - if (hasFocus()) { -#endif - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->mousePressEvent(event); - } - } -} - -void Application::mouseDoublePressEvent(QMouseEvent* event) { -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); - QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); -#else - QPointF transformedPos; -#endif - QMouseEvent mappedEvent(event->type(), - transformedPos, - event->screenPos(), event->button(), - event->buttons(), event->modifiers()); - getEntities()->mouseDoublePressEvent(&mappedEvent); - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - - _controllerScriptingInterface->emitMouseDoublePressEvent(event); -} - -void Application::mouseReleaseEvent(QMouseEvent* event) { - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); - QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); -#else - QPointF transformedPos; -#endif - QMouseEvent mappedEvent(event->type(), - transformedPos, - event->screenPos(), event->button(), - event->buttons(), event->modifiers()); - - getEntities()->mouseReleaseEvent(&mappedEvent); - - _controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - - if (hasFocus()) { - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->mouseReleaseEvent(event); - } - } -} - -void Application::touchUpdateEvent(QTouchEvent* event) { - _altPressed = false; - - if (event->type() == QEvent::TouchUpdate) { - TouchEvent thisEvent(*event, _lastTouchEvent); - _controllerScriptingInterface->emitTouchUpdateEvent(thisEvent); // send events to any registered scripts - _lastTouchEvent = thisEvent; - } - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isTouchCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->touchUpdateEvent(event); - } - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchUpdateEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchUpdateEvent(event); - } -} - -void Application::touchBeginEvent(QTouchEvent* event) { - _altPressed = false; - TouchEvent thisEvent(*event); // on touch begin, we don't compare to last event - _controllerScriptingInterface->emitTouchBeginEvent(thisEvent); // send events to any registered scripts - - _lastTouchEvent = thisEvent; // and we reset our last event to this event before we call our update - touchUpdateEvent(event); - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isTouchCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->touchBeginEvent(event); - } - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchBeginEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchBeginEvent(event); - } - -} - -void Application::touchEndEvent(QTouchEvent* event) { - _altPressed = false; - TouchEvent thisEvent(*event, _lastTouchEvent); - _controllerScriptingInterface->emitTouchEndEvent(thisEvent); // send events to any registered scripts - _lastTouchEvent = thisEvent; - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isTouchCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->touchEndEvent(event); - } - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchEndEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchEndEvent(event); - } - // put any application specific touch behavior below here.. -} - -void Application::touchGestureEvent(QGestureEvent* event) { - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchGestureEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchGestureEvent(event); - } -} - -void Application::wheelEvent(QWheelEvent* event) const { - _altPressed = false; - _controllerScriptingInterface->emitWheelEvent(event); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isWheelCaptured() || getLoginDialogPoppedUp()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->wheelEvent(event); - } -} - -void Application::dropEvent(QDropEvent *event) { - const QMimeData* mimeData = event->mimeData(); - for (auto& url : mimeData->urls()) { - QString urlString = url.toString(); - if (acceptURL(urlString, true)) { - event->acceptProposedAction(); - } - } -} - -void Application::dragEnterEvent(QDragEnterEvent* event) { - event->acceptProposedAction(); -} - -// This is currently not used, but could be invoked if the user wants to go to the place embedded in an -// Interface-taken snapshot. (It was developed for drag and drop, before we had asset-server loading or in-world browsers.) -bool Application::acceptSnapshot(const QString& urlString) { - QUrl url(urlString); - QString snapshotPath = url.toLocalFile(); - - SnapshotMetaData* snapshotData = DependencyManager::get()->parseSnapshotData(snapshotPath); - if (snapshotData) { - if (!snapshotData->getURL().toString().isEmpty()) { - DependencyManager::get()->handleLookupString(snapshotData->getURL().toString()); - } - } else { - OffscreenUi::asyncWarning("", "No location details were found in the file\n" + - snapshotPath + "\nTry dragging in an authentic Hifi snapshot."); - } - return true; -} - -#ifdef Q_OS_WIN -#include -#include -#include -#pragma comment(lib, "pdh.lib") -#pragma comment(lib, "ntdll.lib") - -extern "C" { - enum SYSTEM_INFORMATION_CLASS { - SystemBasicInformation = 0, - SystemProcessorPerformanceInformation = 8, - }; - - struct SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION { - LARGE_INTEGER IdleTime; - LARGE_INTEGER KernelTime; - LARGE_INTEGER UserTime; - LARGE_INTEGER DpcTime; - LARGE_INTEGER InterruptTime; - ULONG InterruptCount; - }; - - struct SYSTEM_BASIC_INFORMATION { - ULONG Reserved; - ULONG TimerResolution; - ULONG PageSize; - ULONG NumberOfPhysicalPages; - ULONG LowestPhysicalPageNumber; - ULONG HighestPhysicalPageNumber; - ULONG AllocationGranularity; - ULONG_PTR MinimumUserModeAddress; - ULONG_PTR MaximumUserModeAddress; - ULONG_PTR ActiveProcessorsAffinityMask; - CCHAR NumberOfProcessors; - }; - - NTSYSCALLAPI NTSTATUS NTAPI NtQuerySystemInformation( - _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass, - _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation, - _In_ ULONG SystemInformationLength, - _Out_opt_ PULONG ReturnLength - ); - -} -template -NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, T& t) { - return NtQuerySystemInformation(SystemInformationClass, &t, (ULONG)sizeof(T), nullptr); -} - -template -NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, std::vector& t) { - return NtQuerySystemInformation(SystemInformationClass, t.data(), (ULONG)(sizeof(T) * t.size()), nullptr); -} - - -template -void updateValueAndDelta(std::pair& pair, T newValue) { - auto& value = pair.first; - auto& delta = pair.second; - delta = (value != 0) ? newValue - value : 0; - value = newValue; -} - -struct MyCpuInfo { - using ValueAndDelta = std::pair; - std::string name; - ValueAndDelta kernel { 0, 0 }; - ValueAndDelta user { 0, 0 }; - ValueAndDelta idle { 0, 0 }; - float kernelUsage { 0.0f }; - float userUsage { 0.0f }; - - void update(const SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION& cpuInfo) { - updateValueAndDelta(kernel, cpuInfo.KernelTime.QuadPart); - updateValueAndDelta(user, cpuInfo.UserTime.QuadPart); - updateValueAndDelta(idle, cpuInfo.IdleTime.QuadPart); - auto totalTime = kernel.second + user.second + idle.second; - if (totalTime != 0) { - kernelUsage = (FLOAT)kernel.second / totalTime; - userUsage = (FLOAT)user.second / totalTime; - } else { - kernelUsage = userUsage = 0.0f; - } - } -}; - -void updateCpuInformation() { - static std::once_flag once; - static SYSTEM_BASIC_INFORMATION systemInfo {}; - static SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION cpuTotals; - static std::vector cpuInfos; - static std::vector myCpuInfos; - static MyCpuInfo myCpuTotals; - std::call_once(once, [&] { - NtQuerySystemInformation( SystemBasicInformation, systemInfo); - cpuInfos.resize(systemInfo.NumberOfProcessors); - myCpuInfos.resize(systemInfo.NumberOfProcessors); - for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { - myCpuInfos[i].name = "cpu." + std::to_string(i); - } - myCpuTotals.name = "cpu.total"; - }); - NtQuerySystemInformation(SystemProcessorPerformanceInformation, cpuInfos); - - // Zero the CPU totals. - memset(&cpuTotals, 0, sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); - for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { - auto& cpuInfo = cpuInfos[i]; - // KernelTime includes IdleTime. - cpuInfo.KernelTime.QuadPart -= cpuInfo.IdleTime.QuadPart; - - // Update totals - cpuTotals.IdleTime.QuadPart += cpuInfo.IdleTime.QuadPart; - cpuTotals.KernelTime.QuadPart += cpuInfo.KernelTime.QuadPart; - cpuTotals.UserTime.QuadPart += cpuInfo.UserTime.QuadPart; - - // Update friendly structure - auto& myCpuInfo = myCpuInfos[i]; - myCpuInfo.update(cpuInfo); - PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { - { "kernel", myCpuInfo.kernelUsage }, - { "user", myCpuInfo.userUsage } - }); - } - - myCpuTotals.update(cpuTotals); - PROFILE_COUNTER(app, myCpuTotals.name.c_str(), { - { "kernel", myCpuTotals.kernelUsage }, - { "user", myCpuTotals.userUsage } - }); -} - - -static ULARGE_INTEGER lastCPU, lastSysCPU, lastUserCPU; -static int numProcessors; -static HANDLE self; -static PDH_HQUERY cpuQuery; -static PDH_HCOUNTER cpuTotal; - -void initCpuUsage() { - SYSTEM_INFO sysInfo; - FILETIME ftime, fsys, fuser; - - GetSystemInfo(&sysInfo); - numProcessors = sysInfo.dwNumberOfProcessors; - - GetSystemTimeAsFileTime(&ftime); - memcpy(&lastCPU, &ftime, sizeof(FILETIME)); - - self = GetCurrentProcess(); - GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); - memcpy(&lastSysCPU, &fsys, sizeof(FILETIME)); - memcpy(&lastUserCPU, &fuser, sizeof(FILETIME)); - - PdhOpenQuery(NULL, NULL, &cpuQuery); - PdhAddCounter(cpuQuery, "\\Processor(_Total)\\% Processor Time", NULL, &cpuTotal); - PdhCollectQueryData(cpuQuery); -} - -void getCpuUsage(vec3& systemAndUser) { - FILETIME ftime, fsys, fuser; - ULARGE_INTEGER now, sys, user; - - GetSystemTimeAsFileTime(&ftime); - memcpy(&now, &ftime, sizeof(FILETIME)); - - GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); - memcpy(&sys, &fsys, sizeof(FILETIME)); - memcpy(&user, &fuser, sizeof(FILETIME)); - systemAndUser.x = (sys.QuadPart - lastSysCPU.QuadPart); - systemAndUser.y = (user.QuadPart - lastUserCPU.QuadPart); - systemAndUser /= (float)(now.QuadPart - lastCPU.QuadPart); - systemAndUser /= (float)numProcessors; - systemAndUser *= 100.0f; - lastCPU = now; - lastUserCPU = user; - lastSysCPU = sys; - - PDH_FMT_COUNTERVALUE counterVal; - PdhCollectQueryData(cpuQuery); - PdhGetFormattedCounterValue(cpuTotal, PDH_FMT_DOUBLE, NULL, &counterVal); - systemAndUser.z = (float)counterVal.doubleValue; -} - -void setupCpuMonitorThread() { - initCpuUsage(); - auto cpuMonitorThread = QThread::currentThread(); - - QTimer* timer = new QTimer(); - timer->setInterval(50); - QObject::connect(timer, &QTimer::timeout, [] { - updateCpuInformation(); - vec3 kernelUserAndSystem; - getCpuUsage(kernelUserAndSystem); - PROFILE_COUNTER(app, "cpuProcess", { { "system", kernelUserAndSystem.x }, { "user", kernelUserAndSystem.y } }); - PROFILE_COUNTER(app, "cpuSystem", { { "system", kernelUserAndSystem.z } }); - }); - QObject::connect(cpuMonitorThread, &QThread::finished, [=] { - timer->deleteLater(); - cpuMonitorThread->deleteLater(); - }); - timer->start(); -} - -#endif - -void Application::idle() { - PerformanceTimer perfTimer("idle"); - - // Update the deadlock watchdog - updateHeartbeat(); - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - - // These tasks need to be done on our first idle, because we don't want the showing of - // overlay subwindows to do a showDesktop() until after the first time through - static bool firstIdle = true; - if (firstIdle) { - firstIdle = false; - connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); - } -#endif - -#ifdef Q_OS_WIN - // If tracing is enabled then monitor the CPU in a separate thread - static std::once_flag once; - std::call_once(once, [&] { - if (trace_app().isDebugEnabled()) { - QThread* cpuMonitorThread = new QThread(qApp); - cpuMonitorThread->setObjectName("cpuMonitorThread"); - QObject::connect(cpuMonitorThread, &QThread::started, [this] { setupCpuMonitorThread(); }); - QObject::connect(qApp, &QCoreApplication::aboutToQuit, cpuMonitorThread, &QThread::quit); - cpuMonitorThread->start(); - } - }); -#endif - - auto displayPlugin = getActiveDisplayPlugin(); -#if !defined(DISABLE_QML) - if (displayPlugin) { - auto uiSize = displayPlugin->getRecommendedUiSize(); - // Bit of a hack since there's no device pixel ratio change event I can find. - if (offscreenUi->size() != fromGlm(uiSize)) { - qCDebug(interfaceapp) << "Device pixel ratio changed, triggering resize to " << uiSize; - offscreenUi->resize(fromGlm(uiSize)); - } - } -#endif - - if (displayPlugin) { - PROFILE_COUNTER_IF_CHANGED(app, "present", float, displayPlugin->presentRate()); - } - PROFILE_COUNTER_IF_CHANGED(app, "renderLoopRate", float, getRenderLoopRate()); - PROFILE_COUNTER_IF_CHANGED(app, "currentDownloads", uint32_t, ResourceCache::getLoadingRequests().length()); - PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", uint32_t, ResourceCache::getPendingRequestCount()); - PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get()->getStat("Processing").toInt()); - PROFILE_COUNTER_IF_CHANGED(app, "pendingProcessing", int, DependencyManager::get()->getStat("PendingProcessing").toInt()); - auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); - PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_graphicsEngine.getGPUContext()->getFrameTimerGPUAverage()); - - PROFILE_RANGE(app, __FUNCTION__); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - steamClient->runCallbacks(); - } - - if (auto oculusPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { - oculusPlugin->handleOVREvents(); - } - - float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND; - _lastTimeUpdated.start(); - -#if !defined(DISABLE_QML) - // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. - if (offscreenUi && offscreenUi->getWindow()) { - auto activeFocusItem = offscreenUi->getWindow()->activeFocusItem(); - if (_keyboardDeviceHasFocus && activeFocusItem != offscreenUi->getRootItem()) { - _keyboardMouseDevice->pluginFocusOutEvent(); - _keyboardDeviceHasFocus = false; - synthesizeKeyReleasEvents(); - } else if (activeFocusItem == offscreenUi->getRootItem()) { - _keyboardDeviceHasFocus = true; - } - } -#endif - - checkChangeCursor(); - -#if !defined(DISABLE_QML) - auto stats = Stats::getInstance(); - if (stats) { - stats->updateStats(); - } - auto animStats = AnimStats::getInstance(); - if (animStats) { - animStats->updateStats(); - } -#endif - - // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing - // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing - // details normally. -#ifdef Q_OS_ANDROID - bool showWarnings = false; -#else - bool showWarnings = getLogger()->extraDebugging(); -#endif - PerformanceWarning warn(showWarnings, "idle()"); - - { - _gameWorkload.updateViews(_viewFrustum, getMyAvatar()->getHeadPosition()); - _gameWorkload._engine->run(); - } - { - PerformanceTimer perfTimer("update"); - PerformanceWarning warn(showWarnings, "Application::idle()... update()"); - static const float BIGGEST_DELTA_TIME_SECS = 0.25f; - update(glm::clamp(secondsSinceLastUpdate, 0.0f, BIGGEST_DELTA_TIME_SECS)); - } - - { // Update keyboard focus highlight - if (!_keyboardFocusedEntity.get().isInvalidID()) { - const quint64 LOSE_FOCUS_AFTER_ELAPSED_TIME = 30 * USECS_PER_SECOND; // if idle for 30 seconds, drop focus - quint64 elapsedSinceAcceptedKeyPress = usecTimestampNow() - _lastAcceptedKeyPress; - if (elapsedSinceAcceptedKeyPress > LOSE_FOCUS_AFTER_ELAPSED_TIME) { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - } else { - if (auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get())) { - EntityItemProperties properties; - properties.setPosition(entity->getWorldPosition()); - properties.setRotation(entity->getWorldOrientation()); - properties.setDimensions(entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); - DependencyManager::get()->editEntity(_keyboardFocusHighlightID, properties); - } - } - } - } - - { - if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) { - QUuid entityId = _keyboardFocusedEntity.get(); - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - _keyboardFocusWaitingOnRenderable = false; - setKeyboardFocusEntity(entityId); - } - } - - { - PerformanceTimer perfTimer("pluginIdle"); - PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()"); - getActiveDisplayPlugin()->idle(); - auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); - foreach(auto inputPlugin, inputPlugins) { - if (inputPlugin->isActive()) { - inputPlugin->idle(); - } - } - } - { - PerformanceTimer perfTimer("rest"); - PerformanceWarning warn(showWarnings, "Application::idle()... rest of it"); - _idleLoopStdev.addValue(secondsSinceLastUpdate); - - // Record standard deviation and reset counter if needed - const int STDEV_SAMPLES = 500; - if (_idleLoopStdev.getSamples() > STDEV_SAMPLES) { - _idleLoopMeasuredJitter = _idleLoopStdev.getStDev(); - _idleLoopStdev.reset(); - } - } - - _overlayConductor.update(secondsSinceLastUpdate); - - _gameLoopCounter.increment(); -} - -ivec2 Application::getMouse() const { - return getApplicationCompositor().getReticlePosition(); -} - -FaceTracker* Application::getActiveFaceTracker() { - auto dde = DependencyManager::get(); - - return dde->isActive() ? static_cast(dde.data()) : nullptr; -} - -FaceTracker* Application::getSelectedFaceTracker() { - FaceTracker* faceTracker = nullptr; -#ifdef HAVE_DDE - if (Menu::getInstance()->isOptionChecked(MenuOption::UseCamera)) { - faceTracker = DependencyManager::get().data(); - } -#endif - return faceTracker; -} - -void Application::setActiveFaceTracker() const { -#ifdef HAVE_DDE - bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); - bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); - Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::UseAudioForMouth)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::VelocityFilter)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setVisible(isUsingDDE); - auto ddeTracker = DependencyManager::get(); - ddeTracker->setIsMuted(isMuted); - ddeTracker->setEnabled(isUsingDDE && !isMuted); -#endif -} - -#ifdef HAVE_IVIEWHMD -void Application::setActiveEyeTracker() { - auto eyeTracker = DependencyManager::get(); - if (!eyeTracker->isInitialized()) { - return; - } - - bool isEyeTracking = Menu::getInstance()->isOptionChecked(MenuOption::SMIEyeTracking); - bool isSimulating = Menu::getInstance()->isOptionChecked(MenuOption::SimulateEyeTracking); - eyeTracker->setEnabled(isEyeTracking, isSimulating); - - Menu::getInstance()->getActionForOption(MenuOption::OnePointCalibration)->setEnabled(isEyeTracking && !isSimulating); - Menu::getInstance()->getActionForOption(MenuOption::ThreePointCalibration)->setEnabled(isEyeTracking && !isSimulating); - Menu::getInstance()->getActionForOption(MenuOption::FivePointCalibration)->setEnabled(isEyeTracking && !isSimulating); -} - -void Application::calibrateEyeTracker1Point() { - DependencyManager::get()->calibrate(1); -} - -void Application::calibrateEyeTracker3Points() { - DependencyManager::get()->calibrate(3); -} - -void Application::calibrateEyeTracker5Points() { - DependencyManager::get()->calibrate(5); -} -#endif - -bool Application::exportEntities(const QString& filename, - const QVector& entityIDs, - const glm::vec3* givenOffset) { - QHash entities; - - auto nodeList = DependencyManager::get(); - const QUuid myAvatarID = nodeList->getSessionUUID(); - - auto entityTree = getEntities()->getTree(); - auto exportTree = std::make_shared(); - exportTree->setMyAvatar(getMyAvatar()); - exportTree->createRootElement(); - glm::vec3 root(TREE_SCALE, TREE_SCALE, TREE_SCALE); - bool success = true; - entityTree->withReadLock([entityIDs, entityTree, givenOffset, myAvatarID, &root, &entities, &success, &exportTree] { - for (auto entityID : entityIDs) { // Gather entities and properties. - auto entityItem = entityTree->findEntityByEntityItemID(entityID); - if (!entityItem) { - qCWarning(interfaceapp) << "Skipping export of" << entityID << "that is not in scene."; - continue; - } - - if (!givenOffset) { - EntityItemID parentID = entityItem->getParentID(); - bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); - if (!parentIsAvatar && (parentID.isInvalidID() || - !entityIDs.contains(parentID) || - !entityTree->findEntityByEntityItemID(parentID))) { - // If parent wasn't selected, we want absolute position, which isn't in properties. - auto position = entityItem->getWorldPosition(); - root.x = glm::min(root.x, position.x); - root.y = glm::min(root.y, position.y); - root.z = glm::min(root.z, position.z); - } - } - entities[entityID] = entityItem; - } - - if (entities.size() == 0) { - success = false; - return; - } - - if (givenOffset) { - root = *givenOffset; - } - for (EntityItemPointer& entityDatum : entities) { - auto properties = entityDatum->getProperties(); - EntityItemID parentID = properties.getParentID(); - bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); - if (parentIsAvatar) { - properties.setParentID(AVATAR_SELF_ID); - } else { - if (parentID.isInvalidID()) { - properties.setPosition(properties.getPosition() - root); - } else if (!entities.contains(parentID)) { - entityDatum->globalizeProperties(properties, "Parent %3 of %2 %1 is not selected for export.", -root); - } // else valid parent -- don't offset - } - exportTree->addEntity(entityDatum->getEntityItemID(), properties); - } - }); - if (success) { - success = exportTree->writeToJSONFile(filename.toLocal8Bit().constData()); - - // restore the main window's active state - _window->activateWindow(); - } - return success; -} - -bool Application::exportEntities(const QString& filename, float x, float y, float z, float scale) { - glm::vec3 center(x, y, z); - glm::vec3 minCorner = center - vec3(scale); - float cubeSize = scale * 2; - AACube boundingCube(minCorner, cubeSize); - QVector entities; - auto entityTree = getEntities()->getTree(); - entityTree->withReadLock([&] { - entityTree->evalEntitiesInCube(boundingCube, PickFilter(), entities); - }); - return exportEntities(filename, entities, ¢er); -} - -void Application::loadSettings() { - - sessionRunTime.set(0); // Just clean living. We're about to saveSettings, which will update value. - DependencyManager::get()->loadSettings(); - DependencyManager::get()->loadSettings(); - - // DONT CHECK IN - //DependencyManager::get()->setAutomaticLODAdjust(false); - - auto menu = Menu::getInstance(); - menu->loadSettings(); - - // override the menu option show overlays to always be true on startup - menu->setIsOptionChecked(MenuOption::Overlays, true); - - // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. - auto pluginManager = PluginManager::getInstance(); - auto plugins = pluginManager->getPreferredDisplayPlugins(); - if (plugins.size() > 0) { - for (auto plugin : plugins) { - if (auto action = menu->getActionForOption(plugin->getName())) { - action->setChecked(true); - action->trigger(); - // Find and activated highest priority plugin, bail for the rest - break; - } - } - } - - bool isFirstPerson = false; - if (_firstRun.get()) { - // If this is our first run, and no preferred devices were set, default to - // an HMD device if available. - auto displayPlugins = pluginManager->getDisplayPlugins(); - for (auto& plugin : displayPlugins) { - if (plugin->isHmd()) { - if (auto action = menu->getActionForOption(plugin->getName())) { - action->setChecked(true); - action->trigger(); - break; - } - } - } - - isFirstPerson = (qApp->isHMDMode()); - - } else { - // if this is not the first run, the camera will be initialized differently depending on user settings - - if (qApp->isHMDMode()) { - // if the HMD is active, use first-person camera, unless the appropriate setting is checked - isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonHMD); - } else { - // if HMD is not active, only use first person if the menu option is checked - isFirstPerson = menu->isOptionChecked(MenuOption::FirstPerson); - } - } - - // finish initializing the camera, based on everything we checked above. Third person camera will be used if no settings - // dictated that we should be in first person - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, isFirstPerson); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !isFirstPerson); - _myCamera.setMode((isFirstPerson) ? CAMERA_MODE_FIRST_PERSON : CAMERA_MODE_THIRD_PERSON); - cameraMenuChanged(); - - auto inputs = pluginManager->getInputPlugins(); - for (auto plugin : inputs) { - if (!plugin->isActive()) { - plugin->activate(); - } - } - - getMyAvatar()->loadData(); - _settingsLoaded = true; -} - -void Application::saveSettings() const { - sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SECOND); - DependencyManager::get()->saveSettings(); - DependencyManager::get()->saveSettings(); - - Menu::getInstance()->saveSettings(); - getMyAvatar()->saveData(); - PluginManager::getInstance()->saveSettings(); -} - -bool Application::importEntities(const QString& urlOrFilename, const bool isObservable, const qint64 callerId) { - bool success = false; - _entityClipboard->withWriteLock([&] { - _entityClipboard->eraseAllOctreeElements(); - - success = _entityClipboard->readFromURL(urlOrFilename, isObservable, callerId); - if (success) { - _entityClipboard->reaverageOctreeElements(); - } - }); - return success; -} - -QVector Application::pasteEntities(float x, float y, float z) { - return _entityClipboard->sendEntities(&_entityEditSender, getEntities()->getTree(), x, y, z); -} - -void Application::init() { - // Make sure Login state is up to date -#if !defined(DISABLE_QML) - DependencyManager::get()->toggleLoginDialog(); -#endif - DependencyManager::get()->init(); - - _timerStart.start(); - _lastTimeUpdated.start(); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - // when +connect_lobby in command line, join steam lobby - const QString STEAM_LOBBY_COMMAND_LINE_KEY = "+connect_lobby"; - int lobbyIndex = arguments().indexOf(STEAM_LOBBY_COMMAND_LINE_KEY); - if (lobbyIndex != -1) { - QString lobbyId = arguments().value(lobbyIndex + 1); - steamClient->joinLobby(lobbyId); - } - } - - - qCDebug(interfaceapp) << "Loaded settings"; - - // fire off an immediate domain-server check in now that settings are loaded - if (!isServerlessMode()) { - DependencyManager::get()->sendDomainServerCheckIn(); - } - - // This allows collision to be set up properly for shape entities supported by GeometryCache. - // This is before entity setup to ensure that it's ready for whenever instance collision is initialized. - ShapeEntityItem::setShapeInfoCalulator(ShapeEntityItem::ShapeInfoCalculator(&shapeInfoCalculator)); - - getEntities()->init(); - getEntities()->setEntityLoadingPriorityFunction([this](const EntityItem& item) { - auto dims = item.getScaledDimensions(); - auto maxSize = glm::compMax(dims); - - if (maxSize <= 0.0f) { - return 0.0f; - } - - auto distance = glm::distance(getMyAvatar()->getWorldPosition(), item.getWorldPosition()); - return atan2(maxSize, distance); - }); - - ObjectMotionState::setShapeManager(&_shapeManager); - _physicsEngine->init(); - - EntityTreePointer tree = getEntities()->getTree(); - _entitySimulation->init(tree, _physicsEngine, &_entityEditSender); - tree->setSimulation(_entitySimulation); - - auto entityScriptingInterface = DependencyManager::get(); - - // connect the _entityCollisionSystem to our EntityTreeRenderer since that's what handles running entity scripts - connect(_entitySimulation.get(), &PhysicalEntitySimulation::entityCollisionWithEntity, - getEntities().data(), &EntityTreeRenderer::entityCollisionWithEntity); - - // connect the _entities (EntityTreeRenderer) to our script engine's EntityScriptingInterface for firing - // of events related clicking, hovering over, and entering entities - getEntities()->connectSignalsToSlots(entityScriptingInterface.data()); - - // Make sure any new sounds are loaded as soon as know about them. - connect(tree.get(), &EntityTree::newCollisionSoundURL, this, [this](QUrl newURL, EntityItemID id) { - getEntities()->setCollisionSound(id, DependencyManager::get()->getSound(newURL)); - }, Qt::QueuedConnection); - connect(getMyAvatar().get(), &MyAvatar::newCollisionSoundURL, this, [this](QUrl newURL) { - if (auto avatar = getMyAvatar()) { - auto sound = DependencyManager::get()->getSound(newURL); - avatar->setCollisionSound(sound); - } - }, Qt::QueuedConnection); - - _gameWorkload.startup(getEntities()->getWorkloadSpace(), _graphicsEngine.getRenderScene(), _entitySimulation); - _entitySimulation->setWorkloadSpace(getEntities()->getWorkloadSpace()); -} - -void Application::pauseUntilLoginDetermined() { - if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "pauseUntilLoginDetermined"); - return; - } - - auto myAvatar = getMyAvatar(); - _previousAvatarTargetScale = myAvatar->getTargetScale(); - _previousAvatarSkeletonModel = myAvatar->getSkeletonModelURL().toString(); - myAvatar->setTargetScale(1.0f); - myAvatar->setSkeletonModelURLFromScript(myAvatar->defaultFullAvatarModelUrl().toString()); - myAvatar->setEnableMeshVisible(false); - - _controllerScriptingInterface->disableMapping(STANDARD_TO_ACTION_MAPPING_NAME); - - { - auto userInputMapper = DependencyManager::get(); - if (userInputMapper->loadMapping(NO_MOVEMENT_MAPPING_JSON)) { - _controllerScriptingInterface->enableMapping(NO_MOVEMENT_MAPPING_NAME); - } - } - - const auto& nodeList = DependencyManager::get(); - // save interstitial mode setting until resuming. - _interstitialModeEnabled = nodeList->getDomainHandler().getInterstitialModeEnabled(); - nodeList->getDomainHandler().setInterstitialModeEnabled(false); - - auto menu = Menu::getInstance(); - menu->getMenu("Edit")->setVisible(false); - menu->getMenu("View")->setVisible(false); - menu->getMenu("Navigate")->setVisible(false); - menu->getMenu("Settings")->setVisible(false); - _developerMenuVisible = menu->getMenu("Developer")->isVisible(); - menu->setIsOptionChecked(MenuOption::Stats, false); - if (_developerMenuVisible) { - menu->getMenu("Developer")->setVisible(false); - } - _previousCameraMode = _myCamera.getMode(); - _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - cameraModeChanged(); - - // disconnect domain handler. - nodeList->getDomainHandler().disconnect(); - -} - -void Application::resumeAfterLoginDialogActionTaken() { - if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "resumeAfterLoginDialogActionTaken"); - return; - } - - if (!isHMDMode() && getDesktopTabletBecomesToolbarSetting()) { - auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); - toolbar->writeProperty("visible", true); - } else { - getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(true); - getApplicationCompositor().getReticleInterface()->setVisible(true); - } - - updateSystemTabletMode(); - - { - auto userInputMapper = DependencyManager::get(); - userInputMapper->unloadMapping(NO_MOVEMENT_MAPPING_JSON); - _controllerScriptingInterface->disableMapping(NO_MOVEMENT_MAPPING_NAME); - } - - auto myAvatar = getMyAvatar(); - myAvatar->setTargetScale(_previousAvatarTargetScale); - myAvatar->setSkeletonModelURLFromScript(_previousAvatarSkeletonModel); - myAvatar->setEnableMeshVisible(true); - - _controllerScriptingInterface->enableMapping(STANDARD_TO_ACTION_MAPPING_NAME); - - const auto& nodeList = DependencyManager::get(); - nodeList->getDomainHandler().setInterstitialModeEnabled(_interstitialModeEnabled); - { - auto scriptEngines = DependencyManager::get().data(); - // this will force the model the look at the correct directory (weird order of operations issue) - scriptEngines->reloadLocalFiles(); - - // if the --scripts command-line argument was used. - if (!_defaultScriptsLocation.exists() && (arguments().indexOf(QString("--").append(SCRIPTS_SWITCH))) != -1) { - scriptEngines->loadDefaultScripts(); - scriptEngines->defaultScriptsLocationOverridden(true); - } else { - scriptEngines->loadScripts(); - } - } - - auto accountManager = DependencyManager::get(); - auto addressManager = DependencyManager::get(); - - // restart domain handler. - nodeList->getDomainHandler().resetting(); - - QVariant testProperty = property(hifi::properties::TEST); - if (testProperty.isValid()) { - const auto testScript = property(hifi::properties::TEST).toUrl(); - // Set last parameter to exit interface when the test script finishes, if so requested - DependencyManager::get()->loadScript(testScript, false, false, false, false, quitWhenFinished); - // This is done so we don't get a "connection time-out" message when we haven't passed in a URL. - if (arguments().contains("--url")) { - auto reply = SandboxUtils::getStatus(); - connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); - } - } else { - auto reply = SandboxUtils::getStatus(); - connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); - } - - auto menu = Menu::getInstance(); - menu->getMenu("Edit")->setVisible(true); - menu->getMenu("View")->setVisible(true); - menu->getMenu("Navigate")->setVisible(true); - menu->getMenu("Settings")->setVisible(true); - menu->getMenu("Developer")->setVisible(_developerMenuVisible); - _myCamera.setMode(_previousCameraMode); - cameraModeChanged(); -} - -void Application::loadAvatarScripts(const QVector& urls) { - auto scriptEngines = DependencyManager::get(); - auto runningScripts = scriptEngines->getRunningScripts(); - for (auto url : urls) { - int index = runningScripts.indexOf(url); - if (index < 0) { - auto scriptEnginePointer = scriptEngines->loadScript(url, false); - if (scriptEnginePointer) { - scriptEnginePointer->setType(ScriptEngine::Type::AVATAR); - } - } - } -} - -void Application::unloadAvatarScripts() { - auto scriptEngines = DependencyManager::get(); - auto urls = scriptEngines->getRunningScripts(); - for (auto url : urls) { - auto scriptEngine = scriptEngines->getScriptEngine(url); - if (scriptEngine->getType() == ScriptEngine::Type::AVATAR) { - scriptEngines->stopScript(url, false); - } - } -} - -void Application::updateLOD(float deltaTime) const { - PerformanceTimer perfTimer("LOD"); - // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode - if (!isThrottleRendering()) { - float presentTime = getActiveDisplayPlugin()->getAveragePresentTime(); - float engineRunTime = (float)(_graphicsEngine.getRenderEngine()->getConfiguration().get()->getCPURunTime()); - float gpuTime = getGPUContext()->getFrameTimerGPUAverage(); - float batchTime = getGPUContext()->getFrameTimerBatchAverage(); - auto lodManager = DependencyManager::get(); - lodManager->setRenderTimes(presentTime, engineRunTime, batchTime, gpuTime); - lodManager->autoAdjustLOD(deltaTime); - } else { - DependencyManager::get()->resetLODAdjust(); - } -} - -void Application::pushPostUpdateLambda(void* key, const std::function& func) { - std::unique_lock guard(_postUpdateLambdasLock); - _postUpdateLambdas[key] = func; -} - -// Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent to everyone. -// (Maybe this code should be moved there?) -// The principal result is to call updateLookAtTargetAvatar() and then setLookAtPosition(). -// Note that it is called BEFORE we update position or joints based on sensors, etc. -void Application::updateMyAvatarLookAtPosition() { - PerformanceTimer perfTimer("lookAt"); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); - - auto myAvatar = getMyAvatar(); - myAvatar->updateLookAtTargetAvatar(); - FaceTracker* faceTracker = getActiveFaceTracker(); - auto eyeTracker = DependencyManager::get(); - - bool isLookingAtSomeone = false; - bool isHMD = qApp->isHMDMode(); - glm::vec3 lookAtSpot; - if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { - // Look at the point that the user is looking at. - glm::vec3 lookAtPosition = eyeTracker->getLookAtPosition(); - if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - lookAtPosition.x = -lookAtPosition.x; - } - if (isHMD) { - // TODO -- this code is probably wrong, getHeadPose() returns something in sensor frame, not avatar - glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); - glm::quat hmdRotation = glm::quat_cast(headPose); - lookAtSpot = _myCamera.getPosition() + myAvatar->getWorldOrientation() * (hmdRotation * lookAtPosition); - } else { - lookAtSpot = myAvatar->getHead()->getEyePosition() - + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * lookAtPosition); - } - } else { - AvatarSharedPointer lookingAt = myAvatar->getLookAtTargetAvatar().lock(); - bool haveLookAtCandidate = lookingAt && myAvatar.get() != lookingAt.get(); - auto avatar = static_pointer_cast(lookingAt); - bool mutualLookAtSnappingEnabled = avatar && avatar->getLookAtSnappingEnabled() && myAvatar->getLookAtSnappingEnabled(); - if (haveLookAtCandidate && mutualLookAtSnappingEnabled) { - // If I am looking at someone else, look directly at one of their eyes - isLookingAtSomeone = true; - auto lookingAtHead = avatar->getHead(); - - const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; - glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; - glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() - - lookingAtHead->getEyePosition()); - float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); - - if (faceAngle < MAXIMUM_FACE_ANGLE) { - // Randomly look back and forth between look targets - eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? - LEFT_EYE : myAvatar->getEyeContactTarget(); - switch (target) { - case LEFT_EYE: - lookAtSpot = lookingAtHead->getLeftEyePosition(); - break; - case RIGHT_EYE: - lookAtSpot = lookingAtHead->getRightEyePosition(); - break; - case MOUTH: - lookAtSpot = lookingAtHead->getMouthPosition(); - break; - } - } else { - // Just look at their head (mid point between eyes) - lookAtSpot = lookingAtHead->getEyePosition(); - } - } else { - // I am not looking at anyone else, so just look forward - auto headPose = myAvatar->getControllerPoseInWorldFrame(controller::Action::HEAD); - if (headPose.isValid()) { - lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE)); - } else { - lookAtSpot = myAvatar->getHead()->getEyePosition() + - (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); - } - } - - // Deflect the eyes a bit to match the detected gaze from the face tracker if active. - if (faceTracker && !faceTracker->isMuted()) { - float eyePitch = faceTracker->getEstimatedEyePitch(); - float eyeYaw = faceTracker->getEstimatedEyeYaw(); - const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; - glm::vec3 origin = myAvatar->getHead()->getEyePosition(); - float deflection = faceTracker->getEyeDeflection(); - if (isLookingAtSomeone) { - deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; - } - lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( - eyePitch * deflection, eyeYaw * deflection, 0.0f))) * - glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); - } - } - - myAvatar->getHead()->setLookAtPosition(lookAtSpot); -} - -void Application::updateThreads(float deltaTime) { - PerformanceTimer perfTimer("updateThreads"); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::updateThreads()"); - - // parse voxel packets - if (!_enableProcessOctreeThread) { - _octreeProcessor.threadRoutine(); - _entityEditSender.threadRoutine(); - } -} - -void Application::toggleOverlays() { - auto menu = Menu::getInstance(); - menu->setIsOptionChecked(MenuOption::Overlays, !menu->isOptionChecked(MenuOption::Overlays)); -} - -void Application::setOverlaysVisible(bool visible) { - auto menu = Menu::getInstance(); - menu->setIsOptionChecked(MenuOption::Overlays, visible); -} - -void Application::centerUI() { - _overlayConductor.centerUI(); -} - -void Application::cycleCamera() { - auto menu = Menu::getInstance(); - if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { - - menu->setIsOptionChecked(MenuOption::FullscreenMirror, false); - menu->setIsOptionChecked(MenuOption::FirstPerson, true); - - } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { - - menu->setIsOptionChecked(MenuOption::FirstPerson, false); - menu->setIsOptionChecked(MenuOption::ThirdPerson, true); - - } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { - - menu->setIsOptionChecked(MenuOption::ThirdPerson, false); - menu->setIsOptionChecked(MenuOption::FullscreenMirror, true); - - } else if (menu->isOptionChecked(MenuOption::IndependentMode) || menu->isOptionChecked(MenuOption::CameraEntityMode)) { - // do nothing if in independent or camera entity modes - return; - } - cameraMenuChanged(); // handle the menu change -} - -void Application::cameraModeChanged() { - switch (_myCamera.getMode()) { - case CAMERA_MODE_FIRST_PERSON: - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); - break; - case CAMERA_MODE_THIRD_PERSON: - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - break; - case CAMERA_MODE_MIRROR: - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, true); - break; - case CAMERA_MODE_INDEPENDENT: - Menu::getInstance()->setIsOptionChecked(MenuOption::IndependentMode, true); - break; - case CAMERA_MODE_ENTITY: - Menu::getInstance()->setIsOptionChecked(MenuOption::CameraEntityMode, true); - break; - default: - break; - } - cameraMenuChanged(); -} - -void Application::changeViewAsNeeded(float boomLength) { - // Switch between first and third person views as needed - // This is called when the boom length has changed - bool boomLengthGreaterThanMinimum = (boomLength > MyAvatar::ZOOM_MIN); - - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON && boomLengthGreaterThanMinimum) { - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, false); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON && !boomLengthGreaterThanMinimum) { - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, false); - cameraMenuChanged(); - } -} - -void Application::cameraMenuChanged() { - auto menu = Menu::getInstance(); - if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { - if (!isHMDMode() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { - _mirrorYawOffset = 0.0f; - _myCamera.setMode(CAMERA_MODE_MIRROR); - getMyAvatar()->reset(false, false, false); // to reset any active MyAvatar::FollowHelpers - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); - } - } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { - if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) { - _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); - } - } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { - if (_myCamera.getMode() != CAMERA_MODE_THIRD_PERSON) { - _myCamera.setMode(CAMERA_MODE_THIRD_PERSON); - if (getMyAvatar()->getBoomLength() == MyAvatar::ZOOM_MIN) { - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); - } - } - } else if (menu->isOptionChecked(MenuOption::IndependentMode)) { - if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { - _myCamera.setMode(CAMERA_MODE_INDEPENDENT); - } - } else if (menu->isOptionChecked(MenuOption::CameraEntityMode)) { - if (_myCamera.getMode() != CAMERA_MODE_ENTITY) { - _myCamera.setMode(CAMERA_MODE_ENTITY); - } - } -} - -void Application::resetPhysicsReadyInformation() { - // we've changed domains or cleared out caches or something. we no longer know enough about the - // collision information of nearby entities to make running bullet be safe. - _fullSceneReceivedCounter = 0; - _fullSceneCounterAtLastPhysicsCheck = 0; - _gpuTextureMemSizeStabilityCount = 0; - _gpuTextureMemSizeAtLastCheck = 0; - _physicsEnabled = false; - _octreeProcessor.startEntitySequence(); -} - - -void Application::reloadResourceCaches() { - resetPhysicsReadyInformation(); - - // Query the octree to refresh everything in view - _queryExpiry = SteadyClock::now(); - _octreeQuery.incrementConnectionID(); - - queryOctree(NodeType::EntityServer, PacketType::EntityQuery); - - // Clear the entities and their renderables - getEntities()->clear(); - - DependencyManager::get()->clearCache(); - DependencyManager::get()->clearCache(); - - // Clear all the resource caches - DependencyManager::get()->clear(); - DependencyManager::get()->refreshAll(); - DependencyManager::get()->refreshAll(); - MaterialCache::instance().refreshAll(); - DependencyManager::get()->refreshAll(); - ShaderCache::instance().refreshAll(); - DependencyManager::get()->refreshAll(); - DependencyManager::get()->refreshAll(); - - DependencyManager::get()->reset(); // Force redownload of .fst models - - DependencyManager::get()->reloadAllScripts(); - getOffscreenUI()->clearCache(); - - DependencyManager::get()->createKeyboard(); - - getMyAvatar()->resetFullAvatarURL(); -} - -void Application::rotationModeChanged() const { - if (!Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { - getMyAvatar()->setHeadPitch(0); - } -} - -void Application::setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions) { - if (qApp->getLoginDialogPoppedUp()) { - return; - } - - auto entityScriptingInterface = DependencyManager::get(); - if (_keyboardFocusHighlightID == UNKNOWN_ENTITY_ID || !entityScriptingInterface->isAddedEntity(_keyboardFocusHighlightID)) { - EntityItemProperties properties; - properties.setType(EntityTypes::Box); - properties.setAlpha(1.0f); - properties.setColor({ 0xFF, 0xEF, 0x00 }); - properties.setPrimitiveMode(PrimitiveMode::LINES); - properties.getPulse().setMin(0.5); - properties.getPulse().setMax(1.0f); - properties.getPulse().setColorMode(PulseMode::IN_PHASE); - properties.setIgnorePickIntersection(true); - _keyboardFocusHighlightID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); - } - - // Position focus - EntityItemProperties properties; - properties.setPosition(position); - properties.setRotation(rotation); - properties.setDimensions(dimensions); - properties.setVisible(true); - entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); -} - -QUuid Application::getKeyboardFocusEntity() const { - return _keyboardFocusedEntity.get(); -} - -void Application::setKeyboardFocusEntity(const QUuid& id) { - if (_keyboardFocusedEntity.get() != id) { - if (qApp->getLoginDialogPoppedUp() && !_loginDialogID.isNull()) { - if (id == _loginDialogID) { - emit loginDialogFocusEnabled(); - } else if (!_keyboardFocusWaitingOnRenderable) { - // that's the only entity we want in focus; - return; - } - } - - _keyboardFocusedEntity.set(id); - - auto entityScriptingInterface = DependencyManager::get(); - if (id != UNKNOWN_ENTITY_ID) { - EntityPropertyFlags desiredProperties; - desiredProperties += PROP_VISIBLE; - desiredProperties += PROP_SHOW_KEYBOARD_FOCUS_HIGHLIGHT; - auto properties = entityScriptingInterface->getEntityProperties(id); - if (properties.getVisible()) { - auto entities = getEntities(); - auto entityId = _keyboardFocusedEntity.get(); - auto entityItemRenderable = entities->renderableForEntityId(entityId); - if (!entityItemRenderable) { - _keyboardFocusWaitingOnRenderable = true; - } else if (entityItemRenderable->wantsKeyboardFocus()) { - entities->setProxyWindow(entityId, _window->windowHandle()); - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->pluginFocusOutEvent(); - } - _lastAcceptedKeyPress = usecTimestampNow(); - - if (properties.getShowKeyboardFocusHighlight()) { - if (auto entity = entities->getEntity(entityId)) { - setKeyboardFocusHighlight(entity->getWorldPosition(), entity->getWorldOrientation(), - entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); - return; - } - } - } - } - } - - EntityItemProperties properties; - properties.setVisible(false); - entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); - } -} - -void Application::updateDialogs(float deltaTime) const { - PerformanceTimer perfTimer("updateDialogs"); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); - auto dialogsManager = DependencyManager::get(); - - QPointer octreeStatsDialog = dialogsManager->getOctreeStatsDialog(); - if (octreeStatsDialog) { - octreeStatsDialog->update(); - } -} - -void Application::updateSecondaryCameraViewFrustum() { - // TODO: Fix this by modeling the way the secondary camera works on how the main camera works - // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary - // camera should be. - - // Code based on SecondaryCameraJob - auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); - assert(renderConfig); - auto camera = dynamic_cast(renderConfig->getConfig("SecondaryCamera")); - - if (!camera || !camera->isEnabled()) { - return; - } - - ViewFrustum secondaryViewFrustum; - if (camera->portalProjection && !camera->attachedEntityId.isNull() && !camera->portalEntranceEntityId.isNull()) { - auto entityScriptingInterface = DependencyManager::get(); - EntityItemPointer portalEntrance = qApp->getEntities()->getTree()->findEntityByID(camera->portalEntranceEntityId); - EntityItemPointer portalExit = qApp->getEntities()->getTree()->findEntityByID(camera->attachedEntityId); - - glm::vec3 portalEntrancePropertiesPosition = portalEntrance->getWorldPosition(); - glm::quat portalEntrancePropertiesRotation = portalEntrance->getWorldOrientation(); - glm::mat4 worldFromPortalEntranceRotation = glm::mat4_cast(portalEntrancePropertiesRotation); - glm::mat4 worldFromPortalEntranceTranslation = glm::translate(portalEntrancePropertiesPosition); - glm::mat4 worldFromPortalEntrance = worldFromPortalEntranceTranslation * worldFromPortalEntranceRotation; - glm::mat4 portalEntranceFromWorld = glm::inverse(worldFromPortalEntrance); - - glm::vec3 portalExitPropertiesPosition = portalExit->getWorldPosition(); - glm::quat portalExitPropertiesRotation = portalExit->getWorldOrientation(); - glm::vec3 portalExitPropertiesDimensions = portalExit->getScaledDimensions(); - glm::vec3 halfPortalExitPropertiesDimensions = 0.5f * portalExitPropertiesDimensions; - - glm::mat4 worldFromPortalExitRotation = glm::mat4_cast(portalExitPropertiesRotation); - glm::mat4 worldFromPortalExitTranslation = glm::translate(portalExitPropertiesPosition); - glm::mat4 worldFromPortalExit = worldFromPortalExitTranslation * worldFromPortalExitRotation; - - glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); - glm::vec3 mainCameraPositionPortalEntrance = vec3(portalEntranceFromWorld * vec4(mainCameraPositionWorld, 1.0f)); - mainCameraPositionPortalEntrance = vec3(-mainCameraPositionPortalEntrance.x, mainCameraPositionPortalEntrance.y, - -mainCameraPositionPortalEntrance.z); - glm::vec3 portalExitCameraPositionWorld = vec3(worldFromPortalExit * vec4(mainCameraPositionPortalEntrance, 1.0f)); - - secondaryViewFrustum.setPosition(portalExitCameraPositionWorld); - secondaryViewFrustum.setOrientation(portalExitPropertiesRotation); - - float nearClip = mainCameraPositionPortalEntrance.z + portalExitPropertiesDimensions.z * 2.0f; - // `mainCameraPositionPortalEntrance` should technically be `mainCameraPositionPortalExit`, - // but the values are the same. - glm::vec3 upperRight = halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; - glm::vec3 bottomLeft = -halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; - glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); - secondaryViewFrustum.setProjection(frustum); - } else if (camera->mirrorProjection && !camera->attachedEntityId.isNull()) { - auto entityScriptingInterface = DependencyManager::get(); - auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); - glm::vec3 mirrorPropertiesPosition = entityProperties.getPosition(); - glm::quat mirrorPropertiesRotation = entityProperties.getRotation(); - glm::vec3 mirrorPropertiesDimensions = entityProperties.getDimensions(); - glm::vec3 halfMirrorPropertiesDimensions = 0.5f * mirrorPropertiesDimensions; - - // setup mirror from world as inverse of world from mirror transformation using inverted x and z for mirrored image - // TODO: we are assuming here that UP is world y-axis - glm::mat4 worldFromMirrorRotation = glm::mat4_cast(mirrorPropertiesRotation) * glm::scale(vec3(-1.0f, 1.0f, -1.0f)); - glm::mat4 worldFromMirrorTranslation = glm::translate(mirrorPropertiesPosition); - glm::mat4 worldFromMirror = worldFromMirrorTranslation * worldFromMirrorRotation; - glm::mat4 mirrorFromWorld = glm::inverse(worldFromMirror); - - // get mirror camera position by reflecting main camera position's z coordinate in mirror space - glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); - glm::vec3 mainCameraPositionMirror = vec3(mirrorFromWorld * vec4(mainCameraPositionWorld, 1.0f)); - glm::vec3 mirrorCameraPositionMirror = vec3(mainCameraPositionMirror.x, mainCameraPositionMirror.y, - -mainCameraPositionMirror.z); - glm::vec3 mirrorCameraPositionWorld = vec3(worldFromMirror * vec4(mirrorCameraPositionMirror, 1.0f)); - - // set frustum position to be mirrored camera and set orientation to mirror's adjusted rotation - glm::quat mirrorCameraOrientation = glm::quat_cast(worldFromMirrorRotation); - secondaryViewFrustum.setPosition(mirrorCameraPositionWorld); - secondaryViewFrustum.setOrientation(mirrorCameraOrientation); - - // build frustum using mirror space translation of mirrored camera - float nearClip = mirrorCameraPositionMirror.z + mirrorPropertiesDimensions.z * 2.0f; - glm::vec3 upperRight = halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; - glm::vec3 bottomLeft = -halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; - glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); - secondaryViewFrustum.setProjection(frustum); - } else { - if (!camera->attachedEntityId.isNull()) { - auto entityScriptingInterface = DependencyManager::get(); - auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); - secondaryViewFrustum.setPosition(entityProperties.getPosition()); - secondaryViewFrustum.setOrientation(entityProperties.getRotation()); - } else { - secondaryViewFrustum.setPosition(camera->position); - secondaryViewFrustum.setOrientation(camera->orientation); - } - - float aspectRatio = (float)camera->textureWidth / (float)camera->textureHeight; - secondaryViewFrustum.setProjection(camera->vFoV, - aspectRatio, - camera->nearClipPlaneDistance, - camera->farClipPlaneDistance); - } - // Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera, - // which is not what we want here. - secondaryViewFrustum.calculate(); - - _conicalViews.push_back(secondaryViewFrustum); -} - -static bool domainLoadingInProgress = false; - -void Application::update(float deltaTime) { - PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_graphicsEngine._renderFrameCount + 1); - - if (_aboutToQuit) { - return; - } - - - if (!_physicsEnabled) { - if (!domainLoadingInProgress) { - PROFILE_ASYNC_BEGIN(app, "Scene Loading", ""); - domainLoadingInProgress = true; - } - - // we haven't yet enabled physics. we wait until we think we have all the collision information - // for nearby entities before starting bullet up. - quint64 now = usecTimestampNow(); - if (isServerlessMode() || _octreeProcessor.isLoadSequenceComplete()) { - bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); - - if (gpuTextureMemSizeStable() || !enableInterstitial) { - // we've received a new full-scene octree stats packet, or it's been long enough to try again anyway - _lastPhysicsCheckTime = now; - _fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter; - _lastQueriedViews.clear(); // Force new view. - - // process octree stats packets are sent in between full sends of a scene (this isn't currently true). - // We keep physics disabled until we've received a full scene and everything near the avatar in that - // scene is ready to compute its collision shape. - if (getMyAvatar()->isReadyForPhysics()) { - _physicsEnabled = true; - setIsInterstitialMode(false); - getMyAvatar()->updateMotionBehaviorFromMenu(); - } - } - } - } else if (domainLoadingInProgress) { - domainLoadingInProgress = false; - PROFILE_ASYNC_END(app, "Scene Loading", ""); - } - - auto myAvatar = getMyAvatar(); - { - PerformanceTimer perfTimer("devices"); - - FaceTracker* tracker = getSelectedFaceTracker(); - if (tracker && Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking) != tracker->isMuted()) { - tracker->toggleMute(); - } - - tracker = getActiveFaceTracker(); - if (tracker && !tracker->isMuted()) { - tracker->update(deltaTime); - - // Auto-mute microphone after losing face tracking? - if (tracker->isTracking()) { - _lastFaceTrackerUpdate = usecTimestampNow(); - } else { - const quint64 MUTE_MICROPHONE_AFTER_USECS = 5000000; //5 secs - Menu* menu = Menu::getInstance(); - auto audioClient = DependencyManager::get(); - if (menu->isOptionChecked(MenuOption::AutoMuteAudio) && !audioClient->isMuted()) { - if (_lastFaceTrackerUpdate > 0 - && ((usecTimestampNow() - _lastFaceTrackerUpdate) > MUTE_MICROPHONE_AFTER_USECS)) { - audioClient->setMuted(true); - _lastFaceTrackerUpdate = 0; - } - } else { - _lastFaceTrackerUpdate = 0; - } - } - } else { - _lastFaceTrackerUpdate = 0; - } - - auto userInputMapper = DependencyManager::get(); - - controller::HmdAvatarAlignmentType hmdAvatarAlignmentType; - if (myAvatar->getHmdAvatarAlignmentType() == "eyes") { - hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Eyes; - } else { - hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Head; - } - - controller::InputCalibrationData calibrationData = { - myAvatar->getSensorToWorldMatrix(), - createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()), - myAvatar->getHMDSensorMatrix(), - myAvatar->getCenterEyeCalibrationMat(), - myAvatar->getHeadCalibrationMat(), - myAvatar->getSpine2CalibrationMat(), - myAvatar->getHipsCalibrationMat(), - myAvatar->getLeftFootCalibrationMat(), - myAvatar->getRightFootCalibrationMat(), - myAvatar->getRightArmCalibrationMat(), - myAvatar->getLeftArmCalibrationMat(), - myAvatar->getRightHandCalibrationMat(), - myAvatar->getLeftHandCalibrationMat(), - hmdAvatarAlignmentType - }; - - InputPluginPointer keyboardMousePlugin; - for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { - if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { - keyboardMousePlugin = inputPlugin; - } else if (inputPlugin->isActive()) { - inputPlugin->pluginUpdate(deltaTime, calibrationData); - } - } - - userInputMapper->setInputCalibrationData(calibrationData); - userInputMapper->update(deltaTime); - - if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { - keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); - } - // Transfer the user inputs to the driveKeys - // FIXME can we drop drive keys and just have the avatar read the action states directly? - myAvatar->clearDriveKeys(); - if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT && !isInterstitialMode()) { - if (!_controllerScriptingInterface->areActionsCaptured() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); - if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - myAvatar->setDriveKey(MyAvatar::DELTA_PITCH, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_PITCH)); - myAvatar->setDriveKey(MyAvatar::DELTA_YAW, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_YAW)); - myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); - } - } - myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); - } - - myAvatar->setSprintMode((bool)userInputMapper->getActionState(controller::Action::SPRINT)); - static const std::vector avatarControllerActions = { - controller::Action::LEFT_HAND, - controller::Action::RIGHT_HAND, - controller::Action::LEFT_FOOT, - controller::Action::RIGHT_FOOT, - controller::Action::HIPS, - controller::Action::SPINE2, - controller::Action::HEAD, - controller::Action::LEFT_HAND_THUMB1, - controller::Action::LEFT_HAND_THUMB2, - controller::Action::LEFT_HAND_THUMB3, - controller::Action::LEFT_HAND_THUMB4, - controller::Action::LEFT_HAND_INDEX1, - controller::Action::LEFT_HAND_INDEX2, - controller::Action::LEFT_HAND_INDEX3, - controller::Action::LEFT_HAND_INDEX4, - controller::Action::LEFT_HAND_MIDDLE1, - controller::Action::LEFT_HAND_MIDDLE2, - controller::Action::LEFT_HAND_MIDDLE3, - controller::Action::LEFT_HAND_MIDDLE4, - controller::Action::LEFT_HAND_RING1, - controller::Action::LEFT_HAND_RING2, - controller::Action::LEFT_HAND_RING3, - controller::Action::LEFT_HAND_RING4, - controller::Action::LEFT_HAND_PINKY1, - controller::Action::LEFT_HAND_PINKY2, - controller::Action::LEFT_HAND_PINKY3, - controller::Action::LEFT_HAND_PINKY4, - controller::Action::RIGHT_HAND_THUMB1, - controller::Action::RIGHT_HAND_THUMB2, - controller::Action::RIGHT_HAND_THUMB3, - controller::Action::RIGHT_HAND_THUMB4, - controller::Action::RIGHT_HAND_INDEX1, - controller::Action::RIGHT_HAND_INDEX2, - controller::Action::RIGHT_HAND_INDEX3, - controller::Action::RIGHT_HAND_INDEX4, - controller::Action::RIGHT_HAND_MIDDLE1, - controller::Action::RIGHT_HAND_MIDDLE2, - controller::Action::RIGHT_HAND_MIDDLE3, - controller::Action::RIGHT_HAND_MIDDLE4, - controller::Action::RIGHT_HAND_RING1, - controller::Action::RIGHT_HAND_RING2, - controller::Action::RIGHT_HAND_RING3, - controller::Action::RIGHT_HAND_RING4, - controller::Action::RIGHT_HAND_PINKY1, - controller::Action::RIGHT_HAND_PINKY2, - controller::Action::RIGHT_HAND_PINKY3, - controller::Action::RIGHT_HAND_PINKY4, - controller::Action::LEFT_ARM, - controller::Action::RIGHT_ARM, - controller::Action::LEFT_SHOULDER, - controller::Action::RIGHT_SHOULDER, - controller::Action::LEFT_FORE_ARM, - controller::Action::RIGHT_FORE_ARM, - controller::Action::LEFT_LEG, - controller::Action::RIGHT_LEG, - controller::Action::LEFT_UP_LEG, - controller::Action::RIGHT_UP_LEG, - controller::Action::LEFT_TOE_BASE, - controller::Action::RIGHT_TOE_BASE - }; - - // copy controller poses from userInputMapper to myAvatar. - glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()); - glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); - glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; - for (auto& action : avatarControllerActions) { - controller::Pose pose = userInputMapper->getPoseState(action); - myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); - } - - static const std::vector trackedObjectStringLiterals = { - QStringLiteral("_TrackedObject00"), QStringLiteral("_TrackedObject01"), QStringLiteral("_TrackedObject02"), QStringLiteral("_TrackedObject03"), - QStringLiteral("_TrackedObject04"), QStringLiteral("_TrackedObject05"), QStringLiteral("_TrackedObject06"), QStringLiteral("_TrackedObject07"), - QStringLiteral("_TrackedObject08"), QStringLiteral("_TrackedObject09"), QStringLiteral("_TrackedObject10"), QStringLiteral("_TrackedObject11"), - QStringLiteral("_TrackedObject12"), QStringLiteral("_TrackedObject13"), QStringLiteral("_TrackedObject14"), QStringLiteral("_TrackedObject15") - }; - - // Controlled by the Developer > Avatar > Show Tracked Objects menu. - if (_showTrackedObjects) { - static const std::vector trackedObjectActions = { - controller::Action::TRACKED_OBJECT_00, controller::Action::TRACKED_OBJECT_01, controller::Action::TRACKED_OBJECT_02, controller::Action::TRACKED_OBJECT_03, - controller::Action::TRACKED_OBJECT_04, controller::Action::TRACKED_OBJECT_05, controller::Action::TRACKED_OBJECT_06, controller::Action::TRACKED_OBJECT_07, - controller::Action::TRACKED_OBJECT_08, controller::Action::TRACKED_OBJECT_09, controller::Action::TRACKED_OBJECT_10, controller::Action::TRACKED_OBJECT_11, - controller::Action::TRACKED_OBJECT_12, controller::Action::TRACKED_OBJECT_13, controller::Action::TRACKED_OBJECT_14, controller::Action::TRACKED_OBJECT_15 - }; - - int i = 0; - glm::vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); - for (auto& action : trackedObjectActions) { - controller::Pose pose = userInputMapper->getPoseState(action); - if (pose.valid) { - glm::vec3 pos = transformPoint(myAvatarMatrix, pose.translation); - glm::quat rot = glmExtractRotation(myAvatarMatrix) * pose.rotation; - DebugDraw::getInstance().addMarker(trackedObjectStringLiterals[i], rot, pos, BLUE); - } else { - DebugDraw::getInstance().removeMarker(trackedObjectStringLiterals[i]); - } - i++; - } - } else if (_prevShowTrackedObjects) { - for (auto& key : trackedObjectStringLiterals) { - DebugDraw::getInstance().removeMarker(key); - } - } - _prevShowTrackedObjects = _showTrackedObjects; - } - - updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... - updateDialogs(deltaTime); // update various stats dialogs if present - - auto grabManager = DependencyManager::get(); - grabManager->simulateGrabs(); - - // TODO: break these out into distinct perfTimers when they prove interesting - { - PROFILE_RANGE(app, "PickManager"); - PerformanceTimer perfTimer("pickManager"); - DependencyManager::get()->update(); - } - - { - PROFILE_RANGE(app, "PointerManager"); - PerformanceTimer perfTimer("pointerManager"); - DependencyManager::get()->update(); - } - - QSharedPointer avatarManager = DependencyManager::get(); - - { - PROFILE_RANGE(simulation_physics, "Simulation"); - PerformanceTimer perfTimer("simulation"); - - if (_physicsEnabled) { - auto t0 = std::chrono::high_resolution_clock::now(); - auto t1 = t0; - { - PROFILE_RANGE(simulation_physics, "PrePhysics"); - PerformanceTimer perfTimer("prePhysics)"); - { - PROFILE_RANGE(simulation_physics, "RemoveEntities"); - const VectorOfMotionStates& motionStates = _entitySimulation->getObjectsToRemoveFromPhysics(); - { - PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); - _physicsEngine->removeObjects(motionStates); - } - _entitySimulation->deleteObjectsRemovedFromPhysics(); - } - - { - PROFILE_RANGE(simulation_physics, "AddEntities"); - VectorOfMotionStates motionStates; - getEntities()->getTree()->withReadLock([&] { - _entitySimulation->getObjectsToAddToPhysics(motionStates); - PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); - _physicsEngine->addObjects(motionStates); - }); - } - { - VectorOfMotionStates motionStates; - PROFILE_RANGE(simulation_physics, "ChangeEntities"); - getEntities()->getTree()->withReadLock([&] { - _entitySimulation->getObjectsToChange(motionStates); - VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); - _entitySimulation->setObjectsToChange(stillNeedChange); - }); - } - - _entitySimulation->applyDynamicChanges(); - - t1 = std::chrono::high_resolution_clock::now(); - - { - PROFILE_RANGE(simulation_physics, "Avatars"); - PhysicsEngine::Transaction transaction; - avatarManager->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - avatarManager->handleProcessedPhysicsTransaction(transaction); - myAvatar->getCharacterController()->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - myAvatar->getCharacterController()->handleProcessedPhysicsTransaction(transaction); - myAvatar->prepareForPhysicsSimulation(); - _physicsEngine->enableGlobalContactAddedCallback(myAvatar->isFlying()); - } - - { - PROFILE_RANGE(simulation_physics, "PrepareActions"); - _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { - dynamic->prepareForPhysicsSimulation(); - }); - } - } - auto t2 = std::chrono::high_resolution_clock::now(); - { - PROFILE_RANGE(simulation_physics, "StepPhysics"); - PerformanceTimer perfTimer("stepPhysics"); - getEntities()->getTree()->withWriteLock([&] { - _physicsEngine->stepSimulation(); - }); - } - auto t3 = std::chrono::high_resolution_clock::now(); - { - if (_physicsEngine->hasOutgoingChanges()) { - { - PROFILE_RANGE(simulation_physics, "PostPhysics"); - PerformanceTimer perfTimer("postPhysics"); - // grab the collision events BEFORE handleChangedMotionStates() because at this point - // we have a better idea of which objects we own or should own. - auto& collisionEvents = _physicsEngine->getCollisionEvents(); - - getEntities()->getTree()->withWriteLock([&] { - PROFILE_RANGE(simulation_physics, "HandleChanges"); - PerformanceTimer perfTimer("handleChanges"); - - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); - _entitySimulation->handleChangedMotionStates(outgoingChanges); - avatarManager->handleChangedMotionStates(outgoingChanges); - - const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); - _entitySimulation->handleDeactivatedMotionStates(deactivations); - }); - - // handleCollisionEvents() AFTER handleChangedMotionStates() - { - PROFILE_RANGE(simulation_physics, "CollisionEvents"); - avatarManager->handleCollisionEvents(collisionEvents); - // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk - // deadlock.) - _entitySimulation->handleCollisionEvents(collisionEvents); - } - - { - PROFILE_RANGE(simulation_physics, "MyAvatar"); - myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); - } - - if (PerformanceTimer::isActive() && - Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && - Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsTiming)) { - _physicsEngine->harvestPerformanceStats(); - } - // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework - _physicsEngine->dumpStatsIfNecessary(); - } - auto t4 = std::chrono::high_resolution_clock::now(); - - // NOTE: the getEntities()->update() call below will wait for lock - // and will provide non-physical entity motion - getEntities()->update(true); // update the models... - - auto t5 = std::chrono::high_resolution_clock::now(); - - workload::Timings timings(6); - timings[0] = t1 - t0; // prePhysics entities - timings[1] = t2 - t1; // prePhysics avatars - timings[2] = t3 - t2; // stepPhysics - timings[3] = t4 - t3; // postPhysics - timings[4] = t5 - t4; // non-physical kinematics - timings[5] = workload::Timing_ns((int32_t)(NSECS_PER_SECOND * deltaTime)); // game loop duration - _gameWorkload.updateSimulationTimings(timings); - } - } - } else { - // update the rendering without any simulation - getEntities()->update(false); - } - // remove recently dead avatarEntities - SetOfEntities deadAvatarEntities; - _entitySimulation->takeDeadAvatarEntities(deadAvatarEntities); - avatarManager->removeDeadAvatarEntities(deadAvatarEntities); - } - - // AvatarManager update - { - { - PROFILE_RANGE(simulation, "OtherAvatars"); - PerformanceTimer perfTimer("otherAvatars"); - avatarManager->updateOtherAvatars(deltaTime); - } - - { - PROFILE_RANGE(simulation, "MyAvatar"); - PerformanceTimer perfTimer("MyAvatar"); - qApp->updateMyAvatarLookAtPosition(); - avatarManager->updateMyAvatar(deltaTime); - } - } - - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::update()"); - - updateLOD(deltaTime); - - if (!_loginDialogID.isNull()) { - _loginStateManager.update(getMyAvatar()->getDominantHand(), _loginDialogID); - updateLoginDialogPosition(); - } - - { - PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("overlays"); - _overlays.update(deltaTime); - } - - // Update _viewFrustum with latest camera and view frustum data... - // NOTE: we get this from the view frustum, to make it simpler, since the - // loadViewFrumstum() method will get the correct details from the camera - // We could optimize this to not actually load the viewFrustum, since we don't - // actually need to calculate the view frustum planes to send these details - // to the server. - { - QMutexLocker viewLocker(&_viewMutex); - _myCamera.loadViewFrustum(_viewFrustum); - - _conicalViews.clear(); - _conicalViews.push_back(_viewFrustum); - // TODO: Fix this by modeling the way the secondary camera works on how the main camera works - // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary - // camera should be. - updateSecondaryCameraViewFrustum(); - } - - quint64 now = usecTimestampNow(); - - // Update my voxel servers with my current voxel query... - { - PROFILE_RANGE_EX(app, "QueryOctree", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("queryOctree"); - QMutexLocker viewLocker(&_viewMutex); - - bool viewIsDifferentEnough = false; - if (_conicalViews.size() == _lastQueriedViews.size()) { - for (size_t i = 0; i < _conicalViews.size(); ++i) { - if (!_conicalViews[i].isVerySimilar(_lastQueriedViews[i])) { - viewIsDifferentEnough = true; - break; - } - } - } else { - viewIsDifferentEnough = true; - } - - - // if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it - static const std::chrono::seconds MIN_PERIOD_BETWEEN_QUERIES { 3 }; - auto now = SteadyClock::now(); - if (now > _queryExpiry || viewIsDifferentEnough) { - if (DependencyManager::get()->shouldRenderEntities()) { - queryOctree(NodeType::EntityServer, PacketType::EntityQuery); - } - queryAvatars(); - - _lastQueriedViews = _conicalViews; - _queryExpiry = now + MIN_PERIOD_BETWEEN_QUERIES; - } - } - - // sent nack packets containing missing sequence numbers of received packets from nodes - { - quint64 sinceLastNack = now - _lastNackTime; - const quint64 TOO_LONG_SINCE_LAST_NACK = 1 * USECS_PER_SECOND; - if (sinceLastNack > TOO_LONG_SINCE_LAST_NACK) { - _lastNackTime = now; - sendNackPackets(); - } - } - - // send packet containing downstream audio stats to the AudioMixer - { - quint64 sinceLastNack = now - _lastSendDownstreamAudioStats; - if (sinceLastNack > TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS && !isInterstitialMode()) { - _lastSendDownstreamAudioStats = now; - - QMetaObject::invokeMethod(DependencyManager::get().data(), "sendDownstreamAudioStatsPacket", Qt::QueuedConnection); - } - } - - { - PerformanceTimer perfTimer("avatarManager/postUpdate"); - avatarManager->postUpdate(deltaTime, getMain3DScene()); - } - - { - PROFILE_RANGE_EX(app, "PostUpdateLambdas", 0xffff0000, (uint64_t)0); - PerformanceTimer perfTimer("postUpdateLambdas"); - std::unique_lock guard(_postUpdateLambdasLock); - for (auto& iter : _postUpdateLambdas) { - iter.second(); - } - _postUpdateLambdas.clear(); - } - - - updateRenderArgs(deltaTime); - - { - PerformanceTimer perfTimer("AnimDebugDraw"); - AnimDebugDraw::getInstance().update(); - } - - - { // Game loop is done, mark the end of the frame for the scene transactions and the render loop to take over - PerformanceTimer perfTimer("enqueueFrame"); - getMain3DScene()->enqueueFrame(); - } - - // If the display plugin is inactive then the frames won't be processed so process them here. - if (!getActiveDisplayPlugin()->isActive()) { - getMain3DScene()->processTransactionQueue(); - } -} - -void Application::updateRenderArgs(float deltaTime) { - _graphicsEngine.editRenderArgs([this, deltaTime](AppRenderArgs& appRenderArgs) { - PerformanceTimer perfTimer("editRenderArgs"); - appRenderArgs._headPose = getHMDSensorPose(); - - auto myAvatar = getMyAvatar(); - - // update the avatar with a fresh HMD pose - { - PROFILE_RANGE(render, "/updateAvatar"); - myAvatar->updateFromHMDSensorMatrix(appRenderArgs._headPose); - } - - auto lodManager = DependencyManager::get(); - - float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); - appRenderArgs._sensorToWorldScale = sensorToWorldScale; - appRenderArgs._sensorToWorld = getMyAvatar()->getSensorToWorldMatrix(); - { - PROFILE_RANGE(render, "/buildFrustrumAndArgs"); - { - QMutexLocker viewLocker(&_viewMutex); - // adjust near clip plane to account for sensor scaling. - auto adjustedProjection = glm::perspective(glm::radians(_fieldOfView.get()), - getActiveDisplayPlugin()->getRecommendedAspectRatio(), - DEFAULT_NEAR_CLIP * sensorToWorldScale, - DEFAULT_FAR_CLIP); - _viewFrustum.setProjection(adjustedProjection); - _viewFrustum.calculate(); - } - appRenderArgs._renderArgs = RenderArgs(_graphicsEngine.getGPUContext(), lodManager->getOctreeSizeScale(), - lodManager->getBoundaryLevelAdjust(), lodManager->getLODAngleHalfTan(), RenderArgs::DEFAULT_RENDER_MODE, - RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); - appRenderArgs._renderArgs._scene = getMain3DScene(); - - { - QMutexLocker viewLocker(&_viewMutex); - appRenderArgs._renderArgs.setViewFrustum(_viewFrustum); - } - } - { - PROFILE_RANGE(render, "/resizeGL"); - bool showWarnings = false; - bool suppressShortTimings = false; - auto menu = Menu::getInstance(); - if (menu) { - suppressShortTimings = menu->isOptionChecked(MenuOption::SuppressShortTimings); - showWarnings = menu->isOptionChecked(MenuOption::PipelineWarnings); - } - PerformanceWarning::setSuppressShortTimings(suppressShortTimings); - PerformanceWarning warn(showWarnings, "Application::paintGL()"); - resizeGL(); - } - - this->updateCamera(appRenderArgs._renderArgs, deltaTime); - appRenderArgs._eyeToWorld = _myCamera.getTransform(); - appRenderArgs._isStereo = false; - - { - auto hmdInterface = DependencyManager::get(); - float ipdScale = hmdInterface->getIPDScale(); - - // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. - ipdScale *= sensorToWorldScale; - - auto baseProjection = appRenderArgs._renderArgs.getViewFrustum().getProjection(); - - if (getActiveDisplayPlugin()->isStereo()) { - // Stereo modes will typically have a larger projection matrix overall, - // so we ask for the 'mono' projection matrix, which for stereo and HMD - // plugins will imply the combined projection for both eyes. - // - // This is properly implemented for the Oculus plugins, but for OpenVR - // and Stereo displays I'm not sure how to get / calculate it, so we're - // just relying on the left FOV in each case and hoping that the - // overall culling margin of error doesn't cause popping in the - // right eye. There are FIXMEs in the relevant plugins - _myCamera.setProjection(getActiveDisplayPlugin()->getCullingProjection(baseProjection)); - appRenderArgs._isStereo = true; - - auto& eyeOffsets = appRenderArgs._eyeOffsets; - auto& eyeProjections = appRenderArgs._eyeProjections; - - // FIXME we probably don't need to set the projection matrix every frame, - // only when the display plugin changes (or in non-HMD modes when the user - // changes the FOV manually, which right now I don't think they can. - for_each_eye([&](Eye eye) { - // For providing the stereo eye views, the HMD head pose has already been - // applied to the avatar, so we need to get the difference between the head - // pose applied to the avatar and the per eye pose, and use THAT as - // the per-eye stereo matrix adjustment. - mat4 eyeToHead = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); - // Grab the translation - vec3 eyeOffset = glm::vec3(eyeToHead[3]); - // Apply IPD scaling - mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); - eyeOffsets[eye] = eyeOffsetTransform; - eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); - }); - - // Configure the type of display / stereo - appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); - } - } - - { - QMutexLocker viewLocker(&_viewMutex); - _myCamera.loadViewFrustum(_displayViewFrustum); - appRenderArgs._view = glm::inverse(_displayViewFrustum.getView()); - } - - { - QMutexLocker viewLocker(&_viewMutex); - appRenderArgs._renderArgs.setViewFrustum(_displayViewFrustum); - } - - - // HACK - // load the view frustum - // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. - // Then we can move this logic into the Avatar::simulate call. - myAvatar->preDisplaySide(&appRenderArgs._renderArgs); - }); -} - -void Application::queryAvatars() { - if (!isInterstitialMode()) { - auto avatarPacket = NLPacket::create(PacketType::AvatarQuery); - auto destinationBuffer = reinterpret_cast(avatarPacket->getPayload()); - unsigned char* bufferStart = destinationBuffer; - - uint8_t numFrustums = (uint8_t)_conicalViews.size(); - memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums)); - destinationBuffer += sizeof(numFrustums); - - for (const auto& view : _conicalViews) { - destinationBuffer += view.serialize(destinationBuffer); - } - - avatarPacket->setPayloadSize(destinationBuffer - bufferStart); - - DependencyManager::get()->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); - } -} - - -int Application::sendNackPackets() { - - // iterates through all nodes in NodeList - auto nodeList = DependencyManager::get(); - - int packetsSent = 0; - - nodeList->eachNode([&](const SharedNodePointer& node){ - - if (node->getActiveSocket() && node->getType() == NodeType::EntityServer) { - - auto nackPacketList = NLPacketList::create(PacketType::OctreeDataNack); - - QUuid nodeUUID = node->getUUID(); - - // if there are octree packets from this node that are waiting to be processed, - // don't send a NACK since the missing packets may be among those waiting packets. - if (_octreeProcessor.hasPacketsToProcessFrom(nodeUUID)) { - return; - } - - QSet missingSequenceNumbers; - _octreeServerSceneStats.withReadLock([&] { - // retrieve octree scene stats of this node - if (_octreeServerSceneStats.find(nodeUUID) == _octreeServerSceneStats.end()) { - return; - } - // get sequence number stats of node, prune its missing set, and make a copy of the missing set - SequenceNumberStats& sequenceNumberStats = _octreeServerSceneStats[nodeUUID].getIncomingOctreeSequenceNumberStats(); - sequenceNumberStats.pruneMissingSet(); - missingSequenceNumbers = sequenceNumberStats.getMissingSet(); - }); - - _isMissingSequenceNumbers = (missingSequenceNumbers.size() != 0); - - // construct nack packet(s) for this node - foreach(const OCTREE_PACKET_SEQUENCE& missingNumber, missingSequenceNumbers) { - nackPacketList->writePrimitive(missingNumber); - } - - if (nackPacketList->getNumPackets()) { - packetsSent += (int)nackPacketList->getNumPackets(); - - // send the packet list - nodeList->sendPacketList(std::move(nackPacketList), *node); - } - } - }); - - return packetsSent; -} - -void Application::queryOctree(NodeType_t serverType, PacketType packetType) { - - if (!_settingsLoaded) { - return; // bail early if settings are not loaded - } - - const bool isModifiedQuery = !_physicsEnabled; - if (isModifiedQuery) { - // Create modified view that is a simple sphere. - bool interstitialModeEnabled = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); - - ConicalViewFrustum sphericalView; - sphericalView.setSimpleRadius(INITIAL_QUERY_RADIUS); - - if (interstitialModeEnabled) { - ConicalViewFrustum farView; - farView.set(_viewFrustum); - _octreeQuery.setConicalViews({ sphericalView, farView }); - } else { - _octreeQuery.setConicalViews({ sphericalView }); - } - - _octreeQuery.setOctreeSizeScale(DEFAULT_OCTREE_SIZE_SCALE); - static constexpr float MIN_LOD_ADJUST = -20.0f; - _octreeQuery.setBoundaryLevelAdjust(MIN_LOD_ADJUST); - } else { - _octreeQuery.setConicalViews(_conicalViews); - auto lodManager = DependencyManager::get(); - _octreeQuery.setOctreeSizeScale(lodManager->getOctreeSizeScale()); - _octreeQuery.setBoundaryLevelAdjust(lodManager->getBoundaryLevelAdjust()); - } - _octreeQuery.setReportInitialCompletion(isModifiedQuery); - - - auto nodeList = DependencyManager::get(); - - auto node = nodeList->soloNodeOfType(serverType); - if (node && node->getActiveSocket()) { - _octreeQuery.setMaxQueryPacketsPerSecond(getMaxOctreePacketsPerSecond()); - - auto queryPacket = NLPacket::create(packetType); - - // encode the query data - auto packetData = reinterpret_cast(queryPacket->getPayload()); - int packetSize = _octreeQuery.getBroadcastData(packetData); - queryPacket->setPayloadSize(packetSize); - - // make sure we still have an active socket - nodeList->sendUnreliablePacket(*queryPacket, *node); - } -} - - -bool Application::isHMDMode() const { - return getActiveDisplayPlugin()->isHmd(); -} - -float Application::getNumCollisionObjects() const { - return _physicsEngine ? _physicsEngine->getNumCollisionObjects() : 0; -} - -float Application::getTargetRenderFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } - -QRect Application::getDesirableApplicationGeometry() const { - QRect applicationGeometry = getWindow()->geometry(); - - // If our parent window is on the HMD, then don't use its geometry, instead use - // the "main screen" geometry. - HMDToolsDialog* hmdTools = DependencyManager::get()->getHMDToolsDialog(); - if (hmdTools && hmdTools->hasHMDScreen()) { - QScreen* hmdScreen = hmdTools->getHMDScreen(); - QWindow* appWindow = getWindow()->windowHandle(); - QScreen* appScreen = appWindow->screen(); - - // if our app's screen is the hmd screen, we don't want to place the - // running scripts widget on it. So we need to pick a better screen. - // we will use the screen for the HMDTools since it's a guaranteed - // better screen. - if (appScreen == hmdScreen) { - QScreen* betterScreen = hmdTools->windowHandle()->screen(); - applicationGeometry = betterScreen->geometry(); - } - } - return applicationGeometry; -} - -PickRay Application::computePickRay(float x, float y) const { - vec2 pickPoint { x, y }; - PickRay result; - if (isHMDMode()) { - getApplicationCompositor().computeHmdPickRay(pickPoint, result.origin, result.direction); - } else { - pickPoint /= getCanvasSize(); - if (_myCamera.getMode() == CameraMode::CAMERA_MODE_MIRROR) { - pickPoint.x = 1.0f - pickPoint.x; - } - QMutexLocker viewLocker(&_viewMutex); - _viewFrustum.computePickRay(pickPoint.x, pickPoint.y, result.origin, result.direction); - } - return result; -} - -std::shared_ptr Application::getMyAvatar() const { - return DependencyManager::get()->getMyAvatar(); -} - -glm::vec3 Application::getAvatarPosition() const { - return getMyAvatar()->getWorldPosition(); -} - -void Application::copyViewFrustum(ViewFrustum& viewOut) const { - QMutexLocker viewLocker(&_viewMutex); - viewOut = _viewFrustum; -} - -void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const { - QMutexLocker viewLocker(&_viewMutex); - viewOut = _displayViewFrustum; -} - -// resentSensors() is a bit of vestigial feature. It used to be used for Oculus DK2 to recenter the view around -// the current head orientation. With the introduction of "room scale" tracking we no longer need that particular -// feature. However, we still use this to reset face trackers, eye trackers, audio and to optionally re-load the avatar -// rig and animations from scratch. -void Application::resetSensors(bool andReload) { - DependencyManager::get()->reset(); - DependencyManager::get()->reset(); - _overlayConductor.centerUI(); - getActiveDisplayPlugin()->resetSensors(); - getMyAvatar()->reset(true, andReload); - QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); -} - -void Application::hmdVisibleChanged(bool visible) { - // TODO - // calling start and stop will change audio input and ouput to default audio devices. - // we need to add a pause/unpause functionality to AudioClient for this to work properly -#if 0 - if (visible) { - QMetaObject::invokeMethod(DependencyManager::get().data(), "start", Qt::QueuedConnection); - } else { - QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::QueuedConnection); - } -#endif -} - -void Application::updateWindowTitle() const { - - auto nodeList = DependencyManager::get(); - auto accountManager = DependencyManager::get(); - auto isInErrorState = nodeList->getDomainHandler().isInErrorState(); - - QString buildVersion = " - " - + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) - + " " + applicationVersion(); - - QString loginStatus = accountManager->isLoggedIn() ? "" : " (NOT LOGGED IN)"; - - QString connectionStatus = isInErrorState ? " (ERROR CONNECTING)" : - nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED)"; - QString username = accountManager->getAccountInfo().getUsername(); - - setCrashAnnotation("username", username.toStdString()); - - QString currentPlaceName; - if (isServerlessMode()) { - if (isInErrorState) { - currentPlaceName = "serverless: " + nodeList->getDomainHandler().getErrorDomainURL().toString(); - } else { - currentPlaceName = "serverless: " + DependencyManager::get()->getDomainURL().toString(); - } - } else { - currentPlaceName = DependencyManager::get()->getDomainURL().host(); - if (currentPlaceName.isEmpty()) { - currentPlaceName = nodeList->getDomainHandler().getHostname(); - } - } - - QString title = QString() + (!username.isEmpty() ? username + " @ " : QString()) - + currentPlaceName + connectionStatus + loginStatus + buildVersion; - -#ifndef WIN32 - // crashes with vs2013/win32 - qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str()); -#endif - _window->setWindowTitle(title); - - // updateTitleWindow gets called whenever there's a change regarding the domain, so rather - // than placing this within domainURLChanged, it's placed here to cover the other potential cases. - DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", ""); -} - -void Application::clearDomainOctreeDetails(bool clearAll) { - // before we delete all entities get MyAvatar's AvatarEntityData ready - getMyAvatar()->prepareAvatarEntityDataForReload(); - - // if we're about to quit, we really don't need to do the rest of these things... - if (_aboutToQuit) { - return; - } - - qCDebug(interfaceapp) << "Clearing domain octree details..."; - - resetPhysicsReadyInformation(); - setIsInterstitialMode(true); - - _octreeServerSceneStats.withWriteLock([&] { - _octreeServerSceneStats.clear(); - }); - - // reset the model renderer - clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities(); - - auto skyStage = DependencyManager::get()->getSkyStage(); - - skyStage->setBackgroundMode(graphics::SunSkyStage::SKY_DEFAULT); - - DependencyManager::get()->clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); - MaterialCache::instance().clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); - ShaderCache::instance().clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); -} - -void Application::domainURLChanged(QUrl domainURL) { - // disable physics until we have enough information about our new location to not cause craziness. - resetPhysicsReadyInformation(); - setIsServerlessMode(domainURL.scheme() != URL_SCHEME_HIFI); - if (isServerlessMode()) { - loadServerlessDomain(domainURL); - } - updateWindowTitle(); -} - -void Application::goToErrorDomainURL(QUrl errorDomainURL) { - // disable physics until we have enough information about our new location to not cause craziness. - resetPhysicsReadyInformation(); - setIsServerlessMode(errorDomainURL.scheme() != URL_SCHEME_HIFI); - if (isServerlessMode()) { - loadErrorDomain(errorDomainURL); - } - updateWindowTitle(); -} - -void Application::resettingDomain() { - _notifiedPacketVersionMismatchThisDomain = false; - - clearDomainOctreeDetails(false); -} - -void Application::nodeAdded(SharedNodePointer node) const { - if (node->getType() == NodeType::EntityServer) { - if (!_failedToConnectToEntityServer) { - _entityServerConnectionTimer.stop(); - _entityServerConnectionTimer.setInterval(ENTITY_SERVER_CONNECTION_TIMEOUT); - _entityServerConnectionTimer.start(); - } - } -} - -void Application::nodeActivated(SharedNodePointer node) { - if (node->getType() == NodeType::AssetServer) { - // asset server just connected - check if we have the asset browser showing - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - - if (offscreenUi) { - auto nodeList = DependencyManager::get(); - - if (nodeList->getThisNodeCanWriteAssets()) { - // call reload on the shown asset browser dialog to get the mappings (if permissions allow) - auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; - if (assetDialog) { - QMetaObject::invokeMethod(assetDialog, "reload"); - } - } else { - // we switched to an Asset Server that we can't modify, hide the Asset Browser - offscreenUi->hide("AssetServer"); - } - } -#endif - } - - // If we get a new EntityServer activated, reset lastQueried time - // so we will do a proper query during update - if (node->getType() == NodeType::EntityServer) { - _queryExpiry = SteadyClock::now(); - _octreeQuery.incrementConnectionID(); - - if (!_failedToConnectToEntityServer) { - _entityServerConnectionTimer.stop(); - } - } - - if (node->getType() == NodeType::AudioMixer && !isInterstitialMode()) { - DependencyManager::get()->negotiateAudioFormat(); - } - - if (node->getType() == NodeType::AvatarMixer) { - _queryExpiry = SteadyClock::now(); - - // new avatar mixer, send off our identity packet on next update loop - // Reset skeletonModelUrl if the last server modified our choice. - // Override the avatar url (but not model name) here too. - if (_avatarOverrideUrl.isValid()) { - getMyAvatar()->useFullAvatarURL(_avatarOverrideUrl); - } - - if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->getSkeletonModelURL()) { - getMyAvatar()->resetFullAvatarURL(); - } - getMyAvatar()->markIdentityDataChanged(); - getMyAvatar()->resetLastSent(); - - if (!isInterstitialMode()) { - // transmit a "sendAll" packet to the AvatarMixer we just connected to. - getMyAvatar()->sendAvatarDataPacket(true); - } - } -} - -void Application::nodeKilled(SharedNodePointer node) { - // These are here because connecting NodeList::nodeKilled to OctreePacketProcessor::nodeKilled doesn't work: - // OctreePacketProcessor::nodeKilled is not being called when NodeList::nodeKilled is emitted. - // This may have to do with GenericThread::threadRoutine() blocking the QThread event loop - - _octreeProcessor.nodeKilled(node); - - _entityEditSender.nodeKilled(node); - - if (node->getType() == NodeType::AudioMixer) { - QMetaObject::invokeMethod(DependencyManager::get().data(), "audioMixerKilled"); - } else if (node->getType() == NodeType::EntityServer) { - // we lost an entity server, clear all of the domain octree details - clearDomainOctreeDetails(false); - } else if (node->getType() == NodeType::AssetServer) { - // asset server going away - check if we have the asset browser showing - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; - - if (assetDialog) { - // call reload on the shown asset browser dialog - QMetaObject::invokeMethod(assetDialog, "clear"); - } -#endif - } -} - -void Application::trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket) { - // Attempt to identify the sender from its address. - if (sendingNode) { - const QUuid& nodeUUID = sendingNode->getUUID(); - - // now that we know the node ID, let's add these stats to the stats for that node... - _octreeServerSceneStats.withWriteLock([&] { - if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { - OctreeSceneStats& stats = _octreeServerSceneStats[nodeUUID]; - stats.trackIncomingOctreePacket(message, wasStatsPacket, sendingNode->getClockSkewUsec()); - } - }); - } -} - -bool Application::gpuTextureMemSizeStable() { - auto renderConfig = qApp->getRenderEngine()->getConfiguration(); - auto renderStats = renderConfig->getConfig("Stats"); - - qint64 textureResourceGPUMemSize = renderStats->textureResourceGPUMemSize; - qint64 texturePopulatedGPUMemSize = renderStats->textureResourcePopulatedGPUMemSize; - qint64 textureTransferSize = renderStats->texturePendingGPUTransferSize; - - if (_gpuTextureMemSizeAtLastCheck == textureResourceGPUMemSize) { - _gpuTextureMemSizeStabilityCount++; - } else { - _gpuTextureMemSizeStabilityCount = 0; - } - _gpuTextureMemSizeAtLastCheck = textureResourceGPUMemSize; - - if (_gpuTextureMemSizeStabilityCount >= _minimumGPUTextureMemSizeStabilityCount) { - return (textureResourceGPUMemSize == texturePopulatedGPUMemSize) && (textureTransferSize == 0); - } - return false; -} - -int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode) { - // parse the incoming stats datas stick it in a temporary object for now, while we - // determine which server it belongs to - int statsMessageLength = 0; - - const QUuid& nodeUUID = sendingNode->getUUID(); - - // now that we know the node ID, let's add these stats to the stats for that node... - _octreeServerSceneStats.withWriteLock([&] { - OctreeSceneStats& octreeStats = _octreeServerSceneStats[nodeUUID]; - statsMessageLength = octreeStats.unpackFromPacket(message); - - if (octreeStats.isFullScene()) { - _fullSceneReceivedCounter++; - } - }); - - return statsMessageLength; -} - -void Application::packetSent(quint64 length) { -} - -void Application::addingEntityWithCertificate(const QString& certificateID, const QString& placeName) { - auto ledger = DependencyManager::get(); - ledger->updateLocation(certificateID, placeName); -} - -void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointer scriptEngine) { - - scriptEngine->setEmitScriptUpdatesFunction([this]() { - SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); - return !entityServerNode || isPhysicsEnabled(); - }); - - // setup the packet sender of the script engine's scripting interfaces so - // we can use the same ones from the application. - auto entityScriptingInterface = DependencyManager::get(); - entityScriptingInterface->setPacketSender(&_entityEditSender); - entityScriptingInterface->setEntityTree(getEntities()->getTree()); - - if (property(hifi::properties::TEST).isValid()) { - scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance()); - } - - scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); - scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); - - // hook our avatar and avatar hash map object into this script engine - getMyAvatar()->registerMetaTypes(scriptEngine); - - scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("Camera", &_myCamera); - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get().data()); -#endif - - ClipboardScriptingInterface* clipboardScriptable = new ClipboardScriptingInterface(); - scriptEngine->registerGlobalObject("Clipboard", clipboardScriptable); - connect(scriptEngine.data(), &ScriptEngine::finished, clipboardScriptable, &ClipboardScriptingInterface::deleteLater); - - scriptEngine->registerGlobalObject("Overlays", &_overlays); - qScriptRegisterMetaType(scriptEngine.data(), RayToOverlayIntersectionResultToScriptValue, - RayToOverlayIntersectionResultFromScriptValue); - -#if !defined(DISABLE_QML) - scriptEngine->registerGlobalObject("OffscreenFlags", getOffscreenUI()->getFlags()); - scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); -#endif - - qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); - qScriptRegisterMetaType(scriptEngine.data(), - wrapperToScriptValue, wrapperFromScriptValue); - scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); - - qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); - qScriptRegisterMetaType(scriptEngine.data(), - wrapperToScriptValue, wrapperFromScriptValue); - scriptEngine->registerGlobalObject("Tablet", DependencyManager::get().data()); - // FIXME remove these deprecated names for the tablet scripting interface - scriptEngine->registerGlobalObject("tabletInterface", DependencyManager::get().data()); - - auto toolbarScriptingInterface = DependencyManager::get().data(); - DependencyManager::get().data()->setToolbarScriptingInterface(toolbarScriptingInterface); - - scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); - scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, - LocationScriptingInterface::locationSetter, "Window"); - // register `location` on the global object. - scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, - LocationScriptingInterface::locationSetter); - - bool clientScript = scriptEngine->isClientScript(); - scriptEngine->registerFunction("OverlayWindow", clientScript ? QmlWindowClass::constructor : QmlWindowClass::restricted_constructor); -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) - scriptEngine->registerFunction("OverlayWebWindow", clientScript ? QmlWebWindowClass::constructor : QmlWebWindowClass::restricted_constructor); -#endif - scriptEngine->registerFunction("QmlFragment", clientScript ? QmlFragmentClass::constructor : QmlFragmentClass::restricted_constructor); - - scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); - scriptEngine->registerGlobalObject("DesktopPreviewProvider", DependencyManager::get().data()); -#if !defined(DISABLE_QML) - scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); -#endif - scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); - scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); - scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("AvatarBookmarks", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("LocationBookmarks", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("RayPick", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("LaserPointers", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Picks", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Pointers", DependencyManager::get().data()); - - // Caches - scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("TextureCache", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("ModelCache", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("DialogsManager", _dialogsManagerScriptingInterface); - - scriptEngine->registerGlobalObject("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - scriptEngine->registerGlobalObject("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - scriptEngine->registerGlobalObject("AccountServices", AccountServicesScriptingInterface::getInstance()); - qScriptRegisterMetaType(scriptEngine.data(), DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue); - - scriptEngine->registerGlobalObject("FaceTracker", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("HMD", DependencyManager::get().data()); - scriptEngine->registerFunction("HMD", "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0); - scriptEngine->registerFunction("HMD", "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0); - - scriptEngine->registerGlobalObject("Scene", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); - scriptEngine->registerGlobalObject("Workload", _gameWorkload._engine->getConfiguration().get()); - - GraphicsScriptingInterface::registerMetaTypes(scriptEngine.data()); - scriptEngine->registerGlobalObject("Graphics", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); - - scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("GooglePoly", DependencyManager::get().data()); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptEngine.data(), steamClient.get())); - } - auto scriptingInterface = DependencyManager::get(); - scriptEngine->registerGlobalObject("Controller", scriptingInterface.data()); - UserInputMapper::registerControllerTypes(scriptEngine.data()); - - auto recordingInterface = DependencyManager::get(); - scriptEngine->registerGlobalObject("Recording", recordingInterface.data()); - - auto entityScriptServerLog = DependencyManager::get(); - scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); - scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); - scriptEngine->registerGlobalObject("Selection", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("WalletScriptingInterface", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); - scriptEngine->registerGlobalObject("ResourceRequestObserver", DependencyManager::get().data()); - - registerInteractiveWindowMetaType(scriptEngine.data()); - - auto pickScriptingInterface = DependencyManager::get(); - pickScriptingInterface->registerMetaTypes(scriptEngine.data()); - - // connect this script engines printedMessage signal to the global ScriptEngines these various messages - auto scriptEngines = DependencyManager::get().data(); - connect(scriptEngine.data(), &ScriptEngine::printedMessage, scriptEngines, &ScriptEngines::onPrintedMessage); - connect(scriptEngine.data(), &ScriptEngine::errorMessage, scriptEngines, &ScriptEngines::onErrorMessage); - connect(scriptEngine.data(), &ScriptEngine::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage); - connect(scriptEngine.data(), &ScriptEngine::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage); - connect(scriptEngine.data(), &ScriptEngine::clearDebugWindow, scriptEngines, &ScriptEngines::onClearDebugWindow); - -} - -bool Application::canAcceptURL(const QString& urlString) const { - QUrl url(urlString); - if (url.query().contains(WEB_VIEW_TAG)) { - return false; - } else if (urlString.startsWith(URL_SCHEME_HIFI)) { - return true; - } - QString lowerPath = url.path().toLower(); - for (auto& pair : _acceptedExtensions) { - if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { - return true; - } - } - return false; -} - -bool Application::acceptURL(const QString& urlString, bool defaultUpload) { - QUrl url(urlString); - - if (url.scheme() == URL_SCHEME_HIFI) { - // this is a hifi URL - have the AddressManager handle it - QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", - Qt::AutoConnection, Q_ARG(const QString&, urlString)); - return true; - } - - QString lowerPath = url.path().toLower(); - for (auto& pair : _acceptedExtensions) { - if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { - AcceptURLMethod method = pair.second; - return (this->*method)(urlString); - } - } - - if (defaultUpload && !url.fileName().isEmpty() && url.isLocalFile()) { - showAssetServerWidget(urlString); - } - return defaultUpload; -} - -void Application::setSessionUUID(const QUuid& sessionUUID) const { - Physics::setSessionUUID(sessionUUID); -} - -bool Application::askToSetAvatarUrl(const QString& url) { - QUrl realUrl(url); - if (realUrl.isLocalFile()) { - OffscreenUi::asyncWarning("", "You can not use local files for avatar components."); - return false; - } - - // Download the FST file, to attempt to determine its model type - QVariantHash fstMapping = FSTReader::downloadMapping(url); - - FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); - - QString modelName = fstMapping["name"].toString(); - QString modelLicense = fstMapping["license"].toString(); - - bool agreeToLicense = true; // assume true - //create set avatar callback - auto setAvatar = [=] (QString url, QString modelName) { - ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Set Avatar", - "Would you like to use '" + modelName + "' for your avatar?", - QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - - bool ok = (QMessageBox::Ok == static_cast(answer.toInt())); - if (ok) { - getMyAvatar()->useFullAvatarURL(url, modelName); - emit fullAvatarURLChanged(url, modelName); - } else { - qCDebug(interfaceapp) << "Declined to use the avatar"; - } - }); - }; - - if (!modelLicense.isEmpty()) { - // word wrap the license text to fit in a reasonable shaped message box. - const int MAX_CHARACTERS_PER_LINE = 90; - modelLicense = simpleWordWrap(modelLicense, MAX_CHARACTERS_PER_LINE); - - ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Avatar Usage License", - modelLicense + "\nDo you agree to these terms?", - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - QObject::connect(dlg, &ModalDialogListener::response, this, [=, &agreeToLicense] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - - agreeToLicense = (static_cast(answer.toInt()) == QMessageBox::Yes); - if (agreeToLicense) { - switch (modelType) { - case FSTReader::HEAD_AND_BODY_MODEL: { - setAvatar(url, modelName); - break; - } - default: - OffscreenUi::asyncWarning("", modelName + "Does not support a head and body as required."); - break; - } - } else { - qCDebug(interfaceapp) << "Declined to agree to avatar license"; - } - - //auto offscreenUi = getOffscreenUI(); - }); - } else { - setAvatar(url, modelName); - } - - return true; -} - - -bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { - QString shortName = scriptFilenameOrURL; - - QUrl scriptURL { scriptFilenameOrURL }; - - if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { - int startIndex = shortName.lastIndexOf('/') + 1; - int endIndex = shortName.lastIndexOf('?'); - shortName = shortName.mid(startIndex, endIndex - startIndex); - } - -#ifdef DISABLE_QML - DependencyManager::get()->loadScript(scriptFilenameOrURL); -#else - QString message = "Would you like to run this script:\n" + shortName; - ModalDialogListener* dlg = OffscreenUi::asyncQuestion(getWindow(), "Run Script", message, - QMessageBox::Yes | QMessageBox::No); - - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - const QString& fileName = scriptFilenameOrURL; - if (static_cast(answer.toInt()) == QMessageBox::Yes) { - qCDebug(interfaceapp) << "Chose to run the script: " << fileName; - DependencyManager::get()->loadScript(fileName); - } else { - qCDebug(interfaceapp) << "Declined to run the script"; - } - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - }); -#endif - return true; -} - -bool Application::askToWearAvatarAttachmentUrl(const QString& url) { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - int requestNumber = ++_avatarAttachmentRequest; - connect(reply, &QNetworkReply::finished, [this, reply, url, requestNumber]() { - - if (requestNumber != _avatarAttachmentRequest) { - // this request has been superseded by another more recent request - reply->deleteLater(); - return; - } - - QNetworkReply::NetworkError networkError = reply->error(); - if (networkError == QNetworkReply::NoError) { - // download success - QByteArray contents = reply->readAll(); - - QJsonParseError jsonError; - auto doc = QJsonDocument::fromJson(contents, &jsonError); - if (jsonError.error == QJsonParseError::NoError) { - - auto jsonObject = doc.object(); - - // retrieve optional name field from JSON - QString name = tr("Unnamed Attachment"); - auto nameValue = jsonObject.value("name"); - if (nameValue.isString()) { - name = nameValue.toString(); - } - - auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); - auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); - ModalDialogListener* dlg = OffscreenUi::asyncQuestion(avatarAttachmentConfirmationTitle, - avatarAttachmentConfirmationMessage, - QMessageBox::Ok | QMessageBox::Cancel); - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - if (static_cast(answer.toInt()) == QMessageBox::Yes) { - // add attachment to avatar - auto myAvatar = getMyAvatar(); - assert(myAvatar); - auto attachmentDataVec = myAvatar->getAttachmentData(); - AttachmentData attachmentData; - attachmentData.fromJson(jsonObject); - attachmentDataVec.push_back(attachmentData); - myAvatar->setAttachmentData(attachmentDataVec); - } else { - qCDebug(interfaceapp) << "User declined to wear the avatar attachment"; - } - }); - } else { - // json parse error - auto avatarAttachmentParseErrorString = tr("Error parsing attachment JSON from url: \"%1\""); - displayAvatarAttachmentWarning(avatarAttachmentParseErrorString.arg(url)); - } - } else { - // download failure - auto avatarAttachmentDownloadErrorString = tr("Error downloading attachment JSON from url: \"%1\""); - displayAvatarAttachmentWarning(avatarAttachmentDownloadErrorString.arg(url)); - } - reply->deleteLater(); - }); - return true; -} - -void Application::replaceDomainContent(const QString& url) { - qCDebug(interfaceapp) << "Attempting to replace domain content"; - QByteArray urlData(url.toUtf8()); - auto limitedNodeList = DependencyManager::get(); - const auto& domainHandler = limitedNodeList->getDomainHandler(); - - auto octreeFilePacket = NLPacket::create(PacketType::DomainContentReplacementFromUrl, urlData.size(), true); - octreeFilePacket->write(urlData); - limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); - - auto addressManager = DependencyManager::get(); - addressManager->handleLookupString(DOMAIN_SPAWNING_POINT); - QString newHomeAddress = addressManager->getHost() + DOMAIN_SPAWNING_POINT; - qCDebug(interfaceapp) << "Setting new home bookmark to: " << newHomeAddress; - DependencyManager::get()->setHomeLocationToAddress(newHomeAddress); -} - -bool Application::askToReplaceDomainContent(const QString& url) { - QString methodDetails; - const int MAX_CHARACTERS_PER_LINE = 90; - if (DependencyManager::get()->getThisNodeCanReplaceContent()) { - QUrl originURL { url }; - if (originURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { - // Create a confirmation dialog when this call is made - static const QString infoText = simpleWordWrap("Your domain's content will be replaced with a new content set. " - "If you want to save what you have now, create a backup before proceeding. For more information about backing up " - "and restoring content, visit the documentation page at: ", MAX_CHARACTERS_PER_LINE) + - "\nhttps://docs.highfidelity.com/create-and-explore/start-working-in-your-sandbox/restoring-sandbox-content"; - - ModalDialogListener* dig = OffscreenUi::asyncQuestion("Are you sure you want to replace this domain's content set?", - infoText, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - QObject::connect(dig, &ModalDialogListener::response, this, [=] (QVariant answer) { - QString details; - if (static_cast(answer.toInt()) == QMessageBox::Yes) { - // Given confirmation, send request to domain server to replace content - replaceDomainContent(url); - details = "SuccessfulRequestToReplaceContent"; - } else { - details = "UserDeclinedToReplaceContent"; - } - QJsonObject messageProperties = { - { "status", details }, - { "content_set_url", url } - }; - UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - QObject::disconnect(dig, &ModalDialogListener::response, this, nullptr); - }); - } else { - methodDetails = "ContentSetDidNotOriginateFromMarketplace"; - QJsonObject messageProperties = { - { "status", methodDetails }, - { "content_set_url", url } - }; - UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - } - } else { - methodDetails = "UserDoesNotHavePermissionToReplaceContent"; - static const QString warningMessage = simpleWordWrap("The domain owner must enable 'Replace Content' " - "permissions for you in this domain's server settings before you can continue.", MAX_CHARACTERS_PER_LINE); - OffscreenUi::asyncWarning("You do not have permissions to replace domain content", warningMessage, - QMessageBox::Ok, QMessageBox::Ok); - - QJsonObject messageProperties = { - { "status", methodDetails }, - { "content_set_url", url } - }; - UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - } - return true; -} - -void Application::displayAvatarAttachmentWarning(const QString& message) const { - auto avatarAttachmentWarningTitle = tr("Avatar Attachment Failure"); - OffscreenUi::asyncWarning(avatarAttachmentWarningTitle, message); -} - -void Application::showDialog(const QUrl& widgetUrl, const QUrl& tabletUrl, const QString& name) const { - auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); - auto hmd = DependencyManager::get(); - bool onTablet = false; - - if (!tablet->getToolbarMode()) { - onTablet = tablet->pushOntoStack(tabletUrl); - if (onTablet) { - toggleTabletUI(true); - } - } else { -#if !defined(DISABLE_QML) - getOffscreenUI()->show(widgetUrl, name); -#endif - } -} - -void Application::showScriptLogs() { - QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); - defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); - DependencyManager::get()->loadScript(defaultScriptsLoc.toString()); -} - -void Application::showAssetServerWidget(QString filePath) { - if (!DependencyManager::get()->getThisNodeCanWriteAssets() || getLoginDialogPoppedUp()) { - return; - } - static const QUrl url { "hifi/AssetServer.qml" }; - - auto startUpload = [=](QQmlContext* context, QObject* newObject){ - if (!filePath.isEmpty()) { - emit uploadRequest(filePath); - } - }; - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - auto hmd = DependencyManager::get(); - if (tablet->getToolbarMode()) { - getOffscreenUI()->show(url, "AssetServer", startUpload); - } else { - if (!hmd->getShouldShowTablet() && !isHMDMode()) { - getOffscreenUI()->show(url, "AssetServer", startUpload); - } else { - static const QUrl url("hifi/dialogs/TabletAssetServer.qml"); - if (!tablet->isPathLoaded(url)) { - tablet->pushOntoStack(url); - } - } - } - - startUpload(nullptr, nullptr); -} - -void Application::addAssetToWorldFromURL(QString url) { - - QString filename; - if (url.contains("filename")) { - filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. - } - if (url.contains("poly.google.com/downloads")) { - filename = url.section('/', -1); - if (url.contains("noDownload")) { - filename.remove(".zip?noDownload=false"); - } else { - filename.remove(".zip"); - } - - } - - if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { - QString errorInfo = "You do not have permissions to write to the Asset Server."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filename, errorInfo); - return; - } - - addAssetToWorldInfo(filename, "Downloading model file " + filename + "."); - - auto request = DependencyManager::get()->createResourceRequest( - nullptr, QUrl(url), true, -1, "Application::addAssetToWorldFromURL"); - connect(request, &ResourceRequest::finished, this, &Application::addAssetToWorldFromURLRequestFinished); - request->send(); -} - -void Application::addAssetToWorldFromURLRequestFinished() { - auto request = qobject_cast(sender()); - auto url = request->getUrl().toString(); - auto result = request->getResult(); - - QString filename; - bool isBlocks = false; - - if (url.contains("filename")) { - filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. - } - if (url.contains("poly.google.com/downloads")) { - filename = url.section('/', -1); - if (url.contains("noDownload")) { - filename.remove(".zip?noDownload=false"); - } else { - filename.remove(".zip"); - } - isBlocks = true; - } - - if (result == ResourceRequest::Success) { - QTemporaryDir temporaryDir; - temporaryDir.setAutoRemove(false); - if (temporaryDir.isValid()) { - QString temporaryDirPath = temporaryDir.path(); - QString downloadPath = temporaryDirPath + "/" + filename; - - QFile tempFile(downloadPath); - if (tempFile.open(QIODevice::WriteOnly)) { - tempFile.write(request->getData()); - addAssetToWorldInfoClear(filename); // Remove message from list; next one added will have a different key. - tempFile.close(); - qApp->getFileDownloadInterface()->runUnzip(downloadPath, url, true, false, isBlocks); - } else { - QString errorInfo = "Couldn't open temporary file for download"; - qWarning(interfaceapp) << errorInfo; - addAssetToWorldError(filename, errorInfo); - } - } else { - QString errorInfo = "Couldn't create temporary directory for download"; - qWarning(interfaceapp) << errorInfo; - addAssetToWorldError(filename, errorInfo); - } - } else { - qWarning(interfaceapp) << "Error downloading" << url << ":" << request->getResultString(); - addAssetToWorldError(filename, "Error downloading " + filename + " : " + request->getResultString()); - } - - request->deleteLater(); -} - - -QString filenameFromPath(QString filePath) { - return filePath.right(filePath.length() - filePath.lastIndexOf("/") - 1); -} - -void Application::addAssetToWorldUnzipFailure(QString filePath) { - QString filename = filenameFromPath(QUrl(filePath).toLocalFile()); - qWarning(interfaceapp) << "Couldn't unzip file" << filePath; - addAssetToWorldError(filename, "Couldn't unzip file " + filename + "."); -} - -void Application::addAssetToWorld(QString path, QString zipFile, bool isZip, bool isBlocks) { - // Automatically upload and add asset to world as an alternative manual process initiated by showAssetServerWidget(). - QString mapping; - QString filename = filenameFromPath(path); - if (isZip || isBlocks) { - QString assetName = zipFile.section("/", -1).remove(QRegExp("[.]zip(.*)$")); - QString assetFolder = path.section("model_repo/", -1); - mapping = "/" + assetName + "/" + assetFolder; - } else { - mapping = "/" + filename; - } - - // Test repeated because possibly different code paths. - if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { - QString errorInfo = "You do not have permissions to write to the Asset Server."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filename, errorInfo); - return; - } - - addAssetToWorldInfo(filename, "Adding " + mapping.mid(1) + " to the Asset Server."); - - addAssetToWorldWithNewMapping(path, mapping, 0, isZip, isBlocks); -} - -void Application::addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy, bool isZip, bool isBlocks) { - auto request = DependencyManager::get()->createGetMappingRequest(mapping); - - QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { - const int MAX_COPY_COUNT = 100; // Limit number of duplicate assets; recursion guard. - auto result = request->getError(); - if (result == GetMappingRequest::NotFound) { - addAssetToWorldUpload(filePath, mapping, isZip, isBlocks); - } else if (result != GetMappingRequest::NoError) { - QString errorInfo = "Could not map asset name: " - + mapping.left(mapping.length() - QString::number(copy).length() - 1); - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else if (copy < MAX_COPY_COUNT - 1) { - if (copy > 0) { - mapping = mapping.remove(mapping.lastIndexOf("-"), QString::number(copy).length() + 1); - } - copy++; - mapping = mapping.insert(mapping.lastIndexOf("."), "-" + QString::number(copy)); - addAssetToWorldWithNewMapping(filePath, mapping, copy, isZip, isBlocks); - } else { - QString errorInfo = "Too many copies of asset name: " - + mapping.left(mapping.length() - QString::number(copy).length() - 1); - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } - request->deleteLater(); - }); - - request->start(); -} - -void Application::addAssetToWorldUpload(QString filePath, QString mapping, bool isZip, bool isBlocks) { - qInfo(interfaceapp) << "Uploading" << filePath << "to Asset Server as" << mapping; - auto upload = DependencyManager::get()->createUpload(filePath); - QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { - if (upload->getError() != AssetUpload::NoError) { - QString errorInfo = "Could not upload model to the Asset Server."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else { - addAssetToWorldSetMapping(filePath, mapping, hash, isZip, isBlocks); - } - - // Remove temporary directory created by Clara.io market place download. - int index = filePath.lastIndexOf("/model_repo/"); - if (index > 0) { - QString tempDir = filePath.left(index); - qCDebug(interfaceapp) << "Removing temporary directory at: " + tempDir; - QDir(tempDir).removeRecursively(); - } - - upload->deleteLater(); - }); - - upload->start(); -} - -void Application::addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash, bool isZip, bool isBlocks) { - auto request = DependencyManager::get()->createSetMappingRequest(mapping, hash); - connect(request, &SetMappingRequest::finished, this, [=](SetMappingRequest* request) mutable { - if (request->getError() != SetMappingRequest::NoError) { - QString errorInfo = "Could not set asset mapping."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else { - // to prevent files that aren't models or texture files from being loaded into world automatically - if ((filePath.toLower().endsWith(OBJ_EXTENSION) || filePath.toLower().endsWith(FBX_EXTENSION)) || - ((filePath.toLower().endsWith(JPG_EXTENSION) || filePath.toLower().endsWith(PNG_EXTENSION)) && - ((!isBlocks) && (!isZip)))) { - addAssetToWorldAddEntity(filePath, mapping); - } else { - qCDebug(interfaceapp) << "Zipped contents are not supported entity files"; - addAssetToWorldInfoDone(filenameFromPath(filePath)); - } - } - request->deleteLater(); - }); - - request->start(); -} - -void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) { - EntityItemProperties properties; - properties.setName(mapping.right(mapping.length() - 1)); - if (filePath.toLower().endsWith(PNG_EXTENSION) || filePath.toLower().endsWith(JPG_EXTENSION)) { - properties.setType(EntityTypes::Image); - properties.setImageURL(QString("atp:" + mapping)); - properties.setKeepAspectRatio(false); - } else { - properties.setType(EntityTypes::Model); - properties.setModelURL("atp:" + mapping); - properties.setShapeType(SHAPE_TYPE_SIMPLE_COMPOUND); - } - properties.setCollisionless(true); // Temporarily set so that doesn't collide with avatar. - properties.setVisible(false); // Temporarily set so that don't see at large unresized dimensions. - bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); - properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); - glm::vec3 positionOffset = getMyAvatar()->getWorldOrientation() * (getMyAvatar()->getSensorToWorldScale() * glm::vec3(0.0f, 0.0f, -2.0f)); - properties.setPosition(getMyAvatar()->getWorldPosition() + positionOffset); - properties.setRotation(getMyAvatar()->getWorldOrientation()); - properties.setGravity(glm::vec3(0.0f, 0.0f, 0.0f)); - auto entityID = DependencyManager::get()->addEntity(properties); - - // Note: Model dimensions are not available here; model is scaled per FBX mesh in RenderableModelEntityItem::update() later - // on. But FBX dimensions may be in cm, so we monitor for the dimension change and rescale again if warranted. - - if (entityID == QUuid()) { - QString errorInfo = "Could not add model " + mapping + " to world."; - qWarning(interfaceapp) << "Could not add model to world: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else { - // Monitor when asset is rendered in world so that can resize if necessary. - _addAssetToWorldResizeList.insert(entityID, 0); // List value is count of checks performed. - if (!_addAssetToWorldResizeTimer.isActive()) { - _addAssetToWorldResizeTimer.start(); - } - - // Close progress message box. - addAssetToWorldInfoDone(filenameFromPath(filePath)); - } -} - -void Application::addAssetToWorldCheckModelSize() { - if (_addAssetToWorldResizeList.size() == 0) { - return; - } - - auto item = _addAssetToWorldResizeList.begin(); - while (item != _addAssetToWorldResizeList.end()) { - auto entityID = item.key(); - - EntityPropertyFlags propertyFlags; - propertyFlags += PROP_NAME; - propertyFlags += PROP_DIMENSIONS; - auto entityScriptingInterface = DependencyManager::get(); - auto properties = entityScriptingInterface->getEntityProperties(entityID, propertyFlags); - auto name = properties.getName(); - auto dimensions = properties.getDimensions(); - - bool doResize = false; - - const glm::vec3 DEFAULT_DIMENSIONS = glm::vec3(0.1f, 0.1f, 0.1f); - if (dimensions != DEFAULT_DIMENSIONS) { - - // Scale model so that its maximum is exactly specific size. - const float MAXIMUM_DIMENSION = getMyAvatar()->getSensorToWorldScale(); - auto previousDimensions = dimensions; - auto scale = std::min(MAXIMUM_DIMENSION / dimensions.x, std::min(MAXIMUM_DIMENSION / dimensions.y, - MAXIMUM_DIMENSION / dimensions.z)); - dimensions *= scale; - qInfo(interfaceapp) << "Model" << name << "auto-resized from" << previousDimensions << " to " << dimensions; - doResize = true; - - item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. - } else { - // Increment count of checks done. - _addAssetToWorldResizeList[entityID]++; - - const int CHECK_MODEL_SIZE_MAX_CHECKS = 300; - if (_addAssetToWorldResizeList[entityID] > CHECK_MODEL_SIZE_MAX_CHECKS) { - // Have done enough checks; model was either the default size or something's gone wrong. - - // Rescale all dimensions. - const glm::vec3 UNIT_DIMENSIONS = glm::vec3(1.0f, 1.0f, 1.0f); - dimensions = UNIT_DIMENSIONS; - qInfo(interfaceapp) << "Model" << name << "auto-resize timed out; resized to " << dimensions; - doResize = true; - - item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. - } else { - // No action on this entity; advance to next. - ++item; - } - } - - if (doResize) { - EntityItemProperties properties; - properties.setDimensions(dimensions); - properties.setVisible(true); - if (!name.toLower().endsWith(PNG_EXTENSION) && !name.toLower().endsWith(JPG_EXTENSION)) { - properties.setCollisionless(false); - } - bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); - properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); - properties.setLastEdited(usecTimestampNow()); - entityScriptingInterface->editEntity(entityID, properties); - } - } - - // Stop timer if nothing in list to check. - if (_addAssetToWorldResizeList.size() == 0) { - _addAssetToWorldResizeTimer.stop(); - } -} - - -void Application::addAssetToWorldInfo(QString modelName, QString infoText) { - // Displays the most recent info message, subject to being overridden by error messages. - - if (_aboutToQuit) { - return; - } - - /* - Cancel info timer if running. - If list has an entry for modelName, delete it (just one). - Append modelName, infoText to list. - Display infoText in message box unless an error is being displayed (i.e., error timer is running). - Show message box if not already visible. - */ - - _addAssetToWorldInfoTimer.stop(); - - addAssetToWorldInfoClear(modelName); - - _addAssetToWorldInfoKeys.append(modelName); - _addAssetToWorldInfoMessages.append(infoText); - - if (!_addAssetToWorldErrorTimer.isActive()) { - if (!_addAssetToWorldMessageBox) { - _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, - "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); - connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); - } - - _addAssetToWorldMessageBox->setProperty("text", "\n" + infoText); - _addAssetToWorldMessageBox->setVisible(true); - } -} - -void Application::addAssetToWorldInfoClear(QString modelName) { - // Clears modelName entry from message list without affecting message currently displayed. - - if (_aboutToQuit) { - return; - } - - /* - Delete entry for modelName from list. - */ - - auto index = _addAssetToWorldInfoKeys.indexOf(modelName); - if (index > -1) { - _addAssetToWorldInfoKeys.removeAt(index); - _addAssetToWorldInfoMessages.removeAt(index); - } -} - -void Application::addAssetToWorldInfoDone(QString modelName) { - // Continues to display this message if the latest for a few seconds, then deletes it and displays the next latest. - - if (_aboutToQuit) { - return; - } - - /* - Delete entry for modelName from list. - (Re)start the info timer to update message box. ... onAddAssetToWorldInfoTimeout() - */ - - addAssetToWorldInfoClear(modelName); - _addAssetToWorldInfoTimer.start(); -} - -void Application::addAssetToWorldInfoTimeout() { - if (_aboutToQuit) { - return; - } - - /* - If list not empty, display last message in list (may already be displayed ) unless an error is being displayed. - If list empty, close the message box unless an error is being displayed. - */ - - if (!_addAssetToWorldErrorTimer.isActive() && _addAssetToWorldMessageBox) { - if (_addAssetToWorldInfoKeys.length() > 0) { - _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); - } else { - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox->setVisible(false); - _addAssetToWorldMessageBox->deleteLater(); - _addAssetToWorldMessageBox = nullptr; - } - } -} - -void Application::addAssetToWorldError(QString modelName, QString errorText) { - // Displays the most recent error message for a few seconds. - - if (_aboutToQuit) { - return; - } - - /* - If list has an entry for modelName, delete it. - Display errorText in message box. - Show message box if not already visible. - (Re)start error timer. ... onAddAssetToWorldErrorTimeout() - */ - - addAssetToWorldInfoClear(modelName); - - if (!_addAssetToWorldMessageBox) { - _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, - "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); - connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); - } - - _addAssetToWorldMessageBox->setProperty("text", "\n" + errorText); - _addAssetToWorldMessageBox->setVisible(true); - - _addAssetToWorldErrorTimer.start(); -} - -void Application::addAssetToWorldErrorTimeout() { - if (_aboutToQuit) { - return; - } - - /* - If list is not empty, display message from last entry. - If list is empty, close the message box. - */ - - if (_addAssetToWorldMessageBox) { - if (_addAssetToWorldInfoKeys.length() > 0) { - _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); - } else { - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox->setVisible(false); - _addAssetToWorldMessageBox->deleteLater(); - _addAssetToWorldMessageBox = nullptr; - } - } -} - - -void Application::addAssetToWorldMessageClose() { - // Clear messages, e.g., if Interface is being closed or domain changes. - - /* - Call if user manually closes message box. - Call if domain changes. - Call if application is shutting down. - - Stop timers. - Close the message box if open. - Clear lists. - */ - - _addAssetToWorldInfoTimer.stop(); - _addAssetToWorldErrorTimer.stop(); - - if (_addAssetToWorldMessageBox) { - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox->setVisible(false); - _addAssetToWorldMessageBox->deleteLater(); - _addAssetToWorldMessageBox = nullptr; - } - - _addAssetToWorldInfoKeys.clear(); - _addAssetToWorldInfoMessages.clear(); -} - -void Application::onAssetToWorldMessageBoxClosed() { - if (_addAssetToWorldMessageBox) { - // User manually closed message box; perhaps because it has become stuck, so reset all messages. - qInfo(interfaceapp) << "User manually closed download status message box"; - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox = nullptr; - addAssetToWorldMessageClose(); - } -} - - -void Application::handleUnzip(QString zipFile, QStringList unzipFile, bool autoAdd, bool isZip, bool isBlocks) { - if (autoAdd) { - if (!unzipFile.isEmpty()) { - for (int i = 0; i < unzipFile.length(); i++) { - if (QFileInfo(unzipFile.at(i)).isFile()) { - qCDebug(interfaceapp) << "Preparing file for asset server: " << unzipFile.at(i); - addAssetToWorld(unzipFile.at(i), zipFile, isZip, isBlocks); - } - } - } else { - addAssetToWorldUnzipFailure(zipFile); - } - } else { - showAssetServerWidget(unzipFile.first()); - } -} - -void Application::packageModel() { - ModelPackager::package(); -} - -void Application::openUrl(const QUrl& url) const { - if (!url.isEmpty()) { - if (url.scheme() == URL_SCHEME_HIFI) { - DependencyManager::get()->handleLookupString(url.toString()); - } else if (url.scheme() == URL_SCHEME_HIFIAPP) { - DependencyManager::get()->openSystemApp(url.path()); - } else { - // address manager did not handle - ask QDesktopServices to handle - QDesktopServices::openUrl(url); - } - } -} - -void Application::loadDialog() { - ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"), - getPreviousScriptLocation(), - tr("JavaScript Files (*.js)")); - connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - disconnect(dlg, &ModalDialogListener::response, this, nullptr); - const QString& response = answer.toString(); - if (!response.isEmpty() && QFile(response).exists()) { - setPreviousScriptLocation(QFileInfo(response).absolutePath()); - DependencyManager::get()->loadScript(response, true, false, false, true); // Don't load from cache - } - }); -} - -QString Application::getPreviousScriptLocation() { - QString result = _previousScriptLocation.get(); - return result; -} - -void Application::setPreviousScriptLocation(const QString& location) { - _previousScriptLocation.set(location); -} - -void Application::loadScriptURLDialog() const { - ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); - connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { - disconnect(dlg, &ModalDialogListener::response, this, nullptr); - const QString& newScript = response.toString(); - if (QUrl(newScript).scheme() == "atp") { - OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP"); - } else if (!newScript.isEmpty()) { - DependencyManager::get()->loadScript(newScript.trimmed()); - } - }); -} - -SharedSoundPointer Application::getSampleSound() const { - return _sampleSound; -} - -void Application::loadLODToolsDialog() { - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->lodTools(); - } else { - tablet->pushOntoStack("hifi/dialogs/TabletLODTools.qml"); - } -} - -void Application::loadEntityStatisticsDialog() { - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->octreeStatsDetails(); - } else { - tablet->pushOntoStack("hifi/dialogs/TabletEntityStatistics.qml"); - } -} - -void Application::loadDomainConnectionDialog() { - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->showDomainConnectionDialog(); - } else { - tablet->pushOntoStack("hifi/dialogs/TabletDCDialog.qml"); - } -} - -void Application::toggleLogDialog() { -#ifndef ANDROID_APP_QUEST_INTERFACE - if (getLoginDialogPoppedUp()) { - return; - } - if (! _logDialog) { - - bool keepOnTop =_keepLogWindowOnTop.get(); -#ifdef Q_OS_WIN - _logDialog = new LogDialog(keepOnTop ? qApp->getWindow() : nullptr, getLogger()); -#elif !defined(Q_OS_ANDROID) - _logDialog = new LogDialog(nullptr, getLogger()); - - if (keepOnTop) { - Qt::WindowFlags flags = _logDialog->windowFlags() | Qt::Tool; - _logDialog->setWindowFlags(flags); - } -#endif - } - - if (_logDialog->isVisible()) { - _logDialog->hide(); - } else { - _logDialog->show(); - } -#endif -} - - void Application::recreateLogWindow(int keepOnTop) { - _keepLogWindowOnTop.set(keepOnTop != 0); - if (_logDialog) { - bool toggle = _logDialog->isVisible(); - _logDialog->close(); - _logDialog = nullptr; - - if (toggle) { - toggleLogDialog(); - } - } - } - -void Application::toggleEntityScriptServerLogDialog() { - if (! _entityScriptServerLogDialog) { - _entityScriptServerLogDialog = new EntityScriptServerLogDialog(nullptr); - } - - if (_entityScriptServerLogDialog->isVisible()) { - _entityScriptServerLogDialog->hide(); - } else { - _entityScriptServerLogDialog->show(); - } -} - -void Application::loadAddAvatarBookmarkDialog() const { - auto avatarBookmarks = DependencyManager::get(); -} - -void Application::loadAvatarBrowser() const { - auto tablet = dynamic_cast(DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); - // construct the url to the marketplace item - QString url = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace?category=avatars"; - - QString MARKETPLACES_INJECT_SCRIPT_PATH = "file:///" + qApp->applicationDirPath() + "/scripts/system/html/js/marketplacesInject.js"; - tablet->gotoWebScreen(url, MARKETPLACES_INJECT_SCRIPT_PATH); - DependencyManager::get()->openTablet(); -} - -void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) { - postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] { - // Get a screenshot and save it - QString path = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, - TestScriptingInterface::getInstance()->getTestResultsLocation()); - - // If we're not doing an animated snapshot as well... - if (!includeAnimated) { - if (!path.isEmpty()) { - // Tell the dependency manager that the capture of the still snapshot has taken place. - emit DependencyManager::get()->stillSnapshotTaken(path, notify); - } - } else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) { - // Get an animated GIF snapshot and save it - SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); - } - }); -} - -void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { - postLambdaEvent([notify, filename, this] { - QString snapshotPath = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, - TestScriptingInterface::getInstance()->getTestResultsLocation()); - - emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, notify); - }); -} - -void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { - postLambdaEvent([notify, filename, cubemapOutputFormat, cameraPosition] { - DependencyManager::get()->save360Snapshot(cameraPosition, cubemapOutputFormat, notify, filename); - }); -} - -void Application::shareSnapshot(const QString& path, const QUrl& href) { - postLambdaEvent([path, href] { - // not much to do here, everything is done in snapshot code... - DependencyManager::get()->uploadSnapshot(path, href); - }); -} - -float Application::getRenderResolutionScale() const { - auto menu = Menu::getInstance(); - if (!menu) { - return 1.0f; - } - if (menu->isOptionChecked(MenuOption::RenderResolutionOne)) { - return 1.0f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionTwoThird)) { - return 0.666f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionHalf)) { - return 0.5f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionThird)) { - return 0.333f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionQuarter)) { - return 0.25f; - } else { - return 1.0f; - } -} - -void Application::notifyPacketVersionMismatch() { - if (!_notifiedPacketVersionMismatchThisDomain && !isInterstitialMode()) { - _notifiedPacketVersionMismatchThisDomain = true; - - QString message = "The location you are visiting is running an incompatible server version.\n"; - message += "Content may not display properly."; - - OffscreenUi::asyncWarning("", message); - } -} - -void Application::checkSkeleton() const { - if (getMyAvatar()->getSkeletonModel()->isActive() && !getMyAvatar()->getSkeletonModel()->hasSkeleton()) { - qCDebug(interfaceapp) << "MyAvatar model has no skeleton"; - - QString message = "Your selected avatar body has no skeleton.\n\nThe default body will be loaded..."; - OffscreenUi::asyncWarning("", message); - - getMyAvatar()->useFullAvatarURL(AvatarData::defaultFullAvatarModelUrl(), DEFAULT_FULL_AVATAR_MODEL_NAME); - } else { - _physicsEngine->setCharacterController(getMyAvatar()->getCharacterController()); - } -} - -void Application::activeChanged(Qt::ApplicationState state) { - switch (state) { - case Qt::ApplicationActive: - _isForeground = true; - break; - - case Qt::ApplicationSuspended: - case Qt::ApplicationHidden: - case Qt::ApplicationInactive: - default: - _isForeground = false; - break; - } -} - -void Application::windowMinimizedChanged(bool minimized) { - // initialize the _minimizedWindowTimer - static std::once_flag once; - std::call_once(once, [&] { - connect(&_minimizedWindowTimer, &QTimer::timeout, this, [] { - QCoreApplication::postEvent(QCoreApplication::instance(), new QEvent(static_cast(Idle)), Qt::HighEventPriority); - }); - }); - - // avoid rendering to the display plugin but continue posting Idle events, - // so that physics continues to simulate and the deadlock watchdog knows we're alive - if (!minimized && !getActiveDisplayPlugin()->isActive()) { - _minimizedWindowTimer.stop(); - getActiveDisplayPlugin()->activate(); - } else if (minimized && getActiveDisplayPlugin()->isActive()) { - getActiveDisplayPlugin()->deactivate(); - _minimizedWindowTimer.start(THROTTLED_SIM_FRAME_PERIOD_MS); - } -} - -void Application::postLambdaEvent(const std::function& f) { - if (this->thread() == QThread::currentThread()) { - f(); - } else { - QCoreApplication::postEvent(this, new LambdaEvent(f)); - } -} - -void Application::sendLambdaEvent(const std::function& f) { - if (this->thread() == QThread::currentThread()) { - f(); - } else { - LambdaEvent event(f); - QCoreApplication::sendEvent(this, &event); - } -} - -void Application::initPlugins(const QStringList& arguments) { - QCommandLineOption display("display", "Preferred displays", "displays"); - QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "displays"); - QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "inputs"); - - QCommandLineParser parser; - parser.addOption(display); - parser.addOption(disableDisplays); - parser.addOption(disableInputs); - parser.parse(arguments); - - if (parser.isSet(display)) { - auto preferredDisplays = parser.value(display).split(',', QString::SkipEmptyParts); - qInfo() << "Setting prefered display plugins:" << preferredDisplays; - PluginManager::getInstance()->setPreferredDisplayPlugins(preferredDisplays); - } - - if (parser.isSet(disableDisplays)) { - auto disabledDisplays = parser.value(disableDisplays).split(',', QString::SkipEmptyParts); - qInfo() << "Disabling following display plugins:" << disabledDisplays; - PluginManager::getInstance()->disableDisplays(disabledDisplays); - } - - if (parser.isSet(disableInputs)) { - auto disabledInputs = parser.value(disableInputs).split(',', QString::SkipEmptyParts); - qInfo() << "Disabling following input plugins:" << disabledInputs; - PluginManager::getInstance()->disableInputs(disabledInputs); - } -} - -void Application::shutdownPlugins() { -} - -glm::uvec2 Application::getCanvasSize() const { - return glm::uvec2(_glWidget->width(), _glWidget->height()); -} - -QRect Application::getRenderingGeometry() const { - auto geometry = _glWidget->geometry(); - auto topLeft = geometry.topLeft(); - auto topLeftScreen = _glWidget->mapToGlobal(topLeft); - geometry.moveTopLeft(topLeftScreen); - return geometry; -} - -glm::uvec2 Application::getUiSize() const { - static const uint MIN_SIZE = 1; - glm::uvec2 result(MIN_SIZE); - if (_displayPlugin) { - result = getActiveDisplayPlugin()->getRecommendedUiSize(); - } - return result; -} - -QRect Application::getRecommendedHUDRect() const { - auto uiSize = getUiSize(); - QRect result(0, 0, uiSize.x, uiSize.y); - if (_displayPlugin) { - result = getActiveDisplayPlugin()->getRecommendedHUDRect(); - } - return result; -} - -glm::vec2 Application::getDeviceSize() const { - static const int MIN_SIZE = 1; - glm::vec2 result(MIN_SIZE); - if (_displayPlugin) { - result = getActiveDisplayPlugin()->getRecommendedRenderSize(); - } - return result; -} - -bool Application::isThrottleRendering() const { - if (_displayPlugin) { - return getActiveDisplayPlugin()->isThrottled(); - } - return false; -} - -bool Application::hasFocus() const { - bool result = (QApplication::activeWindow() != nullptr); -#if defined(Q_OS_WIN) - // On Windows, QWidget::activateWindow() - as called in setFocus() - makes the application's taskbar icon flash but doesn't - // take user focus away from their current window. So also check whether the application is the user's current foreground - // window. - result = result && (HWND)QApplication::activeWindow()->winId() == GetForegroundWindow(); -#endif - return result; -} - -void Application::setFocus() { - // Note: Windows doesn't allow a user focus to be taken away from another application. Instead, it changes the color of and - // flashes the taskbar icon. - auto window = qApp->getWindow(); - window->activateWindow(); -} - -void Application::raise() { - auto windowState = qApp->getWindow()->windowState(); - if (windowState & Qt::WindowMinimized) { - if (windowState & Qt::WindowMaximized) { - qApp->getWindow()->showMaximized(); - } else if (windowState & Qt::WindowFullScreen) { - qApp->getWindow()->showFullScreen(); - } else { - qApp->getWindow()->showNormal(); - } - } - qApp->getWindow()->raise(); -} - -void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { - if (maxOctreePPS != _maxOctreePPS) { - _maxOctreePPS = maxOctreePPS; - maxOctreePacketsPerSecond.set(_maxOctreePPS); - } -} - -int Application::getMaxOctreePacketsPerSecond() const { - return _maxOctreePPS; -} - -qreal Application::getDevicePixelRatio() { - return (_window && _window->windowHandle()) ? _window->windowHandle()->devicePixelRatio() : 1.0; -} - -DisplayPluginPointer Application::getActiveDisplayPlugin() const { - if (QThread::currentThread() != thread()) { - std::unique_lock lock(_displayPluginLock); - return _displayPlugin; - } - - if (!_aboutToQuit && !_displayPlugin) { - const_cast(this)->updateDisplayMode(); - Q_ASSERT(_displayPlugin); - } - return _displayPlugin; -} - - -#if !defined(DISABLE_QML) -static const char* EXCLUSION_GROUP_KEY = "exclusionGroup"; - -static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active) { - auto menu = Menu::getInstance(); - QString name = displayPlugin->getName(); - auto grouping = displayPlugin->getGrouping(); - QString groupingMenu { "" }; - Q_ASSERT(!menu->menuItemExists(MenuOption::OutputMenu, name)); - - // assign the meny grouping based on plugin grouping - switch (grouping) { - case Plugin::ADVANCED: - groupingMenu = "Advanced"; - break; - case Plugin::DEVELOPER: - groupingMenu = "Developer"; - break; - default: - groupingMenu = "Standard"; - break; - } - - static QActionGroup* displayPluginGroup = nullptr; - if (!displayPluginGroup) { - displayPluginGroup = new QActionGroup(menu); - displayPluginGroup->setExclusive(true); - } - auto parent = menu->getMenu(MenuOption::OutputMenu); - auto action = menu->addActionToQMenuAndActionHash(parent, - name, QKeySequence(Qt::CTRL + (Qt::Key_0 + index)), qApp, - SLOT(updateDisplayMode()), - QAction::NoRole, Menu::UNSPECIFIED_POSITION, groupingMenu); - - action->setCheckable(true); - action->setChecked(active); - displayPluginGroup->addAction(action); - - action->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(displayPluginGroup)); - Q_ASSERT(menu->menuItemExists(MenuOption::OutputMenu, name)); -} -#endif - -void Application::updateDisplayMode() { - // Unsafe to call this method from anything but the main thread - if (QThread::currentThread() != thread()) { - qFatal("Attempted to switch display plugins from a non-main thread"); - } - - // Once time initialization code that depends on the UI being available - auto displayPlugins = getDisplayPlugins(); - - // Default to the first item on the list, in case none of the menu items match - DisplayPluginPointer newDisplayPlugin = displayPlugins.at(0); - auto menu = getPrimaryMenu(); - if (menu) { - foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { - QString name = displayPlugin->getName(); - QAction* action = menu->getActionForOption(name); - // Menu might have been removed if the display plugin lost - if (!action) { - continue; - } - if (action->isChecked()) { - newDisplayPlugin = displayPlugin; - break; - } - } - } - - if (newDisplayPlugin == _displayPlugin) { - return; - } - - setDisplayPlugin(newDisplayPlugin); -} - -void Application::setDisplayPlugin(DisplayPluginPointer newDisplayPlugin) { - if (newDisplayPlugin == _displayPlugin) { - return; - } - - // FIXME don't have the application directly set the state of the UI, - // instead emit a signal that the display plugin is changing and let - // the desktop lock itself. Reduces coupling between the UI and display - // plugins - auto offscreenUi = getOffscreenUI(); - auto desktop = offscreenUi ? offscreenUi->getDesktop() : nullptr; - auto menu = Menu::getInstance(); - - // Make the switch atomic from the perspective of other threads - { - std::unique_lock lock(_displayPluginLock); - bool wasRepositionLocked = false; - if (desktop) { - // Tell the desktop to no reposition (which requires plugin info), until we have set the new plugin, below. - wasRepositionLocked = desktop->property("repositionLocked").toBool(); - desktop->setProperty("repositionLocked", true); - } - - if (_displayPlugin) { - disconnect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent); - _displayPlugin->deactivate(); - } - - auto oldDisplayPlugin = _displayPlugin; - bool active = newDisplayPlugin->activate(); - - if (!active) { - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - - // If the new plugin fails to activate, fallback to last display - qWarning() << "Failed to activate display: " << newDisplayPlugin->getName(); - newDisplayPlugin = oldDisplayPlugin; - - if (newDisplayPlugin) { - qWarning() << "Falling back to last display: " << newDisplayPlugin->getName(); - active = newDisplayPlugin->activate(); - } - - // If there is no last display, or - // If the last display fails to activate, fallback to desktop - if (!active) { - newDisplayPlugin = displayPlugins.at(0); - qWarning() << "Falling back to display: " << newDisplayPlugin->getName(); - active = newDisplayPlugin->activate(); - } - - if (!active) { - qFatal("Failed to activate fallback plugin"); - } - } - - if (offscreenUi) { - offscreenUi->resize(fromGlm(newDisplayPlugin->getRecommendedUiSize())); - } - getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); - _displayPlugin = newDisplayPlugin; - connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent, Qt::DirectConnection); - if (desktop) { - desktop->setProperty("repositionLocked", wasRepositionLocked); - } - } - - bool isHmd = _displayPlugin->isHmd(); - qCDebug(interfaceapp) << "Entering into" << (isHmd ? "HMD" : "Desktop") << "Mode"; - - // Only log/emit after a successful change - UserActivityLogger::getInstance().logAction("changed_display_mode", { - { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, - { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" }, - { "hmd", isHmd } - }); - emit activeDisplayPluginChanged(); - - // reset the avatar, to set head and hand palms back to a reasonable default pose. - getMyAvatar()->reset(false); - - // switch to first person if entering hmd and setting is checked - if (menu) { - QAction* action = menu->getActionForOption(newDisplayPlugin->getName()); - if (action) { - action->setChecked(true); - } - - if (isHmd && menu->isOptionChecked(MenuOption::FirstPersonHMD)) { - menu->setIsOptionChecked(MenuOption::FirstPerson, true); - cameraMenuChanged(); - } - - // Remove the mirror camera option from menu if in HMD mode - auto mirrorAction = menu->getActionForOption(MenuOption::FullscreenMirror); - mirrorAction->setVisible(!isHmd); - } - - Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); -} - -void Application::switchDisplayMode() { - if (!_autoSwitchDisplayModeSupportedHMDPlugin) { - return; - } - bool currentHMDWornStatus = _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); - if (currentHMDWornStatus != _previousHMDWornStatus) { - // Switch to respective mode as soon as currentHMDWornStatus changes - if (currentHMDWornStatus) { - qCDebug(interfaceapp) << "Switching from Desktop to HMD mode"; - endHMDSession(); - setActiveDisplayPlugin(_autoSwitchDisplayModeSupportedHMDPluginName); - } else { - qCDebug(interfaceapp) << "Switching from HMD to desktop mode"; - setActiveDisplayPlugin(DESKTOP_DISPLAY_PLUGIN_NAME); - startHMDStandBySession(); - } - } - _previousHMDWornStatus = currentHMDWornStatus; -} - -void Application::setShowBulletWireframe(bool value) { - _physicsEngine->setShowBulletWireframe(value); -} - -void Application::setShowBulletAABBs(bool value) { - _physicsEngine->setShowBulletAABBs(value); -} - -void Application::setShowBulletContactPoints(bool value) { - _physicsEngine->setShowBulletContactPoints(value); -} - -void Application::setShowBulletConstraints(bool value) { - _physicsEngine->setShowBulletConstraints(value); -} - -void Application::setShowBulletConstraintLimits(bool value) { - _physicsEngine->setShowBulletConstraintLimits(value); -} - -void Application::createLoginDialog() { - const glm::vec3 LOGIN_DIMENSIONS { 0.89f, 0.5f, 0.01f }; - const auto OFFSET = glm::vec2(0.7f, -0.1f); - auto cameraPosition = _myCamera.getPosition(); - auto cameraOrientation = _myCamera.getOrientation(); - auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; - auto headLookVec = (cameraOrientation * Vectors::FRONT); - // DEFAULT_DPI / tablet scale percentage - const float DPI = 31.0f / (75.0f / 100.0f); - auto offset = headLookVec * OFFSET.x; - auto position = (cameraPosition + offset) + (upVec * OFFSET.y); - - EntityItemProperties properties; - properties.setType(EntityTypes::Web); - properties.setName("LoginDialogEntity"); - properties.setSourceUrl(LOGIN_DIALOG.toString()); - properties.setPosition(position); - properties.setRotation(cameraOrientation); - properties.setDimensions(LOGIN_DIMENSIONS); - properties.setPrimitiveMode(PrimitiveMode::SOLID); - properties.getGrab().setGrabbable(false); - properties.setIgnorePickIntersection(false); - properties.setAlpha(1.0f); - properties.setDPI(DPI); - properties.setVisible(true); - - auto entityScriptingInterface = DependencyManager::get(); - _loginDialogID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); - - auto keyboard = DependencyManager::get().data(); - if (!keyboard->getAnchorID().isNull() && !_loginDialogID.isNull()) { - auto keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); - - EntityItemProperties properties; - properties.setPosition(position + keyboardLocalOffset); - properties.setRotation(cameraOrientation * Quaternions::Y_180); - - entityScriptingInterface->editEntity(keyboard->getAnchorID(), properties); - keyboard->setResetKeyboardPositionOnRaise(false); - } - setKeyboardFocusEntity(_loginDialogID); - emit loginDialogFocusEnabled(); - getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(false); - getApplicationCompositor().getReticleInterface()->setVisible(false); - if (!_loginStateManager.isSetUp()) { - _loginStateManager.setUp(); - } -} - -void Application::updateLoginDialogPosition() { - const float LOOK_AWAY_THRESHOLD_ANGLE = 70.0f; - const auto OFFSET = glm::vec2(0.7f, -0.1f); - - auto entityScriptingInterface = DependencyManager::get(); - EntityPropertyFlags desiredProperties; - desiredProperties += PROP_POSITION; - auto properties = entityScriptingInterface->getEntityProperties(_loginDialogID, desiredProperties); - auto positionVec = properties.getPosition(); - auto cameraPositionVec = _myCamera.getPosition(); - auto cameraOrientation = cancelOutRollAndPitch(_myCamera.getOrientation()); - auto headLookVec = (cameraOrientation * Vectors::FRONT); - auto entityToHeadVec = positionVec - cameraPositionVec; - auto pointAngle = (glm::acos(glm::dot(glm::normalize(entityToHeadVec), glm::normalize(headLookVec))) * 180.0f / PI); - auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; - auto offset = headLookVec * OFFSET.x; - auto newPositionVec = (cameraPositionVec + offset) + (upVec * OFFSET.y); - - bool outOfBounds = glm::distance(positionVec, cameraPositionVec) > 1.0f; - - if (pointAngle > LOOK_AWAY_THRESHOLD_ANGLE || outOfBounds) { - { - EntityItemProperties properties; - properties.setPosition(newPositionVec); - properties.setRotation(cameraOrientation); - entityScriptingInterface->editEntity(_loginDialogID, properties); - } - - { - glm::vec3 keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); - glm::quat keyboardOrientation = cameraOrientation * glm::quat(glm::radians(glm::vec3(-30.0f, 180.0f, 0.0f))); - - EntityItemProperties properties; - properties.setPosition(newPositionVec + keyboardLocalOffset); - properties.setRotation(keyboardOrientation); - entityScriptingInterface->editEntity(DependencyManager::get()->getAnchorID(), properties); - } - } -} - -bool Application::hasRiftControllers() { - return PluginUtils::isOculusTouchControllerAvailable(); -} - -bool Application::hasViveControllers() { - return PluginUtils::isViveControllerAvailable(); -} - -void Application::onDismissedLoginDialog() { - _loginDialogPoppedUp = false; - loginDialogPoppedUp.set(false); - auto keyboard = DependencyManager::get().data(); - keyboard->setResetKeyboardPositionOnRaise(true); - if (!_loginDialogID.isNull()) { - DependencyManager::get()->deleteEntity(_loginDialogID); - _loginDialogID = QUuid(); - _loginStateManager.tearDown(); - } - resumeAfterLoginDialogActionTaken(); -} - -void Application::setShowTrackedObjects(bool value) { - _showTrackedObjects = value; -} - -void Application::startHMDStandBySession() { - _autoSwitchDisplayModeSupportedHMDPlugin->startStandBySession(); -} - -void Application::endHMDSession() { - _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); -} - -mat4 Application::getEyeProjection(int eye) const { - QMutexLocker viewLocker(&_viewMutex); - if (isHMDMode()) { - return getActiveDisplayPlugin()->getEyeProjection((Eye)eye, _viewFrustum.getProjection()); - } - return _viewFrustum.getProjection(); -} - -mat4 Application::getEyeOffset(int eye) const { - // FIXME invert? - return getActiveDisplayPlugin()->getEyeToHeadTransform((Eye)eye); -} - -mat4 Application::getHMDSensorPose() const { - if (isHMDMode()) { - return getActiveDisplayPlugin()->getHeadPose(); - } - return mat4(); -} - -void Application::deadlockApplication() { - qCDebug(interfaceapp) << "Intentionally deadlocked Interface"; - // Using a loop that will *technically* eventually exit (in ~600 billion years) - // to avoid compiler warnings about a loop that will never exit - for (uint64_t i = 1; i != 0; ++i) { - QThread::sleep(1); - } -} - -// cause main thread to be unresponsive for 35 seconds -void Application::unresponsiveApplication() { - // to avoid compiler warnings about a loop that will never exit - uint64_t start = usecTimestampNow(); - uint64_t UNRESPONSIVE_FOR_SECONDS = 35; - uint64_t UNRESPONSIVE_FOR_USECS = UNRESPONSIVE_FOR_SECONDS * USECS_PER_SECOND; - qCDebug(interfaceapp) << "Intentionally cause Interface to be unresponsive for " << UNRESPONSIVE_FOR_SECONDS << " seconds"; - while (usecTimestampNow() - start < UNRESPONSIVE_FOR_USECS) { - QThread::sleep(1); - } -} - -void Application::setActiveDisplayPlugin(const QString& pluginName) { - DisplayPluginPointer newDisplayPlugin; - for (DisplayPluginPointer displayPlugin : PluginManager::getInstance()->getDisplayPlugins()) { - QString name = displayPlugin->getName(); - if (pluginName == name) { - newDisplayPlugin = displayPlugin; - break; - } - } - - if (newDisplayPlugin) { - setDisplayPlugin(newDisplayPlugin); - } -} - -void Application::handleLocalServerConnection() const { - auto server = qobject_cast(sender()); - - qCDebug(interfaceapp) << "Got connection on local server from additional instance - waiting for parameters"; - - auto socket = server->nextPendingConnection(); - - connect(socket, &QLocalSocket::readyRead, this, &Application::readArgumentsFromLocalSocket); - - qApp->getWindow()->raise(); - qApp->getWindow()->activateWindow(); -} - -void Application::readArgumentsFromLocalSocket() const { - auto socket = qobject_cast(sender()); - - auto message = socket->readAll(); - socket->deleteLater(); - - qCDebug(interfaceapp) << "Read from connection: " << message; - - // If we received a message, try to open it as a URL - if (message.length() > 0) { - qApp->openUrl(QString::fromUtf8(message)); - } -} - -void Application::showDesktop() { -} - -CompositorHelper& Application::getApplicationCompositor() const { - return *DependencyManager::get(); -} - - -// virtual functions required for PluginContainer -ui::Menu* Application::getPrimaryMenu() { - auto appMenu = _window->menuBar(); - auto uiMenu = dynamic_cast(appMenu); - return uiMenu; -} - -void Application::showDisplayPluginsTools(bool show) { - DependencyManager::get()->hmdTools(show); -} - -GLWidget* Application::getPrimaryWidget() { - return _glWidget; -} - -MainWindow* Application::getPrimaryWindow() { - return getWindow(); -} - -QOpenGLContext* Application::getPrimaryContext() { - return _glWidget->qglContext(); -} - -bool Application::makeRenderingContextCurrent() { - return true; -} - -bool Application::isForeground() const { - return _isForeground && !_window->isMinimized(); -} - -// FIXME? perhaps two, one for the main thread and one for the offscreen UI rendering thread? -static const int UI_RESERVED_THREADS = 1; -// Windows won't let you have all the cores -static const int OS_RESERVED_THREADS = 1; - -void Application::updateThreadPoolCount() const { - auto reservedThreads = UI_RESERVED_THREADS + OS_RESERVED_THREADS + _displayPlugin->getRequiredThreadCount(); - auto availableThreads = QThread::idealThreadCount() - reservedThreads; - auto threadPoolSize = std::max(MIN_PROCESSING_THREAD_POOL_SIZE, availableThreads); - qCDebug(interfaceapp) << "Ideal Thread Count " << QThread::idealThreadCount(); - qCDebug(interfaceapp) << "Reserved threads " << reservedThreads; - qCDebug(interfaceapp) << "Setting thread pool size to " << threadPoolSize; - QThreadPool::globalInstance()->setMaxThreadCount(threadPoolSize); -} - -void Application::updateSystemTabletMode() { - if (_settingsLoaded && !getLoginDialogPoppedUp()) { - qApp->setProperty(hifi::properties::HMD, isHMDMode()); - if (isHMDMode()) { - DependencyManager::get()->setToolbarMode(getHmdTabletBecomesToolbarSetting()); - } else { - DependencyManager::get()->setToolbarMode(getDesktopTabletBecomesToolbarSetting()); - } - } -} - -QUuid Application::getTabletScreenID() const { - auto HMD = DependencyManager::get(); - return HMD->getCurrentTabletScreenID(); -} - -QUuid Application::getTabletHomeButtonID() const { - auto HMD = DependencyManager::get(); - return HMD->getCurrentHomeButtonID(); -} - -QUuid Application::getTabletFrameID() const { - auto HMD = DependencyManager::get(); - return HMD->getCurrentTabletFrameID(); -} - -QVector Application::getTabletIDs() const { - // Most important first. - QVector result; - auto HMD = DependencyManager::get(); - result << HMD->getCurrentTabletScreenID(); - result << HMD->getCurrentHomeButtonID(); - result << HMD->getCurrentTabletFrameID(); - return result; -} - -void Application::setAvatarOverrideUrl(const QUrl& url, bool save) { - _avatarOverrideUrl = url; - _saveAvatarOverrideUrl = save; -} - -void Application::saveNextPhysicsStats(QString filename) { - _physicsEngine->saveNextPhysicsStats(filename); -} - -void Application::copyToClipboard(const QString& text) { - if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "copyToClipboard"); - return; - } - - // assume that the address is being copied because the user wants a shareable address - QApplication::clipboard()->setText(text); -} - -QString Application::getGraphicsCardType() { - return GPUIdent::getInstance()->getName(); -} - -#if defined(Q_OS_ANDROID) -void Application::beforeEnterBackground() { - auto nodeList = DependencyManager::get(); - nodeList->setSendDomainServerCheckInEnabled(false); - nodeList->reset(true); - clearDomainOctreeDetails(); -} - - - -void Application::enterBackground() { - QMetaObject::invokeMethod(DependencyManager::get().data(), - "stop", Qt::BlockingQueuedConnection); -// Quest only supports one plugin which can't be deactivated currently -#if !defined(ANDROID_APP_QUEST_INTERFACE) - if (getActiveDisplayPlugin()->isActive()) { - getActiveDisplayPlugin()->deactivate(); - } -#endif -} - -void Application::enterForeground() { - QMetaObject::invokeMethod(DependencyManager::get().data(), - "start", Qt::BlockingQueuedConnection); -// Quest only supports one plugin which can't be deactivated currently -#if !defined(ANDROID_APP_QUEST_INTERFACE) - if (!getActiveDisplayPlugin() || getActiveDisplayPlugin()->isActive() || !getActiveDisplayPlugin()->activate()) { - qWarning() << "Could not re-activate display plugin"; - } -#endif - auto nodeList = DependencyManager::get(); - nodeList->setSendDomainServerCheckInEnabled(true); -} - - -void Application::toggleAwayMode(){ - QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); - QCoreApplication::sendEvent (this, &event); -} - - -#endif - - -#include "Application.moc" From 7567e0d355b8ac58ae6b40f1c9c7953465f128d0 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 12 Mar 2019 15:48:42 -0700 Subject: [PATCH 024/135] debugging the root of the fbx, it is not 0 in some cases --- libraries/fbx/src/FBXSerializer.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index e6e3d73815..f91eeb6519 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -508,6 +508,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } else if (child.name == "Objects") { + //qCDebug(modelformat) << " the root model id is " << getID(child.properties); foreach (const FBXNode& object, child.children) { nodeParentId++; if (object.name == "Geometry") { @@ -1169,8 +1170,19 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr counter++; } } - _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); - _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + + if ("2830302416448" == getID(connection.properties, 1) || "2830302416448" == getID(connection.properties, 2)) { + if ("2829544143536" == getID(connection.properties, 1)) { + _connectionParentMap.insert(getID(connection.properties, 1), "0"); + _connectionChildMap.insert("0", getID(connection.properties, 1)); + } + qCDebug(modelformat) << " parent map inserted with id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)) << " id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)); + } else { + _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); + _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + } + + // qCDebug(modelformat) << " child map inserted with id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)) << " id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)); } } } @@ -1220,13 +1232,16 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); + for (QHash::const_iterator modelIDPair = modelIDsToNames.constBegin(); modelIDPair != modelIDsToNames.constEnd(); modelIDPair++) { + qCDebug(modelformat) << " model ID " << modelIDPair.key() << " name " << modelIDPair.value(); + } + // get the list of models in depth-first traversal order QVector modelIDs; QSet remainingFBXModels; for (QHash::const_iterator fbxModel = fbxModels.constBegin(); fbxModel != fbxModels.constEnd(); fbxModel++) { // models with clusters must be parented to the cluster top // Unless the model is a root node. - qCDebug(modelformat) << "fbx model name " << fbxModel.key(); bool isARootNode = !modelIDs.contains(_connectionParentMap.value(fbxModel.key())); if (!isARootNode) { foreach(const QString& deformerID, _connectionChildMap.values(fbxModel.key())) { @@ -1235,6 +1250,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr continue; } QString topID = getTopModelID(_connectionParentMap, fbxModels, _connectionChildMap.value(clusterID), url); + qCDebug(modelformat) << "fbx model name " << fbxModel.value().name << " top id " << topID << " modelID " << fbxModel.key(); _connectionChildMap.remove(_connectionParentMap.take(fbxModel.key()), fbxModel.key()); _connectionParentMap.insert(fbxModel.key(), topID); goto outerBreak; @@ -1258,6 +1274,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } QString topID = getTopModelID(_connectionParentMap, fbxModels, first, url); + qCDebug(modelformat) << "topper name fbx name " << modelIDsToNames.value(first) << " top id " << topID << " top name " << modelIDsToNames.value(topID); + qCDebug(modelformat) << "parent id " << _connectionParentMap.value(topID) << " parent name " << modelIDsToNames.value(_connectionParentMap.value(topID)) << " remaining models parent value " << remainingFBXModels.contains(_connectionParentMap.value(topID)); appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, fbxModels, remainingFBXModels, modelIDs, true); } From fff0d1a80e6801ad44e90974474289fb71b311cf Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 12 Mar 2019 17:43:23 -0700 Subject: [PATCH 025/135] Rig.cpp: Fix for index out of range assert in debug builds --- libraries/animation/src/Rig.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 07bdfde189..fb55bd2c98 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1210,7 +1210,8 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons _networkAnimState.blendTime += deltaTime; alpha = _computeNetworkAnimation ? (_networkAnimState.blendTime / TOTAL_BLEND_TIME) : (1.0f - (_networkAnimState.blendTime / TOTAL_BLEND_TIME)); alpha = glm::clamp(alpha, 0.0f, 1.0f); - for (size_t i = 0; i < _networkPoseSet._relativePoses.size(); i++) { + size_t numJoints = std::min(_networkPoseSet._relativePoses.size(), _internalPoseSet._relativePoses.size()); + for (size_t i = 0; i < numJoints; i++) { _networkPoseSet._relativePoses[i].blend(_internalPoseSet._relativePoses[i], alpha); } } From ddd411e1137935ca03ea4da0f97cb21a7d86a844 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 17:46:52 -0700 Subject: [PATCH 026/135] Removed test code. --- interface/src/Application.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7dddaecadb..de4a6bb167 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1804,6 +1804,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) // Do not show login dialog if requested not to on the command line QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); int index = arguments().indexOf(hifiNoLoginCommandLineKey); @@ -1813,6 +1814,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } showLoginScreen(); +#else + resumeAfterLoginDialogActionTaken(); +#endif }); // Make sure we don't time out during slow operations at startup From ea84847950e61db2534e86fd7487004b45e78559 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 13 Mar 2019 16:20:38 +1300 Subject: [PATCH 027/135] Update AnimStateDictionary JSDoc per feedback --- libraries/animation/src/Rig.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 74fd5d600b..82ab067472 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -91,6 +91,7 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig /**jsdoc *

An AnimStateDictionary object may have the following properties. It may also have other properties, set by * scripts.

+ *

Warning: These properties are subject to change. * * * @@ -171,11 +172,6 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig * * - * - * - * * * * - * - * * * * From 93d7a4ae3b4ade0ac97017ee4737622cad825f5c Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 13 Mar 2019 11:14:15 -0700 Subject: [PATCH 028/135] will no longer allow a non-zero parent of the root of an fbx model --- libraries/animation/src/AnimClip.cpp | 2 +- libraries/fbx/src/FBXSerializer.cpp | 39 +++------------------------- 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 5d846a8f84..4fe02e9307 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -154,7 +154,7 @@ void AnimClip::copyFromNetworkAnim() { const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); - qCDebug(animation) << " avatar unit scale " << avatarUnitScale << " animation unit scale " << animationUnitScale << " avatar height " << avatarHeightInMeters << " animation height " << animHeightInMeters << " avatar scale " << avatarHipsParentScale << " animation scale " << animHipsParentScale; + boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; } diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index c542eef6a6..4bb499bd84 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -142,7 +142,6 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen visitedNodes.append(nodeID); // Append each node we visit const FBXModel& fbxModel = fbxModels.value(nodeID); - qCDebug(modelformat) << "this fbx model name is " << fbxModel.name; globalTransform = glm::translate(fbxModel.translation) * fbxModel.preTransform * glm::mat4_cast(fbxModel.preRotation * fbxModel.rotation * fbxModel.postRotation) * fbxModel.postTransform * globalTransform; if (fbxModel.hasGeometricOffset) { @@ -202,23 +201,13 @@ public: void appendModelIDs(const QString& parentID, const QMultiMap& connectionChildMap, QHash& fbxModels, QSet& remainingModels, QVector& modelIDs, bool isRootNode = false) { if (remainingModels.contains(parentID)) { - qCDebug(modelformat) << " remaining models contains parent " << parentID; modelIDs.append(parentID); remainingModels.remove(parentID); } - int parentIndex = 1000; - if (isRootNode) { - qCDebug(modelformat) << " found a root node " << parentID; - parentIndex = -1; - } else { - parentIndex = modelIDs.size() - 1; - } - //int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; + int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; foreach (const QString& childID, connectionChildMap.values(parentID)) { - qCDebug(modelformat) << " searching children, parent id " << parentID; if (remainingModels.contains(childID)) { FBXModel& fbxModel = fbxModels[childID]; - qCDebug(modelformat) << " child id " << fbxModel.name; if (fbxModel.parentIndex == -1) { fbxModel.parentIndex = parentIndex; appendModelIDs(childID, connectionChildMap, fbxModels, remainingModels, modelIDs); @@ -452,7 +441,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; - int nodeParentId = -1; foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { @@ -508,9 +496,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } else if (child.name == "Objects") { - //qCDebug(modelformat) << " the root model id is " << getID(child.properties); foreach (const FBXNode& object, child.children) { - nodeParentId++; if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { meshes.insert(getID(object.properties), extractMesh(object, meshIndex)); @@ -519,7 +505,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr blendshapes.append(extracted); } } else if (object.name == "Model") { - qCDebug(modelformat) << "model name from object properties " << getName(object.properties) << " node parentID " << nodeParentId; QString name = getName(object.properties); QString id = getID(object.properties); modelIDsToNames.insert(id, name); @@ -1174,19 +1159,13 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr counter++; } } - - if ("2830302416448" == getID(connection.properties, 1) || "2830302416448" == getID(connection.properties, 2)) { - if ("2829544143536" == getID(connection.properties, 1)) { - _connectionParentMap.insert(getID(connection.properties, 1), "0"); - _connectionChildMap.insert("0", getID(connection.properties, 1)); - } - qCDebug(modelformat) << " parent map inserted with id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)) << " id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)); + if (_connectionParentMap.value(getID(connection.properties, 1)) == "0") { + // don't assign the new parent + qCDebug(modelformat) << "root node " << getID(connection.properties, 1) << " has discarded parent " << getID(connection.properties, 2); } else { _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } - - // qCDebug(modelformat) << " child map inserted with id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)) << " id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)); } } } @@ -1211,7 +1190,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr #endif } - // TODO: check if is code is needed if (!lights.empty()) { if (hifiGlobalNodeID.isEmpty()) { @@ -1236,10 +1214,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); - for (QHash::const_iterator modelIDPair = modelIDsToNames.constBegin(); modelIDPair != modelIDsToNames.constEnd(); modelIDPair++) { - qCDebug(modelformat) << " model ID " << modelIDPair.key() << " name " << modelIDPair.value(); - } - // get the list of models in depth-first traversal order QVector modelIDs; QSet remainingFBXModels; @@ -1254,7 +1228,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr continue; } QString topID = getTopModelID(_connectionParentMap, fbxModels, _connectionChildMap.value(clusterID), url); - qCDebug(modelformat) << "fbx model name " << fbxModel.value().name << " top id " << topID << " modelID " << fbxModel.key(); _connectionChildMap.remove(_connectionParentMap.take(fbxModel.key()), fbxModel.key()); _connectionParentMap.insert(fbxModel.key(), topID); goto outerBreak; @@ -1278,8 +1251,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } QString topID = getTopModelID(_connectionParentMap, fbxModels, first, url); - qCDebug(modelformat) << "topper name fbx name " << modelIDsToNames.value(first) << " top id " << topID << " top name " << modelIDsToNames.value(topID); - qCDebug(modelformat) << "parent id " << _connectionParentMap.value(topID) << " parent name " << modelIDsToNames.value(_connectionParentMap.value(topID)) << " remaining models parent value " << remainingFBXModels.contains(_connectionParentMap.value(topID)); appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, fbxModels, remainingFBXModels, modelIDs, true); } @@ -1304,8 +1275,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.parentIndex = fbxModel.parentIndex; int jointIndex = hfmModel.joints.size(); - qCDebug(modelformat) << "fbx joint name " << fbxModel.name << " joint index " << jointIndex << " parent index " << joint.parentIndex; - joint.translation = fbxModel.translation; // these are usually in centimeters joint.preTransform = fbxModel.preTransform; joint.preRotation = fbxModel.preRotation; From 277ef56f4941e9d7d9742be7a23406b3b05fd516 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Mar 2019 10:24:19 +1300 Subject: [PATCH 029/135] Fill in JSDoc for new flow functions --- interface/src/avatar/MyAvatar.cpp | 33 +++++++++++++++++++++++++++++++ interface/src/avatar/MyAvatar.h | 15 +++++++------- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e0353da1b4..e0e9b5b648 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5443,6 +5443,39 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys } } +/**jsdoc + * Flow options currently used in flow simulation. + * @typedef {object} MyAvatar.FlowData + * @property {boolean} initialized - true if flow has been initialized for the current avatar, false + * if it hasn't. + * @property {boolean} active - true if flow is enabled, false if it isn't. + * @property {boolean} colliding - true if collisions are enabled, false if they aren't. + * @property {Object} physicsData - The physics configuration for each group of joints + * that has been configured. + * @property {Object} collisions - The collisions configuration for each joint that + * has collisions configured. + * @property {Object} threads - The threads hat have been configured, with the name of the first joint as + * the ThreadName and an array of the indexes of all the joints in the thread as the value. + */ +/**jsdoc + * A set of physics options currently used in flow simulation. + * @typedef {object} MyAvatar.FlowPhysicsData + * @property {boolean} active - true to enable flow on the joint, false if it isn't., + * @property {number} radius - The thickness of segments and knots. (Needed for collisions.) + * @property {number} gravity - Y-value of the gravity vector. + * @property {number} inertia - Rotational inertia multiplier. + * @property {number} damping - The amount of damping on joint oscillation. + * @property {number} stiffness - How stiff each thread is. + * @property {number} delta - Delta time for every integration step. + * @property {number[]} jointIndices - The indexes of the joints the options are applied to. + */ +/**jsdoc + * A set of collision options currently used in flow simulation. + * @typedef {object} MyAvatar.FlowCollisionsData + * @property {number} radius - Collision sphere radius. + * @property {number} offset - Offset of the collision sphere from the joint. + * @property {number} jointIndex - The index of the joint the options are applied to. + */ QVariantMap MyAvatar::getFlowData() { QVariantMap result; if (QThread::currentThread() != thread()) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index c7585311b8..bd112bfacc 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1563,16 +1563,17 @@ public: Q_INVOKABLE void useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig = QVariantMap(), const QVariantMap& collisionsConfig = QVariantMap()); /**jsdoc - * @function MyAvatar.getFlowData - * @returns {object} - */ + * Gets the current flow configuration. + * @function MyAvatar.getFlowData + * @returns {MyAvatar.FlowData} + */ Q_INVOKABLE QVariantMap getFlowData(); /**jsdoc - * returns the indices of every colliding flow joint - * @function MyAvatar.getCollidingFlowJoints - * @returns {int[]} - */ + * Gets the indexes of currently colliding flow joints. + * @function MyAvatar.getCollidingFlowJoints + * @returns {number[]} The indexes of currently colliding flow joints. + */ Q_INVOKABLE QVariantList getCollidingFlowJoints(); public slots: From 74edea80346209b6d7c7665360e78d2a212bef25 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 14 Mar 2019 10:25:31 +1300 Subject: [PATCH 030/135] Miscellaneous JSDoc fixes --- interface/src/avatar/MyAvatar.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index bd112bfacc..8951bc7fed 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -652,7 +652,7 @@ public: * Restores a default role animation. *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or - * "walkFwd". To get the full list of roles, use {#link MyAvatar.getAnimationRoles}. For each role, + * "walkFwd". To get the full list of roles, use {@link MyAvatar.getAnimationRoles}. For each role, * the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are * blended together with procedural data (such as look-at vectors, hand sensors etc.). You can change the animation clip * (.FBX) associated with a specified animation role using {@link MyAvatar.overrideRoleAnimation}. @@ -1555,7 +1555,7 @@ public: * @param {boolean} isActive - true if flow simulation is enabled on the joint, false if it isn't. * @param {boolean} isCollidable - true to enable collisions in the flow simulation, false to * disable. - * @param {Object} [physicsConfig>] - Physic configurations for particular entity + * @param {Object} [physicsConfig>] - Physics configurations for particular entity * and avatar joints. * @param {Object} [collisionsConfig] - Collision configurations for particular * entity and avatar joints. From 9d739277c8a358376532ded6156d587ca638871b Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 13 Mar 2019 14:26:27 -0700 Subject: [PATCH 031/135] changed the fix so that we allow the root to be child --- libraries/fbx/src/FBXSerializer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 4bb499bd84..f4eb1d57d9 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1162,6 +1162,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr if (_connectionParentMap.value(getID(connection.properties, 1)) == "0") { // don't assign the new parent qCDebug(modelformat) << "root node " << getID(connection.properties, 1) << " has discarded parent " << getID(connection.properties, 2); + _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else { _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); From c14b135f2b960acad5d3a089a1edddd82b826777 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Wed, 13 Mar 2019 15:42:04 -0700 Subject: [PATCH 032/135] Fix flow touch and scale issues --- interface/src/avatar/MyAvatar.cpp | 3 +- libraries/animation/src/Flow.cpp | 66 ++++++++++++++++++++----------- libraries/animation/src/Flow.h | 8 +++- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9211be3b4f..ecf904c7f4 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5381,7 +5381,7 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys } auto collisionJoints = collisionsConfig.keys(); if (collisionJoints.size() > 0) { - collisionSystem.resetCollisions(); + collisionSystem.clearSelfCollisions(); for (auto &jointName : collisionJoints) { int jointIndex = getJointIndex(jointName); FlowCollisionSettings collisionsSettings; @@ -5396,6 +5396,7 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys collisionSystem.addCollisionSphere(jointIndex, collisionsSettings); } } + flow.updateScale(); } } diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp index 5bc2021e5e..1f9e72bf28 100644 --- a/libraries/animation/src/Flow.cpp +++ b/libraries/animation/src/Flow.cpp @@ -67,17 +67,23 @@ void FlowCollisionSystem::addCollisionSphere(int jointIndex, const FlowCollision auto collision = FlowCollisionSphere(jointIndex, settings, isTouch); collision.setPosition(position); if (isSelfCollision) { - _selfCollisions.push_back(collision); + if (!isTouch) { + _selfCollisions.push_back(collision); + } else { + _selfTouchCollisions.push_back(collision); + } } else { _othersCollisions.push_back(collision); } - }; + void FlowCollisionSystem::resetCollisions() { _allCollisions.clear(); _othersCollisions.clear(); + _selfTouchCollisions.clear(); _selfCollisions.clear(); } + FlowCollisionResult FlowCollisionSystem::computeCollision(const std::vector collisions) { FlowCollisionResult result; if (collisions.size() > 1) { @@ -106,6 +112,10 @@ void FlowCollisionSystem::setScale(float scale) { _selfCollisions[j]._radius = _selfCollisions[j]._initialRadius * scale; _selfCollisions[j]._offset = _selfCollisions[j]._initialOffset * scale; } + for (size_t j = 0; j < _selfTouchCollisions.size(); j++) { + _selfTouchCollisions[j]._radius = _selfTouchCollisions[j]._initialRadius * scale; + _selfTouchCollisions[j]._offset = _selfTouchCollisions[j]._initialOffset * scale; + } }; std::vector FlowCollisionSystem::checkFlowThreadCollisions(FlowThread* flowThread) { @@ -178,9 +188,9 @@ void FlowCollisionSystem::setCollisionSettingsByJoint(int jointIndex, const Flow } void FlowCollisionSystem::prepareCollisions() { _allCollisions.clear(); - _allCollisions.resize(_selfCollisions.size() + _othersCollisions.size()); - std::copy(_selfCollisions.begin(), _selfCollisions.begin() + _selfCollisions.size(), _allCollisions.begin()); - std::copy(_othersCollisions.begin(), _othersCollisions.begin() + _othersCollisions.size(), _allCollisions.begin() + _selfCollisions.size()); + _allCollisions.insert(_allCollisions.end(), _selfCollisions.begin(), _selfCollisions.end()); + _allCollisions.insert(_allCollisions.end(), _othersCollisions.begin(), _othersCollisions.end()); + _allCollisions.insert(_allCollisions.end(), _selfTouchCollisions.begin(), _selfTouchCollisions.end()); _othersCollisions.clear(); } @@ -273,18 +283,20 @@ void FlowJoint::setRecoveryPosition(const glm::vec3& recoveryPosition) { } void FlowJoint::update(float deltaTime) { - glm::vec3 accelerationOffset = glm::vec3(0.0f); - if (_settings._stiffness > 0.0f) { - glm::vec3 recoveryVector = _recoveryPosition - _currentPosition; - float recoveryFactor = powf(_settings._stiffness, 3.0f); - accelerationOffset = recoveryVector * recoveryFactor; - } - FlowNode::update(deltaTime, accelerationOffset); - if (_anchored) { - if (!_isHelper) { - _currentPosition = _updatedPosition; - } else { - _currentPosition = _parentPosition; + if (_settings._active) { + glm::vec3 accelerationOffset = glm::vec3(0.0f); + if (_settings._stiffness > 0.0f) { + glm::vec3 recoveryVector = _recoveryPosition - _currentPosition; + float recoveryFactor = powf(_settings._stiffness, 3.0f); + accelerationOffset = recoveryVector * recoveryFactor; + } + FlowNode::update(deltaTime, accelerationOffset); + if (_anchored) { + if (!_isHelper) { + _currentPosition = _updatedPosition; + } else { + _currentPosition = _parentPosition; + } } } }; @@ -674,6 +686,14 @@ bool Flow::updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t thr return true; } +void Flow::updateCollisionJoint(FlowCollisionSphere& collision, AnimPoseVec& absolutePoses) { + glm::quat jointRotation; + getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation); + getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation); + glm::vec3 worldOffset = jointRotation * collision._offset; + collision._position = collision._position + worldOffset; +} + void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) { updateAbsolutePoses(relativePoses, absolutePoses); for (auto &jointData : _flowJointData) { @@ -695,11 +715,11 @@ void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) } auto &selfCollisions = _collisionSystem.getSelfCollisions(); for (auto &collision : selfCollisions) { - glm::quat jointRotation; - getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation); - getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation); - glm::vec3 worldOffset = jointRotation * collision._offset; - collision._position = collision._position + worldOffset; + updateCollisionJoint(collision, absolutePoses); + } + auto &selfTouchCollisions = _collisionSystem.getSelfTouchCollisions(); + for (auto &collision : selfTouchCollisions) { + updateCollisionJoint(collision, absolutePoses); } _collisionSystem.prepareCollisions(); } @@ -710,7 +730,7 @@ void Flow::setJoints(AnimPoseVec& relativePoses, const std::vector& overri for (int jointIndex : joints) { auto &joint = _flowJointData[jointIndex]; if (jointIndex >= 0 && jointIndex < (int)relativePoses.size() && !overrideFlags[jointIndex]) { - relativePoses[jointIndex].rot() = joint.getCurrentRotation(); + relativePoses[jointIndex].rot() = joint.getSettings()._active ? joint.getCurrentRotation() : joint.getInitialRotation(); } } } diff --git a/libraries/animation/src/Flow.h b/libraries/animation/src/Flow.h index ad81c2be77..5dc1a3ba3e 100644 --- a/libraries/animation/src/Flow.h +++ b/libraries/animation/src/Flow.h @@ -140,6 +140,7 @@ public: std::vector checkFlowThreadCollisions(FlowThread* flowThread); std::vector& getSelfCollisions() { return _selfCollisions; }; + std::vector& getSelfTouchCollisions() { return _selfTouchCollisions; }; void setOthersCollisions(const std::vector& othersCollisions) { _othersCollisions = othersCollisions; } void prepareCollisions(); void resetCollisions(); @@ -150,9 +151,11 @@ public: void setActive(bool active) { _active = active; } bool getActive() const { return _active; } const std::vector& getCollisions() const { return _selfCollisions; } + void clearSelfCollisions() { _selfCollisions.clear(); } protected: std::vector _selfCollisions; std::vector _othersCollisions; + std::vector _selfTouchCollisions; std::vector _allCollisions; float _scale { 1.0f }; bool _active { false }; @@ -210,7 +213,7 @@ public: bool isHelper() const { return _isHelper; } const FlowPhysicsSettings& getSettings() { return _settings; } - void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; } + void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; _initialRadius = _settings._radius; } const glm::vec3& getCurrentPosition() const { return _currentPosition; } int getIndex() const { return _index; } @@ -222,6 +225,7 @@ public: const glm::quat& getCurrentRotation() const { return _currentRotation; } const glm::vec3& getCurrentTranslation() const { return _initialTranslation; } const glm::vec3& getInitialPosition() const { return _initialPosition; } + const glm::quat& getInitialRotation() const { return _initialRotation; } bool isColliding() const { return _colliding; } protected: @@ -297,6 +301,7 @@ public: void setPhysicsSettingsForGroup(const QString& group, const FlowPhysicsSettings& settings); const std::map& getGroupSettings() const { return _groupSettings; } void cleanUp(); + void updateScale() { setScale(_scale); } signals: void onCleanup(); @@ -311,6 +316,7 @@ private: void setJoints(AnimPoseVec& relativePoses, const std::vector& overrideFlags); void updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses); + void updateCollisionJoint(FlowCollisionSphere& collision, AnimPoseVec& absolutePoses); bool updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t threadIndex); void updateGroupSettings(const QString& group, const FlowPhysicsSettings& settings); void setScale(float scale); From 53b5a599b1c7600908ed5d41ddb27886c2eb814a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Mar 2019 09:12:16 +1300 Subject: [PATCH 033/135] Reinstate avatar script type tag --- tools/jsdoc/plugins/hifi.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index b4350ddbdb..184da9b618 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -107,6 +107,9 @@ exports.handlers = { if (e.doclet.hifiClientEntity) { rows.push("Client Entity Scripts"); } + if (e.doclet.hifiAvatar) { + rows.push("Avatar Scripts"); + } if (e.doclet.hifiServerEntity) { rows.push("Server Entity Scripts"); } @@ -155,6 +158,13 @@ exports.defineTags = function (dictionary) { } }); + // @hifi-avatar-script + dictionary.defineTag("hifi-avatar", { + onTagged: function (doclet, tag) { + doclet.hifiAvatar = true; + } + }); + // @hifi-client-entity dictionary.defineTag("hifi-client-entity", { onTagged: function (doclet, tag) { From f0bbd8954657c26cc45ae1c77f504116035f198d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Mar 2019 11:05:58 +1300 Subject: [PATCH 034/135] Update JSDoc README with solution for out-of-memory problem --- tools/jsdoc/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/jsdoc/README.md b/tools/jsdoc/README.md index f9864a21e4..f3dda84291 100644 --- a/tools/jsdoc/README.md +++ b/tools/jsdoc/README.md @@ -15,6 +15,13 @@ To generate html documentation for the High Fidelity JavaScript API: The out folder should contain index.html. +If you get a "JavaScript heap out of memory" error when running the `jsdoc` command you need to increase the amount of memory +available to it. For example, to increase the memory available to 2GB on Windows: +* `where jsdoc` to find the `jsdoc.cmd` file. +* Edit the `jsdoc.cmd` file to add `--max-old-space-size=2048` after the `node` and/or `node.exe` commands. + +Reference: https://medium.com/@vuongtran/how-to-solve-process-out-of-memory-in-node-js-5f0de8f8464c + To generate the grav automation files, run node gravPrep.js after you have made a JSdoc output folder. This will create files that are needed for hifi-grav and hifi-grav-content repos From 748a80fed718bfe24a01ba1998ef3b9b772d9c26 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 14 Mar 2019 20:02:19 -0700 Subject: [PATCH 035/135] Store device ifconfig results in ifconfig.txt. --- tools/nitpick/src/TestRunnerMobile.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index ed48c37290..73f1da5d26 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -67,14 +67,26 @@ void TestRunnerMobile::connectDevice() { if (!_adbInterface) { _adbInterface = new AdbInterface(); } - + + // Get list of devices QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename; appendLog(command); system(command.toStdString().c_str()); if (!QFile::exists(devicesFullFilename)) { - QMessageBox::critical(0, "Internal error", "devicesFullFilename not found"); + QMessageBox::critical(0, "Internal error", "devices.txt not found"); + exit (-1); + } + + // Get device IP address + QString ifconfigFullFilename{ _workingFolder + "/ifconfig.txt" }; + command = _adbInterface->getAdbCommand() + " shell ifconfig > " + ifconfigFullFilename; + appendLog(command); + system(command.toStdString().c_str()); + + if (!QFile::exists(ifconfigFullFilename)) { + QMessageBox::critical(0, "Internal error", "ifconfig.txt not found"); exit (-1); } From 2794a134c17d48f85044d2ebd8d7dee8c9127544 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 10:44:59 -0700 Subject: [PATCH 036/135] Add master injector gain to audio-mixer --- .../src/audio/AudioMixerClientData.h | 3 + .../src/audio/AudioMixerSlave.cpp | 56 ++++++++++++------- assignment-client/src/audio/AudioMixerSlave.h | 7 ++- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 653749f619..f9d113c53d 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -84,6 +84,8 @@ public: float getMasterAvatarGain() const { return _masterAvatarGain; } void setMasterAvatarGain(float gain) { _masterAvatarGain = gain; } + float getMasterInjectorGain() const { return _masterInjectorGain; } + void setMasterInjectorGain(float gain) { _masterInjectorGain = gain; } AudioLimiter audioLimiter; @@ -189,6 +191,7 @@ private: int _frameToSendStats { 0 }; float _masterAvatarGain { 1.0f }; // per-listener mixing gain, applied only to avatars + float _masterInjectorGain { 1.0f }; // per-listener mixing gain, applied only to injectors CodecPluginPointer _codec; QString _selectedCodecName; diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index a920b45161..f7f8e8a9c1 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -50,7 +50,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& // mix helpers inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd); -inline float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream, +inline float computeGain(float masterAvatarGain, float masterInjectorGain, const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho); inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); @@ -338,8 +338,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } if (!isThrottling) { - updateHRTFParameters(stream, *listenerAudioStream, - listenerData->getMasterAvatarGain()); + updateHRTFParameters(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + listenerData->getMasterInjectorGain()); } return false; }); @@ -363,8 +363,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } if (!isThrottling) { - updateHRTFParameters(stream, *listenerAudioStream, - listenerData->getMasterAvatarGain()); + updateHRTFParameters(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + listenerData->getMasterInjectorGain()); } return false; }); @@ -381,13 +381,13 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { stream.approximateVolume = approximateVolume(stream, listenerAudioStream); } else { if (shouldBeSkipped(stream, *listener, *listenerAudioStream, *listenerData)) { - addStream(stream, *listenerAudioStream, 0.0f, isSoloing); + addStream(stream, *listenerAudioStream, 0.0f, 0.0f, isSoloing); streams.skipped.push_back(move(stream)); ++stats.activeToSkipped; return true; } - addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), listenerData->getMasterInjectorGain(), isSoloing); if (shouldBeInactive(stream)) { @@ -423,7 +423,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { return true; } - addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), listenerData->getMasterInjectorGain(), isSoloing); if (shouldBeInactive(stream)) { @@ -491,7 +491,9 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain, bool isSoloing) { + float masterAvatarGain, + float masterInjectorGain, + bool isSoloing) { ++stats.totalMixes; auto streamToAdd = mixableStream.positionalStream; @@ -504,9 +506,10 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre float distance = glm::max(glm::length(relativePosition), EPSILON); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); - float gain = masterListenerGain; + float gain = masterAvatarGain; if (!isSoloing) { - gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho); + gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, + distance, isEcho); } const int HRTF_DATASET_INDEX = 1; @@ -585,8 +588,9 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre } void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream, - AvatarAudioStream& listeningNodeStream, - float masterListenerGain) { + AvatarAudioStream& listeningNodeStream, + float masterAvatarGain, + float masterInjectorGain) { auto streamToAdd = mixableStream.positionalStream; // check if this is a server echo of a source back to itself @@ -595,7 +599,8 @@ void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho); + float gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, + distance, isEcho); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); mixableStream.hrtf->setParameterHistory(azimuth, distance, gain); @@ -720,6 +725,7 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi // injector: apply attenuation if (streamToAdd.getType() == PositionalAudioStream::Injector) { gain *= reinterpret_cast(&streamToAdd)->getAttenuationRatio(); + // injector: skip master gain } // avatar: skip attenuation - it is too costly to approximate @@ -729,16 +735,23 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi float distance = glm::length(relativePosition); return gain / distance; - // avatar: skip master gain - it is constant for all streams + // avatar: skip master gain } -float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho) { +float computeGain(float masterAvatarGain, + float masterInjectorGain, + const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, + const glm::vec3& relativePosition, + float distance, + bool isEcho) { float gain = 1.0f; // injector: apply attenuation if (streamToAdd.getType() == PositionalAudioStream::Injector) { gain *= reinterpret_cast(&streamToAdd)->getAttenuationRatio(); + // apply master gain + gain *= masterInjectorGain; // avatar: apply fixed off-axis attenuation to make them quieter as they turn away } else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) { @@ -754,8 +767,8 @@ float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNo gain *= offAxisCoefficient; - // apply master gain, only to avatars - gain *= masterListenerGain; + // apply master gain + gain *= masterAvatarGain; } auto& audioZones = AudioMixer::getAudioZones(); @@ -797,8 +810,9 @@ float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNo return gain; } -float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, - const glm::vec3& relativePosition) { +float computeAzimuth(const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, + const glm::vec3& relativePosition) { glm::quat inverseOrientation = glm::inverse(listeningNodeStream.getOrientation()); glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; diff --git a/assignment-client/src/audio/AudioMixerSlave.h b/assignment-client/src/audio/AudioMixerSlave.h index 3d979da1fc..9765ea8639 100644 --- a/assignment-client/src/audio/AudioMixerSlave.h +++ b/assignment-client/src/audio/AudioMixerSlave.h @@ -57,10 +57,13 @@ private: bool prepareMix(const SharedNodePointer& listener); void addStream(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain, bool isSoloing); + float masterAvatarGain, + float masterInjectorGain, + bool isSoloing); void updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain); + float masterAvatarGain, + float masterInjectorGain); void resetHRTFState(AudioMixerClientData::MixableStream& mixableStream); void addStreams(Node& listener, AudioMixerClientData& listenerData); From a5a305f1816cb56c9a0434d41c446d74ccca38d2 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 12:05:51 -0700 Subject: [PATCH 037/135] Handle InjectorGainSet packet at the audio-mixer --- assignment-client/src/audio/AudioMixer.cpp | 1 + .../src/audio/AudioMixerClientData.cpp | 14 ++++++++++++++ assignment-client/src/audio/AudioMixerClientData.h | 1 + libraries/networking/src/udt/PacketHeaders.h | 2 +- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index f67c54239e..201e24d4b9 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -97,6 +97,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : PacketType::RadiusIgnoreRequest, PacketType::RequestsDomainListData, PacketType::PerAvatarGainSet, + PacketType::InjectorGainSet, PacketType::AudioSoloRequest }, this, "queueAudioPacket"); diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 90698bfac8..b8d3ec62a6 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -92,6 +92,9 @@ int AudioMixerClientData::processPackets(ConcurrentAddedStreams& addedStreams) { case PacketType::PerAvatarGainSet: parsePerAvatarGainSet(*packet, node); break; + case PacketType::InjectorGainSet: + parseInjectorGainSet(*packet, node); + break; case PacketType::NodeIgnoreRequest: parseNodeIgnoreRequest(packet, node); break; @@ -205,6 +208,17 @@ void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const } } +void AudioMixerClientData::parseInjectorGainSet(ReceivedMessage& message, const SharedNodePointer& node) { + QUuid uuid = node->getUUID(); + + uint8_t packedGain; + message.readPrimitive(&packedGain); + float gain = unpackFloatGainFromByte(packedGain); + + setMasterInjectorGain(gain); + qCDebug(audio) << "Setting MASTER injector gain for " << uuid << " to " << gain; +} + void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) { auto it = std::find_if(_streams.active.cbegin(), _streams.active.cend(), [nodeID](const MixableStream& mixableStream){ return mixableStream.nodeStreamID.nodeID == nodeID && mixableStream.nodeStreamID.streamID.isNull(); diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index f9d113c53d..4a1ca7f9b5 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -63,6 +63,7 @@ public: void negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node); void parseRequestsDomainListData(ReceivedMessage& message); void parsePerAvatarGainSet(ReceivedMessage& message, const SharedNodePointer& node); + void parseInjectorGainSet(ReceivedMessage& message, const SharedNodePointer& node); void parseNodeIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); void parseRadiusIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); void parseSoloRequest(QSharedPointer message, const SharedNodePointer& node); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 0ec7c40ca4..413ff14b17 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -57,7 +57,7 @@ public: ICEServerQuery, OctreeStats, SetAvatarTraits, - UNUSED_PACKET_TYPE, + InjectorGainSet, AssignmentClientStatus, NoisyMute, AvatarIdentity, From cc2868a6f4ba5097d88045863584e3e83c7d3862 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Mar 2019 12:46:47 -0700 Subject: [PATCH 038/135] Can get server IP. --- tools/nitpick/src/TestRunnerMobile.cpp | 84 +++++++++++++++++++++++++- tools/nitpick/src/TestRunnerMobile.h | 3 + 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 73f1da5d26..ad5151bcc0 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -9,6 +9,7 @@ // #include "TestRunnerMobile.h" +#include #include #include #include @@ -67,7 +68,7 @@ void TestRunnerMobile::connectDevice() { if (!_adbInterface) { _adbInterface = new AdbInterface(); } - + // Get list of devices QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename; @@ -202,6 +203,8 @@ void TestRunnerMobile::runInterface() { _adbInterface = new AdbInterface(); } + sendServerIPToDevice(); + _statusLabel->setText("Starting Interface"); QString testScript = (_runFullSuite->isChecked()) @@ -245,3 +248,82 @@ void TestRunnerMobile::pullFolder() { _statusLabel->setText("Pull complete"); #endif } + +void TestRunnerMobile::sendServerIPToDevice() { + // Get device IP + QFile ifconfigFile{ _workingFolder + "/ifconfig.txt" }; + if (!ifconfigFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'ifconfig.txt'"); + exit(-1); + } + + QTextStream stream(&ifconfigFile); + QString line = ifconfigFile.readLine(); + while (!line.isNull()) { + // The device IP is in the line following the "wlan0" line + line = ifconfigFile.readLine(); + if (line.left(6) == "wlan0 ") { + break; + } + } + + // The following line looks like this "inet addr:192.168.0.15 Bcast:192.168.0.255 Mask:255.255.255.0" + // Extract the address and mask + line = ifconfigFile.readLine(); + QStringList lineParts = line.split(':'); + if (lineParts.size() < 4) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "IP address line not in expected format: " + line); + exit(-1); + } + + qint64 deviceIP = convertToBinary(lineParts[1].split(' ')[0]); + qint64 deviceMask = convertToBinary(lineParts[3].split(' ')[0]); + qint64 deviceSubnet = deviceMask & deviceIP; + + // The device needs to be on the same subnet as the server + // To find which of our IPs is the server - choose the 1st that is on the same subnet + // If more than one found then report an error + + QString serverIP; + + QList interfaces = QNetworkInterface::allInterfaces(); + for (int i = 0; i < interfaces.count(); i++) { + QList entries = interfaces.at(i).addressEntries(); + for (int j = 0; j < entries.count(); j++) { + if (entries.at(j).ip().protocol() == QAbstractSocket::IPv4Protocol) { + qint64 hostIP = convertToBinary(entries.at(j).ip().toString()); + qint64 hostMask = convertToBinary(entries.at(j).netmask().toString()); + qint64 hostSubnet = hostMask & hostIP; + + if (hostSubnet == deviceSubnet) { + if (!serverIP.isNull()) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Cannot identify server IP (multiple interfaces on device submask)"); + return; + } else { + union { + uint32_t ip; + uint8_t bytes[4]; + } u; + u.ip = hostIP; + + serverIP = QString::number(u.bytes[3]) + '.' + QString::number(u.bytes[2]) + '.' + QString::number(u.bytes[1]) + '.' + QString::number(u.bytes[0]); + } + } + } + } + } + + ifconfigFile.close(); +} + +qint64 TestRunnerMobile::convertToBinary(const QString& str) { + QString binary; + foreach (const QString& s, str.split(".")) { + binary += QString::number(s.toInt(), 2).rightJustified(8, '0'); + } + + return binary.toLongLong(NULL, 2); +} \ No newline at end of file diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 7dbf5456b3..7554a075c8 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -52,6 +52,9 @@ public: void pullFolder(); + void sendServerIPToDevice(); + qint64 convertToBinary (const QString& str); + private: QPushButton* _connectDeviceButton; QPushButton* _pullFolderButton; From 9e8e389e99c406c22dadea2987e07cd8e9075aaa Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Mar 2019 13:38:43 -0700 Subject: [PATCH 039/135] Improved detection of device model. --- tools/nitpick/src/TestRunnerMobile.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index ad5151bcc0..72b3ea5cc9 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -98,6 +98,8 @@ void TestRunnerMobile::connectDevice() { QString line2 = devicesFile.readLine(); const QString DEVICE{ "device" }; + const QString MODEL{ "model" }; + if (line2.contains("unauthorized")) { QMessageBox::critical(0, "Unauthorized device detected", "Please allow USB debugging on device"); } else if (line2.contains(DEVICE)) { @@ -110,10 +112,21 @@ void TestRunnerMobile::connectDevice() { QStringList tokens = line2.split(QRegExp("[\r\n\t ]+")); QString deviceID = tokens[0]; - QString modelID = tokens[3].split(':')[1]; + // Find the model entry + int i; + for (i = 0; i < tokens.size(); ++i) { + if (tokens[i].contains(MODEL)) { + break; + } + } + _modelName = "UNKNOWN"; - if (modelNames.count(modelID) == 1) { - _modelName = modelNames[modelID]; + if (i < tokens.size()) { + QString modelID = tokens[i].split(':')[1]; + + if (modelNames.count(modelID) == 1) { + _modelName = modelNames[modelID]; + } } _detectedDeviceLabel->setText(_modelName + " [" + deviceID + "]"); From b91a1d930a1303b3d0a9c38daa3115f79176862e Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Mar 2019 13:44:01 -0700 Subject: [PATCH 040/135] Missing newline. --- tools/nitpick/src/TestRunnerMobile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 72b3ea5cc9..0710e48008 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -339,4 +339,4 @@ qint64 TestRunnerMobile::convertToBinary(const QString& str) { } return binary.toLongLong(NULL, 2); -} \ No newline at end of file +} From 2300fe471d8909aaeb63b664cd095220075fdfb3 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 14 Mar 2019 13:18:12 -0700 Subject: [PATCH 041/135] we now read frame zero of the animation to override the bind pose in fbxs that are before version 7500 --- libraries/fbx/src/FBXSerializer.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 52f4189bdb..8ff3005ddc 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -441,6 +441,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; + int fbxVersionNumber = -1; foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { @@ -463,6 +464,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } + } else if (object.name == "FBXVersion") { + fbxVersionNumber = object.properties.at(0).toInt(); + qCDebug(modelformat) << "the fbx version number " << fbxVersionNumber; } } } else if (child.name == "GlobalSettings") { @@ -1309,8 +1313,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.bindTransformFoundInCluster = false; - hfmModel.joints.append(joint); - QString rotationID = localRotations.value(modelID); AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID)); AnimationCurve yRotCurve = animationCurves.value(yComponents.value(rotationID)); @@ -1333,7 +1335,13 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr xPosCurve.values.isEmpty() ? defaultPosValues.x : xPosCurve.values.at(i % xPosCurve.values.size()), yPosCurve.values.isEmpty() ? defaultPosValues.y : yPosCurve.values.at(i % yPosCurve.values.size()), zPosCurve.values.isEmpty() ? defaultPosValues.z : zPosCurve.values.at(i % zPosCurve.values.size())); + if ((fbxVersionNumber < 7500) && (i == 0)) { + joint.translation = hfmModel.animationFrames[i].translations[jointIndex]; + joint.rotation = hfmModel.animationFrames[i].rotations[jointIndex]; + } + } + hfmModel.joints.append(joint); } // NOTE: shapeVertices are in joint-frame From ca0379f6de206502722aa27f31137870fc762677 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 17:24:50 -0700 Subject: [PATCH 042/135] Send InjectorGainSet packet to the audio-mixer --- libraries/networking/src/NodeList.cpp | 25 +++++++++++++++++++++++++ libraries/networking/src/NodeList.h | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index e6eb6087b0..eec710322e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -265,6 +265,8 @@ void NodeList::reset(bool skipDomainHandlerReset) { _avatarGainMap.clear(); _avatarGainMapLock.unlock(); + _injectorGain = 0.0f; + if (!skipDomainHandlerReset) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); @@ -1087,6 +1089,29 @@ float NodeList::getAvatarGain(const QUuid& nodeID) { return 0.0f; } +void NodeList::setInjectorGain(float gain) { + auto audioMixer = soloNodeOfType(NodeType::AudioMixer); + if (audioMixer) { + // setup the packet + auto setInjectorGainPacket = NLPacket::create(PacketType::InjectorGainSet, sizeof(float), true); + + // We need to convert the gain in dB (from the script) to an amplitude before packing it. + setInjectorGainPacket->writePrimitive(packFloatGainToByte(fastExp2f(gain / 6.02059991f))); + + qCDebug(networking) << "Sending Set Injector Gain packet with Gain:" << gain; + + sendPacket(std::move(setInjectorGainPacket), *audioMixer); + _injectorGain = gain; + + } else { + qWarning() << "Couldn't find audio mixer to send set gain request"; + } +} + +float NodeList::getInjectorGain() { + return _injectorGain; +} + void NodeList::kickNodeBySessionID(const QUuid& nodeID) { // send a request to domain-server to kick the node with the given session ID // the domain-server will handle the persistence of the kick (via username or IP) diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index e135bc937d..d2a1212d64 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -83,6 +83,8 @@ public: bool isPersonalMutingNode(const QUuid& nodeID) const; void setAvatarGain(const QUuid& nodeID, float gain); float getAvatarGain(const QUuid& nodeID); + void setInjectorGain(float gain); + float getInjectorGain(); void kickNodeBySessionID(const QUuid& nodeID); void muteNodeBySessionID(const QUuid& nodeID); @@ -181,6 +183,8 @@ private: mutable QReadWriteLock _avatarGainMapLock; tbb::concurrent_unordered_map _avatarGainMap; + std::atomic _injectorGain { 0.0f }; + void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); #if defined(Q_OS_ANDROID) Setting::Handle _ignoreRadiusEnabled { "IgnoreRadiusEnabled", false }; From b5a45be7b986995e8be2e5f1fffe071331b3e514 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 15 Mar 2019 01:08:53 +0100 Subject: [PATCH 043/135] avatar doctor documentation urls --- interface/src/avatar/AvatarDoctor.cpp | 65 ++++++++++++++------------- interface/src/avatar/AvatarDoctor.h | 2 + 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index 04a426c3db..43e50ea049 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -55,7 +55,7 @@ static QStringList HAND_MAPPING_SUFFIXES = { "HandThumb1", }; -const QUrl DEFAULT_DOCS_URL = QUrl("https://docs.highfidelity.com/create/avatars/create-avatars.html#create-your-own-avatar"); +const QUrl PACKAGE_AVATAR_DOCS_BASE_URL = QUrl("https://docs.highfidelity.com/create/avatars/package-avatar.html"); AvatarDoctor::AvatarDoctor(const QUrl& avatarFSTFileUrl) : _avatarFSTFileUrl(avatarFSTFileUrl) { @@ -85,7 +85,7 @@ void AvatarDoctor::startDiagnosing() { const auto resourceLoaded = [this, resource](bool success) { // MODEL if (!success) { - _errors.push_back({ "Model file cannot be opened.", DEFAULT_DOCS_URL }); + addError("Model file cannot be opened.", "missing-file"); emit complete(getErrors()); return; } @@ -93,45 +93,45 @@ void AvatarDoctor::startDiagnosing() { const auto model = resource.data(); const auto avatarModel = resource.data()->getHFMModel(); if (!avatarModel.originalURL.endsWith(".fbx")) { - _errors.push_back({ "Unsupported avatar model format.", DEFAULT_DOCS_URL }); + addError("Unsupported avatar model format.", "unsupported-format"); emit complete(getErrors()); return; } // RIG if (avatarModel.joints.isEmpty()) { - _errors.push_back({ "Avatar has no rig.", DEFAULT_DOCS_URL }); + addError("Avatar has no rig.", "no-rig"); } else { auto jointNames = avatarModel.getJointNames(); if (avatarModel.joints.length() > NETWORKED_JOINTS_LIMIT) { - _errors.push_back({tr( "Avatar has over %n bones.", "", NETWORKED_JOINTS_LIMIT), DEFAULT_DOCS_URL }); + addError(tr( "Avatar has over %n bones.", "", NETWORKED_JOINTS_LIMIT), "maximum-bone-limit"); } // Avatar does not have Hips bone mapped if (!jointNames.contains("Hips")) { - _errors.push_back({ "Hips are not mapped.", DEFAULT_DOCS_URL }); + addError("Hips are not mapped.", "hips-not-mapped"); } if (!jointNames.contains("Spine")) { - _errors.push_back({ "Spine is not mapped.", DEFAULT_DOCS_URL }); + addError("Spine is not mapped.", "spine-not-mapped"); } if (!jointNames.contains("Spine1")) { - _errors.push_back({ "Chest (Spine1) is not mapped.", DEFAULT_DOCS_URL }); + addError("Chest (Spine1) is not mapped.", "chest-not-mapped"); } if (!jointNames.contains("Neck")) { - _errors.push_back({ "Neck is not mapped.", DEFAULT_DOCS_URL }); + addError("Neck is not mapped.", "neck-not-mapped"); } if (!jointNames.contains("Head")) { - _errors.push_back({ "Head is not mapped.", DEFAULT_DOCS_URL }); + addError("Head is not mapped.", "head-not-mapped"); } if (!jointNames.contains("LeftEye")) { if (jointNames.contains("RightEye")) { - _errors.push_back({ "LeftEye is not mapped.", DEFAULT_DOCS_URL }); + addError("LeftEye is not mapped.", "eye-not-mapped"); } else { - _errors.push_back({ "Eyes are not mapped.", DEFAULT_DOCS_URL }); + addError("Eyes are not mapped.", "eye-not-mapped"); } } else if (!jointNames.contains("RightEye")) { - _errors.push_back({ "RightEye is not mapped.", DEFAULT_DOCS_URL }); + addError("RightEye is not mapped.", "eye-not-mapped"); } const auto checkJointAsymmetry = [jointNames] (const QStringList& jointMappingSuffixes) { @@ -159,13 +159,13 @@ void AvatarDoctor::startDiagnosing() { }; if (checkJointAsymmetry(ARM_MAPPING_SUFFIXES)) { - _errors.push_back({ "Asymmetrical arm bones.", DEFAULT_DOCS_URL }); + addError("Asymmetrical arm bones.", "asymmetrical-bones"); } if (checkJointAsymmetry(HAND_MAPPING_SUFFIXES)) { - _errors.push_back({ "Asymmetrical hand bones.", DEFAULT_DOCS_URL }); + addError("Asymmetrical hand bones.", "asymmetrical-bones"); } if (checkJointAsymmetry(LEG_MAPPING_SUFFIXES)) { - _errors.push_back({ "Asymmetrical leg bones.", DEFAULT_DOCS_URL }); + addError("Asymmetrical leg bones.", "asymmetrical-bones"); } // Multiple skeleton root joints checkup @@ -177,7 +177,7 @@ void AvatarDoctor::startDiagnosing() { } if (skeletonRootJoints > 1) { - _errors.push_back({ "Multiple top-level joints found.", DEFAULT_DOCS_URL }); + addError("Multiple top-level joints found.", "multiple-top-level-joints"); } Rig rig; @@ -191,9 +191,9 @@ void AvatarDoctor::startDiagnosing() { const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { - _errors.push_back({ "Avatar is possibly too short.", DEFAULT_DOCS_URL }); + addError("Avatar is possibly too short.", "short-avatar"); } else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { - _errors.push_back({ "Avatar is possibly too tall.", DEFAULT_DOCS_URL }); + addError("Avatar is possibly too tall.", "tall-avatar"); } // HipsNotOnGround @@ -204,7 +204,7 @@ void AvatarDoctor::startDiagnosing() { const auto hipJoint = avatarModel.joints.at(avatarModel.getJointIndex("Hips")); if (hipsPosition.y < HIPS_GROUND_MIN_Y) { - _errors.push_back({ "Hips are on ground.", DEFAULT_DOCS_URL }); + addError("Hips are on ground.", "hips-on-ground"); } } } @@ -223,7 +223,7 @@ void AvatarDoctor::startDiagnosing() { const auto hipsToSpine = glm::length(hipsPosition - spinePosition); const auto spineToChest = glm::length(spinePosition - chestPosition); if (hipsToSpine < HIPS_SPINE_CHEST_MIN_SEPARATION && spineToChest < HIPS_SPINE_CHEST_MIN_SEPARATION) { - _errors.push_back({ "Hips/Spine/Chest overlap.", DEFAULT_DOCS_URL }); + addError("Hips/Spine/Chest overlap.", "overlap-error"); } } } @@ -240,21 +240,21 @@ void AvatarDoctor::startDiagnosing() { const auto& uniqueJointValues = jointValues.toSet(); for (const auto& jointName: uniqueJointValues) { if (jointValues.count(jointName) > 1) { - _errors.push_back({ tr("%1 is mapped multiple times.").arg(jointName), DEFAULT_DOCS_URL }); + addError(tr("%1 is mapped multiple times.").arg(jointName), "mapped-multiple-times"); } } } if (!isDescendantOfJointWhenJointsExist("Spine", "Hips")) { - _errors.push_back({ "Spine is not a child of Hips.", DEFAULT_DOCS_URL }); + addError("Spine is not a child of Hips.", "spine-not-child"); } if (!isDescendantOfJointWhenJointsExist("Spine1", "Spine")) { - _errors.push_back({ "Spine1 is not a child of Spine.", DEFAULT_DOCS_URL }); + addError("Spine1 is not a child of Spine.", "spine1-not-child"); } if (!isDescendantOfJointWhenJointsExist("Head", "Spine1")) { - _errors.push_back({ "Head is not a child of Spine1.", DEFAULT_DOCS_URL }); + addError("Head is not a child of Spine1.", "head-not-child"); } } @@ -300,7 +300,7 @@ void AvatarDoctor::startDiagnosing() { connect(resource.data(), &GeometryResource::finished, this, resourceLoaded); } } else { - _errors.push_back({ "Model file cannot be opened", DEFAULT_DOCS_URL }); + addError("Model file cannot be opened", "missing-file"); emit complete(getErrors()); } } @@ -345,7 +345,7 @@ void AvatarDoctor::diagnoseTextures() { QUrl(avatarModel.originalURL)).resolved(QUrl("textures")); if (texturesFound == 0) { - _errors.push_back({ tr("No textures assigned."), DEFAULT_DOCS_URL }); + addError(tr("No textures assigned."), "no-textures-assigned"); } if (!externalTextures.empty()) { @@ -356,11 +356,10 @@ void AvatarDoctor::diagnoseTextures() { auto checkTextureLoadingComplete = [this]() mutable { if (_checkedTextureCount == _externalTextureCount) { if (_missingTextureCount > 0) { - _errors.push_back({ tr("Missing %n texture(s).","", _missingTextureCount), DEFAULT_DOCS_URL }); + addError(tr("Missing %n texture(s).","", _missingTextureCount), "missing-textures"); } if (_unsupportedTextureCount > 0) { - _errors.push_back({ tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount), - DEFAULT_DOCS_URL }); + addError(tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount), "unsupported-textures"); } emit complete(getErrors()); @@ -411,6 +410,12 @@ void AvatarDoctor::diagnoseTextures() { } } +void AvatarDoctor::addError(const QString& errorMessage, const QString& docFragment) { + QUrl documentationURL = PACKAGE_AVATAR_DOCS_BASE_URL; + documentationURL.setFragment(docFragment); + _errors.push_back({ errorMessage, documentationURL }); +} + QVariantList AvatarDoctor::getErrors() const { QVariantList result; for (const auto& error : _errors) { diff --git a/interface/src/avatar/AvatarDoctor.h b/interface/src/avatar/AvatarDoctor.h index 780f600bed..1465a5defc 100644 --- a/interface/src/avatar/AvatarDoctor.h +++ b/interface/src/avatar/AvatarDoctor.h @@ -40,6 +40,8 @@ signals: private: void diagnoseTextures(); + void addError(const QString& errorMessage, const QString& docFragment); + QUrl _avatarFSTFileUrl; QVector _errors; From 714115adef8b485f95872c3a409f0c1e44b8abaf Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 15 Mar 2019 21:36:46 +0100 Subject: [PATCH 044/135] - Video, and new Docs url in QML --- .../hifi/avatarPackager/AvatarPackagerApp.qml | 11 ++++++++-- .../avatarPackager/AvatarPackagerHeader.qml | 20 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 8afc60fd90..278ce36362 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -133,7 +133,7 @@ Item { states: [ State { name: AvatarPackagerState.main - PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; backButtonVisible: false } + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; videoEnabled: true; backButtonVisible: false } PropertyChanges { target: avatarPackagerMain; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } }, @@ -229,7 +229,11 @@ Item { } function openDocs() { - Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/create-avatars#how-to-package-your-avatar"); + Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/package-avatar.html"); + } + + function openVideo() { + Qt.openUrlExternally("https://youtu.be/zrkEowu_yps"); } AvatarPackagerHeader { @@ -243,6 +247,9 @@ Item { onDocsButtonClicked: { avatarPackager.openDocs(); } + onVideoButtonClicked: { + avatarPackager.openVideo(); + } } Item { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 25201bf81e..31528a8557 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -13,6 +13,7 @@ ShadowRectangle { property string title: qsTr("Avatar Packager") property alias docsEnabled: docs.visible + property alias videoEnabled: video.visible property bool backButtonVisible: true // If false, is not visible and does not take up space property bool backButtonEnabled: true // If false, is not visible but does not affect space property bool canRename: false @@ -24,6 +25,7 @@ ShadowRectangle { signal backButtonClicked signal docsButtonClicked + signal videoButtonClicked RalewayButton { id: back @@ -126,6 +128,20 @@ ShadowRectangle { } } + RalewayButton { + id: video + visible: false + size: 28 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: docs.left + anchors.rightMargin: 16 + + text: qsTr("Video") + + onClicked: videoButtonClicked() + } + RalewayButton { id: docs visible: false @@ -137,8 +153,6 @@ ShadowRectangle { text: qsTr("Docs") - onClicked: { - docsButtonClicked(); - } + onClicked: docsButtonClicked() } } From 6ed4937dc0f627074bb533d5b5c1d938a03a599c Mon Sep 17 00:00:00 2001 From: milad Date: Mon, 18 Mar 2019 10:12:32 -0700 Subject: [PATCH 045/135] Updated engine code to not emit if dominant hand isn't actually changed --- interface/src/avatar/MyAvatar.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 02ef91cdba..9813070308 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -324,8 +324,11 @@ QString MyAvatar::getDominantHand() const { void MyAvatar::setDominantHand(const QString& hand) { if (hand == DOMINANT_LEFT_HAND || hand == DOMINANT_RIGHT_HAND) { - _dominantHand.set(hand); - emit dominantHandChanged(hand); + bool changed = (hand != _dominantHand.get()); + if (changed) { + _dominantHand.set(hand); + emit dominantHandChanged(hand); + } } } From 7a5bbb8f6f42b59e85f8678e066cca7b3dfcfa4c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 18 Mar 2019 14:43:32 -0700 Subject: [PATCH 046/135] fix laser ghosting --- .../src/RenderablePolyLineEntityItem.cpp | 90 +++++++++++-------- .../src/RenderablePolyLineEntityItem.h | 2 +- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 98f79780be..df52934b87 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -95,19 +95,18 @@ bool PolyLineEntityRenderer::needsRenderUpdate() const { } bool PolyLineEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { - return ( - entity->pointsChanged() || - entity->widthsChanged() || - entity->normalsChanged() || - entity->texturesChanged() || - entity->colorsChanged() || - _isUVModeStretch != entity->getIsUVModeStretch() || - _glow != entity->getGlow() || - _faceCamera != entity->getFaceCamera() - ); + if (entity->pointsChanged() || entity->widthsChanged() || entity->normalsChanged() || entity->texturesChanged() || entity->colorsChanged()) { + return true; + } + + if (_isUVModeStretch != entity->getIsUVModeStretch() || _glow != entity->getGlow() || _faceCamera != entity->getFaceCamera()) { + return true; + } + + return Parent::needsRenderUpdateFromTypedEntity(entity); } -void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { +void PolyLineEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { auto pointsChanged = entity->pointsChanged(); auto widthsChanged = entity->widthsChanged(); auto normalsChanged = entity->normalsChanged(); @@ -119,10 +118,6 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo entity->resetPolyLineChanged(); - // Transform - updateModelTransformAndBound(); - _renderTransform = getModelTransform(); - // Textures if (entity->texturesChanged()) { entity->resetTexturesChanged(); @@ -131,7 +126,9 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (!textures.isEmpty()) { entityTextures = QUrl(textures); } - _texture = DependencyManager::get()->getTexture(entityTextures); + withWriteLock([&] { + _texture = DependencyManager::get()->getTexture(entityTextures); + }); _textureAspectRatio = 1.0f; _textureLoaded = false; } @@ -145,11 +142,13 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo // Data bool faceCameraChanged = faceCamera != _faceCamera; - if (faceCameraChanged || glow != _glow) { - _faceCamera = faceCamera; - _glow = glow; - updateData(); - } + withWriteLock([&] { + if (faceCameraChanged || glow != _glow) { + _faceCamera = faceCamera; + _glow = glow; + updateData(); + } + }); // Geometry if (pointsChanged) { @@ -165,10 +164,21 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo _colors = entity->getStrokeColors(); _color = toGlm(entity->getColor()); } - if (_isUVModeStretch != isUVModeStretch || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged || faceCameraChanged) { - _isUVModeStretch = isUVModeStretch; - updateGeometry(); - } + + bool uvModeStretchChanged = _isUVModeStretch != isUVModeStretch; + _isUVModeStretch = isUVModeStretch; + + void* key = (void*)this; + AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [=]() { + withWriteLock([&] { + updateModelTransformAndBound(); + _renderTransform = getModelTransform(); + + if (uvModeStretchChanged || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged || faceCameraChanged) { + updateGeometry(); + } + }); + }); } void PolyLineEntityRenderer::updateGeometry() { @@ -267,22 +277,32 @@ void PolyLineEntityRenderer::updateData() { } void PolyLineEntityRenderer::doRender(RenderArgs* args) { - if (_numVertices < 2) { - return; - } - PerformanceTimer perfTimer("RenderablePolyLineEntityItem::render"); Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; - if (!_pipeline || !_glowPipeline) { + size_t numVertices; + Transform transform; + gpu::TexturePointer texture; + withReadLock([&] { + numVertices = _numVertices; + transform = _renderTransform; + texture = _textureLoaded ? _texture->getGPUTexture() : DependencyManager::get()->getWhiteTexture(); + + batch.setResourceBuffer(0, _polylineGeometryBuffer); + batch.setUniformBuffer(0, _polylineDataBuffer); + }); + + if (numVertices < 2) { + return; + } + + if (!_pipeline) { buildPipeline(); } batch.setPipeline(_glow ? _glowPipeline : _pipeline); - batch.setModelTransform(_renderTransform); - batch.setResourceTexture(0, _textureLoaded ? _texture->getGPUTexture() : DependencyManager::get()->getWhiteTexture()); - batch.setResourceBuffer(0, _polylineGeometryBuffer); - batch.setUniformBuffer(0, _polylineDataBuffer); - batch.draw(gpu::TRIANGLE_STRIP, (gpu::uint32)(2 * _numVertices), 0); + batch.setModelTransform(transform); + batch.setResourceTexture(0, texture); + batch.draw(gpu::TRIANGLE_STRIP, (gpu::uint32)(2 * numVertices), 0); } diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index b8a3ad3b3e..3815b57671 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -31,7 +31,7 @@ public: protected: virtual bool needsRenderUpdate() const override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; - virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; + virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; virtual ItemKey getKey() override; virtual ShapeKey getShapeKey() override; From ad2c4718a396c6c5065be5736269b76b44bc7919 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 18 Mar 2019 12:13:36 -0700 Subject: [PATCH 047/135] Fix loading relative files --- libraries/gpu/src/gpu/FrameReader.cpp | 2 +- tools/noramlizeFrame.py | 62 --------------------------- tools/normalizeFrame.py | 7 ++- 3 files changed, 6 insertions(+), 65 deletions(-) delete mode 100644 tools/noramlizeFrame.py diff --git a/libraries/gpu/src/gpu/FrameReader.cpp b/libraries/gpu/src/gpu/FrameReader.cpp index 2fe143ee90..812f4db7cc 100644 --- a/libraries/gpu/src/gpu/FrameReader.cpp +++ b/libraries/gpu/src/gpu/FrameReader.cpp @@ -40,7 +40,7 @@ public: auto lastSlash = filename.rfind('/'); result = filename.substr(0, lastSlash + 1); } else { - std::string result = QFileInfo(filename.c_str()).absoluteDir().canonicalPath().toStdString(); + result = QFileInfo(filename.c_str()).absoluteDir().canonicalPath().toStdString(); if (*result.rbegin() != '/') { result += '/'; } diff --git a/tools/noramlizeFrame.py b/tools/noramlizeFrame.py deleted file mode 100644 index f1012f6428..0000000000 --- a/tools/noramlizeFrame.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import json -import shutil -import sys - -def scriptRelative(*paths): - scriptdir = os.path.dirname(os.path.realpath(sys.argv[0])) - result = os.path.join(scriptdir, *paths) - result = os.path.realpath(result) - result = os.path.normcase(result) - return result - - - -class FrameProcessor: - def __init__(self, filename): - self.filename = filename - dir, name = os.path.split(self.filename) - self.dir = dir - self.ktxDir = os.path.join(self.dir, 'ktx') - os.makedirs(self.ktxDir, exist_ok=True) - self.resDir = scriptRelative("../interface/resources") - - if (name.endswith(".json")): - self.name = name[0:-5] - else: - self.name = name - self.filename = self.filename + '.json' - - with open(self.filename, 'r') as f: - self.json = json.load(f) - - - def processKtx(self, texture): - if texture is None: return - if not 'ktxFile' in texture: return - sourceKtx = texture['ktxFile'] - if sourceKtx.startswith(':'): - sourceKtx = sourceKtx[1:] - while sourceKtx.startswith('/'): - sourceKtx = sourceKtx[1:] - sourceKtx = os.path.join(self.resDir, sourceKtx) - sourceKtxDir, sourceKtxName = os.path.split(sourceKtx) - destKtx = os.path.join(self.ktxDir, sourceKtxName) - if not os.path.isfile(destKtx): - shutil.copy(sourceKtx, destKtx) - newValue = 'ktx/' + sourceKtxName - texture['ktxFile'] = newValue - - - def process(self): - for texture in self.json['textures']: - self.processKtx(texture) - - with open(self.filename, 'w') as f: - json.dump(self.json, f, indent=2) - -fp = FrameProcessor("D:/Frames/20190114_1629.json") -fp.process() - - -#C:\Users\bdavi\git\hifi\interface\resources\meshes \ No newline at end of file diff --git a/tools/normalizeFrame.py b/tools/normalizeFrame.py index 36b2038d5c..892609bbf0 100644 --- a/tools/normalizeFrame.py +++ b/tools/normalizeFrame.py @@ -36,7 +36,10 @@ class FrameProcessor: if not 'ktxFile' in texture: return sourceKtx = texture['ktxFile'] if sourceKtx.startswith(':'): - sourceKtx = os.path.join(self.resDir, sourceKtx[3:]) + sourceKtx = sourceKtx[1:] + while sourceKtx.startswith('/'): + sourceKtx = sourceKtx[1:] + sourceKtx = os.path.join(self.resDir, sourceKtx) sourceKtxDir, sourceKtxName = os.path.split(sourceKtx) destKtx = os.path.join(self.ktxDir, sourceKtxName) if not os.path.isfile(destKtx): @@ -52,7 +55,7 @@ class FrameProcessor: with open(self.filename, 'w') as f: json.dump(self.json, f, indent=2) -fp = FrameProcessor("D:/Frames/20190110_1635.json") +fp = FrameProcessor("D:/Frames/20190112_1647.json") fp.process() From 3775633cc3b61f8391d7dc7088265fd3118fd8a1 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 20 Mar 2019 15:29:13 -0700 Subject: [PATCH 048/135] Case 20407 - Pay-In API doesn't display error when no username specified --- .../resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 626ac4da65..e159344d5c 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -2248,6 +2248,7 @@ Item { if (sendAssetStep.selectedRecipientUserName === "") { console.log("SendAsset: Script didn't specify a recipient username!"); sendAssetHome.visible = false; + root.nextActiveView = 'paymentFailure'; return; } From 8947d4d133320b44001433d9330b274cc85ec96e Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 20 Mar 2019 15:45:06 -0700 Subject: [PATCH 049/135] When adding new Node clear any dangling Connection objects to its address --- libraries/networking/src/LimitedNodeList.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index a9dbc12b09..18a180ad79 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -677,6 +677,9 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t // If there is a new node with the same socket, this is a reconnection, kill the old node removeOldNode(findNodeWithAddr(publicSocket)); removeOldNode(findNodeWithAddr(localSocket)); + // If there is an old Connection to the new node's address kill it + _nodeSocket.cleanupConnection(publicSocket); + _nodeSocket.cleanupConnection(localSocket); auto it = _connectionIDs.find(uuid); if (it == _connectionIDs.end()) { From 3186a9468231daff21687c76999469c012910824 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 20 Mar 2019 15:48:05 -0700 Subject: [PATCH 050/135] Add Users.setInjectorGain() and Users.getInjectorGain() to the scripting interface --- .../script-engine/src/UsersScriptingInterface.cpp | 9 +++++++++ .../script-engine/src/UsersScriptingInterface.h | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index fef11c12e9..a0593d3ff8 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -51,6 +51,15 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { return DependencyManager::get()->getAvatarGain(nodeID); } +void UsersScriptingInterface::setInjectorGain(float gain) { + // ask the NodeList to set the audio injector gain + DependencyManager::get()->setInjectorGain(gain); +} + +float UsersScriptingInterface::getInjectorGain() { + return DependencyManager::get()->getInjectorGain(); +} + void UsersScriptingInterface::kick(const QUuid& nodeID) { // ask the NodeList to kick the user with the given session ID DependencyManager::get()->kickNodeBySessionID(nodeID); diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 57de205066..17a84248a1 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -90,6 +90,21 @@ public slots: */ float getAvatarGain(const QUuid& nodeID); + /**jsdoc + * Sets the audio injector gain at the server. + * Units are Decibels (dB) + * @function Users.setInjectorGain + * @param {number} gain (in dB) + */ + void setInjectorGain(float gain); + + /**jsdoc + * Gets the audio injector gain at the server. + * @function Users.getInjectorGain + * @returns {number} gain (in dB) + */ + float getInjectorGain(); + /**jsdoc * Kick/ban another user. Removes them from the server and prevents them from returning. Bans by either user name (if * available) or machine fingerprint otherwise. This will only do anything if you're an admin of the domain you're in. From 61b7b8b66963a5f1fbae027e81fcc0705d074e34 Mon Sep 17 00:00:00 2001 From: Sam Gondelman Date: Thu, 21 Mar 2019 08:36:32 -0700 Subject: [PATCH 051/135] reduce lambda copies --- .../entities-renderer/src/RenderablePolyLineEntityItem.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index df52934b87..2430643ce2 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -167,14 +167,16 @@ void PolyLineEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& bool uvModeStretchChanged = _isUVModeStretch != isUVModeStretch; _isUVModeStretch = isUVModeStretch; + + bool geometryChanged = uvModeStretchChanged || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged || faceCameraChanged; void* key = (void*)this; - AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [=]() { + AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [this, geometryChanged] () { withWriteLock([&] { updateModelTransformAndBound(); _renderTransform = getModelTransform(); - if (uvModeStretchChanged || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged || faceCameraChanged) { + if (geometryChanged) { updateGeometry(); } }); From 7311c3ac06aa3e1425c169afc22f0d0c2842900e Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 21 Mar 2019 11:51:49 -0700 Subject: [PATCH 052/135] Move the new audio volume API from Users scripting interface to Audio scripting interface --- interface/src/scripting/Audio.cpp | 36 ++++++++++++++++--- interface/src/scripting/Audio.h | 30 ++++++++++++++++ .../src/UsersScriptingInterface.cpp | 9 ----- .../src/UsersScriptingInterface.h | 15 -------- 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index bf43db3044..e0474b7bba 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -374,6 +374,18 @@ void Audio::handlePushedToTalk(bool enabled) { } } +void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { + withWriteLock([&] { + _devices.chooseInputDevice(device, isHMD); + }); +} + +void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { + withWriteLock([&] { + _devices.chooseOutputDevice(device, isHMD); + }); +} + void Audio::setReverb(bool enable) { withWriteLock([&] { DependencyManager::get()->setReverb(enable); @@ -386,14 +398,28 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { }); } -void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { +void Audio::setAvatarGain(float gain) { withWriteLock([&] { - _devices.chooseInputDevice(device, isHMD); + // ask the NodeList to set the master avatar gain + DependencyManager::get()->setAvatarGain("", gain); }); } -void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { - withWriteLock([&] { - _devices.chooseOutputDevice(device, isHMD); +float Audio::getAvatarGain() { + return resultWithReadLock([&] { + return DependencyManager::get()->getAvatarGain(""); + }); +} + +void Audio::setInjectorGain(float gain) { + withWriteLock([&] { + // ask the NodeList to set the audio injector gain + DependencyManager::get()->setInjectorGain(gain); + }); +} + +float Audio::getInjectorGain() { + return resultWithReadLock([&] { + return DependencyManager::get()->getInjectorGain(); }); } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 9ee230fc29..14a75d5ffe 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -170,6 +170,36 @@ public: */ Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); + /**jsdoc + * Sets the master avatar gain at the server. + * Units are Decibels (dB) + * @function Audio.setAvatarGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setAvatarGain(float gain); + + /**jsdoc + * Gets the master avatar gain at the server. + * @function Audio.getAvatarGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getAvatarGain(); + + /**jsdoc + * Sets the audio injector gain at the server. + * Units are Decibels (dB) + * @function Audio.setInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setInjectorGain(float gain); + + /**jsdoc + * Gets the audio injector gain at the server. + * @function Audio.getInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getInjectorGain(); + /**jsdoc * Starts making an audio recording of the audio being played in-world (i.e., not local-only audio) to a file in WAV format. * @function Audio.startRecording diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index a0593d3ff8..fef11c12e9 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -51,15 +51,6 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { return DependencyManager::get()->getAvatarGain(nodeID); } -void UsersScriptingInterface::setInjectorGain(float gain) { - // ask the NodeList to set the audio injector gain - DependencyManager::get()->setInjectorGain(gain); -} - -float UsersScriptingInterface::getInjectorGain() { - return DependencyManager::get()->getInjectorGain(); -} - void UsersScriptingInterface::kick(const QUuid& nodeID) { // ask the NodeList to kick the user with the given session ID DependencyManager::get()->kickNodeBySessionID(nodeID); diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 17a84248a1..57de205066 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -90,21 +90,6 @@ public slots: */ float getAvatarGain(const QUuid& nodeID); - /**jsdoc - * Sets the audio injector gain at the server. - * Units are Decibels (dB) - * @function Users.setInjectorGain - * @param {number} gain (in dB) - */ - void setInjectorGain(float gain); - - /**jsdoc - * Gets the audio injector gain at the server. - * @function Users.getInjectorGain - * @returns {number} gain (in dB) - */ - float getInjectorGain(); - /**jsdoc * Kick/ban another user. Removes them from the server and prevents them from returning. Bans by either user name (if * available) or machine fingerprint otherwise. This will only do anything if you're an admin of the domain you're in. From 4fd2c4449d7b322dcb045172ec01d69375079693 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 21 Mar 2019 12:15:03 -0700 Subject: [PATCH 053/135] new procedural uniforms for time --- .../src/RenderableShapeEntityItem.cpp | 2 +- .../src/RenderableZoneEntityItem.cpp | 2 +- .../procedural/src/procedural/Procedural.cpp | 13 +++++++++++-- .../procedural/src/procedural/Procedural.h | 19 ++++++++++++------- .../src/procedural/ProceduralCommon.slh | 8 ++++++-- .../src/procedural/ProceduralSkybox.cpp | 4 ++-- .../src/procedural/ProceduralSkybox.h | 5 ++++- 7 files changed, 37 insertions(+), 16 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index b33eb619c8..0375e236a4 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -261,7 +261,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { if (_procedural.isReady()) { outColor = _procedural.getColor(outColor); outColor.a *= _procedural.isFading() ? Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) : 1.0f; - _procedural.prepare(batch, _position, _dimensions, _orientation, ProceduralProgramKey(outColor.a < 1.0f)); + _procedural.prepare(batch, _position, _dimensions, _orientation, _created, ProceduralProgramKey(outColor.a < 1.0f)); proceduralRender = true; } }); diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 631148c27a..8a7fa3f8e7 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -33,7 +33,7 @@ using namespace render::entities; ZoneEntityRenderer::ZoneEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { - _background->setSkybox(std::make_shared()); + _background->setSkybox(std::make_shared(entity->getCreated())); } void ZoneEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) { diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index ff8c270371..c5bfa43e75 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -225,11 +225,13 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, + const quint64& created, const ProceduralProgramKey key) { std::lock_guard lock(_mutex); _entityDimensions = size; _entityPosition = position; _entityOrientation = glm::mat3_cast(orientation); + _entityCreated = created; if (!_shaderPath.isEmpty()) { auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _shaderModified) { @@ -278,7 +280,10 @@ void Procedural::prepare(gpu::Batch& batch, _proceduralPipelines[key] = gpu::Pipeline::create(program, key.isTransparent() ? _transparentState : _opaqueState); - _start = usecTimestampNow(); + _lastCompile = usecTimestampNow(); + if (_firstCompile == 0) { + _firstCompile = _lastCompile; + } _frameCount = 0; recompiledShader = true; } @@ -371,7 +376,11 @@ void Procedural::setupUniforms() { _uniforms.push_back([=](gpu::Batch& batch) { _standardInputs.position = vec4(_entityPosition, 1.0f); // Minimize floating point error by doing an integer division to milliseconds, before the floating point division to seconds - _standardInputs.time = (float)((usecTimestampNow() - _start) / USECS_PER_MSEC) / MSECS_PER_SECOND; + auto now = usecTimestampNow(); + _standardInputs.timeSinceLastCompile = (float)((now - _lastCompile) / USECS_PER_MSEC) / MSECS_PER_SECOND; + _standardInputs.timeSinceFirstCompile = (float)((now - _firstCompile) / USECS_PER_MSEC) / MSECS_PER_SECOND; + _standardInputs.timeSinceEntityCreation = (float)((now - _entityCreated) / USECS_PER_MSEC) / MSECS_PER_SECOND; + // Date { diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 8477e69afc..b8fd77b052 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -82,7 +82,8 @@ public: bool isReady() const; bool isEnabled() const { return _enabled; } - void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const ProceduralProgramKey key = ProceduralProgramKey()); + void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, + const quint64& created, const ProceduralProgramKey key = ProceduralProgramKey()); glm::vec4 getColor(const glm::vec4& entityColor) const; quint64 getFadeStartTime() const { return _fadeStartTime; } @@ -106,9 +107,10 @@ protected: vec4 date; vec4 position; vec4 scale; - float time; + float timeSinceLastCompile; + float timeSinceFirstCompile; + float timeSinceEntityCreation; int frameCount; - vec2 _spare1; vec4 resolution[4]; mat4 orientation; }; @@ -116,9 +118,10 @@ protected: static_assert(0 == offsetof(StandardInputs, date), "ProceduralOffsets"); static_assert(16 == offsetof(StandardInputs, position), "ProceduralOffsets"); static_assert(32 == offsetof(StandardInputs, scale), "ProceduralOffsets"); - static_assert(48 == offsetof(StandardInputs, time), "ProceduralOffsets"); - static_assert(52 == offsetof(StandardInputs, frameCount), "ProceduralOffsets"); - static_assert(56 == offsetof(StandardInputs, _spare1), "ProceduralOffsets"); + static_assert(48 == offsetof(StandardInputs, timeSinceLastCompile), "ProceduralOffsets"); + static_assert(52 == offsetof(StandardInputs, timeSinceFirstCompile), "ProceduralOffsets"); + static_assert(56 == offsetof(StandardInputs, timeSinceEntityCreation), "ProceduralOffsets"); + static_assert(60 == offsetof(StandardInputs, frameCount), "ProceduralOffsets"); static_assert(64 == offsetof(StandardInputs, resolution), "ProceduralOffsets"); static_assert(128 == offsetof(StandardInputs, orientation), "ProceduralOffsets"); @@ -126,7 +129,8 @@ protected: ProceduralData _data; bool _enabled { false }; - uint64_t _start { 0 }; + uint64_t _lastCompile { 0 }; + uint64_t _firstCompile { 0 }; int32_t _frameCount { 0 }; // Rendering object descriptions, from userData @@ -152,6 +156,7 @@ protected: glm::vec3 _entityDimensions; glm::vec3 _entityPosition; glm::mat3 _entityOrientation; + quint64 _entityCreated; private: void setupUniforms(); diff --git a/libraries/procedural/src/procedural/ProceduralCommon.slh b/libraries/procedural/src/procedural/ProceduralCommon.slh index bd894a9e92..6e73534440 100644 --- a/libraries/procedural/src/procedural/ProceduralCommon.slh +++ b/libraries/procedural/src/procedural/ProceduralCommon.slh @@ -36,9 +36,11 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer { // Offset 48 float globalTime; // Offset 52 - int frameCount; + float localCreatedTime; // Offset 56 - vec2 _spare1; + float entityTime; + // Offset 60 + int frameCount; // Offset 64, acts as vec4[4] for alignment purposes vec3 channelResolution[4]; // Offset 128, acts as vec4[3] for alignment purposes @@ -52,6 +54,8 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer { #define iWorldPosition standardInputs.worldPosition #define iWorldScale standardInputs.worldScale #define iGlobalTime standardInputs.globalTime +#define iLocalCreatedTime standardInputs.localCreatedTime +#define iEntityTime standardInputs.entityTime #define iFrameCount standardInputs.frameCount #define iChannelResolution standardInputs.channelResolution #define iWorldOrientation standardInputs.worldOrientation diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index 6eb6d531e1..bf8e408e70 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -17,7 +17,7 @@ #include #include -ProceduralSkybox::ProceduralSkybox() : graphics::Skybox() { +ProceduralSkybox::ProceduralSkybox(quint64 created) : graphics::Skybox(), _created(created) { _procedural._vertexSource = gpu::Shader::createVertex(shader::graphics::vertex::skybox)->getSource(); _procedural._opaqueFragmentSource = shader::Source::get(shader::procedural::fragment::proceduralSkybox); // Adjust the pipeline state for background using the stencil test @@ -59,7 +59,7 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, batch.setModelTransform(Transform()); // only for Mac auto& procedural = skybox._procedural; - procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat()); + procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat(), skybox.getCreated()); skybox.prepare(batch); batch.draw(gpu::TRIANGLE_STRIP, 4); } diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h index 5db1078a5f..1b01b891d3 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.h +++ b/libraries/procedural/src/procedural/ProceduralSkybox.h @@ -19,7 +19,7 @@ class ProceduralSkybox: public graphics::Skybox { public: - ProceduralSkybox(); + ProceduralSkybox(quint64 created = 0); void parse(const QString& userData) { _procedural.setProceduralData(ProceduralData::parse(userData)); } @@ -29,8 +29,11 @@ public: void render(gpu::Batch& batch, const ViewFrustum& frustum) const override; static void render(gpu::Batch& batch, const ViewFrustum& frustum, const ProceduralSkybox& skybox); + quint64 getCreated() const { return _created; } + protected: mutable Procedural _procedural; + quint64 _created; }; typedef std::shared_ptr< ProceduralSkybox > ProceduralSkyboxPointer; From b0e2b5fde1aebf9c13689bb885c6791808a060b4 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 21 Mar 2019 14:47:48 -0700 Subject: [PATCH 054/135] Remove unneeded variable. --- tools/nitpick/src/TestRunnerMobile.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 1ab3ed7737..105d30fca4 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -197,10 +197,6 @@ void TestRunnerMobile::installAPK() { return; } - // Remove the path - QStringList parts = installerPathname.split('/'); - _installerFilename = parts[parts.length() - 1]; - _statusLabel->setText("Installing"); QString command = _adbInterface->getAdbCommand() + " install -r -d " + installerPathname + " >" + _workingFolder + "/installOutput.txt"; appendLog(command); From e6c720f793f79217fe11f99381b6caf878efdf3b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:12:31 -0700 Subject: [PATCH 055/135] Add AudioClient mixing gains for local injectors and system sounds --- libraries/audio-client/src/AudioClient.cpp | 4 +++- libraries/audio-client/src/AudioClient.h | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1c10d24f23..79811ac98f 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1366,7 +1366,9 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - float gain = injector->getVolume(); + bool isSystemSound = !injector->isPositionSet() && !injector->isAmbisonic(); + + float gain = injector->getVolume() * (isSystemSound ? _systemInjectorGain : _localInjectorGain); if (injector->isAmbisonic()) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index b9648219a5..6e1f48d5f4 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -239,6 +239,8 @@ public slots: void setInputVolume(float volume, bool emitSignal = true); void setReverb(bool reverb); void setReverbOptions(const AudioEffectOptions* options); + void setLocalInjectorGain(float gain) { _localInjectorGain = gain; }; + void setSystemInjectorGain(float gain) { _systemInjectorGain = gain; }; void outputNotify(); @@ -393,6 +395,8 @@ private: int16_t* _outputScratchBuffer { NULL }; // for local audio (used by audio injectors thread) + std::atomic _localInjectorGain { 1.0f }; + std::atomic _systemInjectorGain { 1.0f }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; From 23a6a66528ac68792594cd0cf7547737c7ddba94 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:21:54 -0700 Subject: [PATCH 056/135] Add local injector gains to the Audio scripting interface --- interface/src/scripting/Audio.cpp | 34 ++++++++++++++++++++++++++ interface/src/scripting/Audio.h | 40 +++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index e0474b7bba..7e1a35762a 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -423,3 +423,37 @@ float Audio::getInjectorGain() { return DependencyManager::get()->getInjectorGain(); }); } + +void Audio::setLocalInjectorGain(float gain) { + withWriteLock([&] { + if (_localInjectorGain != gain) { + _localInjectorGain = gain; + // convert dB to amplitude + gain = fastExp2f(gain / 6.02059991f); + DependencyManager::get()->setLocalInjectorGain(gain); + } + }); +} + +float Audio::getLocalInjectorGain() { + return resultWithReadLock([&] { + return _localInjectorGain; + }); +} + +void Audio::setSystemInjectorGain(float gain) { + withWriteLock([&] { + if (_systemInjectorGain != gain) { + _systemInjectorGain = gain; + // convert dB to amplitude + gain = fastExp2f(gain / 6.02059991f); + DependencyManager::get()->setSystemInjectorGain(gain); + } + }); +} + +float Audio::getSystemInjectorGain() { + return resultWithReadLock([&] { + return _systemInjectorGain; + }); +} diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 14a75d5ffe..d6823ea452 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -171,7 +171,7 @@ public: Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); /**jsdoc - * Sets the master avatar gain at the server. + * Sets the avatar gain at the server. * Units are Decibels (dB) * @function Audio.setAvatarGain * @param {number} gain (in dB) @@ -179,14 +179,14 @@ public: Q_INVOKABLE void setAvatarGain(float gain); /**jsdoc - * Gets the master avatar gain at the server. + * Gets the avatar gain at the server. * @function Audio.getAvatarGain * @returns {number} gain (in dB) */ Q_INVOKABLE float getAvatarGain(); /**jsdoc - * Sets the audio injector gain at the server. + * Sets the injector gain at the server. * Units are Decibels (dB) * @function Audio.setInjectorGain * @param {number} gain (in dB) @@ -194,12 +194,42 @@ public: Q_INVOKABLE void setInjectorGain(float gain); /**jsdoc - * Gets the audio injector gain at the server. + * Gets the injector gain at the server. * @function Audio.getInjectorGain * @returns {number} gain (in dB) */ Q_INVOKABLE float getInjectorGain(); + /**jsdoc + * Sets the local injector gain in the client. + * Units are Decibels (dB) + * @function Audio.setLocalInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setLocalInjectorGain(float gain); + + /**jsdoc + * Gets the local injector gain in the client. + * @function Audio.getLocalInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getLocalInjectorGain(); + + /**jsdoc + * Sets the injector gain for system sounds. + * Units are Decibels (dB) + * @function Audio.setSystemInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setSystemInjectorGain(float gain); + + /**jsdoc + * Gets the injector gain for system sounds. + * @function Audio.getSystemInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getSystemInjectorGain(); + /**jsdoc * Starts making an audio recording of the audio being played in-world (i.e., not local-only audio) to a file in WAV format. * @function Audio.startRecording @@ -380,6 +410,8 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; + float _localInjectorGain { 0.0f }; // in dB + float _systemInjectorGain { 0.0f }; // in dB bool _isClipping { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _enableWarnWhenMuted { true }; From e8ddee280d6e8c11f897791c7d7041a1edd1a7c7 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:24:30 -0700 Subject: [PATCH 057/135] Quantize and limit the local injector gains to match the network protocol --- interface/src/scripting/Audio.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 7e1a35762a..6dd1c40ef5 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -430,6 +430,8 @@ void Audio::setLocalInjectorGain(float gain) { _localInjectorGain = gain; // convert dB to amplitude gain = fastExp2f(gain / 6.02059991f); + // quantize and limit to match NodeList::setInjectorGain() + gain = unpackFloatGainFromByte(packFloatGainToByte(gain)); DependencyManager::get()->setLocalInjectorGain(gain); } }); @@ -447,6 +449,8 @@ void Audio::setSystemInjectorGain(float gain) { _systemInjectorGain = gain; // convert dB to amplitude gain = fastExp2f(gain / 6.02059991f); + // quantize and limit to match NodeList::setInjectorGain() + gain = unpackFloatGainFromByte(packFloatGainToByte(gain)); DependencyManager::get()->setSystemInjectorGain(gain); } }); From 77ea47a9dbbb49c626e3ccae79fbcd34645fffd1 Mon Sep 17 00:00:00 2001 From: David Back Date: Fri, 22 Mar 2019 11:21:39 -0700 Subject: [PATCH 058/135] fix no name material override, hide auto scale text after sliding --- .../Editor/AvatarExporter/AvatarExporter.cs | 21 +++++++++++------- tools/unity-avatar-exporter/Assets/README.txt | 2 +- .../avatarExporter.unitypackage | Bin 74615 -> 74729 bytes 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index 11d83a52e8..4e06772f4b 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -17,7 +17,7 @@ using System.Text.RegularExpressions; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.3.7"; + static readonly string AVATAR_EXPORTER_VERSION = "0.4.0"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; @@ -697,11 +697,10 @@ class AvatarExporter : MonoBehaviour { if (materialDatas.Count > 0) { string materialJson = "{ "; foreach (var materialData in materialDatas) { - // if this is the only material in the mapping and it is the default name No Name mapped to No Name, + // if this is the only material in the mapping and it is mapped to default material name No Name, // then the avatar has no embedded materials and this material should be applied to all meshes string materialName = materialData.Key; - if (materialMappings.Count == 1 && materialName == DEFAULT_MATERIAL_NAME && - materialMappings[materialName] == DEFAULT_MATERIAL_NAME) { + if (materialMappings.Count == 1 && materialName == DEFAULT_MATERIAL_NAME) { materialJson += "\"all\": "; } else { materialJson += "\"mat::" + materialName + "\": "; @@ -1300,6 +1299,7 @@ class ExportProjectWindow : EditorWindow { const float DEFAULT_AVATAR_HEIGHT = 1.755f; const float MAXIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; const float MINIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f; + const float SLIDER_DIFFERENCE_REMOVE_TEXT = 0.01f; readonly Color COLOR_YELLOW = Color.yellow; //new Color(0.9176f, 0.8274f, 0.0f); readonly Color COLOR_BACKGROUND = new Color(0.5f, 0.5f, 0.5f); @@ -1314,6 +1314,7 @@ class ExportProjectWindow : EditorWindow { Vector2 warningScrollPosition = new Vector2(0, 0); string scaleWarningText = ""; float sliderScale = 0.30103f; + float originalSliderScale; public delegate void OnExportDelegate(string projectDirectory, string projectName, float scale); OnExportDelegate onExportCallback; @@ -1344,6 +1345,8 @@ class ExportProjectWindow : EditorWindow { SetAvatarScale(newScale); scaleWarningText = "Avatar's scale automatically adjusted to be within the recommended range."; } + + originalSliderScale = sliderScale; } void OnGUI() { @@ -1460,10 +1463,9 @@ class ExportProjectWindow : EditorWindow { Close(); } - // when any value changes check for any errors and update scale warning if we are not exporting + // When a text field changes check for any errors if we didn't just check errors from clicking Export above if (GUI.changed && !export) { CheckForErrors(false); - UpdateScaleWarning(); } } @@ -1521,13 +1523,14 @@ class ExportProjectWindow : EditorWindow { } void UpdateScaleWarning() { - // called on any input changes + // called on any scale changes float height = GetAvatarHeight(); if (height < MINIMUM_RECOMMENDED_HEIGHT) { scaleWarningText = "The height of the avatar is below the recommended minimum."; } else if (height > MAXIMUM_RECOMMENDED_HEIGHT) { scaleWarningText = "The height of the avatar is above the recommended maximum."; - } else { + } else if (Mathf.Abs(originalSliderScale - sliderScale) > SLIDER_DIFFERENCE_REMOVE_TEXT) { + // once moving slider beyond a small threshold, remove the automatically scaled text scaleWarningText = ""; } } @@ -1555,6 +1558,8 @@ class ExportProjectWindow : EditorWindow { // adjust slider scale value to match the new actual scale value sliderScale = GetSliderScaleFromActualScale(actualScale); + + UpdateScaleWarning(); } float GetSliderScaleFromActualScale(float actualScale) { diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 0b5cb49117..410314d8b4 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,6 +1,6 @@ High Fidelity, Inc. Avatar Exporter -Version 0.3.7 +Version 0.4.0 Note: It is recommended to use Unity versions between 2017.4.15f1 and 2018.2.12f1 for this Avatar Exporter. diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index d906cfc0a45055b79a0ed8667441799fea682af3..328972736bea1c62716589eb2c0cad5171001f2d 100644 GIT binary patch delta 74262 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%0DP)&)c5gO^Ga*>!vx;Z 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

    jV;9GJz8I}aM(Zh=9i<)~Fn3Taj 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ફ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 From faf11480b639e1b56e3a740b9223651376c51459 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 22 Mar 2019 11:22:14 -0700 Subject: [PATCH 059/135] Working changes before merge. --- tools/nitpick/src/Nitpick.cpp | 2 +- tools/nitpick/src/TestRunnerMobile.cpp | 12 ++++++------ tools/nitpick/src/TestRunnerMobile.h | 2 +- tools/nitpick/src/common.h | 1 + 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index e72de9d1ad..e6894ee883 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.3"); + setWindowTitle("Nitpick - " + nitpickVersion); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 105d30fca4..a848c94755 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -211,8 +211,6 @@ void TestRunnerMobile::runInterface() { _adbInterface = new AdbInterface(); } - sendServerIPToDevice(); - _statusLabel->setText("Starting Interface"); QString testScript = (_runFullSuite->isChecked()) @@ -230,7 +228,7 @@ void TestRunnerMobile::runInterface() { QString command = _adbInterface->getAdbCommand() + " shell am start -n " + startCommand + " --es args \\\"" + - " --url file:///~/serverless/tutorial.json" + + " --url hifi://" + getServerIP() + "/0,0,0" " --no-updater" + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + @@ -257,8 +255,8 @@ void TestRunnerMobile::pullFolder() { #endif } -void TestRunnerMobile::sendServerIPToDevice() { - // Get device IP +QString TestRunnerMobile::getServerIP() { + // Get device IP (ifconfig.txt was created when connecting) QFile ifconfigFile{ _workingFolder + "/ifconfig.txt" }; if (!ifconfigFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), @@ -309,7 +307,7 @@ void TestRunnerMobile::sendServerIPToDevice() { if (!serverIP.isNull()) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Cannot identify server IP (multiple interfaces on device submask)"); - return; + return QString("CANNOT IDENTIFY SERVER IP"); } else { union { uint32_t ip; @@ -325,6 +323,8 @@ void TestRunnerMobile::sendServerIPToDevice() { } ifconfigFile.close(); + + return serverIP; } qint64 TestRunnerMobile::convertToBinary(const QString& str) { diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 7554a075c8..b94cd47647 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -52,7 +52,7 @@ public: void pullFolder(); - void sendServerIPToDevice(); + QString getServerIP(); qint64 convertToBinary (const QString& str); private: diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index 17090c46db..09bf23fdfc 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -60,4 +60,5 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; +const QString nitpickVersion{ "21660" }; #endif // hifi_common_h \ No newline at end of file From c1c45d8a014627c2da1fcda7ada05267a5534e96 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 22 Mar 2019 11:41:02 -0700 Subject: [PATCH 060/135] revert some now unnecessary changes that were impacting performance (cherry picked from commit b6984de16c2fd17f04ea72de7b339c31db6467ab) --- .../render-utils/src/CauterizedModel.cpp | 1 - libraries/render-utils/src/Model.cpp | 20 +++++++------------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 6ec69b5e20..7cad337dd5 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -178,7 +178,6 @@ void CauterizedModel::updateClusterMatrices() { } } } - computeMeshPartLocalBounds(); // post the blender if we're not currently waiting for one to finish auto modelBlender = DependencyManager::get(); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 30c4000bc7..c0dbf16f9d 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1346,19 +1346,14 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { } void Model::computeMeshPartLocalBounds() { - render::Transaction transaction; - auto meshStates = _meshStates; - for (auto renderItem : _modelMeshRenderItemIDs) { - transaction.updateItem(renderItem, [this, meshStates](ModelMeshPartPayload& data) { - const Model::MeshState& state = meshStates.at(data._meshIndex); - if (_useDualQuaternionSkinning) { - data.computeAdjustedLocalBound(state.clusterDualQuaternions); - } else { - data.computeAdjustedLocalBound(state.clusterMatrices); - } - }); + for (auto& part : _modelMeshRenderItems) { + const Model::MeshState& state = _meshStates.at(part->_meshIndex); + if (_useDualQuaternionSkinning) { + part->computeAdjustedLocalBound(state.clusterDualQuaternions); + } else { + part->computeAdjustedLocalBound(state.clusterMatrices); + } } - AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); } // virtual @@ -1391,7 +1386,6 @@ void Model::updateClusterMatrices() { } } } - computeMeshPartLocalBounds(); // post the blender if we're not currently waiting for one to finish auto modelBlender = DependencyManager::get(); From a1660dad9558758a7c0ad1ef42a5f62739262f7b Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 22 Mar 2019 12:30:49 -0700 Subject: [PATCH 061/135] Follow dynamic updates to hero zones; make reserved fraction a domain setting --- assignment-client/src/avatars/AvatarMixer.cpp | 69 ++++++++++++++++--- assignment-client/src/avatars/AvatarMixer.h | 11 ++- .../src/avatars/AvatarMixerClientData.cpp | 17 ++--- .../src/avatars/AvatarMixerSlave.cpp | 7 +- .../src/avatars/AvatarMixerSlave.h | 4 +- .../src/avatars/AvatarMixerSlavePool.cpp | 3 +- .../src/avatars/AvatarMixerSlavePool.h | 9 ++- assignment-client/src/avatars/MixerAvatar.h | 4 ++ .../resources/describe-settings.json | 9 +++ 9 files changed, 104 insertions(+), 29 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 33e1034128..ffe084bc33 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -253,10 +253,26 @@ void AvatarMixer::start() { int lockWait, nodeTransform, functor; + // Set our query each frame { _entityViewer.queryOctree(); } + // Dirty the hero status if there's been an entity change. + { + if (_dirtyHeroStatus) { + _dirtyHeroStatus = false; + nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + std::for_each(cbegin, cend, [](const SharedNodePointer& node) { + if (node->getType() == NodeType::Agent) { + auto& avatar = static_cast(node->getLinkedData())->getAvatar(); + avatar.setNeedsHeroCheck(); + } + }); + }); + } + } + // Allow nodes to process any pending/queued packets across our worker threads { auto start = usecTimestampNow(); @@ -973,19 +989,31 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { { const QString CONNECTION_RATE = "connection_rate"; auto nodeList = DependencyManager::get(); - auto defaultConnectionRate = nodeList->getMaxConnectionRate(); - int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toInt((int)defaultConnectionRate); - nodeList->setMaxConnectionRate(connectionRate); + bool success; + int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toString().toInt(&success); + if (success) { + nodeList->setMaxConnectionRate(connectionRate); + } + } + + { // Fraction of downstream bandwidth reserved for 'hero' avatars: + static const QString PRIORITY_FRACTION_KEY = "priority_fraction"; + if (avatarMixerGroupObject.contains(PRIORITY_FRACTION_KEY)) { + bool isDouble = avatarMixerGroupObject[PRIORITY_FRACTION_KEY].isDouble(); + float priorityFraction = float(avatarMixerGroupObject[PRIORITY_FRACTION_KEY].toDouble()); + _slavePool.setPriorityReservedFraction(std::min(std::max(0.0f, priorityFraction), 1.0f)); + qCDebug(avatars) << "Avatar mixer reserving" << priorityFraction << "of bandwidth for priority avatars"; + } } const QString AVATARS_SETTINGS_KEY = "avatars"; static const QString MIN_HEIGHT_OPTION = "min_avatar_height"; - float settingMinHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); + float settingMinHeight = avatarMixerGroupObject[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); _domainMinimumHeight = glm::clamp(settingMinHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); static const QString MAX_HEIGHT_OPTION = "max_avatar_height"; - float settingMaxHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); + float settingMaxHeight = avatarMixerGroupObject[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); _domainMaximumHeight = glm::clamp(settingMaxHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); // make sure that the domain owner didn't flip min and max @@ -997,11 +1025,11 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { << "and a maximum avatar height of" << _domainMaximumHeight; static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist"; - _slaveSharedData.skeletonURLWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION] + _slaveSharedData.skeletonURLWhitelist = avatarMixerGroupObject[AVATAR_WHITELIST_OPTION] .toString().split(',', QString::KeepEmptyParts); static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar"; - _slaveSharedData.skeletonReplacementURL = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION] + _slaveSharedData.skeletonReplacementURL = avatarMixerGroupObject[REPLACEMENT_AVATAR_OPTION] .toString(); if (_slaveSharedData.skeletonURLWhitelist.count() == 1 && _slaveSharedData.skeletonURLWhitelist[0].isEmpty()) { @@ -1018,9 +1046,12 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { void AvatarMixer::setupEntityQuery() { _entityViewer.init(); + EntityTreePointer entityTree = _entityViewer.getTree(); DependencyManager::registerInheritance(); - DependencyManager::set(_entityViewer.getTree()); - _slaveSharedData.entityTree = _entityViewer.getTree(); + DependencyManager::set(entityTree); + + connect(entityTree.get(), &EntityTree::addingEntityPointer, this, &AvatarMixer::entityAdded); + connect(entityTree.get(), &EntityTree::deletingEntityPointer, this, &AvatarMixer::entityChange); // ES query: {"avatarPriority": true, "type": "Zone"} QJsonObject priorityZoneQuery; @@ -1028,6 +1059,7 @@ void AvatarMixer::setupEntityQuery() { priorityZoneQuery["type"] = "Zone"; _entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery); + _slaveSharedData.entityTree = entityTree; } void AvatarMixer::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { @@ -1064,6 +1096,25 @@ void AvatarMixer::handleOctreePacket(QSharedPointer message, Sh } } +void AvatarMixer::entityAdded(EntityItem* entity) { + if (entity->getType() == EntityTypes::Zone) { + _dirtyHeroStatus = true; + entity->registerChangeHandler([this](const EntityItemID& entityItemID) { + this->entityChange(); + }); + } +} + +void AvatarMixer::entityRemoved(EntityItem * entity) { + if (entity->getType() == EntityTypes::Zone) { + _dirtyHeroStatus = true; + } +} + +void AvatarMixer::entityChange() { + _dirtyHeroStatus = true; +} + void AvatarMixer::aboutToFinish() { DependencyManager::destroy(); DependencyManager::destroy(); diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 9393ea6c56..f65f04f279 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -34,8 +34,8 @@ public: static bool shouldReplicateTo(const Node& from, const Node& to) { return to.getType() == NodeType::DownstreamAvatarMixer && - to.getPublicSocket() != from.getPublicSocket() && - to.getLocalSocket() != from.getLocalSocket(); + to.getPublicSocket() != from.getPublicSocket() && + to.getLocalSocket() != from.getLocalSocket(); } public slots: @@ -80,6 +80,7 @@ private: // Attach to entity tree for avatar-priority zone info. EntityTreeHeadlessViewer _entityViewer; + bool _dirtyHeroStatus { true }; // Dirty the needs-hero-update // FIXME - new throttling - use these values somehow float _trailingMixRatio { 0.0f }; @@ -146,6 +147,12 @@ private: AvatarMixerSlavePool _slavePool; SlaveSharedData _slaveSharedData; + +public slots: + // Avatar zone possibly changed + void entityAdded(EntityItem* entity); + void entityRemoved(EntityItem* entity); + void entityChange(); }; #endif // hifi_AvatarMixer_h diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 557c5c9fe3..a6b675efa4 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -141,22 +141,15 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); auto newPosition = getPosition(); - if (newPosition != oldPosition) { -//#define AVATAR_HERO_TEST_HACK -#ifdef AVATAR_HERO_TEST_HACK - { - const static QString heroKey { "HERO" }; - _avatar->setPriorityAvatar(_avatar->getDisplayName().contains(heroKey)); - } -#else + if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; FindPriorityZone findPriorityZone { newPosition, false } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); - //if (findPriorityZone.isInPriorityZone) { - // qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; - //} -#endif + _avatar->setNeedsHeroCheck(false); + if (findPriorityZone.isInPriorityZone) { + qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; + } } return true; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index e59c81f4b7..e7de764708 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -43,12 +43,14 @@ void AvatarMixerSlave::configure(ConstIter begin, ConstIter end) { void AvatarMixerSlave::configureBroadcast(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, - float maxKbpsPerNode, float throttlingRatio) { + float maxKbpsPerNode, float throttlingRatio, + float priorityReservedFraction) { _begin = begin; _end = end; _lastFrameTimestamp = lastFrameTimestamp; _maxKbpsPerNode = maxKbpsPerNode; _throttlingRatio = throttlingRatio; + _avatarHeroFraction = priorityReservedFraction; } void AvatarMixerSlave::harvestStats(AvatarMixerSlaveStats& stats) { @@ -308,7 +310,6 @@ namespace { } // Close anonymous namespace. void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) { - const float AVATAR_HERO_FRACTION { 0.4f }; const Node* destinationNode = node.data(); auto nodeList = DependencyManager::get(); @@ -343,7 +344,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // max number of avatarBytes per frame (13 900, typical) const int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); - const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * AVATAR_HERO_FRACTION); // 5555, typical + const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * _avatarHeroFraction); // 5555, typical // keep track of the number of other avatars held back in this frame int numAvatarsHeldBack = 0; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.h b/assignment-client/src/avatars/AvatarMixerSlave.h index 8c5ad6b181..f14e50e11f 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.h +++ b/assignment-client/src/avatars/AvatarMixerSlave.h @@ -110,7 +110,8 @@ public: void configure(ConstIter begin, ConstIter end); void configureBroadcast(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, - float maxKbpsPerNode, float throttlingRatio); + float maxKbpsPerNode, float throttlingRatio, + float priorityReservedFraction); void processIncomingPackets(const SharedNodePointer& node); void broadcastAvatarData(const SharedNodePointer& node); @@ -140,6 +141,7 @@ private: p_high_resolution_clock::time_point _lastFrameTimestamp; float _maxKbpsPerNode { 0.0f }; float _throttlingRatio { 0.0f }; + float _avatarHeroFraction { 0.4f }; AvatarMixerSlaveStats _stats; SlaveSharedData* _sharedData; diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index 013d914cbe..357b347a94 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -76,7 +76,8 @@ void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end, float maxKbpsPerNode, float throttlingRatio) { _function = &AvatarMixerSlave::broadcastAvatarData; _configure = [=](AvatarMixerSlave& slave) { - slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio); + slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio, + _priorityReservedFraction); }; run(begin, end); } diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h index 71a9ace0d3..b05abde2a3 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.h +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h @@ -73,7 +73,10 @@ public: void each(std::function functor); void setNumThreads(int numThreads); - int numThreads() { return _numThreads; } + int numThreads() const { return _numThreads; } + + void setPriorityReservedFraction(float fraction) { _priorityReservedFraction = fraction; } + float getPriorityReservedFraction() const { return _priorityReservedFraction; } private: void run(ConstIter begin, ConstIter end); @@ -91,7 +94,11 @@ private: ConditionVariable _poolCondition; void (AvatarMixerSlave::*_function)(const SharedNodePointer& node); std::function _configure; + + // Set from Domain Settings: + float _priorityReservedFraction { 0.4f }; int _numThreads { 0 }; + int _numStarted { 0 }; // guarded by _mutex int _numFinished { 0 }; // guarded by _mutex int _numStopped { 0 }; // guarded by _mutex diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 3e80704495..01e5e91b44 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -19,8 +19,12 @@ class MixerAvatar : public AvatarData { public: + bool getNeedsHeroCheck() const { return _needsHeroCheck; } + void setNeedsHeroCheck(bool needsHeroCheck = true) + { _needsHeroCheck = needsHeroCheck; } private: + bool _needsHeroCheck { false }; }; using MixerAvatarSharedPointer = std::shared_ptr; diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 140c7d6c17..352106dcf7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1310,6 +1310,15 @@ "placeholder": "50", "default": "50", "advanced": true + }, + { + "name": "priority_fraction", + "type": "double", + "label": "Hero Bandwidth", + "help": "Fraction of downstream bandwidth reserved for avatars in 'Hero' zones", + "placeholder": "0.40", + "default": "0.40", + "advanced": true } ] }, From 137c25f907711cc3f926d06b40c463d06518e004 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 22 Mar 2019 15:40:13 -0700 Subject: [PATCH 062/135] Use a 1 m offset for position test; call-out nonavatars in stats web page --- assignment-client/src/avatars/AvatarMixer.cpp | 8 +++++++- assignment-client/src/avatars/AvatarMixerClientData.cpp | 8 ++++---- assignment-client/src/avatars/AvatarMixerClientData.h | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index ffe084bc33..c6cd12f30a 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -843,7 +843,7 @@ void AvatarMixer::sendStatsPacket() { QJsonObject avatarsObject; auto nodeList = DependencyManager::get(); - // add stats for each listerner + // add stats for each listener nodeList->eachNode([&](const SharedNodePointer& node) { QJsonObject avatarStats; @@ -867,6 +867,12 @@ void AvatarMixer::sendStatsPacket() { avatarStats["delta_full_vs_avatar_data_kbps"] = (double)outboundAvatarDataKbps - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble(); } + + if (node->getType() != NodeType::Agent) { // Nodes that aren't avatars + const QString displayName + { node->getType() == NodeType::EntityScriptServer ? "ENTITY SCRIPT SERVER" : "ENTITY SERVER" }; + avatarStats["display_name"] = displayName; + } } avatarsObject[uuidStringWithoutCurlyBraces(node->getUUID())] = avatarStats; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index a6b675efa4..4880f73226 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -23,6 +23,9 @@ #include "AvatarMixerSlave.h" +// Offset from reported position for priority-zone purposes: +const glm::vec3 AvatarMixerClientData::AVATAR_CENTER_OFFSET { 0.0f, 1.0f, 0.0 }; + AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) : NodeData(nodeID, nodeLocalID) { // in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID @@ -143,13 +146,10 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared auto newPosition = getPosition(); if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; - FindPriorityZone findPriorityZone { newPosition, false } ; + FindPriorityZone findPriorityZone { newPosition + AVATAR_CENTER_OFFSET } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); _avatar->setNeedsHeroCheck(false); - if (findPriorityZone.isInPriorityZone) { - qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; - } } return true; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 98c8d7e15b..492dfc4720 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -223,6 +223,7 @@ private: PerNodeTraitVersions _perNodeSentTraitVersions; std::atomic_bool _isIgnoreRadiusEnabled { false }; + static const glm::vec3 AVATAR_CENTER_OFFSET; }; #endif // hifi_AvatarMixerClientData_h From 55b3b5034a0dad81c70dc499033ac478fc3d6a4a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 22 Mar 2019 15:47:13 -0700 Subject: [PATCH 063/135] always update OwingAvatarID of AvatarEntities --- interface/src/avatar/MyAvatar.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 298e661f24..4ea0d37710 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3454,11 +3454,6 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { QUuid oldSessionID = getSessionUUID(); Avatar::setSessionUUID(sessionUUID); QUuid newSessionID = getSessionUUID(); - if (DependencyManager::get()->getSessionUUID().isNull()) { - // we don't actually have a connection to a domain right now - // so there is no need to queue AvatarEntity messages --> bail early - return; - } if (newSessionID != oldSessionID) { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; @@ -3467,6 +3462,7 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { _avatarEntitiesLock.withReadLock([&] { avatarEntityIDs = _packedAvatarEntityData.keys(); }); + bool sendPackets = DependencyManager::get()->getSessionUUID().isNull(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); entityTree->withWriteLock([&] { for (const auto& entityID : avatarEntityIDs) { @@ -3474,12 +3470,14 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { if (!entity) { continue; } + // update OwningAvatarID so entity can be identified as "ours" later entity->setOwningAvatarID(newSessionID); - // NOTE: each attached AvatarEntity should already have the correct updated parentID - // via magic in SpatiallyNestable, but when an AvatarEntity IS parented to MyAvatar - // we need to update the "packedAvatarEntityData" we send to the avatar-mixer - // so that others will get the updated state. - if (entity->getParentID() == newSessionID) { + // NOTE: each attached AvatarEntity already have the correct updated parentID + // via magic in SpatiallyNestable, hence we check agains newSessionID + if (sendPackets && entity->getParentID() == newSessionID) { + // but when we have a real session and the AvatarEntity is parented to MyAvatar + // we need to update the "packedAvatarEntityData" sent to the avatar-mixer + // because it contains a stale parentID somewhere deep inside packetSender->queueEditAvatarEntityMessage(entityTree, entityID); } } From 7db498a1308d9147613ae8b1be79845d0647cb67 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 22 Mar 2019 16:20:54 -0700 Subject: [PATCH 064/135] fix shadows when tablet is open and misc threading issues --- .../src/RenderableMaterialEntityItem.cpp | 6 +++++- .../src/RenderableShapeEntityItem.cpp | 18 ++++++++++++++---- .../src/RenderableTextEntityItem.cpp | 8 +++----- libraries/render-utils/src/GeometryCache.h | 11 ++++++----- .../render-utils/src/RenderCommonTask.cpp | 6 +++++- .../render-utils/src/RenderDeferredTask.cpp | 8 ++++---- .../render-utils/src/RenderDeferredTask.h | 10 +++++----- .../render-utils/src/RenderForwardTask.cpp | 10 +++++++--- libraries/render-utils/src/RenderForwardTask.h | 3 ++- .../src/simple_transparent_textured.slf | 4 ++-- 10 files changed, 53 insertions(+), 31 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 2eb877b0e1..15842336f5 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -186,7 +186,11 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (_networkMaterial->isLoaded()) { onMaterialRequestFinished(!_networkMaterial->isFailed()); } else { - connect(_networkMaterial.data(), &Resource::finished, this, onMaterialRequestFinished); + connect(_networkMaterial.data(), &Resource::finished, this, [&](bool success) { + withWriteLock([&] { + onMaterialRequestFinished(success); + }); + }); } } } else if (materialDataChanged && usingMaterialData) { diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index b33eb619c8..28b0bb8f23 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -249,10 +249,14 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { graphics::MultiMaterial materials; auto geometryCache = DependencyManager::get(); GeometryCache::Shape geometryShape; + PrimitiveMode primitiveMode; + RenderLayer renderLayer; bool proceduralRender = false; glm::vec4 outColor; withReadLock([&] { geometryShape = geometryCache->getShapeForEntityShape(_shape); + primitiveMode = _primitiveMode; + renderLayer = _renderLayer; batch.setModelTransform(_renderTransform); // use a transform with scale, rotation, registration point and translation materials = _materials["0"]; auto& schema = materials.getSchemaBuffer().get(); @@ -267,7 +271,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { }); if (proceduralRender) { - if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { + if (render::ShapeKey(args->_globalShapeKey).isWireframe() || primitiveMode == PrimitiveMode::LINES) { geometryCache->renderWireShape(batch, geometryShape, outColor); } else { geometryCache->renderShape(batch, geometryShape, outColor); @@ -275,10 +279,16 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { } else if (!useMaterialPipeline(materials)) { // FIXME, support instanced multi-shape rendering using multidraw indirect outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; - if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) { - geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, args->_shapePipeline); + render::ShapePipelinePointer pipeline; + if (renderLayer == RenderLayer::WORLD) { + pipeline = GeometryCache::getShapePipeline(false, outColor.a < 1.0f, true, false); } else { - geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, args->_shapePipeline); + pipeline = GeometryCache::getShapePipeline(false, outColor.a < 1.0f, true, false, false, true); + } + if (render::ShapeKey(args->_globalShapeKey).isWireframe() || primitiveMode == PrimitiveMode::LINES) { + geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); + } else { + geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); } } else { if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index a3e1a2f56d..81f367a956 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -162,10 +162,12 @@ void TextEntityRenderer::doRender(RenderArgs* args) { glm::vec4 backgroundColor; Transform modelTransform; glm::vec3 dimensions; + BillboardMode billboardMode; bool layered; withReadLock([&] { modelTransform = _renderTransform; dimensions = _dimensions; + billboardMode = _billboardMode; float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; textColor = glm::vec4(_textColor, fadeRatio * _textAlpha); @@ -190,7 +192,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { } auto transformToTopLeft = modelTransform; - transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), _billboardMode, args->getViewFrustum().getPosition())); + transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), billboardMode, args->getViewFrustum().getPosition())); transformToTopLeft.postTranslate(dimensions * glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left transformToTopLeft.setScale(1.0f); // Use a scale of one so that the text is not deformed @@ -210,10 +212,6 @@ void TextEntityRenderer::doRender(RenderArgs* args) { glm::vec2 bounds = glm::vec2(dimensions.x - (_leftMargin + _rightMargin), dimensions.y - (_topMargin + _bottomMargin)); _textRenderer->draw(batch, _leftMargin / scale, -_topMargin / scale, _text, textColor, bounds / scale, layered); } - - if (layered) { - DependencyManager::get()->unsetKeyLightBatch(batch); - } } QSizeF TextEntityRenderer::textSize(const QString& text) const { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index cd3454bf38..e84f2e25a4 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -368,6 +368,12 @@ public: const ShapeData * getShapeData(Shape shape) const; graphics::MeshPointer meshFromShape(Shape geometryShape, glm::vec3 color); + + static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true, + bool unlit = false, bool depthBias = false, bool forward = false); + static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true, + bool unlit = false, bool depthBias = false); + private: GeometryCache(); @@ -471,11 +477,6 @@ private: gpu::PipelinePointer _simpleOpaqueWebBrowserPipeline; gpu::ShaderPointer _simpleTransparentWebBrowserShader; gpu::PipelinePointer _simpleTransparentWebBrowserPipeline; - - static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool forward = false); - static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false); }; #endif // hifi_GeometryCache_h diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index 385e384efe..b1a62625b2 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -95,7 +95,11 @@ void DrawLayered3D::run(const RenderContextPointer& renderContext, const Inputs& // Setup lighting model for all items; batch.setUniformBuffer(ru::Buffer::LightModel, lightingModel->getParametersBuffer()); - renderShapes(renderContext, _shapePlumber, inItems, _maxDrawn); + if (_opaquePass) { + renderStateSortShapes(renderContext, _shapePlumber, inItems, _maxDrawn); + } else { + renderShapes(renderContext, _shapePlumber, inItems, _maxDrawn); + } args->_batch = nullptr; }); } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 089d267711..ea2b05a6fa 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -216,8 +216,8 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("DrawHazeDeferred", drawHazeInputs); // Render transparent objects forward in LightingBuffer - const auto transparentsInputs = DrawDeferred::Inputs(transparents, hazeFrame, lightFrame, lightingModel, lightClusters, shadowFrame, jitter).asVarying(); - task.addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); + const auto transparentsInputs = RenderTransparentDeferred::Inputs(transparents, hazeFrame, lightFrame, lightingModel, lightClusters, shadowFrame, jitter).asVarying(); + task.addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); const auto outlineRangeTimer = task.addJob("BeginHighlightRangeTimer", "Highlight"); @@ -436,7 +436,7 @@ void RenderDeferredTaskDebug::build(JobModel& task, const render::Varying& input } -void DrawDeferred::run(const RenderContextPointer& renderContext, const Inputs& inputs) { +void RenderTransparentDeferred::run(const RenderContextPointer& renderContext, const Inputs& inputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -453,7 +453,7 @@ void DrawDeferred::run(const RenderContextPointer& renderContext, const Inputs& RenderArgs* args = renderContext->args; - gpu::doInBatch("DrawDeferred::run", args->_context, [&](gpu::Batch& batch) { + gpu::doInBatch("RenderTransparentDeferred::run", args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; // Setup camera, projection and viewport for all items diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 0a188ec3a6..3eb1153928 100644 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -19,7 +19,7 @@ #include "LightClusters.h" #include "RenderShadowTask.h" -class DrawDeferredConfig : public render::Job::Config { +class RenderTransparentDeferredConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY newStats) Q_PROPERTY(int maxDrawn MEMBER maxDrawn NOTIFY dirty) @@ -41,13 +41,13 @@ protected: int _numDrawn{ 0 }; }; -class DrawDeferred { +class RenderTransparentDeferred { public: using Inputs = render::VaryingSet7; - using Config = DrawDeferredConfig; - using JobModel = render::Job::ModelI; + using Config = RenderTransparentDeferredConfig; + using JobModel = render::Job::ModelI; - DrawDeferred(render::ShapePlumberPointer shapePlumber) + RenderTransparentDeferred(render::ShapePlumberPointer shapePlumber) : _shapePlumber{ shapePlumber } {} void configure(const Config& config) { _maxDrawn = config.maxDrawn; } diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 73692b41c2..0bc117bdb9 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -98,7 +98,7 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend // Draw opaques forward const auto opaqueInputs = DrawForward::Inputs(opaques, lightingModel).asVarying(); - task.addJob("DrawOpaques", opaqueInputs, shapePlumber); + task.addJob("DrawOpaques", opaqueInputs, shapePlumber, true); // Similar to light stage, background stage has been filled by several potential render items and resolved for the frame in this job const auto backgroundInputs = DrawBackgroundStage::Inputs(lightingModel, backgroundFrame).asVarying(); @@ -106,7 +106,7 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend // Draw transparent objects forward const auto transparentInputs = DrawForward::Inputs(transparents, lightingModel).asVarying(); - task.addJob("DrawTransparents", transparentInputs, shapePlumber); + task.addJob("DrawTransparents", transparentInputs, shapePlumber, false); // Layered const auto nullJitter = Varying(glm::vec2(0.0f, 0.0f)); @@ -261,7 +261,11 @@ void DrawForward::run(const RenderContextPointer& renderContext, const Inputs& i args->_globalShapeKey = globalKey._flags.to_ulong(); // Render items - renderStateSortShapes(renderContext, _shapePlumber, inItems, -1, globalKey); + if (_opaquePass) { + renderStateSortShapes(renderContext, _shapePlumber, inItems, -1, globalKey); + } else { + renderShapes(renderContext, _shapePlumber, inItems, -1, globalKey); + } args->_batch = nullptr; args->_globalShapeKey = 0; diff --git a/libraries/render-utils/src/RenderForwardTask.h b/libraries/render-utils/src/RenderForwardTask.h index 85b51ad5fa..40d004ddb2 100755 --- a/libraries/render-utils/src/RenderForwardTask.h +++ b/libraries/render-utils/src/RenderForwardTask.h @@ -76,12 +76,13 @@ public: using Inputs = render::VaryingSet2; using JobModel = render::Job::ModelI; - DrawForward(const render::ShapePlumberPointer& shapePlumber) : _shapePlumber(shapePlumber) {} + DrawForward(const render::ShapePlumberPointer& shapePlumber, bool opaquePass) : _shapePlumber(shapePlumber), _opaquePass(opaquePass) {} void run(const render::RenderContextPointer& renderContext, const Inputs& inputs); private: render::ShapePlumberPointer _shapePlumber; + bool _opaquePass; }; #endif // hifi_RenderForwardTask_h diff --git a/libraries/render-utils/src/simple_transparent_textured.slf b/libraries/render-utils/src/simple_transparent_textured.slf index f1bb2b1ea2..9f8a88c7c2 100644 --- a/libraries/render-utils/src/simple_transparent_textured.slf +++ b/libraries/render-utils/src/simple_transparent_textured.slf @@ -16,8 +16,8 @@ <@include gpu/Color.slh@> <@include render-utils/ShaderConstants.h@> -<@include ForwardGlobalLight.slh@> -<$declareEvalGlobalLightingAlphaBlended()$> +<@include DeferredGlobalLight.slh@> +<$declareEvalGlobalLightingAlphaBlendedWithHaze()$> <@include gpu/Transform.slh@> <$declareStandardCameraTransform()$> From d7a1ecdbb3ee5b808acb4f2d95b104e72eb568ca Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 22 Mar 2019 17:19:39 -0700 Subject: [PATCH 065/135] Expose hero-status to scripts --- interface/src/avatar/MyAvatar.h | 1 + libraries/avatars/src/AvatarData.cpp | 7 ++++--- libraries/avatars/src/AvatarData.h | 2 ++ libraries/avatars/src/ScriptAvatarData.cpp | 8 ++++++++ libraries/avatars/src/ScriptAvatarData.h | 4 ++++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index aadc8ee268..ccfa629fea 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -184,6 +184,7 @@ class MyAvatar : public Avatar { * @property {Mat4} controllerLeftHandMatrix Read-only. * @property {Mat4} controllerRightHandMatrix Read-only. * @property {number} sensorToWorldScale Read-only. + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only */ // FIXME: `glm::vec3 position` is not accessible from QML, so this exposes position in a QML-native type Q_PROPERTY(QVector3D qmlPosition READ getQmlPosition) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 26407c3564..3355228eea 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1143,10 +1143,11 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // we store the hand state as well as other items in a shared bitset. The hand state is an octal, but is split // into two sections to maintain backward compatibility. The bits are ordered as such (0-7 left to right). // AA 6/1/18 added three more flags bits 8,9, and 10 for procedural audio, blink, and eye saccade enabled - // +---+-----+-----+--+--+--+--+-----+ - // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|xxxxx| - // +---+-----+-----+--+--+--+--+-----+ + // +---+-----+-----+--+--+--+--+--+----+ + // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|He|xxxx| + // +---+-----+-----+--+--+--+--+--+----+ // Hand state - H0,H1,H2 is found in the 3rd, 4th, and 8th bits + // Hero-avatar status (He) - 12th bit auto newHandState = getSemiNibbleAt(bitItems, HAND_STATE_START_BIT) + (oneAtBit16(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 95bbcbeb16..424384c15e 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -453,6 +453,8 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float sensorToWorldScale READ getSensorToWorldScale) + Q_PROPERTY(bool hasPriority READ getHasPriority) + public: virtual QString getName() const override { return QString("Avatar:") + _displayName; } diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index a716a40ad8..18717c8ca3 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -343,6 +343,14 @@ glm::mat4 ScriptAvatarData::getControllerRightHandMatrix() const { // END // +bool ScriptAvatarData::getHasPriority() const { + if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { + return sharedAvatarData->getHasPriority(); + } else { + return false; + } +} + glm::quat ScriptAvatarData::getAbsoluteJointRotationInObjectFrame(int index) const { if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { return sharedAvatarData->getAbsoluteJointRotationInObjectFrame(index); diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 91bac61728..01f7ff360a 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -68,6 +68,8 @@ class ScriptAvatarData : public QObject { Q_PROPERTY(glm::mat4 controllerLeftHandMatrix READ getControllerLeftHandMatrix) Q_PROPERTY(glm::mat4 controllerRightHandMatrix READ getControllerRightHandMatrix) + Q_PROPERTY(bool hasPriority READ getHasPriority) + public: ScriptAvatarData(AvatarSharedPointer avatarData); @@ -133,6 +135,8 @@ public: glm::mat4 getControllerLeftHandMatrix() const; glm::mat4 getControllerRightHandMatrix() const; + bool getHasPriority() const; + signals: void displayNameChanged(); void sessionDisplayNameChanged(); From e671a124c3c5d6a14510111417f6346c81d8efe4 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 17:58:17 -0700 Subject: [PATCH 066/135] Cleanup --- assignment-client/src/audio/AudioMixerClientData.cpp | 6 +++--- interface/src/scripting/Audio.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index b8d3ec62a6..41b72c04d2 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -200,11 +200,11 @@ void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const if (avatarUUID.isNull()) { // set the MASTER avatar gain setMasterAvatarGain(gain); - qCDebug(audio) << "Setting MASTER avatar gain for " << uuid << " to " << gain; + qCDebug(audio) << "Setting MASTER avatar gain for" << uuid << "to" << gain; } else { // set the per-source avatar gain setGainForAvatar(avatarUUID, gain); - qCDebug(audio) << "Setting avatar gain adjustment for hrtf[" << uuid << "][" << avatarUUID << "] to " << gain; + qCDebug(audio) << "Setting avatar gain adjustment for hrtf[" << uuid << "][" << avatarUUID << "] to" << gain; } } @@ -216,7 +216,7 @@ void AudioMixerClientData::parseInjectorGainSet(ReceivedMessage& message, const float gain = unpackFloatGainFromByte(packedGain); setMasterInjectorGain(gain); - qCDebug(audio) << "Setting MASTER injector gain for " << uuid << " to " << gain; + qCDebug(audio) << "Setting MASTER injector gain for" << uuid << "to" << gain; } void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) { diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 6dd1c40ef5..ac5ddd6a6a 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -401,13 +401,13 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { void Audio::setAvatarGain(float gain) { withWriteLock([&] { // ask the NodeList to set the master avatar gain - DependencyManager::get()->setAvatarGain("", gain); + DependencyManager::get()->setAvatarGain(QUuid(), gain); }); } float Audio::getAvatarGain() { return resultWithReadLock([&] { - return DependencyManager::get()->getAvatarGain(""); + return DependencyManager::get()->getAvatarGain(QUuid()); }); } From cbeb4b0b208fc40bc00d32e31bcce3b6903cc7e0 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 23 Mar 2019 06:48:37 -0700 Subject: [PATCH 067/135] Persist the audio-mixer settings across domain changes and server resets --- libraries/networking/src/NodeList.cpp | 38 ++++++++++++++++++--------- libraries/networking/src/NodeList.h | 3 ++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index eec710322e..0021a594bc 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -265,8 +265,6 @@ void NodeList::reset(bool skipDomainHandlerReset) { _avatarGainMap.clear(); _avatarGainMapLock.unlock(); - _injectorGain = 0.0f; - if (!skipDomainHandlerReset) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); @@ -1018,6 +1016,14 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { // also send them the current ignore radius state. sendIgnoreRadiusStateToNode(newNode); + + // also send the current avatar and injector gains + if (_avatarGain != 0.0f) { + setAvatarGain(QUuid(), _avatarGain); + } + if (_injectorGain != 0.0f) { + setInjectorGain(_injectorGain); + } } if (newNode->getType() == NodeType::AvatarMixer) { // this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session, @@ -1064,13 +1070,17 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { if (nodeID.isNull()) { qCDebug(networking) << "Sending Set MASTER Avatar Gain packet with Gain:" << gain; - } else { - qCDebug(networking) << "Sending Set Avatar Gain packet with UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; - } - sendPacket(std::move(setAvatarGainPacket), *audioMixer); - QWriteLocker lock{ &_avatarGainMapLock }; - _avatarGainMap[nodeID] = gain; + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + _avatarGain = gain; + + } else { + qCDebug(networking) << "Sending Set Avatar Gain packet with UUID:" << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; + + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + QWriteLocker lock{ &_avatarGainMapLock }; + _avatarGainMap[nodeID] = gain; + } } else { qWarning() << "Couldn't find audio mixer to send set gain request"; @@ -1081,10 +1091,14 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { } float NodeList::getAvatarGain(const QUuid& nodeID) { - QReadLocker lock{ &_avatarGainMapLock }; - auto it = _avatarGainMap.find(nodeID); - if (it != _avatarGainMap.cend()) { - return it->second; + if (nodeID.isNull()) { + return _avatarGain; + } else { + QReadLocker lock{ &_avatarGainMapLock }; + auto it = _avatarGainMap.find(nodeID); + if (it != _avatarGainMap.cend()) { + return it->second; + } } return 0.0f; } diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index d2a1212d64..f871560fba 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -183,7 +183,8 @@ private: mutable QReadWriteLock _avatarGainMapLock; tbb::concurrent_unordered_map _avatarGainMap; - std::atomic _injectorGain { 0.0f }; + std::atomic _avatarGain { 0.0f }; // in dB + std::atomic _injectorGain { 0.0f }; // in dB void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); #if defined(Q_OS_ANDROID) From 649bb92e6c8d628ce4825e2dc946cdf624b981e0 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 23 Mar 2019 16:00:02 -0700 Subject: [PATCH 068/135] Prototype an updated Audio tab with 3 independent volume controls --- interface/resources/qml/hifi/audio/Audio.qml | 176 +++++++++++++++--- .../qml/hifi/audio/LoopbackAudio.qml | 16 +- .../qml/hifi/audio/PlaySampleSound.qml | 16 +- 3 files changed, 169 insertions(+), 39 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index da306f911b..92dfcb4117 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -85,8 +85,19 @@ Rectangle { } function updateMyAvatarGainFromQML(sliderValue, isReleased) { - if (Users.getAvatarGain(myAvatarUuid) != sliderValue) { - Users.setAvatarGain(myAvatarUuid, sliderValue); + if (AudioScriptingInterface.getAvatarGain() != sliderValue) { + AudioScriptingInterface.setAvatarGain(sliderValue); + } + } + function updateInjectorGainFromQML(sliderValue, isReleased) { + if (AudioScriptingInterface.getInjectorGain() != sliderValue) { + AudioScriptingInterface.setInjectorGain(sliderValue); // server side + AudioScriptingInterface.setLocalInjectorGain(sliderValue); // client side + } + } + function updateSystemInjectorGainFromQML(sliderValue, isReleased) { + if (AudioScriptingInterface.getSystemInjectorGain() != sliderValue) { + AudioScriptingInterface.setSystemInjectorGain(sliderValue); } } @@ -254,6 +265,14 @@ Rectangle { color: hifi.colors.white; text: qsTr("Choose input device"); } + + AudioControls.LoopbackAudio { + x: margins.paddings + + visible: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR); + anchors { right: parent.right } + } } ListView { @@ -301,13 +320,6 @@ Rectangle { } } } - AudioControls.LoopbackAudio { - x: margins.paddings - - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { left: parent.left; leftMargin: margins.paddings } - } Separator {} @@ -335,6 +347,14 @@ Rectangle { color: hifi.colors.white; text: qsTr("Choose output device"); } + + AudioControls.PlaySampleSound { + x: margins.paddings + + visible: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR); + anchors { right: parent.right } + } } ListView { @@ -370,20 +390,20 @@ Rectangle { } Item { - id: gainContainer + id: avatarGainContainer x: margins.paddings; width: parent.width - margins.paddings*2 - height: gainSliderTextMetrics.height + height: avatarGainSliderTextMetrics.height HifiControlsUit.Slider { - id: gainSlider + id: avatarGainSlider anchors.right: parent.right height: parent.height width: 200 minimumValue: -60.0 maximumValue: 20.0 stepSize: 5 - value: Users.getAvatarGain(myAvatarUuid) + value: AudioScriptingInterface.getAvatarGain() onValueChanged: { updateMyAvatarGainFromQML(value, false); } @@ -399,7 +419,7 @@ Rectangle { // Do nothing. } onDoubleClicked: { - gainSlider.value = 0.0 + avatarGainSlider.value = 0.0 } onPressed: { // Pass through to Slider @@ -413,13 +433,13 @@ Rectangle { } } TextMetrics { - id: gainSliderTextMetrics - text: gainSliderText.text - font: gainSliderText.font + id: avatarGainSliderTextMetrics + text: avatarGainSliderText.text + font: avatarGainSliderText.font } RalewayRegular { // The slider for my card is special, it controls the master gain - id: gainSliderText; + id: avatarGainSliderText; text: "Avatar volume"; size: 16; anchors.left: parent.left; @@ -429,12 +449,122 @@ Rectangle { } } - AudioControls.PlaySampleSound { - x: margins.paddings + Item { + id: injectorGainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: injectorGainSliderTextMetrics.height - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { left: parent.left; leftMargin: margins.paddings } + HifiControlsUit.Slider { + id: injectorGainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: AudioScriptingInterface.getInjectorGain() + onValueChanged: { + updateInjectorGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateInjectorGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + injectorGainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: injectorGainSliderTextMetrics + text: injectorGainSliderText.text + font: injectorGainSliderText.font + } + RalewayRegular { + id: injectorGainSliderText; + text: "Environment volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + } + + Item { + id: systemInjectorGainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: systemInjectorGainSliderTextMetrics.height + + HifiControlsUit.Slider { + id: systemInjectorGainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: AudioScriptingInterface.getSystemInjectorGain() + onValueChanged: { + updateSystemInjectorGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateSystemInjectorGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + systemInjectorGainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: systemInjectorGainSliderTextMetrics + text: systemInjectorGainSliderText.text + font: systemInjectorGainSliderText.font + } + RalewayRegular { + id: systemInjectorGainSliderText; + text: "System Sound volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } } } } diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 8ec0ffc496..74bc0f67dc 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -44,7 +44,7 @@ RowLayout { } HifiControlsUit.Button { - text: audioLoopedBack ? qsTr("STOP TESTING YOUR VOICE") : qsTr("TEST YOUR VOICE"); + text: audioLoopedBack ? qsTr("STOP TESTING") : qsTr("TEST YOUR VOICE"); color: audioLoopedBack ? hifi.buttons.red : hifi.buttons.blue; onClicked: { if (audioLoopedBack) { @@ -57,11 +57,11 @@ RowLayout { } } - RalewayRegular { - Layout.leftMargin: 2; - size: 14; - color: "white"; - font.italic: true - text: audioLoopedBack ? qsTr("Speak in your input") : ""; - } +// RalewayRegular { +// Layout.leftMargin: 2; +// size: 14; +// color: "white"; +// font.italic: true +// text: audioLoopedBack ? qsTr("Speak in your input") : ""; +// } } diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index b9d9727dab..0eb78f3efe 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -56,16 +56,16 @@ RowLayout { HifiConstants { id: hifi; } HifiControlsUit.Button { - text: isPlaying ? qsTr("STOP TESTING YOUR SOUND") : qsTr("TEST YOUR SOUND"); + text: isPlaying ? qsTr("STOP TESTING") : qsTr("TEST YOUR SOUND"); color: isPlaying ? hifi.buttons.red : hifi.buttons.blue; onClicked: isPlaying ? stopSound() : playSound(); } - RalewayRegular { - Layout.leftMargin: 2; - size: 14; - color: "white"; - font.italic: true - text: isPlaying ? qsTr("Listen to your output") : ""; - } +// RalewayRegular { +// Layout.leftMargin: 2; +// size: 14; +// color: "white"; +// font.italic: true +// text: isPlaying ? qsTr("Listen to your output") : ""; +// } } From 8872e9e4e7b318fef033ca8b174fc12d80125fcc Mon Sep 17 00:00:00 2001 From: Simon Walton <36682372+SimonWalton-HiFi@users.noreply.github.com> Date: Sat, 23 Mar 2019 20:05:26 -0900 Subject: [PATCH 069/135] Remove unused debugging variable --- assignment-client/src/avatars/AvatarMixer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index c6cd12f30a..32fb5b9b1a 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -1005,7 +1005,6 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { { // Fraction of downstream bandwidth reserved for 'hero' avatars: static const QString PRIORITY_FRACTION_KEY = "priority_fraction"; if (avatarMixerGroupObject.contains(PRIORITY_FRACTION_KEY)) { - bool isDouble = avatarMixerGroupObject[PRIORITY_FRACTION_KEY].isDouble(); float priorityFraction = float(avatarMixerGroupObject[PRIORITY_FRACTION_KEY].toDouble()); _slavePool.setPriorityReservedFraction(std::min(std::max(0.0f, priorityFraction), 1.0f)); qCDebug(avatars) << "Avatar mixer reserving" << priorityFraction << "of bandwidth for priority avatars"; From de5643b5b99a257c9fbe3f2475fed384c25899fa Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 25 Mar 2019 10:33:32 -0700 Subject: [PATCH 070/135] quint64 -> uint64_t --- libraries/procedural/src/procedural/Procedural.cpp | 4 ++-- libraries/procedural/src/procedural/Procedural.h | 10 +++++----- .../procedural/src/procedural/ProceduralSkybox.cpp | 2 +- libraries/procedural/src/procedural/ProceduralSkybox.h | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index c5bfa43e75..9940da0b9a 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -225,7 +225,7 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, - const quint64& created, + const uint64_t& created, const ProceduralProgramKey key) { std::lock_guard lock(_mutex); _entityDimensions = size; @@ -233,7 +233,7 @@ void Procedural::prepare(gpu::Batch& batch, _entityOrientation = glm::mat3_cast(orientation); _entityCreated = created; if (!_shaderPath.isEmpty()) { - auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); + auto lastModified = (uint64_t)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _shaderModified) { QFile file(_shaderPath); file.open(QIODevice::ReadOnly); diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index b8fd77b052..956cef368f 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -83,10 +83,10 @@ public: bool isReady() const; bool isEnabled() const { return _enabled; } void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, - const quint64& created, const ProceduralProgramKey key = ProceduralProgramKey()); + const uint64_t& created, const ProceduralProgramKey key = ProceduralProgramKey()); glm::vec4 getColor(const glm::vec4& entityColor) const; - quint64 getFadeStartTime() const { return _fadeStartTime; } + uint64_t getFadeStartTime() const { return _fadeStartTime; } bool isFading() const { return _doesFade && _isFading; } void setIsFading(bool isFading) { _isFading = isFading; } void setDoesFade(bool doesFade) { _doesFade = doesFade; } @@ -136,7 +136,7 @@ protected: // Rendering object descriptions, from userData QString _shaderSource; QString _shaderPath; - quint64 _shaderModified { 0 }; + uint64_t _shaderModified { 0 }; NetworkShaderPointer _networkShader; bool _shaderDirty { true }; bool _uniformsDirty { true }; @@ -156,12 +156,12 @@ protected: glm::vec3 _entityDimensions; glm::vec3 _entityPosition; glm::mat3 _entityOrientation; - quint64 _entityCreated; + uint64_t _entityCreated; private: void setupUniforms(); - mutable quint64 _fadeStartTime { 0 }; + mutable uint64_t _fadeStartTime { 0 }; mutable bool _hasStartedFade { false }; mutable bool _isFading { false }; bool _doesFade { true }; diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index bf8e408e70..53df1532dc 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -17,7 +17,7 @@ #include #include -ProceduralSkybox::ProceduralSkybox(quint64 created) : graphics::Skybox(), _created(created) { +ProceduralSkybox::ProceduralSkybox(uint64_t created) : graphics::Skybox(), _created(created) { _procedural._vertexSource = gpu::Shader::createVertex(shader::graphics::vertex::skybox)->getSource(); _procedural._opaqueFragmentSource = shader::Source::get(shader::procedural::fragment::proceduralSkybox); // Adjust the pipeline state for background using the stencil test diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h index 1b01b891d3..a1d7ea8fa7 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.h +++ b/libraries/procedural/src/procedural/ProceduralSkybox.h @@ -19,7 +19,7 @@ class ProceduralSkybox: public graphics::Skybox { public: - ProceduralSkybox(quint64 created = 0); + ProceduralSkybox(uint64_t created = 0); void parse(const QString& userData) { _procedural.setProceduralData(ProceduralData::parse(userData)); } @@ -29,11 +29,11 @@ public: void render(gpu::Batch& batch, const ViewFrustum& frustum) const override; static void render(gpu::Batch& batch, const ViewFrustum& frustum, const ProceduralSkybox& skybox); - quint64 getCreated() const { return _created; } + uint64_t getCreated() const { return _created; } protected: mutable Procedural _procedural; - quint64 _created; + uint64_t _created; }; typedef std::shared_ptr< ProceduralSkybox > ProceduralSkyboxPointer; From 57a02bc1d57584de4de914ffc8c960bbe653658a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 25 Mar 2019 11:29:25 -0700 Subject: [PATCH 071/135] capture lambda by value --- .../entities-renderer/src/RenderableMaterialEntityItem.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 15842336f5..b474fb2266 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -169,7 +169,7 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (urlChanged && !usingMaterialData) { _networkMaterial = MaterialCache::instance().getMaterial(_materialURL); - auto onMaterialRequestFinished = [&, oldParentID, oldParentMaterialName, newCurrentMaterialName](bool success) { + auto onMaterialRequestFinished = [this, oldParentID, oldParentMaterialName, newCurrentMaterialName](bool success) { if (success) { deleteMaterial(oldParentID, oldParentMaterialName); _texturesLoaded = false; @@ -186,7 +186,7 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (_networkMaterial->isLoaded()) { onMaterialRequestFinished(!_networkMaterial->isFailed()); } else { - connect(_networkMaterial.data(), &Resource::finished, this, [&](bool success) { + connect(_networkMaterial.data(), &Resource::finished, this, [this, onMaterialRequestFinished](bool success) { withWriteLock([&] { onMaterialRequestFinished(success); }); From 4658e34b4be6bd1340e56d320f775fb1d2ee3d7b Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 11:29:27 -0700 Subject: [PATCH 072/135] update indent spacing --- interface/resources/qml/hifi/audio/Audio.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index ded28506b8..46ea64a323 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -669,10 +669,10 @@ Rectangle { } } AudioControls.PlaySampleSound { - id: playSampleSound - x: margins.paddings - anchors.top: systemInjectorGainContainer.bottom; - anchors.topMargin: 10; + id: playSampleSound + x: margins.paddings + anchors.top: systemInjectorGainContainer.bottom; + anchors.topMargin: 10; } } } From afe46cd78bfcaf759bf3099c8a37d7d7bb4406fb Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 11:29:34 -0700 Subject: [PATCH 073/135] update indent spacing From 7f5d9cbd40ae95518ef9ae5cfc6cb64e05698d71 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 22 Mar 2019 13:16:56 -0700 Subject: [PATCH 074/135] adding push to talk fix for loadData --- interface/resources/qml/hifi/audio/MicBar.qml | 19 +++++++++++-------- interface/src/scripting/Audio.cpp | 8 ++++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index f51da9c381..e1b3e05d43 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -18,7 +18,10 @@ import TabletScriptingInterface 1.0 Rectangle { HifiConstants { id: hifi; } + property var muted: AudioScriptingInterface.muted; readonly property var level: AudioScriptingInterface.inputLevel; + readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; + readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; property bool gated: false; Component.onCompleted: { @@ -67,10 +70,10 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { - if (AudioScriptingInterface.pushToTalk) { + if (pushToTalk) { return; } - AudioScriptingInterface.muted = !AudioScriptingInterface.muted; + muted = !muted; Tablet.playSound(TabletEnums.ButtonClick); } drag.target: dragTarget; @@ -115,7 +118,7 @@ Rectangle { readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; id: image; - source: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? pushToTalkIcon : AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : unmutedIcon; width: 30; height: 30; @@ -138,9 +141,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: muted ? colors.muted : colors.unmuted; - visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; + visible: (pushToTalk && !pushingToTalk) || muted; anchors { left: parent.left; @@ -159,7 +162,7 @@ Rectangle { color: parent.color; - text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (muted ? "MUTED" : "MUTE"); font.pointSize: 12; } @@ -169,7 +172,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? (HMD.active ? 27 : 25) : 50; + width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; color: parent.color; } @@ -180,7 +183,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? (HMD.active ? 27 : 25) : 50; + width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; color: parent.color; } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index b1b5077e60..0e0d13ae45 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -224,10 +224,10 @@ void Audio::saveData() { } void Audio::loadData() { - _desktopMuted = _desktopMutedSetting.get(); - _hmdMuted = _hmdMutedSetting.get(); - _pttDesktop = _pttDesktopSetting.get(); - _pttHMD = _pttHMDSetting.get(); + setMutedDesktop(_desktopMutedSetting.get()); + setMutedHMD(_hmdMutedSetting.get()); + setPTTDesktop(_pttDesktopSetting.get()); + setPTTHMD(_pttHMDSetting.get()); auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted()), Q_ARG(bool, false)); From f551d49f4f0ab549fbb1a7c086012d12b4aed87e Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 11:31:05 -0700 Subject: [PATCH 075/135] adding signal for updating push to talk variables --- interface/resources/qml/hifi/audio/MicBar.qml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index e1b3e05d43..c154eabfaa 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -20,13 +20,20 @@ Rectangle { property var muted: AudioScriptingInterface.muted; readonly property var level: AudioScriptingInterface.inputLevel; - readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; - readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; + property var pushToTalk: AudioScriptingInterface.pushToTalk; + property var pushingToTalk: AudioScriptingInterface.pushingToTalk; property bool gated: false; Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + AudioScriptingInterface.pushToTalkChanged.connect(function() { + pushToTalk = AudioScriptingInterface.pushToTalk; + }); + AudioScriptingInterface.pushingToTalkChanged.connect(function() { + pushingToTalk = AudioScriptingInterface.pushingToTalk; + }); + } property bool standalone: false; From cd2dbbb955012431b92886756ca11d0d6d6e91d4 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 25 Mar 2019 11:52:52 -0700 Subject: [PATCH 076/135] fix logic typo --- interface/src/avatar/MyAvatar.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4ea0d37710..b204588774 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3462,7 +3462,7 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { _avatarEntitiesLock.withReadLock([&] { avatarEntityIDs = _packedAvatarEntityData.keys(); }); - bool sendPackets = DependencyManager::get()->getSessionUUID().isNull(); + bool sendPackets = !DependencyManager::get()->getSessionUUID().isNull(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); entityTree->withWriteLock([&] { for (const auto& entityID : avatarEntityIDs) { @@ -3473,7 +3473,7 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { // update OwningAvatarID so entity can be identified as "ours" later entity->setOwningAvatarID(newSessionID); // NOTE: each attached AvatarEntity already have the correct updated parentID - // via magic in SpatiallyNestable, hence we check agains newSessionID + // via magic in SpatiallyNestable, hence we check against newSessionID if (sendPackets && entity->getParentID() == newSessionID) { // but when we have a real session and the AvatarEntity is parented to MyAvatar // we need to update the "packedAvatarEntityData" sent to the avatar-mixer From 59a420b1602d2bc8eb9a494601658993bc295e0c Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 12:24:13 -0700 Subject: [PATCH 077/135] adding signals + slots to when muted + display mode changes --- interface/resources/qml/hifi/audio/MicBar.qml | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index c154eabfaa..d161f12d70 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -27,6 +27,13 @@ Rectangle { Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + HMD.displayModeChanged.connect(function() { + muted = AudioScriptingInterface.muted; + pushToTalk = AudioScriptingInterface.pushToTalk; + }); + AudioScriptingInterface.mutedChanged.connect(function() { + muted = AudioScriptingInterface.muted; + }); AudioScriptingInterface.pushToTalkChanged.connect(function() { pushToTalk = AudioScriptingInterface.pushToTalk; }); @@ -94,16 +101,16 @@ Rectangle { QtObject { id: colors; - readonly property string unmuted: "#FFF"; - readonly property string muted: "#E2334D"; + readonly property string unmutedColor: "#FFF"; + readonly property string mutedColor: "#E2334D"; readonly property string gutter: "#575757"; readonly property string greenStart: "#39A38F"; readonly property string greenEnd: "#1FC6A6"; readonly property string yellow: "#C0C000"; - readonly property string red: colors.muted; + readonly property string red: colors.mutedColor; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; - readonly property string icon: AudioScriptingInterface.muted ? muted : unmuted; + readonly property string icon: muted ? colors.mutedColor : unmutedColor; } Item { @@ -148,8 +155,6 @@ Rectangle { Item { id: status; - readonly property string color: muted ? colors.muted : colors.unmuted; - visible: (pushToTalk && !pushingToTalk) || muted; anchors { @@ -167,7 +172,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - color: parent.color; + color: colors.icon; text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (muted ? "MUTED" : "MUTE"); font.pointSize: 12; @@ -181,7 +186,7 @@ Rectangle { width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; - color: parent.color; + color: colors.icon; } Rectangle { @@ -192,7 +197,7 @@ Rectangle { width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; - color: parent.color; + color: colors.icon; } } From dc996c267f5af7a0203a1d9a4de84a630b68f2b7 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 12:24:13 -0700 Subject: [PATCH 078/135] adding signals + slots to when muted + display mode changes From b78ae80ae621fc657f6882faa8ff8e84e6f282a9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 26 Mar 2019 09:40:47 +1300 Subject: [PATCH 079/135] Address review comments and add further examples --- .../src/AgentScriptingInterface.h | 33 ++- .../src/avatars/ScriptableAvatar.h | 49 ++-- interface/src/avatar/MyAvatar.cpp | 21 +- interface/src/avatar/MyAvatar.h | 225 ++++++++++-------- .../animation/src/AnimInverseKinematics.h | 23 +- libraries/animation/src/AnimOverlay.h | 16 +- .../src/avatars-renderer/Avatar.h | 22 +- libraries/avatars/src/AvatarData.cpp | 6 +- libraries/avatars/src/AvatarData.h | 104 +++++--- 9 files changed, 310 insertions(+), 189 deletions(-) diff --git a/assignment-client/src/AgentScriptingInterface.h b/assignment-client/src/AgentScriptingInterface.h index 1242634dd5..b1a8aaff96 100644 --- a/assignment-client/src/AgentScriptingInterface.h +++ b/assignment-client/src/AgentScriptingInterface.h @@ -18,8 +18,8 @@ #include "Agent.h" /**jsdoc - * The Agent API enables an assignment client to emulate an avatar. In particular, setting isAvatar = - * true connects the assignment client to the avatar and audio mixers and enables the {@link Avatar} API to be used. + * The Agent API enables an assignment client to emulate an avatar. Setting isAvatar = true connects + * the assignment client to the avatar and audio mixers, and enables the {@link Avatar} API to be used. * * @namespace Agent * @@ -29,13 +29,13 @@ * false. * @property {boolean} isPlayingAvatarSound - true if the script has a sound to play, otherwise false. * Sounds are played when isAvatar is true, from the position and with the orientation of the - * scripted avatar's head.Read-only. + * scripted avatar's head. Read-only. * @property {boolean} isListeningToAudioStream - true if the agent is "listening" to the audio stream from the * domain, otherwise false. * @property {boolean} isNoiseGateEnabled - true if the noise gate is enabled, otherwise false. When * enabled, the input audio stream is blocked (fully attenuated) if it falls below an adaptive threshold. - * @property {number} lastReceivedAudioLoudness - The current loudness of the audio input, nominal range 0.0 (no - * sound) – 1.0 (the onset of clipping). Read-only. + * @property {number} lastReceivedAudioLoudness - The current loudness of the audio input. Nominal range [0.0 (no + * sound) – 1.0 (the onset of clipping)]. Read-only. * @property {Uuid} sessionUUID - The unique ID associated with the agent's current session in the domain. Read-only. */ class AgentScriptingInterface : public QObject { @@ -63,9 +63,9 @@ public: public slots: /**jsdoc - * Sets whether or not the script should emulate an avatar. + * Sets whether the script should emulate an avatar. * @function Agent.setIsAvatar - * @param {boolean} isAvatar - true if the script should act as if an avatar, otherwise false. + * @param {boolean} isAvatar - true if the script emulates an avatar, otherwise false. * @example
* (function () { * Agent.setIsAvatar(true); @@ -76,9 +76,14 @@ public slots: void setIsAvatar(bool isAvatar) const { _agent->setIsAvatar(isAvatar); } /**jsdoc - * Checks whether or not the script is emulating an avatar. + * Checks whether the script is emulating an avatar. * @function Agent.isAvatar - * @returns {boolean} true if the script is acting as if an avatar, otherwise false. + * @returns {boolean} true if the script is emulating an avatar, otherwise false. + * @example + * (function () { + * print("Agent is avatar: " + Agent.isAvatar()); + * print("Agent is avatar: " + Agent.isAvatar); // Same result. + * }()); */ bool isAvatar() const { return _agent->isAvatar(); } @@ -86,7 +91,15 @@ public slots: * Plays a sound from the position and with the orientation of the emulated avatar's head. No sound is played unless * isAvatar == true. * @function Agent.playAvatarSound - * @param {SoundObject} avatarSound - The sound to play. + * @param {SoundObject} avatarSound - The sound played. + * @example + * (function () { + * Agent.isAvatar = true; + * var sound = SoundCache.getSound(Script.resourcesPath() + "sounds/sample.wav"); + * Script.setTimeout(function () { // Give the sound time to load. + * Agent.playAvatarSound(sound); + * }, 1000); + * }()); */ void playAvatarSound(SharedSoundPointer avatarSound) const { _agent->playAvatarSound(avatarSound); } diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 37e82947ae..fbe5675bd8 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -20,7 +20,7 @@ /**jsdoc * The Avatar API is used to manipulate scriptable avatars on the domain. This API is a subset of the - * {@link MyAvatar} API. To enable this API, set {@link Agent|Agent.isAvatatr} to true. + * {@link MyAvatar} API. To enable this API, set {@link Agent|Agent.isAvatar} to true. * *

For Interface, client entity, and avatar scripts, see {@link MyAvatar}.

* @@ -30,13 +30,13 @@ * * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. * @property {Vec3} position - The position of the avatar. - * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 - * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. + * @property {number} scale=1.0 - The scale of the avatar. The value can be set to anything between 0.005 and + * 1000.0. When the scale value is fetched, it may temporarily be further limited by the domain's settings. * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in * the application of physics. Read-only. * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar * but is otherwise not used or changed by Interface. - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * @property {number} bodyYaw - The left or right rotation about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". @@ -57,13 +57,12 @@ * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting * into the domain. * @property {string} displayName - The avatar's display name. - * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the - * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the - * time. - * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's - * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. - * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. - * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * @property {string} sessionDisplayName - displayName's sanitized and default version defined by the avatar mixer + * rather than Interface clients. The result is unique among all avatars present in the domain at the time. + * @property {boolean} lookAtSnappingEnabled=true - true if the avatar's eyes snap to look at another avatar's + * eyes when the other avatar is in the line of sight and also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The avatar's FST file. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
* Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. @@ -75,6 +74,12 @@ * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. + * + * @example
+ * (function () { + * Agent.setIsAvatar(true); + * print("Position: " + JSON.stringify(Avatar.position)); // 0, 0, 0 + * }()); */ class ScriptableAvatar : public AvatarData, public Dependency { @@ -90,14 +95,14 @@ public: /**jsdoc * Starts playing an animation on the avatar. * @function Avatar.startAnimation - * @param {string} url - The URL to the animation file. Animation files need to be .FBX format but only need to contain + * @param {string} url - The animation file's URL. Animation files need to be in the FBX format but only need to contain * the avatar skeleton and animation data. * @param {number} [fps=30] - The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. * @param {number} [priority=1] - Not used. * @param {boolean} [loop=false] - true if the animation should loop, false if it shouldn't. * @param {boolean} [hold=false] - Not used. - * @param {number} [firstFrame=0] - The frame the animation should start at. - * @param {number} [lastFrame=3.403e+38] - The frame the animation should stop at. + * @param {number} [firstFrame=0] - The frame at which the animation starts. + * @param {number} [lastFrame=3.403e+38] - The frame at which the animation stops. * @param {string[]} [maskedJoints=[]] - The names of joints that should not be animated. */ /// Allows scripts to run animations. @@ -115,6 +120,9 @@ public: * Gets the details of the current avatar animation that is being or was recently played. * @function Avatar.getAnimationDetails * @returns {Avatar.AnimationDetails} The current or recent avatar animation. + * @example + * var animationDetails = Avatar.getAnimationDetails(); + * print("Animation details: " + JSON.stringify(animationDetails)); */ Q_INVOKABLE AnimationDetails getAnimationDetails(); @@ -146,18 +154,21 @@ public: bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } /**jsdoc - * Gets the avatar entities as binary data. - *

Warning: Potentially a very expensive call. Do not use if possible.

+ * Gets details of all avatar entities. + *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.getAvatarEntityData - * @returns {AvatarEntityMap} The avatar entities as binary data. + * @returns {AvatarEntityMap} Details of the avatar entities. + * @example
+ * var avatarEntityData = Avatar.getAvatarEntityData(); + * print("Avatar entities: " + JSON.stringify(avatarEntityData)); */ Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * Sets the avatar entities from binary data. + * Sets all avatar entities from an object. *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.setAvatarEntityData - * @param {AvatarEntityMap} avatarEntityData - The avatar entities as binary data. + * @param {AvatarEntityMap} avatarEntityData - Details of the avatar entities. */ Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e0e9b5b648..12c260ccde 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3542,8 +3542,8 @@ void MyAvatar::clearScaleRestriction() { /**jsdoc * A teleport target. * @typedef {object} MyAvatar.GoToProperties - * @property {Vec3} position - The new position for the avatar, in world coordinates. - * @property {Quat} [orientation] - The new orientation for the avatar. + * @property {Vec3} position - The avatar's new position. + * @property {Quat} [orientation] - The avatar's new orientation. */ void MyAvatar::goToLocation(const QVariant& propertiesVar) { qCDebug(interfaceapp, "MyAvatar QML goToLocation"); @@ -3902,8 +3902,7 @@ void MyAvatar::setCollisionWithOtherAvatarsFlags() { } /**jsdoc - * A collision capsule is a cylinder with hemispherical ends. It is used, in particular, to approximate the extents of an - * avatar. + * A collision capsule is a cylinder with hemispherical ends. It is often used to approximate the extents of an avatar. * @typedef {object} MyAvatar.CollisionCapsule * @property {Vec3} start - The bottom end of the cylinder, excluding the bottom hemisphere. * @property {Vec3} end - The top end of the cylinder, excluding the top hemisphere. @@ -5361,12 +5360,12 @@ void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) /**jsdoc * Physics options to use in the flow simulation of a joint. * @typedef {object} MyAvatar.FlowPhysicsOptions - * @property {boolean} [active=true] - true to enable flow on the joint, false if it isn't., - * @property {number} [radius=0.01] - The thickness of segments and knots. (Needed for collisions.) + * @property {boolean} [active=true] - true to enable flow on the joint, otherwise false. + * @property {number} [radius=0.01] - The thickness of segments and knots (needed for collisions). * @property {number} [gravity=-0.0096] - Y-value of the gravity vector. * @property {number} [inertia=0.8] - Rotational inertia multiplier. * @property {number} [damping=0.85] - The amount of damping on joint oscillation. - * @property {number} [stiffness=0.0] - How stiff each thread is. + * @property {number} [stiffness=0.0] - The stiffness of each thread. * @property {number} [delta=0.55] - Delta time for every integration step. */ /**jsdoc @@ -5454,18 +5453,18 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys * that has been configured. * @property {Object} collisions - The collisions configuration for each joint that * has collisions configured. - * @property {Object} threads - The threads hat have been configured, with the name of the first joint as - * the ThreadName and an array of the indexes of all the joints in the thread as the value. + * @property {Object} threads - The threads that have been configured, with the first joint's name as the + * ThreadName and value as an array of the indexes of all the joints in the thread. */ /**jsdoc * A set of physics options currently used in flow simulation. * @typedef {object} MyAvatar.FlowPhysicsData - * @property {boolean} active - true to enable flow on the joint, false if it isn't., + * @property {boolean} active - true to enable flow on the joint, otherwise false. * @property {number} radius - The thickness of segments and knots. (Needed for collisions.) * @property {number} gravity - Y-value of the gravity vector. * @property {number} inertia - Rotational inertia multiplier. * @property {number} damping - The amount of damping on joint oscillation. - * @property {number} stiffness - How stiff each thread is. + * @property {number} stiffness - The stiffness of each thread. * @property {number} delta - Delta time for every integration step. * @property {number[]} jointIndices - The indexes of the joints the options are applied to. */ diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 8951bc7fed..3159348871 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -72,13 +72,13 @@ class MyAvatar : public Avatar { * * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. * @property {Vec3} position - The position of the avatar. - * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 - * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. + * @property {number} scale=1.0 - The scale of the avatar. The value can be set to anything between 0.005 and + * 1000.0. When the scale value is fetched, it may temporarily be further limited by the domain's settings. * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in * the application of physics. Read-only. * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar * but is otherwise not used or changed by Interface. - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * @property {number} bodyYaw - The left or right rotation about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". @@ -99,13 +99,12 @@ class MyAvatar : public Avatar { * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting * into the domain. * @property {string} displayName - The avatar's display name. - * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the - * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the - * time. - * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's - * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. - * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. - * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * @property {string} sessionDisplayName - displayName's sanitized and default version defined by the avatar + * mixer rather than Interface clients. The result is unique among all avatars present in the domain at the time. + * @property {boolean} lookAtSnappingEnabled=true - true if the avatar's eyes snap to look at another avatar's + * eyes when the other avatar is in the line of sight and also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The avatar's FST file. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
* Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. @@ -149,12 +148,12 @@ class MyAvatar : public Avatar { * property value is audioListenerModeCustom. * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the * audioListenerMode property value is audioListenerModeCustom. - * @property {boolean} hasScriptedBlendshapes=false - Blendshapes will be transmitted over the network if set to true.
+ * @property {boolean} hasScriptedBlendshapes=false - true to transmit blendshapes over the network.
* Note: Currently doesn't work. Use {@link MyAvatar.setForceFaceTrackerConnected} instead. - * @property {boolean} hasProceduralBlinkFaceMovement=true - If true then procedural blinking is turned on. - * @property {boolean} hasProceduralEyeFaceMovement=true - If true then procedural eye movement is turned on. - * @property {boolean} hasAudioEnabledFaceMovement=true - If true then voice audio will move the mouth - * blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. + * @property {boolean} hasProceduralBlinkFaceMovement=true - true if procedural blinking is turned on. + * @property {boolean} hasProceduralEyeFaceMovement=true - true if procedural eye movement is turned on. + * @property {boolean} hasAudioEnabledFaceMovement=true - true to move the mouth blendshapes with voice audio + * when MyAvatar.hasScriptedBlendshapes is enabled. * @property {number} rotationRecenterFilterLength - Configures how quickly the avatar root rotates to recenter its facing * direction to match that of the user's torso based on head and hands orientation. A smaller value makes the * recentering happen more quickly. The minimum value is 0.01. @@ -178,23 +177,23 @@ class MyAvatar : public Avatar { * the palm, in avatar coordinates. If the hand isn't being positioned by a controller, the value is * {@link Vec3(0)|Vec3.ZERO}. Read-only. * - * @property {Pose} leftHandPose - The pose of the left hand as determined by the hand controllers, relative to the avatar. + * @property {Pose} leftHandPose - The left hand's pose as determined by the hand controllers, relative to the avatar. * Read-only. - * @property {Pose} rightHandPose - The pose right hand position as determined by the hand controllers, relative to the - * avatar. Read-only. - * @property {Pose} leftHandTipPose - The pose of the left hand as determined by the hand controllers, relative to the - * avatar, with the position adjusted to be 0.3m along the direction of the palm. Read-only. - * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, relative to the - * avatar, with the position adjusted by 0.3m along the direction of the palm. Read-only. + * @property {Pose} rightHandPose - The right hand's pose as determined by the hand controllers, relative to the avatar. + * Read-only. + * @property {Pose} leftHandTipPose - The left hand's pose as determined by the hand controllers, relative to the avatar, + * with the position adjusted by 0.3m along the direction of the palm. Read-only. + * @property {Pose} rightHandTipPose - The right hand's pose as determined by the hand controllers, relative to the avatar, + * with the position adjusted by 0.3m along the direction of the palm. Read-only. * * @property {number} energy - Deprecated: This property will be removed from the API. * @property {boolean} isAway - true if your avatar is away (i.e., inactive), false if it is * active. * - * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to - * the center of gravity model that balances the center of gravity over the base of support of the feet. Setting the - * value false results in the default behavior where the hips are positioned under the head. - * @property {boolean} hmdLeanRecenterEnabled=true - If true then the avatar is re-centered to be under the + * @property {boolean} centerOfGravityModelEnabled=true - true if the avatar hips are placed according to + * the center of gravity model that balances the center of gravity over the base of support of the feet. Set the + * value to false for default behavior where the hips are positioned under the head. + * @property {boolean} hmdLeanRecenterEnabled=true - true IF the avatar is re-centered to be under the * head's position. In room-scale VR, this behavior is what causes your avatar to follow your HMD as you walk around * the room. Setting the value false is useful if you want to pin the avatar to a fixed position. * @property {boolean} collisionsEnabled - Set to true to enable the avatar to collide with the environment, @@ -227,9 +226,9 @@ class MyAvatar : public Avatar { * where MyAvatar.sessionUUID is not available (e.g., if not connected to a domain). Note: Likely to be deprecated. * Read-only. * - * @property {number} walkSpeed - Adjusts the walk speed of your avatar. - * @property {number} walkBackwardSpeed - Adjusts the walk backward speed of your avatar. - * @property {number} sprintSpeed - Adjusts the sprint speed of your avatar. + * @property {number} walkSpeed - The walk speed of your avatar. + * @property {number} walkBackwardSpeed - The walk backward speed of your avatar. + * @property {number} sprintSpeed - The sprint speed of your avatar. * @property {MyAvatar.SitStandModelType} userRecenterModel - Controls avatar leaning and recentering behavior. * @property {number} isInSittingState - true if your avatar is sitting (avatar leaning is disabled, * recenntering is enabled), false if it is standing (avatar leaning is enabled, and avatar recenters if it @@ -237,8 +236,8 @@ class MyAvatar : public Avatar { * user sits or stands, unless isSitStandStateLocked == true. Setting the property value overrides the * current siting / standing state, which is updated when the user next sits or stands unless * isSitStandStateLocked == true. - * @property {boolean} isSitStandStateLocked - true locks the avatar sitting / standing state, i.e., disables - * automatically changing it based on the user sitting or standing. + * @property {boolean} isSitStandStateLocked - true to lock the avatar sitting/standing state, i.e., use this + * to disable automatically changing state. * @property {boolean} allowTeleporting - true if teleporting is enabled in the Interface settings, * false if it isn't. Read-only. * @@ -396,7 +395,7 @@ public: *
- * + * * * * function animStateHandler(dictionary) { * print("Anim state dictionary: " + JSON.stringify(dictionary)); @@ -715,7 +715,7 @@ public: /**jsdoc - * Gets whether or not you do snap turns in HMD mode. + * Gets whether you do snap turns in HMD mode. * @function MyAvatar.getSnapTurn * @returns {boolean} true if you do snap turns in HMD mode; false if you do smooth turns in HMD * mode. @@ -761,7 +761,7 @@ public: Q_INVOKABLE QString getHmdAvatarAlignmentType() const; /**jsdoc - * Sets whether the avatar hips are balanced over the feet or positioned under the head. + * Sets whether the avatar's hips are balanced over the feet or positioned under the head. * @function MyAvatar.setCenterOfGravityModelEnabled * @param {boolean} enabled - true to balance the hips over the feet, false to position the hips * under the head. @@ -777,7 +777,7 @@ public: Q_INVOKABLE bool getCenterOfGravityModelEnabled() const { return _centerOfGravityModelEnabled; } /**jsdoc - * Sets whether or not the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering + * Sets whether the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering * causes your avatar to follow your HMD as you walk around the room. Disabling recentering is useful if you want to pin * the avatar to a fixed position. * @function MyAvatar.setHMDLeanRecenterEnabled @@ -787,7 +787,7 @@ public: Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } /**jsdoc - * Gets whether or not the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering + * Gets whether the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering * causes your avatar to follow your HMD as you walk around the room. * @function MyAvatar.getHMDLeanRecenterEnabled * @returns {boolean} true if recentering is enabled, false if not. @@ -864,7 +864,7 @@ public: float getDriveKey(DriveKeys key) const; /**jsdoc - * Gets the value of a drive key, regardless of whether or not it is disabled. + * Gets the value of a drive key, regardless of whether it is disabled. * @function MyAvatar.getRawDriveKey * @param {MyAvatar.DriveKeys} key - The drive key. * @returns {number} The value of the drive key. @@ -889,14 +889,15 @@ public: Q_INVOKABLE void disableDriveKey(DriveKeys key); /**jsdoc - * Enables the acction associated with a drive key. + * Enables the action associated with a drive key. The action may have been disabled with + * {@link MyAvatar.disableDriveKey|disableDriveKey}. * @function MyAvatar.enableDriveKey * @param {MyAvatar.DriveKeys} key - The drive key to enable. */ Q_INVOKABLE void enableDriveKey(DriveKeys key); /**jsdoc - * Checks whether or not a drive key is disabled. + * Checks whether a drive key is disabled. * @function MyAvatar.isDriveKeyDisabled * @param {DriveKeys} key - The drive key to check. * @returns {boolean} true if the drive key is disabled, false if it isn't. @@ -925,7 +926,7 @@ public: Q_INVOKABLE void triggerRotationRecenter(); /**jsdoc - * Gets whether or not the avatar is configured to keep its center of gravity under its head. + * Gets whether the avatar is configured to keep its center of gravity under its head. * @function MyAvatar.isRecenteringHorizontally * @returns {boolean} true if the avatar is keeping its center of gravity under its head position, * false if not. @@ -967,8 +968,8 @@ public: Q_INVOKABLE float getHeadFinalPitch() const { return getHead()->getFinalPitch(); } /**jsdoc - * If a face tracker is connected and being used, gets the estimated pitch of the user's head scaled such that the avatar - * looks at the edge of the view frustum when the user looks at the edge of their screen. + * If a face tracker is connected and being used, gets the estimated pitch of the user's head scaled. This is scale such + * that the avatar looks at the edge of the view frustum when the user looks at the edge of their screen. * @function MyAvatar.getHeadDeltaPitch * @returns {number} The pitch that the avatar's head should be if a face tracker is connected and being used, otherwise * 0, in degrees. @@ -1080,8 +1081,8 @@ public: /**jsdoc * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand, relative to the avatar, as * positioned by a hand controller (e.g., Oculus Touch or Vive), and translated 0.3m along the palm. - *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints - * for hand animation.) If you are using the Leap Motion, the return value's valid property will be + *

Note: Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints + * for hand animation.) If you are using Leap Motion, the return value's valid property will be * false and any pose values returned will not be meaningful.

* @function MyAvatar.getLeftHandTipPose * @returns {Pose} The pose of the avatar's left hand, relative to the avatar, as positioned by a hand controller, and @@ -1092,8 +1093,8 @@ public: /**jsdoc * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's right hand, relative to the avatar, as * positioned by a hand controller (e.g., Oculus Touch or Vive), and translated 0.3m along the palm. - *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints - * for hand animation.) If you are using the Leap Motion, the return value's valid property will be + *

Note: Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints + * for hand animation.) If you are using Leap Motion, the return value's valid property will be * false and any pose values returned will not be meaningful.

* @function MyAvatar.getRightHandTipPose * @returns {Pose} The pose of the avatar's right hand, relative to the avatar, as positioned by a hand controller, and @@ -1247,14 +1248,14 @@ public: void clearWornAvatarEntities(); /**jsdoc - * Checks whether your avatar is flying or not. + * Checks whether your avatar is flying. * @function MyAvatar.isFlying * @returns {boolean} true if your avatar is flying and not taking off or falling, false if not. */ Q_INVOKABLE bool isFlying(); /**jsdoc - * Checks whether your avatar is in the air or not. + * Checks whether your avatar is in the air. * @function MyAvatar.isInAir * @returns {boolean} true if your avatar is taking off, flying, or falling, otherwise false * because your avatar is on the ground. @@ -1332,9 +1333,9 @@ public: Q_INVOKABLE void setAvatarScale(float scale); /**jsdoc - * Sets whether or not the avatar should collide with entities. + * Sets whether the avatar should collide with entities. *

Note: A false value won't disable collisions if the avatar is in a zone that disallows - * collisionless avatars, however the false value will be set so that collisions are disabled as soon as the + * collisionless avatars. However, the false value will be set so that collisions are disabled as soon as the * avatar moves to a position where collisionless avatars are allowed. * @function MyAvatar.setCollisionsEnabled * @param {boolean} enabled - true to enable the avatar to collide with entities, false to @@ -1343,7 +1344,7 @@ public: Q_INVOKABLE void setCollisionsEnabled(bool enabled); /**jsdoc - * Gets whether or not the avatar will currently collide with entities. + * Gets whether the avatar will currently collide with entities. *

Note: The avatar will always collide with entities if in a zone that disallows collisionless avatars. * @function MyAvatar.getCollisionsEnabled * @returns {boolean} true if the avatar will currently collide with entities, false if it won't. @@ -1351,7 +1352,7 @@ public: Q_INVOKABLE bool getCollisionsEnabled(); /**jsdoc - * Sets whether or not the avatar should collide with other avatars. + * Sets whether the avatar should collide with other avatars. * @function MyAvatar.setOtherAvatarsCollisionsEnabled * @param {boolean} enabled - true to enable the avatar to collide with other avatars, false * to disable. @@ -1359,7 +1360,7 @@ public: Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled); /**jsdoc - * Gets whether or not the avatar will collide with other avatars. + * Gets whether the avatar will collide with other avatars. * @function MyAvatar.getOtherAvatarsCollisionsEnabled * @returns {boolean} true if the avatar will collide with other avatars, false if it won't. */ @@ -1395,6 +1396,10 @@ public: * @function MyAvatar.getAbsoluteJointRotationInObjectFrame * @param {number} index - The index of the joint. * @returns {Quat} The rotation of the joint relative to the avatar. + * @example

+ * var headIndex = MyAvatar.getJointIndex("Head"); + * var headRotation = MyAvatar.getAbsoluteJointRotationInObjectFrame(headIndex); + * print("Head rotation: " + JSON.stringify(Quat.safeEulerAngles(headRotation))); // Degrees */ virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; @@ -1404,6 +1409,10 @@ public: * @function MyAvatar.getAbsoluteJointTranslationInObjectFrame * @param {number} index - The index of the joint. * @returns {Vec3} The translation of the joint relative to the avatar. + * @example + * var headIndex = MyAvatar.getJointIndex("Head"); + * var headTranslation = MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex); + * print("Head translation: " + JSON.stringify(headTranslation)); */ virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; @@ -1500,36 +1509,56 @@ public: void prepareAvatarEntityDataForReload(); /**jsdoc - * Creates a new grab, that grabs an entity. + * Creates a new grab that grabs an entity. * @function MyAvatar.grab * @param {Uuid} targetID - The ID of the entity to grab. * @param {number} parentJointIndex - The avatar joint to use to grab the entity. - * @param {Vec3} offset - The target's local positional relative to the joint. + * @param {Vec3} offset - The target's local position relative to the joint. * @param {Quat} rotationalOffset - The target's local rotation relative to the joint. * @returns {Uuid} The ID of the new grab. + * @example + * var entityPosition = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })); + * var entityRotation = MyAvatar.orientation; + * var entityID = Entities.addEntity({ + * type: "Box", + * position: entityPosition, + * rotation: entityRotation, + * dimensions: { x: 0.5, y: 0.5, z: 0.5 } + * }); + * var rightHandJoint = MyAvatar.getJointIndex("RightHand"); + * var relativePosition = Entities.worldToLocalPosition(entityPosition, MyAvatar.SELF_ID, rightHandJoint); + * var relativeRotation = Entities.worldToLocalRotation(entityRotation, MyAvatar.SELF_ID, rightHandJoint); + * var grabID = MyAvatar.grab(entityID, rightHandJoint, relativePosition, relativeRotation); + * + * Script.setTimeout(function () { + * MyAvatar.releaseGrab(grabID); + * Entities.deleteEntity(entityID); + * }, 10000); */ Q_INVOKABLE const QUuid grab(const QUuid& targetID, int parentJointIndex, glm::vec3 positionalOffset, glm::quat rotationalOffset); /**jsdoc - * Releases (deletes) a grab, to stop grabbing an entity. + * Releases (deletes) a grab to stop grabbing an entity. * @function MyAvatar.releaseGrab * @param {Uuid} grabID - The ID of the grab to release. */ Q_INVOKABLE void releaseGrab(const QUuid& grabID); /**jsdoc - * Gets the avatar entities as binary data. + * Gets details of all avatar entities. * @function MyAvatar.getAvatarEntityData - * @override - * @returns {AvatarEntityMap} The avatar entities as binary data. + * @returns {AvatarEntityMap} Details of the avatar entities. + * @example + * var avatarEntityData = MyAvatar.getAvatarEntityData(); + * print("Avatar entities: " + JSON.stringify(avatarEntityData)); */ AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * Sets the avatar entities from binary data. + * Sets all avatar entities from an object. * @function MyAvatar.setAvatarEntityData - * @param {AvatarEntityMap} avatarEntityData - The avatar entities as binary data. + * @param {AvatarEntityMap} avatarEntityData - Details of the avatar entities. */ void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; @@ -1549,7 +1578,7 @@ public: /**jsdoc * Enables and disables flow simulation of physics on the avatar's hair, clothes, and body parts. See - * {@link https://docs.highfidelity.com/create/avatars/create-avatars/add-flow.html|Add Flow to Your Avatar} for more + * {@link https://docs.highfidelity.com/create/avatars/add-flow.html|Add Flow to Your Avatar} for more * information. * @function MyAvatar.useFlow * @param {boolean} isActive - true if flow simulation is enabled on the joint, false if it isn't. @@ -1585,7 +1614,7 @@ public slots: * MyAvatar.resetSize(); * * for (var i = 0; i < 5; i++){ - * print ("Growing by 5 percent"); + * print("Growing by 5 percent"); * MyAvatar.increaseSize(); * } */ @@ -1598,7 +1627,7 @@ public slots: * MyAvatar.resetSize(); * * for (var i = 0; i < 5; i++){ - * print ("Shrinking by 5 percent"); + * print("Shrinking by 5 percent"); * MyAvatar.decreaseSize(); * } */ @@ -1695,7 +1724,7 @@ public slots: /**jsdoc - * Adds a thrust to your avatar's current thrust, to be applied for a short while. + * Adds a thrust to your avatar's current thrust to be applied for a short while. * @function MyAvatar.addThrust * @param {Vec3} thrust - The thrust direction and magnitude. * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. @@ -1735,7 +1764,9 @@ public slots: void setToggleHips(bool followHead); /**jsdoc - * Displays base of support of feet debug graphics. + * Displays the base of support area debug graphics if in HMD mode. If your head goes outside this area your avatar's hips + * are moved to counterbalance your avatar, and if your head moves too far then your avatar's position is moved (i.e., a + * step happens). * @function MyAvatar.setEnableDebugDrawBaseOfSupport * @param {boolean} enabled - true to show the debug graphics, false to hide. */ @@ -1805,7 +1836,7 @@ public slots: void setEnableDebugDrawDetailedCollision(bool isEnabled); /**jsdoc - * Gets whether or not your avatar mesh is visible. + * Gets whether your avatar mesh is visible. * @function MyAvatar.getEnableMeshVisible * @returns {boolean} true if your avatar's mesh is visible, otherwise false. */ @@ -1830,7 +1861,7 @@ public slots: void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const; /**jsdoc - * Sets whether or not your avatar mesh is visible to you. + * Sets whether your avatar mesh is visible to you. * @function MyAvatar.setEnableMeshVisible * @param {boolean} enabled - true to show your avatar mesh, false to hide. * @example @@ -1842,7 +1873,7 @@ public slots: virtual void setEnableMeshVisible(bool isEnabled) override; /**jsdoc - * Sets whether or not inverse kinematics (IK) for your avatar. + * Sets whether inverse kinematics (IK) is enabled for your avatar. * @function MyAvatar.setEnableInverseKinematics * @param {boolean} enabled - true to enable IK, false to disable. */ @@ -1854,7 +1885,8 @@ public slots: *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for * information on animation graphs.

* @function MyAvatar.getAnimGraphOverrideUrl - * @returns {string} The URL of the override animation graph. "" if there is no override animation graph. + * @returns {string} The URL of the override animation graph JSON file. "" if there is no override animation + * graph. */ QUrl getAnimGraphOverrideUrl() const; // thread-safe @@ -1863,25 +1895,27 @@ public slots: *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for * information on animation graphs.

* @function MyAvatar.setAnimGraphOverrideUrl - * @param {string} url - The URL of the animation graph to use. Set to "" to clear an override. + * @param {string} url - The URL of the animation graph JSON file to use. Set to "" to clear an override. */ void setAnimGraphOverrideUrl(QUrl value); // thread-safe /**jsdoc - * Gets the URL of animation graph that's currently being used for avatar animations. + * Gets the URL of animation graph (i.e., the avatar animation JSON) that's currently being used for avatar animations. *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for * information on animation graphs.

* @function MyAvatar.getAnimGraphUrl - * @returns {string} The URL of the current animation graph. + * @returns {string} The URL of the current animation graph JSON file. + * @Example
+ * print(MyAvatar.getAnimGraphUrl()); */ QUrl getAnimGraphUrl() const; // thread-safe /**jsdoc - * Sets the current animation graph to use for avatar animations and makes it the default. + * Sets the current animation graph (i.e., the avatar animation JSON) to use for avatar animations and makes it the default. *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for * information on animation graphs.

* @function MyAvatar.setAnimGraphUrl - * @param {string} url - The URL of the animation graph to use. + * @param {string} url - The URL of the animation graph JSON file to use. */ void setAnimGraphUrl(const QUrl& url); // thread-safe @@ -1963,11 +1997,14 @@ signals: void otherAvatarsCollisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when the avatar's animation graph URL changes. + * Triggered when the avatar's animation graph being used changes. * @function MyAvatar.animGraphUrlChanged - * @param {string} url - The URL of the new animation graph. + * @param {string} url - The URL of the new animation graph JSON file. * @returns {Signal} - */ + * @example
+ * MyAvatar.animGraphUrlChanged.connect(function (url) { + * print("Avatar animation JSON changed to: " + url); + * }); */ void animGraphUrlChanged(const QUrl& url); /**jsdoc diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index d5a110ea76..6d58ecedec 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -66,24 +66,23 @@ public: * * * - * - * + * + * * - * * diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h index 623672143e..1ad4e100db 100644 --- a/libraries/animation/src/AnimOverlay.h +++ b/libraries/animation/src/AnimOverlay.h @@ -34,17 +34,21 @@ public: * * * - * - * - * - * + * + * * * * - * - * + * + * * * * diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index cec440eb1a..87e6af5bb3 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -203,7 +203,7 @@ public: /**jsdoc * Gets the default rotation of a joint (in the current avatar) relative to its parent. *

For information on the joint hierarchy used, see - * Avatar Standards.

+ * Avatar Standards.

* @function MyAvatar.getDefaultJointRotation * @param {number} index - The joint index. * @returns {Quat} The default rotation of the joint if the joint index is valid, otherwise {@link Quat(0)|Quat.IDENTITY}. @@ -214,7 +214,7 @@ public: * Gets the default translation of a joint (in the current avatar) relative to its parent, in model coordinates. *

Warning: These coordinates are not necessarily in meters.

*

For information on the joint hierarchy used, see - * Avatar Standards.

+ * Avatar Standards.

* @function MyAvatar.getDefaultJointTranslation * @param {number} index - The joint index. * @returns {Vec3} The default translation of the joint (in model coordinates) if the joint index is valid, otherwise @@ -223,22 +223,30 @@ public: Q_INVOKABLE virtual glm::vec3 getDefaultJointTranslation(int index) const; /**jsdoc - * Provides read-only access to the default joint rotations in avatar coordinates. + * Gets the default joint rotations in avatar coordinates. * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame * @param index {number} - The joint index. - * @returns {Quat} The rotation of this joint in avatar coordinates. + * @returns {Quat} The default rotation of the joint in avatar coordinates. + * @example
+ * var headIndex = MyAvatar.getJointIndex("Head"); + * var defaultHeadRotation = MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(headIndex); + * print("Default head rotation: " + JSON.stringify(Quat.safeEulerAngles(defaultHeadRotation))); // Degrees */ Q_INVOKABLE virtual glm::quat getAbsoluteDefaultJointRotationInObjectFrame(int index) const; /**jsdoc - * Provides read-only access to the default joint translations in avatar coordinates. + * Gets the default joint translations in avatar coordinates. * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame * @param index {number} - The joint index. - * @returns {Vec3} The position of this joint in avatar coordinates. + * @returns {Vec3} The default position of the joint in avatar coordinates. + * @example + * var headIndex = MyAvatar.getJointIndex("Head"); + * var defaultHeadTranslation = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(headIndex); + * print("Default head translation: " + JSON.stringify(defaultHeadTranslation)); */ Q_INVOKABLE virtual glm::vec3 getAbsoluteDefaultJointTranslationInObjectFrame(int index) const; @@ -459,7 +467,7 @@ public: Q_INVOKABLE virtual quint16 getParentJointIndex() const override { return SpatiallyNestable::getParentJointIndex(); } /**jsdoc - * sets the joint of the entity or avatar that the avatar is parented to. + * Sets the joint of the entity or avatar that the avatar is parented to. * @function MyAvatar.setParentJointIndex * @param {number} parentJointIndex - he joint of the entity or avatar that the avatar should be parented to. Use * 65535 or -1 to parent to the entity or avatar's position and orientation rather than a diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 317d9ec903..39dfaa8a1a 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2802,7 +2802,7 @@ glm::vec3 AvatarData::getAbsoluteJointTranslationInObjectFrame(int index) const /**jsdoc * Information on an attachment worn by the avatar. * @typedef {object} AttachmentData - * @property {string} modelUrl - The URL of the model file. Models can be .FBX or .OBJ format. + * @property {string} modelUrl - The URL of the model file. Models can be FBX or OBJ format. * @property {string} jointName - The offset to apply to the model relative to the joint position. * @property {Vec3} translation - The offset from the joint that the attachment is positioned at. * @property {Vec3} rotation - The rotation applied to the model relative to the joint orientation. @@ -3015,6 +3015,10 @@ float AvatarData::_avatarSortCoefficientSize { 8.0f }; float AvatarData::_avatarSortCoefficientCenter { 0.25f }; float AvatarData::_avatarSortCoefficientAge { 1.0f }; +/**jsdoc + * An object with the UUIDs of avatar entities as keys and avatar entity properties objects as values. + * @typedef {Object.} AvatarEntityMap + */ QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEntityMap& value) { QScriptValue obj = engine->newObject(); for (auto entityID : value.keys()) { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index e2d24465cb..00e7e67923 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -61,10 +61,6 @@ using AvatarSharedPointer = std::shared_ptr; using AvatarWeakPointer = std::weak_ptr; using AvatarHash = QHash; -/**jsdoc - * An object with the UUIDs of avatar entities as keys and binary blobs, being the entity properties, as values. - * @typedef {Object.>} AvatarEntityMap - */ using AvatarEntityMap = QMap; using PackedAvatarEntityMap = QMap; // similar to AvatarEntityMap, but different internal format using AvatarEntityIDs = QSet; @@ -439,13 +435,13 @@ class AvatarData : public QObject, public SpatiallyNestable { // IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h and ScriptableAvatar.h. /* * @property {Vec3} position - The position of the avatar. - * @property {number} scale=1.0 - The scale of the avatar. When setting, the value is limited to between 0.005 - * and 1000.0. When getting, the value may temporarily be further limited by the domain's settings. - * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * @property {number} scale=1.0 - The scale of the avatar. The value can be set to anything between 0.005 and + * 1000.0. When the scale value is fetched, it may temporarily be further limited by the domain's settings. + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in * the application of physics. Read-only. - * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar + * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar * but is otherwise not used or changed by Interface. - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * @property {number} bodyYaw - The left or right rotation about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". @@ -461,28 +457,27 @@ class AvatarData : public QObject, public SpatiallyNestable { * sometimes called "bank". * @property {Vec3} velocity - The current velocity of the avatar. * @property {Vec3} angularVelocity - The current angular velocity of the avatar. - * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the + * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the * domain. - * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting + * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting * into the domain. * @property {string} displayName - The avatar's display name. - * @property {string} sessionDisplayName - Sanitized, defaulted version of displayName that is defined by the - * avatar mixer rather than by Interface clients. The result is unique among all avatars present in the domain at the - * time. - * @property {boolean} lookAtSnappingEnabled=true - If true, the avatar's eyes snap to look at another avatar's - * eyes if generally in the line of sight and the other avatar also has lookAtSnappingEnabled == true. - * @property {string} skeletonModelURL - The URL of the avatar model's .fst file. - * @property {AttachmentData[]} attachmentData - Information on the attachments worn by the avatar.
+ * @property {string} sessionDisplayName - displayName's sanitized and default version defined by the avatar + * mixer rather than Interface clients. The result is unique among all avatars present in the domain at the time. + * @property {boolean} lookAtSnappingEnabled=true - true if the avatar's eyes snap to look at another avatar's + * eyes when the other avatar is in the line of sight and also has lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The avatar's FST file. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
* Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. - * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the + * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the * avatar's size, orientation, and position in the virtual world. Read-only. - * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the * avatar. Read-only. - * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the * avatar. Read-only. - * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) @@ -785,7 +780,7 @@ public: /**jsdoc * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointRotation * @param {number} index - The index of the joint. * @returns {Quat} The rotation of the joint relative to its parent. @@ -796,7 +791,7 @@ public: * Gets the translation of a joint relative to its parent, in model coordinates. *

Warning: These coordinates are not necessarily in meters.

*

For information on the joint hierarchy used, see - * Avatar Standards.

+ * Avatar Standards.

* @function Avatar.getJointTranslation * @param {number} index - The index of the joint. * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. @@ -897,7 +892,7 @@ public: Q_INVOKABLE virtual void clearJointData(const QString& name); /**jsdoc - * Checks that the data for a joint are valid. + * Checks if the data for a joint are valid. * @function Avatar.isJointDataValid * @param {string} name - The name of the joint. * @returns {boolean} true if the joint data are valid, false if not. @@ -906,7 +901,7 @@ public: /**jsdoc * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. + * Avatar Standards. * @function Avatar.getJointRotation * @param {string} name - The name of the joint. * @returns {Quat} The rotation of the joint relative to its parent. @@ -921,7 +916,7 @@ public: * Gets the translation of a joint relative to its parent, in model coordinates. *

Warning: These coordinates are not necessarily in meters.

*

For information on the joint hierarchy used, see - * Avatar Standards.

+ * Avatar Standards.

* @function Avatar.getJointTranslation * @param {number} name - The name of the joint. * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. @@ -1062,8 +1057,13 @@ public: * your avatar's face, use {@link Avatar.setForceFaceTrackerConnected} or {@link MyAvatar.setForceFaceTrackerConnected}. * @function Avatar.setBlendshape * @param {string} name - The name of the blendshape, per the - * {@link https://docs.highfidelity.com/create/avatars/create-avatars/avatar-standards.html#blendshapes Avatar Standards}. + * {@link https://docs.highfidelity.com/create/avatars/avatar-standards.html#blendshapes Avatar Standards}. * @param {number} value - A value between 0.0 and 1.0. + * @example
+ * MyAvatar.setForceFaceTrackerConnected(true); + * MyAvatar.setBlendshape("JawOpen", 1.0); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE void setBlendshape(QString name, float val) { _headData->setBlendshape(name, val); } @@ -1170,7 +1170,7 @@ public: * @example * var attachments = MyAvatar.getaAttachmentData(); * for (var i = 0; i < attachments.length; i++) { - * print (attachments[i].modelURL); + * print(attachments[i].modelURL); * } * * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". @@ -1318,6 +1318,14 @@ public: * @function Avatar.getSensorToWorldMatrix * @returns {Mat4} The scale, rotation, and translation transform from the user's real world to the avatar's size, * orientation, and position in the virtual world. + * @example + * var sensorToWorldMatrix = MyAvatar.getSensorToWorldMatrix(); + * print("Sensor to woprld matrix: " + JSON.stringify(sensorToWorldMatrix)); + * print("Rotation: " + JSON.stringify(Mat4.extractRotation(sensorToWorldMatrix))); + * print("Translation: " + JSON.stringify(Mat4.extractTranslation(sensorToWorldMatrix))); + * print("Scale: " + JSON.stringify(Mat4.extractScale(sensorToWorldMatrix))); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ // thread safe Q_INVOKABLE glm::mat4 getSensorToWorldMatrix() const; @@ -1335,6 +1343,14 @@ public: * Gets the rotation and translation of the left hand controller relative to the avatar. * @function Avatar.getControllerLeftHandMatrix * @returns {Mat4} The rotation and translation of the left hand controller relative to the avatar. + * @example + * var leftHandMatrix = MyAvatar.getControllerLeftHandMatrix(); + * print("Controller left hand matrix: " + JSON.stringify(leftHandMatrix)); + * print("Rotation: " + JSON.stringify(Mat4.extractRotation(leftHandMatrix))); + * print("Translation: " + JSON.stringify(Mat4.extractTranslation(leftHandMatrix))); + * print("Scale: " + JSON.stringify(Mat4.extractScale(leftHandMatrix))); // Always 1,1,1. + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ // thread safe Q_INVOKABLE glm::mat4 getControllerLeftHandMatrix() const; @@ -1409,6 +1425,12 @@ signals: * Triggered when the avatar's displayName property value changes. * @function Avatar.displayNameChanged * @returns {Signal} + * @example + * MyAvatar.displayNameChanged.connect(function () { + * print("Avatar display name changed to: " + MyAvatar.displayName); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void displayNameChanged(); @@ -1416,6 +1438,12 @@ signals: * Triggered when the avatar's sessionDisplayName property value changes. * @function Avatar.sessionDisplayNameChanged * @returns {Signal} + * @example + * MyAvatar.sessionDisplayNameChanged.connect(function () { + * print("Avatar session display name changed to: " + MyAvatar.sessionDisplayName); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void sessionDisplayNameChanged(); @@ -1423,6 +1451,12 @@ signals: * Triggered when the avatar's model (i.e., skeletonModelURL property value) is changed. * @function Avatar.skeletonModelURLChanged * @returns {Signal} + * @example + * MyAvatar.skeletonModelURLChanged.connect(function () { + * print("Skeleton model changed to: " + MyAvatar.skeletonModelURL); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void skeletonModelURLChanged(); @@ -1431,6 +1465,12 @@ signals: * @function Avatar.lookAtSnappingChanged * @param {boolean} enabled - true if look-at snapping is enabled, false if not. * @returns {Signal} + * @example + * MyAvatar.lookAtSnappingChanged.connect(function () { + * print("Avatar look-at snapping changed to: " + MyAvatar.lookAtSnappingEnabled); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void lookAtSnappingChanged(bool enabled); @@ -1438,6 +1478,12 @@ signals: * Triggered when the avatar's sessionUUID property value changes. * @function Avatar.sessionUUIDChanged * @returns {Signal} + * @example + * MyAvatar.sessionUUIDChanged.connect(function () { + * print("Avatar session UUID changed to: " + MyAvatar.sessionUUID); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void sessionUUIDChanged(); From 839a03ebe6daa68be3ca5c34155fc3e8109a57d0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 26 Mar 2019 09:41:07 +1300 Subject: [PATCH 080/135] Miscellaneous JSDoc fixes noticed in passing --- interface/src/ui/overlays/Overlays.cpp | 22 +++++++++++----------- libraries/shared/src/shared/Camera.h | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index eec6eddf44..2ade5edda5 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -1876,7 +1876,7 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a * parentID set, otherwise the same value as position. * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a - * parentID set, otherwise the same value as rotation. Synonym: localOrientation. + * parentID set, otherwise the same value as rotation. Synonym: localOrientation. * @property {boolean} ignorePickIntersection=false - If true, picks ignore the overlay. ignoreRayIntersection is a synonym. * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of objects in the world, but behind the HUD. * @property {boolean} drawHUDLayer=false - If true, the overlay is rendered in front of everything, including the HUD. @@ -1916,7 +1916,7 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a * parentID set, otherwise the same value as position. * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a - * parentID set, otherwise the same value as rotation. Synonym: localOrientation. + * parentID set, otherwise the same value as rotation. Synonym: localOrientation. * @property {boolean} isSolid=false - Synonyms: solid, isFilled, and filled. * Antonyms: isWire and wire. * @property {boolean} ignorePickIntersection=false - If true, picks ignore the overlay. ignoreRayIntersection is a synonym. @@ -1927,11 +1927,11 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if * parentID is an avatar skeleton. A value of 65535 means "no joint". * - * @property {number} startAt = 0 - The counter - clockwise angle from the overlay's x-axis that drawing starts at, in degrees. - * @property {number} endAt = 360 - The counter - clockwise angle from the overlay's x-axis that drawing ends at, in degrees. - * @property {number} outerRadius = 1 - The outer radius of the overlay, in meters.Synonym: radius. - * @property {number} innerRadius = 0 - The inner radius of the overlay, in meters. - * @property {Color} color = 255, 255, 255 - The color of the overlay.Setting this value also sets the values of + * @property {number} startAt = 0 - The counter - clockwise angle from the overlay's x-axis that drawing starts at in degrees. + * @property {number} endAt = 360 - The counter - clockwise angle from the overlay's x-axis that drawing ends at in degrees. + * @property {number} outerRadius = 1 - The outer radius of the overlay in meters. Synonym: radius. + * @property {number} innerRadius = 0 - The inner radius of the overlay in meters. + * @property {Color} color = 255, 255, 255 - The color of the overlay. Setting this value also sets the values of * innerStartColor, innerEndColor, outerStartColor, and outerEndColor. * @property {Color} startColor - Sets the values of innerStartColor and outerStartColor. * Write - only. @@ -1945,9 +1945,9 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {Color} innerEndColor - The color at the inner end point of the overlay. * @property {Color} outerStartColor - The color at the outer start point of the overlay. * @property {Color} outerEndColor - The color at the outer end point of the overlay. - * @property {number} alpha = 0.5 - The opacity of the overlay, 0.0 -1.0.Setting this value also sets + * @property {number} alpha = 0.5 - The opacity of the overlay, 0.0 -1.0. Setting this value also sets * the values of innerStartAlpha, innerEndAlpha, outerStartAlpha, and - * outerEndAlpha.Synonym: Alpha; write - only. + * outerEndAlpha. Synonym: Alpha; write - only. * @property {number} startAlpha - Sets the values of innerStartAlpha and outerStartAlpha. * Write - only. * @property {number} endAlpha - Sets the values of innerEndAlpha and outerEndAlpha. @@ -1964,9 +1964,9 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {boolean} hasTickMarks = false - If true, tick marks are drawn. * @property {number} majorTickMarksAngle = 0 - The angle between major tick marks, in degrees. * @property {number} minorTickMarksAngle = 0 - The angle between minor tick marks, in degrees. - * @property {number} majorTickMarksLength = 0 - The length of the major tick marks, in meters.A positive value draws tick marks + * @property {number} majorTickMarksLength = 0 - The length of the major tick marks, in meters. A positive value draws tick marks * outwards from the inner radius; a negative value draws tick marks inwards from the outer radius. - * @property {number} minorTickMarksLength = 0 - The length of the minor tick marks, in meters.A positive value draws tick marks + * @property {number} minorTickMarksLength = 0 - The length of the minor tick marks, in meters. A positive value draws tick marks * outwards from the inner radius; a negative value draws tick marks inwards from the outer radius. * @property {Color} majorTickMarksColor = 0, 0, 0 - The color of the major tick marks. * @property {Color} minorTickMarksColor = 0, 0, 0 - The color of the minor tick marks. diff --git a/libraries/shared/src/shared/Camera.h b/libraries/shared/src/shared/Camera.h index 31e6228bb9..0132e58d18 100644 --- a/libraries/shared/src/shared/Camera.h +++ b/libraries/shared/src/shared/Camera.h @@ -138,7 +138,7 @@ public slots: * var pickRay = Camera.computePickRay(event.x, event.y); * var intersection = Entities.findRayIntersection(pickRay); * if (intersection.intersects) { - * print ("You clicked on entity " + intersection.entityID); + * print("You clicked on entity " + intersection.entityID); * } * } * From 4a832be8c65efb9bc5504a802702427bed58c34b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 26 Mar 2019 10:03:30 +1300 Subject: [PATCH 081/135] Fix JSDoc post-merge --- interface/src/avatar/MyAvatar.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index f95ea898ba..edb686a6a6 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1609,8 +1609,7 @@ public: public slots: /**jsdoc - * @function MyAvatar.setSessionUUID - * @param {Uuid} sessionUUID + * @comment Uses the base class's JSDoc. */ virtual void setSessionUUID(const QUuid& sessionUUID) override; From 1608b24be15df3fffeac3b074cfc4d977c3f9ec9 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 25 Mar 2019 14:28:54 -0700 Subject: [PATCH 082/135] ase 20832 - Inventory app login and cancel buttons don't work on logout --- .../resources/qml/hifi/commerce/marketplace/Marketplace.qml | 2 +- interface/src/Application.cpp | 4 ---- scripts/system/commerce/wallet.js | 3 +++ scripts/system/marketplaces/marketplaces.js | 3 +-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 7dcdf9b434..619547ef43 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -664,7 +664,7 @@ Rectangle { text: "LOG IN" onClicked: { - sendToScript({method: 'needsLogIn_loginClicked'}); + sendToScript({method: 'marketplace_loginClicked'}); } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0c46404b39..1852c0007e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1206,10 +1206,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { - auto tabletScriptingInterface = DependencyManager::get(); - if (tabletScriptingInterface) { - tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); - } auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->deleteEntity(getTabletScreenID()); entityScriptingInterface->deleteEntity(getTabletHomeButtonID()); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 17ff918243..86806fd8b4 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -377,6 +377,9 @@ function deleteSendMoneyParticleEffect() { } function onUsernameChanged() { + if (ui.checkIsOpen()) { + ui.open(WALLET_QML_SOURCE); + } } var MARKETPLACE_QML_PATH = "hifi/commerce/marketplace/Marketplace.qml"; diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index e059081741..38287e3af3 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -615,11 +615,10 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { openMarketplace(message.itemId, message.itemEdition); break; case 'passphrasePopup_cancelClicked': - case 'needsLogIn_cancelClicked': // Should/must NOT check for wallet setup. openMarketplace(); break; - case 'needsLogIn_loginClicked': + case 'marketplace_loginClicked': openLoginWindow(); break; case 'disableHmdPreview': From 0173c87695f60b5430d205e6b703348f6d356091 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 22 Mar 2019 02:43:06 +0100 Subject: [PATCH 083/135] Fix clashing hyperlinks in AvatarPackager project page --- interface/resources/qml/hifi/avatarPackager/AvatarProject.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index bf8c06d1b3..e5bffa7829 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -339,8 +339,8 @@ Item { visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.hasErrors anchors { - top: notForSaleMessage.bottom - topMargin: 16 + top: notForSaleMessage.visible ? notForSaleMessage.bottom : infoMessage .bottom + bottom: showFilesText.top horizontalCenter: parent.horizontalCenter } From 46f897b69330e92e1a41d9130b414afd2a0eceea Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 25 Mar 2019 15:42:30 -0700 Subject: [PATCH 084/135] Better estimate of avatar centre for zone membership --- assignment-client/src/avatars/AvatarMixerClientData.cpp | 9 +++------ assignment-client/src/avatars/AvatarMixerClientData.h | 1 - assignment-client/src/avatars/MixerAvatar.h | 3 +++ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 4880f73226..2175018824 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -23,9 +23,6 @@ #include "AvatarMixerSlave.h" -// Offset from reported position for priority-zone purposes: -const glm::vec3 AvatarMixerClientData::AVATAR_CENTER_OFFSET { 0.0f, 1.0f, 0.0 }; - AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) : NodeData(nodeID, nodeLocalID) { // in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID @@ -132,7 +129,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared incrementNumOutOfOrderSends(); } _lastReceivedSequenceNumber = sequenceNumber; - glm::vec3 oldPosition = getPosition(); + glm::vec3 oldPosition = _avatar->getCentroidPosition(); bool oldHasPriority = _avatar->getHasPriority(); // compute the offset to the data payload @@ -143,10 +140,10 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared // Regardless of what the client says, restore the priority as we know it without triggering any update. _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); - auto newPosition = getPosition(); + auto newPosition = _avatar->getCentroidPosition(); if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; - FindPriorityZone findPriorityZone { newPosition + AVATAR_CENTER_OFFSET } ; + FindPriorityZone findPriorityZone { newPosition } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); _avatar->setNeedsHeroCheck(false); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 492dfc4720..98c8d7e15b 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -223,7 +223,6 @@ private: PerNodeTraitVersions _perNodeSentTraitVersions; std::atomic_bool _isIgnoreRadiusEnabled { false }; - static const glm::vec3 AVATAR_CENTER_OFFSET; }; #endif // hifi_AvatarMixerClientData_h diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 01e5e91b44..f812917614 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -22,6 +22,9 @@ public: bool getNeedsHeroCheck() const { return _needsHeroCheck; } void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } + // Bounding-box World centre: + glm::vec3 getCentroidPosition() const + { return getWorldPosition() + _globalBoundingBoxOffset; } private: bool _needsHeroCheck { false }; From 1057166418e4c0526c2caa6e7a02c6b06b4fd63c Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 10:44:59 -0700 Subject: [PATCH 085/135] Add master injector gain to audio-mixer --- .../src/audio/AudioMixerClientData.h | 3 + .../src/audio/AudioMixerSlave.cpp | 56 ++++++++++++------- assignment-client/src/audio/AudioMixerSlave.h | 7 ++- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 653749f619..f9d113c53d 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -84,6 +84,8 @@ public: float getMasterAvatarGain() const { return _masterAvatarGain; } void setMasterAvatarGain(float gain) { _masterAvatarGain = gain; } + float getMasterInjectorGain() const { return _masterInjectorGain; } + void setMasterInjectorGain(float gain) { _masterInjectorGain = gain; } AudioLimiter audioLimiter; @@ -189,6 +191,7 @@ private: int _frameToSendStats { 0 }; float _masterAvatarGain { 1.0f }; // per-listener mixing gain, applied only to avatars + float _masterInjectorGain { 1.0f }; // per-listener mixing gain, applied only to injectors CodecPluginPointer _codec; QString _selectedCodecName; diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index a920b45161..f7f8e8a9c1 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -50,7 +50,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& // mix helpers inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd); -inline float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream, +inline float computeGain(float masterAvatarGain, float masterInjectorGain, const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho); inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); @@ -338,8 +338,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } if (!isThrottling) { - updateHRTFParameters(stream, *listenerAudioStream, - listenerData->getMasterAvatarGain()); + updateHRTFParameters(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + listenerData->getMasterInjectorGain()); } return false; }); @@ -363,8 +363,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } if (!isThrottling) { - updateHRTFParameters(stream, *listenerAudioStream, - listenerData->getMasterAvatarGain()); + updateHRTFParameters(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + listenerData->getMasterInjectorGain()); } return false; }); @@ -381,13 +381,13 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { stream.approximateVolume = approximateVolume(stream, listenerAudioStream); } else { if (shouldBeSkipped(stream, *listener, *listenerAudioStream, *listenerData)) { - addStream(stream, *listenerAudioStream, 0.0f, isSoloing); + addStream(stream, *listenerAudioStream, 0.0f, 0.0f, isSoloing); streams.skipped.push_back(move(stream)); ++stats.activeToSkipped; return true; } - addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), listenerData->getMasterInjectorGain(), isSoloing); if (shouldBeInactive(stream)) { @@ -423,7 +423,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { return true; } - addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), listenerData->getMasterInjectorGain(), isSoloing); if (shouldBeInactive(stream)) { @@ -491,7 +491,9 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain, bool isSoloing) { + float masterAvatarGain, + float masterInjectorGain, + bool isSoloing) { ++stats.totalMixes; auto streamToAdd = mixableStream.positionalStream; @@ -504,9 +506,10 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre float distance = glm::max(glm::length(relativePosition), EPSILON); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); - float gain = masterListenerGain; + float gain = masterAvatarGain; if (!isSoloing) { - gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho); + gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, + distance, isEcho); } const int HRTF_DATASET_INDEX = 1; @@ -585,8 +588,9 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre } void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream, - AvatarAudioStream& listeningNodeStream, - float masterListenerGain) { + AvatarAudioStream& listeningNodeStream, + float masterAvatarGain, + float masterInjectorGain) { auto streamToAdd = mixableStream.positionalStream; // check if this is a server echo of a source back to itself @@ -595,7 +599,8 @@ void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho); + float gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, + distance, isEcho); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); mixableStream.hrtf->setParameterHistory(azimuth, distance, gain); @@ -720,6 +725,7 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi // injector: apply attenuation if (streamToAdd.getType() == PositionalAudioStream::Injector) { gain *= reinterpret_cast(&streamToAdd)->getAttenuationRatio(); + // injector: skip master gain } // avatar: skip attenuation - it is too costly to approximate @@ -729,16 +735,23 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi float distance = glm::length(relativePosition); return gain / distance; - // avatar: skip master gain - it is constant for all streams + // avatar: skip master gain } -float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho) { +float computeGain(float masterAvatarGain, + float masterInjectorGain, + const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, + const glm::vec3& relativePosition, + float distance, + bool isEcho) { float gain = 1.0f; // injector: apply attenuation if (streamToAdd.getType() == PositionalAudioStream::Injector) { gain *= reinterpret_cast(&streamToAdd)->getAttenuationRatio(); + // apply master gain + gain *= masterInjectorGain; // avatar: apply fixed off-axis attenuation to make them quieter as they turn away } else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) { @@ -754,8 +767,8 @@ float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNo gain *= offAxisCoefficient; - // apply master gain, only to avatars - gain *= masterListenerGain; + // apply master gain + gain *= masterAvatarGain; } auto& audioZones = AudioMixer::getAudioZones(); @@ -797,8 +810,9 @@ float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNo return gain; } -float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, - const glm::vec3& relativePosition) { +float computeAzimuth(const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, + const glm::vec3& relativePosition) { glm::quat inverseOrientation = glm::inverse(listeningNodeStream.getOrientation()); glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; diff --git a/assignment-client/src/audio/AudioMixerSlave.h b/assignment-client/src/audio/AudioMixerSlave.h index 3d979da1fc..9765ea8639 100644 --- a/assignment-client/src/audio/AudioMixerSlave.h +++ b/assignment-client/src/audio/AudioMixerSlave.h @@ -57,10 +57,13 @@ private: bool prepareMix(const SharedNodePointer& listener); void addStream(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain, bool isSoloing); + float masterAvatarGain, + float masterInjectorGain, + bool isSoloing); void updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain); + float masterAvatarGain, + float masterInjectorGain); void resetHRTFState(AudioMixerClientData::MixableStream& mixableStream); void addStreams(Node& listener, AudioMixerClientData& listenerData); From b15651f1ebcf161fb50253bc9333e906cd27454b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 12:05:51 -0700 Subject: [PATCH 086/135] Handle InjectorGainSet packet at the audio-mixer --- assignment-client/src/audio/AudioMixer.cpp | 1 + .../src/audio/AudioMixerClientData.cpp | 14 ++++++++++++++ assignment-client/src/audio/AudioMixerClientData.h | 1 + libraries/networking/src/udt/PacketHeaders.h | 2 +- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index f67c54239e..201e24d4b9 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -97,6 +97,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : PacketType::RadiusIgnoreRequest, PacketType::RequestsDomainListData, PacketType::PerAvatarGainSet, + PacketType::InjectorGainSet, PacketType::AudioSoloRequest }, this, "queueAudioPacket"); diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 90698bfac8..b8d3ec62a6 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -92,6 +92,9 @@ int AudioMixerClientData::processPackets(ConcurrentAddedStreams& addedStreams) { case PacketType::PerAvatarGainSet: parsePerAvatarGainSet(*packet, node); break; + case PacketType::InjectorGainSet: + parseInjectorGainSet(*packet, node); + break; case PacketType::NodeIgnoreRequest: parseNodeIgnoreRequest(packet, node); break; @@ -205,6 +208,17 @@ void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const } } +void AudioMixerClientData::parseInjectorGainSet(ReceivedMessage& message, const SharedNodePointer& node) { + QUuid uuid = node->getUUID(); + + uint8_t packedGain; + message.readPrimitive(&packedGain); + float gain = unpackFloatGainFromByte(packedGain); + + setMasterInjectorGain(gain); + qCDebug(audio) << "Setting MASTER injector gain for " << uuid << " to " << gain; +} + void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) { auto it = std::find_if(_streams.active.cbegin(), _streams.active.cend(), [nodeID](const MixableStream& mixableStream){ return mixableStream.nodeStreamID.nodeID == nodeID && mixableStream.nodeStreamID.streamID.isNull(); diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index f9d113c53d..4a1ca7f9b5 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -63,6 +63,7 @@ public: void negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node); void parseRequestsDomainListData(ReceivedMessage& message); void parsePerAvatarGainSet(ReceivedMessage& message, const SharedNodePointer& node); + void parseInjectorGainSet(ReceivedMessage& message, const SharedNodePointer& node); void parseNodeIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); void parseRadiusIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); void parseSoloRequest(QSharedPointer message, const SharedNodePointer& node); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 0ec7c40ca4..413ff14b17 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -57,7 +57,7 @@ public: ICEServerQuery, OctreeStats, SetAvatarTraits, - UNUSED_PACKET_TYPE, + InjectorGainSet, AssignmentClientStatus, NoisyMute, AvatarIdentity, From 755762e8ecf7a7a1d70f21ba6a6c15557f5f2914 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 15 Mar 2019 17:24:50 -0700 Subject: [PATCH 087/135] Send InjectorGainSet packet to the audio-mixer --- libraries/networking/src/NodeList.cpp | 25 +++++++++++++++++++++++++ libraries/networking/src/NodeList.h | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index e6eb6087b0..eec710322e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -265,6 +265,8 @@ void NodeList::reset(bool skipDomainHandlerReset) { _avatarGainMap.clear(); _avatarGainMapLock.unlock(); + _injectorGain = 0.0f; + if (!skipDomainHandlerReset) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); @@ -1087,6 +1089,29 @@ float NodeList::getAvatarGain(const QUuid& nodeID) { return 0.0f; } +void NodeList::setInjectorGain(float gain) { + auto audioMixer = soloNodeOfType(NodeType::AudioMixer); + if (audioMixer) { + // setup the packet + auto setInjectorGainPacket = NLPacket::create(PacketType::InjectorGainSet, sizeof(float), true); + + // We need to convert the gain in dB (from the script) to an amplitude before packing it. + setInjectorGainPacket->writePrimitive(packFloatGainToByte(fastExp2f(gain / 6.02059991f))); + + qCDebug(networking) << "Sending Set Injector Gain packet with Gain:" << gain; + + sendPacket(std::move(setInjectorGainPacket), *audioMixer); + _injectorGain = gain; + + } else { + qWarning() << "Couldn't find audio mixer to send set gain request"; + } +} + +float NodeList::getInjectorGain() { + return _injectorGain; +} + void NodeList::kickNodeBySessionID(const QUuid& nodeID) { // send a request to domain-server to kick the node with the given session ID // the domain-server will handle the persistence of the kick (via username or IP) diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index e135bc937d..d2a1212d64 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -83,6 +83,8 @@ public: bool isPersonalMutingNode(const QUuid& nodeID) const; void setAvatarGain(const QUuid& nodeID, float gain); float getAvatarGain(const QUuid& nodeID); + void setInjectorGain(float gain); + float getInjectorGain(); void kickNodeBySessionID(const QUuid& nodeID); void muteNodeBySessionID(const QUuid& nodeID); @@ -181,6 +183,8 @@ private: mutable QReadWriteLock _avatarGainMapLock; tbb::concurrent_unordered_map _avatarGainMap; + std::atomic _injectorGain { 0.0f }; + void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); #if defined(Q_OS_ANDROID) Setting::Handle _ignoreRadiusEnabled { "IgnoreRadiusEnabled", false }; From 4a6e495f5fa96eaa4772aaeb615035a050ec3a0f Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 20 Mar 2019 15:48:05 -0700 Subject: [PATCH 088/135] Add Users.setInjectorGain() and Users.getInjectorGain() to the scripting interface --- .../script-engine/src/UsersScriptingInterface.cpp | 9 +++++++++ .../script-engine/src/UsersScriptingInterface.h | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 9beb52f20a..7b30e087e5 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -51,6 +51,15 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { return DependencyManager::get()->getAvatarGain(nodeID); } +void UsersScriptingInterface::setInjectorGain(float gain) { + // ask the NodeList to set the audio injector gain + DependencyManager::get()->setInjectorGain(gain); +} + +float UsersScriptingInterface::getInjectorGain() { + return DependencyManager::get()->getInjectorGain(); +} + void UsersScriptingInterface::kick(const QUuid& nodeID) { if (_kickConfirmationOperator) { diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index f8ca974b8b..d6750b263d 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -97,6 +97,21 @@ public slots: */ float getAvatarGain(const QUuid& nodeID); + /**jsdoc + * Sets the audio injector gain at the server. + * Units are Decibels (dB) + * @function Users.setInjectorGain + * @param {number} gain (in dB) + */ + void setInjectorGain(float gain); + + /**jsdoc + * Gets the audio injector gain at the server. + * @function Users.getInjectorGain + * @returns {number} gain (in dB) + */ + float getInjectorGain(); + /**jsdoc * Kick/ban another user. Removes them from the server and prevents them from returning. Bans by either user name (if * available) or machine fingerprint otherwise. This will only do anything if you're an admin of the domain you're in. From a2d261d20ca1273b99ab90952945569baa43a153 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 21 Mar 2019 11:51:49 -0700 Subject: [PATCH 089/135] Move the new audio volume API from Users scripting interface to Audio scripting interface --- interface/src/scripting/Audio.cpp | 36 ++++++++++++++++--- interface/src/scripting/Audio.h | 30 ++++++++++++++++ .../src/UsersScriptingInterface.cpp | 9 ----- .../src/UsersScriptingInterface.h | 15 -------- 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index b1b5077e60..f9560c84f7 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -377,6 +377,18 @@ void Audio::handlePushedToTalk(bool enabled) { } } +void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { + withWriteLock([&] { + _devices.chooseInputDevice(device, isHMD); + }); +} + +void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { + withWriteLock([&] { + _devices.chooseOutputDevice(device, isHMD); + }); +} + void Audio::setReverb(bool enable) { withWriteLock([&] { DependencyManager::get()->setReverb(enable); @@ -389,14 +401,28 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { }); } -void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { +void Audio::setAvatarGain(float gain) { withWriteLock([&] { - _devices.chooseInputDevice(device, isHMD); + // ask the NodeList to set the master avatar gain + DependencyManager::get()->setAvatarGain("", gain); }); } -void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { - withWriteLock([&] { - _devices.chooseOutputDevice(device, isHMD); +float Audio::getAvatarGain() { + return resultWithReadLock([&] { + return DependencyManager::get()->getAvatarGain(""); + }); +} + +void Audio::setInjectorGain(float gain) { + withWriteLock([&] { + // ask the NodeList to set the audio injector gain + DependencyManager::get()->setInjectorGain(gain); + }); +} + +float Audio::getInjectorGain() { + return resultWithReadLock([&] { + return DependencyManager::get()->getInjectorGain(); }); } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 9ee230fc29..14a75d5ffe 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -170,6 +170,36 @@ public: */ Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); + /**jsdoc + * Sets the master avatar gain at the server. + * Units are Decibels (dB) + * @function Audio.setAvatarGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setAvatarGain(float gain); + + /**jsdoc + * Gets the master avatar gain at the server. + * @function Audio.getAvatarGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getAvatarGain(); + + /**jsdoc + * Sets the audio injector gain at the server. + * Units are Decibels (dB) + * @function Audio.setInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setInjectorGain(float gain); + + /**jsdoc + * Gets the audio injector gain at the server. + * @function Audio.getInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getInjectorGain(); + /**jsdoc * Starts making an audio recording of the audio being played in-world (i.e., not local-only audio) to a file in WAV format. * @function Audio.startRecording diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 7b30e087e5..9beb52f20a 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -51,15 +51,6 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { return DependencyManager::get()->getAvatarGain(nodeID); } -void UsersScriptingInterface::setInjectorGain(float gain) { - // ask the NodeList to set the audio injector gain - DependencyManager::get()->setInjectorGain(gain); -} - -float UsersScriptingInterface::getInjectorGain() { - return DependencyManager::get()->getInjectorGain(); -} - void UsersScriptingInterface::kick(const QUuid& nodeID) { if (_kickConfirmationOperator) { diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index d6750b263d..f8ca974b8b 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -97,21 +97,6 @@ public slots: */ float getAvatarGain(const QUuid& nodeID); - /**jsdoc - * Sets the audio injector gain at the server. - * Units are Decibels (dB) - * @function Users.setInjectorGain - * @param {number} gain (in dB) - */ - void setInjectorGain(float gain); - - /**jsdoc - * Gets the audio injector gain at the server. - * @function Users.getInjectorGain - * @returns {number} gain (in dB) - */ - float getInjectorGain(); - /**jsdoc * Kick/ban another user. Removes them from the server and prevents them from returning. Bans by either user name (if * available) or machine fingerprint otherwise. This will only do anything if you're an admin of the domain you're in. From 95b4f954a6a1ed2912a6c47ac2c5c6d86a29d8ea Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:12:31 -0700 Subject: [PATCH 090/135] Add AudioClient mixing gains for local injectors and system sounds --- libraries/audio-client/src/AudioClient.cpp | 4 +++- libraries/audio-client/src/AudioClient.h | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 9d645a1dbf..9fa9a0bc18 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1368,7 +1368,9 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - float gain = options.volume; + bool isSystemSound = !injector->isPositionSet() && !injector->isAmbisonic(); + + float gain = injector->getVolume() * (isSystemSound ? _systemInjectorGain : _localInjectorGain); if (options.ambisonic) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index a153f22bf3..7608bf5cdb 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -241,6 +241,8 @@ public slots: void setInputVolume(float volume, bool emitSignal = true); void setReverb(bool reverb); void setReverbOptions(const AudioEffectOptions* options); + void setLocalInjectorGain(float gain) { _localInjectorGain = gain; }; + void setSystemInjectorGain(float gain) { _systemInjectorGain = gain; }; void outputNotify(); @@ -395,6 +397,8 @@ private: int16_t* _outputScratchBuffer { NULL }; // for local audio (used by audio injectors thread) + std::atomic _localInjectorGain { 1.0f }; + std::atomic _systemInjectorGain { 1.0f }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; From 37429a07b8a7a2262582f4b77779d46db3debdc3 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:21:54 -0700 Subject: [PATCH 091/135] Add local injector gains to the Audio scripting interface --- interface/src/scripting/Audio.cpp | 34 ++++++++++++++++++++++++++ interface/src/scripting/Audio.h | 40 +++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index f9560c84f7..b3c7b25745 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -426,3 +426,37 @@ float Audio::getInjectorGain() { return DependencyManager::get()->getInjectorGain(); }); } + +void Audio::setLocalInjectorGain(float gain) { + withWriteLock([&] { + if (_localInjectorGain != gain) { + _localInjectorGain = gain; + // convert dB to amplitude + gain = fastExp2f(gain / 6.02059991f); + DependencyManager::get()->setLocalInjectorGain(gain); + } + }); +} + +float Audio::getLocalInjectorGain() { + return resultWithReadLock([&] { + return _localInjectorGain; + }); +} + +void Audio::setSystemInjectorGain(float gain) { + withWriteLock([&] { + if (_systemInjectorGain != gain) { + _systemInjectorGain = gain; + // convert dB to amplitude + gain = fastExp2f(gain / 6.02059991f); + DependencyManager::get()->setSystemInjectorGain(gain); + } + }); +} + +float Audio::getSystemInjectorGain() { + return resultWithReadLock([&] { + return _systemInjectorGain; + }); +} diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 14a75d5ffe..d6823ea452 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -171,7 +171,7 @@ public: Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); /**jsdoc - * Sets the master avatar gain at the server. + * Sets the avatar gain at the server. * Units are Decibels (dB) * @function Audio.setAvatarGain * @param {number} gain (in dB) @@ -179,14 +179,14 @@ public: Q_INVOKABLE void setAvatarGain(float gain); /**jsdoc - * Gets the master avatar gain at the server. + * Gets the avatar gain at the server. * @function Audio.getAvatarGain * @returns {number} gain (in dB) */ Q_INVOKABLE float getAvatarGain(); /**jsdoc - * Sets the audio injector gain at the server. + * Sets the injector gain at the server. * Units are Decibels (dB) * @function Audio.setInjectorGain * @param {number} gain (in dB) @@ -194,12 +194,42 @@ public: Q_INVOKABLE void setInjectorGain(float gain); /**jsdoc - * Gets the audio injector gain at the server. + * Gets the injector gain at the server. * @function Audio.getInjectorGain * @returns {number} gain (in dB) */ Q_INVOKABLE float getInjectorGain(); + /**jsdoc + * Sets the local injector gain in the client. + * Units are Decibels (dB) + * @function Audio.setLocalInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setLocalInjectorGain(float gain); + + /**jsdoc + * Gets the local injector gain in the client. + * @function Audio.getLocalInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getLocalInjectorGain(); + + /**jsdoc + * Sets the injector gain for system sounds. + * Units are Decibels (dB) + * @function Audio.setSystemInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setSystemInjectorGain(float gain); + + /**jsdoc + * Gets the injector gain for system sounds. + * @function Audio.getSystemInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getSystemInjectorGain(); + /**jsdoc * Starts making an audio recording of the audio being played in-world (i.e., not local-only audio) to a file in WAV format. * @function Audio.startRecording @@ -380,6 +410,8 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; + float _localInjectorGain { 0.0f }; // in dB + float _systemInjectorGain { 0.0f }; // in dB bool _isClipping { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _enableWarnWhenMuted { true }; From 155bd39da6339410682a3c2bfd8b1c2bcb16947f Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 10:24:30 -0700 Subject: [PATCH 092/135] Quantize and limit the local injector gains to match the network protocol --- interface/src/scripting/Audio.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index b3c7b25745..330ed7abfe 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -433,6 +433,8 @@ void Audio::setLocalInjectorGain(float gain) { _localInjectorGain = gain; // convert dB to amplitude gain = fastExp2f(gain / 6.02059991f); + // quantize and limit to match NodeList::setInjectorGain() + gain = unpackFloatGainFromByte(packFloatGainToByte(gain)); DependencyManager::get()->setLocalInjectorGain(gain); } }); @@ -450,6 +452,8 @@ void Audio::setSystemInjectorGain(float gain) { _systemInjectorGain = gain; // convert dB to amplitude gain = fastExp2f(gain / 6.02059991f); + // quantize and limit to match NodeList::setInjectorGain() + gain = unpackFloatGainFromByte(packFloatGainToByte(gain)); DependencyManager::get()->setSystemInjectorGain(gain); } }); From c15813b44225235a96fd507d031c6e6650d0eedb Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 22 Mar 2019 17:58:17 -0700 Subject: [PATCH 093/135] Cleanup --- assignment-client/src/audio/AudioMixerClientData.cpp | 6 +++--- interface/src/scripting/Audio.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index b8d3ec62a6..41b72c04d2 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -200,11 +200,11 @@ void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const if (avatarUUID.isNull()) { // set the MASTER avatar gain setMasterAvatarGain(gain); - qCDebug(audio) << "Setting MASTER avatar gain for " << uuid << " to " << gain; + qCDebug(audio) << "Setting MASTER avatar gain for" << uuid << "to" << gain; } else { // set the per-source avatar gain setGainForAvatar(avatarUUID, gain); - qCDebug(audio) << "Setting avatar gain adjustment for hrtf[" << uuid << "][" << avatarUUID << "] to " << gain; + qCDebug(audio) << "Setting avatar gain adjustment for hrtf[" << uuid << "][" << avatarUUID << "] to" << gain; } } @@ -216,7 +216,7 @@ void AudioMixerClientData::parseInjectorGainSet(ReceivedMessage& message, const float gain = unpackFloatGainFromByte(packedGain); setMasterInjectorGain(gain); - qCDebug(audio) << "Setting MASTER injector gain for " << uuid << " to " << gain; + qCDebug(audio) << "Setting MASTER injector gain for" << uuid << "to" << gain; } void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) { diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 330ed7abfe..4f2171d451 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -404,13 +404,13 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { void Audio::setAvatarGain(float gain) { withWriteLock([&] { // ask the NodeList to set the master avatar gain - DependencyManager::get()->setAvatarGain("", gain); + DependencyManager::get()->setAvatarGain(QUuid(), gain); }); } float Audio::getAvatarGain() { return resultWithReadLock([&] { - return DependencyManager::get()->getAvatarGain(""); + return DependencyManager::get()->getAvatarGain(QUuid()); }); } From 3d7c3e7b6f6fe3d737cc86e038966617397258f5 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 23 Mar 2019 06:48:37 -0700 Subject: [PATCH 094/135] Persist the audio-mixer settings across domain changes and server resets --- libraries/networking/src/NodeList.cpp | 38 ++++++++++++++++++--------- libraries/networking/src/NodeList.h | 3 ++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index eec710322e..0021a594bc 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -265,8 +265,6 @@ void NodeList::reset(bool skipDomainHandlerReset) { _avatarGainMap.clear(); _avatarGainMapLock.unlock(); - _injectorGain = 0.0f; - if (!skipDomainHandlerReset) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); @@ -1018,6 +1016,14 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { // also send them the current ignore radius state. sendIgnoreRadiusStateToNode(newNode); + + // also send the current avatar and injector gains + if (_avatarGain != 0.0f) { + setAvatarGain(QUuid(), _avatarGain); + } + if (_injectorGain != 0.0f) { + setInjectorGain(_injectorGain); + } } if (newNode->getType() == NodeType::AvatarMixer) { // this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session, @@ -1064,13 +1070,17 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { if (nodeID.isNull()) { qCDebug(networking) << "Sending Set MASTER Avatar Gain packet with Gain:" << gain; - } else { - qCDebug(networking) << "Sending Set Avatar Gain packet with UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; - } - sendPacket(std::move(setAvatarGainPacket), *audioMixer); - QWriteLocker lock{ &_avatarGainMapLock }; - _avatarGainMap[nodeID] = gain; + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + _avatarGain = gain; + + } else { + qCDebug(networking) << "Sending Set Avatar Gain packet with UUID:" << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; + + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + QWriteLocker lock{ &_avatarGainMapLock }; + _avatarGainMap[nodeID] = gain; + } } else { qWarning() << "Couldn't find audio mixer to send set gain request"; @@ -1081,10 +1091,14 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { } float NodeList::getAvatarGain(const QUuid& nodeID) { - QReadLocker lock{ &_avatarGainMapLock }; - auto it = _avatarGainMap.find(nodeID); - if (it != _avatarGainMap.cend()) { - return it->second; + if (nodeID.isNull()) { + return _avatarGain; + } else { + QReadLocker lock{ &_avatarGainMapLock }; + auto it = _avatarGainMap.find(nodeID); + if (it != _avatarGainMap.cend()) { + return it->second; + } } return 0.0f; } diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index d2a1212d64..f871560fba 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -183,7 +183,8 @@ private: mutable QReadWriteLock _avatarGainMapLock; tbb::concurrent_unordered_map _avatarGainMap; - std::atomic _injectorGain { 0.0f }; + std::atomic _avatarGain { 0.0f }; // in dB + std::atomic _injectorGain { 0.0f }; // in dB void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); #if defined(Q_OS_ANDROID) From 195472bd432f75e4c53ba4355307332f8ed049f6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 15 Mar 2019 11:42:10 -0700 Subject: [PATCH 095/135] make equipping of clonables more reliable --- .../controllers/controllerDispatcher.js | 42 ++++++++++++++++--- .../controllerModules/equipEntity.js | 4 +- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index f4c0cbb0d6..0a9fa4dce1 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -12,7 +12,7 @@ LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES, getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers, PointerManager, getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers, - PointerManager, print, Selection, DISPATCHER_HOVERING_LIST, DISPATCHER_HOVERING_STYLE, Keyboard + PointerManager, print, Keyboard */ controllerDispatcherPlugins = {}; @@ -53,6 +53,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.blacklist = []; this.pointerManager = new PointerManager(); this.grabSphereOverlays = [null, null]; + this.targetIDs = {}; // a module can occupy one or more "activity" slots while it's running. If all the required slots for a module are // not set to false (not in use), a module cannot start. When a module is using a slot, that module's name @@ -225,8 +226,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (tabletIndex !== -1 && closebyOverlays.indexOf(HMD.tabletID) === -1) { nearbyOverlays.splice(tabletIndex, 1); } - if (miniTabletIndex !== -1 - && ((closebyOverlays.indexOf(HMD.miniTabletID) === -1) || h !== HMD.miniTabletHand)) { + if (miniTabletIndex !== -1 && + ((closebyOverlays.indexOf(HMD.miniTabletID) === -1) || h !== HMD.miniTabletHand)) { nearbyOverlays.splice(miniTabletIndex, 1); } } @@ -336,6 +337,23 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }); } + // also make sure we have the properties from the current module's target + for (var tIDRunningPluginName in _this.runningPluginNames) { + if (_this.runningPluginNames.hasOwnProperty(tIDRunningPluginName)) { + var targetIDs = _this.targetIDs[tIDRunningPluginName]; + if (targetIDs) { + for (var k = 0; k < targetIDs.length; k++) { + var targetID = targetIDs[k]; + if (!nearbyEntityPropertiesByID[targetID]) { + var targetProps = Entities.getEntityProperties(targetID, DISPATCHER_PROPERTIES); + targetProps.id = targetID; + nearbyEntityPropertiesByID[targetID] = targetProps; + } + } + } + } + } + // bundle up all the data about the current situation var controllerData = { triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue], @@ -402,10 +420,23 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.beginProfileRange("dispatch.run." + runningPluginName); } var runningness = plugin.run(controllerData, deltaTime); + + if (DEBUG) { + if (JSON.stringify(_this.targetIDs[runningPluginName]) != JSON.stringify(runningness.targets)) { + print("controllerDispatcher targetIDs[" + runningPluginName + "] = " + + JSON.stringify(runningness.targets)); + } + } + + _this.targetIDs[runningPluginName] = runningness.targets; if (!runningness.active) { // plugin is finished running, for now. remove it from the list // of running plugins and mark its activity-slots as "not in use" delete _this.runningPluginNames[runningPluginName]; + delete _this.targetIDs[runningPluginName]; + if (DEBUG) { + print("controllerDispatcher deleted targetIDs[" + runningPluginName + "]"); + } _this.markSlots(plugin, false); _this.pointerManager.makePointerInvisible(plugin.parameters.handLaser); if (DEBUG) { @@ -527,8 +558,9 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } if (action === "tablet") { - var tabletIDs = message.blacklist - ? [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHighlightID] : []; + var tabletIDs = message.blacklist ? + [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHighlightID] : + []; if (message.hand === LEFT_HAND) { _this.leftBlacklistTabletIDs = tabletIDs; _this.setLeftBlacklist(); diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index b1c1bc7765..efb1594172 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -595,7 +595,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa equipHotspotBuddy.update(deltaTime, timestamp, controllerData); // if the potentialHotspot is cloneable, clone it and return it - // if the potentialHotspot os not cloneable and locked return null + // if the potentialHotspot is not cloneable and locked return null if (potentialEquipHotspot && (((this.triggerSmoothedSqueezed() || this.secondarySmoothedSqueezed()) && !this.waitForTriggerRelease) || this.messageGrabEntity)) { @@ -603,7 +603,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.targetEntityID = this.grabbedHotspot.entityID; this.startEquipEntity(controllerData); this.equipedWithSecondary = this.secondarySmoothedSqueezed(); - return makeRunningValues(true, [potentialEquipHotspot.entityID], []); + return makeRunningValues(true, [this.targetEntityID], []); } else { return makeRunningValues(false, [], []); } From 4303dd589f756fd1ee04afe58804787b2da08b34 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 16 Mar 2019 08:42:13 -0700 Subject: [PATCH 096/135] make sure equip attachment-points pulled from settings are reasonable before using them --- .../controllerModules/equipEntity.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index efb1594172..a10da110b3 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -7,10 +7,10 @@ /* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print, getControllerJointIndex, - enableDispatcherModule, disableDispatcherModule, entityIsFarGrabbedByOther, Messages, makeDispatcherModuleParameters, + enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, - entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, unhighlightTargetEntity, isInEditMode, getGrabbableData, - entityIsEquippable + entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, isInEditMode, getGrabbableData, + entityIsEquippable, HMD */ Script.include("/~/system/libraries/Xform.js"); @@ -183,8 +183,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var TRIGGER_OFF_VALUE = 0.1; var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab var BUMPER_ON_VALUE = 0.5; + var ATTACHPOINT_MAX_DISTANCE = 3.0; - var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}"; + // var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}"; var UNEQUIP_KEY = "u"; @@ -200,7 +201,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa indicatorOffset: props.grab.equippableIndicatorOffset }; } else { - return null + return null; } } @@ -231,6 +232,12 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; var joints = avatarSettingsData[hotspot.key]; if (joints) { + // make sure they are reasonable + if (joints[jointName] && joints[jointName][0] && + Vec3.length(joints[jointName][0]) > ATTACHPOINT_MAX_DISTANCE) { + print("equipEntity -- Warning: rejecting settings attachPoint " + Vec3.length(joints[jointName][0])); + return undefined; + } return joints[jointName]; } } From 84385e6061378e05ce63ff1e0a8a3998932ab13b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 16 Mar 2019 11:01:44 -0700 Subject: [PATCH 097/135] work around fb-21767 with a timer --- .../controllerModules/equipEntity.js | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index a10da110b3..54b56ff271 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -463,6 +463,8 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa }; this.startEquipEntity = function (controllerData) { + var _this = this; + this.dropGestureReset(); this.clearEquipHaptics(); Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); @@ -505,17 +507,23 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa if (entityIsCloneable(grabbedProperties)) { var cloneID = this.cloneHotspot(grabbedProperties, controllerData); this.targetEntityID = cloneID; - Entities.editEntity(this.targetEntityID, reparentProps); controllerData.nearbyEntityPropertiesByID[this.targetEntityID] = grabbedProperties; isClone = true; - } else if (!grabbedProperties.locked) { - Entities.editEntity(this.targetEntityID, reparentProps); - } else { + } else if (grabbedProperties.locked) { this.grabbedHotspot = null; this.targetEntityID = null; return; } + + // HACK -- when + // https://highfidelity.fogbugz.com/f/cases/21767/entity-edits-shortly-after-an-add-often-fail + // is resolved, this can just be an editEntity rather than a setTimeout. + this.editDelayTimeout = Script.setTimeout(function () { + _this.editDelayTimeout = null; + Entities.editEntity(_this.targetEntityID, reparentProps); + }, 100); + // we don't want to send startEquip message until the trigger is released. otherwise, // guns etc will fire right as they are equipped. this.shouldSendStart = true; @@ -526,7 +534,6 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); - var _this = this; var grabEquipCheck = function() { var args = [_this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(_this.targetEntityID, "startEquip", args); @@ -539,6 +546,12 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa }; this.endEquipEntity = function () { + + if (this.editDelayTimeout) { + Script.clearTimeout(this.editDelayTimeout); + this.editDelayTimeout = null; + } + this.storeAttachPointInSettings(); Entities.editEntity(this.targetEntityID, { parentID: Uuid.NULL, From 7b56bef83815b230cda13711165f60e15c7bfd58 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 23 Mar 2019 16:00:02 -0700 Subject: [PATCH 098/135] Prototype an updated Audio tab with 3 independent volume controls --- interface/resources/qml/hifi/audio/Audio.qml | 158 ++++++++++++++++-- .../qml/hifi/audio/LoopbackAudio.qml | 16 +- .../qml/hifi/audio/PlaySampleSound.qml | 16 +- 3 files changed, 158 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index cd0f290da4..8fdd0368e2 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -87,8 +87,19 @@ Rectangle { } function updateMyAvatarGainFromQML(sliderValue, isReleased) { - if (Users.getAvatarGain(myAvatarUuid) != sliderValue) { - Users.setAvatarGain(myAvatarUuid, sliderValue); + if (AudioScriptingInterface.getAvatarGain() != sliderValue) { + AudioScriptingInterface.setAvatarGain(sliderValue); + } + } + function updateInjectorGainFromQML(sliderValue, isReleased) { + if (AudioScriptingInterface.getInjectorGain() != sliderValue) { + AudioScriptingInterface.setInjectorGain(sliderValue); // server side + AudioScriptingInterface.setLocalInjectorGain(sliderValue); // client side + } + } + function updateSystemInjectorGainFromQML(sliderValue, isReleased) { + if (AudioScriptingInterface.getSystemInjectorGain() != sliderValue) { + AudioScriptingInterface.setSystemInjectorGain(sliderValue); } } @@ -334,6 +345,7 @@ Rectangle { color: hifi.colors.white; text: qsTr("Choose input device"); } + } ListView { @@ -462,22 +474,22 @@ Rectangle { } Item { - id: gainContainer + id: avatarGainContainer x: margins.paddings; anchors.top: outputView.bottom; anchors.topMargin: 10; width: parent.width - margins.paddings*2 - height: gainSliderTextMetrics.height + height: avatarGainSliderTextMetrics.height HifiControlsUit.Slider { - id: gainSlider + id: avatarGainSlider anchors.right: parent.right height: parent.height width: 200 minimumValue: -60.0 maximumValue: 20.0 stepSize: 5 - value: Users.getAvatarGain(myAvatarUuid) + value: AudioScriptingInterface.getAvatarGain() onValueChanged: { updateMyAvatarGainFromQML(value, false); } @@ -493,7 +505,7 @@ Rectangle { // Do nothing. } onDoubleClicked: { - gainSlider.value = 0.0 + avatarGainSlider.value = 0.0 } onPressed: { // Pass through to Slider @@ -507,13 +519,13 @@ Rectangle { } } TextMetrics { - id: gainSliderTextMetrics - text: gainSliderText.text - font: gainSliderText.font + id: avatarGainSliderTextMetrics + text: avatarGainSliderText.text + font: avatarGainSliderText.font } RalewayRegular { // The slider for my card is special, it controls the master gain - id: gainSliderText; + id: avatarGainSliderText; text: "Avatar volume"; size: 16; anchors.left: parent.left; @@ -523,15 +535,129 @@ Rectangle { } } + Item { + id: injectorGainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: injectorGainSliderTextMetrics.height + + HifiControlsUit.Slider { + id: injectorGainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: AudioScriptingInterface.getInjectorGain() + onValueChanged: { + updateInjectorGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateInjectorGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + injectorGainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: injectorGainSliderTextMetrics + text: injectorGainSliderText.text + font: injectorGainSliderText.font + } + RalewayRegular { + id: injectorGainSliderText; + text: "Environment volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + } + + Item { + id: systemInjectorGainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: systemInjectorGainSliderTextMetrics.height + + HifiControlsUit.Slider { + id: systemInjectorGainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: AudioScriptingInterface.getSystemInjectorGain() + onValueChanged: { + updateSystemInjectorGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateSystemInjectorGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + systemInjectorGainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: systemInjectorGainSliderTextMetrics + text: systemInjectorGainSliderText.text + font: systemInjectorGainSliderText.font + } + RalewayRegular { + id: systemInjectorGainSliderText; + text: "System Sound volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + } + AudioControls.PlaySampleSound { id: playSampleSound x: margins.paddings - anchors.top: gainContainer.bottom; + anchors.top: systemInjectorGainContainer.bottom; anchors.topMargin: 10; - - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { left: parent.left; leftMargin: margins.paddings } } } } diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 8ec0ffc496..74bc0f67dc 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -44,7 +44,7 @@ RowLayout { } HifiControlsUit.Button { - text: audioLoopedBack ? qsTr("STOP TESTING YOUR VOICE") : qsTr("TEST YOUR VOICE"); + text: audioLoopedBack ? qsTr("STOP TESTING") : qsTr("TEST YOUR VOICE"); color: audioLoopedBack ? hifi.buttons.red : hifi.buttons.blue; onClicked: { if (audioLoopedBack) { @@ -57,11 +57,11 @@ RowLayout { } } - RalewayRegular { - Layout.leftMargin: 2; - size: 14; - color: "white"; - font.italic: true - text: audioLoopedBack ? qsTr("Speak in your input") : ""; - } +// RalewayRegular { +// Layout.leftMargin: 2; +// size: 14; +// color: "white"; +// font.italic: true +// text: audioLoopedBack ? qsTr("Speak in your input") : ""; +// } } diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index b9d9727dab..0eb78f3efe 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -56,16 +56,16 @@ RowLayout { HifiConstants { id: hifi; } HifiControlsUit.Button { - text: isPlaying ? qsTr("STOP TESTING YOUR SOUND") : qsTr("TEST YOUR SOUND"); + text: isPlaying ? qsTr("STOP TESTING") : qsTr("TEST YOUR SOUND"); color: isPlaying ? hifi.buttons.red : hifi.buttons.blue; onClicked: isPlaying ? stopSound() : playSound(); } - RalewayRegular { - Layout.leftMargin: 2; - size: 14; - color: "white"; - font.italic: true - text: isPlaying ? qsTr("Listen to your output") : ""; - } +// RalewayRegular { +// Layout.leftMargin: 2; +// size: 14; +// color: "white"; +// font.italic: true +// text: isPlaying ? qsTr("Listen to your output") : ""; +// } } From a2d754cebd786741ec7d714576d4462ee3618177 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Mar 2019 10:26:29 -0700 Subject: [PATCH 099/135] fixing audio screen with master --- interface/resources/qml/hifi/audio/Audio.qml | 4 ++++ .../qml/hifi/audio/LoopbackAudio.qml | 19 +++++++++++-------- .../qml/hifi/audio/PlaySampleSound.qml | 17 ++++++++++------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 8fdd0368e2..fe86d7d930 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -540,6 +540,8 @@ Rectangle { x: margins.paddings; width: parent.width - margins.paddings*2 height: injectorGainSliderTextMetrics.height + anchors.top: avatarGainContainer.bottom; + anchors.topMargin: 10; HifiControlsUit.Slider { id: injectorGainSlider @@ -599,6 +601,8 @@ Rectangle { x: margins.paddings; width: parent.width - margins.paddings*2 height: systemInjectorGainSliderTextMetrics.height + anchors.top: injectorGainContainer.bottom; + anchors.topMargin: 10; HifiControlsUit.Slider { id: systemInjectorGainSlider diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 74bc0f67dc..8d1099d38c 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -44,8 +44,11 @@ RowLayout { } HifiControlsUit.Button { - text: audioLoopedBack ? qsTr("STOP TESTING") : qsTr("TEST YOUR VOICE"); + text: audioLoopedBack ? qsTr("STOP TESTING VOICE") : qsTr("TEST YOUR VOICE"); color: audioLoopedBack ? hifi.buttons.red : hifi.buttons.blue; + fontSize: 15; + width: 200; + height: 32; onClicked: { if (audioLoopedBack) { loopbackTimer.stop(); @@ -57,11 +60,11 @@ RowLayout { } } -// RalewayRegular { -// Layout.leftMargin: 2; -// size: 14; -// color: "white"; -// font.italic: true -// text: audioLoopedBack ? qsTr("Speak in your input") : ""; -// } + RalewayRegular { + Layout.leftMargin: 2; + size: 14; + color: "white"; + font.italic: true + text: audioLoopedBack ? qsTr("Speak in your input") : ""; + } } diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index 0eb78f3efe..4675f6087a 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -59,13 +59,16 @@ RowLayout { text: isPlaying ? qsTr("STOP TESTING") : qsTr("TEST YOUR SOUND"); color: isPlaying ? hifi.buttons.red : hifi.buttons.blue; onClicked: isPlaying ? stopSound() : playSound(); + fontSize: 15; + width: 200; + height: 32; } -// RalewayRegular { -// Layout.leftMargin: 2; -// size: 14; -// color: "white"; -// font.italic: true -// text: isPlaying ? qsTr("Listen to your output") : ""; -// } + RalewayRegular { + Layout.leftMargin: 2; + size: 14; + color: "white"; + font.italic: true + text: isPlaying ? qsTr("Listen to your output") : ""; + } } From da6ca38282706ba272b88e47cb6f7155e2839d0c Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 26 Mar 2019 10:48:59 -0700 Subject: [PATCH 100/135] Revert to using avatar's _globalPosition for zone membership According to Tony this should be the hip position, i.e. a joint pos in T-pose, not neccesarily OK root. Also SpatiallyNestable::WorldPos may depend on parent entity and so not known by mixer. --- assignment-client/src/avatars/AvatarMixerClientData.cpp | 4 ++-- assignment-client/src/avatars/MixerAvatar.h | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 2175018824..0dbefb0109 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -129,7 +129,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared incrementNumOutOfOrderSends(); } _lastReceivedSequenceNumber = sequenceNumber; - glm::vec3 oldPosition = _avatar->getCentroidPosition(); + glm::vec3 oldPosition = _avatar->getClientGlobalPosition(); bool oldHasPriority = _avatar->getHasPriority(); // compute the offset to the data payload @@ -140,7 +140,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared // Regardless of what the client says, restore the priority as we know it without triggering any update. _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); - auto newPosition = _avatar->getCentroidPosition(); + auto newPosition = _avatar->getClientGlobalPosition(); if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; FindPriorityZone findPriorityZone { newPosition } ; diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index f812917614..01e5e91b44 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -22,9 +22,6 @@ public: bool getNeedsHeroCheck() const { return _needsHeroCheck; } void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } - // Bounding-box World centre: - glm::vec3 getCentroidPosition() const - { return getWorldPosition() + _globalBoundingBoxOffset; } private: bool _needsHeroCheck { false }; From c0b71150eafa60947b5a1390ac8c18795cb25dd8 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Mar 2019 10:58:04 -0700 Subject: [PATCH 101/135] removing an extra button --- interface/resources/qml/hifi/audio/Audio.qml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 69cbad458b..195dd78a0e 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -438,14 +438,6 @@ Rectangle { color: hifi.colors.white; text: qsTr("Choose output device"); } - - AudioControls.PlaySampleSound { - x: margins.paddings - - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { right: parent.right } - } } ListView { From 08c6acdf99cac8ab9d743f2876386316490a1fa1 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 26 Mar 2019 11:21:13 -0700 Subject: [PATCH 102/135] Improve blendshape precision for small models For avatars authored in meters, the max component length would often be less then 1. In that case, the blendshape offset was not normalized before quantization, resulting in loss of precision. This change will normalize the offset for all cases, except when the max component length is 0. --- libraries/render-utils/src/Model.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 30c4000bc7..b62b5f92f4 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1648,7 +1648,7 @@ using packBlendshapeOffsetTo = void(glm::uvec4& packed, const BlendshapeOffsetUn void packBlendshapeOffsetTo_Pos_F32_3xSN10_Nor_3xSN10_Tan_3xSN10(glm::uvec4& packed, const BlendshapeOffsetUnpacked& unpacked) { float len = glm::compMax(glm::abs(unpacked.positionOffset)); glm::vec3 normalizedPos(unpacked.positionOffset); - if (len > 1.0f) { + if (len > 0.0f) { normalizedPos /= len; } else { len = 1.0f; From 7d212222321e8ee812801070d01aaab74edd8c86 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Mar 2019 16:03:56 -0700 Subject: [PATCH 103/135] the test-audio button causes a local audio-loopback rather than a server one --- interface/resources/qml/hifi/audio/LoopbackAudio.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 8ec0ffc496..255617824b 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -21,13 +21,13 @@ RowLayout { function startAudioLoopback() { if (!audioLoopedBack) { audioLoopedBack = true; - AudioScriptingInterface.setServerEcho(true); + AudioScriptingInterface.setLocalEcho(true); } } function stopAudioLoopback() { if (audioLoopedBack) { audioLoopedBack = false; - AudioScriptingInterface.setServerEcho(false); + AudioScriptingInterface.setLocalEcho(false); } } From 16f842b2ace237bbdfd635a8536ab8426076ff18 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Mar 2019 16:07:18 -0700 Subject: [PATCH 104/135] missed one --- interface/resources/qml/hifi/audio/LoopbackAudio.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 255617824b..578d222eb2 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -17,7 +17,7 @@ import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit RowLayout { - property bool audioLoopedBack: AudioScriptingInterface.getServerEcho(); + property bool audioLoopedBack: AudioScriptingInterface.getLocalEcho(); function startAudioLoopback() { if (!audioLoopedBack) { audioLoopedBack = true; From 5f8139a44c4f1bdf398de68915460c47cbcca629 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Mar 2019 16:56:10 -0700 Subject: [PATCH 105/135] audio-loopback test ignores mute --- libraries/audio-client/src/AudioClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 9d645a1dbf..0bb7256bf6 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1052,7 +1052,7 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); - if (_muted || !_audioOutput || (!_shouldEchoLocally && !hasReverb)) { + if ((_muted && !_shouldEchoLocally) || !_audioOutput || (!_shouldEchoLocally && !hasReverb)) { return; } From b8f79d33649b0fa452dd915df48ba56f512fc166 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 26 Mar 2019 17:41:22 -0700 Subject: [PATCH 106/135] Guard against Node linked-data being null --- assignment-client/src/avatars/AvatarMixer.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 32fb5b9b1a..5f7e197c8f 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -265,8 +265,11 @@ void AvatarMixer::start() { nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { std::for_each(cbegin, cend, [](const SharedNodePointer& node) { if (node->getType() == NodeType::Agent) { - auto& avatar = static_cast(node->getLinkedData())->getAvatar(); - avatar.setNeedsHeroCheck(); + NodeData* nodeData = node->getLinkedData(); + if (nodeData) { + auto& avatar = static_cast(nodeData)->getAvatar(); + avatar.setNeedsHeroCheck(); + } } }); }); From 29af3b16126690d6b2b678dd98b682031fefc8ff Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 27 Feb 2019 10:47:42 -0800 Subject: [PATCH 107/135] add a button to Avatar panel to lock or unlock wearables. allow grabbing / adjusting others' wearables if they are unlocked. --- interface/resources/qml/hifi/AvatarApp.qml | 17 +++ .../+android_interface/HifiConstants.qml | 1 + .../resources/qml/stylesUit/HifiConstants.qml | 1 + interface/src/avatar/MyAvatar.h | 2 +- interface/src/avatar/OtherAvatar.cpp | 2 +- .../src/avatars-renderer/Avatar.cpp | 30 +++++- .../src/avatars-renderer/Avatar.h | 3 +- scripts/system/avatarapp.js | 102 ++++++++++++------ .../libraries/controllerDispatcherUtils.js | 2 - 9 files changed, 116 insertions(+), 44 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 753b9c5a81..a57b5713ed 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -16,6 +16,8 @@ Rectangle { property bool keyboardRaised: false property bool punctuationMode: false + HifiConstants { id: hifi } + HifiControls.Keyboard { id: keyboard z: 1000 @@ -48,6 +50,7 @@ Rectangle { property var jointNames: [] property var currentAvatarSettings; + property bool wearablesLocked; function fetchAvatarModelName(marketId, avatar) { var xmlhttp = new XMLHttpRequest(); @@ -187,6 +190,8 @@ Rectangle { updateCurrentAvatarInBookmarks(currentAvatar); } else if (message.method === 'selectAvatarEntity') { adjustWearables.selectWearableByID(message.entityID); + } else if (message.method === 'wearablesLockedChanged') { + wearablesLocked = message.wearablesLocked; } } @@ -507,6 +512,7 @@ Rectangle { } SquareLabel { + id: adjustLabel anchors.right: parent.right anchors.verticalCenter: wearablesLabel.verticalCenter glyphText: "\ue02e" @@ -515,6 +521,17 @@ Rectangle { adjustWearables.open(currentAvatar); } } + + SquareLabel { + anchors.right: adjustLabel.left + anchors.verticalCenter: wearablesLabel.verticalCenter + anchors.rightMargin: 15 + glyphText: wearablesLocked ? hifi.glyphs.lock : hifi.glyphs.unlock; + + onClicked: { + emitSendToScript({'method' : 'toggleWearablesLock'}); + } + } } Rectangle { diff --git a/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml index d5fab57501..995af90f0b 100644 --- a/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml +++ b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml @@ -344,6 +344,7 @@ Item { readonly property string stop_square: "\ue01e" readonly property string avatarTPose: "\ue01f" readonly property string lock: "\ue006" + readonly property string unlock: "\ue039" readonly property string checkmark: "\ue020" readonly property string leftRightArrows: "\ue021" readonly property string hfc: "\ue022" diff --git a/interface/resources/qml/stylesUit/HifiConstants.qml b/interface/resources/qml/stylesUit/HifiConstants.qml index 75f028cd4f..2394b36106 100644 --- a/interface/resources/qml/stylesUit/HifiConstants.qml +++ b/interface/resources/qml/stylesUit/HifiConstants.qml @@ -330,6 +330,7 @@ QtObject { readonly property string stop_square: "\ue01e" readonly property string avatarTPose: "\ue01f" readonly property string lock: "\ue006" + readonly property string unlock: "\ue039" readonly property string checkmark: "\ue020" readonly property string leftRightArrows: "\ue021" readonly property string hfc: "\ue022" diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index edb686a6a6..3f60b9cada 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -2170,7 +2170,7 @@ private: bool getEnableStepResetRotation() const { return _stepResetRotationEnabled; } void setEnableDrawAverageFacing(bool drawAverage) { _drawAverageFacingEnabled = drawAverage; } bool getEnableDrawAverageFacing() const { return _drawAverageFacingEnabled; } - bool isMyAvatar() const override { return true; } + virtual bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; int _skeletonModelChangeCount { 0 }; diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 11eb6542c4..2058408596 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -365,7 +365,7 @@ void OtherAvatar::handleChangedAvatarEntityData() { // AVATAR ENTITY UPDATE FLOW // - if queueEditEntityMessage() sees "AvatarEntity" HostType it calls _myAvatar->storeAvatarEntityDataPayload() // - storeAvatarEntityDataPayload() saves the payload and flags the trait instance for the entity as updated, - // - ClientTraitsHandler::sendChangedTraitsToMixea() sends the entity bytes to the mixer which relays them to other interfaces + // - ClientTraitsHandler::sendChangedTraitsToMixer() sends the entity bytes to the mixer which relays them to other interfaces // - AvatarHashMap::processBulkAvatarTraits() on other interfaces calls avatar->processTraitInstance() // - AvatarData::processTraitInstance() calls storeAvatarEntityDataPayload(), which sets _avatarEntityDataChanged = true // - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _avatarEntityDataChanged diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 992ee5db96..8cff1cc52a 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -372,11 +372,22 @@ bool Avatar::applyGrabChanges() { target->removeGrab(grab); _avatarGrabs.erase(itr); grabAddedOrRemoved = true; - if (isMyAvatar()) { - const EntityItemPointer& entity = std::dynamic_pointer_cast(target); - if (entity && entity->getEntityHostType() == entity::HostType::AVATAR && entity->getSimulationOwner().getID() == getID()) { - EntityItemProperties properties = entity->getProperties(); - sendPacket(entity->getID()); + const EntityItemPointer& entity = std::dynamic_pointer_cast(target); + if (entity && entity->getEntityHostType() == entity::HostType::AVATAR) { + // grabs are able to move avatar-entities which belong ot other avatars (assuming + // the entities are grabbable, unlocked, etc). Regardless of who released the grab + // on this entity, the entity's owner needs to send off an update. + QUuid entityOwnerID = entity->getOwningAvatarID(); + if (entityOwnerID == getMyAvatarID() || entityOwnerID == AVATAR_SELF_ID) { + bool success; + SpatiallyNestablePointer myAvatarSN = SpatiallyNestable::findByID(entityOwnerID, success); + if (success) { + std::shared_ptr myAvatar = std::dynamic_pointer_cast(myAvatarSN); + if (myAvatar) { + EntityItemProperties properties = entity->getProperties(); + myAvatar->sendPacket(entity->getID(), properties); + } + } } } } else { @@ -2103,3 +2114,12 @@ void Avatar::updateDescendantRenderIDs() { } }); } + +QUuid Avatar::getMyAvatarID() const { + auto nodeList = DependencyManager::get(); + if (nodeList) { + return nodeList->getSessionUUID(); + } else { + return QUuid(); + } +} diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index b4683d6032..2bed13cb12 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -180,7 +180,8 @@ public: /// Returns the distance to use as a LOD parameter. float getLODDistance() const; - virtual bool isMyAvatar() const override { return false; } + QUuid getMyAvatarID() const; + virtual void createOrb() { } enum class LoadingStatus { diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index fb61b914a3..429053bb8b 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -1,6 +1,8 @@ "use strict"; /*jslint vars:true, plusplus:true, forin:true*/ -/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ +/*global Tablet, Script, Entities, MyAvatar, Camera, Quat, HMD, Account, UserActivityLogger, Messages, print, + AvatarBookmarks, ContextOverlay, AddressManager +*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // avatarapp.js @@ -14,7 +16,7 @@ (function() { // BEGIN LOCAL_SCOPE -var request = Script.require('request').request; +// var request = Script.require('request').request; var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml"; Script.include("/~/system/libraries/controllers.js"); @@ -22,7 +24,7 @@ Script.include("/~/system/libraries/controllers.js"); var ENTRY_AVATAR_URL = "avatarUrl"; var ENTRY_AVATAR_ENTITIES = "avatarEntites"; var ENTRY_AVATAR_SCALE = "avatarScale"; -var ENTRY_VERSION = "version"; +// var ENTRY_VERSION = "version"; function executeLater(callback) { Script.setTimeout(callback, 300); @@ -44,7 +46,7 @@ function getMyAvatarWearables() { } var localRotation = entity.properties.localRotation; - entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation) + entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation); wearablesArray.push(entity); } @@ -52,7 +54,7 @@ function getMyAvatarWearables() { } function getMyAvatar() { - var avatar = {} + var avatar = {}; avatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL; avatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale(); avatar[ENTRY_AVATAR_ENTITIES] = getMyAvatarWearables(); @@ -68,7 +70,7 @@ function getMyAvatarSettings() { collisionSoundUrl : MyAvatar.collisionSoundURL, animGraphUrl: MyAvatar.getAnimGraphUrl(), animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(), - } + }; } function updateAvatarWearables(avatar, callback, wearablesOverride) { @@ -76,7 +78,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { var wearables = wearablesOverride ? wearablesOverride : getMyAvatarWearables(); avatar[ENTRY_AVATAR_ENTITIES] = wearables; - sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}) + sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); if(callback) callback(); @@ -101,7 +103,7 @@ var adjustWearables = { this.opened = value; } } -} +}; var currentAvatarWearablesBackup = null; var currentAvatar = null; @@ -112,7 +114,7 @@ function onTargetScaleChanged() { if(currentAvatar.scale !== MyAvatar.getAvatarScale()) { currentAvatar.scale = MyAvatar.getAvatarScale(); if(notifyScaleChanged) { - sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale}) + sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale}); } } } @@ -126,7 +128,7 @@ function onSkeletonModelURLChanged() { function onDominantHandChanged(dominantHand) { if(currentAvatarSettings.dominantHand !== dominantHand) { currentAvatarSettings.dominantHand = dominantHand; - sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand}) + sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand}); } } @@ -140,32 +142,33 @@ function onHmdAvatarAlignmentTypeChanged(type) { function onCollisionsEnabledChanged(enabled) { if(currentAvatarSettings.collisionsEnabled !== enabled) { currentAvatarSettings.collisionsEnabled = enabled; - sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled}) + sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled}); } } function onOtherAvatarsCollisionsEnabledChanged(enabled) { if (currentAvatarSettings.otherAvatarsCollisionsEnabled !== enabled) { currentAvatarSettings.otherAvatarsCollisionsEnabled = enabled; - sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled }) + sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled }); } } function onNewCollisionSoundUrl(url) { if(currentAvatarSettings.collisionSoundUrl !== url) { currentAvatarSettings.collisionSoundUrl = url; - sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url}) + sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url}); } } function onAnimGraphUrlChanged(url) { if (currentAvatarSettings.animGraphUrl !== url) { currentAvatarSettings.animGraphUrl = url; - sendToQml({ 'method': 'settingChanged', 'name': 'animGraphUrl', 'value': currentAvatarSettings.animGraphUrl }) + sendToQml({ 'method': 'settingChanged', 'name': 'animGraphUrl', 'value': currentAvatarSettings.animGraphUrl }); if (currentAvatarSettings.animGraphOverrideUrl !== MyAvatar.getAnimGraphOverrideUrl()) { currentAvatarSettings.animGraphOverrideUrl = MyAvatar.getAnimGraphOverrideUrl(); - sendToQml({ 'method': 'settingChanged', 'name': 'animGraphOverrideUrl', 'value': currentAvatarSettings.animGraphOverrideUrl }) + sendToQml({ 'method': 'settingChanged', 'name': 'animGraphOverrideUrl', + 'value': currentAvatarSettings.animGraphOverrideUrl }); } } } @@ -178,6 +181,33 @@ var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); +function getWearablesLocked() { + var wearablesLocked = true; + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + if (isGrabbable(wearable.id)) { + wearablesLocked = false; + } + }); + + return wearablesLocked; +} + +function lockWearables() { + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + setGrabbable(wearable.id, false); + }); +} + +function unlockWearables() { + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + setGrabbable(wearable.id, true); + }); +} + + function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. switch (message.method) { case 'getAvatars': @@ -201,7 +231,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } } - sendToQml(message) + sendToQml(message); break; case 'selectAvatar': Entities.addingWearable.disconnect(onAddingWearable); @@ -228,7 +258,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See message.properties.localRotationAngles = Quat.safeEulerAngles(message.properties.localRotation); } - sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false}) + sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false}); break; case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); @@ -305,11 +335,11 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See var currentAvatarURL = MyAvatar.getFullAvatarURLFromPreferences(); if(currentAvatarURL !== message.avatarURL) { MyAvatar.useFullAvatarURL(message.avatarURL); - sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL}) + sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL}); } break; case 'navigate': - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system") + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if(message.url.indexOf('app://') === 0) { if (message.url === 'app://marketplace') { tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); @@ -345,7 +375,17 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl); - settings = getMyAvatarSettings(); + currentAvatarSettings = getMyAvatarSettings(); + break; + case 'toggleWearablesLock': + var wearablesLocked = getWearablesLocked(); + wearablesLocked = !wearablesLocked; + if (wearablesLocked) { + lockWearables(); + } else { + unlockWearables(); + } + sendToQml({'method' : 'wearablesLockedChanged', 'wearablesLocked' : wearablesLocked}); break; default: print('Unrecognized message from AvatarApp.qml'); @@ -398,7 +438,7 @@ function ensureWearableSelected(entityID) { function isEntityBeingWorn(entityID) { return Entities.getEntityProperties(entityID, 'parentID').parentID === MyAvatar.sessionUUID; -}; +} function onSelectedEntity(entityID, pointerEvent) { if(selectedAvatarEntityID !== entityID && isEntityBeingWorn(entityID)) @@ -445,14 +485,14 @@ function handleWearableMessages(channel, message, sender) { // for some reasons Entities.getEntityProperties returns more than was asked.. var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); - var properties = {} + var properties = {}; propertyNames.forEach(function(propertyName) { properties[propertyName] = entityProperties[propertyName]; - }) + }); properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); - sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}) + sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}); }, 1000); } else if(parsedMessage.action === 'release') { @@ -481,8 +521,8 @@ function onBookmarkDeleted(bookmarkName) { function onBookmarkAdded(bookmarkName) { var bookmark = AvatarBookmarks.getBookmark(bookmarkName); bookmark.avatarEntites.forEach(function(avatarEntity) { - avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation) - }) + avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation); + }); sendToQml({ 'method': 'bookmarkAdded', 'bookmarkName': bookmarkName, 'bookmark': bookmark }); } @@ -601,14 +641,8 @@ function onTabletScreenChanged(type, url) { onAvatarAppScreen = onAvatarAppScreenNow; if(onAvatarAppScreenNow) { - var message = { - 'method' : 'initialize', - 'data' : { - 'jointNames' : MyAvatar.getJointNames() - } - }; - - sendToQml(message) + sendToQml({ method : 'initialize', data : { jointNames : MyAvatar.getJointNames() }}); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } } diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 5cb95f625d..51645e5502 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -341,8 +341,6 @@ entityIsGrabbable = function (eigProps) { var grabbable = getGrabbableData(eigProps).grabbable; if (!grabbable || eigProps.locked || - isAnothersAvatarEntity(eigProps) || - isAnothersChildEntity(eigProps) || FORBIDDEN_GRAB_TYPES.indexOf(eigProps.type) >= 0) { return false; } From 4fe94a4b32404ab0e096d2274fa53046fcbcb7a4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Mar 2019 14:16:18 -0700 Subject: [PATCH 108/135] when new wearables are added or removed, rerun getWearablesLocked() and update UI --- scripts/system/avatarapp.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 429053bb8b..eb44bc7fbd 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -79,6 +79,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { avatar[ENTRY_AVATAR_ENTITIES] = wearables; sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); if(callback) callback(); @@ -239,6 +240,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See AvatarBookmarks.loadBookmark(message.name); Entities.addingWearable.connect(onAddingWearable); Entities.deletingWearable.connect(onDeletingWearable); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); break; case 'deleteAvatar': AvatarBookmarks.removeBookmark(message.name); @@ -406,9 +408,11 @@ function isGrabbable(entityID) { } function setGrabbable(entityID, grabbable) { - var properties = Entities.getEntityProperties(entityID, ['avatarEntity']); - if (properties.avatarEntity) { - Entities.editEntity(entityID, { grab: { grabbable: grabbable }}); + var properties = Entities.getEntityProperties(entityID, ['avatarEntity', 'grab.grabbable']); + if (properties.avatarEntity && properties.grab.grabable != grabbable) { + var editProps = { grab: { grabbable: grabbable }}; + Entities.editEntity(entityID, editProps); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } } @@ -453,12 +457,14 @@ function onAddingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } function onDeletingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } function handleWearableMessages(channel, message, sender) { From fa36f1214530252ad86af39da0bf2473b27822a1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Mar 2019 16:12:38 -0700 Subject: [PATCH 109/135] lock wearables when adjust-wearables page is opened --- .../src/avatars-renderer/Avatar.cpp | 3 +- scripts/system/avatarapp.js | 31 ++++++------------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 8cff1cc52a..b0a8875cbc 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -384,8 +384,7 @@ bool Avatar::applyGrabChanges() { if (success) { std::shared_ptr myAvatar = std::dynamic_pointer_cast(myAvatarSN); if (myAvatar) { - EntityItemProperties properties = entity->getProperties(); - myAvatar->sendPacket(entity->getID(), properties); + myAvatar->sendPacket(entity->getID()); } } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index eb44bc7fbd..ee337694a2 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -16,7 +16,6 @@ (function() { // BEGIN LOCAL_SCOPE -// var request = Script.require('request').request; var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml"; Script.include("/~/system/libraries/controllers.js"); @@ -24,7 +23,6 @@ Script.include("/~/system/libraries/controllers.js"); var ENTRY_AVATAR_URL = "avatarUrl"; var ENTRY_AVATAR_ENTITIES = "avatarEntites"; var ENTRY_AVATAR_SCALE = "avatarScale"; -// var ENTRY_VERSION = "version"; function executeLater(callback) { Script.setTimeout(callback, 300); @@ -79,7 +77,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { avatar[ENTRY_AVATAR_ENTITIES] = wearables; sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); if(callback) callback(); @@ -174,7 +172,6 @@ function onAnimGraphUrlChanged(url) { } } -var selectedAvatarEntityGrabbable = false; var selectedAvatarEntityID = null; var grabbedAvatarEntityChangeNotifier = null; @@ -240,7 +237,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See AvatarBookmarks.loadBookmark(message.name); Entities.addingWearable.connect(onAddingWearable); Entities.deletingWearable.connect(onDeletingWearable); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); break; case 'deleteAvatar': AvatarBookmarks.removeBookmark(message.name); @@ -265,6 +262,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); + lockWearables(); Entities.mousePressOnEntity.connect(onSelectedEntity); Messages.subscribe('Hifi-Object-Manipulation'); @@ -409,10 +407,10 @@ function isGrabbable(entityID) { function setGrabbable(entityID, grabbable) { var properties = Entities.getEntityProperties(entityID, ['avatarEntity', 'grab.grabbable']); - if (properties.avatarEntity && properties.grab.grabable != grabbable) { + if (properties.avatarEntity && properties.grab.grabbable != grabbable) { var editProps = { grab: { grabbable: grabbable }}; Entities.editEntity(entityID, editProps); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } } @@ -422,18 +420,7 @@ function ensureWearableSelected(entityID) { Script.clearInterval(grabbedAvatarEntityChangeNotifier); grabbedAvatarEntityChangeNotifier = null; } - - if(selectedAvatarEntityID !== null) { - setGrabbable(selectedAvatarEntityID, selectedAvatarEntityGrabbable); - } - selectedAvatarEntityID = entityID; - selectedAvatarEntityGrabbable = isGrabbable(entityID); - - if(selectedAvatarEntityID !== null) { - setGrabbable(selectedAvatarEntityID, true); - } - return true; } @@ -457,14 +444,14 @@ function onAddingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } function onDeletingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } function handleWearableMessages(channel, message, sender) { @@ -647,8 +634,8 @@ function onTabletScreenChanged(type, url) { onAvatarAppScreen = onAvatarAppScreenNow; if(onAvatarAppScreenNow) { - sendToQml({ method : 'initialize', data : { jointNames : MyAvatar.getJointNames() }}); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'initialize', 'data' : { jointNames : MyAvatar.getJointNames() }}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } } From 5695c1580941e94eae37cb28b9afbed3e1bd8e70 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 22 Mar 2019 16:55:21 -0700 Subject: [PATCH 110/135] avoid a deadlock when code invoked by onAddingEntity or onDeletingEntity runs more code that locks the entity tree --- libraries/entities/src/EntityScriptingInterface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index aa4b3902c2..ca914731b5 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1101,13 +1101,13 @@ void EntityScriptingInterface::handleEntityScriptCallMethodPacket(QSharedPointer void EntityScriptingInterface::onAddingEntity(EntityItem* entity) { if (entity->isWearable()) { - emit addingWearable(entity->getEntityItemID()); + QMetaObject::invokeMethod(this, "addingWearable", Q_ARG(QUuid, entity->getEntityItemID())); } } void EntityScriptingInterface::onDeletingEntity(EntityItem* entity) { if (entity->isWearable()) { - emit deletingWearable(entity->getEntityItemID()); + QMetaObject::invokeMethod(this, "deletingWearable", Q_ARG(QUuid, entity->getEntityItemID())); } } From e6c279ee5bd2efe83c931a5621fb8aa9904da3c6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 25 Mar 2019 16:58:29 -0700 Subject: [PATCH 111/135] unlock, rather than lock entities when adjust-attachments window is open, because the windows says you can adjust with hand-controllers --- scripts/system/avatarapp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index ee337694a2..65b422f1ab 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -262,7 +262,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); - lockWearables(); + unlockWearables(); Entities.mousePressOnEntity.connect(onSelectedEntity); Messages.subscribe('Hifi-Object-Manipulation'); From d25d290394c34a4262a8e58d15e88e2dea462a2c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 25 Mar 2019 17:06:24 -0700 Subject: [PATCH 112/135] refer to ungrabbable wearables as 'frozen' rather than locked, because locked is such an overloaded term --- interface/resources/qml/hifi/AvatarApp.qml | 10 +++--- scripts/system/avatarapp.js | 40 +++++++++++----------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index a57b5713ed..997407885b 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -50,7 +50,7 @@ Rectangle { property var jointNames: [] property var currentAvatarSettings; - property bool wearablesLocked; + property bool wearablesFrozen; function fetchAvatarModelName(marketId, avatar) { var xmlhttp = new XMLHttpRequest(); @@ -190,8 +190,8 @@ Rectangle { updateCurrentAvatarInBookmarks(currentAvatar); } else if (message.method === 'selectAvatarEntity') { adjustWearables.selectWearableByID(message.entityID); - } else if (message.method === 'wearablesLockedChanged') { - wearablesLocked = message.wearablesLocked; + } else if (message.method === 'wearablesFrozenChanged') { + wearablesFrozen = message.wearablesFrozen; } } @@ -526,10 +526,10 @@ Rectangle { anchors.right: adjustLabel.left anchors.verticalCenter: wearablesLabel.verticalCenter anchors.rightMargin: 15 - glyphText: wearablesLocked ? hifi.glyphs.lock : hifi.glyphs.unlock; + glyphText: wearablesFrozen ? hifi.glyphs.lock : hifi.glyphs.unlock; onClicked: { - emitSendToScript({'method' : 'toggleWearablesLock'}); + emitSendToScript({'method' : 'toggleWearablesFrozen'}); } } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 65b422f1ab..509497669b 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -77,7 +77,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { avatar[ENTRY_AVATAR_ENTITIES] = wearables; sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); if(callback) callback(); @@ -179,26 +179,26 @@ var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); -function getWearablesLocked() { - var wearablesLocked = true; +function getWearablesFrozen() { + var wearablesFrozen = true; var wearablesArray = getMyAvatarWearables(); wearablesArray.forEach(function(wearable) { if (isGrabbable(wearable.id)) { - wearablesLocked = false; + wearablesFrozen = false; } }); - return wearablesLocked; + return wearablesFrozen; } -function lockWearables() { +function freezeWearables() { var wearablesArray = getMyAvatarWearables(); wearablesArray.forEach(function(wearable) { setGrabbable(wearable.id, false); }); } -function unlockWearables() { +function unfreezeWearables() { var wearablesArray = getMyAvatarWearables(); wearablesArray.forEach(function(wearable) { setGrabbable(wearable.id, true); @@ -237,7 +237,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See AvatarBookmarks.loadBookmark(message.name); Entities.addingWearable.connect(onAddingWearable); Entities.deletingWearable.connect(onDeletingWearable); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); break; case 'deleteAvatar': AvatarBookmarks.removeBookmark(message.name); @@ -262,7 +262,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); - unlockWearables(); + unfreezeWearables(); Entities.mousePressOnEntity.connect(onSelectedEntity); Messages.subscribe('Hifi-Object-Manipulation'); @@ -377,15 +377,15 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See currentAvatarSettings = getMyAvatarSettings(); break; - case 'toggleWearablesLock': - var wearablesLocked = getWearablesLocked(); - wearablesLocked = !wearablesLocked; - if (wearablesLocked) { - lockWearables(); + case 'toggleWearablesFrozen': + var wearablesFrozen = getWearablesFrozen(); + wearablesFrozen = !wearablesFrozen; + if (wearablesFrozen) { + freezeWearables(); } else { - unlockWearables(); + unfreezeWearables(); } - sendToQml({'method' : 'wearablesLockedChanged', 'wearablesLocked' : wearablesLocked}); + sendToQml({'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : wearablesFrozen}); break; default: print('Unrecognized message from AvatarApp.qml'); @@ -410,7 +410,7 @@ function setGrabbable(entityID, grabbable) { if (properties.avatarEntity && properties.grab.grabbable != grabbable) { var editProps = { grab: { grabbable: grabbable }}; Entities.editEntity(entityID, editProps); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } } @@ -444,14 +444,14 @@ function onAddingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } function onDeletingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } function handleWearableMessages(channel, message, sender) { @@ -635,7 +635,7 @@ function onTabletScreenChanged(type, url) { if(onAvatarAppScreenNow) { sendToQml({ 'method' : 'initialize', 'data' : { jointNames : MyAvatar.getJointNames() }}); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } } From e085a00256d8bb1c83e13d481aa3c247d2bd5343 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Mar 2019 09:32:12 -0700 Subject: [PATCH 113/135] cause 'save' button to unghost if an attachment is adjusted via grab while the adjust-wearables page is open --- .../qml/hifi/avatarapp/AdjustWearables.qml | 1 + scripts/system/avatarapp.js | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml index 136d535b3f..391e4fab37 100644 --- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -113,6 +113,7 @@ Rectangle { } else if (prop === 'dimensions') { scalespinner.set(wearable[prop].x / wearable.naturalDimensions.x); } + modified = true; } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 509497669b..6439d30023 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -468,30 +468,35 @@ function handleWearableMessages(channel, message, sender) { } var entityID = parsedMessage.grabbedEntity; + + var updateWearable = function() { + // for some reasons Entities.getEntityProperties returns more than was asked.. + var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; + var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); + var properties = {}; + + propertyNames.forEach(function(propertyName) { + properties[propertyName] = entityProperties[propertyName]; + }); + + properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); + sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, + 'wearableIndex' : -1, 'properties' : properties, updateUI : true}); + + }; + if(parsedMessage.action === 'grab') { if(selectedAvatarEntityID !== entityID) { ensureWearableSelected(entityID); sendToQml({'method' : 'selectAvatarEntity', 'entityID' : selectedAvatarEntityID}); } - grabbedAvatarEntityChangeNotifier = Script.setInterval(function() { - // for some reasons Entities.getEntityProperties returns more than was asked.. - var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; - var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); - var properties = {}; - - propertyNames.forEach(function(propertyName) { - properties[propertyName] = entityProperties[propertyName]; - }); - - properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); - sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}); - - }, 1000); + grabbedAvatarEntityChangeNotifier = Script.setInterval(updateWearable, 1000); } else if(parsedMessage.action === 'release') { if(grabbedAvatarEntityChangeNotifier !== null) { Script.clearInterval(grabbedAvatarEntityChangeNotifier); grabbedAvatarEntityChangeNotifier = null; + updateWearable(); } } } From fb7daa185d63aa7445bf20959ff9ac1df63dee51 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 27 Mar 2019 09:53:55 -0700 Subject: [PATCH 114/135] improved physics for grabbed AvatarEntities --- interface/src/avatar/OtherAvatar.cpp | 12 +++++ .../src/avatars-renderer/Avatar.cpp | 26 ----------- .../src/avatars-renderer/Avatar.h | 2 - libraries/entities/src/EntityItem.cpp | 45 ++++++++++--------- libraries/entities/src/EntityItem.h | 8 ++-- 5 files changed, 41 insertions(+), 52 deletions(-) diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 2058408596..b100b33dc8 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -495,6 +495,18 @@ void OtherAvatar::handleChangedAvatarEntityData() { const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}"); entity->setParentID(NULL_ID); entity->setParentID(oldParentID); + + if (entity->stillHasMyGrabAction()) { + // For this case: we want to ignore transform+velocities coming from authoritative OtherAvatar + // because the MyAvatar is grabbing and we expect the local grab state + // to have enough information to prevent simulation drift. + // + // Clever readers might realize this could cause problems. For example, + // if an ignored OtherAvagtar were to simultanously grab the object then there would be + // a noticeable discrepancy between participants in the distributed physics simulation, + // however the difference would be stable and would not drift. + properties.clearTransformOrVelocityChanges(); + } if (entityTree->updateEntity(entityID, properties)) { entity->updateLastEditedFromRemote(); } else { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index b0a8875cbc..839c4ed1d9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -372,23 +372,6 @@ bool Avatar::applyGrabChanges() { target->removeGrab(grab); _avatarGrabs.erase(itr); grabAddedOrRemoved = true; - const EntityItemPointer& entity = std::dynamic_pointer_cast(target); - if (entity && entity->getEntityHostType() == entity::HostType::AVATAR) { - // grabs are able to move avatar-entities which belong ot other avatars (assuming - // the entities are grabbable, unlocked, etc). Regardless of who released the grab - // on this entity, the entity's owner needs to send off an update. - QUuid entityOwnerID = entity->getOwningAvatarID(); - if (entityOwnerID == getMyAvatarID() || entityOwnerID == AVATAR_SELF_ID) { - bool success; - SpatiallyNestablePointer myAvatarSN = SpatiallyNestable::findByID(entityOwnerID, success); - if (success) { - std::shared_ptr myAvatar = std::dynamic_pointer_cast(myAvatarSN); - if (myAvatar) { - myAvatar->sendPacket(entity->getID()); - } - } - } - } } else { undeleted.push_back(id); } @@ -2113,12 +2096,3 @@ void Avatar::updateDescendantRenderIDs() { } }); } - -QUuid Avatar::getMyAvatarID() const { - auto nodeList = DependencyManager::get(); - if (nodeList) { - return nodeList->getSessionUUID(); - } else { - return QUuid(); - } -} diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 2bed13cb12..974fae2034 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -180,8 +180,6 @@ public: /// Returns the distance to use as a LOD parameter. float getLODDistance() const; - QUuid getMyAvatarID() const; - virtual void createOrb() { } enum class LoadingStatus { diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index f0bf13891b..bd4c6e5c71 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -789,8 +789,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef auto lastEdited = lastEditedFromBufferAdjusted; bool otherOverwrites = overwriteLocalData && !weOwnSimulation; - auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) { - if (stillHasGrabActions()) { + // calculate hasGrab once outside the lambda rather than calling it every time inside + bool hasGrab = stillHasGrabAction(); + auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection, hasGrab](quint64 updatedTimestamp, bool valueChanged) { + if (hasGrab) { return false; } bool simulationChanged = lastEdited > updatedTimestamp; @@ -957,12 +959,18 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // by doing this parsing here... but it's not likely going to fully recover the content. // - if (overwriteLocalData && (getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES))) { + if (overwriteLocalData && + !hasGrab && + (getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES))) { // NOTE: This code is attempting to "repair" the old data we just got from the server to make it more // closely match where the entities should be if they'd stepped forward in time to "now". The server // is sending us data with a known "last simulated" time. That time is likely in the past, and therefore // this "new" data is actually slightly out of date. We calculate the time we need to skip forward and // use our simulation helper routine to get a best estimate of where the entity should be. + // + // NOTE: We don't want to do this in the hasGrab case because grabs "know best" + // (e.g. grabs will prevent drift between distributed physics simulations). + // float skipTimeForward = (float)(now - lastSimulatedFromBufferAdjusted) / (float)(USECS_PER_SECOND); // we want to extrapolate the motion forward to compensate for packet travel time, but @@ -1426,7 +1434,7 @@ void EntityItem::getTransformAndVelocityProperties(EntityItemProperties& propert void EntityItem::upgradeScriptSimulationPriority(uint8_t priority) { uint8_t newPriority = glm::max(priority, _scriptSimulationPriority); - if (newPriority < SCRIPT_GRAB_SIMULATION_PRIORITY && stillHasGrabActions()) { + if (newPriority < SCRIPT_GRAB_SIMULATION_PRIORITY && stillHasMyGrabAction()) { newPriority = SCRIPT_GRAB_SIMULATION_PRIORITY; } if (newPriority != _scriptSimulationPriority) { @@ -1439,7 +1447,7 @@ void EntityItem::upgradeScriptSimulationPriority(uint8_t priority) { void EntityItem::clearScriptSimulationPriority() { // DO NOT markDirtyFlags(Simulation::DIRTY_SIMULATION_OWNERSHIP_PRIORITY) here, because this // is only ever called from the code that actually handles the dirty flags, and it knows best. - _scriptSimulationPriority = stillHasGrabActions() ? SCRIPT_GRAB_SIMULATION_PRIORITY : 0; + _scriptSimulationPriority = stillHasMyGrabAction() ? SCRIPT_GRAB_SIMULATION_PRIORITY : 0; } void EntityItem::setPendingOwnershipPriority(uint8_t priority) { @@ -2186,7 +2194,7 @@ void EntityItem::enableNoBootstrap() { } void EntityItem::disableNoBootstrap() { - if (!stillHasGrabActions()) { + if (!stillHasMyGrabAction()) { _flags &= ~Simulation::SPECIAL_FLAGS_NO_BOOTSTRAPPING; _flags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar @@ -2272,7 +2280,13 @@ bool EntityItem::removeAction(EntitySimulationPointer simulation, const QUuid& a return success; } -bool EntityItem::stillHasGrabActions() const { +bool EntityItem::stillHasGrabAction() const { + return !_grabActions.empty(); +} + +// retutrns 'true' if there exists an action that returns 'true' for EntityActionInterface::isMine() +// (e.g. the action belongs to the MyAvatar instance) +bool EntityItem::stillHasMyGrabAction() const { QList holdActions = getActionsOfType(DYNAMIC_TYPE_HOLD); QList::const_iterator i = holdActions.begin(); while (i != holdActions.end()) { @@ -2700,20 +2714,6 @@ void EntityItem::setLastEdited(quint64 lastEdited) { }); } -quint64 EntityItem::getLastBroadcast() const { - quint64 result; - withReadLock([&] { - result = _lastBroadcast; - }); - return result; -} - -void EntityItem::setLastBroadcast(quint64 lastBroadcast) { - withWriteLock([&] { - _lastBroadcast = lastBroadcast; - }); -} - void EntityItem::markAsChangedOnServer() { withWriteLock([&] { _changedOnServer = usecTimestampNow(); @@ -3479,6 +3479,9 @@ void EntityItem::addGrab(GrabPointer grab) { simulation->addDynamic(action); markDirtyFlags(Simulation::DIRTY_MOTION_TYPE); simulation->changeEntity(getThisPointer()); + + // don't forget to set isMine() for locally-created grabs + action->setIsMine(grab->getOwnerID() == Physics::getSessionUUID()); } } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index fae871a124..01ed949a0c 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -124,8 +124,8 @@ public: { return (float)(usecTimestampNow() - getLastEdited()) / (float)USECS_PER_SECOND; } /// Last time we sent out an edit packet for this entity - quint64 getLastBroadcast() const; - void setLastBroadcast(quint64 lastBroadcast); + quint64 getLastBroadcast() const { return _lastBroadcast; } + void setLastBroadcast(quint64 lastBroadcast) { _lastBroadcast = lastBroadcast; } void markAsChangedOnServer(); quint64 getLastChangedOnServer() const; @@ -562,6 +562,8 @@ public: static void setPrimaryViewFrustumPositionOperator(std::function getPrimaryViewFrustumPositionOperator) { _getPrimaryViewFrustumPositionOperator = getPrimaryViewFrustumPositionOperator; } static glm::vec3 getPrimaryViewFrustumPosition() { return _getPrimaryViewFrustumPositionOperator(); } + bool stillHasMyGrabAction() const; + signals: void requestRenderUpdate(); void spaceUpdate(std::pair data); @@ -574,7 +576,7 @@ protected: void setSimulated(bool simulated) { _simulated = simulated; } const QByteArray getDynamicDataInternal() const; - bool stillHasGrabActions() const; + bool stillHasGrabAction() const; void setDynamicDataInternal(QByteArray dynamicData); virtual void dimensionsChanged() override; From dae69ea4cd75d249562fc3773eaff47cc5dca216 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 27 Mar 2019 10:58:52 -0700 Subject: [PATCH 115/135] Ensure server echo always has unity gain --- .../src/audio/AudioMixerSlave.cpp | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index f7f8e8a9c1..cb90df58e5 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -51,7 +51,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& // mix helpers inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd); inline float computeGain(float masterAvatarGain, float masterInjectorGain, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho); + const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance); inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); @@ -504,14 +504,12 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); + float gain = isEcho ? 1.0f + : (isSoloing ? masterAvatarGain + : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, + relativePosition, distance)); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); - float gain = masterAvatarGain; - if (!isSoloing) { - gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, - distance, isEcho); - } - const int HRTF_DATASET_INDEX = 1; if (!streamToAdd->lastPopSucceeded()) { @@ -599,8 +597,8 @@ void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, - distance, isEcho); + float gain = isEcho ? 1.0f : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, + relativePosition, distance); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); mixableStream.hrtf->setParameterHistory(azimuth, distance, gain); @@ -743,8 +741,7 @@ float computeGain(float masterAvatarGain, const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, - float distance, - bool isEcho) { + float distance) { float gain = 1.0f; // injector: apply attenuation @@ -754,7 +751,7 @@ float computeGain(float masterAvatarGain, gain *= masterInjectorGain; // avatar: apply fixed off-axis attenuation to make them quieter as they turn away - } else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) { + } else if (streamToAdd.getType() == PositionalAudioStream::Microphone) { glm::vec3 rotatedListenerPosition = glm::inverse(streamToAdd.getOrientation()) * relativePosition; // source directivity is based on angle of emission, in local coordinates From fcb45802bde88aefe26154acbeead8ae63a38465 Mon Sep 17 00:00:00 2001 From: Angus Antley Date: Wed, 27 Mar 2019 11:19:28 -0700 Subject: [PATCH 116/135] removed debug print --- libraries/fbx/src/FBXSerializer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 8ff3005ddc..0090eea2e7 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -466,7 +466,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (object.name == "FBXVersion") { fbxVersionNumber = object.properties.at(0).toInt(); - qCDebug(modelformat) << "the fbx version number " << fbxVersionNumber; } } } else if (child.name == "GlobalSettings") { From 36921276c6dedb30178ead92f4fbc6f7b6e0f2c2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 27 Mar 2019 11:52:37 -0700 Subject: [PATCH 117/135] fix transparent shape flickering --- .../src/RenderableShapeEntityItem.cpp | 4 ++-- libraries/render-utils/src/GeometryCache.cpp | 4 ++++ libraries/render-utils/src/GeometryCache.h | 17 ++++++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 28b0bb8f23..33f4f2d751 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -281,9 +281,9 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; render::ShapePipelinePointer pipeline; if (renderLayer == RenderLayer::WORLD) { - pipeline = GeometryCache::getShapePipeline(false, outColor.a < 1.0f, true, false); + pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); } else { - pipeline = GeometryCache::getShapePipeline(false, outColor.a < 1.0f, true, false, false, true); + pipeline = outColor.a < 1.0f ? geometryCache->getForwardTransparentShapePipeline() : geometryCache->getForwardOpaqueShapePipeline(); } if (render::ShapeKey(args->_globalShapeKey).isWireframe() || primitiveMode == PrimitiveMode::LINES) { geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index c189798a42..2e762a0107 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -722,6 +722,8 @@ gpu::ShaderPointer GeometryCache::_unlitFadeShader; render::ShapePipelinePointer GeometryCache::_simpleOpaquePipeline; render::ShapePipelinePointer GeometryCache::_simpleTransparentPipeline; +render::ShapePipelinePointer GeometryCache::_forwardSimpleOpaquePipeline; +render::ShapePipelinePointer GeometryCache::_forwardSimpleTransparentPipeline; render::ShapePipelinePointer GeometryCache::_simpleOpaqueFadePipeline; render::ShapePipelinePointer GeometryCache::_simpleTransparentFadePipeline; render::ShapePipelinePointer GeometryCache::_simpleWirePipeline; @@ -801,6 +803,8 @@ void GeometryCache::initializeShapePipelines() { if (!_simpleOpaquePipeline) { _simpleOpaquePipeline = getShapePipeline(false, false, true, false); _simpleTransparentPipeline = getShapePipeline(false, true, true, false); + _forwardSimpleOpaquePipeline = getShapePipeline(false, false, true, false, false, true); + _forwardSimpleTransparentPipeline = getShapePipeline(false, true, true, false, false, true); _simpleOpaqueFadePipeline = getFadingShapePipeline(false, false, false, false, false); _simpleTransparentFadePipeline = getFadingShapePipeline(false, true, false, false, false); _simpleWirePipeline = getShapePipeline(false, false, true, true); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index e84f2e25a4..4ff061786a 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -181,6 +181,11 @@ public: static void initializeShapePipelines(); + render::ShapePipelinePointer getOpaqueShapePipeline() { assert(_simpleOpaquePipeline != nullptr); return _simpleOpaquePipeline; } + render::ShapePipelinePointer getTransparentShapePipeline() { assert(_simpleTransparentPipeline != nullptr); return _simpleTransparentPipeline; } + render::ShapePipelinePointer getForwardOpaqueShapePipeline() { assert(_forwardSimpleOpaquePipeline != nullptr); return _forwardSimpleOpaquePipeline; } + render::ShapePipelinePointer getForwardTransparentShapePipeline() { assert(_forwardSimpleTransparentPipeline != nullptr); return _forwardSimpleTransparentPipeline; } + // Static (instanced) geometry void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); @@ -369,11 +374,6 @@ public: graphics::MeshPointer meshFromShape(Shape geometryShape, glm::vec3 color); - static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool forward = false); - static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false); - private: GeometryCache(); @@ -467,6 +467,8 @@ private: static gpu::ShaderPointer _unlitFadeShader; static render::ShapePipelinePointer _simpleOpaquePipeline; static render::ShapePipelinePointer _simpleTransparentPipeline; + static render::ShapePipelinePointer _forwardSimpleOpaquePipeline; + static render::ShapePipelinePointer _forwardSimpleTransparentPipeline; static render::ShapePipelinePointer _simpleOpaqueFadePipeline; static render::ShapePipelinePointer _simpleTransparentFadePipeline; static render::ShapePipelinePointer _simpleWirePipeline; @@ -477,6 +479,11 @@ private: gpu::PipelinePointer _simpleOpaqueWebBrowserPipeline; gpu::ShaderPointer _simpleTransparentWebBrowserShader; gpu::PipelinePointer _simpleTransparentWebBrowserPipeline; + + static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true, + bool unlit = false, bool depthBias = false, bool forward = false); + static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true, + bool unlit = false, bool depthBias = false); }; #endif // hifi_GeometryCache_h From e62270fccf4156a238e4e58cf1b8cd814eb7e1aa Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 27 Mar 2019 12:00:30 -0700 Subject: [PATCH 118/135] Fixes for inline jsdoc --- assignment-client/src/avatars/ScriptableAvatar.h | 1 + libraries/avatars/src/AvatarData.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index fbe5675bd8..e5df411099 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -74,6 +74,7 @@ * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. * * @example * (function () { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index c55d5270e1..43ddbda996 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -479,7 +479,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. - * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) From 483b7a67b9322974075b64590907db32f749f226 Mon Sep 17 00:00:00 2001 From: Clement Date: Tue, 12 Feb 2019 15:10:32 -0800 Subject: [PATCH 119/135] Fix simple traits vector bad init --- libraries/avatars/src/AssociatedTraitValues.h | 13 +++++++------ libraries/avatars/src/AvatarTraits.h | 14 +++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/libraries/avatars/src/AssociatedTraitValues.h b/libraries/avatars/src/AssociatedTraitValues.h index e3060a8097..0df8cd9bb5 100644 --- a/libraries/avatars/src/AssociatedTraitValues.h +++ b/libraries/avatars/src/AssociatedTraitValues.h @@ -28,9 +28,10 @@ namespace AvatarTraits { template class AssociatedTraitValues { + using SimpleTypesArray = std::array; public: // constructor that pre-fills _simpleTypes with the default value specified by the template - AssociatedTraitValues() : _simpleTypes(FirstInstancedTrait, defaultValue) {} + AssociatedTraitValues() { std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue); } /// inserts the given value for the given simple trait type void insert(TraitType type, T value) { _simpleTypes[type] = value; } @@ -71,12 +72,12 @@ namespace AvatarTraits { } /// const iterators for the vector of simple type values - typename std::vector::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); } - typename std::vector::const_iterator simpleCEnd() const { return _simpleTypes.cend(); } + typename SimpleTypesArray::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); } + typename SimpleTypesArray::const_iterator simpleCEnd() const { return _simpleTypes.cend(); } /// non-const iterators for the vector of simple type values - typename std::vector::iterator simpleBegin() { return _simpleTypes.begin(); } - typename std::vector::iterator simpleEnd() { return _simpleTypes.end(); } + typename SimpleTypesArray::iterator simpleBegin() { return _simpleTypes.begin(); } + typename SimpleTypesArray::iterator simpleEnd() { return _simpleTypes.end(); } struct TraitWithInstances { TraitType traitType; @@ -96,7 +97,7 @@ namespace AvatarTraits { typename std::vector::iterator instancedEnd() { return _instancedTypes.end(); } private: - std::vector _simpleTypes; + SimpleTypesArray _simpleTypes; /// return the iterator to the matching TraitWithInstances object for a given instanced trait type typename std::vector::iterator instancesForTrait(TraitType traitType) { diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index 4516572e42..5542acd37f 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -14,19 +14,31 @@ #include #include +#include #include #include namespace AvatarTraits { enum TraitType : int8_t { + // Null trait NullTrait = -1, - SkeletonModelURL, + + // Simple traits + SkeletonModelURL = 0, + + + // Instanced traits FirstInstancedTrait, AvatarEntity = FirstInstancedTrait, Grab, + + // Traits count TotalTraitTypes }; + const int NUM_SIMPLE_TRAITS = (int)FirstInstancedTrait; + const int NUM_INSTANCED_TRAITS = (int)TotalTraitTypes - (int)FirstInstancedTrait; + const int NUM_TRAITS = (int)TotalTraitTypes; using TraitInstanceID = QUuid; From d7d5938c20ae6718428e1085fe22b13d6cc93ca5 Mon Sep 17 00:00:00 2001 From: Clement Date: Mon, 18 Mar 2019 17:47:52 -0700 Subject: [PATCH 120/135] Pack all simple traits --- libraries/avatars/src/ClientTraitsHandler.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index bcbe5308c7..c9e3b67f16 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -106,9 +106,10 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { auto traitType = static_cast(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt)); if (initialSend || *simpleIt == Updated) { - if (traitType == AvatarTraits::SkeletonModelURL) { - bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); + bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); + + if (traitType == AvatarTraits::SkeletonModelURL) { // keep track of our skeleton version in case we get an override back _currentSkeletonVersion = _currentTraitVersion; } From 3221e1dbd59c4b333e4285427a3b0a595acf732e Mon Sep 17 00:00:00 2001 From: Clement Date: Tue, 19 Mar 2019 19:39:17 -0700 Subject: [PATCH 121/135] Simplify packing/unpacking for easier extension --- .../src/avatars/AvatarMixerClientData.cpp | 2 +- .../src/avatars/AvatarMixerSlave.cpp | 6 +- libraries/avatars/src/AvatarData.cpp | 151 +++++------------- libraries/avatars/src/AvatarData.h | 22 ++- libraries/avatars/src/AvatarTraits.cpp | 135 ++++++++++++++++ libraries/avatars/src/AvatarTraits.h | 27 ++-- libraries/avatars/src/ClientTraitsHandler.cpp | 6 +- 7 files changed, 209 insertions(+), 140 deletions(-) create mode 100644 libraries/avatars/src/AvatarTraits.cpp diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 557c5c9fe3..dfbeca96ce 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -341,7 +341,7 @@ void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedDa // the returned set traits packet uses the trait version from the incoming packet // so the client knows they should not overwrite if they have since changed the trait - _avatar->packTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion); + AvatarTraits::packVersionedTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion, *_avatar); auto nodeList = DependencyManager::get(); nodeList->sendPacket(std::move(packet), sendingNode); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index e59c81f4b7..fdbdf21607 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -139,7 +139,8 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis if (lastReceivedVersion > lastSentVersionRef) { bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten); // there is an update to this trait, add it to the traits packet - bytesWritten += sendingAvatar->packTrait(traitType, traitsPacketList, lastReceivedVersion); + bytesWritten += AvatarTraits::packVersionedTrait(traitType, traitsPacketList, + lastReceivedVersion, *sendingAvatar); // update the last sent version lastSentVersionRef = lastReceivedVersion; // Remember which versions we sent in this particular packet @@ -194,7 +195,8 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten); // this instance version exists and has never been sent or is newer so we need to send it - bytesWritten += sendingAvatar->packTraitInstance(traitType, instanceID, traitsPacketList, receivedVersion); + bytesWritten += AvatarTraits::packVersionedTraitInstance(traitType, instanceID, traitsPacketList, + receivedVersion, *sendingAvatar); if (sentInstanceIt != sentIDValuePairs.end()) { sentInstanceIt->value = receivedVersion; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 39dfaa8a1a..9c923580f9 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1990,42 +1990,16 @@ QUrl AvatarData::getWireSafeSkeletonModelURL() const { } } -qint64 AvatarData::packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination, - AvatarTraits::TraitVersion traitVersion) { - - qint64 bytesWritten = 0; - - if (traitType == AvatarTraits::SkeletonModelURL) { - - QByteArray encodedSkeletonURL = getWireSafeSkeletonModelURL().toEncoded(); - - if (encodedSkeletonURL.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack simple trait" << traitType << "of size" << encodedSkeletonURL.size() - << "bytes since it exceeds the maximum size" << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - AvatarTraits::TraitWireSize encodedURLSize = encodedSkeletonURL.size(); - bytesWritten += destination.writePrimitive(encodedURLSize); - - bytesWritten += destination.write(encodedSkeletonURL); - } - - return bytesWritten; +QByteArray AvatarData::packSkeletonModelURL() const { + return getWireSafeSkeletonModelURL().toEncoded(); } +void AvatarData::unpackSkeletonModelURL(const QByteArray& data) { + auto skeletonModelURL = QUrl::fromEncoded(data); + setSkeletonModelURL(skeletonModelURL); +} -qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; - +QByteArray AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) { // grab a read lock on the avatar entities and check for entity data for the given ID QByteArray entityBinaryData; _avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] { @@ -2034,104 +2008,48 @@ qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitTy } }); - if (entityBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << entityBinaryData.size() - << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - bytesWritten += destination.write(traitInstanceID.toRfc4122()); - - if (!entityBinaryData.isNull()) { - AvatarTraits::TraitWireSize entityBinarySize = entityBinaryData.size(); - - bytesWritten += destination.writePrimitive(entityBinarySize); - bytesWritten += destination.write(entityBinaryData); - } else { - bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); - } - - return bytesWritten; + return entityBinaryData; } - -qint64 AvatarData::packGrabTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; - +QByteArray AvatarData::packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) { // grab a read lock on the avatar grabs and check for grab data for the given ID QByteArray grabBinaryData; - _avatarGrabsLock.withReadLock([this, &grabBinaryData, &traitInstanceID] { if (_avatarGrabData.contains(traitInstanceID)) { grabBinaryData = _avatarGrabData[traitInstanceID]; } }); - if (grabBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << grabBinaryData.size() - << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - bytesWritten += destination.write(traitInstanceID.toRfc4122()); - - if (!grabBinaryData.isNull()) { - AvatarTraits::TraitWireSize grabBinarySize = grabBinaryData.size(); - - bytesWritten += destination.writePrimitive(grabBinarySize); - bytesWritten += destination.write(grabBinaryData); - } else { - bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); - } - - return bytesWritten; + return grabBinaryData; } -qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; +QByteArray AvatarData::packTrait(AvatarTraits::TraitType traitType) const { + QByteArray traitBinaryData; + // Call packer function + if (traitType == AvatarTraits::SkeletonModelURL) { + traitBinaryData = packSkeletonModelURL(); + } + + return traitBinaryData; +} + +QByteArray AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID) { + QByteArray traitBinaryData; + + // Call packer function if (traitType == AvatarTraits::AvatarEntity) { - bytesWritten += packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion); + traitBinaryData = packAvatarEntityTraitInstance(traitInstanceID); } else if (traitType == AvatarTraits::Grab) { - bytesWritten += packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion); + traitBinaryData = packGrabTraitInstance(traitInstanceID); } - return bytesWritten; -} - -void AvatarData::prepareResetTraitInstances() { - if (_clientTraitsHandler) { - _avatarEntitiesLock.withReadLock([this]{ - foreach (auto entityID, _packedAvatarEntityData.keys()) { - _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); - } - foreach (auto grabID, _avatarGrabData.keys()) { - _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID); - } - }); - } + return traitBinaryData; } void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData) { if (traitType == AvatarTraits::SkeletonModelURL) { - // get the URL from the binary data - auto skeletonModelURL = QUrl::fromEncoded(traitBinaryData); - setSkeletonModelURL(skeletonModelURL); + unpackSkeletonModelURL(traitBinaryData); } } @@ -2152,6 +2070,19 @@ void AvatarData::processDeletedTraitInstance(AvatarTraits::TraitType traitType, } } +void AvatarData::prepareResetTraitInstances() { + if (_clientTraitsHandler) { + _avatarEntitiesLock.withReadLock([this]{ + foreach (auto entityID, _packedAvatarEntityData.keys()) { + _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); + } + foreach (auto grabID, _avatarGrabData.keys()) { + _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID); + } + }); + } +} + QByteArray AvatarData::identityByteArray(bool setIsReplicated) const { QByteArray identityData; QDataStream identityStream(&identityData, QIODevice::Append); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 00e7e67923..dd6f0a9efd 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1134,18 +1134,16 @@ public: // identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange. void processAvatarIdentity(QDataStream& packetStream, bool& identityChanged, bool& displayNameChanged); - qint64 packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination, - AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION); - qint64 packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION); - - void prepareResetTraitInstances(); + QByteArray packTrait(AvatarTraits::TraitType traitType) const; + QByteArray packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); void processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData); void processTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData); void processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); + void prepareResetTraitInstances(); + QByteArray identityByteArray(bool setIsReplicated = false) const; QUrl getWireSafeSkeletonModelURL() const; @@ -1596,13 +1594,13 @@ protected: bool hasParent() const { return !getParentID().isNull(); } bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; } - qint64 packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion); - qint64 packGrabTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion); + QByteArray packSkeletonModelURL() const; + QByteArray packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); + QByteArray packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); + void unpackSkeletonModelURL(const QByteArray& data); + + // isReplicated will be true on downstream Avatar Mixers and their clients, but false on the upstream "master" // Audio Mixer that the replicated avatar is connected to. bool _isReplicated{ false }; diff --git a/libraries/avatars/src/AvatarTraits.cpp b/libraries/avatars/src/AvatarTraits.cpp new file mode 100644 index 0000000000..724f30e2f3 --- /dev/null +++ b/libraries/avatars/src/AvatarTraits.cpp @@ -0,0 +1,135 @@ +// +// AvatarTraits.cpp +// libraries/avatars/src +// +// Created by Clement Brisset on 3/19/19. +// Copyright 2019 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 "AvatarTraits.h" + +#include + +#include "AvatarData.h" + +namespace AvatarTraits { + + qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTrait(traitType); + auto traitBinaryDataSize = traitBinaryData.size(); + + // Verify packed data + if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + return bytesWritten; + } + + qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination, + TraitVersion traitVersion, const AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTrait(traitType); + auto traitBinaryDataSize = traitBinaryData.size(); + + // Verify packed data + if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitVersion)traitVersion); + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + return bytesWritten; + } + + + qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID); + auto traitBinaryDataSize = traitBinaryData.size(); + + + // Verify packed data + if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.write(traitInstanceID.toRfc4122()); + + if (!traitBinaryData.isNull()) { + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + } else { + bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); + } + + return bytesWritten; + } + + qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, TraitVersion traitVersion, + AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID); + auto traitBinaryDataSize = traitBinaryData.size(); + + + // Verify packed data + if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitVersion)traitVersion); + bytesWritten += destination.write(traitInstanceID.toRfc4122()); + + if (!traitBinaryData.isNull()) { + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + } else { + bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); + } + + return bytesWritten; + } + + + qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, + TraitVersion traitVersion) { + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive(traitType); + if (traitVersion > DEFAULT_TRAIT_VERSION) { + bytesWritten += destination.writePrimitive(traitVersion); + } + bytesWritten += destination.write(instanceID.toRfc4122()); + bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE); + return bytesWritten; + } +}; diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index 5542acd37f..adff34f351 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -19,6 +19,9 @@ #include +class ExtendedIODevice; +class AvatarData; + namespace AvatarTraits { enum TraitType : int8_t { // Null trait @@ -36,6 +39,7 @@ namespace AvatarTraits { // Traits count TotalTraitTypes }; + const int NUM_SIMPLE_TRAITS = (int)FirstInstancedTrait; const int NUM_INSTANCED_TRAITS = (int)TotalTraitTypes - (int)FirstInstancedTrait; const int NUM_TRAITS = (int)TotalTraitTypes; @@ -58,22 +62,19 @@ namespace AvatarTraits { const TraitMessageSequence FIRST_TRAIT_SEQUENCE = 0; const TraitMessageSequence MAX_TRAIT_SEQUENCE = INT64_MAX; - inline qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, - TraitVersion traitVersion = NULL_TRAIT_VERSION) { - qint64 bytesWritten = 0; + qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarData& avatar); + qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination, + TraitVersion traitVersion, const AvatarData& avatar); - bytesWritten += destination.writePrimitive(traitType); + qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, AvatarData& avatar); + qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, TraitVersion traitVersion, + AvatarData& avatar); - if (traitVersion > DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } + qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, + TraitVersion traitVersion = NULL_TRAIT_VERSION); - bytesWritten += destination.write(instanceID.toRfc4122()); - - bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE); - - return bytesWritten; - } }; #endif // hifi_AvatarTraits_h diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index c9e3b67f16..a2d21fed54 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -106,7 +106,7 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { auto traitType = static_cast(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt)); if (initialSend || *simpleIt == Updated) { - bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); + bytesWritten += AvatarTraits::packTrait(traitType, *traitsPacketList, *_owningAvatar); if (traitType == AvatarTraits::SkeletonModelURL) { @@ -125,7 +125,9 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { || instanceIDValuePair.value == Updated) { // this is a changed trait we need to send or we haven't send out trait information yet // ask the owning avatar to pack it - bytesWritten += _owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList); + bytesWritten += AvatarTraits::packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, + *traitsPacketList, *_owningAvatar); + } else if (!initialSend && instanceIDValuePair.value == Deleted) { // pack delete for this trait instance bytesWritten += AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id, From 88a19f26e20e7d5e88e1d2b2c00cadf96e5c5540 Mon Sep 17 00:00:00 2001 From: Clement Date: Wed, 20 Mar 2019 17:04:22 -0700 Subject: [PATCH 122/135] Use process function for overrides --- libraries/avatars/src/AvatarTraits.h | 1 - libraries/avatars/src/ClientTraitsHandler.cpp | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index adff34f351..13d64ec225 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -30,7 +30,6 @@ namespace AvatarTraits { // Simple traits SkeletonModelURL = 0, - // Instanced traits FirstInstancedTrait, AvatarEntity = FirstInstancedTrait, diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index a2d21fed54..f6bd66e89a 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -165,11 +165,11 @@ void ClientTraitsHandler::processTraitOverride(QSharedPointer m // override the skeleton URL but do not mark the trait as having changed // so that we don't unecessarily send a new trait packet to the mixer with the overriden URL - auto encodedSkeletonURL = QUrl::fromEncoded(message->readWithoutCopy(traitBinarySize)); auto hasChangesBefore = _hasChangedTraits; - _owningAvatar->setSkeletonModelURL(encodedSkeletonURL); + auto traitBinaryData = message->readWithoutCopy(traitBinarySize); + _owningAvatar->processTrait(traitType, traitBinaryData); // setSkeletonModelURL will flag us for changes to the SkeletonModelURL so we reset some state here to // avoid unnecessarily sending the overriden skeleton model URL back to the mixer From e95efc29e4c955e8f3573e6a6f794a2bdefbdeae Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Wed, 27 Mar 2019 19:00:28 -0700 Subject: [PATCH 123/135] Bug fix after code review. --- tools/nitpick/src/TestRunnerMobile.cpp | 35 +++++++++++++++----------- tools/nitpick/src/TestRunnerMobile.h | 2 ++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index a848c94755..969da02c9e 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -114,19 +114,17 @@ void TestRunnerMobile::connectDevice() { QString deviceID = tokens[0]; // Find the model entry - int i; - for (i = 0; i < tokens.size(); ++i) { - if (tokens[i].contains(MODEL)) { - break; - } - } - _modelName = "UNKNOWN"; - if (i < tokens.size()) { - QString modelID = tokens[i].split(':')[1]; + for (int i = 0; i < tokens.size(); ++i) { + if (tokens[i].contains(MODEL)) { + if (i < tokens.size()) { + QString modelID = tokens[i].split(':')[1]; - if (modelNames.count(modelID) == 1) { - _modelName = modelNames[modelID]; + if (modelNames.count(modelID) == 1) { + _modelName = modelNames[modelID]; + } + } + break; } } @@ -225,10 +223,16 @@ void TestRunnerMobile::runInterface() { startCommand = "io.highfidelity.hifiinterface/.PermissionChecker"; } + QString serverIP { getServerIP() }; + if (serverIP == NETWORK_NOT_FOUND) { + _runInterfacePushbutton->setEnabled(false); + return; + } + QString command = _adbInterface->getAdbCommand() + " shell am start -n " + startCommand + " --es args \\\"" + - " --url hifi://" + getServerIP() + "/0,0,0" + " --url hifi://" + serverIP + "/0,0,0" " --no-updater" + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + @@ -268,10 +272,10 @@ QString TestRunnerMobile::getServerIP() { QString line = ifconfigFile.readLine(); while (!line.isNull()) { // The device IP is in the line following the "wlan0" line - line = ifconfigFile.readLine(); if (line.left(6) == "wlan0 ") { break; } + line = ifconfigFile.readLine(); } // The following line looks like this "inet addr:192.168.0.15 Bcast:192.168.0.255 Mask:255.255.255.0" @@ -280,8 +284,9 @@ QString TestRunnerMobile::getServerIP() { QStringList lineParts = line.split(':'); if (lineParts.size() < 4) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "IP address line not in expected format: " + line); - exit(-1); + "IP address line not in expected format: " + line + "(check that device WIFI is on)"); + + return NETWORK_NOT_FOUND; } qint64 deviceIP = convertToBinary(lineParts[1].split(' ')[0]); diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index b94cd47647..09f847785b 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -80,5 +80,7 @@ private: AdbInterface* _adbInterface; QString _modelName; + + QString NETWORK_NOT_FOUND{ "NETWORK NOT FOUND"}; }; #endif From 3d5035886c8533ccce974e59e3db6ff054f65c10 Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Thu, 27 Dec 2018 12:55:52 +0200 Subject: [PATCH 124/135] Allow logging-in with an email that contains a '+' sign. Previously, attempts to login using an email such as "my+name@example.com" didn't work because the username wasn't URL-encoded when it was sent to the server, so on the server the '+' was changed to a space. --- libraries/networking/src/AccountManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 4647c50496..226433e388 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -536,7 +536,7 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas QByteArray postData; postData.append("grant_type=password&"); - postData.append("username=" + login + "&"); + postData.append("username=" + QUrl::toPercentEncoding(login) + "&"); postData.append("password=" + QUrl::toPercentEncoding(password) + "&"); postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); From de1c40e994092e6c56f16434ba5df051c63743eb Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 28 Mar 2019 11:27:35 -0700 Subject: [PATCH 125/135] Changed version. --- tools/nitpick/src/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index 09bf23fdfc..51f1f85113 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -60,5 +60,5 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; -const QString nitpickVersion{ "21660" }; +const QString nitpickVersion{ "v3.1.5" }; #endif // hifi_common_h \ No newline at end of file From d230fc86db509ef1d55243a983983106b5822272 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 28 Mar 2019 12:23:25 -0700 Subject: [PATCH 126/135] CR comments. --- tools/nitpick/src/TestRunnerMobile.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 969da02c9e..53a74da82f 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -117,12 +117,10 @@ void TestRunnerMobile::connectDevice() { _modelName = "UNKNOWN"; for (int i = 0; i < tokens.size(); ++i) { if (tokens[i].contains(MODEL)) { - if (i < tokens.size()) { - QString modelID = tokens[i].split(':')[1]; + QString modelID = tokens[i].split(':')[1]; - if (modelNames.count(modelID) == 1) { - _modelName = modelNames[modelID]; - } + if (modelNames.count(modelID) == 1) { + _modelName = modelNames[modelID]; } break; } @@ -223,7 +221,7 @@ void TestRunnerMobile::runInterface() { startCommand = "io.highfidelity.hifiinterface/.PermissionChecker"; } - QString serverIP { getServerIP() }; + QString serverIP { getServerIP() }; if (serverIP == NETWORK_NOT_FOUND) { _runInterfacePushbutton->setEnabled(false); return; From 88b7687183b1533716bb0cf44d8f53498c0f1608 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 28 Mar 2019 13:19:16 -0700 Subject: [PATCH 127/135] Case 21726 - Domain lost recent entity edits upon restart When an entity server starts up, grabs the version of the models.json file locally, and then queries the domain server for its copy of the models.json file...iff the domain server version is newer. There was a bug in this process in that the comparison was made between the wrong version, specifically the 'file format version' which doesn't change unless there was a protocol change...and not the data version, which increments every time a change is made to a domain. Therefore, the version of the models.json on the domain server was never downloaded to the entity server, even when it was newer. It would be newer if the entity server assignment was moved to a machine with an old version of the models.json file, which was in fact the case on distributed3 during this period of time. --- domain-server/src/DomainServer.cpp | 10 +++++----- libraries/octree/src/OctreePersistThread.cpp | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8d5cb165cb..5f82700e9c 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1766,14 +1766,14 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerreadPrimitive(&remoteHasExistingData); if (remoteHasExistingData) { constexpr size_t UUID_SIZE_BYTES = 16; auto idData = message->read(UUID_SIZE_BYTES); id = QUuid::fromRfc4122(idData); - message->readPrimitive(&version); - qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << version << ")"; + message->readPrimitive(&dataVersion); + qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << dataVersion << ")"; } else { qCDebug(domain_server) << "Entity server does not have existing data"; } @@ -1782,11 +1782,11 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerwritePrimitive(false); } else { - qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.version << ")"; + qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.dataVersion << ")"; QFile file(entityFilePath); if (file.open(QIODevice::ReadOnly)) { reply->writePrimitive(true); diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index 32ee72ea1c..20ba3cde60 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -82,11 +82,11 @@ void OctreePersistThread::start() { } if (data.readOctreeDataInfoFromData(_cachedJSONData)) { - qCDebug(octree) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.version << ")"; + qCDebug(octree) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.dataVersion << ")"; packet->writePrimitive(true); auto id = data.id.toRfc4122(); packet->write(id); - packet->writePrimitive(data.version); + packet->writePrimitive(data.dataVersion); } else { _cachedJSONData.clear(); qCWarning(octree) << "No octree data found"; @@ -144,8 +144,8 @@ void OctreePersistThread::handleOctreeDataFileReply(QSharedPointersetOctreeVersionInfo(data.id, data.version); + qDebug() << "Setting entity version info to: " << data.id << data.dataVersion; + _tree->setOctreeVersionInfo(data.id, data.dataVersion); } bool persistentFileRead; From 44b92c542b2706bd30f77ccd98529fe14b69c099 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 28 Mar 2019 16:05:24 -0700 Subject: [PATCH 128/135] Case 20499 - Scripts that use AppUI don't call `that.onClosed()` if the script is restarted while the app is open --- scripts/modules/appUi.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js index 3e8e0b1008..9771348377 100644 --- a/scripts/modules/appUi.js +++ b/scripts/modules/appUi.js @@ -353,10 +353,11 @@ function AppUi(properties) { // Close if necessary, clean up any remaining handlers, and remove the button. GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll); GlobalServices.findableByChanged.disconnect(restartNotificationPoll); + that.tablet.screenChanged.disconnect(that.onScreenChanged); if (that.isOpen) { that.close(); + that.onScreenChanged("", ""); } - that.tablet.screenChanged.disconnect(that.onScreenChanged); if (that.button) { if (that.onClicked) { that.button.clicked.disconnect(that.onClicked); From 687409b756081de9997fc4173ee945c7a6827f83 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 29 Mar 2019 01:46:57 +0100 Subject: [PATCH 129/135] ignore case for .fbx file extension in AvatarDoctor --- interface/src/avatar/AvatarDoctor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index 43e50ea049..01a40e89fd 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -92,7 +92,7 @@ void AvatarDoctor::startDiagnosing() { _model = resource; const auto model = resource.data(); const auto avatarModel = resource.data()->getHFMModel(); - if (!avatarModel.originalURL.endsWith(".fbx")) { + if (!avatarModel.originalURL.toLower().endsWith(".fbx")) { addError("Unsupported avatar model format.", "unsupported-format"); emit complete(getErrors()); return; From 7e21a3f372f6c6ce04dffa621bd36c48f0727ef6 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 29 Mar 2019 01:48:57 +0100 Subject: [PATCH 130/135] disable case sensitivity in file browsers (allow .fbx .fBx .FBX instead of only .fbx) --- interface/resources/qml/dialogs/FileDialog.qml | 3 ++- interface/resources/qml/dialogs/TabletFileDialog.qml | 3 ++- .../qml/hifi/tablet/tabletWindows/TabletFileDialog.qml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index ba5e162391..4eea3566b8 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -320,6 +320,7 @@ ModalWindow { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml index 6c4e32dc5a..5bcc42f101 100644 --- a/interface/resources/qml/dialogs/TabletFileDialog.qml +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -285,6 +285,7 @@ TabletModalWindow { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index a27c7b59dc..36a37134bf 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -279,6 +279,7 @@ Rectangle { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory From bc7fb10ab95c3f648a5bb1bd056568410fd4a593 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 28 Mar 2019 17:54:35 -0700 Subject: [PATCH 131/135] Fixes from review --- assignment-client/src/avatars/AvatarMixer.cpp | 4 ++-- assignment-client/src/avatars/AvatarMixer.h | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 5f7e197c8f..9816cebf43 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -262,7 +262,7 @@ void AvatarMixer::start() { { if (_dirtyHeroStatus) { _dirtyHeroStatus = false; - nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + nodeList->nestedEach([](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { std::for_each(cbegin, cend, [](const SharedNodePointer& node) { if (node->getType() == NodeType::Agent) { NodeData* nodeData = node->getLinkedData(); @@ -1108,7 +1108,7 @@ void AvatarMixer::entityAdded(EntityItem* entity) { if (entity->getType() == EntityTypes::Zone) { _dirtyHeroStatus = true; entity->registerChangeHandler([this](const EntityItemID& entityItemID) { - this->entityChange(); + entityChange(); }); } } diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index f65f04f279..10dff5e8a4 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -46,6 +46,11 @@ public slots: void sendStatsPacket() override; + // Avatar zone possibly changed + void entityAdded(EntityItem* entity); + void entityRemoved(EntityItem* entity); + void entityChange(); + private slots: void queueIncomingPacket(QSharedPointer message, SharedNodePointer node); void handleAdjustAvatarSorting(QSharedPointer message, SharedNodePointer senderNode); @@ -147,12 +152,6 @@ private: AvatarMixerSlavePool _slavePool; SlaveSharedData _slaveSharedData; - -public slots: - // Avatar zone possibly changed - void entityAdded(EntityItem* entity); - void entityRemoved(EntityItem* entity); - void entityChange(); }; #endif // hifi_AvatarMixer_h From 709515dd7408d3283dedc00098bdfb85e2b94dd2 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 29 Mar 2019 11:48:15 -0700 Subject: [PATCH 132/135] Use place holder instead of RCC file. --- tools/nitpick/compiledResources/.placeholder | 0 tools/nitpick/compiledResources/resources.rcc | Bin 42 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tools/nitpick/compiledResources/.placeholder delete mode 100644 tools/nitpick/compiledResources/resources.rcc diff --git a/tools/nitpick/compiledResources/.placeholder b/tools/nitpick/compiledResources/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/nitpick/compiledResources/resources.rcc b/tools/nitpick/compiledResources/resources.rcc deleted file mode 100644 index 15f51ed7f4d9aa2328eca21473fd352cf3605021..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42 dcmXRcN-bt!U|?ckU=TsV5D^ey1d|L53;<0C0sQ~~ From 58abfb44c4936664a18299a9d34eb65a76409f55 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 31 Mar 2019 10:35:03 -0700 Subject: [PATCH 133/135] Globally disallow use of the camera or microphone by hosted web content --- interface/src/Application.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 635932ea1c..676534f6db 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2882,11 +2882,19 @@ void Application::initializeGL() { } #if !defined(DISABLE_QML) + QStringList chromiumFlags; + // Bug 21993: disable microphone and camera input + chromiumFlags << "--use-fake-device-for-media-stream"; // Disable signed distance field font rendering on ATI/AMD GPUs, due to // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app std::string vendor{ (const char*)glGetString(GL_VENDOR) }; if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { - qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); + chromiumFlags << "--disable-distance-field-text"; + } + + // Ensure all Qt webengine processes launched from us have the appropriate command line flags + if (!chromiumFlags.empty()) { + qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags.join(' ').toLocal8Bit()); } #endif From 7d4e59bfe3a6227ce6405698e4ab5c8fbbe7621d Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 31 Mar 2019 12:53:24 -0700 Subject: [PATCH 134/135] Force packet version change --- libraries/networking/src/udt/PacketHeaders.h | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 413ff14b17..8c76a3ebd0 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -266,6 +266,7 @@ enum class EntityVersion : PacketVersion { ModelScale, ReOrderParentIDProperties, CertificateTypeProperty, + DisableWebMedia, // Add new versions above here NUM_PACKET_TYPE, From 0a8d195f6f09cdccd157ab0e92bec5f807d616a6 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 1 Apr 2019 11:32:52 -0700 Subject: [PATCH 135/135] adding top padding for my name card --- interface/resources/qml/hifi/NameCard.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 141ddf0077..7e8218b7df 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -129,6 +129,7 @@ Item { height: 40 // Anchors anchors.top: avatarImage.top + anchors.topMargin: avatarImage.visible ? 18 : 0; anchors.left: avatarImage.right anchors.leftMargin: avatarImage.visible ? 5 : 0; anchors.rightMargin: 5;
NameType

Description
leftHandPoleVector{@link Vec3}The direction the elbow should point in rig * coordinates.
leftHandIKEnabledbooleantrue if IK is enabled for the left - * hand.
rightHandIKEnabledbooleantrue if IK is enabled for the right - * hand.
rightHandPosition{@link Vec3}The desired position of the RightHand * joint in rig coordinates.
rightHandRotation{@link Quat}The desired orientation of the @@ -214,8 +210,6 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRig *
rightFootPoleVector{@link Vec3}The direction the knee should face in rig * coordinates.
splineIKEnabledbooleantrue if IK is enabled for the spline.
isTalkingbooleantrue if the avatar is talking.
notIsTalkingbooleantrue if the avatar is not talking.
Make an assignment client script emulate an avatar.Check whether the agent is emulating an avatar.Play a sound from an emulated avatar.Create a scriptable avatar.Report the current animation details.Report the current avatar entities.
8PITCHRotate the user's avatar head and attached camera about its negative * x-axis (i.e., positive values pitch down) at a rate proportional to the control value, if the camera isn't in HMD, * independent, or mirror modes.
9ZOOMZooms the camera in or out.
9ZOOMZoom the camera in or out.
10DELTA_YAWRotate the user's avatar about its y-axis by an amount proportional * to the control value, if the camera isn't in independent or mirror modes.
11DELTA_PITCHRotate the user's avatar head and attached camera about its @@ -471,13 +470,14 @@ public: void setCollisionWithOtherAvatarsFlags() override; /**jsdoc - * Resets the sensor positioning of your HMD (if used) and recenters your avatar body and head. + * Resets the sensor positioning of your HMD (if in use) and recenters your avatar body and head. * @function MyAvatar.resetSensorsAndBody */ Q_INVOKABLE void resetSensorsAndBody(); /**jsdoc - * Moves and orients the avatar, such that it is directly underneath the HMD, with toes pointed forward. + * Moves and orients the avatar, such that it is directly underneath the HMD, with toes pointed forward in the direction of + * the HMD. * @function MyAvatar.centerBody */ Q_INVOKABLE void centerBody(); // thread-safe @@ -486,7 +486,7 @@ public: /**jsdoc * Clears inverse kinematics joint limit history. *

The internal inverse-kinematics system maintains a record of which joints are "locked". Sometimes it is useful to - * forget this history, to prevent contorted joints.

+ * forget this history to prevent contorted joints, e.g., after finishing with an override animation.

* @function MyAvatar.clearIKJointLimitHistory */ Q_INVOKABLE void clearIKJointLimitHistory(); // thread-safe @@ -502,14 +502,14 @@ public: /**jsdoc * Gets the avatar orientation. Suitable for use in QML. * @function MyAvatar.setOrientationVar - * @param {object} newOrientationVar - The avatar orientation. + * @param {object} newOrientationVar - The avatar's orientation. */ Q_INVOKABLE void setOrientationVar(const QVariant& newOrientationVar); /**jsdoc * Gets the avatar orientation. Suitable for use in QML. * @function MyAvatar.getOrientationVar - * @returns {object} The avatar orientation. + * @returns {object} The avatar's orientation. */ Q_INVOKABLE QVariant getOrientationVar() const; @@ -558,9 +558,9 @@ public: *

Note: When using pre-built animation data, it's critical that the joint orientation of the source animation and target * rig are equivalent, since the animation data applies absolute values onto the joints. If the orientations are different, * the avatar will move in unpredictable ways. For more information about avatar joint orientation standards, see - * Avatar Standards.

+ * Avatar Standards.

* @function MyAvatar.overrideAnimation - * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the + * @param url {string} The URL to the animation file. Animation files need to be FBX format, but only need to contain the * avatar skeleton and animation data. * @param fps {number} The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. * @param loop {boolean} Set to true if the animation should loop. @@ -572,6 +572,7 @@ public: * MyAvatar.overrideAnimation(ANIM_URL, 30, true, 0, 53); * Script.setTimeout(function () { * MyAvatar.restoreAnimation(); + * MyAvatar.clearIKJointLimitHistory(); * }, 3000); */ Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); @@ -617,18 +618,18 @@ public: *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or * "walkFwd". To get the full list of roles, use {@ link MyAvatar.getAnimationRoles}. - * For each role, the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how + * For each role, the avatar-animation.json defines when the animation is used, the animation clip (FBX) used, and how * animations are blended together with procedural data (such as look at vectors, hand sensors etc.). - * overrideRoleAnimation() is used to change the animation clip (.FBX) associated with a specified animation + * overrideRoleAnimation() is used to change the animation clip (FBX) associated with a specified animation * role. To end the role animation and restore the default, use {@link MyAvatar.restoreRoleAnimation}.

*

Note: Hand roles only affect the hand. Other 'main' roles, like 'idleStand', 'idleTalk', 'takeoffStand' are full body.

*

Note: When using pre-built animation data, it's critical that the joint orientation of the source animation and target * rig are equivalent, since the animation data applies absolute values onto the joints. If the orientations are different, * the avatar will move in unpredictable ways. For more information about avatar joint orientation standards, see - * Avatar Standards. + * Avatar Standards. * @function MyAvatar.overrideRoleAnimation * @param role {string} The animation role to override - * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the avatar skeleton and animation data. + * @param url {string} The URL to the animation file. Animation files need to be in FBX format, but only need to contain the avatar skeleton and animation data. * @param fps {number} The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. * @param loop {boolean} Set to true if the animation should loop * @param firstFrame {number} The frame the animation should start at @@ -653,9 +654,9 @@ public: *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or * "walkFwd". To get the full list of roles, use {@link MyAvatar.getAnimationRoles}. For each role, - * the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are + * the avatar-animation.json defines when the animation is used, the animation clip (FBX) used, and how animations are * blended together with procedural data (such as look-at vectors, hand sensors etc.). You can change the animation clip - * (.FBX) associated with a specified animation role using {@link MyAvatar.overrideRoleAnimation}. + * (FBX) associated with a specified animation role using {@link MyAvatar.overrideRoleAnimation}. * restoreRoleAnimation() is used to restore a specified animation role's default animation clip. If you have * not specified an override animation for the specified role, this function has no effect. * @function MyAvatar.restoreRoleAnimation @@ -688,10 +689,9 @@ public: * @function MyAvatar.addAnimationStateHandler * @param {function} handler - The animation state handler function to add. * @param {Array|null} propertiesList - The list of {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} - * properties that should be included the in parameter that the handler function is called with. If null + * properties that should be included in the parameter that the handler function is called with. If null * then all properties are included in the call parameter. - * @returns {number} The ID of the animation state handler function if successfully added, undefined if not - * successfully added. + * @returns {number} The ID of the animation state handler function if successfully added, undefined if not. * @example

Log all the animation state dictionary parameters for a short while.Report the rotation of your avatar's head joint relative to your avatar.Report the translation of your avatar's head joint relative to your avatar.Create and grab an entity for a short while.Report the current avatar entities.Make your avatar invisible for 10s.Report the current avatar animation JSON being used.Report when the current avatar animation JSON being used changes.
ValueName

Description
0RelaxToUnderPosesThis is a blend between PreviousSolution and - * UnderPoses: it is 15/16 PreviousSolution and 1/16 UnderPoses. This - * provides some of the benefits of using UnderPoses so that the underlying animation is still visible, - * while at the same time converging faster then using the UnderPoses only initial solution.
1RelaxToLimitCenterPosesThis is a blend between - * Previous Solution and LimitCenterPoses: it is 15/16 PreviousSolution and - * 1/16 LimitCenterPoses. This should converge quickly because it is close to the previous solution, but - * still provides the benefits of avoiding limb locking.
0RelaxToUnderPosesThis is a blend: it is 15/16 PreviousSolution + * and 1/16 UnderPoses. This provides some of the benefits of using UnderPoses so that the + * underlying animation is still visible, while at the same time converging faster then using the + * UnderPoses as the only initial solution.
1RelaxToLimitCenterPosesThis is a blend: it is 15/16 + * PreviousSolution and 1/16 LimitCenterPoses. This should converge quickly because it is + * close to the previous solution, but still provides the benefits of avoiding limb locking.
2PreviousSolutionThe IK system will begin to solve from the same position and * orientations for each joint that was the result from the previous frame.
- * Pros: because the end effectors typically do not move much from frame to frame, this is likely to converge quickly + * Pros: As the end effectors typically do not move much from frame to frame, this is likely to converge quickly * to a valid solution.
* Cons: If the previous solution resulted in an awkward or uncomfortable posture, the next frame will also be * awkward and uncomfortable. It can also result in locked elbows and knees.
3UnderPosesThe IK occurs at one of the top-most layers, it has access to the + *
3UnderPosesThe IK occurs at one of the top-most layers. It has access to the * full posture that was computed via canned animations and blends. We call this animated set of poses the "under * pose". The under poses are what would be visible if IK was completely disabled. Using the under poses as the - * initial conditions of the CCD solve will cause some of the animated motion to be blended in to the result of the + * initial conditions of the CCD solve will cause some of the animated motion to be blended into the result of the * IK. This can result in very natural results, especially if there are only a few IK targets enabled. On the other * hand, because the under poses might be quite far from the desired end effector, it can converge slowly in some * cases, causing it to never reach the IK target in the allotted number of iterations. Also, in situations where all @@ -93,7 +92,7 @@ public: * constraints. This can prevent the IK solution from getting locked or stuck at a particular constraint. For * example, if the arm is pointing straight outward from the body, as the end effector moves towards the body, at * some point the elbow should bend to accommodate. However, because the CCD solver is stuck at a local maximum, it - * will not rotate the elbow, unless the initial conditions already has the elbow bent, which is the case for + * will not rotate the elbow, unless the initial conditions already have the elbow bent, which is the case for * LimitCenterPoses. When all the IK targets are enabled, this result will provide a consistent starting * point for each IK solve, hopefully resulting in a consistent, natural result.
0FullBodyBoneSetAll joints.
1UpperBodyBoneSetOnly the "Spine" joint and its children.
2LowerBodyBoneSetOnly the leg joints and their children.
3LeftArmBoneSetJoints that are children of the "LeftShoulder" joint.
4RightArmBoneSetJoints that are children of the "RightShoulder" + *
3LeftArmBoneSetJoints that are the children of the "LeftShoulder" * joint.
5AboveTheHeadBoneSetJoints that are children of the "Head" joint.
6BelowTheHeadBoneSetJoints that are NOT children of the "head" + *
4RightArmBoneSetJoints that are the children of the "RightShoulder" + * joint.
5AboveTheHeadBoneSetJoints that are the children of the "Head" + * joint.
6BelowTheHeadBoneSetJoints that are NOT the children of the "head" * joint.
7HeadOnlyBoneSetThe "Head" joint.
8SpineOnlyBoneSetThe "Spine" joint.
9EmptyBoneSetNo joints.
10LeftHandBoneSetjoints that are children of the "LeftHand" joint.
11RightHandBoneSetJoints that are children of the "RightHand" joint.
10LeftHandBoneSetjoints that are the children of the "LeftHand" + * joint.
11RightHandBoneSetJoints that are the children of the "RightHand" + * joint.
12HipsOnlyBoneSetThe "Hips" joint.
13BothFeetBoneSetThe "LeftFoot" and "RightFoot" joints.
Report the default rotation of your avatar's head joint relative to your avatar.Report the default translation of your avatar's head joint relative to your avatar.Open your avatar's mouth wide.Report the URLs of all current attachments.Report the sensor to world matrix.Report the left hand controller matrix.Report when your avatar display name changes.Report when your avatar's session display name changes.Report when your avatar's skeleton model changes.Report when your look-at snapping setting changes.Report when your avatar's session UUID changes.Create a scriptable avatar.