Merge branch 'master' into propFix

This commit is contained in:
Sam Gondelman 2019-01-07 14:57:38 -08:00 committed by GitHub
commit db3187ec6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 4298 additions and 869 deletions

1
.gitignore vendored
View file

@ -98,6 +98,7 @@ tools/jsdoc/package-lock.json
# ignore unneeded unity project files for avatar exporter
tools/unity-avatar-exporter/Library
tools/unity-avatar-exporter/Logs
tools/unity-avatar-exporter/Packages
tools/unity-avatar-exporter/ProjectSettings
tools/unity-avatar-exporter/Temp

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"
@ -170,6 +171,7 @@
#include "scripting/Audio.h"
#include "networking/CloseEventSender.h"
#include "scripting/TestScriptingInterface.h"
#include "scripting/PlatformInfoScriptingInterface.h"
#include "scripting/AssetMappingsScriptingInterface.h"
#include "scripting/ClipboardScriptingInterface.h"
#include "scripting/DesktopScriptingInterface.h"
@ -921,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;
}
@ -2283,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);
@ -2616,6 +2619,7 @@ void Application::cleanupBeforeQuit() {
DependencyManager::destroy<PickManager>();
DependencyManager::destroy<KeyboardScriptingInterface>();
DependencyManager::destroy<Keyboard>();
DependencyManager::destroy<AvatarPackager>();
qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete";
}
@ -6988,6 +6992,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe
scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance());
}
scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance());
scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this));
// hook our avatar and avatar hash map object into this script engine
@ -8705,6 +8710,14 @@ void Application::updateLoginDialogOverlayPosition() {
}
}
bool Application::hasRiftControllers() {
return PluginUtils::isOculusTouchControllerAvailable();
}
bool Application::hasViveControllers() {
return PluginUtils::isViveControllerAvailable();
}
void Application::onDismissedLoginDialog() {
_loginDialogPoppedUp = false;
loginDialogPoppedUp.set(false);
@ -8923,6 +8936,10 @@ void Application::copyToClipboard(const QString& text) {
QApplication::clipboard()->setText(text);
}
QString Application::getGraphicsCardType() {
return GPUIdent::getInstance()->getName();
}
#if defined(Q_OS_ANDROID)
void Application::beforeEnterBackground() {
auto nodeList = DependencyManager::get<NodeList>();

View file

@ -326,6 +326,10 @@ public:
void createLoginDialogOverlay();
void updateLoginDialogOverlayPosition();
// Check if a headset is connected
bool hasRiftControllers();
bool hasViveControllers();
#if defined(Q_OS_ANDROID)
void beforeEnterBackground();
void enterBackground();
@ -459,6 +463,8 @@ public slots:
void changeViewAsNeeded(float boomLength);
QString getGraphicsCardType();
private slots:
void onDesktopRootItemCreated(QQuickItem* qmlContext);
void onDesktopRootContextCreated(QQmlContext* qmlContext);
@ -787,6 +793,5 @@ private:
bool _showTrackedObjects { false };
bool _prevShowTrackedObjects { false };
};
#endif // hifi_Application_h

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

@ -0,0 +1,135 @@
//
// Created by Nissim Hadar on 2018/12/28
// Copyright 2013-2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "PlatformInfoScriptingInterface.h"
#include "Application.h"
#include <thread>
#ifdef Q_OS_WIN
#include <Windows.h>
#elif defined Q_OS_MAC
#include <sstream>
#endif
PlatformInfoScriptingInterface* PlatformInfoScriptingInterface::getInstance() {
static PlatformInfoScriptingInterface sharedInstance;
return &sharedInstance;
}
QString PlatformInfoScriptingInterface::getOperatingSystemType() {
#ifdef Q_OS_WIN
return "WINDOWS";
#elif defined Q_OS_MAC
return "MACOS";
#else
return "UNKNOWN";
#endif
}
QString PlatformInfoScriptingInterface::getCPUBrand() {
#ifdef Q_OS_WIN
int CPUInfo[4] = { -1 };
unsigned nExIds, i = 0;
char CPUBrandString[0x40];
// Get the information associated with each extended ID.
__cpuid(CPUInfo, 0x80000000);
nExIds = CPUInfo[0];
for (i = 0x80000000; i <= nExIds; ++i) {
__cpuid(CPUInfo, i);
// Interpret CPU brand string
if (i == 0x80000002) {
memcpy(CPUBrandString, CPUInfo, sizeof(CPUInfo));
} else if (i == 0x80000003) {
memcpy(CPUBrandString + 16, CPUInfo, sizeof(CPUInfo));
} else if (i == 0x80000004) {
memcpy(CPUBrandString + 32, CPUInfo, sizeof(CPUInfo));
}
}
return CPUBrandString;
#elif defined Q_OS_MAC
FILE* stream = popen("sysctl -n machdep.cpu.brand_string", "r");
std::ostringstream hostStream;
while (!feof(stream) && !ferror(stream)) {
char buf[128];
int bytesRead = fread(buf, 1, 128, stream);
hostStream.write(buf, bytesRead);
}
return QString::fromStdString(hostStream.str());
#else
return QString("NO IMPLEMENTED");
#endif
}
unsigned int PlatformInfoScriptingInterface::getNumLogicalCores() {
return std::thread::hardware_concurrency();
}
int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() {
#ifdef Q_OS_WIN
MEMORYSTATUSEX statex;
statex.dwLength = sizeof (statex);
GlobalMemoryStatusEx(&statex);
return statex.ullTotalPhys / 1024 / 1024;
#elif defined Q_OS_MAC
FILE* stream = popen("sysctl -a | grep hw.memsize", "r");
std::ostringstream hostStream;
while (!feof(stream) && !ferror(stream)) {
char buf[128];
int bytesRead = fread(buf, 1, 128, stream);
hostStream.write(buf, bytesRead);
}
QString result = QString::fromStdString(hostStream.str());
QStringList parts = result.split(' ');
return (int)(parts[1].toDouble() / 1024 / 1024);
#else
return -1;
#endif
}
QString PlatformInfoScriptingInterface::getGraphicsCardType() {
#ifdef Q_OS_WIN
return qApp->getGraphicsCardType();
#elif defined Q_OS_MAC
FILE* stream = popen("system_profiler SPDisplaysDataType | grep Chipset", "r");
std::ostringstream hostStream;
while (!feof(stream) && !ferror(stream)) {
char buf[128];
int bytesRead = fread(buf, 1, 128, stream);
hostStream.write(buf, bytesRead);
}
QString result = QString::fromStdString(hostStream.str());
QStringList parts = result.split('\n');
for (int i = 0; i < parts.size(); ++i) {
if (parts[i].toLower().contains("radeon") || parts[i].toLower().contains("nvidia")) {
return parts[i];
}
}
// unkown graphics card
return "UNKNOWN";
#else
return QString("NO IMPLEMENTED");
#endif
}
bool PlatformInfoScriptingInterface::hasRiftControllers() {
return qApp->hasRiftControllers();
}
bool PlatformInfoScriptingInterface::hasViveControllers() {
return qApp->hasViveControllers();
}

View file

@ -0,0 +1,70 @@
//
// Created by Nissim Hadar on 2018/12/28
// Copyright 2013-2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_PlatformInfoScriptingInterface_h
#define hifi_PlatformInfoScriptingInterface_h
#include <QtCore/QObject>
class QScriptValue;
class PlatformInfoScriptingInterface : public QObject {
Q_OBJECT
public slots:
static PlatformInfoScriptingInterface* getInstance();
/**jsdoc
* Returns the Operating Sytem type
* @function Test.getOperatingSystemType
* @returns {string} "WINDOWS", "MACOS" or "UNKNOWN"
*/
QString getOperatingSystemType();
/**jsdoc
* Returns the CPU brand
*function PlatformInfo.getCPUBrand()
* @returns {string} brand of CPU
*/
QString getCPUBrand();
/**jsdoc
* Returns the number of logical CPU cores
*function PlatformInfo.getNumLogicalCores()
* @returns {int} number of logical CPU cores
*/
unsigned int getNumLogicalCores();
/**jsdoc
* Returns the total system memory in megabyte
*function PlatformInfo.getTotalSystemMemory()
* @returns {int} size of memory in megabytes
*/
int getTotalSystemMemoryMB();
/**jsdoc
* Returns the graphics card type
* @function Test.getGraphicsCardType
* @returns {string} graphics card type
*/
QString getGraphicsCardType();
/**jsdoc
* Returns true if Oculus Rift is connected (looks for hand controllers)
* @function Window.hasRift
* @returns {boolean} <code>true</code> if running on Windows, otherwise <code>false</code>.*/
bool hasRiftControllers();
/**jsdoc
* Returns true if HTC Vive is connected (looks for hand controllers)
* @function Window.hasRift
* @returns {boolean} <code>true</code> if running on Windows, otherwise <code>false</code>.*/
bool hasViveControllers();
};
#endif // hifi_PlatformInfoScriptingInterface_h

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

View file

@ -19,66 +19,39 @@
#include <PerfStat.h>
#include <shaders/Shaders.h>
//#define POLYLINE_ENTITY_USE_FADE_EFFECT
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
# include <FadeEffect.h>
#endif
#include "paintStroke_Shared.slh"
using namespace render;
using namespace render::entities;
static uint8_t CUSTOM_PIPELINE_NUMBER { 0 };
static const int32_t PAINTSTROKE_TEXTURE_SLOT { 0 };
static gpu::Stream::FormatPointer polylineFormat;
static gpu::PipelinePointer polylinePipeline;
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
static gpu::PipelinePointer polylineFadePipeline;
#endif
gpu::PipelinePointer PolyLineEntityRenderer::_pipeline = nullptr;
static render::ShapePipelinePointer shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key, gpu::Batch& batch) {
if (!polylinePipeline) {
gpu::ShaderPointer program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke);
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
auto fadeVS = gpu::Shader::createVertex(std::string(paintStroke_fade_vert));
auto fadePS = gpu::Shader::createPixel(std::string(paintStroke_fade_frag));
gpu::ShaderPointer fadeProgram = gpu::Shader::createProgram(fadeVS, fadePS);
#endif
gpu::StatePointer state = gpu::StatePointer(new gpu::State());
state->setDepthTest(true, true, gpu::LESS_EQUAL);
PrepareStencil::testMask(*state);
state->setBlendFunction(true,
gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
polylinePipeline = gpu::Pipeline::create(program, state);
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
_fadePipeline = gpu::Pipeline::create(fadeProgram, state);
#endif
}
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
if (key.isFaded()) {
auto fadeEffect = DependencyManager::get<FadeEffect>();
return std::make_shared<render::ShapePipeline>(_fadePipeline, nullptr, fadeEffect->getBatchSetter(), fadeEffect->getItemUniformSetter());
} else {
#endif
return std::make_shared<render::ShapePipeline>(polylinePipeline, nullptr, nullptr, nullptr);
#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT
}
#endif
}
static const QUrl DEFAULT_POLYLINE_TEXTURE = PathUtils::resourcesUrl("images/paintStroke.png");
PolyLineEntityRenderer::PolyLineEntityRenderer(const EntityItemPointer& entity) : Parent(entity) {
static std::once_flag once;
std::call_once(once, [&] {
CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory);
polylineFormat.reset(new gpu::Stream::Format());
polylineFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), offsetof(Vertex, position));
polylineFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), offsetof(Vertex, normal));
polylineFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), offsetof(Vertex, uv));
polylineFormat->setAttribute(gpu::Stream::COLOR, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB), offsetof(Vertex, color));
});
_texture = DependencyManager::get<TextureCache>()->getTexture(DEFAULT_POLYLINE_TEXTURE);
_verticesBuffer = std::make_shared<gpu::Buffer>();
{ // Initialize our buffers
_polylineDataBuffer = std::make_shared<gpu::Buffer>();
_polylineDataBuffer->resize(sizeof(PolylineData));
PolylineData data { glm::vec2(_faceCamera, _glow), glm::vec2(0.0f) };
_polylineDataBuffer->setSubData(0, data);
_polylineGeometryBuffer = std::make_shared<gpu::Buffer>();
}
}
void PolyLineEntityRenderer::buildPipeline() {
// FIXME: opaque pipeline
gpu::ShaderPointer program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke);
gpu::StatePointer state = gpu::StatePointer(new gpu::State());
state->setCullMode(gpu::State::CullMode::CULL_NONE);
state->setDepthTest(true, true, gpu::LESS_EQUAL);
PrepareStencil::testMask(*state);
state->setBlendFunction(true,
gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
_pipeline = gpu::Pipeline::create(program, state);
}
ItemKey PolyLineEntityRenderer::getKey() {
@ -86,152 +59,164 @@ ItemKey PolyLineEntityRenderer::getKey() {
}
ShapeKey PolyLineEntityRenderer::getShapeKey() {
return ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).build();
return ShapeKey::Builder().withOwnPipeline().withTranslucent().withoutCullFace();
}
bool PolyLineEntityRenderer::needsRenderUpdate() const {
bool textureLoadedChanged = resultWithReadLock<bool>([&] {
return (!_textureLoaded && _texture && _texture->isLoaded());
});
if (textureLoadedChanged) {
return true;
}
return Parent::needsRenderUpdate();
}
bool PolyLineEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const {
return (
entity->pointsChanged() ||
entity->strokeWidthsChanged() ||
entity->widthsChanged() ||
entity->normalsChanged() ||
entity->texturesChanged() ||
entity->strokeColorsChanged()
entity->colorsChanged() ||
_isUVModeStretch != entity->getIsUVModeStretch() ||
_glow != entity->getGlow() ||
_faceCamera != entity->getFaceCamera()
);
}
void PolyLineEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
static const QUrl DEFAULT_POLYLINE_TEXTURE = QUrl(PathUtils::resourcesPath() + "images/paintStroke.png");
QUrl entityTextures = DEFAULT_POLYLINE_TEXTURE;
void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) {
auto pointsChanged = entity->pointsChanged();
auto widthsChanged = entity->widthsChanged();
auto normalsChanged = entity->normalsChanged();
auto colorsChanged = entity->colorsChanged();
bool isUVModeStretch = entity->getIsUVModeStretch();
bool glow = entity->getGlow();
bool faceCamera = entity->getFaceCamera();
entity->resetPolyLineChanged();
// Transform
updateModelTransformAndBound();
_renderTransform = getModelTransform();
// Textures
if (entity->texturesChanged()) {
entity->resetTexturesChanged();
QUrl entityTextures = DEFAULT_POLYLINE_TEXTURE;
auto textures = entity->getTextures();
if (!textures.isEmpty()) {
entityTextures = QUrl(textures);
}
_texture = DependencyManager::get<TextureCache>()->getTexture(entityTextures);
_textureAspectRatio = 1.0f;
_textureLoaded = false;
}
if (!_texture) {
_texture = DependencyManager::get<TextureCache>()->getTexture(entityTextures);
bool textureChanged = false;
if (!_textureLoaded && _texture && _texture->isLoaded()) {
textureChanged = true;
_textureAspectRatio = (float)_texture->getOriginalHeight() / (float)_texture->getOriginalWidth();
_textureLoaded = true;
}
}
void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) {
auto pointsChanged = entity->pointsChanged();
auto strokeWidthsChanged = entity->strokeWidthsChanged();
auto normalsChanged = entity->normalsChanged();
auto strokeColorsChanged = entity->strokeColorsChanged();
bool isUVModeStretch = entity->getIsUVModeStretch();
entity->resetPolyLineChanged();
_polylineTransform = Transform();
_polylineTransform.setTranslation(entity->getWorldPosition());
_polylineTransform.setRotation(entity->getWorldOrientation());
// Data
if (faceCamera != _faceCamera || glow != _glow) {
_faceCamera = faceCamera;
_glow = glow;
updateData();
}
// Geometry
if (pointsChanged) {
_lastPoints = entity->getLinePoints();
_points = entity->getLinePoints();
}
if (strokeWidthsChanged) {
_lastStrokeWidths = entity->getStrokeWidths();
if (widthsChanged) {
_widths = entity->getStrokeWidths();
}
if (normalsChanged) {
_lastNormals = entity->getNormals();
_normals = entity->getNormals();
}
if (strokeColorsChanged) {
_lastStrokeColors = entity->getStrokeColors();
_lastStrokeColors = _lastNormals.size() == _lastStrokeColors.size() ? _lastStrokeColors : QVector<glm::vec3>({ toGlm(entity->getColor()) });
if (colorsChanged) {
_colors = entity->getStrokeColors();
_color = toGlm(entity->getColor());
}
if (pointsChanged || strokeWidthsChanged || normalsChanged || strokeColorsChanged) {
_empty = std::min(_lastPoints.size(), std::min(_lastNormals.size(), _lastStrokeWidths.size())) < 2;
if (!_empty) {
updateGeometry(updateVertices(_lastPoints, _lastNormals, _lastStrokeWidths, _lastStrokeColors, isUVModeStretch, _textureAspectRatio));
}
if (_isUVModeStretch != isUVModeStretch || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged) {
_isUVModeStretch = isUVModeStretch;
updateGeometry();
}
}
void PolyLineEntityRenderer::updateGeometry(const std::vector<Vertex>& vertices) {
_numVertices = (uint32_t)vertices.size();
auto bufferSize = _numVertices * sizeof(Vertex);
if (bufferSize > _verticesBuffer->getSize()) {
_verticesBuffer->resize(bufferSize);
}
_verticesBuffer->setSubData(0, vertices);
}
void PolyLineEntityRenderer::updateGeometry() {
int maxNumVertices = std::min(_points.length(), _normals.length());
std::vector<PolyLineEntityRenderer::Vertex> PolyLineEntityRenderer::updateVertices(const QVector<glm::vec3>& points,
const QVector<glm::vec3>& normals,
const QVector<float>& strokeWidths,
const QVector<glm::vec3>& strokeColors,
const bool isUVModeStretch,
const float textureAspectRatio) {
// Calculate the minimum vector size out of normals, points, and stroke widths
int size = std::min(points.size(), std::min(normals.size(), strokeWidths.size()));
std::vector<Vertex> vertices;
// Guard against an empty polyline
if (size <= 0) {
return vertices;
}
float uCoordInc = 1.0f / size;
float uCoord = 0.0f;
int finalIndex = size - 1;
glm::vec3 binormal;
float accumulatedDistance = 0.0f;
float distanceToLastPoint = 0.0f;
float accumulatedStrokeWidth = 0.0f;
float strokeWidth = 0.0f;
bool doesStrokeWidthVary = false;
for (int i = 1; i < strokeWidths.size(); i++) {
if (strokeWidths[i] != strokeWidths[i - 1]) {
doesStrokeWidthVary = true;
break;
if (_widths.size() >= 0) {
for (int i = 1; i < maxNumVertices; i++) {
float width = PolyLineEntityItem::DEFAULT_LINE_WIDTH;
if (i < _widths.length()) {
width = _widths[i];
}
if (width != _widths[i - 1]) {
doesStrokeWidthVary = true;
break;
}
}
}
for (int i = 0; i <= finalIndex; i++) {
const float& width = strokeWidths.at(i);
const auto& point = points.at(i);
const auto& normal = normals.at(i);
const auto& color = strokeColors.size() == normals.size() ? strokeColors.at(i) : strokeColors.at(0);
int vertexIndex = i * 2;
float uCoordInc = 1.0f / maxNumVertices;
float uCoord = 0.0f;
float accumulatedDistance = 0.0f;
float accumulatedStrokeWidth = 0.0f;
glm::vec3 binormal;
if (!isUVModeStretch && i >= 1) {
distanceToLastPoint = glm::distance(points.at(i), points.at(i - 1));
accumulatedDistance += distanceToLastPoint;
strokeWidth = 2 * strokeWidths[i];
std::vector<PolylineVertex> vertices;
vertices.reserve(maxNumVertices);
for (int i = 0; i < maxNumVertices; i++) {
// Position
glm::vec3 point = _points[i];
if (doesStrokeWidthVary) {
//If the stroke varies along the line the texture will stretch more or less depending on the speed
//because it looks better than using the same method as below
accumulatedStrokeWidth += strokeWidth;
float increaseValue = 1;
if (accumulatedStrokeWidth != 0) {
float newUcoord = glm::ceil(((1.0f / textureAspectRatio) * accumulatedDistance) / (accumulatedStrokeWidth / i));
increaseValue = newUcoord - uCoord;
// uCoord
float width = i < _widths.size() ? _widths[i] : PolyLineEntityItem::DEFAULT_LINE_WIDTH;
if (i > 0) { // First uCoord is 0.0f
if (!_isUVModeStretch) {
accumulatedDistance += glm::distance(point, _points[i - 1]);
if (doesStrokeWidthVary) {
//If the stroke varies along the line the texture will stretch more or less depending on the speed
//because it looks better than using the same method as below
accumulatedStrokeWidth += width;
float increaseValue = 1;
if (accumulatedStrokeWidth != 0) {
float newUcoord = glm::ceil((_textureAspectRatio * accumulatedDistance) / (accumulatedStrokeWidth / i));
increaseValue = newUcoord - uCoord;
}
increaseValue = increaseValue > 0 ? increaseValue : 1;
uCoord += increaseValue;
} else {
// If the stroke width is constant then the textures should keep the aspect ratio along the line
uCoord = (_textureAspectRatio * accumulatedDistance) / width;
}
increaseValue = increaseValue > 0 ? increaseValue : 1;
uCoord += increaseValue;
} else {
//If the stroke width is constant then the textures should keep the aspect ratio along the line
uCoord = ((1.0f / textureAspectRatio) * accumulatedDistance) / strokeWidth;
uCoord += uCoordInc;
}
} else if (vertexIndex >= 2) {
uCoord += uCoordInc;
}
// Color
glm::vec3 color = i < _colors.length() ? _colors[i] : _color;
// Normal
glm::vec3 normal = _normals[i];
// Binormal
// For last point we can assume binormals are the same since it represents the last two vertices of quad
if (i < finalIndex) {
const auto tangent = points.at(i + 1) - point;
binormal = glm::normalize(glm::cross(tangent, normal)) * width;
if (i < maxNumVertices - 1) {
glm::vec3 tangent = _points[i + 1] - point;
binormal = glm::normalize(glm::cross(tangent, normal));
// Check to make sure binormal is not a NAN. If it is, don't add to vertices vector
if (binormal.x != binormal.x) {
@ -239,54 +224,36 @@ std::vector<PolyLineEntityRenderer::Vertex> PolyLineEntityRenderer::updateVertic
}
}
const auto v1 = points.at(i) + binormal;
const auto v2 = points.at(i) - binormal;
vertices.emplace_back(v1, normal, vec2(uCoord, 0.0f), color);
vertices.emplace_back(v2, normal, vec2(uCoord, 1.0f), color);
PolylineVertex vertex = { glm::vec4(point, uCoord), glm::vec4(color, 1.0f), glm::vec4(normal, 0.0f), glm::vec4(binormal, 0.5f * width) };
vertices.push_back(vertex);
}
return vertices;
_numVertices = vertices.size();
_polylineGeometryBuffer->setData(vertices.size() * sizeof(PolylineVertex), (const gpu::Byte*) vertices.data());
}
scriptable::ScriptableModelBase PolyLineEntityRenderer::getScriptableModel() {
// TODO: adapt polyline into a triangles mesh...
return EntityRenderer::getScriptableModel();
void PolyLineEntityRenderer::updateData() {
PolylineData data { glm::vec2(_faceCamera, _glow), glm::vec2(0.0f) };
_polylineDataBuffer->setSubData(0, data);
}
void PolyLineEntityRenderer::doRender(RenderArgs* args) {
if (_empty) {
if (_numVertices < 2) {
return;
}
PerformanceTimer perfTimer("RenderablePolyLineEntityItem::render");
Q_ASSERT(args->_batch);
gpu::Batch& batch = *args->_batch;
batch.setModelTransform(_polylineTransform);
if (_texture && _texture->isLoaded()) {
batch.setResourceTexture(PAINTSTROKE_TEXTURE_SLOT, _texture->getGPUTexture());
} else {
batch.setResourceTexture(PAINTSTROKE_TEXTURE_SLOT, DependencyManager::get<TextureCache>()->getWhiteTexture());
if (!_pipeline) {
buildPipeline();
}
float textureWidth = (float)_texture->getOriginalWidth();
float textureHeight = (float)_texture->getOriginalHeight();
if (textureWidth != 0 && textureHeight != 0) {
_textureAspectRatio = textureWidth / textureHeight;
}
batch.setInputFormat(polylineFormat);
batch.setInputBuffer(0, _verticesBuffer, 0, sizeof(Vertex));
#ifndef POLYLINE_ENTITY_USE_FADE_EFFECT
// glColor4f must be called after setInputFormat if it must be taken into account
if (_isFading) {
batch._glColor4f(1.0f, 1.0f, 1.0f, Interpolate::calculateFadeRatio(_fadeStartTime));
} else {
batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
}
#endif
batch.draw(gpu::TRIANGLE_STRIP, _numVertices, 0);
batch.setPipeline(_pipeline);
batch.setModelTransform(_renderTransform);
batch.setResourceTexture(0, _textureLoaded ? _texture->getGPUTexture() : DependencyManager::get<TextureCache>()->getWhiteTexture());
batch.setResourceBuffer(0, _polylineGeometryBuffer);
batch.setUniformBuffer(0, _polylineDataBuffer);
batch.draw(gpu::TRIANGLE_STRIP, (gpu::uint32)(2 * _numVertices), 0);
}

View file

@ -25,52 +25,40 @@ class PolyLineEntityRenderer : public TypedEntityRenderer<PolyLineEntityItem> {
public:
PolyLineEntityRenderer(const EntityItemPointer& entity);
virtual scriptable::ScriptableModelBase getScriptableModel() override;
// FIXME: shouldn't always be transparent: take into account texture and glow
virtual bool isTransparent() const override { return true; }
protected:
virtual bool needsRenderUpdate() const override;
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene,
Transaction& transaction,
const TypedEntityPointer& entity) override;
virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override;
virtual ItemKey getKey() override;
virtual ShapeKey getShapeKey() override;
virtual void doRender(RenderArgs* args) override;
virtual bool isTransparent() const override { return true; }
void buildPipeline();
void updateGeometry();
void updateData();
struct Vertex {
Vertex() {}
Vertex(const vec3& position, const vec3& normal, const vec2& uv, const vec3& color) : position(position),
normal(normal),
uv(uv),
color(color) {}
vec3 position;
vec3 normal;
vec2 uv;
vec3 color;
};
QVector<glm::vec3> _points;
QVector<glm::vec3> _normals;
QVector<glm::vec3> _colors;
glm::vec3 _color;
QVector<float> _widths;
void updateGeometry(const std::vector<Vertex>& vertices);
static std::vector<Vertex> updateVertices(const QVector<glm::vec3>& points,
const QVector<glm::vec3>& normals,
const QVector<float>& strokeWidths,
const QVector<glm::vec3>& strokeColors,
const bool isUVModeStretch,
const float textureAspectRatio);
Transform _polylineTransform;
QVector<glm::vec3> _lastPoints;
QVector<glm::vec3> _lastNormals;
QVector<glm::vec3> _lastStrokeColors;
QVector<float> _lastStrokeWidths;
gpu::BufferPointer _verticesBuffer;
uint32_t _numVertices { 0 };
bool _empty{ true };
NetworkTexturePointer _texture;
float _textureAspectRatio { 1.0f };
bool _textureLoaded { false };
bool _isUVModeStretch;
bool _faceCamera;
bool _glow;
size_t _numVertices;
gpu::BufferPointer _polylineDataBuffer;
gpu::BufferPointer _polylineGeometryBuffer;
static gpu::PipelinePointer _pipeline;
};
} } // namespace

View file

@ -55,6 +55,7 @@ WebEntityRenderer::ContentType WebEntityRenderer::getContentType(const QString&
const QUrl url(urlString);
auto scheme = url.scheme();
if (scheme == HIFI_URL_SCHEME_ABOUT || scheme == HIFI_URL_SCHEME_HTTP || scheme == HIFI_URL_SCHEME_HTTPS ||
scheme == URL_SCHEME_DATA ||
urlString.toLower().endsWith(".htm") || urlString.toLower().endsWith(".html")) {
return ContentType::HtmlContent;
}

View file

@ -14,21 +14,27 @@
<@include DeferredBufferWrite.slh@>
// the albedo texture
LAYOUT(binding=0) uniform sampler2D originalTexture;
<@include paintStroke.slh@>
<$declarePolyLineBuffers()$>
// the interpolated normal
layout(location=0) in vec3 interpolatedNormal;
layout(location=1) in vec2 varTexcoord;
layout(location=2) in vec4 varColor;
LAYOUT(binding=0) uniform sampler2D _texture;
layout(location=0) in vec3 _normalWS;
layout(location=1) in vec2 _texCoord;
layout(location=2) in vec4 _color;
layout(location=3) in float _distanceFromCenter;
void main(void) {
vec4 texel = texture(originalTexture, varTexcoord);
int frontCondition = 1 -int(gl_FrontFacing) * 2;
vec3 color = varColor.rgb;
vec4 texel = texture(_texture, _texCoord);
int frontCondition = 1 - 2 * int(gl_FrontFacing);
vec3 color = _color.rgb * texel.rgb;
float alpha = texel.a * _color.a;
alpha *= mix(1.0, pow(1.0 - abs(_distanceFromCenter), 10.0), _polylineData.faceCameraGlow.y);
packDeferredFragmentTranslucent(
float(frontCondition) * interpolatedNormal,
texel.a * varColor.a,
color * texel.rgb,
10.0);
float(frontCondition) * _normalWS,
alpha,
color,
DEFAULT_ROUGHNESS);
}

View file

@ -0,0 +1,48 @@
<!
// paintStroke.slh
//
// Created by Sam Gondelman on 12/13/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
!>
<@if not PAINTSTROKE_SLH@>
<@def PAINTSTROKE_SLH@>
<@include paintStroke_Shared.slh@>
<@include gpu/ShaderConstants.h@>
<@func declarePolyLineBuffers() @>
// Hack comment to absorb the extra '//' scribe prepends
#if !defined(GPU_SSBO_TRANSFORM_OBJECT)
LAYOUT(binding=GPU_RESOURCE_BUFFER_SLOT0_TEXTURE) uniform samplerBuffer polylineVerticesBuffer;
PolylineVertex getPolylineVertex(int i) {
int offset = 4 * i;
PolylineVertex vertex;
vertex.positionAndUCoord = texelFetch(polylineVerticesBuffer, offset);
vertex.color = texelFetch(polylineVerticesBuffer, offset + 1);
vertex.normal = texelFetch(polylineVerticesBuffer, offset + 2);
vertex.binormalAndHalfWidth = texelFetch(polylineVerticesBuffer, offset + 3);
return vertex;
}
#else
LAYOUT_STD140(binding=GPU_RESOURCE_BUFFER_SLOT0_STORAGE) buffer polylineVerticesBuffer {
PolylineVertex _vertices[];
};
PolylineVertex getPolylineVertex(int i) {
PolylineVertex vertex = _vertices[i];
return vertex;
}
#endif
LAYOUT_STD140(binding=0) uniform polylineDataBuffer {
PolylineData _polylineData;
};
<@endfunc@>
<@endif@>

View file

@ -17,23 +17,45 @@
<@include gpu/Transform.slh@>
<$declareStandardTransform()$>
// the interpolated normal
layout(location=0) out vec3 interpolatedNormal;
<@include paintStroke.slh@>
<$declarePolyLineBuffers()$>
//the diffuse texture
layout(location=1) out vec2 varTexcoord;
layout(location=2) out vec4 varColor;
layout(location=0) out vec3 _normalWS;
layout(location=1) out vec2 _texCoord;
layout(location=2) out vec4 _color;
layout(location=3) out float _distanceFromCenter;
void main(void) {
varTexcoord = inTexCoord0.st;
PolylineVertex vertex = getPolylineVertex(gl_VertexID / 2);
float evenVertex = float(gl_VertexID % 2 == 0);
// pass along the diffuse color
varColor = color_sRGBAToLinear(inColor);
_texCoord = vec2(vertex.positionAndUCoord.w, mix(1.0, 0.0, evenVertex));
_color = color_sRGBAToLinear(vertex.color);
// standard transform
TransformCamera cam = getTransformCamera();
TransformObject obj = getTransformObject();
<$transformModelToClipPos(cam, obj, inPosition, gl_Position)$>
<$transformModelToEyeDir(cam, obj, inNormal.xyz, interpolatedNormal)$>
_distanceFromCenter = -1.0 + 2.0 * evenVertex;
vec4 position = vec4(vertex.positionAndUCoord.xyz, 1.0);
vec3 normal = vertex.normal.xyz;
vec3 binormal = vertex.binormalAndHalfWidth.xyz;
if (_polylineData.faceCameraGlow.x != 0.0) {
vec4 posEye;
vec3 normalEye;
vec3 binormalEye;
<$transformModelToEyePos(cam, obj, position, posEye)$>
<$transformModelToEyeDir(cam, obj, normal, normalEye)$>
<$transformModelToEyeDir(cam, obj, binormal, binormalEye)$>
vec3 tangentEye = cross(binormalEye, normalEye);
// new normal faces the camera
normalEye = normalize(posEye.xyz);
binormalEye = normalize(cross(normalEye, tangentEye));
posEye.xyz += _distanceFromCenter * vertex.binormalAndHalfWidth.w * binormalEye;
<$transformEyeToClipPos(cam, posEye, gl_Position)$>
<$transformEyeToWorldDir(cam, normalEye, _normalWS)$>
} else {
position.xyz += _distanceFromCenter * vertex.binormalAndHalfWidth.w * binormal;
<$transformModelToClipPos(cam, obj, position, gl_Position)$>
<$transformModelToWorldDir(cam, obj, normal, _normalWS)$>
}
}

View file

@ -0,0 +1,25 @@
// glsl / C++ compatible source as interface for FadeEffect
#ifdef __cplusplus
# define _PL_VEC4 glm::vec4
# define _PL_VEC2 glm::vec2
#else
# define _PL_VEC4 vec4
# define _PL_VEC2 vec2
#endif
struct PolylineVertex {
_PL_VEC4 positionAndUCoord;
_PL_VEC4 color;
_PL_VEC4 normal;
_PL_VEC4 binormalAndHalfWidth;
};
struct PolylineData {
_PL_VEC2 faceCameraGlow;
_PL_VEC2 spare;
};
// <@if 1@>
// Trigger Scribe include
// <@endif@> <!def that !>
//

View file

@ -1,52 +0,0 @@
<@include gpu/Config.slh@>
<$VERSION_HEADER$>
// Generated on <$_SCRIBE_DATE$>
//
// paintStroke_fade.frag
// fragment shader
//
// Created by Olivier Prat on 19/07/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
<@include DeferredBufferWrite.slh@>
<@include Fade.slh@>
<$declareFadeFragment()$>
// the albedo texture
LAYOUT(binding=0) uniform sampler2D originalTexture;
// the interpolated normal
layout(location=0) in vec3 interpolatedNormal;
layout(location=1) in vec2 varTexcoord;
layout(location=2) in vec4 varColor;
layout(location=3) in vec4 _worldPosition;
struct PolyLineUniforms {
vec3 color;
};
LAYOUT(binding=0) uniform polyLineBuffer {
PolyLineUniforms polyline;
};
void main(void) {
vec3 fadeEmissive;
FadeObjectParams fadeParams;
<$fetchFadeObjectParams(fadeParams)$>
applyFade(fadeParams, _worldPosition.xyz, fadeEmissive);
vec4 texel = texture(originalTexture, varTexcoord);
int frontCondition = 1 -int(gl_FrontFacing) * 2;
vec3 color = varColor.rgb;
packDeferredFragmentTranslucent(
interpolatedNormal * float(frontCondition),
texel.a * varColor.a,
polyline.color * texel.rgb + fadeEmissive,
10.0);
}

View file

@ -1,43 +0,0 @@
<@include gpu/Config.slh@>
<$VERSION_HEADER$>
// Generated on <$_SCRIBE_DATE$>
//
// paintStroke_fade.vert
// vertex shader
//
// Created by Olivier Prat on 19/07/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
<@include gpu/Inputs.slh@>
<@include gpu/Color.slh@>
<@include gpu/Transform.slh@>
<$declareStandardTransform()$>
// the interpolated normal
layout(location=0) out vec3 interpolatedNormal;
//the diffuse texture
layout(location=1) out vec2 varTexcoord;
layout(location=2) out vec4 varColor;
layout(location=3) out vec4 _worldPosition;
void main(void) {
varTexcoord = inTexCoord0.st;
// pass along the diffuse color
varColor = color_sRGBAToLinear(inColor);
// standard transform
TransformCamera cam = getTransformCamera();
TransformObject obj = getTransformObject();
<$transformModelToClipPos(cam, obj, inPosition, gl_Position)$>
<$transformModelToEyeDir(cam, obj, inNormal.xyz, interpolatedNormal)$>
<$transformModelToWorldPos(obj, inPosition, _worldPosition)$>
}

View file

@ -523,6 +523,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
CHECK_PROPERTY_CHANGE(PROP_STROKE_NORMALS, normals);
CHECK_PROPERTY_CHANGE(PROP_STROKE_COLORS, strokeColors);
CHECK_PROPERTY_CHANGE(PROP_IS_UV_MODE_STRETCH, isUVModeStretch);
CHECK_PROPERTY_CHANGE(PROP_LINE_GLOW, glow);
CHECK_PROPERTY_CHANGE(PROP_LINE_FACE_CAMERA, faceCamera);
// Shape
CHECK_PROPERTY_CHANGE(PROP_SHAPE, shape);
@ -1047,6 +1049,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
* format.
* @property {boolean} isUVModeStretch=true - If <code>true</code>, the texture is stretched to fill the whole line, otherwise
* the texture repeats along the line.
* @property {bool} glow=false - If <code>true</code>, the alpha of the strokes will drop off farther from the center.
* @property {bool} faceCamera=false - If <code>true</code>, each line segment will rotate to face the camera.
* @example <caption>Draw a textured "V".</caption>
* var entity = Entities.addEntity({
* type: "PolyLine",
@ -1625,6 +1629,8 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_STROKE_NORMALS, normals);
COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_STROKE_COLORS, strokeColors, qVectorVec3Color);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IS_UV_MODE_STRETCH, isUVModeStretch);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_GLOW, glow);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_FACE_CAMERA, faceCamera);
}
// Materials
@ -1944,6 +1950,8 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
COPY_PROPERTY_FROM_QSCRIPTVALUE(normals, qVectorVec3, setNormals);
COPY_PROPERTY_FROM_QSCRIPTVALUE(strokeColors, qVectorVec3, setStrokeColors);
COPY_PROPERTY_FROM_QSCRIPTVALUE(isUVModeStretch, bool, setIsUVModeStretch);
COPY_PROPERTY_FROM_QSCRIPTVALUE(glow, bool, setGlow);
COPY_PROPERTY_FROM_QSCRIPTVALUE(faceCamera, bool, setFaceCamera);
// Shape
COPY_PROPERTY_FROM_QSCRIPTVALUE(shape, QString, setShape);
@ -2196,6 +2204,8 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
COPY_PROPERTY_IF_CHANGED(normals);
COPY_PROPERTY_IF_CHANGED(strokeColors);
COPY_PROPERTY_IF_CHANGED(isUVModeStretch);
COPY_PROPERTY_IF_CHANGED(glow);
COPY_PROPERTY_IF_CHANGED(faceCamera);
// Shape
COPY_PROPERTY_IF_CHANGED(shape);
@ -2512,6 +2522,8 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue
ADD_PROPERTY_TO_MAP(PROP_STROKE_NORMALS, Normals, normals, QVector<vec3>);
ADD_PROPERTY_TO_MAP(PROP_STROKE_COLORS, StrokeColors, strokeColors, QVector<vec3>);
ADD_PROPERTY_TO_MAP(PROP_IS_UV_MODE_STRETCH, IsUVModeStretch, isUVModeStretch, QVector<float>);
ADD_PROPERTY_TO_MAP(PROP_LINE_GLOW, Glow, glow, bool);
ADD_PROPERTY_TO_MAP(PROP_LINE_FACE_CAMERA, FaceCamera, faceCamera, bool);
// Shape
ADD_PROPERTY_TO_MAP(PROP_SHAPE, Shape, shape, QString);
@ -2875,6 +2887,8 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
APPEND_ENTITY_PROPERTY(PROP_STROKE_NORMALS, properties.getPackedNormals());
APPEND_ENTITY_PROPERTY(PROP_STROKE_COLORS, properties.getPackedStrokeColors());
APPEND_ENTITY_PROPERTY(PROP_IS_UV_MODE_STRETCH, properties.getIsUVModeStretch());
APPEND_ENTITY_PROPERTY(PROP_LINE_GLOW, properties.getGlow());
APPEND_ENTITY_PROPERTY(PROP_LINE_FACE_CAMERA, properties.getFaceCamera());
}
// NOTE: Spheres and Boxes are just special cases of Shape, and they need to include their PROP_SHAPE
@ -3303,6 +3317,8 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STROKE_NORMALS, QByteArray, setPackedNormals);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STROKE_COLORS, QByteArray, setPackedStrokeColors);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_IS_UV_MODE_STRETCH, bool, setIsUVModeStretch);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LINE_GLOW, bool, setGlow);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LINE_FACE_CAMERA, bool, setFaceCamera);
}
// NOTE: Spheres and Boxes are just special cases of Shape, and they need to include their PROP_SHAPE
@ -3648,6 +3664,8 @@ void EntityItemProperties::markAllChanged() {
_normalsChanged = true;
_strokeColorsChanged = true;
_isUVModeStretchChanged = true;
_glowChanged = true;
_faceCameraChanged = true;
// Shape
_shapeChanged = true;
@ -4258,6 +4276,12 @@ QList<QString> EntityItemProperties::listChangedProperties() {
if (isUVModeStretchChanged()) {
out += "isUVModeStretch";
}
if (glowChanged()) {
out += "glow";
}
if (faceCameraChanged()) {
out += "faceCamera";
}
// Shape
if (shapeChanged()) {

View file

@ -310,6 +310,8 @@ public:
DEFINE_PROPERTY(PROP_STROKE_NORMALS, Normals, normals, QVector<glm::vec3>, ENTITY_ITEM_DEFAULT_EMPTY_VEC3_QVEC);
DEFINE_PROPERTY(PROP_STROKE_COLORS, StrokeColors, strokeColors, QVector<glm::vec3>, ENTITY_ITEM_DEFAULT_EMPTY_VEC3_QVEC);
DEFINE_PROPERTY(PROP_IS_UV_MODE_STRETCH, IsUVModeStretch, isUVModeStretch, bool, true);
DEFINE_PROPERTY(PROP_LINE_GLOW, Glow, glow, bool, false);
DEFINE_PROPERTY(PROP_LINE_FACE_CAMERA, FaceCamera, faceCamera, bool, false);
// Shape
DEFINE_PROPERTY_REF(PROP_SHAPE, Shape, shape, QString, "Sphere");

View file

@ -291,6 +291,8 @@ enum EntityPropertyList {
PROP_STROKE_NORMALS = PROP_DERIVED_2,
PROP_STROKE_COLORS = PROP_DERIVED_3,
PROP_IS_UV_MODE_STRETCH = PROP_DERIVED_4,
PROP_LINE_GLOW = PROP_DERIVED_5,
PROP_LINE_FACE_CAMERA = PROP_DERIVED_6,
// Shape
PROP_SHAPE = PROP_DERIVED_0,

View file

@ -24,7 +24,6 @@
const float PolyLineEntityItem::DEFAULT_LINE_WIDTH = 0.1f;
const int PolyLineEntityItem::MAX_POINTS_PER_LINE = 60;
EntityItemPointer PolyLineEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) {
EntityItemPointer entity(new PolyLineEntityItem(entityID), [](EntityItem* ptr) { ptr->deleteLater(); });
entity->setProperties(properties);
@ -37,7 +36,6 @@ PolyLineEntityItem::PolyLineEntityItem(const EntityItemID& entityItemID) : Entit
}
EntityItemProperties PolyLineEntityItem::getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const {
QWriteLocker lock(&_quadReadWriteLock);
EntityItemProperties properties = EntityItem::getProperties(desiredProperties, allowEmptyDesiredProperties); // get the properties from our base class
COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getColor);
@ -48,11 +46,13 @@ EntityItemProperties PolyLineEntityItem::getProperties(const EntityPropertyFlags
COPY_ENTITY_PROPERTY_TO_PROPERTIES(normals, getNormals);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(strokeColors, getStrokeColors);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(isUVModeStretch, getIsUVModeStretch);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(glow, getGlow);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(faceCamera, getFaceCamera);
return properties;
}
bool PolyLineEntityItem::setProperties(const EntityItemProperties& properties) {
QWriteLocker lock(&_quadReadWriteLock);
bool somethingChanged = false;
somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class
@ -64,6 +64,8 @@ bool PolyLineEntityItem::setProperties(const EntityItemProperties& properties) {
SET_ENTITY_PROPERTY_FROM_PROPERTIES(normals, setNormals);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(strokeColors, setStrokeColors);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(isUVModeStretch, setIsUVModeStretch);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(glow, setGlow);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(faceCamera, setFaceCamera);
if (somethingChanged) {
bool wantDebug = false;
@ -78,125 +80,59 @@ bool PolyLineEntityItem::setProperties(const EntityItemProperties& properties) {
return somethingChanged;
}
bool PolyLineEntityItem::appendPoint(const glm::vec3& point) {
if (_points.size() > MAX_POINTS_PER_LINE - 1) {
qCDebug(entities) << "MAX POINTS REACHED!";
return false;
}
_points << point;
_pointsChanged = true;
calculateScaleAndRegistrationPoint();
return true;
}
bool PolyLineEntityItem::setStrokeWidths(const QVector<float>& strokeWidths) {
void PolyLineEntityItem::setLinePoints(const QVector<glm::vec3>& points) {
withWriteLock([&] {
_strokeWidths = strokeWidths;
_strokeWidthsChanged = true;
_points = points;
_pointsChanged = true;
});
return true;
computeAndUpdateDimensionsAndPosition();
}
bool PolyLineEntityItem::setNormals(const QVector<glm::vec3>& normals) {
void PolyLineEntityItem::setStrokeWidths(const QVector<float>& strokeWidths) {
withWriteLock([&] {
_widths = strokeWidths;
_widthsChanged = true;
});
computeAndUpdateDimensionsAndPosition();
}
void PolyLineEntityItem::setNormals(const QVector<glm::vec3>& normals) {
withWriteLock([&] {
_normals = normals;
_normalsChanged = true;
});
return true;
}
bool PolyLineEntityItem::setStrokeColors(const QVector<glm::vec3>& strokeColors) {
void PolyLineEntityItem::setStrokeColors(const QVector<glm::vec3>& strokeColors) {
withWriteLock([&] {
_strokeColors = strokeColors;
_strokeColorsChanged = true;
_colors = strokeColors;
_colorsChanged = true;
});
return true;
}
void PolyLineEntityItem::computeAndUpdateDimensionsAndPosition() {
QVector<glm::vec3> points;
QVector<float> widths;
bool PolyLineEntityItem::setLinePoints(const QVector<glm::vec3>& points) {
if (points.size() > MAX_POINTS_PER_LINE) {
return false;
}
bool result = false;
withWriteLock([&] {
//Check to see if points actually changed. If they haven't, return before doing anything else
if (points.size() != _points.size()) {
_pointsChanged = true;
} else if (points.size() == _points.size()) {
//same number of points, so now compare every point
for (int i = 0; i < points.size(); i++) {
if (points.at(i) != _points.at(i)) {
_pointsChanged = true;
break;
}
}
}
if (!_pointsChanged) {
return;
}
_points = points;
result = true;
});
if (result) {
calculateScaleAndRegistrationPoint();
}
return result;
}
void PolyLineEntityItem::calculateScaleAndRegistrationPoint() {
glm::vec3 high(0.0f, 0.0f, 0.0f);
glm::vec3 low(0.0f, 0.0f, 0.0f);
int pointCount = 0;
glm::vec3 firstPoint;
withReadLock([&] {
pointCount = _points.size();
if (pointCount > 0) {
firstPoint = _points.at(0);
}
for (int i = 0; i < pointCount; i++) {
const glm::vec3& point = _points.at(i);
high = glm::max(point, high);
low = glm::min(point, low);
}
points = _points;
widths = _widths;
});
float magnitudeSquared = glm::length2(low - high);
vec3 newScale { 1 };
vec3 newRegistrationPoint { 0.5f };
glm::vec3 maxHalfDim(0.5f * ENTITY_ITEM_DEFAULT_WIDTH);
float maxWidth = 0.0f;
for (int i = 0; i < points.length(); i++) {
maxHalfDim = glm::max(maxHalfDim, glm::abs(points[i]));
maxWidth = glm::max(maxWidth, i < widths.length() ? widths[i] : DEFAULT_LINE_WIDTH);
}
const float EPSILON = 0.0001f;
const float EPSILON_SQUARED = EPSILON * EPSILON;
const float HALF_LINE_WIDTH = 0.075f; // sadly _strokeWidths() don't seem to correspond to reality, so just use a flat assumption of the stroke width
const vec3 QUARTER_LINE_WIDTH { HALF_LINE_WIDTH * 0.5f };
if (pointCount > 1 && magnitudeSquared > EPSILON_SQUARED) {
newScale = glm::abs(high) + glm::abs(low) + vec3(HALF_LINE_WIDTH);
// Center the poly line in the bounding box
glm::vec3 startPointInScaleSpace = firstPoint - low;
startPointInScaleSpace += QUARTER_LINE_WIDTH;
newRegistrationPoint = startPointInScaleSpace / newScale;
}
// if Polyline has only one or fewer points, use default dimension settings
setScaledDimensions(newScale);
EntityItem::setRegistrationPoint(newRegistrationPoint);
setScaledDimensions(2.0f * (maxHalfDim + maxWidth));
}
int PolyLineEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead,
ReadBitstreamToTreeParams& args,
EntityPropertyFlags& propertyFlags, bool overwriteLocalData,
bool& somethingChanged) {
QWriteLocker lock(&_quadReadWriteLock);
int bytesRead = 0;
const unsigned char* dataAt = data;
@ -208,6 +144,8 @@ int PolyLineEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* da
READ_ENTITY_PROPERTY(PROP_STROKE_NORMALS, QVector<glm::vec3>, setNormals);
READ_ENTITY_PROPERTY(PROP_STROKE_COLORS, QVector<glm::vec3>, setStrokeColors);
READ_ENTITY_PROPERTY(PROP_IS_UV_MODE_STRETCH, bool, setIsUVModeStretch);
READ_ENTITY_PROPERTY(PROP_LINE_GLOW, bool, setGlow);
READ_ENTITY_PROPERTY(PROP_LINE_FACE_CAMERA, bool, setFaceCamera);
return bytesRead;
}
@ -222,6 +160,8 @@ EntityPropertyFlags PolyLineEntityItem::getEntityProperties(EncodeBitstreamParam
requestedProperties += PROP_STROKE_NORMALS;
requestedProperties += PROP_STROKE_COLORS;
requestedProperties += PROP_IS_UV_MODE_STRETCH;
requestedProperties += PROP_LINE_GLOW;
requestedProperties += PROP_LINE_FACE_CAMERA;
return requestedProperties;
}
@ -233,7 +173,6 @@ void PolyLineEntityItem::appendSubclassData(OctreePacketData* packetData, Encode
int& propertyCount,
OctreeElement::AppendState& appendState) const {
QWriteLocker lock(&_quadReadWriteLock);
bool successPropertyFits = true;
APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor());
@ -244,6 +183,8 @@ void PolyLineEntityItem::appendSubclassData(OctreePacketData* packetData, Encode
APPEND_ENTITY_PROPERTY(PROP_STROKE_NORMALS, getNormals());
APPEND_ENTITY_PROPERTY(PROP_STROKE_COLORS, getStrokeColors());
APPEND_ENTITY_PROPERTY(PROP_IS_UV_MODE_STRETCH, getIsUVModeStretch());
APPEND_ENTITY_PROPERTY(PROP_LINE_GLOW, getGlow());
APPEND_ENTITY_PROPERTY(PROP_LINE_FACE_CAMERA, getFaceCamera());
}
void PolyLineEntityItem::debugDump() const {
@ -255,61 +196,49 @@ void PolyLineEntityItem::debugDump() const {
qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now);
}
QVector<glm::vec3> PolyLineEntityItem::getLinePoints() const {
QVector<glm::vec3> result;
withReadLock([&] {
result = _points;
return resultWithReadLock<QVector<glm::vec3>>([&] {
return _points;
});
return result;
}
QVector<glm::vec3> PolyLineEntityItem::getNormals() const {
QVector<glm::vec3> result;
withReadLock([&] {
result = _normals;
return resultWithReadLock<QVector<glm::vec3>>([&] {
return _normals;
});
return result;
}
QVector<glm::vec3> PolyLineEntityItem::getStrokeColors() const {
QVector<glm::vec3> result;
withReadLock([&] {
result = _strokeColors;
return resultWithReadLock<QVector<glm::vec3>>([&] {
return _colors;
});
return result;
}
QVector<float> PolyLineEntityItem::getStrokeWidths() const {
QVector<float> result;
withReadLock([&] {
result = _strokeWidths;
return resultWithReadLock<QVector<float>>([&] {
return _widths;
});
return result;
}
QString PolyLineEntityItem::getTextures() const {
QString result;
withReadLock([&] {
result = _textures;
return resultWithReadLock<QString>([&] {
return _textures;
});
return result;
}
void PolyLineEntityItem::setTextures(const QString& textures) {
withWriteLock([&] {
if (_textures != textures) {
_textures = textures;
_texturesChangedFlag = true;
_texturesChanged = true;
}
});
}
void PolyLineEntityItem::setColor(const glm::u8vec3& value) {
withWriteLock([&] {
_strokeColorsChanged = true;
_color = value;
_colorsChanged = true;
});
}

View file

@ -44,35 +44,40 @@ class PolyLineEntityItem : public EntityItem {
glm::u8vec3 getColor() const;
void setColor(const glm::u8vec3& value);
bool setLinePoints(const QVector<glm::vec3>& points);
bool appendPoint(const glm::vec3& point);
static const int MAX_POINTS_PER_LINE;
void setLinePoints(const QVector<glm::vec3>& points);
QVector<glm::vec3> getLinePoints() const;
bool setNormals(const QVector<glm::vec3>& normals);
static const float DEFAULT_LINE_WIDTH;
void setStrokeWidths(const QVector<float>& strokeWidths);
QVector<float> getStrokeWidths() const;
void setNormals(const QVector<glm::vec3>& normals);
QVector<glm::vec3> getNormals() const;
bool setStrokeColors(const QVector<glm::vec3>& strokeColors);
void setStrokeColors(const QVector<glm::vec3>& strokeColors);
QVector<glm::vec3> getStrokeColors() const;
bool setStrokeWidths(const QVector<float>& strokeWidths);
QVector<float> getStrokeWidths() const;
void setIsUVModeStretch(bool isUVModeStretch){ _isUVModeStretch = isUVModeStretch; }
bool getIsUVModeStretch() const{ return _isUVModeStretch; }
QString getTextures() const;
void setTextures(const QString& textures);
virtual ShapeType getShapeType() const override { return SHAPE_TYPE_NONE; }
void setGlow(bool glow) { _glow = glow; }
bool getGlow() const { return _glow; }
void setFaceCamera(bool faceCamera) { _faceCamera = faceCamera; }
bool getFaceCamera() const { return _faceCamera; }
bool pointsChanged() const { return _pointsChanged; }
bool normalsChanged() const { return _normalsChanged; }
bool strokeColorsChanged() const { return _strokeColorsChanged; }
bool strokeWidthsChanged() const { return _strokeWidthsChanged; }
bool texturesChanged() const { return _texturesChangedFlag; }
void resetTexturesChanged() { _texturesChangedFlag = false; }
void resetPolyLineChanged() { _strokeColorsChanged = _strokeWidthsChanged = _normalsChanged = _pointsChanged = false; }
bool colorsChanged() const { return _colorsChanged; }
bool widthsChanged() const { return _widthsChanged; }
bool texturesChanged() const { return _texturesChanged; }
void resetTexturesChanged() { _texturesChanged = false; }
void resetPolyLineChanged() { _colorsChanged = _widthsChanged = _normalsChanged = _pointsChanged = false; }
// never have a ray intersection pick a PolyLineEntityItem.
virtual bool supportsDetailedIntersection() const override { return true; }
@ -85,29 +90,26 @@ class PolyLineEntityItem : public EntityItem {
BoxFace& face, glm::vec3& surfaceNormal,
QVariantMap& extraInfo, bool precisionPicking) const override { return false; }
// disable these external interfaces as PolyLineEntities caculate their own dimensions based on the points they contain
virtual void setRegistrationPoint(const glm::vec3& value) override {}; // FIXME: this is suspicious!
virtual void debugDump() const override;
static const float DEFAULT_LINE_WIDTH;
static const int MAX_POINTS_PER_LINE;
private:
void calculateScaleAndRegistrationPoint();
void computeAndUpdateDimensionsAndPosition();
protected:
glm::u8vec3 _color;
bool _pointsChanged { true };
bool _normalsChanged { true };
bool _strokeColorsChanged { true };
bool _strokeWidthsChanged { true };
QVector<glm::vec3> _points;
QVector<glm::vec3> _normals;
QVector<glm::vec3> _strokeColors;
QVector<float> _strokeWidths;
QVector<glm::vec3> _colors;
QVector<float> _widths;
QString _textures;
bool _isUVModeStretch;
bool _texturesChangedFlag { false };
mutable QReadWriteLock _quadReadWriteLock;
bool _glow;
bool _faceCamera;
bool _pointsChanged { false };
bool _normalsChanged { false };
bool _colorsChanged { false };
bool _widthsChanged { false };
bool _texturesChanged { false };
};
#endif // hifi_PolyLineEntityItem_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

@ -33,6 +33,7 @@ namespace NetworkingConstants {
const QString HIFI_URL_SCHEME_ABOUT = "about";
const QString URL_SCHEME_HIFI = "hifi";
const QString URL_SCHEME_HIFIAPP = "hifiapp";
const QString URL_SCHEME_DATA = "data";
const QString URL_SCHEME_QRC = "qrc";
const QString HIFI_URL_SCHEME_FILE = "file";
const QString HIFI_URL_SCHEME_HTTP = "http";

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

@ -253,7 +253,8 @@ enum class EntityVersion : PacketVersion {
MissingTextProperties,
GrabTraits,
MorePropertiesCleanup,
FixPropertiesFromCleanup
FixPropertiesFromCleanup,
UpdatedPolyLines
};
enum class EntityScriptCallMethodVersion : PacketVersion {
@ -307,7 +308,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
MigrateAvatarEntitiesToTraits,
FarGrabJointsRedux,
JointTransScaled,
GrabTraits
GrabTraits,
CollisionFlag
};
enum class DomainConnectRequestVersion : PacketVersion {

View file

@ -20,6 +20,10 @@
"faceCamera": {
"tooltip": "If enabled, the entity follows the camera of each user, creating a billboard effect."
},
"textBillboardMode": {
"tooltip": "If enabled, determines how the entity will face the camera.",
"jsPropertyName": "billboardMode"
},
"flyingAllowed": {
"tooltip": "If enabled, users can fly in the zone."
},
@ -149,9 +153,25 @@
"originalTextures": {
"tooltip": "A JSON string containing the original texture used on the model."
},
"image": {
"tooltip": "The URL for the image source.",
"jsPropertyName": "textures"
"imageURL": {
"tooltip": "The URL for the image source."
},
"imageColor": {
"tooltip": "The tint to be applied to the image.",
"jsPropertyName": "color"
},
"emissive": {
"tooltip": "If enabled, the image will display at full brightness."
},
"subImage": {
"tooltip": "The area of the image that is displayed."
},
"imageBillboardMode": {
"tooltip": "If enabled, determines how the entity will face the camera.",
"jsPropertyName": "billboardMode"
},
"keepAspectRatio": {
"tooltip": "If enabled, the image will maintain its original aspect ratio."
},
"sourceUrl": {
"tooltip": "The URL for the web page source."

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

@ -956,12 +956,12 @@ div.refresh input[type="button"] {
}
.draggable-number .left-arrow {
top: 3px;
left: 0px;
left: 0;
transform: rotate(180deg);
}
.draggable-number .right-arrow {
top: 3px;
right: 0px;
right: 0;
}
.draggable-number input[type=number] {
position: absolute;
@ -995,6 +995,10 @@ div.refresh input[type="button"] {
font-size: 15px;
}
.rect .rect-row {
margin-bottom: 8px;
}
.row .property {
width: auto;
display: inline-block;
@ -1602,10 +1606,10 @@ input.rename-entity {
margin-left: 4px;
margin-right: 10px;
}
.fstuple label.red, .fstuple label.x {
.fstuple label.red, .fstuple label.x, .fstuple label.w {
color: #C62147;
}
.fstuple label.green, .fstuple label.y {
.fstuple label.green, .fstuple label.y, .fstuple label.h {
color: #359D85;
}
.fstuple label.blue, .fstuple label.z {

View file

@ -140,12 +140,14 @@ const GROUPS = [
step: 0.005,
decimals: 4,
unit: "m",
propertyID: "lineHeight"
propertyID: "lineHeight",
},
{
label: "Face Camera",
type: "bool",
propertyID: "faceCamera"
label: "Billboard Mode",
type: "dropdown",
options: { none: "None", yaw: "Yaw", full: "Full"},
propertyID: "textBillboardMode",
propertyName: "billboardMode", // actual entity property name
},
]
},
@ -478,6 +480,37 @@ const GROUPS = [
placeholder: "URL",
propertyID: "imageURL",
},
{
label: "Color",
type: "color",
propertyID: "imageColor",
propertyName: "color", // actual entity property name
},
{
label: "Emissive",
type: "bool",
propertyID: "emissive",
},
{
label: "Sub Image",
type: "rect",
min: 0,
step: 1,
subLabels: [ "x", "y", "w", "h" ],
propertyID: "subImage",
},
{
label: "Billboard Mode",
type: "dropdown",
options: { none: "None", yaw: "Yaw", full: "Full"},
propertyID: "imageBillboardMode",
propertyName: "billboardMode", // actual entity property name
},
{
label: "Keep Aspect Ratio",
type: "bool",
propertyID: "keepAspectRatio",
},
]
},
{
@ -1424,6 +1457,13 @@ const PROPERTY_NAME_DIVISION = {
SUBPROPERTY: 2,
};
const RECT_ELEMENTS = {
X_NUMBER: 0,
Y_NUMBER: 1,
WIDTH_NUMBER: 2,
HEIGHT_NUMBER: 3,
};
const VECTOR_ELEMENTS = {
X_NUMBER: 0,
Y_NUMBER: 1,
@ -1475,6 +1515,13 @@ function getPropertyInputElement(propertyID) {
return property.elInput;
case 'number-draggable':
return property.elNumber.elInput;
case 'rect':
return {
x: property.elNumberX.elInput,
y: property.elNumberY.elInput,
width: property.elNumberWidth.elInput,
height: property.elNumberHeight.elInput
};
case 'vec3':
case 'vec2':
return { x: property.elNumberX.elInput, y: property.elNumberY.elInput, z: property.elNumberZ.elInput };
@ -1564,6 +1611,13 @@ function resetProperties() {
}
break;
}
case 'rect': {
property.elNumberX.setValue("");
property.elNumberY.setValue("");
property.elNumberWidth.setValue("");
property.elNumberHeight.setValue("");
break;
}
case 'vec3':
case 'vec2': {
property.elNumberX.setValue("");
@ -1748,7 +1802,7 @@ function createDragStartFunction(property) {
function createDragEndFunction(property) {
return function() {
property.dragging = false;
// send an additonal update post-dragging to consider whole property change from dragStart to dragEnd to be 1 action
// send an additional update post-dragging to consider whole property change from dragStart to dragEnd to be 1 action
this.valueChangeFunction();
};
}
@ -1793,6 +1847,18 @@ function createEmitVec3PropertyUpdateFunction(property) {
};
}
function createEmitRectPropertyUpdateFunction(property) {
return function() {
let newValue = {
x: property.elNumberX.elInput.value,
y: property.elNumberY.elInput.value,
width: property.elNumberWidth.elInput.value,
height: property.elNumberHeight.elInput.value,
};
updateProperty(property.name, newValue, property.isParticleProperty);
};
}
function createEmitColorPropertyUpdateFunction(property) {
return function() {
emitColorPropertyUpdate(property.name, property.elNumberR.elInput.value, property.elNumberG.elInput.value,
@ -1951,6 +2017,44 @@ function createNumberDraggableProperty(property, elProperty) {
return elDraggableNumber;
}
function createRectProperty(property, elProperty) {
let propertyData = property.data;
elProperty.className = "rect";
let elXYRow = document.createElement('div');
elXYRow.className = "rect-row fstuple";
elProperty.appendChild(elXYRow);
let elWidthHeightRow = document.createElement('div');
elWidthHeightRow.className = "rect-row fstuple";
elProperty.appendChild(elWidthHeightRow);
let elNumberX = createTupleNumberInput(property, propertyData.subLabels[RECT_ELEMENTS.X_NUMBER]);
let elNumberY = createTupleNumberInput(property, propertyData.subLabels[RECT_ELEMENTS.Y_NUMBER]);
let elNumberWidth = createTupleNumberInput(property, propertyData.subLabels[RECT_ELEMENTS.WIDTH_NUMBER]);
let elNumberHeight = createTupleNumberInput(property, propertyData.subLabels[RECT_ELEMENTS.HEIGHT_NUMBER]);
elXYRow.appendChild(elNumberX.elDiv);
elXYRow.appendChild(elNumberY.elDiv);
elWidthHeightRow.appendChild(elNumberWidth.elDiv);
elWidthHeightRow.appendChild(elNumberHeight.elDiv);
let valueChangeFunction = createEmitRectPropertyUpdateFunction(property);
elNumberX.setValueChangeFunction(valueChangeFunction);
elNumberY.setValueChangeFunction(valueChangeFunction);
elNumberWidth.setValueChangeFunction(valueChangeFunction);
elNumberHeight.setValueChangeFunction(valueChangeFunction);
let elResult = [];
elResult[RECT_ELEMENTS.X_NUMBER] = elNumberX;
elResult[RECT_ELEMENTS.Y_NUMBER] = elNumberY;
elResult[RECT_ELEMENTS.WIDTH_NUMBER] = elNumberWidth;
elResult[RECT_ELEMENTS.HEIGHT_NUMBER] = elNumberHeight;
return elResult;
}
function createVec3Property(property, elProperty) {
let propertyData = property.data;
@ -2273,6 +2377,14 @@ function createProperty(propertyData, propertyElementID, propertyName, propertyI
property.elNumber = createNumberDraggableProperty(property, elProperty);
break;
}
case 'rect': {
let elRect = createRectProperty(property, elProperty);
property.elNumberX = elRect[RECT_ELEMENTS.X_NUMBER];
property.elNumberY = elRect[RECT_ELEMENTS.Y_NUMBER];
property.elNumberWidth = elRect[RECT_ELEMENTS.WIDTH_NUMBER];
property.elNumberHeight = elRect[RECT_ELEMENTS.HEIGHT_NUMBER];
break;
}
case 'vec3': {
let elVec3 = createVec3Property(property, elProperty);
property.elNumberX = elVec3[VECTOR_ELEMENTS.X_NUMBER];
@ -3160,6 +3272,7 @@ function loaded() {
case 'number-draggable':
isPropertyNotNumber = isNaN(propertyValue) || propertyValue === null;
break;
case 'rect':
case 'vec3':
case 'vec2':
isPropertyNotNumber = isNaN(propertyValue.x) || propertyValue.x === null;
@ -3202,6 +3315,12 @@ function loaded() {
property.elNumber.setValue(value);
break;
}
case 'rect':
property.elNumberX.setValue(propertyValue.x);
property.elNumberY.setValue(propertyValue.y);
property.elNumberWidth.setValue(propertyValue.width);
property.elNumberHeight.setValue(propertyValue.height);
break;
case 'vec3':
case 'vec2': {
let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1;

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

@ -469,12 +469,28 @@ void TestRunner::runInterfaceWithTestScript() {
url = "hifi://localhost";
}
QString deleteScript =
QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js";
QString testScript =
QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js";
QString commandLine;
#ifdef Q_OS_WIN
QString exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\"";
QString exeFile;
// First, run script to delete any entities in test area
// Note that this will run to completion before continuing
exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\"";
commandLine = "start /wait \"\" " + exeFile +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + deleteScript + " quitWhenFinished";
system(commandLine.toStdString().c_str());
// Now run the test suite
exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\"";
commandLine = exeFile +
" --url " + url +
" --no-updater" +
@ -485,10 +501,6 @@ void TestRunner::runInterfaceWithTestScript() {
_interfaceWorker->setCommandLine(commandLine);
emit startInterface();
#elif defined Q_OS_MAC
// On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process
// has started.
// Before starting interface, start a process that will resize interface 10s after it opens
// This is performed by creating a bash script that runs to processes
QFile script;
script.setFileName(_workingFolder + "/runInterfaceTests.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
@ -498,7 +510,20 @@ void TestRunner::runInterfaceWithTestScript() {
}
script.write("#!/bin/sh\n\n");
// First, run script to delete any entities in test area
commandLine =
"open -W \"" +_installationFolder + "/interface.app\" --args" +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + deleteScript + " quitWhenFinished\n";
script.write(commandLine.toStdString().c_str());
// On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process
// has started.
// Before starting interface, start a process that will resize interface 10s after it opens
commandLine = _workingFolder +"/waitForStart.sh interface && sleep 10 && " + _workingFolder +"/setInterfaceSizeAndPosition.sh &\n";
script.write(commandLine.toStdString().c_str());
@ -509,7 +534,7 @@ void TestRunner::runInterfaceWithTestScript() {
" --no-login-suggestion"
" --testScript " + testScript + " quitWhenFinished" +
" --testResultsLocation " + _snapshotFolder +
" && " + _workingFolder +"/waitForFinish.sh interface";
" && " + _workingFolder +"/waitForFinish.sh interface\n";
script.write(commandLine.toStdString().c_str());

View file

@ -1,14 +0,0 @@
//
// HelpWindow.cpp
//
// Created by Nissim Hadar on 8 Aug 2017.
// Copyright 2013 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "HelpWindow.h"
HelpWindow::HelpWindow(QWidget *parent) {
setupUi(this);
}

View file

@ -1,22 +0,0 @@
//
// HelpWindow.h
//
// Created by Nissim Hadar on 8 Aug 2017.
// Copyright 2013 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_HelpWindow_h
#define hifi_HelpWindow_h
#include "ui_HelpWindow.h"
class HelpWindow : public QDialog, public Ui::HelpWindow {
Q_OBJECT
public:
HelpWindow(QWidget* parent = Q_NULLPTR);
};
#endif

View file

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>HelpWindow</class>
<widget class="QDialog" name="HelpWindow">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>696</width>
<height>546</height>
</rect>
</property>
<property name="windowTitle">
<string>Nitpick Help</string>
</property>
<widget class="QTextEdit" name="textEdit">
<property name="geometry">
<rect>
<x>50</x>
<y>50</y>
<width>581</width>
<height>381</height>
</rect>
</property>
</widget>
<widget class="QPushButton" name="pushButton">
<property name="geometry">
<rect>
<x>300</x>
<y>460</y>
<width>93</width>
<height>28</height>
</rect>
</property>
<property name="text">
<string>Close</string>
</property>
</widget>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>

View file

@ -15,6 +15,8 @@
#include <shellapi.h>
#endif
#include <QDesktopServices>
Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
_ui.setupUi(this);
@ -36,10 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
_ui.statusLabel->setText("");
_ui.plainTextEdit->setReadOnly(true);
setWindowTitle("Nitpick - v1.2");
// Coming soon to a nitpick near you...
//// _helpWindow.textBrowser->setText()
setWindowTitle("Nitpick - v1.3.2");
}
Nitpick::~Nitpick() {
@ -287,7 +286,7 @@ void Nitpick::about() {
}
void Nitpick::content() {
_helpWindow.show();
QDesktopServices::openUrl(QUrl("https://github.com/highfidelity/hifi/blob/master/tools/nitpick/README.md"));
}
void Nitpick::setUserText(const QString& user) {

View file

@ -18,7 +18,6 @@
#include "../Downloader.h"
#include "../Test.h"
#include "HelpWindow.h"
#include "../TestRunner.h"
#include "../AWSInterface.h"
@ -116,8 +115,6 @@ private:
bool _isRunningFromCommandline{ false };
HelpWindow _helpWindow;
void* _caller;
};

View file

@ -803,7 +803,7 @@
<x>0</x>
<y>0</y>
<width>720</width>
<height>22</height>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@ -843,7 +843,7 @@
</action>
<action name="actionContent">
<property name="text">
<string>Content</string>
<string>Online readme</string>
</property>
</action>
</widget>

View file

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 51b3237a2992bd449a58ade16e52d0e0
guid: 02111c50e71dd664da8ad5c6a6eca767
folderAsset: yes
DefaultImporter:
externalObjects: {}

View file

@ -85,9 +85,9 @@ class AvatarExporter : MonoBehaviour {
{"LeftHandRing3", new Quaternion(-0.4936301f, 0.5097645f, -0.5061787f, -0.4901562f)},
{"LeftHandRing2", new Quaternion(-0.5089865f, 0.4943658f, -0.4909532f, -0.5054707f)},
{"LeftHandRing1", new Quaternion(-0.5020972f, 0.5005084f, -0.4979034f, -0.4994819f)},
{"LeftHandThumb3", new Quaternion(-0.7228092f, 0.2988393f, -0.4472938f, -0.4337862f)},
{"LeftHandThumb2", new Quaternion(-0.7554525f, 0.2018595f, -0.3871402f, -0.4885356f)},
{"LeftHandThumb1", new Quaternion(-0.7276843f, 0.2878546f, -0.439926f, -0.4405459f)},
{"LeftHandThumb3", new Quaternion(-0.6617184f, 0.2884935f, -0.3604706f, -0.5907297f)},
{"LeftHandThumb2", new Quaternion(-0.6935627f, 0.1995147f, -0.2805665f, -0.6328092f)},
{"LeftHandThumb1", new Quaternion(-0.6663674f, 0.278572f, -0.3507071f, -0.5961183f)},
{"LeftEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
{"LeftFoot", new Quaternion(0.009215056f, 0.3612514f, 0.9323555f, -0.01121602f)},
{"LeftHand", new Quaternion(-0.4797408f, 0.5195366f, -0.5279632f, -0.4703038f)},
@ -110,9 +110,9 @@ class AvatarExporter : MonoBehaviour {
{"RightHandRing3", new Quaternion(0.4933217f, 0.5102056f, -0.5064691f, 0.4897075f)},
{"RightHandRing2", new Quaternion(0.5085972f, 0.494844f, -0.4913519f, 0.505007f)},
{"RightHandRing1", new Quaternion(0.502959f, 0.4996676f, -0.4970418f, 0.5003144f)},
{"RightHandThumb3", new Quaternion(0.7221864f, 0.3001843f, -0.4482129f, 0.4329457f)},
{"RightHandThumb2", new Quaternion(0.755621f, 0.20102f, -0.386691f, 0.4889769f)},
{"RightHandThumb1", new Quaternion(0.7277303f, 0.2876409f, -0.4398623f, 0.4406733f)},
{"RightHandThumb3", new Quaternion(0.6611374f, 0.2896575f, -0.3616535f, 0.5900872f)},
{"RightHandThumb2", new Quaternion(0.6937408f, 0.1986776f, -0.279922f, 0.6331626f)},
{"RightHandThumb1", new Quaternion(0.6664271f, 0.2783172f, -0.3505667f, 0.596253f)},
{"RightEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
{"RightFoot", new Quaternion(-0.009482829f, 0.3612484f, 0.9323512f, 0.01144584f)},
{"RightHand", new Quaternion(0.4797273f, 0.5195542f, -0.5279628f, 0.4702987f)},
@ -482,6 +482,7 @@ class ExportProjectWindow : EditorWindow {
const int BUTTON_FONT_SIZE = 16;
const int LABEL_FONT_SIZE = 16;
const int TEXT_FIELD_FONT_SIZE = 14;
const int TEXT_FIELD_HEIGHT = 20;
const int ERROR_FONT_SIZE = 12;
string projectName = "";
@ -508,6 +509,7 @@ class ExportProjectWindow : EditorWindow {
labelStyle.fontSize = LABEL_FONT_SIZE;
GUIStyle textStyle = new GUIStyle(GUI.skin.textField);
textStyle.fontSize = TEXT_FIELD_FONT_SIZE;
textStyle.fixedHeight = TEXT_FIELD_HEIGHT;
GUIStyle errorStyle = new GUIStyle(GUI.skin.label);
errorStyle.fontSize = ERROR_FONT_SIZE;
errorStyle.normal.textColor = Color.red;

View file

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: c7a34be82b3ae554ea097963914b083f
guid: 00403fdc52187214c8418bc0a7f387e2
MonoImporter:
externalObjects: {}
serializedVersion: 2

View file

@ -1,15 +1,19 @@
Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter.
To create a new avatar project:
1. Import your .fbx avatar model into Unity Assets (drag and drop file into Assets window or use Assets menu > Import New Assets).
2. Select the .fbx avatar that you imported in the Assets window, and in the Rig section of the Inspector window set the Animation Type to Humanoid and choose Apply.
3. With the .fbx avatar still selected, select High Fidelity menu > Export New Avatar.
1. Import your .fbx avatar model into your Unity project's Assets by either dragging and dropping the file into the Assets window or by using Assets menu > Import New Assets.
2. Select the .fbx avatar that you imported in step 1 in the Assets window, and in the Rig section of the Inspector window set the Animation Type to Humanoid and choose Apply.
3. With the .fbx avatar still selected in the Assets window, choose High Fidelity menu > Export New Avatar.
4. Select a name for your avatar project (this will be used to create a directory with that name), as well as the target location for your project folder.
5. Once it is exported, your project directory will open in File Explorer.
To update an existing avatar project:
1. Select the existing .fbx avatar in the Assets window that you would like to re-export.
2. Select High Fidelity menu > Update Existing Avatar and choose the .fst file you would like to update.
3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file.
2. Choose High Fidelity menu > Update Existing Avatar and browse to the .fst file you would like to update.
3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your selected avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file before performing the update.
4. Once it is updated, your project directory will open in File Explorer.
* WARNING *
If you are using any external textures as part of your .fbx model, be sure they are copied into the textures folder that is created in the project folder after exporting a new avatar.
For further details including troubleshooting tips, see the full documentation at https://docs.highfidelity.com/create-and-explore/avatars/create-avatars/unity-extension

View file

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 71e72751b2810fc4993ff53291c430b6
guid: 30b2b6221fd08234eb07c4d6d525d32e
TextScriptImporter:
externalObjects: {}
userData:

View file

@ -1 +1 @@
"C:\Program Files\Unity\Editor\Unity.exe" -quit -batchmode -projectPath %CD% -exportPackage "Assets" "avatarExporter.unitypackage"
Unity -quit -batchmode -projectPath %CD% -exportPackage "Assets" "avatarExporter.unitypackage"