diff --git a/interface/resources/icons/tablet-icons/mic-clip-i.svg b/interface/resources/icons/tablet-icons/mic-clip-i.svg new file mode 100644 index 0000000000..f912c1e744 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-clip-i.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/mic-gate-i.svg b/interface/resources/icons/tablet-icons/mic-gate-i.svg new file mode 100644 index 0000000000..8255174532 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-gate-i.svg @@ -0,0 +1,3 @@ + + + diff --git a/interface/resources/icons/tablet-icons/mic-mute-a.svg b/interface/resources/icons/tablet-icons/mic-mute-a.svg index 9dc2c53443..67eafc27c8 100644 --- a/interface/resources/icons/tablet-icons/mic-mute-a.svg +++ b/interface/resources/icons/tablet-icons/mic-mute-a.svg @@ -1,25 +1,3 @@ - - - - - - - - - - - - - - - + + diff --git a/interface/resources/icons/tablet-icons/mic-mute-i.svg b/interface/resources/icons/tablet-icons/mic-mute-i.svg index 9dc2c53443..63af1b0da8 100644 --- a/interface/resources/icons/tablet-icons/mic-mute-i.svg +++ b/interface/resources/icons/tablet-icons/mic-mute-i.svg @@ -1,25 +1,3 @@ - - - - - - - - - - - - - - - + + diff --git a/interface/resources/icons/tablet-icons/mic-mute.svg b/interface/resources/icons/tablet-icons/mic-mute.svg deleted file mode 100644 index bd42fded05..0000000000 --- a/interface/resources/icons/tablet-icons/mic-mute.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-unmute-a.svg b/interface/resources/icons/tablet-icons/mic-unmute-a.svg index b1464f207d..0bf7677017 100644 --- a/interface/resources/icons/tablet-icons/mic-unmute-a.svg +++ b/interface/resources/icons/tablet-icons/mic-unmute-a.svg @@ -1,70 +1,3 @@ - - - -image/svg+xml \ No newline at end of file + + + diff --git a/interface/resources/icons/tablet-icons/mic-unmute-i.svg b/interface/resources/icons/tablet-icons/mic-unmute-i.svg index c4eda55cbc..0bf7677017 100644 --- a/interface/resources/icons/tablet-icons/mic-unmute-i.svg +++ b/interface/resources/icons/tablet-icons/mic-unmute-i.svg @@ -1,22 +1,3 @@ - - - - - - - - - - - - - + + diff --git a/interface/resources/icons/tablet-icons/mic.svg b/interface/resources/icons/tablet-icons/mic.svg index 30b46d18dd..c961351b14 100644 --- a/interface/resources/icons/tablet-icons/mic.svg +++ b/interface/resources/icons/tablet-icons/mic.svg @@ -59,4 +59,4 @@ d="m 27.9,20.9 c 0,0 0,-3.6 0,-3.8 0,-0.7 -0.6,-1.2 -1.3,-1.2 -0.7,0 -1.2,0.6 -1.2,1.3 0,0.2 0,3.4 0,3.7 0,2.6 -2.4,4.8 -5.3,4.8 -2.9,0 -5.3,-2.1 -5.3,-4.8 0,-0.3 0,-3.5 0,-3.8 0,-0.7 -0.5,-1.3 -1.2,-1.3 -0.7,0 -1.3,0.5 -1.3,1.2 0,0.2 0,3.9 0,3.9 0,3.6 2.9,6.6 6.6,7.2 l 0,2.4 -3.1,0 c -0.7,0 -1.3,0.6 -1.3,1.3 0,0.7 0.6,1.3 1.3,1.3 l 8.8,0 c 0.7,0 1.3,-0.6 1.3,-1.3 0,-0.7 -0.6,-1.3 -1.3,-1.3 l -3.2,0 0,-2.4 c 3.6,-0.5 6.5,-3.5 6.5,-7.2 z" id="path6952" inkscape:connector-curvature="0" - style="fill:#ffffff" /> \ No newline at end of file + style="fill:#ffffff" /> diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index 4a071d2d04..3f1f598991 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -7,24 +7,57 @@ // import Hifi 1.0 as Hifi -import QtQuick 2.4 +import QtQuick 2.5 +import QtGraphicalEffects 1.0 import "./hifi/audio" as HifiAudio +import TabletScriptingInterface 1.0 + Item { id: root; objectName: "AvatarInputsBar" property int modality: Qt.NonModal - width: audio.width; - height: audio.height; - x: 10; y: 5; - + readonly property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; + x: 10; + y: 5; readonly property bool shouldReposition: true; + property bool hmdActive: HMD.active; + width: hmdActive ? audio.width : audioApplication.width; + height: hmdActive ? audio.height : audioApplication.height; + + Timer { + id: hmdActiveCheckTimer; + interval: 500; + repeat: true; + onTriggered: { + root.hmdActive = HMD.active; + } + + } HifiAudio.MicBar { id: audio; - visible: AvatarInputs.showAudioTools; + visible: AvatarInputs.showAudioTools && root.hmdActive; standalone: true; - dragTarget: parent; + dragTarget: parent; + } + + HifiAudio.MicBarApplication { + id: audioApplication; + visible: AvatarInputs.showAudioTools && !root.hmdActive; + standalone: true; + dragTarget: parent; + } + + Component.onCompleted: { + HMD.displayModeChanged.connect(function(isHmdMode) { + root.hmdActive = isHmdMode; + }); + } + + BubbleIcon { + dragTarget: parent + visible: !root.hmdActive; } } diff --git a/interface/resources/qml/BubbleIcon.qml b/interface/resources/qml/BubbleIcon.qml new file mode 100644 index 0000000000..f4e99f136c --- /dev/null +++ b/interface/resources/qml/BubbleIcon.qml @@ -0,0 +1,100 @@ +// +// Created by Bradley Austin Davis on 2015/06/19 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "./hifi/audio" as HifiAudio + +import TabletScriptingInterface 1.0 + +Rectangle { + id: bubbleRect + width: bubbleIcon.width + 10 + height: bubbleIcon.height + 10 + radius: 5; + property var dragTarget: null; + property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; + + function updateOpacity() { + if (ignoreRadiusEnabled) { + bubbleRect.opacity = 1.0; + } else { + bubbleRect.opacity = 0.7; + } + } + + Component.onCompleted: { + updateOpacity(); + } + + onIgnoreRadiusEnabledChanged: { + updateOpacity(); + } + + color: "#00000000"; + border { + width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; + color: "#80FFFFFF"; + } + anchors { + left: dragTarget ? dragTarget.right : undefined + top: dragTarget ? dragTarget.top : undefined + } + + // borders are painted over fill, so reduce the fill to fit inside the border + Rectangle { + color: "#55000000"; + width: 40; + height: 40; + + radius: 5; + + anchors { + verticalCenter: parent.verticalCenter; + horizontalCenter: parent.horizontalCenter; + } + } + + MouseArea { + id: mouseArea; + anchors.fill: parent + + hoverEnabled: true; + scrollGestureEnabled: false; + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + Users.toggleIgnoreRadius(); + } + drag.target: dragTarget; + onContainsMouseChanged: { + var rectOpacity = (ignoreRadiusEnabled && containsMouse) ? 1.0 : (containsMouse ? 1.0 : 0.7); + if (containsMouse) { + Tablet.playSound(TabletEnums.ButtonHover); + } + bubbleRect.opacity = rectOpacity; + } + } + Image { + id: bubbleIcon + source: "../icons/tablet-icons/bubble-i.svg"; + sourceSize: Qt.size(32, 32); + smooth: true; + anchors.top: parent.top + anchors.topMargin: (parent.height - bubbleIcon.height) / 2 + anchors.left: parent.left + anchors.leftMargin: (parent.width - bubbleIcon.width) / 2 + } + ColorOverlay { + id: bubbleIconOverlay + anchors.fill: bubbleIcon + source: bubbleIcon + color: "#FFFFFF"; + } +} diff --git a/interface/resources/qml/controlsUit/CheckBoxQQC2.qml b/interface/resources/qml/controlsUit/CheckBoxQQC2.qml index 91d35ecd58..bd71025aa9 100644 --- a/interface/resources/qml/controlsUit/CheckBoxQQC2.qml +++ b/interface/resources/qml/controlsUit/CheckBoxQQC2.qml @@ -24,6 +24,7 @@ CheckBox { leftPadding: 0 property int colorScheme: hifi.colorSchemes.light property string color: hifi.colors.lightGrayText + property int fontSize: hifi.fontSizes.inputLabel readonly property bool isLightColorScheme: colorScheme === hifi.colorSchemes.light property bool isRedCheck: false property bool isRound: false @@ -109,7 +110,7 @@ CheckBox { contentItem: Text { id: root - font.pixelSize: hifi.fontSizes.inputLabel + font.pixelSize: fontSize; font.family: "Raleway" font.weight: Font.DemiBold text: checkBox.text diff --git a/interface/resources/qml/controlsUit/Switch.qml b/interface/resources/qml/controlsUit/Switch.qml index 4e1c21c456..422b08b4eb 100644 --- a/interface/resources/qml/controlsUit/Switch.qml +++ b/interface/resources/qml/controlsUit/Switch.qml @@ -21,6 +21,7 @@ Item { property int switchWidth: 70; readonly property int switchRadius: height/2; property string labelTextOff: ""; + property int labelTextSize: hifi.fontSizes.inputLabel; property string labelGlyphOffText: ""; property int labelGlyphOffSize: 32; property string labelTextOn: ""; @@ -89,7 +90,7 @@ Item { RalewaySemiBold { id: labelOff; text: labelTextOff; - size: hifi.fontSizes.inputLabel; + size: labelTextSize; color: originalSwitch.checked ? hifi.colors.lightGrayText : "#FFFFFF"; anchors.top: parent.top; anchors.right: parent.right; @@ -130,7 +131,7 @@ Item { RalewaySemiBold { id: labelOn; text: labelTextOn; - size: hifi.fontSizes.inputLabel; + size: labelTextSize; color: originalSwitch.checked ? "#FFFFFF" : hifi.colors.lightGrayText; anchors.top: parent.top; anchors.left: parent.left; diff --git a/interface/resources/qml/hifi/EditAvatarInputsBar.qml b/interface/resources/qml/hifi/EditAvatarInputsBar.qml new file mode 100644 index 0000000000..b27b0c8db2 --- /dev/null +++ b/interface/resources/qml/hifi/EditAvatarInputsBar.qml @@ -0,0 +1,152 @@ +// +// EditAvatarInputsBar.qml +// qml/hifi +// +// Audio setup +// +// Created by Wayne Chen on 3/20/2019 +// 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 +// + +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../windows" + +Rectangle { + id: editRect + + HifiConstants { id: hifi; } + + color: hifi.colors.baseGray; + + signal sendToScript(var message); + function emitSendToScript(message) { + sendToScript(message); + } + + function fromScript(message) { + } + + RalewayRegular { + id: title; + color: hifi.colors.white; + text: qsTr("Avatar Inputs Persistent UI Settings") + size: 20 + font.bold: true + anchors { + top: parent.top + left: parent.left + leftMargin: (parent.width - width) / 2 + } + } + + HifiControlsUit.Slider { + id: xSlider + anchors { + top: title.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + label: "X OFFSET: " + value.toFixed(2); + maximumValue: 1.0 + minimumValue: -1.0 + stepSize: 0.05 + value: -0.2 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "x": value + }); + } + } + + HifiControlsUit.Slider { + id: ySlider + anchors { + top: xSlider.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + label: "Y OFFSET: " + value.toFixed(2); + maximumValue: 1.0 + minimumValue: -1.0 + stepSize: 0.05 + value: -0.125 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "y": value + }); + } + } + + HifiControlsUit.Slider { + id: zSlider + anchors { + top: ySlider.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + label: "Z OFFSET: " + value.toFixed(2); + maximumValue: 0.0 + minimumValue: -1.0 + stepSize: 0.05 + value: -0.5 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "z": value + }); + } + } + + HifiControlsUit.Button { + id: setVisibleButton; + text: setVisible ? "SET INVISIBLE" : "SET VISIBLE"; + width: 300; + property bool setVisible: true; + anchors { + top: zSlider.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + onClicked: { + setVisible = !setVisible; + emitSendToScript({ + "method": "setVisible", + "visible": setVisible + }); + } + } + + HifiControlsUit.Button { + id: printButton; + text: "PRINT POSITIONS"; + width: 300; + anchors { + top: setVisibleButton.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + onClicked: { + emitSendToScript({ + "method": "print", + }); + } + } +} 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; diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 195dd78a0e..f7e2494813 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -31,6 +31,8 @@ Rectangle { property string title: "Audio Settings" property int switchHeight: 16 property int switchWidth: 40 + property bool pushToTalk: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; + property bool muted: (bar.currentIndex === 0) ? AudioScriptingInterface.mutedDesktop : AudioScriptingInterface.mutedHMD; readonly property real verticalScrollWidth: 10 readonly property real verticalScrollShaft: 8 signal sendToScript(var message); @@ -44,7 +46,7 @@ Rectangle { property bool isVR: AudioScriptingInterface.context === "VR" - property real rightMostInputLevelPos: 440 + property real rightMostInputLevelPos: root.width //placeholder for control sizes and paddings //recalculates dynamically in case of UI size is changed QtObject { @@ -103,7 +105,9 @@ Rectangle { } } - Component.onCompleted: enablePeakValues(); + Component.onCompleted: { + enablePeakValues(); + } Flickable { id: flickView; @@ -178,15 +182,25 @@ Rectangle { height: root.switchHeight; switchWidth: root.switchWidth; labelTextOn: "Mute microphone"; + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; - checked: AudioScriptingInterface.muted; + checked: muted; onClicked: { - if (AudioScriptingInterface.pushToTalk && !checked) { + if (pushToTalk && !checked) { // disable push to talk if unmuting - AudioScriptingInterface.pushToTalk = false; + if (bar.currentIndex === 0) { + AudioScriptingInterface.pushToTalkDesktop = false; + } + else { + AudioScriptingInterface.pushToTalkHMD = false; + } + } + if (bar.currentIndex === 0) { + AudioScriptingInterface.mutedDesktop = checked; + } + else { + AudioScriptingInterface.mutedHMD = checked; } - AudioScriptingInterface.muted = checked; - checked = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding } } @@ -198,6 +212,7 @@ Rectangle { anchors.topMargin: 24 anchors.left: parent.left labelTextOn: "Noise Reduction"; + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.noiseReduction; onCheckedChanged: { @@ -213,7 +228,8 @@ Rectangle { anchors.top: noiseReductionSwitch.bottom anchors.topMargin: 24 anchors.left: parent.left - labelTextOn: qsTr("Push To Talk (T)"); + labelTextOn: (bar.currentIndex === 0) ? qsTr("Push To Talk (T)") : qsTr("Push To Talk"); + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; onCheckedChanged: { @@ -222,13 +238,6 @@ Rectangle { } else { AudioScriptingInterface.pushToTalkHMD = checked; } - checked = Qt.binding(function() { - if (bar.currentIndex === 0) { - return AudioScriptingInterface.pushToTalkDesktop; - } else { - return AudioScriptingInterface.pushToTalkHMD; - } - }); // restore binding } } } @@ -245,7 +254,8 @@ Rectangle { switchWidth: root.switchWidth; anchors.top: parent.top anchors.left: parent.left - labelTextOn: qsTr("Warn when muted"); + labelTextOn: qsTr("Warn when muted in HMD"); + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.warnWhenMuted; onClicked: { @@ -263,6 +273,7 @@ Rectangle { anchors.topMargin: 24 anchors.left: parent.left labelTextOn: qsTr("Audio Level Meter"); + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AvatarInputs.showAudioTools; onCheckedChanged: { @@ -279,6 +290,7 @@ Rectangle { anchors.topMargin: 24 anchors.left: parent.left labelTextOn: qsTr("Stereo input"); + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.isStereoInput; onCheckedChanged: { @@ -314,7 +326,7 @@ Rectangle { Separator { id: secondSeparator; - anchors.top: pttTextContainer.bottom; + anchors.top: pttTextContainer.visible ? pttTextContainer.bottom : switchesContainer.bottom; anchors.topMargin: 10; } @@ -341,7 +353,7 @@ Rectangle { width: margins.sizeText + margins.sizeLevel; anchors.left: parent.left; anchors.leftMargin: margins.sizeCheckBox; - size: 16; + size: 22; color: hifi.colors.white; text: qsTr("Choose input device"); } @@ -349,16 +361,17 @@ Rectangle { ListView { id: inputView; - width: parent.width - margins.paddings*2; + width: rightMostInputLevelPos; anchors.top: inputDeviceHeader.bottom; anchors.topMargin: 10; x: margins.paddings + interactive: false; height: contentHeight; spacing: 4; clip: true; model: AudioScriptingInterface.devices.input; delegate: Item { - width: rightMostInputLevelPos + width: rightMostInputLevelPos - margins.paddings*2 height: margins.sizeCheckBox > checkBoxInput.implicitHeight ? margins.sizeCheckBox : checkBoxInput.implicitHeight @@ -374,6 +387,7 @@ Rectangle { boxSize: margins.sizeCheckBox / 2 isRound: true text: devicename + fontSize: 16; onPressed: { if (!checked) { stereoInput.checked = false; @@ -407,7 +421,7 @@ Rectangle { Separator { id: thirdSeparator; - anchors.top: loopbackAudio.bottom; + anchors.top: loopbackAudio.visible ? loopbackAudio.bottom : inputView.bottom; anchors.topMargin: 10; } @@ -434,7 +448,7 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: margins.sizeCheckBox anchors.verticalCenter: parent.verticalCenter; - size: 16; + size: 22; color: hifi.colors.white; text: qsTr("Choose output device"); } @@ -443,7 +457,8 @@ Rectangle { ListView { id: outputView width: parent.width - margins.paddings*2 - x: margins.paddings + x: margins.paddings; + interactive: false; height: contentHeight; anchors.top: outputDeviceHeader.bottom; anchors.topMargin: 10; @@ -464,6 +479,7 @@ Rectangle { checked: bar.currentIndex === 0 ? selectedDesktop : selectedHMD; checkable: !checked text: devicename + fontSize: 16 onPressed: { if (!checked) { AudioScriptingInterface.setOutputDevice(info, bar.currentIndex === 1); @@ -526,7 +542,7 @@ Rectangle { RalewayRegular { // The slider for my card is special, it controls the master gain id: avatarGainSliderText; - text: "Avatar volume"; + text: "People volume"; size: 16; anchors.left: parent.left; color: hifi.colors.white; diff --git a/interface/resources/qml/hifi/audio/InputPeak.qml b/interface/resources/qml/hifi/audio/InputPeak.qml index 00f7e63528..d8b166cee4 100644 --- a/interface/resources/qml/hifi/audio/InputPeak.qml +++ b/interface/resources/qml/hifi/audio/InputPeak.qml @@ -12,24 +12,26 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 -Rectangle { +Item { property var peak; width: 70; height: 8; - color: "transparent"; - - Item { + QtObject { id: colors; + readonly property string unmuted: "#FFF"; readonly property string muted: "#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 fill: "#55000000"; } + Text { id: status; @@ -79,23 +81,19 @@ Rectangle { anchors { fill: mask } source: mask start: Qt.point(0, 0); - end: Qt.point(70, 0); + end: Qt.point(bar.width, 0); gradient: Gradient { GradientStop { position: 0; color: colors.greenStart; } GradientStop { - position: 0.8; + position: 0.5; color: colors.greenEnd; } - GradientStop { - position: 0.801; - color: colors.red; - } GradientStop { position: 1; - color: colors.red; + color: colors.yellow; } } } diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index db0614dfdc..d3fd8ee075 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -60,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: 18; + color: "white"; + font.italic: true + text: audioLoopedBack ? qsTr("Speak in your input") : ""; + } } diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index f51da9c381..55378589ec 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -16,14 +16,33 @@ import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { + id: micBar HifiConstants { id: hifi; } + property var muted: AudioScriptingInterface.muted; readonly property var level: AudioScriptingInterface.inputLevel; + readonly property var clipping: AudioScriptingInterface.clipping; + property var pushToTalk: AudioScriptingInterface.pushToTalk; + property var pushingToTalk: AudioScriptingInterface.pushingToTalk; + readonly property var userSpeakingLevel: 0.4; property bool gated: false; 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; + }); + AudioScriptingInterface.pushingToTalkChanged.connect(function() { + pushingToTalk = AudioScriptingInterface.pushingToTalk; + }); } property bool standalone: false; @@ -67,10 +86,10 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { - if (AudioScriptingInterface.pushToTalk) { + if (pushToTalk) { return; } - AudioScriptingInterface.muted = !AudioScriptingInterface.muted; + AudioScriptingInterface.muted = !muted; Tablet.playSound(TabletEnums.ButtonClick); } drag.target: dragTarget; @@ -84,16 +103,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 { @@ -113,9 +132,12 @@ Rectangle { readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; + readonly property string clippingIcon: "../../../icons/tablet-icons/mic-clip-i.svg"; + readonly property string gatedIcon: "../../../icons/tablet-icons/mic-gate-i.svg"; id: image; - source: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? pushToTalkIcon : AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : + clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; width: 30; height: 30; @@ -138,9 +160,7 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; - - visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; + visible: (pushToTalk && !pushingToTalk) || muted; anchors { left: parent.left; @@ -157,9 +177,9 @@ Rectangle { verticalCenter: parent.verticalCenter; } - color: parent.color; + color: colors.icon; - 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,9 +189,9 @@ 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; + color: colors.icon; } Rectangle { @@ -180,9 +200,9 @@ 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; + color: colors.icon; } } diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml new file mode 100644 index 0000000000..bc3f4dff89 --- /dev/null +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -0,0 +1,255 @@ +// +// MicBarApplication.qml +// qml/hifi/audio +// +// Created by Zach Pomerantz on 6/14/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import stylesUit 1.0 +import TabletScriptingInterface 1.0 + +Rectangle { + id: micBar; + readonly property var level: AudioScriptingInterface.inputLevel; + readonly property var clipping: AudioScriptingInterface.clipping; + property var muted: AudioScriptingInterface.muted; + property var pushToTalk: AudioScriptingInterface.pushToTalk; + property var pushingToTalk: AudioScriptingInterface.pushingToTalk; + readonly property var userSpeakingLevel: 0.4; + property bool gated: false; + 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; + }); + } + + readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; + readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; + readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; + readonly property string clippingIcon: "../../../icons/tablet-icons/mic-clip-i.svg"; + readonly property string gatedIcon: "../../../icons/tablet-icons/mic-gate-i.svg"; + property bool standalone: false; + property var dragTarget: null; + + width: 44; + height: 44; + + radius: 5; + opacity: 0.7; + + onLevelChanged: { + var rectOpacity = (muted && (level >= userSpeakingLevel)) ? 1.0 : 0.7; + if (pushToTalk && !pushingToTalk) { + rectOpacity = (mouseArea.containsMouse) ? 1.0 : 0.7; + } else if (mouseArea.containsMouse && rectOpacity != 1.0) { + rectOpacity = 1.0; + } + micBar.opacity = rectOpacity; + } + + color: "#00000000"; + border { + width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; + color: colors.border; + } + + // borders are painted over fill, so reduce the fill to fit inside the border + Rectangle { + color: standalone ? colors.fill : "#00000000"; + width: 40; + height: 40; + + radius: 5; + + anchors { + verticalCenter: parent.verticalCenter; + horizontalCenter: parent.horizontalCenter; + } + } + + MouseArea { + id: mouseArea; + + anchors { + left: icon.left; + right: bar.right; + top: icon.top; + bottom: icon.bottom; + } + + hoverEnabled: true; + scrollGestureEnabled: false; + onClicked: { + if (pushToTalk) { + return; + } + AudioScriptingInterface.muted = !muted; + Tablet.playSound(TabletEnums.ButtonClick); + muted = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding + } + drag.target: dragTarget; + onContainsMouseChanged: { + if (containsMouse) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + } + + QtObject { + id: colors; + + readonly property string unmutedColor: "#FFF"; + readonly property string gatedColor: "#00BDFF"; + 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 fill: "#55000000"; + readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; + readonly property string icon: (muted || clipping) ? mutedColor : gated ? gatedColor : unmutedColor; + } + + Item { + id: icon; + + anchors { + left: parent.left; + top: parent.top; + } + + width: 40; + height: 40; + + Item { + Image { + id: image; + source: (pushToTalk) ? pushToTalkIcon : muted ? mutedIcon : + clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; + width: 29; + height: 32; + anchors { + left: parent.left; + top: parent.top; + topMargin: 5; + } + } + + ColorOverlay { + id: imageOverlay + anchors { fill: image } + source: image; + color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : colors.icon; + } + } + } + + Item { + id: status; + + visible: pushToTalk || (muted && (level >= userSpeakingLevel)); + + anchors { + left: parent.left; + top: icon.bottom; + topMargin: 2; + } + + width: parent.width; + height: statusTextMetrics.height; + + TextMetrics { + id: statusTextMetrics + text: statusText.text + font: statusText.font + } + + RalewaySemiBold { + id: statusText + anchors { + horizontalCenter: parent.horizontalCenter; + verticalCenter: parent.verticalCenter; + } + + color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : (level >= userSpeakingLevel && muted) ? colors.mutedColor : colors.unmutedColor; + font.bold: true + + text: pushToTalk ? (HMD.active ? "PTT" : "PTT-(T)") : (muted ? "MUTED" : "MUTE"); + size: 12; + } + } + + Item { + id: bar; + + anchors { + right: parent.right; + rightMargin: 7; + top: parent.top + topMargin: 5 + } + + width: 8; + height: 32; + + Rectangle { // base + id: baseBar + radius: 4; + anchors { fill: parent } + color: colors.gutter; + } + + Rectangle { // mask + id: mask; + visible: (!(pushToTalk && !pushingToTalk)) + height: parent.height * level; + width: parent.width; + radius: 5; + anchors { + bottom: parent.bottom; + bottomMargin: 0; + left: parent.left; + leftMargin: 0; + } + } + + LinearGradient { + anchors { fill: mask } + visible: (!(pushToTalk && !pushingToTalk)) + source: mask + start: Qt.point(0, 0); + end: Qt.point(0, bar.height); + rotation: 180 + gradient: Gradient { + GradientStop { + position: 0.0; + color: colors.greenStart; + } + GradientStop { + position: 0.5; + color: colors.greenEnd; + } + GradientStop { + position: 1.0; + color: colors.yellow; + } + } + } + } +} diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index 033af99d04..f1351b8cbf 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -64,11 +64,11 @@ RowLayout { 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: 18; + color: "white"; + font.italic: true + text: isPlaying ? qsTr("Listen to your output") : ""; + } } diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index a1da69a44a..1a1e0a96ff 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -40,7 +40,7 @@ Item { } } - HifiAudio.MicBar { + HifiAudio.MicBarApplication { anchors { left: parent.left leftMargin: 30 diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 47f3b774b2..8225d27033 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -338,6 +338,10 @@ Setting::Handle maxOctreePacketsPerSecond{"maxOctreePPS", DEFAULT_MAX_OCTRE Setting::Handle loginDialogPoppedUp{"loginDialogPoppedUp", false}; +static const QUrl AVATAR_INPUTS_BAR_QML = PathUtils::qmlUrl("AvatarInputsBar.qml"); +static const QUrl MIC_BAR_APPLICATION_QML = PathUtils::qmlUrl("hifi/audio/MicBarApplication.qml"); +static const QUrl BUBBLE_ICON_QML = PathUtils::qmlUrl("BubbleIcon.qml"); + 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"; @@ -2372,7 +2376,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); }); auto rootItemLoadedFunctor = [webSurface, url, isTablet] { - Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString()); + Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString() || url == AVATAR_INPUTS_BAR_QML.toString() || + url == BUBBLE_ICON_QML.toString()); }; if (webSurface->getRootItem()) { rootItemLoadedFunctor(); @@ -2878,11 +2883,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 @@ -3291,6 +3304,7 @@ void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml"); offscreenUi->show(qml, "AvatarInputsBar"); #endif + _desktopRootItemCreated = true; } void Application::userKickConfirmation(const QUuid& nodeID) { @@ -8972,6 +8986,38 @@ void Application::updateLoginDialogPosition() { } } +void Application::createAvatarInputsBar() { + const glm::vec3 LOCAL_POSITION { 0.0, 0.0, -1.0 }; + // DEFAULT_DPI / tablet scale percentage + const float DPI = 31.0f / (75.0f / 100.0f); + + EntityItemProperties properties; + properties.setType(EntityTypes::Web); + properties.setName("AvatarInputsBarEntity"); + properties.setSourceUrl(AVATAR_INPUTS_BAR_QML.toString()); + properties.setParentID(getMyAvatar()->getSelfID()); + properties.setParentJointIndex(getMyAvatar()->getJointIndex("_CAMERA_MATRIX")); + properties.setPosition(LOCAL_POSITION); + properties.setLocalRotation(Quaternions::IDENTITY); + //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(); + _avatarInputsBarID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); +} + +void Application::destroyAvatarInputsBar() { + auto entityScriptingInterface = DependencyManager::get(); + if (!_avatarInputsBarID.isNull()) { + entityScriptingInterface->deleteEntity(_avatarInputsBarID); + } +} + bool Application::hasRiftControllers() { return PluginUtils::isOculusTouchControllerAvailable(); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 762ac9585a..99e57f1866 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -330,6 +330,9 @@ public: void createLoginDialog(); void updateLoginDialogPosition(); + void createAvatarInputsBar(); + void destroyAvatarInputsBar(); + // Check if a headset is connected bool hasRiftControllers(); bool hasViveControllers(); @@ -704,12 +707,14 @@ private: int _maxOctreePPS = DEFAULT_MAX_OCTREE_PPS; bool _interstitialModeEnabled{ false }; - bool _loginDialogPoppedUp = false; + bool _loginDialogPoppedUp{ false }; + bool _desktopRootItemCreated{ false }; bool _developerMenuVisible{ false }; QString _previousAvatarSkeletonModel; float _previousAvatarTargetScale; CameraMode _previousCameraMode; QUuid _loginDialogID; + QUuid _avatarInputsBarID; LoginStateManager _loginStateManager; quint64 _lastFaceTrackerUpdate; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 69f7054953..38bdd061c0 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include "Application.h" #include "InterfaceLogging.h" @@ -84,7 +85,6 @@ AvatarManager::AvatarManager(QObject* parent) : AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { AvatarSharedPointer avatar = AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer); - const auto otherAvatar = std::static_pointer_cast(avatar); if (otherAvatar && _space) { std::unique_lock lock(_spaceLock); @@ -210,7 +210,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { { // lock the hash for read to check the size QReadLocker lock(&_hashLock); - if (_avatarHash.size() < 2 && _avatarsToFadeOut.isEmpty()) { + if (_avatarHash.size() < 2) { return; } } @@ -375,19 +375,12 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { qApp->getMain3DScene()->enqueueTransaction(renderTransaction); } - if (!_spaceProxiesToDelete.empty() && _space) { - std::unique_lock lock(_spaceLock); - workloadTransaction.remove(_spaceProxiesToDelete); - _spaceProxiesToDelete.clear(); - } _space->enqueueTransaction(workloadTransaction); _numAvatarsUpdated = numAvatarsUpdated; _numAvatarsNotUpdated = numAvatarsNotUpdated; _numHeroAvatarsUpdated = numHerosUpdated; - simulateAvatarFades(deltaTime); - _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; } @@ -400,31 +393,6 @@ void AvatarManager::postUpdate(float deltaTime, const render::ScenePointer& scen } } -void AvatarManager::simulateAvatarFades(float deltaTime) { - if (_avatarsToFadeOut.empty()) { - return; - } - - QReadLocker locker(&_hashLock); - QVector::iterator avatarItr = _avatarsToFadeOut.begin(); - const render::ScenePointer& scene = qApp->getMain3DScene(); - render::Transaction transaction; - while (avatarItr != _avatarsToFadeOut.end()) { - auto avatar = std::static_pointer_cast(*avatarItr); - avatar->updateFadingStatus(); - if (!avatar->isFading()) { - // fading to zero is such a rare event we push a unique transaction for each - if (avatar->isInScene()) { - avatar->removeFromScene(*avatarItr, scene, transaction); - } - avatarItr = _avatarsToFadeOut.erase(avatarItr); - } else { - ++avatarItr; - } - } - scene->enqueueTransaction(transaction); -} - AvatarSharedPointer AvatarManager::newSharedAvatar(const QUuid& sessionUUID) { auto otherAvatar = new OtherAvatar(qApp->thread()); otherAvatar->setSessionUUID(sessionUUID); @@ -452,7 +420,6 @@ void AvatarManager::buildPhysicsTransaction(PhysicsEngine::Transaction& transact transaction.objectsToRemove.push_back(mState); } avatar->resetDetailedMotionStates(); - } else { if (avatar->getDetailedMotionStates().size() == 0) { avatar->createDetailedMotionStates(avatar); @@ -520,10 +487,6 @@ void AvatarManager::removeDeadAvatarEntities(const SetOfEntities& deadEntities) void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) { auto avatar = std::static_pointer_cast(removedAvatar); - { - std::unique_lock lock(_spaceLock); - _spaceProxiesToDelete.push_back(avatar->getSpaceIndex()); - } AvatarHashMap::handleRemovedAvatar(avatar, removalReason); avatar->tearDownGrabs(); @@ -534,16 +497,39 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar // it might not fire until after we create a new instance for the same remote avatar, which creates a race // on the creation of entities for that avatar instance and the deletion of entities for this instance avatar->removeAvatarEntitiesFromTree(); - if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { + emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID()); emit DependencyManager::get()->enteredIgnoreRadius(); + + workload::Transaction workloadTransaction; + workloadTransaction.remove(avatar->getSpaceIndex()); + _space->enqueueTransaction(workloadTransaction); + + const render::ScenePointer& scene = qApp->getMain3DScene(); + render::Transaction transaction; + avatar->removeFromScene(avatar, scene, transaction); + scene->enqueueTransaction(transaction); } else if (removalReason == KillAvatarReason::AvatarDisconnected) { // remove from node sets, if present DependencyManager::get()->removeFromIgnoreMuteSets(avatar->getSessionUUID()); DependencyManager::get()->avatarDisconnected(avatar->getSessionUUID()); - avatar->fadeOut(qApp->getMain3DScene(), removalReason); + render::Transaction transaction; + auto scene = qApp->getMain3DScene(); + avatar->fadeOut(transaction, removalReason); + + workload::SpacePointer space = _space; + transaction.transitionFinishedOperator(avatar->getRenderItemID(), [space, avatar]() { + const render::ScenePointer& scene = qApp->getMain3DScene(); + render::Transaction transaction; + avatar->removeFromScene(avatar, scene, transaction); + scene->enqueueTransaction(transaction); + + workload::Transaction workloadTransaction; + workloadTransaction.remove(avatar->getSpaceIndex()); + space->enqueueTransaction(workloadTransaction); + }); + scene->enqueueTransaction(transaction); } - _avatarsToFadeOut.push_back(removedAvatar); } void AvatarManager::clearOtherAvatars() { diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 0468fbd809..f9b82da0c1 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -220,8 +220,6 @@ private: explicit AvatarManager(QObject* parent = 0); explicit AvatarManager(const AvatarManager& other); - void simulateAvatarFades(float deltaTime); - AvatarSharedPointer newSharedAvatar(const QUuid& sessionUUID) override; // called only from the AvatarHashMap thread - cannot be called while this thread holds the @@ -231,8 +229,6 @@ private: KillAvatarReason removalReason = KillAvatarReason::NoReason) override; void handleTransitAnimations(AvatarTransit::Status status); - QVector _avatarsToFadeOut; - using SetOfOtherAvatars = std::set; SetOfOtherAvatars _avatarsToChangeInPhysics; @@ -252,7 +248,6 @@ private: mutable std::mutex _spaceLock; workload::SpacePointer _space; - std::vector _spaceProxiesToDelete; AvatarTransit::TransitConfig _transitConfig; }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 27151d0615..9ffcd0184e 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -942,8 +942,6 @@ void MyAvatar::simulate(float deltaTime, bool inView) { } handleChangedAvatarEntityData(); - - updateFadingStatus(); } // As far as I know no HMD system supports a play area of a kilometer in radius. diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index b100b33dc8..2e775a20c3 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -356,7 +356,6 @@ void OtherAvatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "grabs"); applyGrabChanges(); } - updateFadingStatus(); } void OtherAvatar::handleChangedAvatarEntityData() { diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 4f2171d451..caae946116 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -88,44 +88,44 @@ void Audio::setMuted(bool isMuted) { void Audio::setMutedDesktop(bool isMuted) { bool changed = false; withWriteLock([&] { - if (_desktopMuted != isMuted) { + if (_mutedDesktop != isMuted) { changed = true; - _desktopMuted = isMuted; + _mutedDesktop = isMuted; auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); } }); if (changed) { emit mutedChanged(isMuted); - emit desktopMutedChanged(isMuted); + emit mutedDesktopChanged(isMuted); } } bool Audio::getMutedDesktop() const { return resultWithReadLock([&] { - return _desktopMuted; + return _mutedDesktop; }); } void Audio::setMutedHMD(bool isMuted) { bool changed = false; withWriteLock([&] { - if (_hmdMuted != isMuted) { + if (_mutedHMD != isMuted) { changed = true; - _hmdMuted = isMuted; + _mutedHMD = isMuted; auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); } }); if (changed) { emit mutedChanged(isMuted); - emit hmdMutedChanged(isMuted); + emit mutedHMDChanged(isMuted); } } bool Audio::getMutedHMD() const { return resultWithReadLock([&] { - return _hmdMuted; + return _mutedHMD; }); } @@ -217,17 +217,17 @@ void Audio::setPTTHMD(bool enabled) { } void Audio::saveData() { - _desktopMutedSetting.set(getMutedDesktop()); - _hmdMutedSetting.set(getMutedHMD()); + _mutedDesktopSetting.set(getMutedDesktop()); + _mutedHMDSetting.set(getMutedHMD()); _pttDesktopSetting.set(getPTTDesktop()); _pttHMDSetting.set(getPTTHMD()); } void Audio::loadData() { - _desktopMuted = _desktopMutedSetting.get(); - _hmdMuted = _hmdMutedSetting.get(); - _pttDesktop = _pttDesktopSetting.get(); - _pttHMD = _pttHMDSetting.get(); + setMutedDesktop(_mutedDesktopSetting.get()); + setMutedHMD(_mutedHMDSetting.get()); + setPTTDesktop(_pttDesktopSetting.get()); + setPTTHMD(_pttHMDSetting.get()); auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted()), Q_ARG(bool, false)); diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index d6823ea452..00da566b30 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -41,6 +41,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { * @hifi-assignment-client * * @property {boolean} muted - true if the audio input is muted, otherwise false. + * @property {boolean} mutedDesktop - true if the audio input is muted, otherwise false. * @property {boolean} noiseReduction - true if noise reduction is enabled, otherwise false. When * enabled, the input audio signal is blocked (fully attenuated) when it falls below an adaptive threshold set just * above the noise floor. @@ -68,8 +69,8 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) Q_PROPERTY(QString context READ getContext NOTIFY contextChanged) Q_PROPERTY(AudioDevices* devices READ getDevices NOTIFY nop) - Q_PROPERTY(bool desktopMuted READ getMutedDesktop WRITE setMutedDesktop NOTIFY desktopMutedChanged) - Q_PROPERTY(bool hmdMuted READ getMutedHMD WRITE setMutedHMD NOTIFY hmdMutedChanged) + Q_PROPERTY(bool mutedDesktop READ getMutedDesktop WRITE setMutedDesktop NOTIFY mutedDesktopChanged) + Q_PROPERTY(bool mutedHMD READ getMutedHMD WRITE setMutedHMD NOTIFY mutedHMDChanged) Q_PROPERTY(bool pushToTalk READ getPTT WRITE setPTT NOTIFY pushToTalkChanged); Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) @@ -287,19 +288,19 @@ signals: /**jsdoc * Triggered when desktop audio input is muted or unmuted. - * @function Audio.desktopMutedChanged + * @function Audio.mutedDesktopChanged * @param {boolean} isMuted - true if the audio input is muted for desktop mode, otherwise false. * @returns {Signal} */ - void desktopMutedChanged(bool isMuted); + void mutedDesktopChanged(bool isMuted); /**jsdoc * Triggered when HMD audio input is muted or unmuted. - * @function Audio.hmdMutedChanged + * @function Audio.mutedHMDChanged * @param {boolean} isMuted - true if the audio input is muted for HMD mode, otherwise false. * @returns {Signal} */ - void hmdMutedChanged(bool isMuted); + void mutedHMDChanged(bool isMuted); /** * Triggered when Push-to-Talk has been enabled or disabled. @@ -418,12 +419,12 @@ private: bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; - Setting::Handle _desktopMutedSetting{ QStringList { Audio::AUDIO, "desktopMuted" }, true }; - Setting::Handle _hmdMutedSetting{ QStringList { Audio::AUDIO, "hmdMuted" }, true }; + Setting::Handle _mutedDesktopSetting{ QStringList { Audio::AUDIO, "mutedDesktop" }, true }; + Setting::Handle _mutedHMDSetting{ QStringList { Audio::AUDIO, "mutedHMD" }, true }; Setting::Handle _pttDesktopSetting{ QStringList { Audio::AUDIO, "pushToTalkDesktop" }, false }; Setting::Handle _pttHMDSetting{ QStringList { Audio::AUDIO, "pushToTalkHMD" }, false }; - bool _desktopMuted{ true }; - bool _hmdMuted{ false }; + bool _mutedDesktop{ true }; + bool _mutedHMD{ false }; bool _pttDesktop{ false }; bool _pttHMD{ false }; bool _pushingToTalk{ false }; diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 0aa352de23..80604f354b 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "Application.h" #include "Menu.h" @@ -30,6 +31,10 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QObject* parent) : QObject(parent) { _showAudioTools = showAudioToolsSetting.get(); + auto nodeList = DependencyManager::get(); + auto usersScriptingInterface = DependencyManager::get(); + connect(nodeList.data(), &NodeList::ignoreRadiusEnabledChanged, this, &AvatarInputs::ignoreRadiusEnabledChanged); + connect(usersScriptingInterface.data(), &UsersScriptingInterface::enteredIgnoreRadius, this, &AvatarInputs::enteredIgnoreRadiusChanged); } #define AI_UPDATE(name, src) \ @@ -83,6 +88,10 @@ void AvatarInputs::setShowAudioTools(bool showAudioTools) { emit showAudioToolsChanged(_showAudioTools); } +bool AvatarInputs::getIgnoreRadiusEnabled() const { + return DependencyManager::get()->getIgnoreRadiusEnabled(); +} + void AvatarInputs::toggleCameraMute() { FaceTracker* faceTracker = qApp->getSelectedFaceTracker(); if (faceTracker) { diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 6569792807..f53adc1749 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -42,6 +42,8 @@ class AvatarInputs : public QObject { AI_PROPERTY(bool, isHMD, false) Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged) + Q_PROPERTY(bool ignoreRadiusEnabled READ getIgnoreRadiusEnabled NOTIFY ignoreRadiusEnabledChanged) + //Q_PROPERTY(bool enteredIgnoreRadius READ getEnteredIgnoreRadius NOTIFY enteredIgnoreRadiusChanged) public: static AvatarInputs* getInstance(); @@ -55,7 +57,9 @@ public: AvatarInputs(QObject* parent = nullptr); void update(); - bool showAudioTools() const { return _showAudioTools; } + bool showAudioTools() const { return _showAudioTools; } + bool getIgnoreRadiusEnabled() const; + //bool getEnteredIgnoreRadius() const; public slots: @@ -93,6 +97,34 @@ signals: */ void showAudioToolsChanged(bool show); + /**jsdoc + * @function AvatarInputs.avatarEnteredIgnoreRadius + * @param {QUuid} avatarID + * @returns {Signal} + */ + void avatarEnteredIgnoreRadius(QUuid avatarID); + + /**jsdoc + * @function AvatarInputs.avatarLeftIgnoreRadius + * @param {QUuid} avatarID + * @returns {Signal} + */ + void avatarLeftIgnoreRadius(QUuid avatarID); + + /**jsdoc + * @function AvatarInputs.ignoreRadiusEnabledChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void ignoreRadiusEnabledChanged(bool enabled); + + /**jsdoc + * @function AvatarInputs.enteredIgnoreRadiusChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void enteredIgnoreRadiusChanged(); + protected: /**jsdoc @@ -106,6 +138,8 @@ protected: Q_INVOKABLE void toggleCameraMute(); private: + void onAvatarEnteredIgnoreRadius(); + void onAvatarLeftIgnoreRadius(); float _trailingAudioLoudness{ 0 }; bool _showAudioTools { false }; }; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 839c4ed1d9..74940b44cc 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -652,9 +652,8 @@ void Avatar::fadeIn(render::ScenePointer scene) { scene->enqueueTransaction(transaction); } -void Avatar::fadeOut(render::ScenePointer scene, KillAvatarReason reason) { +void Avatar::fadeOut(render::Transaction& transaction, KillAvatarReason reason) { render::Transition::Type transitionType = render::Transition::USER_LEAVE_DOMAIN; - render::Transaction transaction; if (reason == KillAvatarReason::YourAvatarEnteredTheirBubble) { transitionType = render::Transition::BUBBLE_ISECT_TRESPASSER; @@ -662,7 +661,6 @@ void Avatar::fadeOut(render::ScenePointer scene, KillAvatarReason reason) { transitionType = render::Transition::BUBBLE_ISECT_OWNER; } fade(transaction, transitionType); - scene->enqueueTransaction(transaction); } void Avatar::fade(render::Transaction& transaction, render::Transition::Type type) { @@ -672,19 +670,6 @@ void Avatar::fade(render::Transaction& transaction, render::Transition::Type typ transaction.addTransitionToItem(itemId, type, _renderItemID); } } - _isFading = true; -} - -void Avatar::updateFadingStatus() { - if (_isFading) { - render::Transaction transaction; - transaction.queryTransitionOnItem(_renderItemID, [this](render::ItemID id, const render::Transition* transition) { - if (!transition || transition->isFinished) { - _isFading = false; - } - }); - AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); - } } void Avatar::removeFromScene(AvatarSharedPointer self, const render::ScenePointer& scene, render::Transaction& transaction) { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 974fae2034..aef5ac09e9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -520,9 +520,7 @@ public: bool isMoving() const { return _moving; } void fadeIn(render::ScenePointer scene); - void fadeOut(render::ScenePointer scene, KillAvatarReason reason); - bool isFading() const { return _isFading; } - void updateFadingStatus(); + void fadeOut(render::Transaction& transaction, KillAvatarReason reason); // JSDoc is in AvatarData.h. Q_INVOKABLE virtual float getEyeHeight() const override; @@ -727,7 +725,6 @@ protected: bool _initialized { false }; bool _isAnimatingScale { false }; bool _mustFadeIn { false }; - bool _isFading { false }; bool _reconstructSoftEntitiesJointMap { false }; float _modelScale { 1.0f }; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index caa1f9f892..1c4b0cfc53 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1707,6 +1707,7 @@ protected: glm::vec3 _globalBoundingBoxOffset; AABox _defaultBubbleBox; + AABox _fitBoundingBox; mutable ReadWriteLockable _avatarEntitiesLock; AvatarEntityIDs _avatarEntityRemoved; // recently removed AvatarEntity ids diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index c16d65506a..3abd352778 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -439,7 +439,6 @@ void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason remo } auto removedAvatar = _avatarHash.take(sessionUUID); - if (removedAvatar) { removedAvatars.push_back(removedAvatar); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c235460404..6cfff7bc41 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -221,17 +221,15 @@ void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { // remove all entities from the scene auto scene = _viewState->getMain3DScene(); if (scene) { - render::Transaction transaction; for (const auto& entry : _entitiesInScene) { const auto& renderer = entry.second; const EntityItemPointer& entityItem = renderer->getEntity(); if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) { - renderer->removeFromScene(scene, transaction); + fadeOutRenderable(renderer); } else { savedEntities[entry.first] = entry.second; } } - scene->enqueueTransaction(transaction); } _renderablesToUpdate = savedEntities; @@ -258,12 +256,10 @@ void EntityTreeRenderer::clear() { // remove all entities from the scene auto scene = _viewState->getMain3DScene(); if (scene) { - render::Transaction transaction; for (const auto& entry : _entitiesInScene) { const auto& renderer = entry.second; - renderer->removeFromScene(scene, transaction); + fadeOutRenderable(renderer); } - scene->enqueueTransaction(transaction); } else { qCWarning(entitiesrenderer) << "EntitityTreeRenderer::clear(), Unexpected null scene, possibly during application shutdown"; } @@ -1016,10 +1012,7 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { forceRecheckEntities(); // reset our state to force checking our inside/outsideness of entities - // here's where we remove the entity payload from the scene - render::Transaction transaction; - renderable->removeFromScene(scene, transaction); - scene->enqueueTransaction(transaction); + fadeOutRenderable(renderable); } void EntityTreeRenderer::addingEntity(const EntityItemID& entityID) { @@ -1057,13 +1050,26 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool } } +void EntityTreeRenderer::fadeOutRenderable(const EntityRendererPointer& renderable) { + render::Transaction transaction; + auto scene = _viewState->getMain3DScene(); + + transaction.transitionFinishedOperator(renderable->getRenderItemID(), [scene, renderable]() { + render::Transaction transaction; + renderable->removeFromScene(scene, transaction); + scene->enqueueTransaction(transaction); + }); + + scene->enqueueTransaction(transaction); +} + void EntityTreeRenderer::playEntityCollisionSound(const EntityItemPointer& entity, const Collision& collision) { assert((bool)entity); auto renderable = renderableForEntity(entity); - if (!renderable) { - return; + if (!renderable) { + return; } - + SharedSoundPointer collisionSound = renderable->getCollisionSound(); if (!collisionSound) { return; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a511d73210..cee91ad1c7 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -93,6 +93,8 @@ public: /// reloads the entity scripts, calling unload and preload void reloadEntityScripts(); + void fadeOutRenderable(const EntityRendererPointer& renderable); + // event handles which may generate entity related events QUuid mousePressEvent(QMouseEvent* event); void mouseReleaseEvent(QMouseEvent* event); @@ -255,6 +257,7 @@ private: std::unordered_map _renderablesToUpdate; std::unordered_map _entitiesInScene; std::unordered_map _entitiesToAdd; + // For Scene.shouldRenderEntities QList _entityIDsLastInScene; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index a6826da91b..3a56521702 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -148,7 +148,7 @@ EntityRenderer::EntityRenderer(const EntityItemPointer& entity) : _created(entit }); } -EntityRenderer::~EntityRenderer() { } +EntityRenderer::~EntityRenderer() {} // // Smart payload proxy members, implementing the payload interface @@ -421,6 +421,7 @@ void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transa if (fading) { _isFading = Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } + _prevIsTransparent = transparent; updateModelTransformAndBound(); @@ -493,4 +494,4 @@ glm::vec4 EntityRenderer::calculatePulseColor(const glm::vec4& color, const Puls } return result; -} \ No newline at end of file +} diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index ca914731b5..f8c45b792a 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()) { - QMetaObject::invokeMethod(this, "addingWearable", Q_ARG(QUuid, entity->getEntityItemID())); + QMetaObject::invokeMethod(this, "addingWearable", Q_ARG(EntityItemID, entity->getEntityItemID())); } } void EntityScriptingInterface::onDeletingEntity(EntityItem* entity) { if (entity->isWearable()) { - QMetaObject::invokeMethod(this, "deletingWearable", Q_ARG(QUuid, entity->getEntityItemID())); + QMetaObject::invokeMethod(this, "deletingWearable", Q_ARG(EntityItemID, entity->getEntityItemID())); } } diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index c63170de75..2f47ef5e00 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -71,12 +71,12 @@ void UserActivityLoggerScriptingInterface::makeUserConnection(QString otherID, b doLogAction("makeUserConnection", payload); } -void UserActivityLoggerScriptingInterface::bubbleToggled(bool newValue) { - doLogAction(newValue ? "bubbleOn" : "bubbleOff"); +void UserActivityLoggerScriptingInterface::privacyShieldToggled(bool newValue) { + doLogAction(newValue ? "privacyShieldOn" : "privacyShieldOff"); } -void UserActivityLoggerScriptingInterface::bubbleActivated() { - doLogAction("bubbleActivated"); +void UserActivityLoggerScriptingInterface::privacyShieldActivated() { + doLogAction("privacyShieldActivated"); } void UserActivityLoggerScriptingInterface::logAction(QString action, QVariantMap details) { diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index 71d411056d..1cda1235e9 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -30,8 +30,8 @@ public: Q_INVOKABLE void palAction(QString action, QString target); Q_INVOKABLE void palOpened(float secondsOpen); Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details = ""); - Q_INVOKABLE void bubbleToggled(bool newValue); - Q_INVOKABLE void bubbleActivated(); + Q_INVOKABLE void privacyShieldToggled(bool newValue); + Q_INVOKABLE void privacyShieldActivated(); Q_INVOKABLE void logAction(QString action, QVariantMap details = QVariantMap{}); Q_INVOKABLE void commercePurchaseSuccess(QString marketplaceID, QString contentCreator, int cost, bool firstPurchaseOfThisItem); Q_INVOKABLE void commercePurchaseFailure(QString marketplaceID, QString contentCreator, int cost, bool firstPurchaseOfThisItem, QString errorDetails); 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, diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index 1850261c99..16d1d49d9f 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -47,6 +47,10 @@ void Transaction::queryTransitionOnItem(ItemID id, TransitionQueryFunc func) { _queriedTransitions.emplace_back(id, func); } +void Transaction::transitionFinishedOperator(ItemID id, TransitionFinishedFunc func) { + _transitionFinishedOperators.emplace_back(id, func); +} + void Transaction::updateItem(ItemID id, const UpdateFunctorPointer& functor) { _updatedItems.emplace_back(id, functor); } @@ -75,6 +79,7 @@ void Transaction::reserve(const std::vector& transactionContainer) size_t addedTransitionsCount = 0; size_t queriedTransitionsCount = 0; size_t reAppliedTransitionsCount = 0; + size_t transitionFinishedOperatorsCount = 0; size_t highlightResetsCount = 0; size_t highlightRemovesCount = 0; size_t highlightQueriesCount = 0; @@ -85,6 +90,7 @@ void Transaction::reserve(const std::vector& transactionContainer) updatedItemsCount += transaction._updatedItems.size(); resetSelectionsCount += transaction._resetSelections.size(); addedTransitionsCount += transaction._addedTransitions.size(); + transitionFinishedOperatorsCount += transaction._transitionFinishedOperators.size(); queriedTransitionsCount += transaction._queriedTransitions.size(); reAppliedTransitionsCount += transaction._reAppliedTransitions.size(); highlightResetsCount += transaction._highlightResets.size(); @@ -99,6 +105,7 @@ void Transaction::reserve(const std::vector& transactionContainer) _addedTransitions.reserve(addedTransitionsCount); _queriedTransitions.reserve(queriedTransitionsCount); _reAppliedTransitions.reserve(reAppliedTransitionsCount); + _transitionFinishedOperators.reserve(transitionFinishedOperatorsCount); _highlightResets.reserve(highlightResetsCount); _highlightRemoves.reserve(highlightRemovesCount); _highlightQueries.reserve(highlightQueriesCount); @@ -142,6 +149,7 @@ void Transaction::merge(Transaction&& transaction) { moveElements(_resetSelections, transaction._resetSelections); moveElements(_addedTransitions, transaction._addedTransitions); moveElements(_queriedTransitions, transaction._queriedTransitions); + moveElements(_transitionFinishedOperators, transaction._transitionFinishedOperators); moveElements(_reAppliedTransitions, transaction._reAppliedTransitions); moveElements(_highlightResets, transaction._highlightResets); moveElements(_highlightRemoves, transaction._highlightRemoves); @@ -156,6 +164,7 @@ void Transaction::merge(const Transaction& transaction) { copyElements(_addedTransitions, transaction._addedTransitions); copyElements(_queriedTransitions, transaction._queriedTransitions); copyElements(_reAppliedTransitions, transaction._reAppliedTransitions); + copyElements(_transitionFinishedOperators, transaction._transitionFinishedOperators); copyElements(_highlightResets, transaction._highlightResets); copyElements(_highlightRemoves, transaction._highlightRemoves); copyElements(_highlightQueries, transaction._highlightQueries); @@ -168,6 +177,7 @@ void Transaction::clear() { _resetSelections.clear(); _addedTransitions.clear(); _queriedTransitions.clear(); + _transitionFinishedOperators.clear(); _reAppliedTransitions.clear(); _highlightResets.clear(); _highlightRemoves.clear(); @@ -271,6 +281,7 @@ void Scene::processTransactionFrame(const Transaction& transaction) { transitionItems(transaction._addedTransitions); reApplyTransitions(transaction._reAppliedTransitions); queryTransitionItems(transaction._queriedTransitions); + resetTransitionFinishedOperator(transaction._transitionFinishedOperators); // Update the numItemsAtomic counter AFTER the pending changes went through _numAllocatedItems.exchange(maxID); @@ -394,7 +405,7 @@ void Scene::transitionItems(const Transaction::TransitionAdds& transactions) { // Only remove if: // transitioning to something other than none or we're transitioning to none from ELEMENT_LEAVE_DOMAIN or USER_LEAVE_DOMAIN const auto& oldTransitionType = transitionStage->getTransition(transitionId).eventType; - if (transitionType != Transition::NONE || !(oldTransitionType == Transition::ELEMENT_LEAVE_DOMAIN || oldTransitionType == Transition::USER_LEAVE_DOMAIN)) { + if (transitionType != oldTransitionType) { resetItemTransition(itemId); } } @@ -440,6 +451,23 @@ void Scene::queryTransitionItems(const Transaction::TransitionQueries& transacti } } +void Scene::resetTransitionFinishedOperator(const Transaction::TransitionFinishedOperators& operators) { + for (auto& finishedOperator : operators) { + auto itemId = std::get<0>(finishedOperator); + const auto& item = _items[itemId]; + auto func = std::get<1>(finishedOperator); + + if (item.exist() && func != nullptr) { + TransitionStage::Index transitionId = item.getTransitionId(); + if (!TransitionStage::isIndexInvalid(transitionId)) { + _transitionFinishedOperatorMap[transitionId].emplace_back(func); + } else if (func) { + func(); + } + } + } +} + void Scene::resetHighlights(const Transaction::HighlightResets& transactions) { auto outlineStage = getStage(HighlightStage::getName()); if (outlineStage) { @@ -526,9 +554,18 @@ void Scene::setItemTransition(ItemID itemId, Index transitionId) { void Scene::resetItemTransition(ItemID itemId) { auto& item = _items[itemId]; - if (!render::TransitionStage::isIndexInvalid(item.getTransitionId())) { + TransitionStage::Index transitionId = item.getTransitionId(); + if (!render::TransitionStage::isIndexInvalid(transitionId)) { auto transitionStage = getStage(TransitionStage::getName()); - transitionStage->removeTransition(item.getTransitionId()); + + auto finishedOperators = _transitionFinishedOperatorMap[transitionId]; + for (auto finishedOperator : finishedOperators) { + if (finishedOperator) { + finishedOperator(); + } + } + _transitionFinishedOperatorMap.erase(transitionId); + transitionStage->removeTransition(transitionId); setItemTransition(itemId, render::TransitionStage::INVALID_INDEX); } } @@ -587,4 +624,4 @@ void Scene::resetStage(const Stage::Name& name, const StagePointer& stage) { } else { (*found).second = stage; } -} \ No newline at end of file +} diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index f00c74775d..08fbf33bcd 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -32,12 +32,15 @@ class Scene; // These changes must be expressed through the corresponding command from the Transaction // THe Transaction is then queued on the Scene so all the pending transactions can be consolidated and processed at the time // of updating the scene before it s rendered. -// +// + + class Transaction { friend class Scene; public: typedef std::function TransitionQueryFunc; + typedef std::function TransitionFinishedFunc; typedef std::function SelectionHighlightQueryFunc; Transaction() {} @@ -52,6 +55,7 @@ public: void removeTransitionFromItem(ItemID id); void reApplyTransitionToItem(ItemID id); void queryTransitionOnItem(ItemID id, TransitionQueryFunc func); + void transitionFinishedOperator(ItemID id, TransitionFinishedFunc func); template void updateItem(ItemID id, std::function func) { updateItem(id, std::make_shared>(func)); @@ -84,6 +88,7 @@ protected: using Update = std::tuple; using TransitionAdd = std::tuple; using TransitionQuery = std::tuple; + using TransitionFinishedOperator = std::tuple; using TransitionReApply = ItemID; using SelectionReset = Selection; using HighlightReset = std::tuple; @@ -95,6 +100,7 @@ protected: using Updates = std::vector; using TransitionAdds = std::vector; using TransitionQueries = std::vector; + using TransitionFinishedOperators = std::vector; using TransitionReApplies = std::vector; using SelectionResets = std::vector; using HighlightResets = std::vector; @@ -107,6 +113,7 @@ protected: TransitionAdds _addedTransitions; TransitionQueries _queriedTransitions; TransitionReApplies _reAppliedTransitions; + TransitionFinishedOperators _transitionFinishedOperators; SelectionResets _resetSelections; HighlightResets _highlightResets; HighlightRemoves _highlightRemoves; @@ -208,6 +215,7 @@ protected: ItemIDSet _masterNonspatialSet; void resetItems(const Transaction::Resets& transactions); + void resetTransitionFinishedOperator(const Transaction::TransitionFinishedOperators& transactions); void removeItems(const Transaction::Removes& transactions); void updateItems(const Transaction::Updates& transactions); void transitionItems(const Transaction::TransitionAdds& transactions); @@ -223,6 +231,8 @@ protected: mutable std::mutex _selectionsMutex; // mutable so it can be used in the thread safe getSelection const method SelectionMap _selections; + std::unordered_map> _transitionFinishedOperatorMap; + void resetSelections(const Transaction::SelectionResets& transactions); // More actions coming to selections soon: // void removeFromSelection(const Selection& selection); diff --git a/libraries/render/src/render/Transition.h b/libraries/render/src/render/Transition.h index 30bda8aa2a..eca41e9d6c 100644 --- a/libraries/render/src/render/Transition.h +++ b/libraries/render/src/render/Transition.h @@ -50,4 +50,4 @@ namespace render { typedef std::vector TransitionTypes; } -#endif // hifi_render_Transition_h \ No newline at end of file +#endif // hifi_render_Transition_h diff --git a/scripts/developer/createAvatarInputsBarEntity.js b/scripts/developer/createAvatarInputsBarEntity.js new file mode 100644 index 0000000000..deb0cfdd89 --- /dev/null +++ b/scripts/developer/createAvatarInputsBarEntity.js @@ -0,0 +1,138 @@ +"use strict"; + +(function(){ + var AppUi = Script.require("appUi"); + + var ui; + + var onCreateAvatarInputsBarEntity = false; + var micBarEntity = null; + var bubbleIconEntity = null; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var AVATAR_INPUTS_EDIT_QML_SOURCE = "hifi/EditAvatarInputsBar.qml"; + + // DPI + var ENTITY_DPI = 60.0; + // QML NATURAL DIMENSIONS + var MIC_BAR_DIMENSIONS = Vec3.multiply(30.0 / ENTITY_DPI, {x: 0.036, y: 0.048, z: 0.3}); + var BUBBLE_ICON_DIMENSIONS = Vec3.multiply(30.0 / ENTITY_DPI, {x: 0.036, y: 0.036, z: 0.3}); + // ENTITY NAMES + var MIC_BAR_NAME = "AvatarInputsMicBarEntity"; + var BUBBLE_ICON_NAME = "AvatarInputsBubbleIconEntity"; + // CONSTANTS + var LOCAL_POSITION_X_OFFSET = -0.2; + var LOCAL_POSITION_Y_OFFSET = -0.125; + var LOCAL_POSITION_Z_OFFSET = -0.5; + + function fromQml(message) { + if (message.method === "reposition") { + var micBarLocalPosition = Entities.getEntityProperties(micBarEntity).localPosition; + var bubbleIconLocalPosition = Entities.getEntityProperties(bubbleIconEntity).localPosition; + var newMicBarLocalPosition, newBubbleIconLocalPosition; + if (message.x !== undefined) { + newMicBarLocalPosition = { x: -((MIC_BAR_DIMENSIONS.x) / 2) + message.x, y: micBarLocalPosition.y, z: micBarLocalPosition.z }; + newBubbleIconLocalPosition = { x: ((MIC_BAR_DIMENSIONS.x) * 1.2 / 2) + message.x, y: bubbleIconLocalPosition.y, z: bubbleIconLocalPosition.z }; + } else if (message.y !== undefined) { + newMicBarLocalPosition = { x: micBarLocalPosition.x, y: message.y, z: micBarLocalPosition.z }; + newBubbleIconLocalPosition = { x: bubbleIconLocalPosition.x, y: ((MIC_BAR_DIMENSIONS.y - BUBBLE_ICON_DIMENSIONS.y) / 2 + message.y), z: bubbleIconLocalPosition.z }; + } else if (message.z !== undefined) { + newMicBarLocalPosition = { x: micBarLocalPosition.x, y: micBarLocalPosition.y, z: message.z }; + newBubbleIconLocalPosition = { x: bubbleIconLocalPosition.x, y: bubbleIconLocalPosition.y, z: message.z }; + } + var micBarProps = { + localPosition: newMicBarLocalPosition + }; + var bubbleIconProps = { + localPosition: newBubbleIconLocalPosition + }; + + Entities.editEntity(micBarEntity, micBarProps); + Entities.editEntity(bubbleIconEntity, bubbleIconProps); + } else if (message.method === "setVisible") { + if (message.visible !== undefined) { + var props = { + visible: message.visible + }; + Entities.editEntity(micBarEntity, props); + Entities.editEntity(bubbleIconEntity, props); + } + } else if (message.method === "print") { + // prints the local position into the hifi log. + var micBarLocalPosition = Entities.getEntityProperties(micBarEntity).localPosition; + var bubbleIconLocalPosition = Entities.getEntityProperties(bubbleIconEntity).localPosition; + console.log("mic bar local position is at " + JSON.stringify(micBarLocalPosition)); + console.log("bubble icon local position is at " + JSON.stringify(bubbleIconLocalPosition)); + } + }; + + function createEntities() { + if (micBarEntity != null && bubbleIconEntity != null) { + return; + } + // POSITIONS + var micBarLocalPosition = {x: (-(MIC_BAR_DIMENSIONS.x / 2)) + LOCAL_POSITION_X_OFFSET, y: LOCAL_POSITION_Y_OFFSET, z: LOCAL_POSITION_Z_OFFSET}; + var bubbleIconLocalPosition = {x: (MIC_BAR_DIMENSIONS.x * 1.2 / 2) + LOCAL_POSITION_X_OFFSET, y: ((MIC_BAR_DIMENSIONS.y - BUBBLE_ICON_DIMENSIONS.y) / 2 + LOCAL_POSITION_Y_OFFSET), z: LOCAL_POSITION_Z_OFFSET}; + var props = { + type: "Web", + name: MIC_BAR_NAME, + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + localPosition: micBarLocalPosition, + localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, micBarLocalPosition)), + sourceUrl: Script.resourcesPath() + "qml/hifi/audio/MicBarApplication.qml", + // cutoff alpha for detecting transparency + alpha: 0.98, + dimensions: MIC_BAR_DIMENSIONS, + dpi: ENTITY_DPI, + drawInFront: true, + userData: { + grabbable: false + }, + }; + micBarEntity = Entities.addEntity(props, "local"); + var props = { + type: "Web", + name: BUBBLE_ICON_NAME, + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + localPosition: bubbleIconLocalPosition, + localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, bubbleIconLocalPosition)), + sourceUrl: Script.resourcesPath() + "qml/BubbleIcon.qml", + // cutoff alpha for detecting transparency + alpha: 0.98, + dimensions: BUBBLE_ICON_DIMENSIONS, + dpi: ENTITY_DPI, + drawInFront: true, + userData: { + grabbable: false + }, + }; + bubbleIconEntity = Entities.addEntity(props, "local"); + tablet.loadQMLSource(AVATAR_INPUTS_EDIT_QML_SOURCE); + }; + function cleanup() { + if (micBarEntity) { + Entities.deleteEntity(micBarEntity); + } + if (bubbleIconEntity) { + Entities.deleteEntity(bubbleIconEntity); + } + }; + + function setup() { + ui = new AppUi({ + buttonName: "AVBAR", + home: Script.resourcesPath() + "qml/hifi/EditAvatarInputsBar.qml", + onMessage: fromQml, + onOpened: createEntities, + // onClosed: cleanup, + normalButton: "icons/tablet-icons/edit-i.svg", + activeButton: "icons/tablet-icons/edit-a.svg", + }); + }; + + setup(); + + Script.scriptEnding.connect(cleanup); + +}()); diff --git a/scripts/system/audio.js b/scripts/system/audio.js index 19ed3faef2..a161b40ffd 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -75,6 +75,7 @@ button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); Audio.mutedChanged.connect(onMuteToggled); Audio.pushToTalkChanged.connect(onMuteToggled); +HMD.displayModeChanged.connect(onMuteToggled); Script.scriptEnding.connect(function () { if (onAudioScreen) { @@ -84,6 +85,7 @@ Script.scriptEnding.connect(function () { tablet.screenChanged.disconnect(onScreenChanged); Audio.mutedChanged.disconnect(onMuteToggled); Audio.pushToTalkChanged.disconnect(onMuteToggled); + HMD.displayModeChanged.disconnect(onMuteToggled); tablet.removeButton(button); }); diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index e715e97575..9acc5ab123 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -43,7 +43,7 @@ if (HMD.active) { warningOverlayID = Overlays.addOverlay("text3d", { name: "Muted-Warning", - localPosition: { x: 0.0, y: -0.5, z: -1.0 }, + localPosition: { x: 0.0, y: -0.45, z: -1.0 }, localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), text: warningText, textAlpha: 1, @@ -58,20 +58,6 @@ parentID: MyAvatar.SELF_ID, parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX") }); - } else { - var textDimensions = { x: 100, y: 50 }; - warningOverlayID = Overlays.addOverlay("text", { - name: "Muted-Warning", - font: { size: 36 }, - text: warningText, - x: (Window.innerWidth - textDimensions.x) / 2, - y: (Window.innerHeight - textDimensions.y), - width: textDimensions.x, - height: textDimensions.y, - textColor: { red: 226, green: 51, blue: 77 }, - backgroundAlpha: 0, - visible: true - }); } } @@ -141,4 +127,4 @@ Audio.mutedChanged.connect(startOrStopPoll); Audio.warnWhenMutedChanged.connect(startOrStopPoll); -}()); // END LOCAL_SCOPE \ No newline at end of file +}()); // END LOCAL_SCOPE diff --git a/scripts/system/away.js b/scripts/system/away.js index 2af43b2055..e45041139a 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -171,7 +171,7 @@ function goAway(fromStartup) { if (!previousBubbleState) { Users.toggleIgnoreRadius(); } - UserActivityLogger.bubbleToggled(Users.getIgnoreRadiusEnabled()); + UserActivityLogger.privacyShieldToggled(Users.getIgnoreRadiusEnabled()); UserActivityLogger.toggledAway(true); MyAvatar.isAway = true; } @@ -186,7 +186,7 @@ function goActive() { if (Users.getIgnoreRadiusEnabled() !== previousBubbleState) { Users.toggleIgnoreRadius(); - UserActivityLogger.bubbleToggled(Users.getIgnoreRadiusEnabled()); + UserActivityLogger.privacyShieldToggled(Users.getIgnoreRadiusEnabled()); } if (!Window.hasFocus()) { diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index 6ca624872e..eca3b3dcd4 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -90,7 +90,7 @@ // Called from the C++ scripting interface to show the bubble overlay function enteredIgnoreRadius() { createOverlays(); - UserActivityLogger.bubbleActivated(); + UserActivityLogger.privacyShieldActivated(); } // Used to set the state of the bubble HUD button @@ -160,7 +160,7 @@ function onBubbleToggled(enabled, doNotLog) { writeButtonProperties(enabled); if (doNotLog !== true) { - UserActivityLogger.bubbleToggled(enabled); + UserActivityLogger.privacyShieldToggled(enabled); } if (enabled) { createOverlays(); @@ -174,7 +174,7 @@ } // Setup the bubble button - var buttonName = "BUBBLE"; + var buttonName = "SHIELD"; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/bubble-i.svg", diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index 4e06772f4b..1070449080 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -17,9 +17,10 @@ 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.4.0"; + static readonly string AVATAR_EXPORTER_VERSION = "0.4.1"; - static readonly float HIPS_GROUND_MIN_Y = 0.01f; + static readonly float HIPS_MIN_Y_PERCENT_OF_HEIGHT = 0.03f; + static readonly float BELOW_GROUND_THRESHOLD_PERCENT_OF_HEIGHT = -0.15f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; static readonly int MAXIMUM_USER_BONE_COUNT = 256; static readonly string EMPTY_WARNING_TEXT = "None"; @@ -231,7 +232,8 @@ class AvatarExporter : MonoBehaviour { HeadMapped, HeadDescendantOfChest, EyesMapped, - HipsNotOnGround, + HipsNotAtBottom, + ExtentsNotBelowGround, HipsSpineChestNotCoincident, TotalBoneCountUnderLimit, AvatarRuleEnd, @@ -247,18 +249,26 @@ class AvatarExporter : MonoBehaviour { class UserBoneInformation { public string humanName; // bone name in Humanoid if it is mapped, otherwise "" public string parentName; // parent user bone name + public BoneTreeNode boneTreeNode; // node within the user bone tree public int mappingCount; // number of times this bone is mapped in Humanoid public Vector3 position; // absolute position public Quaternion rotation; // absolute rotation - public BoneTreeNode boneTreeNode; public UserBoneInformation() { humanName = ""; parentName = ""; + boneTreeNode = new BoneTreeNode(); mappingCount = 0; position = new Vector3(); rotation = new Quaternion(); - boneTreeNode = new BoneTreeNode(); + } + public UserBoneInformation(string parent, BoneTreeNode treeNode, Vector3 pos) { + humanName = ""; + parentName = parent; + boneTreeNode = treeNode; + mappingCount = 0; + position = pos; + rotation = new Quaternion(); } public bool HasHumanMapping() { return !string.IsNullOrEmpty(humanName); } @@ -266,11 +276,13 @@ class AvatarExporter : MonoBehaviour { class BoneTreeNode { public string boneName; + public string parentName; public List children = new List(); public BoneTreeNode() {} - public BoneTreeNode(string name) { + public BoneTreeNode(string name, string parent) { boneName = name; + parentName = parent; } } @@ -732,9 +744,11 @@ class AvatarExporter : MonoBehaviour { // instantiate a game object of the user avatar to traverse the bone tree to gather // bone parents and positions as well as build a bone tree, then destroy it - GameObject assetGameObject = (GameObject)Instantiate(avatarResource); - TraverseUserBoneTree(assetGameObject.transform); - DestroyImmediate(assetGameObject); + GameObject avatarGameObject = (GameObject)Instantiate(avatarResource, Vector3.zero, Quaternion.identity); + TraverseUserBoneTree(avatarGameObject.transform, userBoneTree); + Bounds bounds = AvatarUtilities.GetAvatarBounds(avatarGameObject); + float height = AvatarUtilities.GetAvatarHeight(avatarGameObject); + DestroyImmediate(avatarGameObject); // iterate over Humanoid bones and update user bone info to increase human mapping counts for each bone // as well as set their Humanoid name and build a Humanoid to user bone mapping @@ -753,10 +767,10 @@ class AvatarExporter : MonoBehaviour { } // generate the list of avatar rule failure strings for any avatar rules that are not satisfied by this avatar - SetFailedAvatarRules(); + SetFailedAvatarRules(bounds, height); } - static void TraverseUserBoneTree(Transform modelBone) { + static void TraverseUserBoneTree(Transform modelBone, BoneTreeNode boneTreeNode) { GameObject gameObject = modelBone.gameObject; // check if this transform is a node containing mesh, light, or camera instead of a bone @@ -770,33 +784,52 @@ class AvatarExporter : MonoBehaviour { if (mesh) { Material[] materials = skinnedMeshRenderer != null ? skinnedMeshRenderer.sharedMaterials : meshRenderer.sharedMaterials; StoreMaterialData(materials); + + // ensure branches within the transform hierarchy that contain meshes are removed from the user bone tree + Transform ancestorBone = modelBone; + string previousBoneName = ""; + // find the name of the root child bone that this mesh is underneath + while (ancestorBone != null) { + if (ancestorBone.parent == null) { + break; + } + previousBoneName = ancestorBone.name; + ancestorBone = ancestorBone.parent; + } + // remove the bone tree node from root's children for the root child bone that has mesh children + if (!string.IsNullOrEmpty(previousBoneName)) { + foreach (BoneTreeNode rootChild in userBoneTree.children) { + if (rootChild.boneName == previousBoneName) { + userBoneTree.children.Remove(rootChild); + break; + } + } + } } else if (!light && !camera) { // if it is in fact a bone, add it to the bone tree as well as user bone infos list with position and parent name - UserBoneInformation userBoneInfo = new UserBoneInformation(); - userBoneInfo.position = modelBone.position; // bone's absolute position - string boneName = modelBone.name; if (modelBone.parent == null) { // if no parent then this is actual root bone node of the user avatar, so consider it's parent as "root" boneName = GetRootBoneName(); // ensure we use the root bone name from the skeleton list for consistency - userBoneTree = new BoneTreeNode(boneName); // initialize root of tree - userBoneInfo.parentName = "root"; - userBoneInfo.boneTreeNode = userBoneTree; + boneTreeNode.boneName = boneName; + boneTreeNode.parentName = "root"; } else { // otherwise add this bone node as a child to it's parent's children list // if its a child of the root bone, use the root bone name from the skeleton list as the parent for consistency string parentName = modelBone.parent.parent == null ? GetRootBoneName() : modelBone.parent.name; - BoneTreeNode boneTreeNode = new BoneTreeNode(boneName); - userBoneInfos[parentName].boneTreeNode.children.Add(boneTreeNode); - userBoneInfo.parentName = parentName; + BoneTreeNode node = new BoneTreeNode(boneName, parentName); + boneTreeNode.children.Add(node); + boneTreeNode = node; } + Vector3 bonePosition = modelBone.position; // bone's absolute position in avatar space + UserBoneInformation userBoneInfo = new UserBoneInformation(boneTreeNode.parentName, boneTreeNode, bonePosition); userBoneInfos.Add(boneName, userBoneInfo); } // recurse over transform node's children for (int i = 0; i < modelBone.childCount; ++i) { - TraverseUserBoneTree(modelBone.GetChild(i)); + TraverseUserBoneTree(modelBone.GetChild(i), boneTreeNode); } } @@ -840,7 +873,7 @@ class AvatarExporter : MonoBehaviour { return ""; } - static void SetFailedAvatarRules() { + static void SetFailedAvatarRules(Bounds avatarBounds, float avatarHeight) { failedAvatarRules.Clear(); string hipsUserBone = ""; @@ -905,18 +938,29 @@ class AvatarExporter : MonoBehaviour { break; case AvatarRule.ChestMapped: if (!humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) { - // check to see if there is a child of Spine that we can suggest to be mapped to Chest - string spineChild = ""; + // check to see if there is an unmapped child of Spine that we can suggest to be mapped to Chest + string chestMappingCandidate = ""; if (!string.IsNullOrEmpty(spineUserBone)) { BoneTreeNode spineTreeNode = userBoneInfos[spineUserBone].boneTreeNode; - if (spineTreeNode.children.Count == 1) { - spineChild = spineTreeNode.children[0].boneName; + foreach (BoneTreeNode spineChildTreeNode in spineTreeNode.children) { + string spineChildBone = spineChildTreeNode.boneName; + if (userBoneInfos[spineChildBone].HasHumanMapping()) { + continue; + } + // a suitable candidate for Chest should have Neck/Head or Shoulder mappings in its descendants + if (IsHumanBoneInHierarchy(spineChildTreeNode, "Neck") || + IsHumanBoneInHierarchy(spineChildTreeNode, "Head") || + IsHumanBoneInHierarchy(spineChildTreeNode, "LeftShoulder") || + IsHumanBoneInHierarchy(spineChildTreeNode, "RightShoulder")) { + chestMappingCandidate = spineChildBone; + break; + } } } failedAvatarRules.Add(avatarRule, "There is no Chest bone mapped in Humanoid for the selected avatar."); // if the only found child of Spine is not yet mapped then add it as a suggestion for Chest mapping - if (!string.IsNullOrEmpty(spineChild) && !userBoneInfos[spineChild].HasHumanMapping()) { - failedAvatarRules[avatarRule] += " It is suggested that you map bone " + spineChild + + if (!string.IsNullOrEmpty(chestMappingCandidate)) { + failedAvatarRules[avatarRule] += " It is suggested that you map bone " + chestMappingCandidate + " to Chest in Humanoid."; } } @@ -949,15 +993,34 @@ class AvatarExporter : MonoBehaviour { } } break; - case AvatarRule.HipsNotOnGround: - // ensure the absolute Y position for the bone mapped to Hips (if its mapped) is at least HIPS_GROUND_MIN_Y + case AvatarRule.HipsNotAtBottom: + // ensure that Hips is not below a proportional percentage of the avatar's height in avatar space if (!string.IsNullOrEmpty(hipsUserBone)) { UserBoneInformation hipsBoneInfo = userBoneInfos[hipsUserBone]; hipsPosition = hipsBoneInfo.position; - if (hipsPosition.y < HIPS_GROUND_MIN_Y) { - failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + - ") should not be at ground level."); + + // find the lowest y position of the bones + float minBoneYPosition = float.MaxValue; + foreach (var userBoneInfo in userBoneInfos) { + Vector3 position = userBoneInfo.Value.position; + if (position.y < minBoneYPosition) { + minBoneYPosition = position.y; + } } + + // check that Hips is within a percentage of avatar's height from the lowest Y point of the avatar + float bottomYRange = HIPS_MIN_Y_PERCENT_OF_HEIGHT * avatarHeight; + if (Mathf.Abs(hipsPosition.y - minBoneYPosition) < bottomYRange) { + failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + + ") should not be at the bottom of the selected avatar."); + } + } + break; + case AvatarRule.ExtentsNotBelowGround: + // ensure the minimum Y extent of the model's bounds is not below a proportional threshold of avatar's height + float belowGroundThreshold = BELOW_GROUND_THRESHOLD_PERCENT_OF_HEIGHT * avatarHeight; + if (avatarBounds.min.y < belowGroundThreshold) { + failedAvatarRules.Add(avatarRule, "The bottom extents of the selected avatar go below ground level."); } break; case AvatarRule.HipsSpineChestNotCoincident: @@ -989,6 +1052,23 @@ class AvatarExporter : MonoBehaviour { } } + static bool IsHumanBoneInHierarchy(BoneTreeNode boneTreeNode, string humanBoneName) { + UserBoneInformation userBoneInfo; + if (userBoneInfos.TryGetValue(boneTreeNode.boneName, out userBoneInfo) && userBoneInfo.humanName == humanBoneName) { + // this bone matches the human bone name being searched for + return true; + } + + // recursively check downward through children bones for target human bone + foreach (BoneTreeNode childNode in boneTreeNode.children) { + if (IsHumanBoneInHierarchy(childNode, humanBoneName)) { + return true; + } + } + + return false; + } + static string CheckHumanBoneMappingRule(AvatarRule avatarRule, string humanBoneName) { string userBoneName = ""; // avatar rule fails if bone is not mapped in Humanoid @@ -999,8 +1079,8 @@ class AvatarExporter : MonoBehaviour { return userBoneName; } - static void CheckUserBoneDescendantOfHumanRule(AvatarRule avatarRule, string userBoneName, string descendantOfHumanName) { - if (string.IsNullOrEmpty(userBoneName)) { + static void CheckUserBoneDescendantOfHumanRule(AvatarRule avatarRule, string descendantUserBoneName, string descendantOfHumanName) { + if (string.IsNullOrEmpty(descendantUserBoneName)) { return; } @@ -1009,27 +1089,26 @@ class AvatarExporter : MonoBehaviour { return; } - string userBone = userBoneName; - string ancestorUserBone = ""; - UserBoneInformation userBoneInfo = new UserBoneInformation(); + string userBoneName = descendantUserBoneName; + UserBoneInformation userBoneInfo = userBoneInfos[userBoneName]; + string descendantHumanName = userBoneInfo.humanName; // iterate upward from user bone through user bone info parent names until root // is reached or the ancestor bone name matches the target descendant of name - while (ancestorUserBone != "root") { - if (userBoneInfos.TryGetValue(userBone, out userBoneInfo)) { - ancestorUserBone = userBoneInfo.parentName; - if (ancestorUserBone == descendantOfUserBoneName) { - return; - } - userBone = ancestorUserBone; + while (userBoneName != "root") { + if (userBoneName == descendantOfUserBoneName) { + return; + } + if (userBoneInfos.TryGetValue(userBoneName, out userBoneInfo)) { + userBoneName = userBoneInfo.parentName; } else { break; } } // avatar rule fails if no ancestor of given user bone matched the descendant of name (no early return) - failedAvatarRules.Add(avatarRule, "The bone mapped to " + userBoneInfo.humanName + " in Humanoid (" + userBoneName + - ") is not a child of the bone mapped to " + descendantOfHumanName + " in Humanoid (" + - descendantOfUserBoneName + ")."); + failedAvatarRules.Add(avatarRule, "The bone mapped to " + descendantHumanName + " in Humanoid (" + + descendantUserBoneName + ") is not a descendant of the bone mapped to " + + descendantOfHumanName + " in Humanoid (" + descendantOfUserBoneName + ")."); } static void CheckAsymmetricalMappingRule(AvatarRule avatarRule, string[] mappingSuffixes, string appendage) { @@ -1296,9 +1375,8 @@ class ExportProjectWindow : EditorWindow { const float MAX_SCALE_SLIDER = 2.0f; const int SLIDER_SCALE_EXPONENT = 10; const float ACTUAL_SCALE_OFFSET = 1.0f; - 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 MAXIMUM_RECOMMENDED_HEIGHT = AvatarUtilities.DEFAULT_AVATAR_HEIGHT * 1.5f; + const float MINIMUM_RECOMMENDED_HEIGHT = AvatarUtilities.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); @@ -1339,9 +1417,9 @@ class ExportProjectWindow : EditorWindow { ShowUtility(); // if the avatar's starting height is outside of the recommended ranges, auto-adjust the scale to default height - float height = GetAvatarHeight(); + float height = AvatarUtilities.GetAvatarHeight(avatarPreviewObject); if (height < MINIMUM_RECOMMENDED_HEIGHT || height > MAXIMUM_RECOMMENDED_HEIGHT) { - float newScale = DEFAULT_AVATAR_HEIGHT / height; + float newScale = AvatarUtilities.DEFAULT_AVATAR_HEIGHT / height; SetAvatarScale(newScale); scaleWarningText = "Avatar's scale automatically adjusted to be within the recommended range."; } @@ -1524,7 +1602,7 @@ class ExportProjectWindow : EditorWindow { void UpdateScaleWarning() { // called on any scale changes - float height = GetAvatarHeight(); + float height = AvatarUtilities.GetAvatarHeight(avatarPreviewObject); if (height < MINIMUM_RECOMMENDED_HEIGHT) { scaleWarningText = "The height of the avatar is below the recommended minimum."; } else if (height > MAXIMUM_RECOMMENDED_HEIGHT) { @@ -1535,23 +1613,6 @@ class ExportProjectWindow : EditorWindow { } } - float GetAvatarHeight() { - // height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers - if (avatarPreviewObject != null) { - Bounds bounds = new Bounds(); - var meshRenderers = avatarPreviewObject.GetComponentsInChildren(); - var skinnedMeshRenderers = avatarPreviewObject.GetComponentsInChildren(); - foreach (var renderer in meshRenderers) { - bounds.Encapsulate(renderer.bounds); - } - foreach (var renderer in skinnedMeshRenderers) { - bounds.Encapsulate(renderer.bounds); - } - return bounds.max.y; - } - return 0.0f; - } - void SetAvatarScale(float actualScale) { // set the new scale uniformly on the preview avatar's transform to show the resulting avatar size avatarPreviewObject.transform.localScale = new Vector3(actualScale, actualScale, actualScale); @@ -1571,3 +1632,28 @@ class ExportProjectWindow : EditorWindow { onCloseCallback(); } } + +class AvatarUtilities { + public const float DEFAULT_AVATAR_HEIGHT = 1.755f; + + public static Bounds GetAvatarBounds(GameObject avatarObject) { + Bounds bounds = new Bounds(); + if (avatarObject != null) { + var meshRenderers = avatarObject.GetComponentsInChildren(); + var skinnedMeshRenderers = avatarObject.GetComponentsInChildren(); + foreach (var renderer in meshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + foreach (var renderer in skinnedMeshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + } + return bounds; + } + + public static float GetAvatarHeight(GameObject avatarObject) { + // height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers + Bounds avatarBounds = GetAvatarBounds(avatarObject); + return avatarBounds.max.y - avatarBounds.min.y; + } +} diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 410314d8b4..0da0fc0d9d 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.4.0 +Version 0.4.1 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 328972736b..2ce40a8a8f 100644 Binary files a/tools/unity-avatar-exporter/avatarExporter.unitypackage and b/tools/unity-avatar-exporter/avatarExporter.unitypackage differ