Merge branch 'master' of https://github.com/highfidelity/hifi into propertyRangeAudit

This commit is contained in:
David Back 2019-01-07 10:36:27 -08:00
commit d25948fbd3
65 changed files with 3444 additions and 251 deletions

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -xeuo pipefail
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}

View file

@ -9,14 +9,19 @@ docker run \
--rm \
--security-opt seccomp:unconfined \
-v "${WORKSPACE}":/home/jenkins/hifi \
-e "RELEASE_NUMBER=${RELEASE_NUMBER}" \
-e "RELEASE_TYPE=${RELEASE_TYPE}" \
-e "ANDROID_BUILD_TARGET=assembleDebug" \
-e "CMAKE_BACKTRACE_URL=${CMAKE_BACKTRACE_URL}" \
-e "CMAKE_BACKTRACE_TOKEN=${CMAKE_BACKTRACE_TOKEN}" \
-e "CMAKE_BACKTRACE_SYMBOLS_TOKEN=${CMAKE_BACKTRACE_SYMBOLS_TOKEN}" \
-e "GA_TRACKING_ID=${GA_TRACKING_ID}" \
-e "GIT_PR_COMMIT=${GIT_PR_COMMIT}" \
-e "VERSION_CODE=${VERSION_CODE}" \
-e RELEASE_NUMBER \
-e RELEASE_TYPE \
-e ANDROID_BUILD_TARGET \
-e ANDROID_BUILD_DIR \
-e CMAKE_BACKTRACE_URL \
-e CMAKE_BACKTRACE_TOKEN \
-e CMAKE_BACKTRACE_SYMBOLS_TOKEN \
-e GA_TRACKING_ID \
-e OAUTH_CLIENT_SECRET \
-e OAUTH_CLIENT_ID \
-e OAUTH_REDIRECT_URI \
-e VERSION_CODE \
"${DOCKER_IMAGE_NAME}" \
sh -c "./build_android.sh"

View file

@ -746,65 +746,27 @@ void AvatarMixer::sendStatsPacket() {
AvatarMixerSlaveStats aggregateStats;
QJsonObject slavesObject;
float secondsSinceLastStats = (float)(start - _lastStatsTime) / (float)USECS_PER_SECOND;
// gather stats
int slaveNumber = 1;
_slavePool.each([&](AvatarMixerSlave& slave) {
QJsonObject slaveObject;
AvatarMixerSlaveStats stats;
slave.harvestStats(stats);
slaveObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(stats.nodesProcessed);
slaveObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(stats.packetsProcessed);
slaveObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(stats.nodesBroadcastedTo);
slaveObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(stats.numBytesSent);
slaveObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(stats.numPacketsSent);
slaveObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(stats.numIdentityPackets);
float averageNodes = ((float)stats.nodesBroadcastedTo / (float)tightLoopFrames);
float averageOutboundAvatarKbps = averageNodes ? ((stats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
slaveObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
float averageOthersIncluded = averageNodes ? stats.numOthersIncluded / averageNodes : 0.0f;
slaveObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
float averageOverBudgetAvatars = averageNodes ? stats.overBudgetAvatars / averageNodes : 0.0f;
slaveObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
slaveObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(stats.processIncomingPacketsElapsedTime);
slaveObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(stats.ignoreCalculationElapsedTime);
slaveObject["timing_3_toByteArray"] = TIGHT_LOOP_STAT_UINT64(stats.toByteArrayElapsedTime);
slaveObject["timing_4_avatarDataPacking"] = TIGHT_LOOP_STAT_UINT64(stats.avatarDataPackingElapsedTime);
slaveObject["timing_5_packetSending"] = TIGHT_LOOP_STAT_UINT64(stats.packetSendingElapsedTime);
slaveObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(stats.jobElapsedTime);
slavesObject[QString::number(slaveNumber)] = slaveObject;
slaveNumber++;
aggregateStats += stats;
});
QJsonObject slavesAggregatObject;
slavesAggregatObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
slavesAggregatObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(aggregateStats.packetsProcessed);
slavesAggregatObject["received_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
slavesAggregatObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(aggregateStats.nodesBroadcastedTo);
slavesAggregatObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(aggregateStats.numBytesSent);
slavesAggregatObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(aggregateStats.numPacketsSent);
slavesAggregatObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityPackets);
float averageNodes = ((float)aggregateStats.nodesBroadcastedTo / (float)tightLoopFrames);
float averageOutboundAvatarKbps = averageNodes ? ((aggregateStats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
slavesAggregatObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
float averageOthersIncluded = averageNodes ? aggregateStats.numOthersIncluded / averageNodes : 0.0f;
slavesAggregatObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
slavesAggregatObject["sent_2_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
float averageOverBudgetAvatars = averageNodes ? aggregateStats.overBudgetAvatars / averageNodes : 0.0f;
slavesAggregatObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
slavesAggregatObject["sent_3_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
@ -814,7 +776,6 @@ void AvatarMixer::sendStatsPacket() {
slavesAggregatObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.jobElapsedTime);
statsObject["slaves_aggregate"] = slavesAggregatObject;
statsObject["slaves_individual"] = slavesObject;
_handleViewFrustumPacketElapsedTime = 0;
_handleAvatarIdentityPacketElapsedTime = 0;

View file

@ -0,0 +1,4 @@
<svg width="149" height="150" viewBox="0 0 149 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M74.3055 0C115.543 0 149 33.5916 149 74.6047C149 116.008 115.543 149.6 74.3055 149.6C33.4569 149.6 0 116.008 0 74.6047C0 33.5916 33.4569 0 74.3055 0ZM74.3055 139.054C109.708 139.054 138.496 110.149 138.496 74.6047C138.496 39.4507 109.708 10.5462 74.3055 10.5462C39.2924 10.5462 10.5039 39.4507 10.5039 74.6047C10.5039 110.149 39.2924 139.054 74.3055 139.054Z" fill="#1FC6A6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M65.3575 89.8376L106.595 43.3562C110.874 38.2784 119.044 45.3092 114.376 50.387L70.0259 100.384C68.0807 102.727 64.9684 102.727 63.0233 101.165L35.0128 78.9008C29.9554 74.6042 36.569 66.4016 41.6264 70.6982L65.3575 89.8376Z" fill="#1FC6A6"/>
</svg>

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

View file

@ -32,6 +32,10 @@ Original.Button {
width: hifi.dimensions.buttonWidth
height: hifi.dimensions.controlLineHeight
property size implicitPadding: Qt.size(20, 16)
property int implicitWidth: buttonContentItem.implicitWidth + implicitPadding.width
property int implicitHeight: buttonContentItem.implicitHeight + implicitPadding.height
HifiConstants { id: hifi }
onHoveredChanged: {
@ -94,6 +98,8 @@ Original.Button {
contentItem: Item {
id: buttonContentItem
implicitWidth: (buttonGlyph.visible ? buttonGlyph.implicitWidth : 0) + buttonText.implicitWidth
implicitHeight: buttonText.implicitHeight
TextMetrics {
id: buttonGlyphTextMetrics;
font: buttonGlyph.font;

View file

@ -254,7 +254,8 @@ Rectangle {
onSaveClicked: function() {
var avatarSettings = {
dominantHand : settings.dominantHandIsLeft ? 'left' : 'right',
collisionsEnabled : settings.avatarCollisionsOn,
collisionsEnabled : settings.environmentCollisionsOn,
otherAvatarsCollisionsEnabled : settings.otherAvatarsCollisionsOn,
animGraphOverrideUrl : settings.avatarAnimationOverrideJSON,
collisionSoundUrl : settings.avatarCollisionSoundUrl
};

View file

@ -0,0 +1,24 @@
import QtQuick 2.6
import "../stylesUit" 1.0
import "../windows" as Windows
import "avatarPackager" 1.0
Windows.ScrollingWindow {
id: root
objectName: "AvatarPackager"
width: 480
height: 706
title: "Avatar Packager"
resizable: false
opacity: parent.opacity
destroyOnHidden: true
implicitWidth: 384; implicitHeight: 640
minSize: Qt.vector2d(480, 706)
HifiConstants { id: hifi }
AvatarPackagerApp {
height: pane.height
width: pane.width
}
}

View file

@ -0,0 +1,396 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQml.Models 2.1
import QtGraphicalEffects 1.0
import Hifi.AvatarPackager.AvatarProjectStatus 1.0
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import "../../controls" 1.0
import "../../dialogs"
import "../avatarapp" 1.0 as AvatarApp
Item {
id: windowContent
HifiConstants { id: hifi }
property alias desktopObject: avatarPackager.desktopObject
MouseArea {
anchors.fill: parent
onClicked: {
unfocusser.forceActiveFocus();
}
Item {
id: unfocusser
visible: false
}
}
InfoBox {
id: fileListPopup
title: "List of Files"
content: Rectangle {
id: fileList
color: "#404040"
anchors.fill: parent
anchors.topMargin: 10
anchors.bottomMargin: 10
anchors.leftMargin: 29
anchors.rightMargin: 29
clip: true
ListView {
anchors.fill: parent
model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles
delegate: Rectangle {
width: parent.width
height: fileText.implicitHeight + 8
color: "#404040"
RalewaySemiBold {
id: fileText
size: 16
elide: Text.ElideLeft
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.topMargin: 4
width: parent.width - 10
color: "white"
text: modelData
}
}
}
}
}
InfoBox {
id: errorPopup
property string errorMessage
boxWidth: 380
boxHeight: 293
content: RalewayRegular {
id: bodyMessage
anchors.fill: parent
anchors.bottomMargin: 10
anchors.leftMargin: 29
anchors.rightMargin: 29
size: 20
color: "white"
text: errorPopup.errorMessage
width: parent.width
wrapMode: Text.WordWrap
}
function show(title, message) {
errorPopup.title = title;
errorMessage = message;
errorPopup.open();
}
}
Rectangle {
id: modalOverlay
anchors.fill: parent
z: 20
color: "#a15d5d5d"
visible: false
// This mouse area captures the cursor events while the modalOverlay is active
MouseArea {
anchors.fill: parent
propagateComposedEvents: false
hoverEnabled: true
}
}
AvatarApp.MessageBox {
id: popup
anchors.fill: parent
visible: false
closeOnClickOutside: true
}
Column {
id: avatarPackager
anchors.fill: parent
state: "main"
states: [
State {
name: AvatarPackagerState.main
PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; backButtonVisible: false }
PropertyChanges { target: avatarPackagerMain; visible: true }
PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer }
},
State {
name: AvatarPackagerState.createProject
PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") }
PropertyChanges { target: createAvatarProject; visible: true }
PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer }
},
State {
name: AvatarPackagerState.project
PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true }
PropertyChanges { target: avatarProject; visible: true }
PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer }
},
State {
name: AvatarPackagerState.projectUpload
PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; backButtonEnabled: false }
PropertyChanges { target: avatarUploader; visible: true }
PropertyChanges { target: avatarPackagerFooter; visible: false }
}
]
property alias showModalOverlay: modalOverlay.visible
property var desktopObject: desktop
function openProject(path) {
let status = AvatarPackagerCore.openAvatarProject(path);
if (status !== AvatarProjectStatus.SUCCESS) {
displayErrorMessage(status);
return status;
}
avatarProject.reset();
avatarPackager.state = AvatarPackagerState.project;
return status;
}
function displayErrorMessage(status) {
if (status === AvatarProjectStatus.SUCCESS) {
return;
}
switch (status) {
case AvatarProjectStatus.ERROR_CREATE_PROJECT_NAME:
errorPopup.show("Project Folder Already Exists", "A folder with that name already exists at that location. Please choose a different project name or location.");
break;
case AvatarProjectStatus.ERROR_CREATE_CREATING_DIRECTORIES:
errorPopup.show("Project Folders Creation Error", "There was a problem creating the Avatar Project directory. Please check the project location and try again.");
break;
case AvatarProjectStatus.ERROR_CREATE_FIND_MODEL:
errorPopup.show("Cannot Find Model File", "There was a problem while trying to find the specified model file. Please verify that it exists at the specified location.");
break;
case AvatarProjectStatus.ERROR_CREATE_OPEN_MODEL:
errorPopup.show("Cannot Open Model File", "There was a problem while trying to open the specified model file.");
break;
case AvatarProjectStatus.ERROR_CREATE_READ_MODEL:
errorPopup.show("Error Read Model File", "There was a problem while trying to read the specified model file. Please check that the file is a valid FBX file and try again.");
break;
case AvatarProjectStatus.ERROR_CREATE_WRITE_FST:
errorPopup.show("Error Writing Project File", "There was a problem while trying to write the FST file.");
break;
case AvatarProjectStatus.ERROR_OPEN_INVALID_FILE_TYPE:
errorPopup.show("Invalid Project Path", "The avatar packager can only open FST files.");
break;
case AvatarProjectStatus.ERROR_OPEN_PROJECT_FOLDER:
errorPopup.show("Project Missing", "Project folder cannot be found. Please locate the folder and copy/move it to its original location.");
break;
case AvatarProjectStatus.ERROR_OPEN_FIND_FST:
errorPopup.show("File Missing", "We cannot find the project file (.fst) in the project folder. Please locate it and move it to the project folder.");
break;
case AvatarProjectStatus.ERROR_OPEN_OPEN_FST:
errorPopup.show("File Read Error", "We cannot read the project file (.fst).");
break;
case AvatarProjectStatus.ERROR_OPEN_FIND_MODEL:
errorPopup.show("File Missing", "We cannot find the avatar model file (.fbx) in the project folder. Please locate it and move it to the project folder.");
break;
default:
errorPopup.show("Error Message Missing", "Error message missing for status " + status);
}
}
function openDocs() {
Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/create-avatars#how-to-package-your-avatar");
}
AvatarPackagerHeader {
z: 100
id: avatarPackagerHeader
colorScheme: root.colorScheme
onBackButtonClicked: {
avatarPackager.state = AvatarPackagerState.main;
}
onDocsButtonClicked: {
avatarPackager.openDocs();
}
}
Item {
height: windowContent.height - avatarPackagerHeader.height - avatarPackagerFooter.height
width: windowContent.width
Rectangle {
anchors.fill: parent
color: "#404040"
}
AvatarProject {
id: avatarProject
colorScheme: root.colorScheme
anchors.fill: parent
}
AvatarProjectUpload {
id: avatarUploader
anchors.fill: parent
root: avatarProject
}
CreateAvatarProject {
id: createAvatarProject
colorScheme: root.colorScheme
anchors.fill: parent
}
Item {
id: avatarPackagerMain
visible: false
anchors.fill: parent
property var footer: Item {
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: createProjectButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: openProjectButton.left
anchors.rightMargin: 22
height: 40
width: 134
text: qsTr("New Project")
colorScheme: root.colorScheme
onClicked: {
createAvatarProject.clearInputs();
avatarPackager.state = AvatarPackagerState.createProject;
}
}
HifiControls.Button {
id: openProjectButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
height: 40
width: 133
text: qsTr("Open Project")
color: hifi.buttons.blue
colorScheme: root.colorScheme
onClicked: {
avatarPackager.showModalOverlay = true;
let browser = avatarPackager.desktopObject.fileDialog({
selectDirectory: false,
dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH),
filter: "Avatar Project FST Files (*.fst)",
title: "Open Project (.fst)",
});
browser.canceled.connect(function() {
avatarPackager.showModalOverlay = false;
});
browser.selectedFile.connect(function(fileUrl) {
let fstFilePath = fileDialogHelper.urlToPath(fileUrl);
avatarPackager.showModalOverlay = false;
avatarPackager.openProject(fstFilePath);
});
}
}
}
Flow {
visible: AvatarPackagerCore.recentProjects.length === 0
anchors {
fill: parent
topMargin: 18
leftMargin: 16
rightMargin: 16
}
RalewayRegular {
size: 20
color: "white"
text: "Use a custom avatar of your choice."
width: parent.width
wrapMode: Text.WordWrap
}
RalewayRegular {
size: 20
color: "white"
text: "<a href='javascript:void'>Visit our docs</a> to learn more about using the packager."
linkColor: "#00B4EF"
width: parent.width
wrapMode: Text.WordWrap
onLinkActivated: {
avatarPackager.openDocs();
}
}
}
Item {
anchors.fill: parent
visible: AvatarPackagerCore.recentProjects.length > 0
RalewayRegular {
id: recentProjectsText
color: 'white'
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: 16
anchors.leftMargin: 16
size: 20
text: "Recent Projects"
onLinkActivated: fileListPopup.open()
}
Column {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
top: recentProjectsText.bottom
topMargin: 16
leftMargin: 16
rightMargin: 16
}
spacing: 10
Repeater {
model: AvatarPackagerCore.recentProjects
AvatarProjectCard {
title: modelData.name
path: modelData.projectPath
onOpen: avatarPackager.openProject(modelData.path)
}
}
}
}
}
}
AvatarPackagerFooter {
id: avatarPackagerFooter
}
}
}

View file

@ -0,0 +1,41 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Rectangle {
id: avatarPackagerFooter
color: "#404040"
height: content === defaultContent ? 0 : 74
visible: content !== defaultContent
width: parent.width
property var content: Item { id: defaultContent }
children: [background, content]
property var background: Rectangle {
anchors.fill: parent
color: "#404040"
Rectangle {
id: topBorder1
anchors.top: parent.top
color: "#252525"
height: 1
width: parent.width
}
Rectangle {
id: topBorder2
anchors.top: topBorder1.bottom
color: "#575757"
height: 1
width: parent.width
}
}
}

View file

@ -0,0 +1,144 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import "../avatarapp" 1.0
ShadowRectangle {
id: root
width: parent.width
height: 74
color: "#252525"
property string title: qsTr("Avatar Packager")
property alias docsEnabled: docs.visible
property bool backButtonVisible: true // If false, is not visible and does not take up space
property bool backButtonEnabled: true // If false, is not visible but does not affect space
property bool canRename: false
property int colorScheme
property color textColor: "white"
property color hoverTextColor: "gray"
property color pressedTextColor: "#6A6A6A"
signal backButtonClicked
signal docsButtonClicked
RalewayButton {
id: back
visible: backButtonEnabled && backButtonVisible
size: 28
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: 16
text: "◀"
onClicked: root.backButtonClicked()
}
Item {
id: titleArea
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: root.backButtonVisible ? back.right : parent.left
anchors.leftMargin: root.backButtonVisible ? 11 : 21
anchors.right: docs.left
states: [
State {
name: "renaming"
PropertyChanges { target: title; visible: false }
PropertyChanges { target: titleInputArea; visible: true }
}
]
Item {
id: title
anchors.fill: parent
RalewaySemiBold {
id: titleNotRenameable
visible: !root.canRename
size: 28
anchors.fill: parent
text: root.title
color: "white"
}
RalewayButton {
id: titleRenameable
visible: root.canRename
enabled: root.canRename
size: 28
anchors.fill: parent
text: root.title
onClicked: {
if (!root.canRename || AvatarPackagerCore.currentAvatarProject === null) {
return;
}
titleArea.state = "renaming";
titleInput.text = AvatarPackagerCore.currentAvatarProject.name;
titleInput.selectAll();
titleInput.forceActiveFocus(Qt.MouseFocusReason);
}
}
}
Item {
id: titleInputArea
visible: false
anchors.fill: parent
HifiControls.TextField {
id: titleInput
anchors.fill: parent
text: ""
colorScheme: root.colorScheme
font.family: "Fira Sans"
font.pixelSize: 28
z: 200
onFocusChanged: {
if (titleArea.state === "renaming" && !focus) {
accepted();
}
}
Keys.onPressed: {
if (event.key === Qt.Key_Escape) {
titleArea.state = "";
}
}
onAccepted: {
if (acceptableInput) {
AvatarPackagerCore.currentAvatarProject.name = text;
}
titleArea.state = "";
}
}
}
}
RalewayButton {
id: docs
visible: false
size: 28
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: 16
text: qsTr("Docs")
onClicked: {
docsButtonClicked();
}
}
}

View file

@ -0,0 +1,10 @@
pragma Singleton
import QtQuick 2.6
Item {
id: singleton
readonly property string main: "main"
readonly property string project: "project"
readonly property string createProject: "createProject"
readonly property string projectUpload: "projectUpload"
}

View file

@ -0,0 +1,336 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.2 as Original
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: root
HifiConstants { id: hifi }
Style { id: style }
property int colorScheme
property var uploader: null
property bool hasSuccessfullyUploaded: true
visible: false
anchors.fill: parent
anchors.margins: 10
function reset() {
hasSuccessfullyUploaded = false;
uploader = null;
}
property var footer: Item {
anchors.fill: parent
Item {
id: uploadFooter
visible: !root.uploader || root.finished || root.uploader.state !== 4
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: uploadButton
visible: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded
enabled: Account.loggedIn
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("Upload")
color: hifi.buttons.blue
colorScheme: root.colorScheme
width: 133
height: 40
onClicked: {
uploadNew();
}
}
HifiControls.Button {
id: updateButton
visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded
enabled: Account.loggedIn
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("Update")
color: hifi.buttons.blue
colorScheme: root.colorScheme
width: 134
height: 40
onClicked: {
showConfirmUploadPopup(uploadNew, uploadUpdate);
}
}
Item {
anchors.fill: parent
visible: root.hasSuccessfullyUploaded
HifiControls.Button {
enabled: Account.loggedIn
anchors.verticalCenter: parent.verticalCenter
anchors.right: viewInInventoryButton.left
anchors.rightMargin: 16
text: qsTr("Update")
color: hifi.buttons.white
colorScheme: root.colorScheme
width: 134
height: 40
onClicked: {
showConfirmUploadPopup(uploadNew, uploadUpdate);
}
}
HifiControls.Button {
id: viewInInventoryButton
enabled: Account.loggedIn
width: 168
height: 40
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("View in Inventory")
color: hifi.buttons.blue
colorScheme: root.colorScheme
onClicked: AvatarPackagerCore.currentAvatarProject.openInInventory()
}
}
}
Rectangle {
id: uploadingItemFooter
anchors.fill: parent
anchors.topMargin: 1
visible: !!root.uploader && !root.finished && root.uploader.state === 4
color: "#00B4EF"
LoadingCircle {
id: runningImage
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 16
width: 28
height: 28
white: true
}
RalewayRegular {
id: stepText
size: 20
anchors.verticalCenter: parent.verticalCenter
anchors.left: runningImage.right
anchors.leftMargin: 16
text: "Adding item to Inventory"
color: "white"
}
}
}
function uploadNew() {
upload(false);
}
function uploadUpdate() {
upload(true);
}
Connections {
target: root.uploader
onStateChanged: {
root.hasSuccessfullyUploaded = newState >= 4;
}
}
function upload(updateExisting) {
root.uploader = AvatarPackagerCore.currentAvatarProject.upload(updateExisting);
console.log("uploader: "+ root.uploader);
root.uploader.send();
avatarPackager.state = AvatarPackagerState.projectUpload;
}
function showConfirmUploadPopup() {
popup.titleText = 'Overwrite Avatar';
popup.bodyText = 'You have previously uploaded the avatar file from this project.' +
' This will overwrite that avatar and you wont be able to access the older version.';
popup.button1text = 'CREATE NEW';
popup.button2text = 'OVERWRITE';
popup.onButton2Clicked = function() {
popup.close();
uploadUpdate();
};
popup.onButton1Clicked = function() {
popup.close();
showConfirmCreateNewPopup();
};
popup.open();
}
function showConfirmCreateNewPopup(confirmCallback) {
popup.titleText = 'Create New';
popup.bodyText = 'This will upload your current files with the same avatar name.' +
' You will lose the ability to update the previously uploaded avatar. Are you sure you want to continue?';
popup.button1text = 'CANCEL';
popup.button2text = 'CONFIRM';
popup.onButton1Clicked = function() {
popup.close()
};
popup.onButton2Clicked = function() {
popup.close();
uploadNew();
};
popup.open();
}
RalewayRegular {
id: infoMessage
states: [
State {
when: root.hasSuccessfullyUploaded
name: "upload-success"
PropertyChanges {
target: infoMessage
text: "Your avatar has been successfully uploaded to our servers. Make changes to your avatar by editing and uploading the project files."
}
},
State {
name: "has-previous-success"
when: !!AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID
PropertyChanges {
target: infoMessage
text: "Click \"Update\" to overwrite the hosted files and update the avatar in your inventory. You will have to “Wear” the avatar again to see changes."
}
}
]
color: 'white'
size: 20
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottomMargin: 24
wrapMode: Text.Wrap
text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users."
}
HifiControls.Button {
id: openFolderButton
visible: false
width: parent.width
anchors.top: infoMessage.bottom
anchors.topMargin: 10
text: qsTr("Open Project Folder")
colorScheme: root.colorScheme
height: 30
onClicked: {
fileDialogHelper.openDirectory(fileDialogHelper.pathToUrl(AvatarPackagerCore.currentAvatarProject.projectFolderPath));
}
}
RalewayRegular {
id: showFilesText
color: 'white'
linkColor: style.colors.blueHighlight
visible: AvatarPackagerCore.currentAvatarProject !== null
anchors.bottom: loginRequiredMessage.top
anchors.bottomMargin: 10
size: 20
text: AvatarPackagerCore.currentAvatarProject ? AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>See list</a>" : ""
onLinkActivated: fileListPopup.open()
}
Rectangle {
id: loginRequiredMessage
visible: !Account.loggedIn
height: !Account.loggedIn ? loginRequiredTextRow.height + 20 : 0
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
color: "#FFD6AD"
border.color: "#F39622"
border.width: 2
radius: 2
Item {
id: loginRequiredTextRow
height: Math.max(loginWarningGlyph.implicitHeight, loginWarningText.implicitHeight)
anchors.fill: parent
anchors.margins: 10
HiFiGlyphs {
id: loginWarningGlyph
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: implicitWidth
size: 48
text: "+"
color: "black"
}
RalewayRegular {
id: loginWarningText
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 16
anchors.left: loginWarningGlyph.right
anchors.right: parent.right
text: "Please login to upload your avatar to High Fidelity hosting."
size: 18
wrapMode: Text.Wrap
}
}
}
}

View file

@ -0,0 +1,102 @@
import QtQuick 2.0
import QtGraphicalEffects 1.0
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: projectCard
height: 80
width: parent.width
property alias title: title.text
property alias path: path.text
property color textColor: "#E3E3E3"
property color hoverTextColor: "#121212"
property color pressedTextColor: "#121212"
property color backgroundColor: "#121212"
property color hoverBackgroundColor: "#E3E3E3"
property color pressedBackgroundColor: "#6A6A6A"
signal open
state: mouseArea.pressed ? "pressed" : (mouseArea.containsMouse ? "hover" : "normal")
states: [
State {
name: "normal"
PropertyChanges { target: background; color: backgroundColor }
PropertyChanges { target: title; color: textColor }
PropertyChanges { target: path; color: textColor }
},
State {
name: "hover"
PropertyChanges { target: background; color: hoverBackgroundColor }
PropertyChanges { target: title; color: hoverTextColor }
PropertyChanges { target: path; color: hoverTextColor }
},
State {
name: "pressed"
PropertyChanges { target: background; color: pressedBackgroundColor }
PropertyChanges { target: title; color: pressedTextColor }
PropertyChanges { target: path; color: pressedTextColor }
}
]
Rectangle {
id: background
width: parent.width
height: parent.height
color: "#121212"
radius: 4
RalewayBold {
id: title
elide: "ElideRight"
anchors {
top: parent.top
topMargin: 13
left: parent.left
leftMargin: 16
right: parent.right
rightMargin: 16
}
text: "<title missing>"
size: 24
}
RalewayRegular {
id: path
anchors {
top: title.bottom
left: parent.left
leftMargin: 32
right: background.right
rightMargin: 16
}
elide: "ElideLeft"
horizontalAlignment: Text.AlignRight
text: "<path missing>"
size: 20
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: open()
}
}
DropShadow {
id: shadow
anchors.fill: background
radius: 4
horizontalOffset: 0
verticalOffset: 4
color: Qt.rgba(0, 0, 0, 0.25)
source: background
}
}

View file

@ -0,0 +1,202 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.2 as Original
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: uploadingScreen
property var root: undefined
visible: false
anchors.fill: parent
Timer {
id: backToProjectTimer
interval: 2000
running: false
repeat: false
onTriggered: {
if (avatarPackager.state === AvatarPackagerState.projectUpload) {
avatarPackager.state = AvatarPackagerState.project;
}
}
}
function stateChangedCallback(newState) {
if (newState >= 4) {
root.uploader.stateChanged.disconnect(stateChangedCallback);
backToProjectTimer.start();
}
}
onVisibleChanged: {
if (visible) {
root.uploader.stateChanged.connect(stateChangedCallback);
root.uploader.finishedChanged.connect(function() {
if (root.uploader.error === 0) {
backToProjectTimer.start();
}
});
}
}
Item {
id: uploadStatus
anchors.fill: parent
Item {
id: statusItem
width: parent.width
height: 256
states: [
State {
name: "success"
when: root.uploader.state >= 4 && root.uploader.error === 0
PropertyChanges { target: uploadSpinner; visible: false }
PropertyChanges { target: errorIcon; visible: false }
PropertyChanges { target: successIcon; visible: true }
},
State {
name: "error"
when: root.uploader.finished && root.uploader.error !== 0
PropertyChanges { target: uploadSpinner; visible: false }
PropertyChanges { target: errorIcon; visible: true }
PropertyChanges { target: successIcon; visible: false }
PropertyChanges { target: errorFooter; visible: true }
PropertyChanges { target: errorMessage; visible: true }
}
]
AnimatedImage {
id: uploadSpinner
visible: true
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
width: 164
height: 164
source: "../../../icons/loader-snake-256.gif"
playing: true
}
HiFiGlyphs {
id: errorIcon
visible: false
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
size: 315
text: "+"
color: "#EA4C5F"
}
Image {
id: successIcon
visible: false
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 148
height: 148
source: "../../../icons/checkmark-stroke.svg"
}
}
Item {
id: statusRows
anchors.top: statusItem.bottom
anchors.left: parent.left
anchors.leftMargin: 12
AvatarUploadStatusItem {
id: statusCategories
uploader: root.uploader
text: "Retrieving categories"
uploaderState: 1
}
AvatarUploadStatusItem {
id: statusUploading
uploader: root.uploader
anchors.top: statusCategories.bottom
text: "Uploading data"
uploaderState: 2
}
AvatarUploadStatusItem {
id: statusResponse
uploader: root.uploader
anchors.top: statusUploading.bottom
text: "Waiting for response"
uploaderState: 3
}
}
RalewayRegular {
id: errorMessage
visible: false
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: errorFooter.top
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.bottomMargin: 32
size: 28
wrapMode: Text.Wrap
color: "white"
text: "We couldn't upload your avatar at this time. Please try again later."
}
AvatarPackagerFooter {
id: errorFooter
anchors.bottom: parent.bottom
visible: false
content: Item {
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: backButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("Back")
color: hifi.buttons.blue
colorScheme: root.colorScheme
width: 133
height: 40
onClicked: {
avatarPackager.state = AvatarPackagerState.project;
}
}
}
}
}
}

View file

@ -0,0 +1,96 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: root
height: 48
property string text: "NO STEP TEXT"
property int uploaderState;
property var uploader;
states: [
State {
name: ""
when: root.uploader === null
},
State {
name: "success"
when: root.uploader.state > uploaderState
PropertyChanges { target: stepText; color: "white" }
PropertyChanges { target: successGlyph; visible: true }
},
State {
name: "fail"
when: root.uploader.error !== 0
PropertyChanges { target: stepText; color: "#EA4C5F" }
PropertyChanges { target: failGlyph; visible: true }
},
State {
name: "running"
when: root.uploader.state === uploaderState
PropertyChanges { target: stepText; color: "white" }
PropertyChanges { target: runningImage; visible: true; playing: true }
}
]
Item {
id: statusItem
width: 48
height: parent.height
LoadingCircle {
id: runningImage
visible: false
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 32
height: 32
}
Image {
id: successGlyph
visible: false
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 30
height: 30
source: "../../../icons/checkmark-stroke.svg"
}
HiFiGlyphs {
id: failGlyph
visible: false
width: implicitWidth
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
size: 48
text: "+"
color: "#EA4C5F"
}
}
RalewayRegular {
id: stepText
anchors.left: statusItem.right
anchors.verticalCenter: parent.verticalCenter
text: root.text
size: 28
color: "#777777"
}
}

View file

@ -0,0 +1,63 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import TabletScriptingInterface 1.0
Item {
id: root
readonly property bool pressed: mouseArea.state == "pressed"
readonly property bool hovered: mouseArea.state == "hovering"
signal clicked()
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.focus = true
Tablet.playSound(TabletEnums.ButtonClick);
root.clicked();
}
property string lastState: ""
states: [
State {
name: ""
StateChangeScript {
script: {
mouseArea.lastState = mouseArea.state;
}
}
},
State {
name: "pressed"
when: mouseArea.containsMouse && mouseArea.pressed
StateChangeScript {
script: {
mouseArea.lastState = mouseArea.state;
}
}
},
State {
name: "hovering"
when: mouseArea.containsMouse
StateChangeScript {
script: {
if (mouseArea.lastState == "") {
Tablet.playSound(TabletEnums.ButtonHover);
}
mouseArea.lastState = mouseArea.state;
}
}
}
]
}
}

View file

@ -0,0 +1,135 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import Hifi.AvatarPackager.AvatarProjectStatus 1.0
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: root
HifiConstants { id: hifi }
property int colorScheme
property var footer: Item {
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: createButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
height: 30
width: 133
text: qsTr("Create")
enabled: false
onClicked: {
let status = AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text);
if (status !== AvatarProjectStatus.SUCCESS) {
avatarPackager.displayErrorMessage(status);
return;
}
avatarProject.reset();
avatarPackager.state = AvatarPackagerState.project;
}
}
}
visible: false
anchors.fill: parent
height: parent.height
width: parent.width
function clearInputs() {
name.text = projectLocation.text = avatarModel.text = textureFolder.text = "";
}
function checkErrors() {
let newErrorMessageText = "";
let projectName = name.text;
let projectFolder = projectLocation.text;
let hasProjectNameError = projectName !== "" && projectFolder !== "" && !AvatarPackagerCore.isValidNewProjectName(projectFolder, projectName);
if (hasProjectNameError) {
newErrorMessageText = "A folder with that name already exists at that location. Please choose a different project name or location.";
}
name.error = projectLocation.error = hasProjectNameError;
errorMessage.text = newErrorMessageText;
createButton.enabled = newErrorMessageText === "" && requiredFieldsFilledIn();
}
function requiredFieldsFilledIn() {
return name.text !== "" && projectLocation.text !== "" && avatarModel.text !== "";
}
RalewayRegular {
id: errorMessage
visible: text !== ""
text: ""
color: "#EA4C5F"
wrapMode: Text.WordWrap
size: 20
anchors {
left: parent.left
right: parent.right
}
}
Column {
id: createAvatarColumns
anchors.top: errorMessage.visible ? errorMessage.bottom : parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 10
spacing: 17
property string defaultFileBrowserPath: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH)
ProjectInputControl {
id: name
label: "Name"
colorScheme: root.colorScheme
onTextChanged: checkErrors()
}
ProjectInputControl {
id: projectLocation
label: "Specify Project Location"
colorScheme: root.colorScheme
browseEnabled: true
browseFolder: true
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
browseTitle: "Project Location"
onTextChanged: checkErrors()
}
ProjectInputControl {
id: avatarModel
label: "Specify Avatar Model (.fbx)"
colorScheme: root.colorScheme
browseEnabled: true
browseFolder: false
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
browseFilter: "Avatar Model File (*.fbx)"
browseTitle: "Open Avatar Model (.fbx)"
onTextChanged: checkErrors()
}
ProjectInputControl {
id: textureFolder
label: "Specify Texture Folder - <i>Optional</i>"
colorScheme: root.colorScheme
browseEnabled: true
browseFolder: true
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
browseTitle: "Texture Folder"
onTextChanged: checkErrors()
}
}
}

View file

@ -0,0 +1,120 @@
import Hifi 1.0 as Hifi
import QtQuick 2.5
import stylesUit 1.0
import controlsUit 1.0 as HifiControlsUit
import "../../controls" as HifiControls
Rectangle {
id: root
visible: false
color: Qt.rgba(.34, .34, .34, 0.6)
z: 999;
anchors.fill: parent
property alias title: titleText.text
property alias content: loader.sourceComponent
property bool closeOnClickOutside: false;
property alias boxWidth: mainContainer.width
property alias boxHeight: mainContainer.height
onVisibleChanged: {
if (visible) {
focus = true;
}
}
function open() {
visible = true;
}
function close() {
visible = false;
}
HifiConstants {
id: hifi
}
// This object is always used in a popup.
// This MouseArea is used to prevent a user from being
// able to click on a button/mouseArea underneath the popup.
MouseArea {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
onClicked: {
if (closeOnClickOutside) {
root.close()
}
}
}
Rectangle {
id: mainContainer
width: Math.max(parent.width * 0.8, 400)
height: parent.height * 0.6
MouseArea {
anchors.fill: parent
propagateComposedEvents: false
hoverEnabled: true
onClicked: function(ev) {
ev.accepted = true;
}
}
anchors.centerIn: parent
color: "#252525"
// TextStyle1
RalewaySemiBold {
id: titleText
size: 24
color: "white"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 30
text: "Title not defined"
}
Item {
anchors.topMargin: 10
anchors.top: titleText.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: button.top
Loader {
id: loader
anchors.fill: parent
}
}
Item {
id: button
height: 40
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: 12
HifiControlsUit.Button {
anchors.centerIn: parent
text: "CLOSE"
onClicked: close()
color: hifi.buttons.noneBorderlessWhite;
colorScheme: hifi.colorSchemes.dark;
}
}
}
}

View file

@ -0,0 +1,16 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
AnimatedImage {
id: root
width: 128
height: 128
property bool white: false
source: white ? "../../../icons/loader-snake-256-wf.gif" : "../../../icons/loader-snake-256.gif"
playing: true
}

View file

@ -0,0 +1,78 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Column {
id: control
anchors.left: parent.left
anchors.leftMargin: 21
anchors.right: parent.right
anchors.rightMargin: 16
height: 75
spacing: 4
property alias label: label.text
property alias browseEnabled: browseButton.visible
property bool browseFolder: false
property string browseFilter: "All Files (*.*)"
property string browseTitle: "Open file"
property string browseDir: ""
property alias text: input.text
property alias error: input.error
property int colorScheme
Row {
RalewaySemiBold {
id: label
size: 20
font.weight: Font.Medium
text: ""
color: "white"
}
}
Row {
width: control.width
spacing: 16
height: 40
HifiControls.TextField {
id: input
colorScheme: control.colorScheme
font.family: "Fira Sans"
font.pixelSize: 18
height: parent.height
width: browseButton.visible ? parent.width - browseButton.width - parent.spacing : parent.width
}
HifiControls.Button {
id: browseButton
visible: false
height: parent.height
width: 133
text: qsTr("Browse")
colorScheme: root.colorScheme
onClicked: {
avatarPackager.showModalOverlay = true;
let browser = avatarPackager.desktopObject.fileDialog({
selectDirectory: browseFolder,
dir: browseDir,
filter: browseFilter,
title: browseTitle,
});
browser.canceled.connect(function() {
avatarPackager.showModalOverlay = false;
});
browser.selectedFile.connect(function(fileUrl) {
input.text = fileDialogHelper.urlToPath(fileUrl);
avatarPackager.showModalOverlay = false;
});
}
}
}
}

View file

@ -0,0 +1,26 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import TabletScriptingInterface 1.0
RalewaySemiBold {
id: root
property color idleColor: "white"
property color hoverColor: "#AFAFAF"
property color pressedColor: "#575757"
color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor)
signal clicked()
ClickableArea {
id: clickable
anchors.fill: root
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,20 @@
import QtQuick 2.5
import QtQuick.Window 2.2
import "../../stylesUit" 1.0
QtObject {
readonly property QtObject colors: QtObject {
readonly property color lightGrayBackground: "#f2f2f2"
readonly property color black: "#000000"
readonly property color white: "#ffffff"
readonly property color blueHighlight: "#00b4ef"
readonly property color inputFieldBackground: "#d4d4d4"
readonly property color yellowishOrange: "#ffb017"
readonly property color blueAccent: "#0093c5"
readonly property color greenHighlight: "#1fc6a6"
readonly property color lightGray: "#afafaf"
readonly property color redHighlight: "#ea4c5f"
readonly property color orangeAccent: "#ff6309"
}
}

View file

@ -0,0 +1,2 @@
module AvatarPackager
singleton AvatarPackagerState 1.0 AvatarPackagerState.qml

View file

@ -23,6 +23,8 @@ Rectangle {
property string button2color: hifi.buttons.blue;
property string button2text: ''
property bool closeOnClickOutside: false;
property var onButton2Clicked;
property var onButton1Clicked;
property var onLinkClicked;
@ -56,6 +58,11 @@ Rectangle {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
onClicked: {
if (closeOnClickOutside) {
root.close()
}
}
}
Rectangle {
@ -68,6 +75,15 @@ Rectangle {
console.debug('mainContainer: height = ', height)
}
MouseArea {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
onClicked: function(ev) {
ev.accepted = true;
}
}
anchors.centerIn: parent
color: "white"

View file

@ -35,7 +35,8 @@ Rectangle {
property real scaleValue: scaleSlider.value / 10
property alias dominantHandIsLeft: leftHandRadioButton.checked
property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked
property alias otherAvatarsCollisionsOn: otherAvatarsCollisionsEnabledCheckBox.checked
property alias environmentCollisionsOn: environmentCollisionsEnabledCheckBox.checked
property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text
property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText
property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text
@ -54,11 +55,11 @@ Rectangle {
} else {
rightHandRadioButton.checked = true;
}
if (settings.otherAvatarsCollisionsEnabled) {
otherAvatarsCollisionsEnabledCheckBox.checked = true;
}
if (settings.collisionsEnabled) {
collisionsEnabledRadiobutton.checked = true;
} else {
collisionsDisabledRadioButton.checked = true;
environmentCollisionsEnabledCheckBox.checked = true;
}
avatarAnimationJSON = settings.animGraphUrl;
@ -255,55 +256,43 @@ Rectangle {
text: "Right"
boxSize: 20
}
HifiConstants {
id: hifi
}
// TextStyle9
RalewaySemiBold {
size: 17;
Layout.row: 1
Layout.column: 0
text: "Avatar Collisions"
text: "Avatar collides with other avatars"
}
ButtonGroup {
id: onOff
}
HifiControlsUit.RadioButton {
id: collisionsEnabledRadiobutton
Layout.row: 1
Layout.column: 1
Layout.leftMargin: -40
ButtonGroup.group: onOff
colorScheme: hifi.colorSchemes.light
fontSize: 17
letterSpacing: 1.4
checked: true
text: "ON"
boxSize: 20
}
HifiConstants {
id: hifi
}
HifiControlsUit.RadioButton {
id: collisionsDisabledRadioButton
HifiControlsUit.CheckBox {
id: otherAvatarsCollisionsEnabledCheckBox;
boxSize: 20;
Layout.row: 1
Layout.column: 2
Layout.rightMargin: 20
ButtonGroup.group: onOff
Layout.leftMargin: 60
colorScheme: hifi.colorSchemes.light
fontSize: 17
letterSpacing: 1.4
}
text: "OFF"
boxSize: 20
// TextStyle9
RalewaySemiBold {
size: 17;
Layout.row: 2
Layout.column: 0
text: "Avatar collides with environment"
}
HifiControlsUit.CheckBox {
id: environmentCollisionsEnabledCheckBox;
boxSize: 20;
Layout.row: 2
Layout.column: 2
Layout.leftMargin: 60
colorScheme: hifi.colorSchemes.light
}
}

View file

@ -0,0 +1,15 @@
import QtQuick 2.0
import "../avatarPackager" 1.0
Item {
id: root
width: 480
height: 706
AvatarPackagerApp {
width: parent.width
height: parent.height
desktopObject: tabletRoot
}
}

View file

@ -158,6 +158,7 @@
#include "audio/AudioScope.h"
#include "avatar/AvatarManager.h"
#include "avatar/MyHead.h"
#include "avatar/AvatarPackager.h"
#include "CrashRecoveryHandler.h"
#include "CrashHandler.h"
#include "devices/DdeFaceTracker.h"
@ -922,6 +923,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set<Keyboard>();
DependencyManager::set<KeyboardScriptingInterface>();
DependencyManager::set<GrabManager>();
DependencyManager::set<AvatarPackager>();
return previousSessionCrashed;
}
@ -2284,7 +2286,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// Setup the mouse ray pick and related operators
{
auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_INCLUDE_NONCOLLIDABLE()), 0.0f, true);
auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES()), 0.0f, true);
mouseRayPick->parentTransform = std::make_shared<MouseTransformNode>();
mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE);
auto mouseRayPickID = DependencyManager::get<PickManager>()->addPick(PickQuery::Ray, mouseRayPick);
@ -2617,6 +2619,7 @@ void Application::cleanupBeforeQuit() {
DependencyManager::destroy<PickManager>();
DependencyManager::destroy<KeyboardScriptingInterface>();
DependencyManager::destroy<Keyboard>();
DependencyManager::destroy<AvatarPackager>();
qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete";
}

View file

@ -35,6 +35,7 @@
#include "assets/ATPAssetMigrator.h"
#include "audio/AudioScope.h"
#include "avatar/AvatarManager.h"
#include "avatar/AvatarPackager.h"
#include "AvatarBookmarks.h"
#include "devices/DdeFaceTracker.h"
#include "MainWindow.h"
@ -144,9 +145,13 @@ Menu::Menu() {
assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets());
}
// Edit > Package Avatar as .fst...
addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0,
qApp, SLOT(packageModel()));
// Edit > Avatar Packager
#ifndef Q_OS_ANDROID
action = addActionToQMenuAndActionHash(editMenu, MenuOption::AvatarPackager);
connect(action, &QAction::triggered, [] {
DependencyManager::get<AvatarPackager>()->open();
});
#endif
// Edit > Reload All Content
addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()));
@ -645,6 +650,8 @@ Menu::Menu() {
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowTrackedObjects, 0, false, qApp, SLOT(setShowTrackedObjects(bool)));
addActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::PackageModel, 0, qApp, SLOT(packageModel()));
// Developer > Hands >>>
MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands");
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false,

View file

@ -46,6 +46,7 @@ namespace MenuOption {
const QString AutoMuteAudio = "Auto Mute Microphone";
const QString AvatarReceiveStats = "Show Receive Stats";
const QString AvatarBookmarks = "Avatar Bookmarks";
const QString AvatarPackager = "Avatar Packager";
const QString Back = "Back";
const QString BinaryEyelidControl = "Binary Eyelid Control";
const QString BookmarkAvatar = "Bookmark Avatar";

View file

@ -267,6 +267,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
if (avatar->getSkeletonModel()->isLoaded()) {
// remove the orb if it is there
avatar->removeOrb();
avatar->updateCollisionGroup(_myAvatar->getOtherAvatarsCollisionsEnabled());
if (avatar->needsPhysicsUpdate()) {
_avatarsToChangeInPhysics.insert(avatar);
}

View file

@ -19,6 +19,7 @@
AvatarMotionState::AvatarMotionState(OtherAvatarPointer avatar, const btCollisionShape* shape) : ObjectMotionState(shape), _avatar(avatar) {
assert(_avatar);
_type = MOTIONSTATE_TYPE_AVATAR;
_collisionGroup = BULLET_COLLISION_GROUP_OTHER_AVATAR;
cacheShapeDiameter();
}
@ -170,8 +171,8 @@ QUuid AvatarMotionState::getSimulatorID() const {
// virtual
void AvatarMotionState::computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const {
group = BULLET_COLLISION_GROUP_OTHER_AVATAR;
mask = Physics::getDefaultCollisionMask(group);
group = _collisionGroup;
mask = _collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS ? 0 : Physics::getDefaultCollisionMask(group);
}
// virtual

View file

@ -66,6 +66,9 @@ public:
void addDirtyFlags(uint32_t flags) { _dirtyFlags |= flags; }
void setCollisionGroup(int32_t group) { _collisionGroup = group; }
int32_t getCollisionGroup() { return _collisionGroup; }
virtual void computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const override;
virtual float getMass() const override;
@ -87,7 +90,7 @@ protected:
OtherAvatarPointer _avatar;
float _diameter { 0.0f };
int32_t _collisionGroup;
uint32_t _dirtyFlags;
};

View file

@ -0,0 +1,149 @@
//
// AvatarPackager.cpp
//
//
// Created by Thijs Wenker on 12/6/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AvatarPackager.h"
#include "Application.h"
#include <QQmlContext>
#include <QQmlEngine>
#include <QUrl>
#include <OffscreenUi.h>
#include "ModelSelector.h"
#include <avatar/MarketplaceItemUploader.h>
#include <mutex>
#include "ui/TabletScriptingInterface.h"
std::once_flag setupQMLTypesFlag;
AvatarPackager::AvatarPackager() {
std::call_once(setupQMLTypesFlag, []() {
qmlRegisterType<FST>();
qmlRegisterType<MarketplaceItemUploader>();
qRegisterMetaType<AvatarPackager*>();
qRegisterMetaType<AvatarProject*>();
qRegisterMetaType<AvatarProjectStatus::AvatarProjectStatus>();
qmlRegisterUncreatableMetaObject(
AvatarProjectStatus::staticMetaObject,
"Hifi.AvatarPackager.AvatarProjectStatus",
1, 0,
"AvatarProjectStatus",
"Error: only enums"
);
});
recentProjectsFromVariantList(_recentProjectsSetting.get());
QDir defaultProjectsDir(AvatarProject::getDefaultProjectsPath());
defaultProjectsDir.mkpath(".");
}
bool AvatarPackager::open() {
const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) {
context->setContextProperty("AvatarPackagerCore", this);
};
static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system";
auto tablet = dynamic_cast<TabletProxy*>(DependencyManager::get<TabletScriptingInterface>()->getTablet(SYSTEM_TABLET));
if (tablet->getToolbarMode()) {
static const QUrl url{ "hifi/AvatarPackagerWindow.qml" };
DependencyManager::get<OffscreenUi>()->show(url, "AvatarPackager", packageModelDialogCreated);
return true;
}
static const QUrl url{ "hifi/tablet/AvatarPackager.qml" };
if (!tablet->isPathLoaded(url)) {
tablet->getTabletSurface()->getSurfaceContext()->setContextProperty("AvatarPackagerCore", this);
tablet->pushOntoStack(url);
return true;
}
return false;
}
void AvatarPackager::addCurrentProjectToRecentProjects() {
const int MAX_RECENT_PROJECTS = 5;
const QString& fstPath = _currentAvatarProject->getFSTPath();
auto removeProjects = QVector<RecentAvatarProject>();
for (const auto& project : _recentProjects) {
if (project.getProjectFSTPath() == fstPath) {
removeProjects.append(project);
}
}
for (const auto& removeProject : removeProjects) {
_recentProjects.removeOne(removeProject);
}
const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath);
_recentProjects.prepend(newRecentProject);
while (_recentProjects.size() > MAX_RECENT_PROJECTS) {
_recentProjects.pop_back();
}
_recentProjectsSetting.set(recentProjectsToVariantList(false));
emit recentProjectsChanged();
}
QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) const {
QVariantList result;
for (const auto& project : _recentProjects) {
QVariantMap projectVariant;
projectVariant.insert("name", project.getProjectName());
projectVariant.insert("path", project.getProjectFSTPath());
if (includeProjectPaths) {
projectVariant.insert("projectPath", project.getProjectPath());
}
result.append(projectVariant);
}
return result;
}
void AvatarPackager::recentProjectsFromVariantList(QVariantList projectsVariant) {
_recentProjects.clear();
for (const auto& projectVariant : projectsVariant) {
auto map = projectVariant.toMap();
_recentProjects.append(RecentAvatarProject(map.value("name").toString(), map.value("path").toString()));
}
}
AvatarProjectStatus::AvatarProjectStatus AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) {
AvatarProjectStatus::AvatarProjectStatus status;
setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath, status));
return status;
}
AvatarProjectStatus::AvatarProjectStatus AvatarPackager::createAvatarProject(const QString& projectsFolder,
const QString& avatarProjectName,
const QString& avatarModelPath,
const QString& textureFolder) {
AvatarProjectStatus::AvatarProjectStatus status;
setAvatarProject(AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder, status));
return status;
}
void AvatarPackager::setAvatarProject(AvatarProject* avatarProject) {
if (avatarProject == _currentAvatarProject) {
return;
}
if (_currentAvatarProject) {
_currentAvatarProject->deleteLater();
}
_currentAvatarProject = avatarProject;
if (_currentAvatarProject) {
addCurrentProjectToRecentProjects();
connect(_currentAvatarProject, &AvatarProject::nameChanged, this, &AvatarPackager::addCurrentProjectToRecentProjects);
QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership);
}
emit avatarProjectChanged();
}

View file

@ -0,0 +1,100 @@
//
// AvatarPackager.h
//
//
// Created by Thijs Wenker on 12/6/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#pragma once
#ifndef hifi_AvatarPackager_h
#define hifi_AvatarPackager_h
#include <QObject>
#include <DependencyManager.h>
#include "FileDialogHelper.h"
#include "avatar/AvatarProject.h"
#include "SettingHandle.h"
class RecentAvatarProject {
public:
RecentAvatarProject() = default;
RecentAvatarProject(QString projectName, QString projectFSTPath) {
_projectName = projectName;
_projectFSTPath = projectFSTPath;
}
RecentAvatarProject(const RecentAvatarProject& other) {
_projectName = other._projectName;
_projectFSTPath = other._projectFSTPath;
}
QString getProjectName() const { return _projectName; }
QString getProjectFSTPath() const { return _projectFSTPath; }
QString getProjectPath() const {
return QFileInfo(_projectFSTPath).absoluteDir().absolutePath();
}
bool operator==(const RecentAvatarProject& other) const {
return _projectName == other._projectName && _projectFSTPath == other._projectFSTPath;
}
private:
QString _projectName;
QString _projectFSTPath;
};
class AvatarPackager : public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
Q_PROPERTY(AvatarProject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged)
Q_PROPERTY(QString AVATAR_PROJECTS_PATH READ getAvatarProjectsPath CONSTANT)
Q_PROPERTY(QVariantList recentProjects READ getRecentProjects NOTIFY recentProjectsChanged)
public:
AvatarPackager();
bool open();
Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus createAvatarProject(const QString& projectsFolder,
const QString& avatarProjectName,
const QString& avatarModelPath,
const QString& textureFolder);
Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus openAvatarProject(const QString& avatarProjectFSTPath);
Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) const {
return AvatarProject::isValidNewProjectName(projectPath, projectName);
}
signals:
void avatarProjectChanged();
void recentProjectsChanged();
private:
Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; };
Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); }
Q_INVOKABLE QVariantList getRecentProjects() const { return recentProjectsToVariantList(true); }
void setAvatarProject(AvatarProject* avatarProject);
void addCurrentProjectToRecentProjects();
AvatarProject* _currentAvatarProject { nullptr };
QVector<RecentAvatarProject> _recentProjects;
QVariantList recentProjectsToVariantList(bool includeProjectPaths) const;
void recentProjectsFromVariantList(QVariantList projectsVariant);
Setting::Handle<QVariantList> _recentProjectsSetting { "io.highfidelity.avatarPackager.recentProjects", QVariantList() };
};
#endif // hifi_AvatarPackager_h

View file

@ -0,0 +1,260 @@
//
// AvatarProject.cpp
//
//
// Created by Thijs Wenker on 12/7/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AvatarProject.h"
#include <FSTReader.h>
#include <QFile>
#include <QFileInfo>
#include <QQmlEngine>
#include <QTimer>
#include "FBXSerializer.h"
#include <ui/TabletScriptingInterface.h>
#include "scripting/HMDScriptingInterface.h"
AvatarProject* AvatarProject::openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status) {
status = AvatarProjectStatus::NONE;
if (!path.toLower().endsWith(".fst")) {
status = AvatarProjectStatus::ERROR_OPEN_INVALID_FILE_TYPE;
return nullptr;
}
QFileInfo fstFileInfo{ path };
if (!fstFileInfo.absoluteDir().exists()) {
status = AvatarProjectStatus::ERROR_OPEN_PROJECT_FOLDER;
return nullptr;
}
if (!fstFileInfo.exists()) {
status = AvatarProjectStatus::ERROR_OPEN_FIND_FST;
return nullptr;
}
QFile file{ fstFileInfo.filePath() };
if (!file.open(QIODevice::ReadOnly)) {
status = AvatarProjectStatus::ERROR_OPEN_OPEN_FST;
return nullptr;
}
const auto project = new AvatarProject(path, file.readAll());
QFileInfo fbxFileInfo{ project->getFBXPath() };
if (!fbxFileInfo.exists()) {
project->deleteLater();
status = AvatarProjectStatus::ERROR_OPEN_FIND_MODEL;
return nullptr;
}
QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership);
status = AvatarProjectStatus::SUCCESS;
return project;
}
AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName,
const QString& avatarModelPath, const QString& textureFolder,
AvatarProjectStatus::AvatarProjectStatus& status) {
status = AvatarProjectStatus::NONE;
if (!isValidNewProjectName(projectsFolder, avatarProjectName)) {
status = AvatarProjectStatus::ERROR_CREATE_PROJECT_NAME;
return nullptr;
}
QDir projectDir(projectsFolder + "/" + avatarProjectName);
if (!projectDir.mkpath(".")) {
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
return nullptr;
}
QDir projectTexturesDir(projectDir.path() + "/textures");
if (!projectTexturesDir.mkpath(".")) {
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
return nullptr;
}
QDir projectScriptsDir(projectDir.path() + "/scripts");
if (!projectScriptsDir.mkpath(".")) {
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
return nullptr;
}
const auto fileName = QFileInfo(avatarModelPath).fileName();
const auto newModelPath = projectDir.absoluteFilePath(fileName);
const auto newFSTPath = projectDir.absoluteFilePath("avatar.fst");
QFile::copy(avatarModelPath, newModelPath);
QFileInfo fbxInfo{ newModelPath };
if (!fbxInfo.exists() || !fbxInfo.isFile()) {
status = AvatarProjectStatus::ERROR_CREATE_FIND_MODEL;
return nullptr;
}
QFile fbx{ fbxInfo.filePath() };
if (!fbx.open(QIODevice::ReadOnly)) {
status = AvatarProjectStatus::ERROR_CREATE_OPEN_MODEL;
return nullptr;
}
std::shared_ptr<hfm::Model> hfmModel;
try {
const QByteArray fbxContents = fbx.readAll();
hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath());
} catch (const QString& error) {
Q_UNUSED(error)
status = AvatarProjectStatus::ERROR_CREATE_READ_MODEL;
return nullptr;
}
QStringList textures{};
auto addTextureToList = [&textures](hfm::Texture texture) mutable {
if (!texture.filename.isEmpty() && texture.content.isEmpty() && !textures.contains(texture.filename)) {
textures << texture.filename;
}
};
foreach(const HFMMaterial material, hfmModel->materials) {
addTextureToList(material.normalTexture);
addTextureToList(material.albedoTexture);
addTextureToList(material.opacityTexture);
addTextureToList(material.glossTexture);
addTextureToList(material.roughnessTexture);
addTextureToList(material.specularTexture);
addTextureToList(material.metallicTexture);
addTextureToList(material.emissiveTexture);
addTextureToList(material.occlusionTexture);
addTextureToList(material.scatteringTexture);
addTextureToList(material.lightmapTexture);
}
QDir textureDir(textureFolder.isEmpty() ? fbxInfo.absoluteDir() : textureFolder);
for (const auto& texture : textures) {
QString sourcePath = textureDir.path() + "/" + texture;
QString targetPath = projectTexturesDir.path() + "/" + texture;
QFileInfo sourceTexturePath(sourcePath);
if (sourceTexturePath.exists()) {
QFile::copy(sourcePath, targetPath);
}
}
auto fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel);
fst->setName(avatarProjectName);
if (!fst->write()) {
status = AvatarProjectStatus::ERROR_CREATE_WRITE_FST;
return nullptr;
}
status = AvatarProjectStatus::SUCCESS;
return new AvatarProject(fst);
}
QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) const {
QStringList result{};
constexpr auto flags = QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden;
if (!scriptsDir.exists()) {
return result;
}
for (const auto& script : scriptsDir.entryInfoList({}, flags)) {
if (script.fileName().toLower().endsWith(".js")) {
result.push_back("scripts/" + script.fileName());
}
}
return result;
}
bool AvatarProject::isValidNewProjectName(const QString& projectPath, const QString& projectName) {
if (projectPath.trimmed().isEmpty() || projectName.trimmed().isEmpty()) {
return false;
}
QDir dir(projectPath + "/" + projectName);
return !dir.exists();
}
AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) :
AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) {
}
AvatarProject::AvatarProject(FST* fst) {
_fst = fst;
auto fileInfo = QFileInfo(getFSTPath());
_directory = fileInfo.absoluteDir();
_fst->setScriptPaths(getScriptPaths(QDir(_directory.path() + "/scripts")));
_fst->write();
refreshProjectFiles();
_projectPath = fileInfo.absoluteDir().absolutePath();
}
void AvatarProject::appendDirectory(const QString& prefix, const QDir& dir) {
constexpr auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden;
for (auto& entry : dir.entryInfoList({}, flags)) {
if (entry.isFile()) {
_projectFiles.append({ entry.absoluteFilePath(), prefix + entry.fileName() });
} else if (entry.isDir()) {
appendDirectory(prefix + entry.fileName() + "/", entry.absoluteFilePath());
}
}
}
void AvatarProject::refreshProjectFiles() {
_projectFiles.clear();
appendDirectory("", _directory);
}
QStringList AvatarProject::getProjectFiles() const {
QStringList paths;
for (auto& path : _projectFiles) {
paths.append(path.relativePath);
}
return paths;
}
MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) {
QUuid itemID;
if (updateExisting) {
itemID = _fst->getMarketplaceID();
}
auto uploader = new MarketplaceItemUploader(getProjectName(), "", QFileInfo(getFSTPath()).fileName(),
itemID, _projectFiles);
connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() {
if (uploader->getError() == MarketplaceItemUploader::Error::None) {
_fst->setMarketplaceID(uploader->getMarketplaceID());
_fst->write();
}
});
return uploader;
}
void AvatarProject::openInInventory() const {
constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 };
auto tablet = dynamic_cast<TabletProxy*>(
DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system"));
tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml");
DependencyManager::get<HMDScriptingInterface>()->openTablet();
tablet->getTabletRoot()->forceActiveFocus();
auto name = getProjectName();
// I'm not a fan of this, but it's the only current option.
QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() {
tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } }));
});
}

View file

@ -0,0 +1,115 @@
//
// AvatarProject.h
//
//
// Created by Thijs Wenker on 12/7/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#pragma once
#ifndef hifi_AvatarProject_h
#define hifi_AvatarProject_h
#include "MarketplaceItemUploader.h"
#include "ProjectFile.h"
#include "FST.h"
#include <QObject>
#include <QDir>
#include <QVariantHash>
#include <QStandardPaths>
namespace AvatarProjectStatus {
Q_NAMESPACE
enum AvatarProjectStatus {
NONE,
SUCCESS,
ERROR_CREATE_PROJECT_NAME,
ERROR_CREATE_CREATING_DIRECTORIES,
ERROR_CREATE_FIND_MODEL,
ERROR_CREATE_OPEN_MODEL,
ERROR_CREATE_READ_MODEL,
ERROR_CREATE_WRITE_FST,
ERROR_OPEN_INVALID_FILE_TYPE,
ERROR_OPEN_PROJECT_FOLDER,
ERROR_OPEN_FIND_FST,
ERROR_OPEN_OPEN_FST,
ERROR_OPEN_FIND_MODEL
};
Q_ENUM_NS(AvatarProjectStatus)
}
class AvatarProject : public QObject {
Q_OBJECT
Q_PROPERTY(FST* fst READ getFST CONSTANT)
Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged)
Q_PROPERTY(QString projectFolderPath READ getProjectPath CONSTANT)
Q_PROPERTY(QString projectFSTPath READ getFSTPath CONSTANT)
Q_PROPERTY(QString projectFBXPath READ getFBXPath CONSTANT)
Q_PROPERTY(QString name READ getProjectName WRITE setProjectName NOTIFY nameChanged)
public:
Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting);
Q_INVOKABLE void openInInventory() const;
Q_INVOKABLE QStringList getProjectFiles() const;
Q_INVOKABLE QString getProjectName() const { return _fst->getName(); }
Q_INVOKABLE void setProjectName(const QString& newProjectName) {
if (newProjectName.trimmed().length() > 0) {
_fst->setName(newProjectName);
_fst->write();
emit nameChanged();
}
}
Q_INVOKABLE QString getProjectPath() const { return _projectPath; }
Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); }
Q_INVOKABLE QString getFBXPath() const {
return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath()));
}
/**
* returns the AvatarProject or a nullptr on failure.
*/
static AvatarProject* openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status);
static AvatarProject* createAvatarProject(const QString& projectsFolder,
const QString& avatarProjectName,
const QString& avatarModelPath,
const QString& textureFolder,
AvatarProjectStatus::AvatarProjectStatus& status);
static bool isValidNewProjectName(const QString& projectPath, const QString& projectName);
static QString getDefaultProjectsPath() {
return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects";
}
signals:
void nameChanged();
void projectFilesChanged();
private:
AvatarProject(const QString& fstPath, const QByteArray& data);
AvatarProject(FST* fst);
~AvatarProject() { _fst->deleteLater(); }
FST* getFST() { return _fst; }
void refreshProjectFiles();
void appendDirectory(const QString& prefix, const QDir& dir);
QStringList getScriptPaths(const QDir& scriptsDir) const;
FST* _fst;
QDir _directory;
QList<ProjectFilePath> _projectFiles{};
QString _projectPath;
};
#endif // hifi_AvatarProject_h

View file

@ -0,0 +1,321 @@
//
// MarketplaceItemUploader.cpp
//
//
// Created by Ryan Huffman on 12/10/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "MarketplaceItemUploader.h"
#include <AccountManager.h>
#include <DependencyManager.h>
#ifndef Q_OS_ANDROID
#include <quazip5/quazip.h>
#include <quazip5/quazipfile.h>
#endif
#include <QTimer>
#include <QBuffer>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
MarketplaceItemUploader::MarketplaceItemUploader(QString title,
QString description,
QString rootFilename,
QUuid marketplaceID,
QList<ProjectFilePath> filePaths) :
_title(title),
_description(description),
_rootFilename(rootFilename),
_marketplaceID(marketplaceID),
_filePaths(filePaths) {
}
void MarketplaceItemUploader::setState(State newState) {
Q_ASSERT(_state != State::Complete);
Q_ASSERT(_error == Error::None);
Q_ASSERT(newState != _state);
_state = newState;
emit stateChanged(newState);
if (newState == State::Complete) {
emit completed();
emit finishedChanged();
}
}
void MarketplaceItemUploader::setError(Error error) {
Q_ASSERT(_state != State::Complete);
Q_ASSERT(_error == Error::None);
_error = error;
emit errorChanged(error);
emit finishedChanged();
}
void MarketplaceItemUploader::send() {
doGetCategories();
}
void MarketplaceItemUploader::doGetCategories() {
setState(State::GettingCategories);
static const QString path = "/api/v1/marketplace/categories";
auto accountManager = DependencyManager::get<AccountManager>();
auto request = accountManager->createRequest(path, AccountManagerAuth::None);
qWarning() << "Request url is: " << request.url();
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* reply = networkAccessManager.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
auto error = reply->error();
if (error == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(reply->readAll());
auto extractCategoryID = [&doc]() -> std::pair<bool, int> {
auto items = doc.object()["data"].toObject()["items"];
if (!items.isArray()) {
qWarning() << "Categories parse error: data.items is not an array";
return { false, 0 };
}
auto itemsArray = items.toArray();
for (const auto item : itemsArray) {
if (!item.isObject()) {
qWarning() << "Categories parse error: item is not an object";
return { false, 0 };
}
auto itemObject = item.toObject();
if (itemObject["name"].toString() == "Avatars") {
auto idValue = itemObject["id"];
if (!idValue.isDouble()) {
qWarning() << "Categories parse error: id is not a number";
return { false, 0 };
}
return { true, (int)idValue.toDouble() };
}
}
qWarning() << "Categories parse error: could not find a category for 'Avatar'";
return { false, 0 };
};
bool success;
std::tie(success, _categoryID) = extractCategoryID();
if (!success) {
qWarning() << "Failed to find marketplace category id";
setError(Error::Unknown);
} else {
qDebug() << "Marketplace Avatar category ID is" << _categoryID;
doUploadAvatar();
}
} else {
setError(Error::Unknown);
}
});
}
void MarketplaceItemUploader::doUploadAvatar() {
#ifdef Q_OS_ANDROID
qWarning() << "Marketplace uploading is not supported on Android";
setError(Error::Unknown);
return;
#else
QBuffer buffer{ &_fileData };
QuaZip zip{ &buffer };
if (!zip.open(QuaZip::Mode::mdAdd)) {
qWarning() << "Failed to open zip";
setError(Error::Unknown);
return;
}
for (auto& filePath : _filePaths) {
qWarning() << "Zipping: " << filePath.absolutePath << filePath.relativePath;
QFileInfo fileInfo{ filePath.absolutePath };
QuaZipFile zipFile{ &zip };
if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(filePath.relativePath))) {
qWarning() << "Could not open zip file:" << zipFile.getZipError();
setError(Error::Unknown);
return;
}
QFile file{ filePath.absolutePath };
if (file.open(QIODevice::ReadOnly)) {
zipFile.write(file.readAll());
} else {
qWarning() << "Failed to open: " << filePath.absolutePath;
}
file.close();
zipFile.close();
if (zipFile.getZipError() != UNZ_OK) {
qWarning() << "Could not close zip file: " << zipFile.getZipError();
setState(State::Complete);
return;
}
}
zip.close();
qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB";
QString path = "/api/v1/marketplace/items";
bool creating = true;
if (!_marketplaceID.isNull()) {
creating = false;
auto idWithBraces = _marketplaceID.toString();
auto idWithoutBraces = idWithBraces.mid(1, idWithBraces.length() - 2);
path += "/" + idWithoutBraces;
}
auto accountManager = DependencyManager::get<AccountManager>();
auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
// TODO(huffman) add JSON escaping
auto escapeJson = [](QString str) -> QString { return str; };
QString jsonString = "{\"marketplace_item\":{";
jsonString += "\"title\":\"" + escapeJson(_title) + "\"";
// Items cannot have their description updated after they have been submitted.
if (creating) {
jsonString += ",\"description\":\"" + escapeJson(_description) + "\"";
}
jsonString += ",\"root_file_key\":\"" + escapeJson(_rootFilename) + "\"";
jsonString += ",\"category_ids\":[" + QStringLiteral("%1").arg(_categoryID) + "]";
jsonString += ",\"license\":0";
jsonString += ",\"files\":\"" + QString::fromLatin1(_fileData.toBase64()) + "\"}}";
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* reply{ nullptr };
if (creating) {
reply = networkAccessManager.post(request, jsonString.toUtf8());
} else {
reply = networkAccessManager.put(request, jsonString.toUtf8());
}
connect(reply, &QNetworkReply::uploadProgress, this, [this](float bytesSent, float bytesTotal) {
if (_state == State::UploadingAvatar) {
emit uploadProgress(bytesSent, bytesTotal);
if (bytesSent >= bytesTotal) {
setState(State::WaitingForUploadResponse);
}
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
_responseData = reply->readAll();
auto error = reply->error();
if (error == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(_responseData.toLatin1());
auto status = doc.object()["status"].toString();
if (status == "success") {
_marketplaceID = QUuid::fromString(doc["data"].toObject()["marketplace_id"].toString());
_itemVersion = doc["data"].toObject()["version"].toDouble();
setState(State::WaitingForInventory);
doWaitForInventory();
} else {
qWarning() << "Got error response while uploading avatar: " << _responseData;
setError(Error::Unknown);
}
} else {
qWarning() << "Got error while uploading avatar: " << reply->error() << reply->errorString() << _responseData;
setError(Error::Unknown);
}
});
setState(State::UploadingAvatar);
#endif
}
void MarketplaceItemUploader::doWaitForInventory() {
static const QString path = "/api/v1/commerce/inventory";
auto accountManager = DependencyManager::get<AccountManager>();
auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* reply = networkAccessManager.post(request, "");
_numRequestsForInventory++;
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
auto data = reply->readAll();
bool success = false;
auto error = reply->error();
if (error == QNetworkReply::NoError) {
// Parse response data
auto doc = QJsonDocument::fromJson(data);
auto isAssetAvailable = [this, &doc]() -> bool {
if (!doc.isObject()) {
return false;
}
auto root = doc.object();
auto status = root["status"].toString();
if (status != "success") {
return false;
}
auto data = root["data"];
if (!data.isObject()) {
return false;
}
auto assets = data.toObject()["assets"];
if (!assets.isArray()) {
return false;
}
for (auto asset : assets.toArray()) {
auto assetObject = asset.toObject();
auto id = QUuid::fromString(assetObject["id"].toString());
if (id.isNull()) {
continue;
}
if (id == _marketplaceID) {
auto version = assetObject["version"];
auto valid = assetObject["valid"];
if (version.isDouble() && valid.isBool()) {
if ((int)version.toDouble() >= _itemVersion && valid.toBool()) {
return true;
}
}
}
}
return false;
};
success = isAssetAvailable();
}
if (success) {
qDebug() << "Found item in inventory";
setState(State::Complete);
} else {
constexpr int MAX_INVENTORY_REQUESTS { 8 };
constexpr int TIME_BETWEEN_INVENTORY_REQUESTS_MS { 5000 };
if (_numRequestsForInventory > MAX_INVENTORY_REQUESTS) {
qDebug() << "Failed to find item in inventory";
setError(Error::Unknown);
} else {
QTimer::singleShot(TIME_BETWEEN_INVENTORY_REQUESTS_MS, [this]() { doWaitForInventory(); });
}
}
});
}

View file

@ -0,0 +1,105 @@
//
// MarketplaceItemUploader.h
//
//
// Created by Ryan Huffman on 12/10/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#pragma once
#ifndef hifi_MarketplaceItemUploader_h
#define hifi_MarketplaceItemUploader_h
#include "ProjectFile.h"
#include <QObject>
#include <QUuid>
class QNetworkReply;
class MarketplaceItemUploader : public QObject {
Q_OBJECT
Q_PROPERTY(bool finished READ getFinished NOTIFY finishedChanged)
Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged)
Q_PROPERTY(State state READ getState NOTIFY stateChanged)
Q_PROPERTY(Error error READ getError NOTIFY errorChanged)
Q_PROPERTY(QString responseData READ getResponseData)
public:
enum class Error {
None,
Unknown,
};
Q_ENUM(Error);
enum class State {
Idle,
GettingCategories,
UploadingAvatar,
WaitingForUploadResponse,
WaitingForInventory,
Complete,
};
Q_ENUM(State);
MarketplaceItemUploader(QString title,
QString description,
QString rootFilename,
QUuid marketplaceID,
QList<ProjectFilePath> filePaths);
Q_INVOKABLE void send();
void setError(Error error);
QString getResponseData() const { return _responseData; }
void setState(State newState);
State getState() const { return _state; }
bool getComplete() const { return _state == State::Complete; }
QUuid getMarketplaceID() const { return _marketplaceID; }
Error getError() const { return _error; }
bool getFinished() const { return _state == State::Complete || _error != Error::None; }
signals:
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
void completed();
void stateChanged(State newState);
void errorChanged(Error error);
// Triggered when the upload has finished, either succesfully completing, or stopping with an error
void finishedChanged();
private:
void doGetCategories();
void doUploadAvatar();
void doWaitForInventory();
QNetworkReply* _reply;
State _state { State::Idle };
Error _error { Error::None };
QString _title;
QString _description;
QString _rootFilename;
QUuid _marketplaceID;
int _categoryID;
int _itemVersion;
QString _responseData;
int _numRequestsForInventory { 0 };
QString _rootFilePath;
QList<ProjectFilePath> _filePaths;
QByteArray _fileData;
};
#endif // hifi_MarketplaceItemUploader_h

View file

@ -3374,7 +3374,6 @@ void MyAvatar::setCollisionsEnabled(bool enabled) {
QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled));
return;
}
_characterController.setCollisionless(!enabled);
emit collisionsEnabledChanged(enabled);
}
@ -3385,6 +3384,20 @@ bool MyAvatar::getCollisionsEnabled() {
return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS;
}
void MyAvatar::setOtherAvatarsCollisionsEnabled(bool enabled) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setOtherAvatarsCollisionsEnabled", Q_ARG(bool, enabled));
return;
}
_collideWithOtherAvatars = enabled;
emit otherAvatarsCollisionsEnabledChanged(enabled);
}
bool MyAvatar::getOtherAvatarsCollisionsEnabled() {
return _collideWithOtherAvatars;
}
void MyAvatar::updateCollisionCapsuleCache() {
glm::vec3 start, end;
float radius;

View file

@ -225,6 +225,7 @@ class MyAvatar : public Avatar {
Q_PROPERTY(bool centerOfGravityModelEnabled READ getCenterOfGravityModelEnabled WRITE setCenterOfGravityModelEnabled)
Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled)
Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled)
Q_PROPERTY(bool otherAvatarsCollisionsEnabled READ getOtherAvatarsCollisionsEnabled WRITE setOtherAvatarsCollisionsEnabled)
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
Q_PROPERTY(bool showPlayArea READ getShowPlayArea WRITE setShowPlayArea)
@ -1064,6 +1065,18 @@ public:
*/
Q_INVOKABLE bool getCollisionsEnabled();
/**jsdoc
* @function MyAvatar.setOtherAvatarsCollisionsEnabled
* @param {boolean} enabled
*/
Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled);
/**jsdoc
* @function MyAvatar.getOtherAvatarsCollisionsEnabled
* @returns {boolean}
*/
Q_INVOKABLE bool getOtherAvatarsCollisionsEnabled();
/**jsdoc
* @function MyAvatar.getCollisionCapsule
* @returns {object}
@ -1491,6 +1504,14 @@ signals:
*/
void collisionsEnabledChanged(bool enabled);
/**jsdoc
* Triggered when collisions with other avatars enabled or disabled
* @function MyAvatar.otherAvatarsCollisionsEnabledChanged
* @param {boolean} enabled
* @returns {Signal}
*/
void otherAvatarsCollisionsEnabledChanged(bool enabled);
/**jsdoc
* Triggered when avatar's animation url changes
* @function MyAvatar.animGraphUrlChanged

View file

@ -120,7 +120,7 @@ bool OtherAvatar::shouldBeInPhysicsSimulation() const {
}
bool OtherAvatar::needsPhysicsUpdate() const {
constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION;
constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION | Simulation::DIRTY_COLLISION_GROUP;
return (_motionState && (bool)(_motionState->getIncomingDirtyFlags() & FLAGS_OF_INTEREST));
}
@ -129,3 +129,17 @@ void OtherAvatar::rebuildCollisionShape() {
_motionState->addDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS);
}
}
void OtherAvatar::updateCollisionGroup(bool myAvatarCollide) {
if (_motionState) {
bool collides = _motionState->getCollisionGroup() == BULLET_COLLISION_GROUP_OTHER_AVATAR && myAvatarCollide;
if (_collideWithOtherAvatars != collides) {
if (!myAvatarCollide) {
_collideWithOtherAvatars = false;
}
auto newCollisionGroup = _collideWithOtherAvatars ? BULLET_COLLISION_GROUP_OTHER_AVATAR : BULLET_COLLISION_GROUP_COLLISIONLESS;
_motionState->setCollisionGroup(newCollisionGroup);
_motionState->addDirtyFlags(Simulation::DIRTY_COLLISION_GROUP);
}
}
}

View file

@ -45,6 +45,8 @@ public:
bool shouldBeInPhysicsSimulation() const;
bool needsPhysicsUpdate() const;
void updateCollisionGroup(bool myAvatarCollide);
friend AvatarManager;
protected:

View file

@ -549,6 +549,7 @@ protected:
glm::vec3 getBodyRightDirection() const { return getWorldOrientation() * IDENTITY_RIGHT; }
glm::vec3 getBodyUpDirection() const { return getWorldOrientation() * IDENTITY_UP; }
void measureMotionDerivatives(float deltaTime);
bool getCollideWithOtherAvatars() const { return _collideWithOtherAvatars; }
float getSkeletonHeight() const;
float getHeadHeight() const;

View file

@ -540,6 +540,10 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
if (_headData->getHasProceduralBlinkFaceMovement()) {
setAtBit16(flags, PROCEDURAL_BLINK_FACE_MOVEMENT);
}
// avatar collisions enabled
if (_collideWithOtherAvatars) {
setAtBit16(flags, COLLIDE_WITH_OTHER_AVATARS);
}
data->flags = flags;
destinationBuffer += sizeof(AvatarDataPacket::AdditionalFlags);
@ -1116,7 +1120,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
auto newHasAudioEnabledFaceMovement = oneAtBit16(bitItems, AUDIO_ENABLED_FACE_MOVEMENT);
auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT);
auto newHasProceduralBlinkFaceMovement = oneAtBit16(bitItems, PROCEDURAL_BLINK_FACE_MOVEMENT);
auto newCollideWithOtherAvatars = oneAtBit16(bitItems, COLLIDE_WITH_OTHER_AVATARS);
bool keyStateChanged = (_keyState != newKeyState);
bool handStateChanged = (_handState != newHandState);
@ -1125,7 +1129,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
bool audioEnableFaceMovementChanged = (_headData->getHasAudioEnabledFaceMovement() != newHasAudioEnabledFaceMovement);
bool proceduralEyeFaceMovementChanged = (_headData->getHasProceduralEyeFaceMovement() != newHasProceduralEyeFaceMovement);
bool proceduralBlinkFaceMovementChanged = (_headData->getHasProceduralBlinkFaceMovement() != newHasProceduralBlinkFaceMovement);
bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged || proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged;
bool collideWithOtherAvatarsChanged = (_collideWithOtherAvatars != newCollideWithOtherAvatars);
bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged ||
proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged || collideWithOtherAvatarsChanged;
_keyState = newKeyState;
_handState = newHandState;
@ -1134,6 +1140,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
_headData->setHasAudioEnabledFaceMovement(newHasAudioEnabledFaceMovement);
_headData->setHasProceduralEyeFaceMovement(newHasProceduralEyeFaceMovement);
_headData->setHasProceduralBlinkFaceMovement(newHasProceduralBlinkFaceMovement);
_collideWithOtherAvatars = newCollideWithOtherAvatars;
sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags);

View file

@ -110,6 +110,7 @@ const int HAND_STATE_FINGER_POINTING_BIT = 7; // 8th bit
const int AUDIO_ENABLED_FACE_MOVEMENT = 8; // 9th bit
const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit
const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit
const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit
const char HAND_STATE_NULL = 0;
@ -1495,6 +1496,7 @@ protected:
int _replicaIndex { 0 };
bool _isNewAvatar { true };
bool _isClientAvatar { false };
bool _collideWithOtherAvatars { true };
// null unless MyAvatar or ScriptableAvatar sending traits data to mixer
std::unique_ptr<ClientTraitsHandler, LaterDeleter> _clientTraitsHandler;

View file

@ -0,0 +1,13 @@
#ifndef hifi_AvatarProjectFile_h
#define hifi_AvatarProjectFile_h
#include <QObject>
class ProjectFilePath {
Q_GADGET;
public:
QString absolutePath;
QString relativePath;
};
#endif // hifi_AvatarProjectFile_h

190
libraries/fbx/src/FST.cpp Normal file
View file

@ -0,0 +1,190 @@
//
// FST.cpp
//
// Created by Ryan Huffman on 12/11/15.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "FST.h"
#include <QDir>
#include <QFileInfo>
#include <hfm/HFM.h>
constexpr float DEFAULT_SCALE { 1.0f };
FST::FST(QString fstPath, QVariantHash data) : _fstPath(std::move(fstPath)) {
auto setValueFromFSTData = [&data] (const QString& propertyID, auto &targetProperty) mutable {
if (data.contains(propertyID)) {
targetProperty = data[propertyID].toString();
data.remove(propertyID);
}
};
setValueFromFSTData(NAME_FIELD, _name);
setValueFromFSTData(FILENAME_FIELD, _modelPath);
setValueFromFSTData(MARKETPLACE_ID_FIELD, _marketplaceID);
if (data.contains(SCRIPT_FIELD)) {
QVariantList scripts = data.values(SCRIPT_FIELD);
for (const auto& script : scripts) {
_scriptPaths.push_back(script.toString());
}
data.remove(SCRIPT_FIELD);
}
_other = data;
}
FST* FST::createFSTFromModel(const QString& fstPath, const QString& modelFilePath, const hfm::Model& hfmModel) {
QVariantHash mapping;
// mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will
// be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file
bool likelyMixamoFile = hfmModel.applicationName == "mixamo.com" ||
(hfmModel.blendshapeChannelNames.contains("BrowsDown_Right") &&
hfmModel.blendshapeChannelNames.contains("MouthOpen") &&
hfmModel.blendshapeChannelNames.contains("Blink_Left") &&
hfmModel.blendshapeChannelNames.contains("Blink_Right") &&
hfmModel.blendshapeChannelNames.contains("Squint_Right"));
mapping.insert(NAME_FIELD, QFileInfo(fstPath).baseName());
mapping.insert(FILENAME_FIELD, QFileInfo(modelFilePath).fileName());
mapping.insert(TEXDIR_FIELD, "textures");
// mixamo/autodesk defaults
mapping.insert(SCALE_FIELD, DEFAULT_SCALE);
QVariantHash joints = mapping.value(JOINT_FIELD).toHash();
joints.insert("jointEyeLeft", hfmModel.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" :
(hfmModel.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye"));
joints.insert("jointEyeRight", hfmModel.jointIndices.contains("jointEyeRight") ? "jointEyeRight" :
hfmModel.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye");
joints.insert("jointNeck", hfmModel.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck");
joints.insert("jointRoot", "Hips");
joints.insert("jointLean", "Spine");
joints.insert("jointLeftHand", "LeftHand");
joints.insert("jointRightHand", "RightHand");
const char* topName = likelyMixamoFile ? "HeadTop_End" : "HeadEnd";
joints.insert("jointHead", hfmModel.jointIndices.contains(topName) ? topName : "Head");
mapping.insert(JOINT_FIELD, joints);
QVariantHash jointIndices;
for (int i = 0; i < hfmModel.joints.size(); i++) {
jointIndices.insert(hfmModel.joints.at(i).name, QString::number(i));
}
mapping.insert(JOINT_INDEX_FIELD, jointIndices);
mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm");
mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm");
mapping.insertMulti(FREE_JOINT_FIELD, "RightArm");
mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm");
// If there are no blendshape mappings, and we detect that this is likely a mixamo file,
// then we can add the default mixamo to "faceshift" mappings
if (likelyMixamoFile) {
QVariantHash blendshapes;
blendshapes.insertMulti("BrowsD_L", QVariantList() << "BrowsDown_Left" << 1.0);
blendshapes.insertMulti("BrowsD_R", QVariantList() << "BrowsDown_Right" << 1.0);
blendshapes.insertMulti("BrowsU_C", QVariantList() << "BrowsUp_Left" << 1.0);
blendshapes.insertMulti("BrowsU_C", QVariantList() << "BrowsUp_Right" << 1.0);
blendshapes.insertMulti("BrowsU_L", QVariantList() << "BrowsUp_Left" << 1.0);
blendshapes.insertMulti("BrowsU_R", QVariantList() << "BrowsUp_Right" << 1.0);
blendshapes.insertMulti("ChinLowerRaise", QVariantList() << "Jaw_Up" << 1.0);
blendshapes.insertMulti("ChinUpperRaise", QVariantList() << "UpperLipUp_Left" << 0.5);
blendshapes.insertMulti("ChinUpperRaise", QVariantList() << "UpperLipUp_Right" << 0.5);
blendshapes.insertMulti("EyeBlink_L", QVariantList() << "Blink_Left" << 1.0);
blendshapes.insertMulti("EyeBlink_R", QVariantList() << "Blink_Right" << 1.0);
blendshapes.insertMulti("EyeOpen_L", QVariantList() << "EyesWide_Left" << 1.0);
blendshapes.insertMulti("EyeOpen_R", QVariantList() << "EyesWide_Right" << 1.0);
blendshapes.insertMulti("EyeSquint_L", QVariantList() << "Squint_Left" << 1.0);
blendshapes.insertMulti("EyeSquint_R", QVariantList() << "Squint_Right" << 1.0);
blendshapes.insertMulti("JawFwd", QVariantList() << "JawForeward" << 1.0);
blendshapes.insertMulti("JawLeft", QVariantList() << "JawRotateY_Left" << 0.5);
blendshapes.insertMulti("JawOpen", QVariantList() << "MouthOpen" << 0.7);
blendshapes.insertMulti("JawRight", QVariantList() << "Jaw_Right" << 1.0);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "JawForeward" << 0.39);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "Jaw_Down" << 0.36);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthNarrow_Left" << 1.0);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthNarrow_Right" << 1.0);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthWhistle_NarrowAdjust_Left" << 0.5);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthWhistle_NarrowAdjust_Right" << 0.5);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "TongueUp" << 1.0);
blendshapes.insertMulti("LipsLowerClose", QVariantList() << "LowerLipIn" << 1.0);
blendshapes.insertMulti("LipsLowerDown", QVariantList() << "LowerLipDown_Left" << 0.7);
blendshapes.insertMulti("LipsLowerDown", QVariantList() << "LowerLipDown_Right" << 0.7);
blendshapes.insertMulti("LipsLowerOpen", QVariantList() << "LowerLipOut" << 1.0);
blendshapes.insertMulti("LipsPucker", QVariantList() << "MouthNarrow_Left" << 1.0);
blendshapes.insertMulti("LipsPucker", QVariantList() << "MouthNarrow_Right" << 1.0);
blendshapes.insertMulti("LipsUpperClose", QVariantList() << "UpperLipIn" << 1.0);
blendshapes.insertMulti("LipsUpperOpen", QVariantList() << "UpperLipOut" << 1.0);
blendshapes.insertMulti("LipsUpperUp", QVariantList() << "UpperLipUp_Left" << 0.7);
blendshapes.insertMulti("LipsUpperUp", QVariantList() << "UpperLipUp_Right" << 0.7);
blendshapes.insertMulti("MouthDimple_L", QVariantList() << "Smile_Left" << 0.25);
blendshapes.insertMulti("MouthDimple_R", QVariantList() << "Smile_Right" << 0.25);
blendshapes.insertMulti("MouthFrown_L", QVariantList() << "Frown_Left" << 1.0);
blendshapes.insertMulti("MouthFrown_R", QVariantList() << "Frown_Right" << 1.0);
blendshapes.insertMulti("MouthLeft", QVariantList() << "Midmouth_Left" << 1.0);
blendshapes.insertMulti("MouthRight", QVariantList() << "Midmouth_Right" << 1.0);
blendshapes.insertMulti("MouthSmile_L", QVariantList() << "Smile_Left" << 1.0);
blendshapes.insertMulti("MouthSmile_R", QVariantList() << "Smile_Right" << 1.0);
blendshapes.insertMulti("Puff", QVariantList() << "CheekPuff_Left" << 1.0);
blendshapes.insertMulti("Puff", QVariantList() << "CheekPuff_Right" << 1.0);
blendshapes.insertMulti("Sneer", QVariantList() << "NoseScrunch_Left" << 0.75);
blendshapes.insertMulti("Sneer", QVariantList() << "NoseScrunch_Right" << 0.75);
blendshapes.insertMulti("Sneer", QVariantList() << "Squint_Left" << 0.5);
blendshapes.insertMulti("Sneer", QVariantList() << "Squint_Right" << 0.5);
mapping.insert(BLENDSHAPE_FIELD, blendshapes);
}
return new FST(fstPath, mapping);
}
QString FST::absoluteModelPath() const {
QFileInfo fileInfo{ _fstPath };
QDir dir{ fileInfo.absoluteDir() };
return dir.absoluteFilePath(_modelPath);
}
void FST::setName(const QString& name) {
_name = name;
emit nameChanged(name);
}
void FST::setModelPath(const QString& modelPath) {
_modelPath = modelPath;
emit modelPathChanged(modelPath);
}
QVariantHash FST::getMapping() const {
QVariantHash mapping;
mapping.unite(_other);
mapping.insert(NAME_FIELD, _name);
mapping.insert(FILENAME_FIELD, _modelPath);
mapping.insert(MARKETPLACE_ID_FIELD, _marketplaceID);
for (const auto& scriptPath : _scriptPaths) {
mapping.insertMulti(SCRIPT_FIELD, scriptPath);
}
return mapping;
}
bool FST::write() {
QFile fst(_fstPath);
if (!fst.open(QIODevice::WriteOnly)) {
return false;
}
fst.write(FSTReader::writeMapping(getMapping()));
return true;
}
void FST::setMarketplaceID(QUuid marketplaceID) {
_marketplaceID = marketplaceID;
emit marketplaceIDChanged();
}

71
libraries/fbx/src/FST.h Normal file
View file

@ -0,0 +1,71 @@
//
// FST.h
//
// Created by Ryan Huffman on 12/11/15.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_FST_h
#define hifi_FST_h
#include <QVariantHash>
#include <QUuid>
#include "FSTReader.h"
namespace hfm {
class Model;
};
class FST : public QObject {
Q_OBJECT
Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameChanged)
Q_PROPERTY(QString modelPath READ getModelPath WRITE setModelPath NOTIFY modelPathChanged)
Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID)
Q_PROPERTY(bool hasMarketplaceID READ getHasMarketplaceID NOTIFY marketplaceIDChanged)
public:
FST(QString fstPath, QVariantHash data);
static FST* createFSTFromModel(const QString& fstPath, const QString& modelFilePath, const hfm::Model& hfmModel);
QString absoluteModelPath() const;
QString getName() const { return _name; }
void setName(const QString& name);
QString getModelPath() const { return _modelPath; }
void setModelPath(const QString& modelPath);
Q_INVOKABLE bool getHasMarketplaceID() const { return !_marketplaceID.isNull(); }
QUuid getMarketplaceID() const { return _marketplaceID; }
void setMarketplaceID(QUuid marketplaceID);
QStringList getScriptPaths() const { return _scriptPaths; }
void setScriptPaths(QStringList scriptPaths) { _scriptPaths = scriptPaths; }
QString getPath() const { return _fstPath; }
QVariantHash getMapping() const;
bool write();
signals:
void nameChanged(const QString& name);
void modelPathChanged(const QString& modelPath);
void marketplaceIDChanged();
private:
QString _fstPath;
QString _name{};
QString _modelPath{};
QUuid _marketplaceID{};
QStringList _scriptPaths{};
QVariantHash _other{};
};
#endif // hifi_FST_h

View file

@ -84,7 +84,7 @@ void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it)
QByteArray FSTReader::writeMapping(const QVariantHash& mapping) {
static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << TYPE_FIELD << SCALE_FIELD << FILENAME_FIELD
<< TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << FREE_JOINT_FIELD
<< MARKETPLACE_ID_FIELD << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << FREE_JOINT_FIELD
<< BLENDSHAPE_FIELD << JOINT_INDEX_FIELD;
QBuffer buffer;
buffer.open(QIODevice::WriteOnly);

View file

@ -18,6 +18,7 @@
static const QString NAME_FIELD = "name";
static const QString TYPE_FIELD = "type";
static const QString FILENAME_FIELD = "filename";
static const QString MARKETPLACE_ID_FIELD = "marketplaceID";
static const QString TEXDIR_FIELD = "texdir";
static const QString LOD_FIELD = "lod";
static const QString JOINT_INDEX_FIELD = "jointIndex";

View file

@ -208,6 +208,44 @@ void AccountManager::setSessionID(const QUuid& sessionID) {
}
}
QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType) {
QNetworkRequest networkRequest;
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter());
networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER,
uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit());
QUrl requestURL = _authURL;
if (requestURL.isEmpty()) { // Assignment client doesn't set _authURL.
requestURL = getMetaverseServerURL();
}
if (path.startsWith("/")) {
requestURL.setPath(path);
} else {
requestURL.setPath("/" + path);
}
if (authType != AccountManagerAuth::None ) {
if (hasValidAccessToken()) {
networkRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER,
_accountInfo.getAccessToken().authorizationHeaderValue());
} else {
if (authType == AccountManagerAuth::Required) {
qCDebug(networking) << "No valid access token present. Bailing on invoked request to"
<< path << "that requires authentication";
return QNetworkRequest();
}
}
}
networkRequest.setUrl(requestURL);
return networkRequest;
}
void AccountManager::sendRequest(const QString& path,
AccountManagerAuth::Type authType,
QNetworkAccessManager::Operation operation,
@ -231,46 +269,10 @@ void AccountManager::sendRequest(const QString& path,
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter());
networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER,
uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit());
QUrl requestURL = _authURL;
if (requestURL.isEmpty()) { // Assignment client doesn't set _authURL.
requestURL = getMetaverseServerURL();
}
if (path.startsWith("/")) {
requestURL.setPath(path);
} else {
requestURL.setPath("/" + path);
}
if (!query.isEmpty()) {
requestURL.setQuery(query);
}
if (authType != AccountManagerAuth::None ) {
if (hasValidAccessToken()) {
networkRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER,
_accountInfo.getAccessToken().authorizationHeaderValue());
} else {
if (authType == AccountManagerAuth::Required) {
qCDebug(networking) << "No valid access token present. Bailing on invoked request to"
<< path << "that requires authentication";
return;
}
}
}
networkRequest.setUrl(requestURL);
QNetworkRequest networkRequest = createRequest(path, authType);
if (VERBOSE_HTTP_REQUEST_DEBUGGING) {
qCDebug(networking) << "Making a request to" << qPrintable(requestURL.toString());
qCDebug(networking) << "Making a request to" << qPrintable(networkRequest.url().toString());
if (!dataByteArray.isEmpty()) {
qCDebug(networking) << "The POST/PUT body -" << QString(dataByteArray);

View file

@ -28,7 +28,8 @@
class JSONCallbackParameters {
public:
JSONCallbackParameters(QObject* callbackReceiver = nullptr, const QString& jsonCallbackMethod = QString(),
JSONCallbackParameters(QObject* callbackReceiver = nullptr,
const QString& jsonCallbackMethod = QString(),
const QString& errorCallbackMethod = QString());
bool isEmpty() const { return !callbackReceiver; }
@ -39,11 +40,11 @@ public:
};
namespace AccountManagerAuth {
enum Type {
None,
Required,
Optional
};
enum Type {
None,
Required,
Optional,
};
}
Q_DECLARE_METATYPE(AccountManagerAuth::Type);
@ -60,6 +61,7 @@ class AccountManager : public QObject, public Dependency {
public:
AccountManager(UserAgentGetter userAgentGetter = DEFAULT_USER_AGENT_GETTER);
QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType);
Q_INVOKABLE void sendRequest(const QString& path,
AccountManagerAuth::Type authType,
QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation,
@ -84,7 +86,7 @@ public:
void requestProfile();
DataServerAccountInfo& getAccountInfo() { return _accountInfo; }
void setAccountInfo(const DataServerAccountInfo &newAccountInfo);
void setAccountInfo(const DataServerAccountInfo& newAccountInfo);
static QJsonObject dataObjectFromResponse(QNetworkReply* requestReply);
@ -104,7 +106,10 @@ public:
public slots:
void requestAccessToken(const QString& login, const QString& password);
void requestAccessTokenWithSteam(QByteArray authSessionTicket);
void requestAccessTokenWithAuthCode(const QString& authCode, const QString& clientId, const QString& clientSecret, const QString& redirectUri);
void requestAccessTokenWithAuthCode(const QString& authCode,
const QString& clientId,
const QString& clientSecret,
const QString& redirectUri);
void refreshAccessToken();
void requestAccessTokenFinished();
@ -159,4 +164,4 @@ private:
bool _limitedCommerce { false };
};
#endif // hifi_AccountManager_h
#endif // hifi_AccountManager_h

View file

@ -113,8 +113,6 @@ LimitedNodeList::LimitedNodeList(int socketListenPort, int dtlsListenPort) :
// handle when a socket connection has its receiver side reset - might need to emit clientConnectionToNodeReset
connect(&_nodeSocket, &udt::Socket::clientHandshakeRequestComplete, this, &LimitedNodeList::clientConnectionToSockAddrReset);
_packetStatTimer.start();
if (_stunSockAddr.getAddress().isNull()) {
// we don't know the stun server socket yet, add it to unfiltered once known
connect(&_stunSockAddr, &HifiSockAddr::lookupCompleted, this, &LimitedNodeList::addSTUNHandlerToUnfiltered);
@ -378,12 +376,6 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe
return false;
}
void LimitedNodeList::collectPacketStats(const NLPacket& packet) {
// stat collection for packets
++_numCollectedPackets;
_numCollectedBytes += packet.getDataSize();
}
void LimitedNodeList::fillPacketHeader(const NLPacket& packet, HMACAuth* hmacAuth) {
if (!PacketTypeEnum::getNonSourcedPackets().contains(packet.getType())) {
packet.writeSourceID(getSessionLocalID());
@ -414,7 +406,6 @@ qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const HifiS
Q_ASSERT_X(!packet.isReliable(), "LimitedNodeList::sendUnreliablePacket",
"Trying to send a reliable packet unreliably.");
collectPacketStats(packet);
fillPacketHeader(packet, hmacAuth);
return _nodeSocket.writePacket(packet, sockAddr);
@ -436,7 +427,6 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr<NLPacket> packet, const HifiS
HMACAuth* hmacAuth) {
Q_ASSERT(!packet->isPartOfMessage());
if (packet->isReliable()) {
collectPacketStats(*packet);
fillPacketHeader(*packet, hmacAuth);
auto size = packet->getDataSize();
@ -490,7 +480,6 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr<NLPacketList> packetList,
for (std::unique_ptr<udt::Packet>& packet : packetList->_packets) {
NLPacket* nlPacket = static_cast<NLPacket*>(packet.get());
collectPacketStats(*nlPacket);
fillPacketHeader(*nlPacket);
}
@ -505,7 +494,6 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr<NLPacketList> packetList,
for (std::unique_ptr<udt::Packet>& packet : packetList->_packets) {
NLPacket* nlPacket = static_cast<NLPacket*>(packet.get());
collectPacketStats(*nlPacket);
fillPacketHeader(*nlPacket, destinationNode.getAuthenticateHash());
}
@ -832,23 +820,6 @@ SharedNodePointer LimitedNodeList::soloNodeOfType(NodeType_t nodeType) {
});
}
void LimitedNodeList::getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond) {
packetsInPerSecond = (float) getPacketReceiver().getInPacketCount() / ((float) _packetStatTimer.elapsed() / 1000.0f);
bytesInPerSecond = (float) getPacketReceiver().getInByteCount() / ((float) _packetStatTimer.elapsed() / 1000.0f);
packetsOutPerSecond = (float) _numCollectedPackets / ((float) _packetStatTimer.elapsed() / 1000.0f);
bytesOutPerSecond = (float) _numCollectedBytes / ((float) _packetStatTimer.elapsed() / 1000.0f);
}
void LimitedNodeList::resetPacketStats() {
getPacketReceiver().resetCounters();
_numCollectedPackets = 0;
_numCollectedBytes = 0;
_packetStatTimer.restart();
}
void LimitedNodeList::removeSilentNodes() {
QSet<SharedNodePointer> killedNodes;

View file

@ -183,9 +183,6 @@ public:
unsigned int broadcastToNodes(std::unique_ptr<NLPacket> packet, const NodeSet& destinationNodeTypes);
SharedNodePointer soloNodeOfType(NodeType_t nodeType);
void getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond);
void resetPacketStats();
std::unique_ptr<NLPacket> constructPingPacket(const QUuid& nodeId, PingType_t pingType = PingType::Agnostic);
std::unique_ptr<NLPacket> constructPingReplyPacket(ReceivedMessage& message);
@ -377,7 +374,6 @@ protected:
qint64 sendPacket(std::unique_ptr<NLPacket> packet, const Node& destinationNode,
const HifiSockAddr& overridenSockAddr);
void collectPacketStats(const NLPacket& packet);
void fillPacketHeader(const NLPacket& packet, HMACAuth* hmacAuth = nullptr);
void setLocalSocket(const HifiSockAddr& sockAddr);
@ -406,10 +402,6 @@ protected:
PacketReceiver* _packetReceiver;
std::atomic<int> _numCollectedPackets { 0 };
std::atomic<int> _numCollectedBytes { 0 };
QElapsedTimer _packetStatTimer;
NodePermissions _permissions;
QPointer<QTimer> _initialSTUNTimer;

View file

@ -212,18 +212,12 @@ void PacketReceiver::handleVerifiedPacket(std::unique_ptr<udt::Packet> packet) {
auto nlPacket = NLPacket::fromBase(std::move(packet));
auto receivedMessage = QSharedPointer<ReceivedMessage>::create(*nlPacket);
_inPacketCount += 1;
_inByteCount += nlPacket->size();
handleVerifiedMessage(receivedMessage, true);
}
void PacketReceiver::handleVerifiedMessagePacket(std::unique_ptr<udt::Packet> packet) {
auto nlPacket = NLPacket::fromBase(std::move(packet));
_inPacketCount += 1;
_inByteCount += nlPacket->size();
auto key = std::pair<HifiSockAddr, udt::Packet::MessageNumber>(nlPacket->getSenderSockAddr(), nlPacket->getMessageNumber());
auto it = _pendingMessages.find(key);
QSharedPointer<ReceivedMessage> message;

View file

@ -49,13 +49,8 @@ public:
PacketReceiver(const PacketReceiver&) = delete;
PacketReceiver& operator=(const PacketReceiver&) = delete;
int getInPacketCount() const { return _inPacketCount; }
int getInByteCount() const { return _inByteCount; }
void setShouldDropPackets(bool shouldDropPackets) { _shouldDropPackets = shouldDropPackets; }
void resetCounters() { _inPacketCount = 0; _inByteCount = 0; }
// If deliverPending is false, ReceivedMessage will only be delivered once all packets for the message have
// been received. If deliverPending is true, ReceivedMessage will be delivered as soon as the first packet
@ -87,8 +82,7 @@ private:
QMutex _packetListenerLock;
QHash<PacketType, Listener> _messageListenerMap;
int _inPacketCount = 0;
int _inByteCount = 0;
bool _shouldDropPackets = false;
QMutex _directConnectSetMutex;
QSet<QObject*> _directlyConnectedObjects;

View file

@ -94,15 +94,11 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy
void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObject) {
auto nodeList = DependencyManager::get<NodeList>();
float packetsInPerSecond, bytesInPerSecond, packetsOutPerSecond, bytesOutPerSecond;
nodeList->getPacketStats(packetsInPerSecond, bytesInPerSecond, packetsOutPerSecond, bytesOutPerSecond);
nodeList->resetPacketStats();
QJsonObject ioStats;
ioStats["inbound_bytes_per_s"] = bytesInPerSecond;
ioStats["inbound_packets_per_s"] = packetsInPerSecond;
ioStats["outbound_bytes_per_s"] = bytesOutPerSecond;
ioStats["outbound_packets_per_s"] = packetsOutPerSecond;
ioStats["inbound_kbps"] = nodeList->getInboundKbps();
ioStats["inbound_pps"] = nodeList->getInboundPPS();
ioStats["outbound_kbps"] = nodeList->getOutboundKbps();
ioStats["outbound_pps"] = nodeList->getOutboundPPS();
statsObject["io_stats"] = ioStats;

View file

@ -38,6 +38,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
return static_cast<PacketVersion>(EntityQueryPacketVersion::ConicalFrustums);
case PacketType::AvatarIdentity:
case PacketType::AvatarData:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::CollisionFlag);
case PacketType::BulkAvatarData:
case PacketType::KillAvatar:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::GrabTraits);

View file

@ -306,7 +306,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
MigrateAvatarEntitiesToTraits,
FarGrabJointsRedux,
JointTransScaled,
GrabTraits
GrabTraits,
CollisionFlag
};
enum class DomainConnectRequestVersion : PacketVersion {

View file

@ -62,7 +62,8 @@ function getMyAvatar() {
function getMyAvatarSettings() {
return {
dominantHand: MyAvatar.getDominantHand(),
collisionsEnabled : MyAvatar.getCollisionsEnabled(),
collisionsEnabled: MyAvatar.getCollisionsEnabled(),
otherAvatarsCollisionsEnabled: MyAvatar.getOtherAvatarsCollisionsEnabled(),
collisionSoundUrl : MyAvatar.collisionSoundURL,
animGraphUrl: MyAvatar.getAnimGraphUrl(),
animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(),
@ -135,6 +136,13 @@ function onCollisionsEnabledChanged(enabled) {
}
}
function onOtherAvatarsCollisionsEnabledChanged(enabled) {
if (currentAvatarSettings.otherAvatarsCollisionsEnabled !== enabled) {
currentAvatarSettings.otherAvatarsCollisionsEnabled = enabled;
sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled })
}
}
function onNewCollisionSoundUrl(url) {
if(currentAvatarSettings.collisionSoundUrl !== url) {
currentAvatarSettings.collisionSoundUrl = url;
@ -323,6 +331,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
currentAvatar.avatarScale = message.avatarScale;
MyAvatar.setDominantHand(message.settings.dominantHand);
MyAvatar.setOtherAvatarsCollisionsEnabled(message.settings.otherAvatarsCollisionsEnabled);
MyAvatar.setCollisionsEnabled(message.settings.collisionsEnabled);
MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl;
MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl);
@ -513,6 +522,7 @@ function off() {
MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged);
MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged);
MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged);
MyAvatar.otherAvatarsCollisionsEnabledChanged.disconnect(onOtherAvatarsCollisionsEnabledChanged);
MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl);
MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged);
MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged);
@ -533,6 +543,7 @@ function on() {
MyAvatar.skeletonModelURLChanged.connect(onSkeletonModelURLChanged);
MyAvatar.dominantHandChanged.connect(onDominantHandChanged);
MyAvatar.collisionsEnabledChanged.connect(onCollisionsEnabledChanged);
MyAvatar.otherAvatarsCollisionsEnabledChanged.connect(onOtherAvatarsCollisionsEnabledChanged);
MyAvatar.newCollisionSoundURL.connect(onNewCollisionSoundUrl);
MyAvatar.animGraphUrlChanged.connect(onAnimGraphUrlChanged);
MyAvatar.targetScaleChanged.connect(onTargetScaleChanged);

View file

@ -476,24 +476,33 @@ void AWSInterface::updateAWS() {
QStringList parts = nextDirectory.split('/');
QString filename = parts[parts.length() - 3] + "/" + parts[parts.length() - 2] + "/" + parts[parts.length() - 1];
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Actual Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n";
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Expected Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
// The directory may contain either 'Result.txt', or 3 images (and a text file named 'TestResults.txt' that is not used)
if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Result.txt")) {
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Difference Image.png"
<< "', 'rb')\n";
<< "Result.txt"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Result.txt" << "', Body=data)\n\n";
} else {
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Actual Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n";
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Expected Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Difference Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n";
}
}
}
@ -510,31 +519,39 @@ void AWSInterface::updateAWS() {
// We need to concatenate the last 3 components, to get `TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]/successes/engine.render.effect.bloom.00000`
QStringList parts = nextDirectory.split('/');
QString filename = parts[parts.length() - 3] + "/" + parts[parts.length() - 2] + "/" + parts[parts.length() - 1];
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Actual Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n";
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Expected Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
// The directory may contain either 'Result.txt', or 3 images (and a text file named 'TestResults.txt' that is not used)
if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Result.txt")) {
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Difference Image.png"
<< "', 'rb')\n";
<< "Result.txt"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Result.txt" << "', Body=data)\n\n";
} else {
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Actual Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n";
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Expected Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Difference Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n";
}
}
}
stream << "data = open('" << _workingDirectory << "/" << _resultsFolder << "/" << HTML_FILENAME << "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << _resultsFolder << "/"
<< HTML_FILENAME << "', Body=data, ContentType='text/html')\n";
<< HTML_FILENAME << "', Body=data, ContentType='text/html')\n";
file.close();
@ -548,7 +565,7 @@ void AWSInterface::updateAWS() {
connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); });
connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
[=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); });
[=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); });
#ifdef Q_OS_WIN
QStringList parameters = QStringList() << filename;

View file

@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
_ui.statusLabel->setText("");
_ui.plainTextEdit->setReadOnly(true);
setWindowTitle("Nitpick - v1.3");
setWindowTitle("Nitpick - v1.3.2");
}
Nitpick::~Nitpick() {