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

This commit is contained in:
Olivier Prat 2017-04-28 19:20:29 +02:00
commit 9fc9c60733
51 changed files with 3138 additions and 1465 deletions

View file

@ -1,7 +1,7 @@
###Dependencies
* [cmake](https://cmake.org/download/) ~> 3.3.2
* [Qt](https://www.qt.io/download-open-source) ~> 5.6.1
* [Qt](https://www.qt.io/download-open-source) ~> 5.6.2
* [OpenSSL](https://www.openssl.org/community/binaries.html)
* IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities.
* [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional)
@ -46,8 +46,8 @@ This can either be entered directly into your shell session before you build or
The path it needs to be set to will depend on where and how Qt5 was installed. e.g.
export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.1/clang_64/lib/cmake/
export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.1-1/lib/cmake
export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.2/clang_64/lib/cmake/
export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.2/lib/cmake
export QT_CMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake
####Generating build files
@ -64,7 +64,7 @@ Any variables that need to be set for CMake to find dependencies can be set as E
For example, to pass the QT_CMAKE_PREFIX_PATH variable during build file generation:
cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.1/lib/cmake
cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.2/lib/cmake
####Finding Dependencies

View file

@ -16,16 +16,12 @@ For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR:
Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change.
###Qt
You can use the online installer or the offline installer.
Download and install the [Qt 5.6.2 for macOS](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-mac-x64-clang-5.6.2.dmg).
* [Download the online installer](https://www.qt.io/download-open-source/#section-2)
* When it asks you to select components, select the following:
* Qt > Qt 5.6
* [Download the offline installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-mac-x64-clang-5.6.1-1.dmg)
Keep the default components checked when going through the installer.
Once Qt is installed, you need to manually configure the following:
* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.1/5.6/clang_64/lib/cmake/` directory.
* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.2/5.6/clang_64/lib/cmake/` directory.
###Xcode
If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles.

View file

@ -8,23 +8,23 @@ Note: Newer versions of Visual Studio are not yet compatible.
###Step 2. Installing CMake
Download and install the CMake 3.8.0-rc2 "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer.
Download and install the [CMake 3.8.0 win64-x64 Installer](https://cmake.org/files/v3.8/cmake-3.8.0-win64-x64.msi). Make sure "Add CMake to system PATH for all users" is checked when going through the installer.
###Step 3. Installing Qt
Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe). Please note that the download file is large (850MB) and may take some time.
Download and install the [Qt 5.6.2 for Windows 64-bit (VS 2013)](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-windows-x86-msvc2013_64-5.6.2.exe).
Make sure to select all components when going through the installer.
Keep the default components checked when going through the installer.
###Step 4. Setting Qt Environment Variable
Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search).
* Set "Variable name": QT_CMAKE_PREFIX_PATH
* Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake`
* Set "Variable value": `%QT_DIR%\5.6\msvc2013_64\lib\cmake`
###Step 5. Installing OpenSSL
Download and install the "Win64 OpenSSL v1.0.2k" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html).
Download and install the [Win64 OpenSSL v1.0.2k Installer](https://slproweb.com/download/Win64OpenSSL-1_0_2k.exe).
###Step 6. Running CMake to Generate Build Files
@ -77,5 +77,5 @@ If not, add the directory where nmake is located to the PATH environment variabl
####Qt is throwing an error
Make sure you have the correct version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly.
Make sure you have the correct version (5.6.2) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly.

View file

@ -50,6 +50,12 @@
"type": "inverseKinematics",
"data": {
"targets": [
{
"jointName": "Hips",
"positionVar": "hipsPosition",
"rotationVar": "hipsRotation",
"typeVar": "hipsType"
},
{
"jointName": "RightHand",
"positionVar": "rightHandPosition",
@ -75,10 +81,10 @@
"typeVar": "leftFootType"
},
{
"jointName": "Neck",
"positionVar": "neckPosition",
"rotationVar": "neckRotation",
"typeVar": "neckType"
"jointName": "Spine2",
"positionVar": "spine2Position",
"rotationVar": "spine2Rotation",
"typeVar": "spine2Type"
},
{
"jointName": "Head",
@ -91,20 +97,27 @@
"children": []
},
{
"id": "manipulatorOverlay",
"id": "hipsManipulatorOverlay",
"type": "overlay",
"data": {
"alpha": 1.0,
"boneSet": "spineOnly"
"alpha": 0.0,
"boneSet": "hipsOnly"
},
"children": [
{
"id": "spineLean",
"id": "hipsManipulator",
"type": "manipulator",
"data": {
"alpha": 0.0,
"alphaVar": "hipsManipulatorAlpha",
"joints": [
{ "type": "absoluteRotation", "jointName": "Spine", "var": "lean" }
{
"jointName": "Hips",
"rotationType": "absolute",
"translationType": "absolute",
"rotationVar": "hipsManipulatorRotation",
"translationVar": "hipsManipulatorPosition"
}
]
},
"children": []

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#EF3B4E;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1_1_">
<path class="st0" d="M47.2,41.3l-9.1-9.1c-0.8-0.8-1.9-1.1-3-1l-2.4-2.4c1.8-2.6,2.8-5.7,2.8-9c0-8.9-7.2-16.1-16.1-16.1
S3.3,11,3.3,19.8c0,8.9,7.2,16.1,16.1,16.1c4.1,0,7.8-1.5,10.6-4l2.2,2.2c-0.2,1.1,0.1,2.2,1,3l9.1,9.1c1.4,1.4,3.6,1.4,4.9,0
C48.5,44.9,48.5,42.7,47.2,41.3z M19.4,32.2c-6.8,0-12.3-5.5-12.3-12.3S12.6,7.6,19.4,7.6s12.3,5.5,12.3,12.3
C31.8,26.6,26.2,32.2,19.4,32.2z"/>
</g>
<circle class="st1" cx="43.5" cy="6.5" r="5.9"/>
</svg>

After

Width:  |  Height:  |  Size: 911 B

View file

@ -17,7 +17,7 @@ import QtGraphicalEffects 1.0
import "toolbars"
import "../styles-uit"
Rectangle {
Item {
id: root;
property string userName: "";
property string placeName: "";
@ -35,6 +35,7 @@ Rectangle {
property string timePhrase: pastTime(timestamp);
property int onlineUsers: 0;
property bool isConcurrency: action === 'concurrency';
property bool isAnnouncement: action === 'announcement';
property bool isStacked: !isConcurrency && drillDownToPlace;
property int textPadding: 10;
@ -44,7 +45,7 @@ Rectangle {
property int textSizeSmall: 18;
property int stackShadowNarrowing: 5;
property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif");
property int shadowHeight: 20;
property int shadowHeight: 10;
HifiConstants { id: hifi }
function pastTime(timestamp) { // Answer a descriptive string
@ -69,6 +70,40 @@ Rectangle {
}
property bool hasGif: imageUrl.indexOf('.gif') === (imageUrl.length - 4);
DropShadow {
visible: isStacked;
anchors.fill: shadow1;
source: shadow1;
verticalOffset: 2;
radius: 4;
samples: 9;
color: hifi.colors.baseGrayShadow;
}
Rectangle {
id: shadow1;
visible: isStacked;
width: parent.width - stackShadowNarrowing;
height: shadowHeight;
anchors {
top: parent.bottom;
horizontalCenter: parent.horizontalCenter;
}
}
DropShadow {
anchors.fill: base;
source: base;
verticalOffset: 2;
radius: 4;
samples: 9;
color: hifi.colors.baseGrayShadow;
}
Rectangle {
id: base;
color: "white";
anchors.fill: parent;
}
AnimatedImage {
id: animation;
// Always visible, to drive loading, but initially covered up by lobby during load.
@ -80,7 +115,7 @@ Rectangle {
id: lobby;
visible: !hasGif || (animation.status !== Image.Ready);
width: parent.width - (isConcurrency ? 0 : (2 * smallMargin));
height: parent.height - (isConcurrency ? 0 : smallMargin);
height: parent.height - messageHeight - (isConcurrency ? 0 : smallMargin);
source: thumbnail || defaultThumbnail;
fillMode: Image.PreserveAspectCrop;
anchors {
@ -95,41 +130,13 @@ Rectangle {
}
}
}
Rectangle {
id: shadow1;
visible: isStacked;
width: parent.width - stackShadowNarrowing;
height: shadowHeight / 2;
anchors {
top: parent.bottom;
horizontalCenter: parent.horizontalCenter;
}
gradient: Gradient {
GradientStop { position: 0.0; color: "gray" }
GradientStop { position: 1.0; color: "white" }
}
}
Rectangle {
id: shadow2;
visible: isStacked;
width: shadow1.width - stackShadowNarrowing;
height: shadowHeight / 2;
anchors {
top: shadow1.bottom;
horizontalCenter: parent.horizontalCenter;
}
gradient: Gradient {
GradientStop { position: 0.0; color: "gray" }
GradientStop { position: 1.0; color: "white" }
}
}
property int dropHorizontalOffset: 0;
property int dropVerticalOffset: 1;
property int dropRadius: 2;
property int dropSamples: 9;
property int dropSpread: 0;
DropShadow {
visible: true;
visible: showPlace; // Do we have to check for whatever the modern equivalent is for desktop.gradientsSupported?
source: place;
anchors.fill: place;
horizontalOffset: dropHorizontalOffset;
@ -139,12 +146,12 @@ Rectangle {
color: hifi.colors.black;
spread: dropSpread;
}
RalewayLight {
RalewaySemiBold {
id: place;
visible: showPlace;
text: placeName;
color: hifi.colors.white;
size: 38;
size: textSize;
elide: Text.ElideRight; // requires constrained width
anchors {
top: parent.top;
@ -153,57 +160,44 @@ Rectangle {
margins: textPadding;
}
}
Rectangle {
id: rectRow
z: 1
width: message.width + (users.visible ? users.width + bottomRow.spacing : 0)
+ (icon.visible ? icon.width + bottomRow.spacing: 0) + bottomRow.spacing;
height: messageHeight + 1;
radius: 25
anchors {
bottom: parent.bottom
left: parent.left
leftMargin: textPadding
bottomMargin: textPadding
Row {
FiraSansRegular {
id: users;
visible: isConcurrency || isAnnouncement;
text: onlineUsers;
size: textSize;
color: messageColor;
anchors.verticalCenter: message.verticalCenter;
}
Row {
id: bottomRow
FiraSansRegular {
id: users;
visible: isConcurrency;
text: onlineUsers;
size: textSize;
color: messageColor;
anchors.verticalCenter: message.verticalCenter;
}
Image {
id: icon;
source: "../../images/snap-icon.svg"
width: 40;
height: 40;
visible: action === 'snapshot';
}
RalewayRegular {
id: message;
text: isConcurrency ? ((onlineUsers === 1) ? "person" : "people") : (drillDownToPlace ? "snapshots" : ("by " + userName));
size: textSizeSmall;
color: messageColor;
elide: Text.ElideRight; // requires a width to be specified`
anchors {
bottom: parent.bottom;
bottomMargin: parent.spacing;
}
}
spacing: textPadding;
height: messageHeight;
Image {
id: icon;
source: "../../images/snap-icon.svg"
width: 40;
height: 40;
visible: (action === 'snapshot') && (messageHeight >= 40);
}
RalewayRegular {
id: message;
text: isConcurrency ? ((onlineUsers === 1) ? "person" : "people") : (isAnnouncement ? "connections" : (drillDownToPlace ? "snapshots" : ("by " + userName)));
size: textSizeSmall;
color: messageColor;
elide: Text.ElideRight; // requires a width to be specified`
width: root.width - textPadding
- (users.visible ? users.width + parent.spacing : 0)
- (icon.visible ? icon.width + parent.spacing : 0)
- (actionIcon.width + (2 * smallMargin));
anchors {
bottom: parent.bottom;
left: parent.left;
leftMargin: 4
bottomMargin: parent.spacing;
}
}
spacing: textPadding;
height: messageHeight;
anchors {
bottom: parent.bottom;
left: parent.left;
leftMargin: textPadding;
}
}
// These two can be supplied to provide hover behavior.
// For example, AddressBarDialog provides functions that set the current list view item
@ -218,37 +212,22 @@ Rectangle {
onEntered: hoverThunk();
onExited: unhoverThunk();
}
Rectangle {
id: rectIcon
z: 1
width: 32
height: 32
radius: 15
StateImage {
id: actionIcon;
imageURL: "../../images/info-icon-2-state.svg";
size: 30;
buttonState: messageArea.containsMouse ? 1 : 0;
anchors {
bottom: parent.bottom;
right: parent.right;
bottomMargin: textPadding;
rightMargin: textPadding;
}
StateImage {
id: actionIcon;
imageURL: "../../images/info-icon-2-state.svg";
size: 32;
buttonState: messageArea.containsMouse ? 1 : 0;
anchors {
bottom: parent.bottom;
right: parent.right;
//margins: smallMargin;
}
margins: smallMargin;
}
}
MouseArea {
id: messageArea;
width: rectIcon.width;
height: rectIcon.height;
anchors.fill: rectIcon
width: parent.width;
height: messageHeight;
anchors.top: lobby.bottom;
acceptedButtons: Qt.LeftButton;
onClicked: goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId));
hoverEnabled: true;

View file

@ -0,0 +1,218 @@
//
// Feed.qml
// qml/hifi
//
// Displays a particular type of feed
//
// Created by Howard Stearns on 4/18/2017
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import Hifi 1.0
import QtQuick 2.5
import QtGraphicalEffects 1.0
import "toolbars"
import "../styles-uit"
Column {
id: root;
visible: false;
property int cardWidth: 212;
property int cardHeight: 152;
property int textPadding: 10;
property int smallMargin: 4;
property int messageHeight: 40;
property int textSize: 24;
property int textSizeSmall: 18;
property int stackShadowNarrowing: 5;
property int stackedCardShadowHeight: 4;
property int labelSize: 20;
property string metaverseServerUrl: '';
property string actions: 'snapshot';
onActionsChanged: fillDestinations();
Component.onCompleted: fillDestinations();
property string labelText: actions;
property string filter: '';
onFilterChanged: filterChoicesByText();
property var goFunction: null;
HifiConstants { id: hifi }
ListModel { id: suggestions; }
function resolveUrl(url) {
return (url.indexOf('/') === 0) ? (metaverseServerUrl + url) : url;
}
function makeModelData(data) { // create a new obj from data
// ListModel elements will only ever have those properties that are defined by the first obj that is added.
// So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story.
var name = data.place_name,
tags = data.tags || [data.action, data.username],
description = data.description || "",
thumbnail_url = data.thumbnail_url || "";
if (actions === 'concurrency,snapshot') {
// A temporary hack for simulating announcements. We won't use this in production, but if requested, we'll use this data like announcements.
data.details.connections = 4;
data.action = 'announcement';
}
return {
place_name: name,
username: data.username || "",
path: data.path || "",
created_at: data.created_at || "",
action: data.action || "",
thumbnail_url: resolveUrl(thumbnail_url),
image_url: resolveUrl(data.details && data.details.image_url),
metaverseId: (data.id || "").toString(), // Some are strings from server while others are numbers. Model objects require uniformity.
tags: tags,
description: description,
online_users: data.details.connections || data.details.concurrency || 0,
drillDownToPlace: false,
searchText: [name].concat(tags, description || []).join(' ').toUpperCase()
}
}
property var allStories: [];
property var placeMap: ({}); // Used for making stacks.
property int requestId: 0;
function getUserStoryPage(pageNumber, cb, cb1) { // cb(error) after all pages of domain data have been added to model
// If supplied, cb1 will be run after the first page IFF it is not the last, for responsiveness.
var options = [
'now=' + new Date().toISOString(),
'include_actions=' + actions,
'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'),
'require_online=true',
'protocol=' + encodeURIComponent(AddressManager.protocolVersion()),
'page=' + pageNumber
];
var url = metaverseBase + 'user_stories?' + options.join('&');
var thisRequestId = ++requestId;
getRequest(url, function (error, data) {
if ((thisRequestId !== requestId) || handleError(url, error, data, cb)) {
return; // abandon stale requests
}
allStories = allStories.concat(data.user_stories.map(makeModelData));
if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now
if ((pageNumber === 1) && cb1) {
cb1();
}
return getUserStoryPage(pageNumber + 1, cb);
}
cb();
});
}
function fillDestinations() { // Public
var filter = makeFilteredStoryProcessor(), counter = 0;
allStories = [];
suggestions.clear();
placeMap = {};
getUserStoryPage(1, function (error) {
allStories.slice(counter).forEach(filter);
console.log('user stories query', actions, error || 'ok', allStories.length, 'filtered to', suggestions.count);
root.visible = !!suggestions.count;
}, function () { // If there's more than a page, put what we have in the model right away, keeping track of how many are processed.
allStories.forEach(function (story) {
counter++;
filter(story);
root.visible = !!suggestions.count;
});
});
}
function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches
var words = filter.toUpperCase().split(/\s+/).filter(identity);
function suggestable(story) {
if (story.action === 'snapshot') {
return true;
}
return (story.place_name !== AddressManager.placename); // Not our entry, but do show other entry points to current domain.
}
function matches(story) {
if (!words.length) {
return suggestable(story);
}
return words.every(function (word) {
return story.searchText.indexOf(word) >= 0;
});
}
function addToSuggestions(place) {
var collapse = ((actions === 'concurrency,snapshot') && (place.action !== 'concurrency')) || (place.action === 'announcement');
if (collapse) {
var existing = placeMap[place.place_name];
if (existing) {
existing.drillDownToPlace = true;
return;
}
}
suggestions.append(place);
if (collapse) {
placeMap[place.place_name] = suggestions.get(suggestions.count - 1);
} else if (place.action === 'concurrency') {
suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories).
}
}
return function (story) {
if (matches(story)) {
addToSuggestions(story);
}
};
}
function filterChoicesByText() {
suggestions.clear();
placeMap = {};
allStories.forEach(makeFilteredStoryProcessor());
root.visible = !!suggestions.count;
}
RalewayBold {
id: label;
text: labelText;
color: hifi.colors.blueAccent;
size: labelSize;
}
ListView {
id: scroll;
model: suggestions;
orientation: ListView.Horizontal;
highlightMoveDuration: -1;
highlightMoveVelocity: -1;
highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.primaryHighlight; z: 1; }
currentIndex: -1;
spacing: 12;
width: parent.width;
height: cardHeight + stackedCardShadowHeight;
delegate: Card {
id: card;
width: cardWidth;
height: cardHeight;
goFunction: root.goFunction;
userName: model.username;
placeName: model.place_name;
hifiUrl: model.place_name + model.path;
thumbnail: model.thumbnail_url;
imageUrl: model.image_url;
action: model.action;
timestamp: model.created_at;
onlineUsers: model.online_users;
storyId: model.metaverseId;
drillDownToPlace: model.drillDownToPlace;
textPadding: root.textPadding;
smallMargin: root.smallMargin;
messageHeight: root.messageHeight;
textSize: root.textSize;
textSizeSmall: root.textSizeSmall;
stackShadowNarrowing: root.stackShadowNarrowing;
shadowHeight: root.stackedCardShadowHeight;
hoverThunk: function () { scroll.currentIndex = index; }
unhoverThunk: function () { scroll.currentIndex = -1; }
}
}
}

View file

@ -30,18 +30,15 @@ StackView {
width: parent !== null ? parent.width : undefined
height: parent !== null ? parent.height : undefined
property var eventBridge;
property var allStories: [];
property int cardWidth: 460;
property int cardHeight: 320;
property int cardWidth: 212;
property int cardHeight: 152;
property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/";
property var tablet: null;
Component { id: tabletWebView; TabletWebView {} }
Component.onCompleted: {
fillDestinations();
updateLocationText(false);
fillDestinations();
addressLine.focus = !HMD.active;
root.parentChanged.connect(center);
center();
@ -157,7 +154,7 @@ StackView {
left: parent.left;
}
HifiStyles.RalewayLight {
HifiStyles.RalewayRegular {
id: notice;
font.pixelSize: hifi.fonts.pixelSize * 0.7;
anchors {
@ -190,7 +187,6 @@ StackView {
}
font.pixelSize: hifi.fonts.pixelSize * 0.75
onTextChanged: {
filterChoicesByText();
updateLocationText(text.length > 0);
}
onAccepted: {
@ -225,109 +221,78 @@ StackView {
}
}
}
Rectangle {
id: topBar
height: 37
color: hifiStyleConstants.colors.white
anchors.right: parent.right
anchors.rightMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.topMargin: 0
anchors.top: addressBar.bottom
Row {
id: thing
spacing: 5 * hifi.layout.spacing
anchors {
top: parent.top;
left: parent.left
leftMargin: 25
}
TabletTextButton {
id: allTab;
text: "ALL";
property string includeActions: 'snapshot,concurrency';
selected: allTab === selectedTab;
action: tabSelect;
}
TabletTextButton {
id: placeTab;
text: "PLACES";
property string includeActions: 'concurrency';
selected: placeTab === selectedTab;
action: tabSelect;
}
TabletTextButton {
id: snapTab;
text: "SNAP";
property string includeActions: 'snapshot';
selected: snapTab === selectedTab;
action: tabSelect;
id: bgMain;
anchors {
top: addressBar.bottom;
bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom;
left: parent.left;
right: parent.right;
}
Rectangle {
id: addressShadow;
width: parent.width;
height: 42 - 33;
gradient: Gradient {
GradientStop { position: 0.0; color: "gray" }
GradientStop { position: 1.0; color: "white" }
}
}
}
Rectangle {
id: bgMain
color: hifiStyleConstants.colors.white
anchors.bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom
anchors.bottomMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.top: topBar.bottom
anchors.topMargin: 0
ListModel { id: suggestions }
ListView {
id: scroll
property int stackedCardShadowHeight: 0;
clip: true
spacing: 14
Rectangle { // Column margins require QtQuick 2.7, which we don't use yet.
id: column;
property real pad: 10;
width: bgMain.width - column.pad;
height: stack.height;
color: "transparent";
anchors {
bottom: parent.bottom
top: parent.top
left: parent.left
right: parent.right
leftMargin: 10
left: parent.left;
leftMargin: column.pad;
top: addressShadow.bottom;
topMargin: column.pad;
}
model: suggestions
orientation: ListView.Vertical
delegate: Card {
width: cardWidth;
height: cardHeight;
goFunction: goCard;
userName: model.username;
placeName: model.place_name;
hifiUrl: model.place_name + model.path;
thumbnail: model.thumbnail_url;
imageUrl: model.image_url;
action: model.action;
timestamp: model.created_at;
onlineUsers: model.online_users;
storyId: model.metaverseId;
drillDownToPlace: model.drillDownToPlace;
shadowHeight: scroll.stackedCardShadowHeight;
hoverThunk: function () { scroll.currentIndex = index; }
unhoverThunk: function () { scroll.currentIndex = -1; }
Column {
id: stack;
width: column.width;
spacing: 33 - places.labelSize;
Feed {
id: happeningNow;
width: parent.width;
cardWidth: 312 + (2 * 4);
cardHeight: 163 + (2 * 4);
metaverseServerUrl: addressBarDialog.metaverseServerUrl;
labelText: 'HAPPENING NOW';
//actions: 'concurrency,snapshot'; // uncomment this line instead of next to produce fake announcement data for testing.
actions: 'announcement';
filter: addressLine.text;
goFunction: goCard;
}
Feed {
id: places;
width: parent.width;
cardWidth: 210;
cardHeight: 110 + messageHeight;
messageHeight: 44;
metaverseServerUrl: addressBarDialog.metaverseServerUrl;
labelText: 'PLACES';
actions: 'concurrency';
filter: addressLine.text;
goFunction: goCard;
}
Feed {
id: snapshots;
width: parent.width;
cardWidth: 143 + (2 * 4);
cardHeight: 75 + messageHeight + 4;
messageHeight: 32;
textPadding: 6;
metaverseServerUrl: addressBarDialog.metaverseServerUrl;
labelText: 'RECENT SNAPS';
actions: 'snapshot';
filter: addressLine.text;
goFunction: goCard;
}
}
highlightMoveDuration: -1;
highlightMoveVelocity: -1;
highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.blueHighlight; z: 1; }
}
}
@ -409,131 +374,13 @@ StackView {
return true;
}
function resolveUrl(url) {
return (url.indexOf('/') === 0) ? (addressBarDialog.metaverseServerUrl + url) : url;
}
function makeModelData(data) { // create a new obj from data
// ListModel elements will only ever have those properties that are defined by the first obj that is added.
// So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story.
var name = data.place_name,
tags = data.tags || [data.action, data.username],
description = data.description || "",
thumbnail_url = data.thumbnail_url || "";
return {
place_name: name,
username: data.username || "",
path: data.path || "",
created_at: data.created_at || "",
action: data.action || "",
thumbnail_url: resolveUrl(thumbnail_url),
image_url: resolveUrl(data.details.image_url),
metaverseId: (data.id || "").toString(), // Some are strings from server while others are numbers. Model objects require uniformity.
tags: tags,
description: description,
online_users: data.details.concurrency || 0,
drillDownToPlace: false,
searchText: [name].concat(tags, description || []).join(' ').toUpperCase()
}
}
function suggestable(place) {
if (place.action === 'snapshot') {
return true;
}
return (place.place_name !== AddressManager.placename); // Not our entry, but do show other entry points to current domain.
}
property var selectedTab: allTab;
function tabSelect(textButton) {
selectedTab = textButton;
fillDestinations();
}
property var placeMap: ({});
function addToSuggestions(place) {
var collapse = allTab.selected && (place.action !== 'concurrency');
if (collapse) {
var existing = placeMap[place.place_name];
if (existing) {
existing.drillDownToPlace = true;
return;
}
}
suggestions.append(place);
if (collapse) {
placeMap[place.place_name] = suggestions.get(suggestions.count - 1);
} else if (place.action === 'concurrency') {
suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories).
}
}
property int requestId: 0;
function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model
var options = [
'now=' + new Date().toISOString(),
'include_actions=' + selectedTab.includeActions,
'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'),
'require_online=true',
'protocol=' + encodeURIComponent(AddressManager.protocolVersion()),
'page=' + pageNumber
];
var url = metaverseBase + 'user_stories?' + options.join('&');
var thisRequestId = ++requestId;
getRequest(url, function (error, data) {
if ((thisRequestId !== requestId) || handleError(url, error, data, cb)) {
return;
}
var stories = data.user_stories.map(function (story) { // explicit single-argument function
return makeModelData(story, url);
});
allStories = allStories.concat(stories);
stories.forEach(makeFilteredPlaceProcessor());
if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now
return getUserStoryPage(pageNumber + 1, cb);
}
cb();
});
}
function makeFilteredPlaceProcessor() { // answer a function(placeData) that adds it to suggestions if it matches
var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity),
data = allStories;
function matches(place) {
if (!words.length) {
return suggestable(place);
}
return words.every(function (word) {
return place.searchText.indexOf(word) >= 0;
});
}
return function (place) {
if (matches(place)) {
addToSuggestions(place);
}
};
}
function filterChoicesByText() {
suggestions.clear();
placeMap = {};
allStories.forEach(makeFilteredPlaceProcessor());
}
function fillDestinations() {
allStories = [];
suggestions.clear();
placeMap = {};
getUserStoryPage(1, function (error) {
console.log('user stories query', error || 'ok', allStories.length);
});
}
function updateLocationText(enteringAddress) {
if (enteringAddress) {
notice.text = "Go To a place, @user, path, or network address:";
notice.color = hifiStyleConstants.colors.baseGrayHighlight;
} else {
notice.text = AddressManager.isConnected ? "Your location:" : "Not Connected";
notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.baseGrayHighlight : hifiStyleConstants.colors.redHighlight;
notice.text = AddressManager.isConnected ? "YOUR LOCATION" : "NOT CONNECTED";
notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.blueHighlight : hifiStyleConstants.colors.redHighlight;
// Display hostname, which includes ip address, localhost, and other non-placenames.
location.text = (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : '');
}

View file

@ -535,6 +535,7 @@ bool setupEssentials(int& argc, char** argv) {
DependencyManager::set<OctreeStatsProvider>(nullptr, qApp->getOcteeSceneStats());
DependencyManager::set<AvatarBookmarks>();
DependencyManager::set<LocationBookmarks>();
DependencyManager::set<Snapshot>();
return previousSessionCrashed;
}
@ -2052,6 +2053,7 @@ void Application::initializeUi() {
rootContext->setContextProperty("Scene", DependencyManager::get<SceneScriptingInterface>().data());
rootContext->setContextProperty("Render", _renderEngine->getConfiguration().get());
rootContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface());
rootContext->setContextProperty("Snapshot", DependencyManager::get<Snapshot>().data());
rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor());
@ -5504,6 +5506,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance());
scriptEngine->registerGlobalObject("Stats", Stats::getInstance());
scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance());
scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get<Snapshot>().data());
scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance());
scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get<AudioClient>()->getStats().data());
scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get<AudioScope>().data());
@ -6448,7 +6451,7 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa
// Get a screenshot and save it
QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio));
// If we're not doing an animated snapshot as well...
if (!includeAnimated || !(SnapshotAnimated::alsoTakeAnimatedSnapshot.get())) {
if (!includeAnimated) {
// Tell the dependency manager that the capture of the still snapshot has taken place.
emit DependencyManager::get<WindowScriptingInterface>()->stillSnapshotTaken(path, notify);
} else {

View file

@ -23,6 +23,8 @@
#include "DiscoverabilityManager.h"
#include "Menu.h"
#include <QThread>
const Discoverability::Mode DEFAULT_DISCOVERABILITY_MODE = Discoverability::Friends;
DiscoverabilityManager::DiscoverabilityManager() :
@ -37,6 +39,13 @@ const QString API_USER_HEARTBEAT_PATH = "/api/v1/user/heartbeat";
const QString SESSION_ID_KEY = "session_id";
void DiscoverabilityManager::updateLocation() {
// since we store the last location and compare it to
// the current one in this function, we need to do this in
// the object's main thread (or use a mutex)
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "updateLocation");
return;
}
auto accountManager = DependencyManager::get<AccountManager>();
auto addressManager = DependencyManager::get<AddressManager>();
auto& domainHandler = DependencyManager::get<NodeList>()->getDomainHandler();
@ -143,7 +152,7 @@ void DiscoverabilityManager::removeLocation() {
void DiscoverabilityManager::setDiscoverabilityMode(Discoverability::Mode discoverabilityMode) {
if (static_cast<Discoverability::Mode>(_mode.get()) != discoverabilityMode) {
// update the setting to the new value
_mode.set(static_cast<int>(discoverabilityMode));
updateLocation(); // update right away

View file

@ -156,6 +156,8 @@ Menu::Menu() {
// Audio > Show Level Meter
addCheckableActionToQMenuAndActionHash(audioMenu, MenuOption::AudioTools, 0, false);
addCheckableActionToQMenuAndActionHash(audioMenu, MenuOption::AudioNoiseReduction, 0, true,
audioIO.data(), SLOT(toggleAudioNoiseReduction()));
// Avatar menu ----------------------------------
MenuWrapper* avatarMenu = addMenu("Avatar");
@ -196,6 +198,9 @@ Menu::Menu() {
0, // QML Qt::Key_Apostrophe,
qApp, SLOT(resetSensors()));
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableCharacterController, 0, true,
avatar.get(), SLOT(updateMotionBehaviorFromMenu()));
// Avatar > AvatarBookmarks related menus -- Note: the AvatarBookmarks class adds its own submenus here.
auto avatarBookmarks = DependencyManager::get<AvatarBookmarks>();
avatarBookmarks->setupMenus(this, avatarMenu);
@ -532,10 +537,6 @@ Menu::Menu() {
avatar.get(), SLOT(updateMotionBehaviorFromMenu()),
UNSPECIFIED_POSITION, "Developer");
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableCharacterController, 0, true,
avatar.get(), SLOT(updateMotionBehaviorFromMenu()),
UNSPECIFIED_POSITION, "Developer");
// Developer > Hands >>>
MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands");
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false,
@ -622,8 +623,6 @@ Menu::Menu() {
QString("../../hifi/tablet/TabletAudioPreferences.qml"), "AudioPreferencesDialog");
});
addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioNoiseReduction, 0, true,
audioIO.data(), SLOT(toggleAudioNoiseReduction()));
addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoServerAudio, 0, false,
audioIO.data(), SLOT(toggleServerEcho()));
addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoLocalAudio, 0, false,

View file

@ -36,7 +36,7 @@ namespace MenuOption {
const QString AssetMigration = "ATP Asset Migration";
const QString AssetServer = "Asset Browser";
const QString Attachments = "Attachments...";
const QString AudioNoiseReduction = "Audio Noise Reduction";
const QString AudioNoiseReduction = "Noise Reduction";
const QString AudioScope = "Show Scope";
const QString AudioScopeFiftyFrames = "Fifty";
const QString AudioScopeFiveFrames = "Five";
@ -96,7 +96,7 @@ namespace MenuOption {
const QString DontRenderEntitiesAsScene = "Don't Render Entities as Scene";
const QString EchoLocalAudio = "Echo Local Audio";
const QString EchoServerAudio = "Echo Server Audio";
const QString EnableCharacterController = "Enable avatar collisions";
const QString EnableCharacterController = "Collide with world";
const QString EnableInverseKinematics = "Enable Inverse Kinematics";
const QString EntityScriptServerLog = "Entity Script Server Log";
const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation";

View file

@ -465,6 +465,10 @@ public:
void removeHoldAction(AvatarActionHold* holdAction); // thread-safe
void updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& postUpdatePose);
// derive avatar body position and orientation from the current HMD Sensor location.
// results are in HMD frame
glm::mat4 deriveBodyFromHMDSensor() const;
public slots:
void increaseSize();
void decreaseSize();
@ -553,9 +557,7 @@ private:
void setVisibleInSceneIfReady(Model* model, const render::ScenePointer& scene, bool visiblity);
// derive avatar body position and orientation from the current HMD Sensor location.
// results are in HMD frame
glm::mat4 deriveBodyFromHMDSensor() const;
private:
virtual void updatePalms() override {}
void lateUpdatePalms();

View file

@ -119,7 +119,13 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
headParams.rigHeadPosition = extractTranslation(rigHMDMat);
headParams.rigHeadOrientation = extractRotation(rigHMDMat);
headParams.worldHeadOrientation = extractRotation(worldHMDMat);
// TODO: if hips target sensor is valid.
// Copy it into headParams.hipsMatrix, and set headParams.hipsEnabled to true.
headParams.hipsEnabled = false;
} else {
headParams.hipsEnabled = false;
headParams.isInHMD = false;
// We don't have a valid localHeadPosition.

View file

@ -168,6 +168,28 @@ void WindowScriptingInterface::ensureReticleVisible() const {
}
}
/// Display a "browse to directory" dialog. If `directory` is an invalid file or directory the browser will start at the current
/// working directory.
/// \param const QString& title title of the window
/// \param const QString& directory directory to start the file browser at
/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog`
/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue`
QScriptValue WindowScriptingInterface::browseDir(const QString& title, const QString& directory) {
ensureReticleVisible();
QString path = directory;
if (path.isEmpty()) {
path = getPreviousBrowseLocation();
}
#ifndef Q_OS_WIN
path = fixupPathForMac(directory);
#endif
QString result = OffscreenUi::getExistingDirectory(nullptr, title, path);
if (!result.isEmpty()) {
setPreviousBrowseLocation(QFileInfo(result).absolutePath());
}
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
}
/// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current
/// working directory.
/// \param const QString& title title of the window
@ -278,6 +300,10 @@ void WindowScriptingInterface::makeConnection(bool success, const QString& userN
}
}
void WindowScriptingInterface::displayAnnouncement(const QString& message) {
emit announcement(message);
}
bool WindowScriptingInterface::isPhysicsEnabled() {
return qApp->isPhysicsEnabled();
}

View file

@ -51,6 +51,7 @@ public slots:
QScriptValue confirm(const QString& message = "");
QScriptValue prompt(const QString& message = "", const QString& defaultText = "");
CustomPromptResult customPrompt(const QVariant& config);
QScriptValue browseDir(const QString& title = "", const QString& directory = "");
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
@ -58,6 +59,7 @@ public slots:
void copyToClipboard(const QString& text);
void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f);
void makeConnection(bool success, const QString& userNameOrError);
void displayAnnouncement(const QString& message);
void shareSnapshot(const QString& path, const QUrl& href = QUrl(""));
bool isPhysicsEnabled();
@ -73,12 +75,13 @@ signals:
void svoImportRequested(const QString& url);
void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo);
void stillSnapshotTaken(const QString& pathStillSnapshot, bool notify);
void snapshotShared(const QString& error);
void snapshotShared(bool isError, const QString& reply);
void processingGifStarted(const QString& pathStillSnapshot);
void processingGifCompleted(const QString& pathAnimatedSnapshot);
void connectionAdded(const QString& connectionName);
void connectionError(const QString& errorString);
void announcement(const QString& message);
void messageBoxClosed(int id, int button);

View file

@ -116,11 +116,6 @@ void setupPreferences() {
auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter);
preferences->addPreference(preference);
}
{
auto getter = []()->bool { return SnapshotAnimated::alsoTakeAnimatedSnapshot.get(); };
auto setter = [](bool value) { SnapshotAnimated::alsoTakeAnimatedSnapshot.set(value); };
preferences->addPreference(new CheckPreference(SNAPSHOTS, "Take Animated GIF Snapshot", getter, setter));
}
{
auto getter = []()->float { return SnapshotAnimated::snapshotAnimatedDuration.get(); };
auto setter = [](float value) { SnapshotAnimated::snapshotAnimatedDuration.set(value); };

View file

@ -194,3 +194,10 @@ void Snapshot::uploadSnapshot(const QString& filename, const QUrl& href) {
multiPart);
}
QString Snapshot::getSnapshotsLocation() {
return snapshotsLocation.get("");
}
void Snapshot::setSnapshotsLocation(const QString& location) {
snapshotsLocation.set(location);
}

View file

@ -18,6 +18,7 @@
#include <QStandardPaths>
#include <SettingHandle.h>
#include <DependencyManager.h>
class QFile;
class QTemporaryFile;
@ -32,7 +33,9 @@ private:
QUrl _URL;
};
class Snapshot {
class Snapshot : public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
public:
static QString saveSnapshot(QImage image);
static QTemporaryFile* saveTempSnapshot(QImage image);
@ -40,6 +43,10 @@ public:
static Setting::Handle<QString> snapshotsLocation;
static void uploadSnapshot(const QString& filename, const QUrl& href = QUrl(""));
public slots:
Q_INVOKABLE QString getSnapshotsLocation();
Q_INVOKABLE void setSnapshotsLocation(const QString& location);
private:
static QFile* savedFileForSnapshot(QImage & image, bool isTemporary);
};

View file

@ -49,6 +49,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) {
userStoryObject.insert("place_name", placeName);
userStoryObject.insert("path", currentPath);
userStoryObject.insert("action", "snapshot");
userStoryObject.insert("audience", "for_url");
rootObject.insert("user_story", userStoryObject);
auto accountManager = DependencyManager::get<AccountManager>();
@ -61,7 +62,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) {
QJsonDocument(rootObject).toJson());
} else {
emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(contents);
emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(true, contents);
delete this;
}
}
@ -72,12 +73,13 @@ void SnapshotUploader::uploadFailure(QNetworkReply& reply) {
if (replyString.size() == 0) {
replyString = reply.errorString();
}
emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(replyString); // maybe someday include _inWorldLocation, _filename?
emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(true, replyString); // maybe someday include _inWorldLocation, _filename?
delete this;
}
void SnapshotUploader::createStorySuccess(QNetworkReply& reply) {
emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(QString());
QString replyString = reply.readAll();
emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(false, replyString);
delete this;
}
@ -87,7 +89,7 @@ void SnapshotUploader::createStoryFailure(QNetworkReply& reply) {
if (replyString.size() == 0) {
replyString = reply.errorString();
}
emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(replyString);
emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(true, replyString);
delete this;
}

View file

@ -51,6 +51,7 @@
#include "ui/AvatarInputs.h"
#include "avatar/AvatarManager.h"
#include "scripting/GlobalServicesScriptingInterface.h"
#include "ui/Snapshot.h"
static const float DPI = 30.47f;
static const float INCHES_TO_METERS = 1.0f / 39.3701f;
@ -177,6 +178,7 @@ void Web3DOverlay::loadSourceURL() {
_webSurface->getRootContext()->setContextProperty("Quat", new Quat());
_webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get<AvatarManager>()->getMyAvatar().get());
_webSurface->getRootContext()->setContextProperty("Entities", DependencyManager::get<EntityScriptingInterface>().data());
_webSurface->getRootContext()->setContextProperty("Snapshot", DependencyManager::get<Snapshot>().data());
if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") {
auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();

View file

@ -86,7 +86,9 @@ void AnimInverseKinematics::setTargetVars(
void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std::vector<IKTarget>& targets, const AnimPoseVec& underPoses) {
// build a list of valid targets from _targetVarVec and animVars
_maxTargetIndex = -1;
_hipsTargetIndex = -1;
bool removeUnfoundJoints = false;
for (auto& targetVar : _targetVarVec) {
if (targetVar.jointIndex == -1) {
// this targetVar hasn't been validated yet...
@ -105,15 +107,18 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std::
AnimPose defaultPose = _skeleton->getAbsolutePose(targetVar.jointIndex, underPoses);
glm::quat rotation = animVars.lookupRigToGeometry(targetVar.rotationVar, defaultPose.rot());
glm::vec3 translation = animVars.lookupRigToGeometry(targetVar.positionVar, defaultPose.trans());
if (target.getType() == IKTarget::Type::HipsRelativeRotationAndPosition) {
translation += _hipsOffset;
}
target.setPose(rotation, translation);
target.setIndex(targetVar.jointIndex);
targets.push_back(target);
if (targetVar.jointIndex > _maxTargetIndex) {
_maxTargetIndex = targetVar.jointIndex;
}
// record the index of the hips ik target.
if (target.getIndex() == _hipsIndex) {
_hipsTargetIndex = (int)targets.size() - 1;
}
}
}
}
@ -242,18 +247,21 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe
// the tip's parent-relative as we proceed up the chain
glm::quat tipParentOrientation = absolutePoses[pivotIndex].rot();
// NOTE: if this code is removed, the head will remain rigid, causing the spine/hips to thrust forward backward
// as the head is nodded.
if (targetType == IKTarget::Type::HmdHead) {
// rotate tip directly to target orientation
tipOrientation = target.getRotation();
glm::quat tipRelativeRotation = glm::normalize(tipOrientation * glm::inverse(tipParentOrientation));
glm::quat tipRelativeRotation = glm::inverse(tipParentOrientation) * tipOrientation;
// enforce tip's constraint
// then enforce tip's constraint
RotationConstraint* constraint = getConstraint(tipIndex);
if (constraint) {
bool constrained = constraint->apply(tipRelativeRotation);
if (constrained) {
tipOrientation = glm::normalize(tipRelativeRotation * tipParentOrientation);
tipRelativeRotation = glm::normalize(tipOrientation * glm::inverse(tipParentOrientation));
tipOrientation = tipParentOrientation * tipRelativeRotation;
tipRelativeRotation = tipRelativeRotation;
}
}
// store the relative rotation change in the accumulator
@ -277,7 +285,9 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe
const float MIN_AXIS_LENGTH = 1.0e-4f;
RotationConstraint* constraint = getConstraint(pivotIndex);
if (constraint && constraint->isLowerSpine() && tipIndex != _headIndex) {
// only allow swing on lowerSpine if there is a hips IK target.
if (_hipsTargetIndex < 0 && constraint && constraint->isLowerSpine() && tipIndex != _headIndex) {
// for these types of targets we only allow twist at the lower-spine
// (this prevents the hand targets from bending the spine too much and thereby driving the hips too far)
glm::vec3 twistAxis = absolutePoses[pivotIndex].trans() - absolutePoses[pivotsParentIndex].trans();
@ -420,13 +430,13 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
_relativePoses[i].trans() = underPoses[i].trans();
}
if (!_relativePoses.empty()) {
if (!underPoses.empty()) {
// Sometimes the underpose itself can violate the constraints. Rather than
// clamp the animation we dynamically expand each constraint to accomodate it.
std::map<int, RotationConstraint*>::iterator constraintItr = _constraints.begin();
while (constraintItr != _constraints.end()) {
int index = constraintItr->first;
constraintItr->second->dynamicallyAdjustLimits(_relativePoses[index].rot());
constraintItr->second->dynamicallyAdjustLimits(underPoses[index].rot());
++constraintItr;
}
}
@ -441,64 +451,76 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
computeTargets(animVars, targets, underPoses);
}
// debug render ik targets
if (context.getEnableDebugDrawIKTargets()) {
const vec4 WHITE(1.0f);
glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3());
for (auto& target : targets) {
glm::mat4 geomTargetMat = createMatFromQuatAndPos(target.getRotation(), target.getTranslation());
glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat;
QString name = QString("ikTarget%1").arg(target.getIndex());
DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE);
}
} else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) {
// remove markers if they were added last frame.
for (auto& target : targets) {
QString name = QString("ikTarget%1").arg(target.getIndex());
DebugDraw::getInstance().removeMyAvatarMarker(name);
}
}
_previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets();
if (targets.empty()) {
// no IK targets but still need to enforce constraints
std::map<int, RotationConstraint*>::iterator constraintItr = _constraints.begin();
while (constraintItr != _constraints.end()) {
int index = constraintItr->first;
glm::quat rotation = _relativePoses[index].rot();
constraintItr->second->apply(rotation);
_relativePoses[index].rot() = rotation;
++constraintItr;
}
_relativePoses = underPoses;
} else {
{
PROFILE_RANGE_EX(simulation_animation, "ik/shiftHips", 0xffff00ff, 0);
// shift hips according to the _hipsOffset from the previous frame
float offsetLength = glm::length(_hipsOffset);
const float MIN_HIPS_OFFSET_LENGTH = 0.03f;
if (offsetLength > MIN_HIPS_OFFSET_LENGTH && _hipsIndex >= 0) {
// but only if offset is long enough
float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength);
if (_hipsParentIndex == -1) {
// the hips are the root so _hipsOffset is in the correct frame
_relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans() + scaleFactor * _hipsOffset;
if (_hipsTargetIndex >= 0 && _hipsTargetIndex < (int)targets.size()) {
// slam the hips to match the _hipsTarget
AnimPose absPose = targets[_hipsTargetIndex].getPose();
int parentIndex = _skeleton->getParentIndex(targets[_hipsTargetIndex].getIndex());
if (parentIndex != -1) {
_relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(parentIndex, _relativePoses).inverse() * absPose;
} else {
// the hips are NOT the root so we need to transform _hipsOffset into hips local-frame
glm::quat hipsFrameRotation = _relativePoses[_hipsParentIndex].rot();
int index = _skeleton->getParentIndex(_hipsParentIndex);
while (index != -1) {
hipsFrameRotation *= _relativePoses[index].rot();
index = _skeleton->getParentIndex(index);
_relativePoses[_hipsIndex] = absPose;
}
} else {
// if there is no hips target, shift hips according to the _hipsOffset from the previous frame
float offsetLength = glm::length(_hipsOffset);
const float MIN_HIPS_OFFSET_LENGTH = 0.03f;
if (offsetLength > MIN_HIPS_OFFSET_LENGTH && _hipsIndex >= 0) {
float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength);
glm::vec3 hipsOffset = scaleFactor * _hipsOffset;
if (_hipsParentIndex == -1) {
_relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans() + hipsOffset;
} else {
auto absHipsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses);
absHipsPose.trans() += hipsOffset;
_relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(_hipsParentIndex, _relativePoses).inverse() * absHipsPose;
}
_relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans()
+ glm::inverse(glm::normalize(hipsFrameRotation)) * (scaleFactor * _hipsOffset);
}
}
// update all HipsRelative targets to account for the hips shift/ik target.
auto shiftedHipsAbsPose = _skeleton->getAbsolutePose(_hipsIndex, _relativePoses);
auto underHipsAbsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses);
auto absHipsOffset = shiftedHipsAbsPose.trans() - underHipsAbsPose.trans();
for (auto& target: targets) {
if (target.getType() == IKTarget::Type::HipsRelativeRotationAndPosition) {
auto pose = target.getPose();
pose.trans() = pose.trans() + absHipsOffset;
target.setPose(pose.rot(), pose.trans());
}
}
}
{
PROFILE_RANGE_EX(simulation_animation, "ik/debugDraw", 0xffff00ff, 0);
// debug render ik targets
if (context.getEnableDebugDrawIKTargets()) {
const vec4 WHITE(1.0f);
glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3());
for (auto& target : targets) {
glm::mat4 geomTargetMat = createMatFromQuatAndPos(target.getRotation(), target.getTranslation());
glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat;
QString name = QString("ikTarget%1").arg(target.getIndex());
DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE);
}
} else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) {
// remove markers if they were added last frame.
for (auto& target : targets) {
QString name = QString("ikTarget%1").arg(target.getIndex());
DebugDraw::getInstance().removeMyAvatarMarker(name);
}
}
_previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets();
}
{
@ -506,64 +528,70 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
solveWithCyclicCoordinateDescent(targets);
}
{
if (_hipsTargetIndex < 0) {
PROFILE_RANGE_EX(simulation_animation, "ik/measureHipsOffset", 0xffff00ff, 0);
// measure new _hipsOffset for next frame
// by looking for discrepancies between where a targeted endEffector is
// and where it wants to be (after IK solutions are done)
glm::vec3 newHipsOffset = Vectors::ZERO;
for (auto& target: targets) {
int targetIndex = target.getIndex();
if (targetIndex == _headIndex && _headIndex != -1) {
// special handling for headTarget
if (target.getType() == IKTarget::Type::RotationOnly) {
// we want to shift the hips to bring the underPose closer
// to where the head happens to be (overpose)
glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans();
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f;
newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under);
} else if (target.getType() == IKTarget::Type::HmdHead) {
// we want to shift the hips to bring the head to its designated position
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
_hipsOffset += target.getTranslation() - actual;
// and ignore all other targets
newHipsOffset = _hipsOffset;
break;
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
glm::vec3 targetPosition = target.getTranslation();
newHipsOffset += targetPosition - actualPosition;
// Add downward pressure on the hips
newHipsOffset *= 0.95f;
newHipsOffset -= 1.0f;
}
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
glm::vec3 targetPosition = target.getTranslation();
newHipsOffset += targetPosition - actualPosition;
}
}
// smooth transitions by relaxing _hipsOffset toward the new value
const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f;
float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f;
_hipsOffset += (newHipsOffset - _hipsOffset) * tau;
// clamp the hips offset
float hipsOffsetLength = glm::length(_hipsOffset);
if (hipsOffsetLength > _maxHipsOffsetLength) {
_hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength;
}
computeHipsOffset(targets, underPoses, dt);
} else {
_hipsOffset = Vectors::ZERO;
}
}
}
return _relativePoses;
}
void AnimInverseKinematics::computeHipsOffset(const std::vector<IKTarget>& targets, const AnimPoseVec& underPoses, float dt) {
// measure new _hipsOffset for next frame
// by looking for discrepancies between where a targeted endEffector is
// and where it wants to be (after IK solutions are done)
glm::vec3 newHipsOffset = Vectors::ZERO;
for (auto& target: targets) {
int targetIndex = target.getIndex();
if (targetIndex == _headIndex && _headIndex != -1) {
// special handling for headTarget
if (target.getType() == IKTarget::Type::RotationOnly) {
// we want to shift the hips to bring the underPose closer
// to where the head happens to be (overpose)
glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans();
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f;
newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under);
} else if (target.getType() == IKTarget::Type::HmdHead) {
// we want to shift the hips to bring the head to its designated position
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
_hipsOffset += target.getTranslation() - actual;
// and ignore all other targets
newHipsOffset = _hipsOffset;
break;
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
glm::vec3 targetPosition = target.getTranslation();
newHipsOffset += targetPosition - actualPosition;
// Add downward pressure on the hips
const float PRESSURE_SCALE_FACTOR = 0.95f;
const float PRESSURE_TRANSLATION_OFFSET = 1.0f;
newHipsOffset *= PRESSURE_SCALE_FACTOR;
newHipsOffset -= PRESSURE_TRANSLATION_OFFSET;
}
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
glm::vec3 targetPosition = target.getTranslation();
newHipsOffset += targetPosition - actualPosition;
}
}
// smooth transitions by relaxing _hipsOffset toward the new value
const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f;
float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f;
_hipsOffset += (newHipsOffset - _hipsOffset) * tau;
// clamp the hips offset
float hipsOffsetLength = glm::length(_hipsOffset);
if (hipsOffsetLength > _maxHipsOffsetLength) {
_hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength;
}
}
void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) {
// manually adjust scale here
const float METERS_TO_CENTIMETERS = 100.0f;
@ -594,6 +622,22 @@ void AnimInverseKinematics::clearConstraints() {
_constraints.clear();
}
// set up swing limits around a swingTwistConstraint in an ellipse, where lateralSwingTheta is the swing limit for lateral swings (side to side)
// anteriorSwingTheta is swing limit for forward and backward swings. (where x-axis of reference rotation is sideways and -z-axis is forward)
static void setEllipticalSwingLimits(SwingTwistConstraint* stConstraint, float lateralSwingTheta, float anteriorSwingTheta) {
assert(stConstraint);
const int NUM_SUBDIVISIONS = 8;
std::vector<float> minDots;
minDots.reserve(NUM_SUBDIVISIONS);
float dTheta = TWO_PI / NUM_SUBDIVISIONS;
float theta = 0.0f;
for (int i = 0; i < NUM_SUBDIVISIONS; i++) {
minDots.push_back(cosf(glm::length(glm::vec2(anteriorSwingTheta * cosf(theta), lateralSwingTheta * sinf(theta)))));
theta += dTheta;
}
stConstraint->setSwingLimits(minDots);
}
void AnimInverseKinematics::initConstraints() {
if (!_skeleton) {
return;
@ -783,41 +827,31 @@ void AnimInverseKinematics::initConstraints() {
} else if (baseName.startsWith("Spine", Qt::CaseSensitive)) {
SwingTwistConstraint* stConstraint = new SwingTwistConstraint();
stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot());
const float MAX_SPINE_TWIST = PI / 12.0f;
const float MAX_SPINE_TWIST = PI / 20.0f;
stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST);
std::vector<float> minDots;
const float MAX_SPINE_SWING = PI / 10.0f;
minDots.push_back(cosf(MAX_SPINE_SWING));
stConstraint->setSwingLimits(minDots);
// limit lateral swings more then forward-backward swings
const float MAX_SPINE_LATERAL_SWING = PI / 30.0f;
const float MAX_SPINE_ANTERIOR_SWING = PI / 20.0f;
setEllipticalSwingLimits(stConstraint, MAX_SPINE_LATERAL_SWING, MAX_SPINE_ANTERIOR_SWING);
if (0 == baseName.compare("Spine1", Qt::CaseSensitive)
|| 0 == baseName.compare("Spine", Qt::CaseSensitive)) {
stConstraint->setLowerSpine(true);
}
constraint = static_cast<RotationConstraint*>(stConstraint);
} else if (baseName.startsWith("Hips2", Qt::CaseSensitive)) {
SwingTwistConstraint* stConstraint = new SwingTwistConstraint();
stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot());
const float MAX_SPINE_TWIST = PI / 8.0f;
stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST);
std::vector<float> minDots;
const float MAX_SPINE_SWING = PI / 14.0f;
minDots.push_back(cosf(MAX_SPINE_SWING));
stConstraint->setSwingLimits(minDots);
constraint = static_cast<RotationConstraint*>(stConstraint);
} else if (0 == baseName.compare("Neck", Qt::CaseSensitive)) {
SwingTwistConstraint* stConstraint = new SwingTwistConstraint();
stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot());
const float MAX_NECK_TWIST = PI / 9.0f;
const float MAX_NECK_TWIST = PI / 10.0f;
stConstraint->setTwistLimits(-MAX_NECK_TWIST, MAX_NECK_TWIST);
std::vector<float> minDots;
const float MAX_NECK_SWING = PI / 8.0f;
minDots.push_back(cosf(MAX_NECK_SWING));
stConstraint->setSwingLimits(minDots);
// limit lateral swings more then forward-backward swings
const float MAX_NECK_LATERAL_SWING = PI / 10.0f;
const float MAX_NECK_ANTERIOR_SWING = PI / 8.0f;
setEllipticalSwingLimits(stConstraint, MAX_NECK_LATERAL_SWING, MAX_NECK_ANTERIOR_SWING);
constraint = static_cast<RotationConstraint*>(stConstraint);
} else if (0 == baseName.compare("Head", Qt::CaseSensitive)) {
@ -872,7 +906,7 @@ void AnimInverseKinematics::initConstraints() {
// we determine the max/min angles by rotating the swing limit lines from parent- to child-frame
// then measure the angles to swing the yAxis into alignment
const float MIN_KNEE_ANGLE = 0.0f;
const float MIN_KNEE_ANGLE = 0.097f; // ~5 deg
const float MAX_KNEE_ANGLE = 7.0f * PI / 8.0f;
glm::quat invReferenceRotation = glm::inverse(referenceRotation);
glm::vec3 minSwingAxis = invReferenceRotation * glm::angleAxis(MIN_KNEE_ANGLE, hingeAxis) * Vectors::UNIT_Y;

View file

@ -55,6 +55,7 @@ protected:
RotationConstraint* getConstraint(int index);
void clearConstraints();
void initConstraints();
void computeHipsOffset(const std::vector<IKTarget>& targets, const AnimPoseVec& underPoses, float dt);
// no copies
AnimInverseKinematics(const AnimInverseKinematics&) = delete;
@ -91,6 +92,7 @@ protected:
int _headIndex { -1 };
int _hipsIndex { -1 };
int _hipsParentIndex { -1 };
int _hipsTargetIndex { -1 };
// _maxTargetIndex is tracked to help optimize the recalculation of absolute poses
// during the the cyclic coordinate descent algorithm

View file

@ -12,6 +12,16 @@
#include "AnimUtil.h"
#include "AnimationLogging.h"
AnimManipulator::JointVar::JointVar(const QString& jointNameIn, Type rotationTypeIn, Type translationTypeIn,
const QString& rotationVarIn, const QString& translationVarIn) :
jointName(jointNameIn),
rotationType(rotationTypeIn),
translationType(translationTypeIn),
rotationVar(rotationVarIn),
translationVar(translationVarIn),
jointIndex(-1),
hasPerformedJointLookup(false) {}
AnimManipulator::AnimManipulator(const QString& id, float alpha) :
AnimNode(AnimNode::Type::Manipulator, id),
_alpha(alpha) {
@ -36,7 +46,10 @@ const AnimPoseVec& AnimManipulator::overlay(const AnimVariantMap& animVars, cons
}
for (auto& jointVar : _jointVars) {
if (!jointVar.hasPerformedJointLookup) {
// map from joint name to joint index and cache the result.
jointVar.jointIndex = _skeleton->nameToJointIndex(jointVar.jointName);
if (jointVar.jointIndex < 0) {
qCWarning(animation) << "AnimManipulator could not find jointName" << jointVar.jointName << "in skeleton";
@ -100,34 +113,62 @@ AnimPose AnimManipulator::computeRelativePoseFromJointVar(const AnimVariantMap&
AnimPose defaultAbsPose = _skeleton->getAbsolutePose(jointVar.jointIndex, underPoses);
if (jointVar.type == JointVar::Type::AbsoluteRotation || jointVar.type == JointVar::Type::AbsolutePosition) {
// compute relative translation
glm::vec3 relTrans;
switch (jointVar.translationType) {
case JointVar::Type::Absolute: {
glm::vec3 absTrans = animVars.lookupRigToGeometry(jointVar.translationVar, defaultAbsPose.trans());
if (jointVar.type == JointVar::Type::AbsoluteRotation) {
defaultAbsPose.rot() = animVars.lookupRigToGeometry(jointVar.var, defaultAbsPose.rot());
} else if (jointVar.type == JointVar::Type::AbsolutePosition) {
defaultAbsPose.trans() = animVars.lookupRigToGeometry(jointVar.var, defaultAbsPose.trans());
// convert to from absolute to relative.
AnimPose parentAbsPose;
int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex);
if (parentIndex >= 0) {
parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses);
}
// convert from absolute to relative
relTrans = transformPoint(parentAbsPose.inverse(), absTrans);
break;
}
// because jointVar is absolute, we must use an absolute parent frame to convert into a relative pose.
AnimPose parentAbsPose = AnimPose::identity;
int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex);
if (parentIndex >= 0) {
parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses);
}
// convert from absolute to relative
return parentAbsPose.inverse() * defaultAbsPose;
} else {
// override the default rel pose
AnimPose relPose = defaultRelPose;
if (jointVar.type == JointVar::Type::RelativeRotation) {
relPose.rot() = animVars.lookupRigToGeometry(jointVar.var, defaultRelPose.rot());
} else if (jointVar.type == JointVar::Type::RelativePosition) {
relPose.trans() = animVars.lookupRigToGeometry(jointVar.var, defaultRelPose.trans());
}
return relPose;
case JointVar::Type::Relative:
relTrans = animVars.lookupRigToGeometryVector(jointVar.translationVar, defaultRelPose.trans());
break;
case JointVar::Type::UnderPose:
relTrans = underPoses[jointVar.jointIndex].trans();
break;
case JointVar::Type::Default:
default:
relTrans = defaultRelPose.trans();
break;
}
glm::quat relRot;
switch (jointVar.rotationType) {
case JointVar::Type::Absolute: {
glm::quat absRot = animVars.lookupRigToGeometry(jointVar.translationVar, defaultAbsPose.rot());
// convert to from absolute to relative.
AnimPose parentAbsPose;
int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex);
if (parentIndex >= 0) {
parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses);
}
// convert from absolute to relative
relRot = glm::inverse(parentAbsPose.rot()) * absRot;
break;
}
case JointVar::Type::Relative:
relRot = animVars.lookupRigToGeometry(jointVar.translationVar, defaultRelPose.rot());
break;
case JointVar::Type::UnderPose:
relRot = underPoses[jointVar.jointIndex].rot();
break;
case JointVar::Type::Default:
default:
relRot = defaultRelPose.rot();
break;
}
return AnimPose(glm::vec3(1), relRot, relTrans);
}

View file

@ -31,17 +31,20 @@ public:
struct JointVar {
enum class Type {
AbsoluteRotation = 0,
AbsolutePosition,
RelativeRotation,
RelativePosition,
Absolute,
Relative,
UnderPose,
Default,
NumTypes
};
JointVar(const QString& varIn, const QString& jointNameIn, Type typeIn) : var(varIn), jointName(jointNameIn), type(typeIn), jointIndex(-1), hasPerformedJointLookup(false) {}
QString var = "";
JointVar(const QString& jointNameIn, Type rotationType, Type translationType, const QString& rotationVarIn, const QString& translationVarIn);
QString jointName = "";
Type type = Type::AbsoluteRotation;
Type rotationType = Type::Absolute;
Type translationType = Type::Absolute;
QString rotationVar = "";
QString translationVar = "";
int jointIndex = -1;
bool hasPerformedJointLookup = false;
bool isRelative = false;

View file

@ -79,10 +79,10 @@ static AnimStateMachine::InterpType stringToInterpType(const QString& str) {
static const char* animManipulatorJointVarTypeToString(AnimManipulator::JointVar::Type type) {
switch (type) {
case AnimManipulator::JointVar::Type::AbsoluteRotation: return "absoluteRotation";
case AnimManipulator::JointVar::Type::AbsolutePosition: return "absolutePosition";
case AnimManipulator::JointVar::Type::RelativeRotation: return "relativeRotation";
case AnimManipulator::JointVar::Type::RelativePosition: return "relativePosition";
case AnimManipulator::JointVar::Type::Absolute: return "absolute";
case AnimManipulator::JointVar::Type::Relative: return "relative";
case AnimManipulator::JointVar::Type::UnderPose: return "underPose";
case AnimManipulator::JointVar::Type::Default: return "default";
case AnimManipulator::JointVar::Type::NumTypes: return nullptr;
};
return nullptr;
@ -339,7 +339,8 @@ static const char* boneSetStrings[AnimOverlay::NumBoneSets] = {
"spineOnly",
"empty",
"leftHand",
"rightHand"
"rightHand",
"hipsOnly"
};
static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) {
@ -406,17 +407,25 @@ static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const Q
}
auto jointObj = jointValue.toObject();
READ_STRING(type, jointObj, id, jsonUrl, nullptr);
READ_STRING(jointName, jointObj, id, jsonUrl, nullptr);
READ_STRING(var, jointObj, id, jsonUrl, nullptr);
READ_STRING(rotationType, jointObj, id, jsonUrl, nullptr);
READ_STRING(translationType, jointObj, id, jsonUrl, nullptr);
READ_STRING(rotationVar, jointObj, id, jsonUrl, nullptr);
READ_STRING(translationVar, jointObj, id, jsonUrl, nullptr);
AnimManipulator::JointVar::Type jointVarType = stringToAnimManipulatorJointVarType(type);
if (jointVarType == AnimManipulator::JointVar::Type::NumTypes) {
qCCritical(animation) << "AnimNodeLoader, bad type in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr;
AnimManipulator::JointVar::Type jointVarRotationType = stringToAnimManipulatorJointVarType(rotationType);
if (jointVarRotationType == AnimManipulator::JointVar::Type::NumTypes) {
qCWarning(animation) << "AnimNodeLoader, bad rotationType in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString();
jointVarRotationType = AnimManipulator::JointVar::Type::Default;
}
AnimManipulator::JointVar jointVar(var, jointName, jointVarType);
AnimManipulator::JointVar::Type jointVarTranslationType = stringToAnimManipulatorJointVarType(translationType);
if (jointVarTranslationType == AnimManipulator::JointVar::Type::NumTypes) {
qCWarning(animation) << "AnimNodeLoader, bad translationType in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString();
jointVarTranslationType = AnimManipulator::JointVar::Type::Default;
}
AnimManipulator::JointVar jointVar(jointName, jointVarRotationType, jointVarTranslationType, rotationVar, translationVar);
node->addJointVar(jointVar);
};

View file

@ -34,6 +34,7 @@ void AnimOverlay::buildBoneSet(BoneSet boneSet) {
case SpineOnlyBoneSet: buildSpineOnlyBoneSet(); break;
case LeftHandBoneSet: buildLeftHandBoneSet(); break;
case RightHandBoneSet: buildRightHandBoneSet(); break;
case HipsOnlyBoneSet: buildHipsOnlyBoneSet(); break;
default:
case EmptyBoneSet: buildEmptyBoneSet(); break;
}
@ -188,6 +189,13 @@ void AnimOverlay::buildRightHandBoneSet() {
});
}
void AnimOverlay::buildHipsOnlyBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int hipsJoint = _skeleton->nameToJointIndex("Hips");
_boneSetVec[hipsJoint] = 1.0f;
}
// for AnimDebugDraw rendering
const AnimPoseVec& AnimOverlay::getPosesInternal() const {
return _poses;

View file

@ -37,6 +37,7 @@ public:
EmptyBoneSet,
LeftHandBoneSet,
RightHandBoneSet,
HipsOnlyBoneSet,
NumBoneSets
};
@ -75,6 +76,7 @@ public:
void buildEmptyBoneSet();
void buildLeftHandBoneSet();
void buildRightHandBoneSet();
void buildHipsOnlyBoneSet();
// no copies
AnimOverlay(const AnimOverlay&) = delete;

View file

@ -165,6 +165,15 @@ public:
}
}
glm::vec3 lookupRigToGeometryVector(const QString& key, const glm::vec3& defaultValue) const {
if (key.isEmpty()) {
return defaultValue;
} else {
auto iter = _map.find(key);
return iter != _map.end() ? transformVectorFast(_rigToGeometryMat, iter->second.getVec3()) : defaultValue;
}
}
const glm::quat& lookupRaw(const QString& key, const glm::quat& defaultValue) const {
if (key.isEmpty()) {
return defaultValue;

View file

@ -21,13 +21,14 @@ public:
RotationOnly,
HmdHead,
HipsRelativeRotationAndPosition,
Unknown,
Unknown
};
IKTarget() {}
const glm::vec3& getTranslation() const { return _pose.trans(); }
const glm::quat& getRotation() const { return _pose.rot(); }
const AnimPose& getPose() const { return _pose; }
int getIndex() const { return _index; }
Type getType() const { return _type; }

View file

@ -1024,6 +1024,17 @@ void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) {
_animVars.set("isTalking", params.isTalking);
_animVars.set("notIsTalking", !params.isTalking);
if (params.hipsEnabled) {
_animVars.set("hipsType", (int)IKTarget::Type::RotationAndPosition);
_animVars.set("hipsPosition", extractTranslation(params.hipsMatrix));
_animVars.set("hipsRotation", glmExtractRotation(params.hipsMatrix) * Quaternions::Y_180);
} else {
_animVars.set("hipsType", (int)IKTarget::Type::Unknown);
}
// by default this IK target is disabled.
_animVars.set("spine2Type", (int)IKTarget::Type::Unknown);
}
void Rig::updateFromEyeParameters(const EyeParameters& params) {
@ -1161,13 +1172,19 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f
const glm::vec3 bodyCapsuleStart = bodyCapsuleCenter - glm::vec3(0, params.bodyCapsuleHalfHeight, 0);
const glm::vec3 bodyCapsuleEnd = bodyCapsuleCenter + glm::vec3(0, params.bodyCapsuleHalfHeight, 0);
// TODO: add isHipsEnabled
bool bodySensorTrackingEnabled = params.isLeftFootEnabled || params.isRightFootEnabled;
if (params.isLeftEnabled) {
// prevent the hand IK targets from intersecting the body capsule
glm::vec3 handPosition = params.leftPosition;
glm::vec3 displacement;
if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) {
handPosition -= displacement;
if (!bodySensorTrackingEnabled) {
// prevent the hand IK targets from intersecting the body capsule
glm::vec3 displacement;
if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) {
handPosition -= displacement;
}
}
_animVars.set("leftHandPosition", handPosition);
@ -1181,11 +1198,14 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f
if (params.isRightEnabled) {
// prevent the hand IK targets from intersecting the body capsule
glm::vec3 handPosition = params.rightPosition;
glm::vec3 displacement;
if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) {
handPosition -= displacement;
if (!bodySensorTrackingEnabled) {
// prevent the hand IK targets from intersecting the body capsule
glm::vec3 displacement;
if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) {
handPosition -= displacement;
}
}
_animVars.set("rightHandPosition", handPosition);

View file

@ -45,6 +45,8 @@ public:
glm::quat worldHeadOrientation = glm::quat(); // world space (-z forward)
glm::quat rigHeadOrientation = glm::quat(); // rig space (-z forward)
glm::vec3 rigHeadPosition = glm::vec3(); // rig space
glm::mat4 hipsMatrix = glm::mat4(); // rig space
bool hipsEnabled = false;
bool isInHMD = false;
int neckJointIndex = -1;
bool isTalking = false;

View file

@ -122,3 +122,10 @@ bool Quat::equal(const glm::quat& q1, const glm::quat& q2) {
return q1 == q2;
}
glm::quat Quat::cancelOutRollAndPitch(const glm::quat& q) {
return ::cancelOutRollAndPitch(q);
}
glm::quat Quat::cancelOutRoll(const glm::quat& q) {
return ::cancelOutRoll(q);
}

View file

@ -60,6 +60,8 @@ public slots:
float dot(const glm::quat& q1, const glm::quat& q2);
void print(const QString& label, const glm::quat& q);
bool equal(const glm::quat& q1, const glm::quat& q2);
glm::quat cancelOutRollAndPitch(const glm::quat& q);
glm::quat cancelOutRoll(const glm::quat& q);
};
#endif // hifi_Quat_h

View file

@ -0,0 +1,118 @@
//
// hipsIKTest.js
//
// Created by Anthony Thibault on 4/24/17
// Copyright 2017 High Fidelity, Inc.
//
// Test procedural manipulation of the Avatar hips IK target.
// Pull the left and right triggers on your hand controllers, you hips should begin to gyrate in an amusing mannor.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* global Xform */
Script.include("/~/system/libraries/Xform.js");
var calibrated = false;
var rightTriggerPressed = false;
var leftTriggerPressed = false;
var MAPPING_NAME = "com.highfidelity.hipsIkTest";
var mapping = Controller.newMapping(MAPPING_NAME);
mapping.from([Controller.Standard.RTClick]).peek().to(function (value) {
rightTriggerPressed = (value !== 0) ? true : false;
});
mapping.from([Controller.Standard.LTClick]).peek().to(function (value) {
leftTriggerPressed = (value !== 0) ? true : false;
});
Controller.enableMapping(MAPPING_NAME);
var ANIM_VARS = [
"headType",
"hipsType",
"hipsPosition",
"hipsRotation"
];
var ZERO = {x: 0, y: 0, z: 0};
var X_AXIS = {x: 1, y: 0, z: 0};
var Y_AXIS = {x: 0, y: 1, z: 0};
var Y_180 = {x: 0, y: 1, z: 0, w: 0};
var Y_180_XFORM = new Xform(Y_180, {x: 0, y: 0, z: 0});
var hips = undefined;
function computeCurrentXform(jointIndex) {
var currentXform = new Xform(MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex),
MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex));
return Xform.mul(Y_180_XFORM, currentXform);
}
function calibrate() {
hips = computeCurrentXform(MyAvatar.getJointIndex("Hips"));
}
var ikTypes = {
RotationAndPosition: 0,
RotationOnly: 1,
HmdHead: 2,
HipsRelativeRotationAndPosition: 3,
Off: 4
};
function circleOffset(radius, theta, normal) {
var pos = {x: radius * Math.cos(theta), y: radius * Math.sin(theta), z: 0};
var lookAtRot = Quat.lookAt(normal, ZERO, X_AXIS);
return Vec3.multiplyQbyV(lookAtRot, pos);
}
var handlerId;
function update(dt) {
if (rightTriggerPressed && leftTriggerPressed) {
if (!calibrated) {
calibrate();
calibrated = true;
if (handlerId) {
MyAvatar.removeAnimationStateHandler(handlerId);
handlerId = undefined;
} else {
var n = Y_AXIS;
var t = 0;
handlerId = MyAvatar.addAnimationStateHandler(function (props) {
t += (1 / 60) * 4;
var result = {}, xform;
if (hips) {
xform = hips;
result.hipsType = ikTypes.RotationAndPosition;
result.hipsPosition = Vec3.sum(xform.pos, circleOffset(0.1, t, n));
result.hipsRotation = xform.rot;
result.headType = ikTypes.RotationAndPosition;
} else {
result.headType = ikTypes.HmdHead;
result.hipsType = props.hipsType;
result.hipsPosition = props.hipsPosition;
result.hipsRotation = props.hipsRotation;
}
return result;
}, ANIM_VARS);
}
}
} else {
calibrated = false;
}
}
Script.update.connect(update);
Script.scriptEnding.connect(function () {
Controller.disableMapping(MAPPING_NAME);
Script.update.disconnect(update);
});

View file

@ -0,0 +1,307 @@
/* global Xform */
Script.include("/~/system/libraries/Xform.js");
var TRACKED_OBJECT_POSES = [
"TrackedObject00", "TrackedObject01", "TrackedObject02", "TrackedObject03",
"TrackedObject04", "TrackedObject05", "TrackedObject06", "TrackedObject07",
"TrackedObject08", "TrackedObject09", "TrackedObject10", "TrackedObject11",
"TrackedObject12", "TrackedObject13", "TrackedObject14", "TrackedObject15"
];
var triggerPressHandled = false;
var rightTriggerPressed = false;
var leftTriggerPressed = false;
var MAPPING_NAME = "com.highfidelity.viveMotionCapture";
var mapping = Controller.newMapping(MAPPING_NAME);
mapping.from([Controller.Standard.RTClick]).peek().to(function (value) {
rightTriggerPressed = (value !== 0) ? true : false;
});
mapping.from([Controller.Standard.LTClick]).peek().to(function (value) {
leftTriggerPressed = (value !== 0) ? true : false;
});
Controller.enableMapping(MAPPING_NAME);
var leftFoot;
var rightFoot;
var hips;
var spine2;
var FEET_ONLY = 0;
var FEET_AND_HIPS = 1;
var FEET_AND_CHEST = 2;
var FEET_HIPS_AND_CHEST = 3;
var AUTO = 4;
var SENSOR_CONFIG_NAMES = [
"FeetOnly",
"FeetAndHips",
"FeetAndChest",
"FeetHipsAndChest",
"Auto"
];
var sensorConfig = AUTO;
var Y_180 = {x: 0, y: 1, z: 0, w: 0};
var Y_180_XFORM = new Xform(Y_180, {x: 0, y: 0, z: 0});
function computeOffsetXform(defaultToReferenceXform, pose, jointIndex) {
var poseXform = new Xform(pose.rotation, pose.translation);
var defaultJointXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(jointIndex),
MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(jointIndex));
var referenceJointXform = Xform.mul(defaultToReferenceXform, defaultJointXform);
return Xform.mul(poseXform.inv(), referenceJointXform);
}
function computeDefaultToReferenceXform() {
var headIndex = MyAvatar.getJointIndex("Head");
if (headIndex >= 0) {
var defaultHeadXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(headIndex),
MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(headIndex));
var currentHeadXform = new Xform(Quat.cancelOutRollAndPitch(MyAvatar.getAbsoluteJointRotationInObjectFrame(headIndex)),
MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex));
var defaultToReferenceXform = Xform.mul(currentHeadXform, defaultHeadXform.inv());
return defaultToReferenceXform;
} else {
return Xform.ident();
}
}
function calibrate() {
leftFoot = undefined;
rightFoot = undefined;
hips = undefined;
spine2 = undefined;
var defaultToReferenceXform = computeDefaultToReferenceXform();
var poses = [];
if (Controller.Hardware.Vive) {
TRACKED_OBJECT_POSES.forEach(function (key) {
var channel = Controller.Hardware.Vive[key];
var pose = Controller.getPoseValue(channel);
if (pose.valid) {
poses.push({
channel: channel,
pose: pose
});
}
});
}
print("AJT: calibrating, num tracked poses = " + poses.length + ", sensorConfig = " + SENSOR_CONFIG_NAMES[sensorConfig]);
var config = sensorConfig;
if (config === AUTO) {
if (poses.length === 2) {
config = FEET_ONLY;
} else if (poses.length === 3) {
config = FEET_AND_HIPS;
} else if (poses.length >= 4) {
config = FEET_HIPS_AND_CHEST;
} else {
print("AJT: auto config failed: poses.length = " + poses.length);
config = FEET_ONLY;
}
}
if (poses.length >= 2) {
// sort by y
poses.sort(function(a, b) {
var ay = a.pose.translation.y;
var by = b.pose.translation.y;
return ay - by;
});
if (poses[0].pose.translation.x > poses[1].pose.translation.x) {
rightFoot = poses[0];
leftFoot = poses[1];
} else {
rightFoot = poses[1];
leftFoot = poses[0];
}
// compute offsets
rightFoot.offsetXform = computeOffsetXform(defaultToReferenceXform, rightFoot.pose, MyAvatar.getJointIndex("RightFoot"));
leftFoot.offsetXform = computeOffsetXform(defaultToReferenceXform, leftFoot.pose, MyAvatar.getJointIndex("LeftFoot"));
print("AJT: rightFoot = " + JSON.stringify(rightFoot));
print("AJT: leftFoot = " + JSON.stringify(leftFoot));
if (config === FEET_ONLY) {
// we're done!
} else if (config === FEET_AND_HIPS && poses.length >= 3) {
hips = poses[2];
} else if (config === FEET_AND_CHEST && poses.length >= 3) {
spine2 = poses[2];
} else if (config === FEET_HIPS_AND_CHEST && poses.length >= 4) {
hips = poses[2];
spine2 = poses[3];
} else {
// TODO: better error messages
print("AJT: could not calibrate for sensor config " + SENSOR_CONFIG_NAMES[config] + ", please try again!");
}
if (hips) {
hips.offsetXform = computeOffsetXform(defaultToReferenceXform, hips.pose, MyAvatar.getJointIndex("Hips"));
print("AJT: hips = " + JSON.stringify(hips));
}
if (spine2) {
spine2.offsetXform = computeOffsetXform(defaultToReferenceXform, spine2.pose, MyAvatar.getJointIndex("Spine2"));
print("AJT: spine2 = " + JSON.stringify(spine2));
}
} else {
print("AJT: could not find two trackers, try again!");
}
}
var ikTypes = {
RotationAndPosition: 0,
RotationOnly: 1,
HmdHead: 2,
HipsRelativeRotationAndPosition: 3,
Off: 4
};
var handlerId;
function computeIKTargetXform(jointInfo) {
var pose = Controller.getPoseValue(jointInfo.channel);
var offsetXform = jointInfo.offsetXform;
return Xform.mul(Y_180_XFORM, Xform.mul(new Xform(pose.rotation, pose.translation), offsetXform));
}
function update(dt) {
if (rightTriggerPressed && leftTriggerPressed) {
if (!triggerPressHandled) {
triggerPressHandled = true;
if (handlerId) {
print("AJT: UN-CALIBRATE!");
// go back to normal, vive pucks will be ignored.
leftFoot = undefined;
rightFoot = undefined;
hips = undefined;
spine2 = undefined;
if (handlerId) {
print("AJT: un-hooking animation state handler");
MyAvatar.removeAnimationStateHandler(handlerId);
handlerId = undefined;
}
} else {
print("AJT: CALIBRATE!");
calibrate();
var animVars = [];
if (leftFoot) {
animVars.push("leftFootType");
animVars.push("leftFootPosition");
animVars.push("leftFootRotation");
}
if (rightFoot) {
animVars.push("rightFootType");
animVars.push("rightFootPosition");
animVars.push("rightFootRotation");
}
if (hips) {
animVars.push("hipsType");
animVars.push("hipsPosition");
animVars.push("hipsRotation");
}
if (spine2) {
animVars.push("spine2Type");
animVars.push("spine2Position");
animVars.push("spine2Rotation");
}
// hook up new anim state handler that maps vive pucks to ik system.
handlerId = MyAvatar.addAnimationStateHandler(function (props) {
var result = {}, xform;
if (rightFoot) {
xform = computeIKTargetXform(rightFoot);
result.rightFootType = ikTypes.RotationAndPosition;
result.rightFootPosition = xform.pos;
result.rightFootRotation = xform.rot;
}
if (leftFoot) {
xform = computeIKTargetXform(leftFoot);
result.leftFootType = ikTypes.RotationAndPosition;
result.leftFootPosition = xform.pos;
result.leftFootRotation = xform.rot;
}
if (hips) {
xform = computeIKTargetXform(hips);
result.hipsType = ikTypes.RotationAndPosition;
result.hipsPosition = xform.pos;
result.hipsRotation = xform.rot;
}
if (spine2) {
xform = computeIKTargetXform(spine2);
result.spine2Type = ikTypes.RotationAndPosition;
result.spine2Position = xform.pos;
result.spine2Rotation = xform.rot;
}
return result;
}, animVars);
}
}
} else {
triggerPressHandled = false;
}
var drawMarkers = false;
if (drawMarkers) {
var RED = {x: 1, y: 0, z: 0, w: 1};
var BLUE = {x: 0, y: 0, z: 1, w: 1};
if (leftFoot) {
var leftFootPose = Controller.getPoseValue(leftFoot.channel);
DebugDraw.addMyAvatarMarker("leftFootTracker", leftFootPose.rotation, leftFootPose.translation, BLUE);
}
if (rightFoot) {
var rightFootPose = Controller.getPoseValue(rightFoot.channel);
DebugDraw.addMyAvatarMarker("rightFootTracker", rightFootPose.rotation, rightFootPose.translation, RED);
}
if (hips) {
var hipsPose = Controller.getPoseValue(hips.channel);
DebugDraw.addMyAvatarMarker("hipsTracker", hipsPose.rotation, hipsPose.translation, GREEN);
}
}
var drawReferencePose = false;
if (drawReferencePose) {
var GREEN = {x: 0, y: 1, z: 0, w: 1};
var defaultToReferenceXform = computeDefaultToReferenceXform();
var leftFootIndex = MyAvatar.getJointIndex("LeftFoot");
if (leftFootIndex > 0) {
var defaultLeftFootXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(leftFootIndex),
MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(leftFootIndex));
var referenceLeftFootXform = Xform.mul(defaultToReferenceXform, defaultLeftFootXform);
DebugDraw.addMyAvatarMarker("leftFootTracker", referenceLeftFootXform.rot, referenceLeftFootXform.pos, GREEN);
}
}
}
Script.update.connect(update);
Script.scriptEnding.connect(function () {
Controller.disableMapping(MAPPING_NAME);
Script.update.disconnect(update);
});

View file

@ -18,14 +18,14 @@ function shutdown() {
});
}
var WHITE = {x: 1, y: 1, z: 1, w: 1};
var BLUE = {x: 0, y: 0, z: 1, w: 1};
function update(dt) {
if (Controller.Hardware.Vive) {
TRACKED_OBJECT_POSES.forEach(function (key) {
var pose = Controller.getPoseValue(Controller.Hardware.Vive[key]);
if (pose.valid) {
DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, WHITE);
DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, BLUE);
} else {
DebugDraw.removeMyAvatarMarker(key);
}

View file

@ -111,12 +111,12 @@ var EQUIP_RADIUS = 0.2; // radius used for palm vs equip-hotspot for equipping.
// has reached the required position, and then grow larger once the hand is close enough to equip.
var EQUIP_HOTSPOT_RENDER_RADIUS = 0.0; // radius used for palm vs equip-hotspot for rendering hot-spots
var MAX_EQUIP_HOTSPOT_RADIUS = 1.0;
var MAX_FAR_TO_NEAR_EQUIP_HOTSPOT_RADIUS = 0.5; // radius used for far to near equipping object.
var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
var NEAR_GRAB_RADIUS = 0.1; // radius used for palm vs object for near grabbing.
var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand
var FAR_TO_NEAR_GRAB_MAX_DISTANCE = 0.3; // In far to near grabbing conversion,grab the object if distancetoObject from hand is less than this.
var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing.
var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed
@ -155,6 +155,7 @@ var INCHES_TO_METERS = 1.0 / 39.3701;
// these control how long an abandoned pointer line or action will hang around
var ACTION_TTL = 15; // seconds
var ACTION_TTL_ZERO = 0; // seconds
var ACTION_TTL_REFRESH = 5;
var PICKS_PER_SECOND_PER_HAND = 60;
var MSECS_PER_SEC = 1000.0;
@ -192,6 +193,7 @@ var FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"];
var holdEnabled = true;
var nearGrabEnabled = true;
var farGrabEnabled = true;
var farToNearGrab = false;
var myAvatarScalingEnabled = true;
var objectScalingEnabled = true;
var mostRecentSearchingHand = RIGHT_HAND;
@ -2097,6 +2099,15 @@ function MyController(hand) {
return true;
};
this.entityIsFarToNearGrabbable = function (objectPosition, handPosition, maxDistance) {
var distanceToObjectFromHand = Vec3.length(Vec3.subtract(handPosition, objectPosition));
if (distanceToObjectFromHand > maxDistance) {
return false;
}
return true;
};
this.chooseNearEquipHotspots = function(candidateEntities, distance) {
var equippableHotspots = flatten(candidateEntities.map(function(entityID) {
return _this.collectEquipHotspots(entityID);
@ -2125,6 +2136,34 @@ function MyController(hand) {
return null;
}
};
this.chooseNearEquipHotspotsForFarToNearEquip = function(candidateEntities, distance) {
var equippableHotspots = flatten(candidateEntities.map(function(entityID) {
return _this.collectEquipHotspots(entityID);
})).filter(function(hotspot) {
return (Vec3.distance(hotspot.worldPosition, getControllerWorldLocation(_this.handToController(), true).position) <
hotspot.radius + distance);
});
return equippableHotspots;
};
this.chooseBestEquipHotspotForFarToNearEquip = function(candidateEntities) {
var DISTANCE = 1;
var equippableHotspots = this.chooseNearEquipHotspotsForFarToNearEquip(candidateEntities, DISTANCE);
var _this = this;
if (equippableHotspots.length > 0) {
// sort by distance
equippableHotspots.sort(function(a, b) {
var handControllerLocation = getControllerWorldLocation(_this.handToController(), true);
var aDistance = Vec3.distance(a.worldPosition, handControllerLocation.position);
var bDistance = Vec3.distance(b.worldPosition, handControllerLocation.position);
return aDistance - bDistance;
});
return equippableHotspots[0];
} else {
return null;
}
};
this.searchEnter = function() {
mostRecentSearchingHand = this.hand;
@ -2680,6 +2719,55 @@ function MyController(hand) {
this.grabbedThingID);
var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition));
var candidateHotSpotEntities = Entities.findEntities(controllerLocation.position,MAX_FAR_TO_NEAR_EQUIP_HOTSPOT_RADIUS);
entityPropertiesCache.addEntities(candidateHotSpotEntities);
var potentialEquipHotspot = this.chooseBestEquipHotspotForFarToNearEquip(candidateHotSpotEntities);
if (potentialEquipHotspot && (potentialEquipHotspot.entityID == this.grabbedThingID)) {
if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && holdEnabled) {
this.grabbedHotspot = potentialEquipHotspot;
this.grabbedThingID = potentialEquipHotspot.entityID;
this.grabbedIsOverlay = false;
var success = Entities.updateAction(this.grabbedThingID, this.actionID, {
targetPosition: newTargetPosition,
linearTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
targetRotation: this.currentObjectRotation,
angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
ttl: ACTION_TTL_ZERO
});
if (success) {
this.actionTimeout = now + (ACTION_TTL_ZERO * MSECS_PER_SEC);
} else {
print("continueDistanceHolding -- updateAction failed");
}
this.setState(STATE_HOLD, "equipping '" + entityPropertiesCache.getProps(this.grabbedThingID).name + "'");
return;
}
}
var rayPositionOnEntity = Vec3.subtract(grabbedProperties.position, this.offsetPosition);
//Far to Near Grab: If object is draw by user inside FAR_TO_NEAR_GRAB_MAX_DISTANCE, grab it
if (this.entityIsFarToNearGrabbable(rayPositionOnEntity, controllerLocation.position, FAR_TO_NEAR_GRAB_MAX_DISTANCE)) {
this.farToNearGrab = true;
var success = Entities.updateAction(this.grabbedThingID, this.actionID, {
targetPosition: newTargetPosition,
linearTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
targetRotation: this.currentObjectRotation,
angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
ttl: ACTION_TTL_ZERO // Overriding ACTION_TTL,Assign ACTION_TTL_ZERO so that the object is dropped down immediately after the trigger is released.
});
if (success) {
this.actionTimeout = now + (ACTION_TTL_ZERO * MSECS_PER_SEC);
} else {
print("continueDistanceHolding -- updateAction failed");
}
this.setState(STATE_NEAR_GRABBING , "near grab entity '" + this.grabbedThingID + "'");
return;
}
this.linearTimeScale = (this.linearTimeScale / 2);
if (this.linearTimeScale <= DISTANCE_HOLDING_ACTION_TIMEFRAME) {
this.linearTimeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
@ -3058,6 +3146,15 @@ function MyController(hand) {
this.nearGrabbing = function(deltaTime, timestamp) {
this.grabPointSphereOff();
var ttl = ACTION_TTL;
if (this.farToNearGrab) {
ttl = ACTION_TTL_ZERO; // farToNearGrab - Assign ACTION_TTL_ZERO so that, the object is dropped down immediately after the trigger is released.
if(!this.triggerClicked){
this.farToNearGrab = false;
}
}
if (this.state == STATE_NEAR_GRABBING && (!this.triggerClicked && this.secondaryReleased())) {
this.callEntityMethodOnGrabbed("releaseGrab");
this.setState(STATE_OFF, "trigger released");
@ -3225,20 +3322,20 @@ function MyController(hand) {
this.maybeScale(props);
}
if (this.actionID && this.actionTimeout - now < ACTION_TTL_REFRESH * MSECS_PER_SEC) {
if (this.actionID && this.actionTimeout - now < ttl * MSECS_PER_SEC) {
// if less than a 5 seconds left, refresh the actions ttl
var success = Entities.updateAction(this.grabbedThingID, this.actionID, {
hand: this.hand === RIGHT_HAND ? "right" : "left",
timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
relativePosition: this.offsetPosition,
relativeRotation: this.offsetRotation,
ttl: ACTION_TTL,
ttl: ttl,
kinematic: this.kinematicGrab,
kinematicSetVelocity: true,
ignoreIK: this.ignoreIK
});
if (success) {
this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
this.actionTimeout = now + (ttl * MSECS_PER_SEC);
} else {
print("continueNearGrabbing -- updateAction failed");
Entities.deleteAction(this.grabbedThingID, this.actionID);

View file

@ -343,7 +343,7 @@ var toolBar = (function () {
activeButton = tablet.addButton({
icon: "icons/tablet-icons/edit-i.svg",
activeIcon: "icons/tablet-icons/edit-a.svg",
text: "EDIT",
text: "CREATE",
sortOrder: 10
});
tablet.screenChanged.connect(function (type, url) {
@ -2094,3 +2094,4 @@ entityListTool.webView.webEventReceived.connect(function (data) {
});
}()); // END LOCAL_SCOPE

View file

@ -1,15 +1,17 @@
<html>
<head>
<title>Share</title>
<link rel="stylesheet" type="text/css" href="css/edit-style.css">
<link rel="stylesheet" type="text/css" href="css/hifi-style.css">
<link rel="stylesheet" type="text/css" href="css/SnapshotReview.css">
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
<script type="text/javascript" src="js/SnapshotReview.js"></script>
</head>
<body>
<div class="snapsection title">
<label>Snap</label>
<div class="title">
<label>Snapshots</label>
<label id="settingsLabel" for="snapshotSettings">Settings</label>
<input type="button" class="hifi-glyph naked" id="snapshotSettings" value="@" onclick="snapshotSettings()" />
</div>
<hr />
<div id="snapshot-pane">
@ -17,30 +19,16 @@
</div>
</div>
<div id="snapshot-controls">
<div class="snapsection" id="snap-buttons">
<div id="sharing">
<div class="button">
<span class="compound-button">
<input type="button" class="blue" id="share" value="Share in Feed" onclick="shareSelected()" />
<span class="glyph"></span>
</span>
</div>
</div>
<div class="button">
<input type="button" class="black" id="close" value="Don't Share" onclick="doNotShare()" />
</div>
</div>
<hr />
<div class="snapsection" id="snap-settings">
<span class="setting">
<input type="button" class="glyph naked" id="snapshotSettings" value="@" onclick="snapshotSettings()" />
<label for="snapshotSettings">Settings</label>
</span>
<span class="setting checkbox">
<input id="openFeed" type="checkbox" checked />
<label for="openFeed">Open feed after</label>
</span>
<div id="snap-settings">
<label>CAMERA CAPTURES</label><br />
<form action="">
<input type="radio" name="cameraCaptures" id="stillAndGif" value="stillAndGif" /><label for="stillAndGif"><span><span></span></span>Still + GIF</label>
<br />
<input type="radio" name="cameraCaptures" id="stillOnly" value="stillOnly" /><label for="stillOnly"><span><span></span></span>Still Only</label>
</form>
</div>
<input type="button" id="snap-button" onclick="takeSnapshot()" />
<div id="snap-settings-right"></div>
</div>
</body>
</html>

View file

@ -8,142 +8,280 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
*/
body {
padding-top: 0;
padding-bottom: 14px;
}
/*
// START styling of top bar and its contents
*/
.snapsection {
padding-top: 14px;
text-align: center;
}
.snapsection.title {
padding-top: 0;
.title {
padding: 6px 10px;
text-align: left;
height: 26px;
line-height: 26px;
clear: both;
}
.title label {
font-size: 18px;
position: relative;
top: 12px;
font-size: 18px;
float: left;
}
#snapshotSettings {
position: relative;
float: right;
}
#settingsLabel {
position: relative;
float: right;
font-family: Raleway-SemiBold;
font-size: 14px;
}
.hifi-glyph {
font-size: 30px;
top: -4px;
}
input[type=button].naked {
color: #afafaf;
background: none;
}
input[type=button].naked:hover {
color: #ffffff;
}
input[type=button].naked:active {
color: #afafaf;
}
/*
// END styling of top bar and its contents
*/
/*
// START styling of snapshot instructions panel
*/
.snapshotInstructions {
font-family: Raleway-Regular;
margin: 0 20px;
width: 100%;
height: 50%;
}
/*
// END styling of snapshot instructions panel
*/
/*
// START styling of snapshot pane and its contents
*/
#snapshot-pane {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
padding-top: 56px;
padding-bottom: 175px;
height: 560px;
display: flex;
justify-content: center;
align-items: center;
}
#snapshot-images {
height: 100%;
width: 100%;
position: relative;
}
#snapshot-images > div {
position: relative;
text-align: center;
}
#snapshot-images img {
max-width: 100%;
max-height: 100%;
}
.gifLabel {
position:absolute;
left: 15px;
top: 10px;
font-family: Raleway-SemiBold;
font-size: 18px;
color: white;
text-shadow: 2px 2px 3px #000000;
}
/*
// END styling of snapshot pane and its contents
*/
/*
// START styling of share bar
*/
.shareControls {
display: flex;
justify-content: space-between;
flex-direction: row;
align-items: center;
height: 50px;
line-height: 60px;
width: calc(100% - 8px);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
vertical-align: middle;
bottom: 4px;
left: 4px;
right: 4px;
}
#snapshot-images div.property {
margin-top: 0;
.shareButtons {
display: flex;
align-items: center;
margin-left: 30px;
height: 100%;
width: 80%;
}
.blastToConnections {
text-align: left;
margin-right: 25px;
height: 29px;
}
.shareWithEveryone {
background: #DDDDDD url(../img/shareToFeed.png) no-repeat scroll center;
border-width: 0px;
text-align: left;
margin-right: 8px;
height: 29px;
width: 30px;
border-radius: 3px;
}
.facebookButton {
background-image: url(../img/fb_logo.png);
width: 29px;
height: 29px;
display: inline-block;
margin-right: 8px;
}
.twitterButton {
background-image: url(../img/twitter_logo.png);
width: 29px;
height: 29px;
display: inline-block;
margin-right: 8px;
border-radius: 3px;
}
.showShareButtonsButtonDiv {
display: inline-flex;
align-items: center;
font-family: Raleway-SemiBold;
font-size: 14px;
color: white;
text-shadow: 2px 2px 3px #000000;
height: 100%;
margin-right: 10px;
width: 20%;
}
.showShareButton {
width: 40px;
height: 40px;
border-radius: 50%;
border-width: 0;
margin-left: 5px;
outline: none;
}
.showShareButton.active {
border-color: #00b4ef;
border-width: 3px;
background-color: white;
}
.showShareButton.active:hover {
background-color: #afafaf;
}
.showShareButton.active:active {
background-color: white;
}
.showShareButton.inactive {
border-width: 0;
background-color: white;
}
.showShareButton.inactive:hover {
background-color: #afafaf;
}
.showShareButton.inactive:active {
background-color: white;
}
.showShareButtonDots {
display: flex;
width: 32px;
height: 40px;
position: absolute;
top: 50%;
left: 7px;
transform: translate(0%, -50%);
top: 5px;
right: 14px;
pointer-events: none;
}
#snapshot-images img {
box-sizing: border-box;
padding: 0 7px 0 7px;
}
#snapshot-images img.multiple {
padding-left: 28px;
.showShareButtonDots > span {
width: 10px;
height: 10px;
margin: auto;
background-color: #0093C5;
border-radius: 50%;
border-width: 0;
display: inline;
}
/*
// END styling of share overlay
*/
/*
// START styling of snapshot controls (bottom panel) and its contents
*/
#snapshot-controls {
width: 100%;
position: absolute;
left: 0;
bottom: 14px;
overflow: hidden;
display: flex;
justify-content: center;
}
#snap-settings {
display: inline;
width: 150px;
margin: 2px auto 0 auto;
}
#snap-settings form input {
margin-bottom: 5px;
}
.prompt {
font-family: Raleway-SemiBold;
font-size: 14px;
}
div.button {
padding-top: 21px;
}
.compound-button {
position: relative;
height: auto;
}
.compound-button input {
padding-left: 40px;
}
.compound-button .glyph {
display: inline-block;
position: absolute;
left: 12px;
top: 16px;
width: 23px;
height: 23px;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgaGVpZ2h0PSI0MCIKICAgd2lkdGg9IjQwIgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgdmlld0JveD0iMCAwIDQwIDQwIgogICB5PSIwcHgiCiAgIHg9IjBweCIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGEzNCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48ZGM6dGl0bGU+PC9kYzp0aXRsZT48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczMyIiAvPjxzdHlsZQogICAgIGlkPSJzdHlsZTQiCiAgICAgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiM0MTQwNDI7fQoJLnN0MXtmaWxsOiNDQ0NDQ0M7fQoJLnN0MntmaWxsOiMxMzk4QkI7fQoJLnN0M3tmaWxsOiMzMUQ4RkY7fQo8L3N0eWxlPjxnCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTEwKSIKICAgICBpZD0iTGF5ZXJfMSI+PGNpcmNsZQogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJjaXJjbGUxMyIKICAgICAgIHI9IjQuNDQwMDAwMSIKICAgICAgIGN5PSIxMjYuMTciCiAgICAgICBjeD0iMjAuNTQwMDAxIgogICAgICAgY2xhc3M9InN0MSIgLz48cGF0aAogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMTUiCiAgICAgICBkPSJtIDI4Ljg3LDEzOS4yNiBjIDAuMDEsLTAuMDEgMC4wMiwtMC4wMiAwLjAzLC0wLjAzIGwgMCwtMS44NiBjIDAsLTIuNjggLTIuMzMsLTQuNzcgLTUsLTQuNzcgbCAtNi40MiwwIGMgLTIuNjgsMCAtNC44NSwyLjA5IC00Ljg1LDQuNzcgbCAwLDEuODggMTYuMjQsMCB6IgogICAgICAgY2xhc3M9InN0MSIgLz48cGF0aAogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMTciCiAgICAgICBkPSJtIDM4LjE3LDEyMy40MiBjIDAsLTMuOTcgLTMuMjIsLTcuMTkgLTcuMTksLTcuMTkgbCAtMjAuMzEsMCBjIC0zLjk3LDAgLTcuMTksMy4yMiAtNy4xOSw3LjE5IGwgMCwxNC4xOCBjIDAsMy45NyAzLjIyLDcuMTkgNy4xOSw3LjE5IGwgMjAuMzEsMCBjIDMuOTcsMCA3LjE5LC0zLjIyIDcuMTksLTcuMTkgbCAwLC0xNC4xOCB6IG0gLTEuNzgsMTQuMjcgYyAwLDMuMDMgLTIuNDYsNS40OSAtNS40OSw1LjQ5IGwgLTIwLjMyLDAgYyAtMy4wMywwIC01LjQ5LC0yLjQ2IC01LjQ5LC01LjQ5IGwgMCwtMTQuMTkgYyAwLC0zLjAzIDIuNDYsLTUuNDkgNS40OSwtNS40OSBsIDIwLjMzLDAgYyAzLjAzLDAgNS40OSwyLjQ2IDUuNDksNS40OSBsIDAsMTQuMTkgeiIKICAgICAgIGNsYXNzPSJzdDEiIC8+PC9nPjxnCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTEwKSIKICAgICBpZD0iTGF5ZXJfMiIgLz48L3N2Zz4=);
background-repeat: no-repeat;
background-size: 23px 23px;
}
.setting {
display: inline-table;
height: 28px;
}
.setting label {
display: table-cell;
vertical-align: middle;
font-family: Raleway-SemiBold;
font-size: 14px;
}
.setting + .setting {
margin-left: 18px;
}
input[type=button].naked {
font-size: 40px;
line-height: 40px;
width: 30px;
#snap-button {
width: 72px;
height: 72px;
padding: 0;
margin: 0 0 -6px 0;
position: relative;
top: -6px;
left: -8px;
background: none;
border-radius: 50%;
background: #EA4C5F;
border: 3px solid white;
margin: 2px auto 0 auto;
box-sizing: content-box;
display: inline;
outline:none;
}
#snap-button:disabled {
background: gray;
}
#snap-button:hover:enabled {
background: #C62147;
}
#snap-button:active:enabled {
background: #EA4C5F;
}
#snap-settings-right {
display: inline;
width: 150px;
margin: auto;
}
/*
// END styling of snapshot controls (bottom panel) and its contents
*/
input[type=button].naked:hover {
color: #00b4ef;
background: none;
/*
// START misc styling
*/
body {
padding: 0;
margin: 0;
overflow: hidden;
}
p {
margin: 2px 0;
}
h4 {
margin: 14px 0 0 0;
}
.centeredImage {
margin: 0 auto;
display: block;
}
/*
// END misc styling
*/

View file

@ -0,0 +1,170 @@
/*
// hifi-style.css
//
// Created by Zach Fox on 2017-04-18
// 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
*/
@font-face {
font-family: Raleway-Regular;
src: url(../../../../resources/fonts/Raleway-Regular.ttf), /* Windows production */
url(../../../../fonts/Raleway-Regular.ttf), /* OSX production */
url(../../../../interface/resources/fonts/Raleway-Regular.ttf); /* Development, running script in /HiFi/examples */
}
@font-face {
font-family: Raleway-Light;
src: url(../../../../resources/fonts/Raleway-Light.ttf),
url(../../../../fonts/Raleway-Light.ttf),
url(../../../../interface/resources/fonts/Raleway-Light.ttf);
}
@font-face {
font-family: Raleway-Bold;
src: url(../../../../resources/fonts/Raleway-Bold.ttf),
url(../../../../fonts/Raleway-Bold.ttf),
url(../../../../interface/resources/fonts/Raleway-Bold.ttf);
}
@font-face {
font-family: Raleway-SemiBold;
src: url(../../../../resources/fonts/Raleway-SemiBold.ttf),
url(../../../../fonts/Raleway-SemiBold.ttf),
url(../../../../interface/resources/fonts/Raleway-SemiBold.ttf);
}
@font-face {
font-family: FiraSans-SemiBold;
src: url(../../../../resources/fonts/FiraSans-SemiBold.ttf),
url(../../../../fonts/FiraSans-SemiBold.ttf),
url(../../../../interface/resources/fonts/FiraSans-SemiBold.ttf);
}
@font-face {
font-family: AnonymousPro-Regular;
src: url(../../../../resources/fonts/AnonymousPro-Regular.ttf),
url(../../../../fonts/AnonymousPro-Regular.ttf),
url(../../../../interface/resources/fonts/AnonymousPro-Regular.ttf);
}
@font-face {
font-family: HiFi-Glyphs;
src: url(../../../../resources/fonts/hifi-glyphs.ttf),
url(../../../../fonts/hifi-glyphs.ttf),
url(../../../../interface/resources/fonts/hifi-glyphs.ttf);
}
body {
color: #afafaf;
background-color: #404040;
font-family: Raleway-Regular;
font-size: 15px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
overflow-x: hidden;
overflow-y: auto;
}
hr {
border: none;
background: #404040 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAjSURBVBhXY1RVVf3PgARYjIyMoEwIYHRwcEBRwQSloYCBAQCwjgPMiI7W2QAAAABJRU5ErkJggg==) repeat-x top left;
padding: 1px;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
width: 100%;
position: absolute;
}
.hifi-glyph {
font-family: HiFi-Glyphs;
border: none;
//margin: -10px;
padding: 0;
}
input[type=radio] {
width: 2em;
margin: 0;
padding: 0;
font-size: 1em;
opacity: 0;
}
input[type=radio] + label{
display: inline-block;
margin-left: -2em;
line-height: 2em;
}
input[type=radio] + label > span{
display: inline-block;
width: 20px;
height: 20px;
margin: 5px;
border-radius: 50%;
background: #6B6A6B;
background-image: linear-gradient(#7D7D7D, #6B6A6B);
vertical-align: bottom;
}
input[type=radio]:checked + label > span{
background-image: linear-gradient(#7D7D7D, #6B6A6B);
}
input[type=radio]:active + label > span,
input[type=radio]:hover + label > span{
background-image: linear-gradient(#FFFFFF, #AFAFAF);
}
input[type=radio]:checked + label > span > span,
input[type=radio]:active + label > span > span{
display: block;
width: 10px;
height: 10px;
margin: 3px;
border: 2px solid #36CDFF;
border-radius: 50%;
background: #00B4EF;
}
.grayButton {
font-family: FiraSans-SemiBold;
color: white;
padding: 0px 10px;
border-width: 0px;
background-image: linear-gradient(#FFFFFF, #AFAFAF);
}
.grayButton:hover {
background-image: linear-gradient(#FFFFFF, #FFFFFF);
}
.grayButton:active {
background-image: linear-gradient(#AFAFAF, #AFAFAF);
}
.grayButton:disabled {
background-image: linear-gradient(#FFFFFF, ##AFAFAF);
}
.blueButton {
font-family: FiraSans-SemiBold;
color: white;
padding: 0px 10px;
border-radius: 3px;
border-width: 0px;
background-image: linear-gradient(#00B4EF, #1080B8);
min-height: 30px;
}
.blueButton:hover {
background-image: linear-gradient(#00B4EF, #00B4EF);
}
.blueButton:active {
background-image: linear-gradient(#1080B8, #1080B8);
}
.blueButton:disabled {
background-image: linear-gradient(#FFFFFF, #AFAFAF);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

View file

@ -10,117 +10,325 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
var paths = [], idCounter = 0, imageCount;
function addImage(data) {
if (!data.localPath) {
var paths = [];
var idCounter = 0;
var imageCount = 0;
function showSetupInstructions() {
var snapshotImagesDiv = document.getElementById("snapshot-images");
snapshotImagesDiv.className = "snapshotInstructions";
snapshotImagesDiv.innerHTML = '<img class="centeredImage" src="./img/snapshotIcon.png" alt="Snapshot Instructions" width="64" height="64"/>' +
'<br/>' +
'<p>This app lets you take and share snaps and GIFs with your connections in High Fidelity.</p>' +
"<h4>Setup Instructions</h4>" +
"<p>Before you can begin taking snaps, please choose where you'd like to save snaps on your computer:</p>" +
'<br/>' +
'<div style="text-align:center;">' +
'<input class="blueButton" style="margin-left:auto;margin-right:auto;width:130px;" type="button" value="CHOOSE" onclick="chooseSnapshotLocation()" />' +
'</div>';
document.getElementById("snap-button").disabled = true;
}
function showSetupComplete() {
var snapshotImagesDiv = document.getElementById("snapshot-images");
snapshotImagesDiv.className = "snapshotInstructions";
snapshotImagesDiv.innerHTML = '<img class="centeredImage" src="./img/snapshotIcon.png" alt="Snapshot Instructions" width="64" height="64"/>' +
'<br/>' +
"<h4>You're all set!</h4>" +
'<p>Try taking a snapshot by pressing the red button below.</p>';
}
function chooseSnapshotLocation() {
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "chooseSnapshotLocation"
}));
}
function clearImages() {
document.getElementById("snap-button").disabled = false;
var snapshotImagesDiv = document.getElementById("snapshot-images");
snapshotImagesDiv.classList.remove("snapshotInstructions");
while (snapshotImagesDiv.hasChildNodes()) {
snapshotImagesDiv.removeChild(snapshotImagesDiv.lastChild);
}
paths = [];
imageCount = 0;
idCounter = 0;
}
function addImage(image_data, isGifLoading, isShowingPreviousImages, canSharePreviousImages, hifiShareButtonsDisabled) {
if (!image_data.localPath) {
return;
}
var div = document.createElement("DIV"),
input = document.createElement("INPUT"),
label = document.createElement("LABEL"),
img = document.createElement("IMG"),
div2 = document.createElement("DIV"),
id = "p" + idCounter++;
img.id = id + "img";
function toggle() { data.share = input.checked; }
div.style.height = "" + Math.floor(100 / imageCount) + "%";
var id = "p" + idCounter++;
// imageContainer setup
var imageContainer = document.createElement("DIV");
imageContainer.id = id;
imageContainer.style.width = "100%";
imageContainer.style.height = "251px";
imageContainer.style.display = "flex";
imageContainer.style.justifyContent = "center";
imageContainer.style.alignItems = "center";
imageContainer.style.position = "relative";
// img setup
var img = document.createElement("IMG");
img.id = id + "img";
if (imageCount > 1) {
img.setAttribute("class", "multiple");
}
img.src = data.localPath;
div.appendChild(img);
if (imageCount > 1) { // I'd rather use css, but the included stylesheet is quite particular.
// Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state.
label.setAttribute('for', id); // cannot do label.for =
input.id = id;
input.type = "checkbox";
input.checked = false;
data.share = input.checked;
input.addEventListener('change', toggle);
div2.setAttribute("class", "property checkbox");
div2.appendChild(input);
div2.appendChild(label);
div.appendChild(div2);
} else {
data.share = true;
img.src = image_data.localPath;
imageContainer.appendChild(img);
document.getElementById("snapshot-images").appendChild(imageContainer);
paths.push(image_data.localPath);
var isGif = img.src.split('.').pop().toLowerCase() === "gif";
if (isGif) {
imageContainer.innerHTML += '<span class="gifLabel">GIF</span>';
}
if (!isGifLoading && !isShowingPreviousImages) {
shareForUrl(id);
} else if (isShowingPreviousImages && canSharePreviousImages) {
appendShareBar(id, image_data.story_id, isGif, hifiShareButtonsDisabled)
}
document.getElementById("snapshot-images").appendChild(div);
paths.push(data);
}
function handleShareButtons(messageOptions) {
var openFeed = document.getElementById('openFeed');
openFeed.checked = messageOptions.openFeedAfterShare;
openFeed.onchange = function () {
function appendShareBar(divID, story_id, isGif, hifiShareButtonsDisabled) {
var story_url = "https://highfidelity.com/user_stories/" + story_id;
var parentDiv = document.getElementById(divID);
parentDiv.setAttribute('data-story-id', story_id);
document.getElementById(divID).appendChild(createShareBar(divID, isGif, story_url, hifiShareButtonsDisabled));
if (divID === "p0") {
selectImageToShare(divID, true);
}
}
function createShareBar(parentID, isGif, shareURL, hifiShareButtonsDisabled) {
var shareBar = document.createElement("div");
shareBar.id = parentID + "shareBar";
shareBar.className = "shareControls";
var shareButtonsDivID = parentID + "shareButtonsDiv";
var showShareButtonsButtonDivID = parentID + "showShareButtonsButtonDiv";
var showShareButtonsButtonID = parentID + "showShareButtonsButton";
var showShareButtonsLabelID = parentID + "showShareButtonsLabel";
var blastToConnectionsButtonID = parentID + "blastToConnectionsButton";
var shareWithEveryoneButtonID = parentID + "shareWithEveryoneButton";
var facebookButtonID = parentID + "facebookButton";
var twitterButtonID = parentID + "twitterButton";
shareBar.innerHTML += '' +
'<div class="shareButtons" id="' + shareButtonsDivID + '" style="visibility:hidden">' +
'<input type="button"' + (hifiShareButtonsDisabled ? ' disabled' : '') + ' class="blastToConnections blueButton" id="' + blastToConnectionsButtonID + '" value="BLAST TO MY CONNECTIONS" onclick="blastToConnections(' + parentID + ', ' + isGif + ')" />' +
'<input type="button"' + (hifiShareButtonsDisabled ? ' disabled' : '') + ' class="shareWithEveryone" id="' + shareWithEveryoneButtonID + '" onclick="shareWithEveryone(' + parentID + ', ' + isGif + ')" />' +
'<a class="facebookButton" id="' + facebookButtonID + '" onclick="shareButtonClicked(' + parentID + ')" target="_blank" href="https://www.facebook.com/dialog/feed?app_id=1585088821786423&link=' + shareURL + '"></a>' +
'<a class="twitterButton" id="' + twitterButtonID + '" onclick="shareButtonClicked(' + parentID + ')" target="_blank" href="https://twitter.com/intent/tweet?text=I%20just%20took%20a%20snapshot!&url=' + shareURL + '&via=highfidelity&hashtags=VR,HiFi"></a>' +
'</div>' +
'<div class="showShareButtonsButtonDiv" id="' + showShareButtonsButtonDivID + '">' +
'<label id="' + showShareButtonsLabelID + '" for="' + showShareButtonsButtonID + '">SHARE</label>' +
'<input type="button" class="showShareButton inactive" id="' + showShareButtonsButtonID + '" onclick="selectImageToShare(' + parentID + ', true)" />' +
'<div class="showShareButtonDots">' +
'<span></span><span></span><span></span>' +
'</div>' +
'</div>';
// Add onclick handler to parent DIV's img to toggle share buttons
document.getElementById(parentID + 'img').onclick = function () { selectImageToShare(parentID, true) };
return shareBar;
}
function selectImageToShare(selectedID, isSelected) {
if (selectedID.id) {
selectedID = selectedID.id; // sometimes (?), `selectedID` is passed as an HTML object to these functions; we just want the ID
}
var imageContainer = document.getElementById(selectedID);
var image = document.getElementById(selectedID + 'img');
var shareBar = document.getElementById(selectedID + "shareBar");
var shareButtonsDiv = document.getElementById(selectedID + "shareButtonsDiv");
var showShareButtonsButton = document.getElementById(selectedID + "showShareButtonsButton");
if (isSelected) {
showShareButtonsButton.onclick = function () { selectImageToShare(selectedID, false) };
showShareButtonsButton.classList.remove("inactive");
showShareButtonsButton.classList.add("active");
image.onclick = function () { selectImageToShare(selectedID, false) };
imageContainer.style.outline = "4px solid #00b4ef";
imageContainer.style.outlineOffset = "-4px";
shareBar.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
shareButtonsDiv.style.visibility = "visible";
} else {
showShareButtonsButton.onclick = function () { selectImageToShare(selectedID, true) };
showShareButtonsButton.classList.remove("active");
showShareButtonsButton.classList.add("inactive");
image.onclick = function () { selectImageToShare(selectedID, true) };
imageContainer.style.outline = "none";
shareBar.style.backgroundColor = "rgba(0, 0, 0, 0.0)";
shareButtonsDiv.style.visibility = "hidden";
}
}
function shareForUrl(selectedID) {
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "shareSnapshotForUrl",
data: paths[parseInt(selectedID.substring(1))]
}));
}
function blastToConnections(selectedID, isGif) {
selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID
document.getElementById(selectedID + "blastToConnectionsButton").disabled = true;
document.getElementById(selectedID + "shareWithEveryoneButton").disabled = true;
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "blastToConnections",
story_id: document.getElementById(selectedID).getAttribute("data-story-id"),
isGif: isGif
}));
}
function shareWithEveryone(selectedID, isGif) {
selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID
document.getElementById(selectedID + "blastToConnectionsButton").disabled = true;
document.getElementById(selectedID + "shareWithEveryoneButton").disabled = true;
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "shareSnapshotWithEveryone",
story_id: document.getElementById(selectedID).getAttribute("data-story-id"),
isGif: isGif
}));
}
function shareButtonClicked(selectedID) {
selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "shareButtonClicked",
story_id: document.getElementById(selectedID).getAttribute("data-story-id")
}));
}
function cancelSharing(selectedID) {
selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID
var shareBar = document.getElementById(selectedID + "shareBar");
shareBar.style.display = "inline";
}
function handleCaptureSetting(setting) {
var stillAndGif = document.getElementById('stillAndGif');
var stillOnly = document.getElementById('stillOnly');
stillAndGif.checked = setting;
stillOnly.checked = !setting;
stillAndGif.onclick = function () {
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: (openFeed.checked ? "setOpenFeedTrue" : "setOpenFeedFalse")
action: "captureStillAndGif"
}));
};
if (!messageOptions.canShare) {
// this means you may or may not be logged in, but can't share
// because you are not in a public place.
document.getElementById("sharing").innerHTML = "<p class='prompt'>Snapshots can be shared when they're taken in shareable places.";
}
stillOnly.onclick = function () {
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "captureStillOnly"
}));
}
}
window.onload = function () {
// Something like the following will allow testing in a browser.
//addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'});
//addImage({ localPath: 'http://lorempixel.com/1512/1680' });
// Uncomment the line below to test functionality in a browser.
// See definition of "testInBrowser()" to modify tests.
//testInBrowser(true);
openEventBridge(function () {
// Set up a handler for receiving the data, and tell the .js we are ready to receive it.
EventBridge.scriptEventReceived.connect(function (message) {
message = JSON.parse(message);
if (message.type !== "snapshot") {
return;
}
switch (message.action) {
case 'showSetupInstructions':
showSetupInstructions();
break;
case 'snapshotLocationChosen':
clearImages();
showSetupComplete();
break;
case 'clearPreviousImages':
clearImages();
break;
case 'showPreviousImages':
clearImages();
var messageOptions = message.options;
imageCount = message.image_data.length;
message.image_data.forEach(function (element, idx, array) {
addImage(element, true, true, message.canShare, message.image_data[idx].buttonDisabled);
});
break;
case 'addImages':
// The last element of the message contents list contains a bunch of options,
// including whether or not we can share stuff
// The other elements of the list contain image paths.
var messageOptions = message.options;
// The last element of the message contents list contains a bunch of options,
// including whether or not we can share stuff
// The other elements of the list contain image paths.
var messageOptions = message.action.pop();
handleShareButtons(messageOptions);
if (messageOptions.containsGif) {
if (messageOptions.processingGif) {
imageCount = message.image_data.length + 1; // "+1" for the GIF that'll finish processing soon
message.image_data.unshift({ localPath: messageOptions.loadingGifPath });
message.image_data.forEach(function (element, idx, array) {
addImage(element, idx === 0, false, false);
});
} else {
var gifPath = message.image_data[0].localPath;
var p0img = document.getElementById('p0img');
p0img.src = gifPath;
if (messageOptions.containsGif) {
if (messageOptions.processingGif) {
imageCount = message.action.length + 1; // "+1" for the GIF that'll finish processing soon
message.action.unshift({ localPath: messageOptions.loadingGifPath });
message.action.forEach(addImage);
document.getElementById('p0').disabled = true;
} else {
var gifPath = message.action[0].localPath;
document.getElementById('p0').disabled = false;
document.getElementById('p0img').src = gifPath;
paths[0].localPath = gifPath;
}
} else {
imageCount = message.action.length;
message.action.forEach(addImage);
paths[0] = gifPath;
shareForUrl("p0");
}
} else {
imageCount = message.image_data.length;
message.image_data.forEach(function (element, idx, array) {
addImage(element, false, false, false);
});
}
break;
case 'captureSettings':
handleCaptureSetting(message.setting);
break;
case 'snapshotUploadComplete':
var isGif = message.image_url.split('.').pop().toLowerCase() === "gif";
appendShareBar(isGif || imageCount === 1 ? "p0" : "p1", message.story_id, isGif);
break;
default:
console.log("Unknown message action received in SnapshotReview.js.");
break;
}
});
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "ready"
}));
});
});;
};
// beware of bug: Cannot send objects at top level. (Nested in arrays is fine.)
function shareSelected() {
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: paths
}));
}
function doNotShare() {
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: []
}));
}
function snapshotSettings() {
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "openSettings"
}));
}
function takeSnapshot() {
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "takeSnapshot"
}));
}
function testInBrowser(isTestingSetupInstructions) {
if (isTestingSetupInstructions) {
showSetupInstructions();
} else {
imageCount = 1;
//addImage({ localPath: 'http://lorempixel.com/553/255' });
addImage({ localPath: 'C:/Users/valef/Desktop/hifi-snap-by-zfox-on-2017-04-26_10-26-53.gif' }, false, true, true, false);
}
}

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@
// Distributed under the Apache License, Version 2.0
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar */
/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar, Snapshot */
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
(function() { // BEGIN LOCAL_SCOPE
@ -24,28 +24,79 @@ var buttonConnected = false;
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var button = tablet.addButton({
icon: "icons/tablet-icons/snap-i.svg",
activeIcon: "icons/tablet-icons/snap-a.svg",
text: buttonName,
sortOrder: 5
});
function shouldOpenFeedAfterShare() {
var persisted = Settings.getValue('openFeedAfterShare', true); // might answer true, false, "true", or "false"
return persisted && (persisted !== 'false');
var snapshotOptions;
var imageData = [];
var storyIDsToMaybeDelete = [];
var shareAfterLogin = false;
var snapshotToShareAfterLogin;
var METAVERSE_BASE = location.metaverseServerUrl;
// It's totally unnecessary to return to C++ to perform many of these requests, such as DELETEing an old story,
// POSTING a new one, PUTTING a new audience, or GETTING story data. It's far more efficient to do all of that within JS
function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request.
var httpRequest = new XMLHttpRequest(), key;
// QT bug: apparently doesn't handle onload. Workaround using readyState.
httpRequest.onreadystatechange = function () {
var READY_STATE_DONE = 4;
var HTTP_OK = 200;
if (httpRequest.readyState >= READY_STATE_DONE) {
var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText,
response = !error && httpRequest.responseText,
contentType = !error && httpRequest.getResponseHeader('content-type');
if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc.
try {
response = JSON.parse(response);
} catch (e) {
error = e;
}
}
callback(error, response);
}
};
if (typeof options === 'string') {
options = { uri: options };
}
if (options.url) {
options.uri = options.url;
}
if (!options.method) {
options.method = 'GET';
}
if (options.body && (options.method === 'GET')) { // add query parameters
var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&';
for (key in options.body) {
params.push(key + '=' + options.body[key]);
}
options.uri += appender + params.join('&');
delete options.body;
}
if (options.json) {
options.headers = options.headers || {};
options.headers["Content-type"] = "application/json";
options.body = JSON.stringify(options.body);
}
for (key in options.headers || {}) {
httpRequest.setRequestHeader(key, options.headers[key]);
}
httpRequest.open(options.method, options.uri, true);
httpRequest.send(options.body);
}
function showFeedWindow() {
if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar"))
|| (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) {
tablet.loadQMLSource("TabletAddressDialog.qml");
function openLoginWindow() {
if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false))
|| (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) {
Menu.triggerOption("Login / Sign Up");
} else {
tablet.initialScreen("TabletAddressDialog.qml");
tablet.loadQMLOnTop("../../dialogs/TabletLoginDialog.qml");
HMD.openTablet();
}
}
var outstanding;
var readyData;
var shareAfterLogin = false;
var snapshotToShareAfterLogin;
function onMessage(message) {
// Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following:
// 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.)
@ -58,91 +109,257 @@ function onMessage(message) {
}
var isLoggedIn;
var needsLogin = false;
switch (message.action) {
case 'ready': // Send it.
case 'ready': // DOM is ready and page has loaded
tablet.emitScriptEvent(JSON.stringify({
type: "snapshot",
action: readyData
action: "captureSettings",
setting: Settings.getValue("alsoTakeAnimatedSnapshot", true)
}));
outstanding = 0;
if (Snapshot.getSnapshotsLocation() !== "") {
tablet.emitScriptEvent(JSON.stringify({
type: "snapshot",
action: "showPreviousImages",
options: snapshotOptions,
image_data: imageData,
canShare: !isDomainOpen(Settings.getValue("previousSnapshotDomainID"))
}));
} else {
tablet.emitScriptEvent(JSON.stringify({
type: "snapshot",
action: "showSetupInstructions"
}));
Settings.setValue("previousStillSnapPath", "");
Settings.setValue("previousStillSnapStoryID", "");
Settings.setValue("previousStillSnapSharingDisabled", false);
Settings.setValue("previousAnimatedSnapPath", "");
Settings.setValue("previousAnimatedSnapStoryID", "");
Settings.setValue("previousAnimatedSnapSharingDisabled", false);
}
break;
case 'chooseSnapshotLocation':
var snapshotPath = Window.browseDir("Choose Snapshots Directory", "", "");
if (snapshotPath) { // not cancelled
Snapshot.setSnapshotsLocation(snapshotPath);
tablet.emitScriptEvent(JSON.stringify({
type: "snapshot",
action: "snapshotLocationChosen"
}));
}
break;
case 'openSettings':
if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar"))
|| (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) {
if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false))
|| (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) {
Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "General Preferences");
} else {
tablet.loadQMLOnTop("TabletGeneralPreferences.qml");
}
break;
case 'setOpenFeedFalse':
Settings.setValue('openFeedAfterShare', false);
case 'captureStillAndGif':
print("Changing Snapshot Capture Settings to Capture Still + GIF");
Settings.setValue("alsoTakeAnimatedSnapshot", true);
break;
case 'setOpenFeedTrue':
Settings.setValue('openFeedAfterShare', true);
case 'captureStillOnly':
print("Changing Snapshot Capture Settings to Capture Still Only");
Settings.setValue("alsoTakeAnimatedSnapshot", false);
break;
case 'takeSnapshot':
takeSnapshot();
break;
case 'shareSnapshotForUrl':
isLoggedIn = Account.isLoggedIn();
if (isLoggedIn) {
print('Sharing snapshot with audience "for_url":', message.data);
Window.shareSnapshot(message.data, message.href || href);
} else {
// TODO
}
break;
case 'blastToConnections':
isLoggedIn = Account.isLoggedIn();
storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1);
if (message.isGif) {
Settings.setValue("previousAnimatedSnapSharingDisabled", true);
} else {
Settings.setValue("previousStillSnapSharingDisabled", true);
}
if (isLoggedIn) {
print('Uploading new story for announcement!');
request({
uri: METAVERSE_BASE + '/api/v1/user_stories/' + message.story_id,
method: 'GET'
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("ERROR getting details about existing snapshot story:", error || response.status);
return;
} else {
var requestBody = {
user_story: {
audience: "for_connections",
action: "announcement",
path: response.user_story.path,
place_name: response.user_story.place_name,
thumbnail_url: response.user_story.thumbnail_url,
// For historical reasons, the server doesn't take nested JSON objects.
// Thus, I'm required to STRINGIFY what should be a nested object.
details: JSON.stringify({
shareable_url: response.user_story.details.shareable_url,
image_url: response.user_story.details.image_url
})
}
}
request({
uri: METAVERSE_BASE + '/api/v1/user_stories',
method: 'POST',
json: true,
body: requestBody
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("ERROR uploading announcement story: ", error || response.status);
if (message.isGif) {
Settings.setValue("previousAnimatedSnapSharingDisabled", false);
} else {
Settings.setValue("previousStillSnapSharingDisabled", false);
}
return;
} else {
print("SUCCESS uploading announcement story! Story ID:", response.user_story.id);
}
});
}
});
} else {
openLoginWindow();
}
break;
case 'shareSnapshotWithEveryone':
isLoggedIn = Account.isLoggedIn();
storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1);
if (message.isGif) {
Settings.setValue("previousAnimatedSnapSharingDisabled", true);
} else {
Settings.setValue("previousStillSnapSharingDisabled", true);
}
if (isLoggedIn) {
print('Modifying audience of story ID', message.story_id, "to 'for_feed'");
var requestBody = {
audience: "for_feed"
}
if (message.isAnnouncement) {
requestBody.action = "announcement";
print('...Also announcing!');
}
request({
uri: METAVERSE_BASE + '/api/v1/user_stories/' + message.story_id,
method: 'PUT',
json: true,
body: requestBody
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("ERROR changing audience: ", error || response.status);
if (message.isGif) {
Settings.setValue("previousAnimatedSnapSharingDisabled", false);
} else {
Settings.setValue("previousStillSnapSharingDisabled", false);
}
return;
} else {
print("SUCCESS changing audience" + (message.isAnnouncement ? " and posting announcement!" : "!"));
}
});
} else {
openLoginWindow();
shareAfterLogin = true;
snapshotToShareAfterLogin = { path: message.data, href: message.href || href };
}
break;
case 'shareButtonClicked':
print('Twitter or FB "Share" button clicked! Removing ID', message.story_id, 'from storyIDsToMaybeDelete[].');
storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1);
print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete));
break;
default:
//tablet.webEventReceived.disconnect(onMessage); // <<< It's probably this that's missing?!
HMD.closeTablet();
isLoggedIn = Account.isLoggedIn();
message.action.forEach(function (submessage) {
if (submessage.share && !isLoggedIn) {
needsLogin = true;
submessage.share = false;
shareAfterLogin = true;
snapshotToShareAfterLogin = {path: submessage.localPath, href: submessage.href || href};
}
if (submessage.share) {
print('sharing', submessage.localPath);
outstanding = true;
Window.shareSnapshot(submessage.localPath, submessage.href || href);
} else {
print('not sharing', submessage.localPath);
}
});
if (outstanding && shouldOpenFeedAfterShare()) {
showFeedWindow();
outstanding = false;
}
if (needsLogin) { // after the possible feed, so that the login is on top
var isLoggedIn = Account.isLoggedIn();
if (!isLoggedIn) {
if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar"))
|| (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) {
Menu.triggerOption("Login / Sign Up");
} else {
tablet.loadQMLOnTop("../../dialogs/TabletLoginDialog.qml");
HMD.openTablet();
}
}
}
print('Unknown message action received by snapshot.js!');
break;
}
}
var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html");
var isInSnapshotReview = false;
function confirmShare(data) {
tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL);
readyData = data;
tablet.webEventReceived.connect(onMessage);
HMD.openTablet();
isInSnapshotReview = true;
var shouldActivateButton = false;
function onButtonClicked() {
if (isInSnapshotReview){
// for toolbar-mode: go back to home screen, this will close the window.
tablet.gotoHomeScreen();
} else {
shouldActivateButton = true;
var previousStillSnapPath = Settings.getValue("previousStillSnapPath");
var previousStillSnapStoryID = Settings.getValue("previousStillSnapStoryID");
var previousStillSnapSharingDisabled = Settings.getValue("previousStillSnapSharingDisabled");
var previousAnimatedSnapPath = Settings.getValue("previousAnimatedSnapPath");
var previousAnimatedSnapStoryID = Settings.getValue("previousAnimatedSnapStoryID");
var previousAnimatedSnapSharingDisabled = Settings.getValue("previousAnimatedSnapSharingDisabled");
snapshotOptions = {
containsGif: previousAnimatedSnapPath !== "",
processingGif: false,
shouldUpload: false
}
imageData = [];
if (previousAnimatedSnapPath !== "") {
imageData.push({ localPath: previousAnimatedSnapPath, story_id: previousAnimatedSnapStoryID, buttonDisabled: previousAnimatedSnapSharingDisabled });
}
if (previousStillSnapPath !== "") {
imageData.push({ localPath: previousStillSnapPath, story_id: previousStillSnapStoryID, buttonDisabled: previousStillSnapSharingDisabled });
}
tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL);
tablet.webEventReceived.connect(onMessage);
HMD.openTablet();
isInSnapshotReview = true;
}
}
function snapshotShared(errorMessage) {
if (!errorMessage) {
print('snapshot uploaded and shared');
function snapshotUploaded(isError, reply) {
if (!isError) {
var replyJson = JSON.parse(reply);
var storyID = replyJson.user_story.id;
storyIDsToMaybeDelete.push(storyID);
var imageURL = replyJson.user_story.details.image_url;
var isGif = imageURL.split('.').pop().toLowerCase() === "gif";
print('SUCCESS: Snapshot uploaded! Story with audience:for_url created! ID:', storyID);
tablet.emitScriptEvent(JSON.stringify({
type: "snapshot",
action: "snapshotUploadComplete",
story_id: storyID,
image_url: imageURL,
}));
if (isGif) {
Settings.setValue("previousAnimatedSnapStoryID", storyID);
} else {
Settings.setValue("previousStillSnapStoryID", storyID);
}
} else {
print(errorMessage);
}
if ((--outstanding <= 0) && shouldOpenFeedAfterShare()) {
showFeedWindow();
print(reply);
}
}
var href, domainId;
function onClicked() {
function takeSnapshot() {
tablet.emitScriptEvent(JSON.stringify({
type: "snapshot",
action: "clearPreviousImages"
}));
Settings.setValue("previousStillSnapPath", "");
Settings.setValue("previousStillSnapStoryID", "");
Settings.setValue("previousStillSnapSharingDisabled", false);
Settings.setValue("previousAnimatedSnapPath", "");
Settings.setValue("previousAnimatedSnapStoryID", "");
Settings.setValue("previousAnimatedSnapSharingDisabled", false);
// Raising the desktop for the share dialog at end will interact badly with clearOverlayWhenMoving.
// Turn it off now, before we start futzing with things (and possibly moving).
clearOverlayWhenMoving = MyAvatar.getClearOverlayWhenMoving(); // Do not use Settings. MyAvatar keeps a separate copy.
@ -152,14 +369,25 @@ function onClicked() {
// Even the domainId could change (e.g., if the user falls into a teleporter while recording).
href = location.href;
domainId = location.domainId;
Settings.setValue("previousSnapshotDomainID", domainId);
maybeDeleteSnapshotStories();
// update button states
resetOverlays = Menu.isOptionChecked("Overlays"); // For completness. Certainly true if the button is visible to be clicke.
resetOverlays = Menu.isOptionChecked("Overlays"); // For completeness. Certainly true if the button is visible to be clicked.
reticleVisible = Reticle.visible;
Reticle.visible = false;
Window.stillSnapshotTaken.connect(stillSnapshotTaken);
Window.processingGifStarted.connect(processingGifStarted);
Window.processingGifCompleted.connect(processingGifCompleted);
var includeAnimated = Settings.getValue("alsoTakeAnimatedSnapshot", true);
if (includeAnimated) {
Window.processingGifStarted.connect(processingGifStarted);
} else {
Window.stillSnapshotTaken.connect(stillSnapshotTaken);
}
if (buttonConnected) {
button.clicked.disconnect(onButtonClicked);
buttonConnected = false;
}
// hide overlays if they are on
if (resetOverlays) {
@ -170,13 +398,17 @@ function onClicked() {
Script.setTimeout(function () {
HMD.closeTablet();
Script.setTimeout(function () {
Window.takeSnapshot(false, true, 1.91);
Window.takeSnapshot(false, includeAnimated, 1.91);
}, SNAPSHOT_DELAY);
}, FINISH_SOUND_DELAY);
}
function isDomainOpen(id) {
var request = new XMLHttpRequest();
print("Checking open status of domain with ID:", id);
if (!id) {
return false;
}
var options = [
'now=' + new Date().toISOString(),
'include_actions=concurrency',
@ -184,15 +416,19 @@ function isDomainOpen(id) {
'restriction=open,hifi' // If we're sharing, we're logged in
// If we're here, protocol matches, and it is online
];
var url = location.metaverseServerUrl + "/api/v1/user_stories?" + options.join('&');
request.open("GET", url, false);
request.send();
if (request.status !== 200) {
return false;
}
var response = JSON.parse(request.response); // Not parsed for us.
return (response.status === 'success') &&
response.total_entries;
var url = METAVERSE_BASE + "/api/v1/user_stories?" + options.join('&');
return request({
uri: url,
method: 'GET'
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("ERROR getting open status of domain: ", error || response.status);
return false;
} else {
return response.total_entries;
}
});
}
function stillSnapshotTaken(pathStillSnapshot, notify) {
@ -203,20 +439,30 @@ function stillSnapshotTaken(pathStillSnapshot, notify) {
Menu.setIsOptionChecked("Overlays", true);
}
Window.stillSnapshotTaken.disconnect(stillSnapshotTaken);
if (!buttonConnected) {
button.clicked.connect(onButtonClicked);
buttonConnected = true;
}
// A Snapshot Review dialog might be left open indefinitely after taking the picture,
// during which time the user may have moved. So stash that info in the dialog so that
// it records the correct href. (We can also stash in .jpegs, but not .gifs.)
// last element in data array tells dialog whether we can share or not
var confirmShareContents = [
{ localPath: pathStillSnapshot, href: href },
{
containsGif: false,
processingGif: false,
canShare: !!isDomainOpen(domainId),
openFeedAfterShare: shouldOpenFeedAfterShare()
}];
confirmShare(confirmShareContents);
snapshotOptions = {
containsGif: false,
processingGif: false,
canShare: !isDomainOpen(domainId)
};
imageData = [{ localPath: pathStillSnapshot, href: href }];
Settings.setValue("previousStillSnapPath", pathStillSnapshot);
tablet.emitScriptEvent(JSON.stringify({
type: "snapshot",
action: "addImages",
options: snapshotOptions,
image_data: imageData
}));
if (clearOverlayWhenMoving) {
MyAvatar.setClearOverlayWhenMoving(true); // not until after the share dialog
}
@ -225,8 +471,7 @@ function stillSnapshotTaken(pathStillSnapshot, notify) {
function processingGifStarted(pathStillSnapshot) {
Window.processingGifStarted.disconnect(processingGifStarted);
button.clicked.disconnect(onClicked);
buttonConnected = false;
Window.processingGifCompleted.connect(processingGifCompleted);
// show hud
Reticle.visible = reticleVisible;
// show overlays if they were on
@ -234,16 +479,22 @@ function processingGifStarted(pathStillSnapshot) {
Menu.setIsOptionChecked("Overlays", true);
}
var confirmShareContents = [
{ localPath: pathStillSnapshot, href: href },
{
containsGif: true,
processingGif: true,
loadingGifPath: Script.resolvePath(Script.resourcesPath() + 'icons/loadingDark.gif'),
canShare: !!isDomainOpen(domainId),
openFeedAfterShare: shouldOpenFeedAfterShare()
}];
confirmShare(confirmShareContents);
snapshotOptions = {
containsGif: true,
processingGif: true,
loadingGifPath: Script.resolvePath(Script.resourcesPath() + 'icons/loadingDark.gif'),
canShare: !isDomainOpen(domainId)
};
imageData = [{ localPath: pathStillSnapshot, href: href }];
Settings.setValue("previousStillSnapPath", pathStillSnapshot);
tablet.emitScriptEvent(JSON.stringify({
type: "snapshot",
action: "addImages",
options: snapshotOptions,
image_data: imageData
}));
if (clearOverlayWhenMoving) {
MyAvatar.setClearOverlayWhenMoving(true); // not until after the share dialog
}
@ -252,57 +503,72 @@ function processingGifStarted(pathStillSnapshot) {
function processingGifCompleted(pathAnimatedSnapshot) {
Window.processingGifCompleted.disconnect(processingGifCompleted);
button.clicked.connect(onClicked);
buttonConnected = true;
if (!buttonConnected) {
button.clicked.connect(onButtonClicked);
buttonConnected = true;
}
var confirmShareContents = [
{ localPath: pathAnimatedSnapshot, href: href },
{
containsGif: true,
processingGif: false,
canShare: !!isDomainOpen(domainId),
openFeedAfterShare: shouldOpenFeedAfterShare()
}];
readyData = confirmShareContents;
snapshotOptions = {
containsGif: true,
processingGif: false,
canShare: !isDomainOpen(domainId)
}
imageData = [{ localPath: pathAnimatedSnapshot, href: href }];
Settings.setValue("previousAnimatedSnapPath", pathAnimatedSnapshot);
tablet.emitScriptEvent(JSON.stringify({
type: "snapshot",
action: readyData
action: "addImages",
options: snapshotOptions,
image_data: imageData
}));
}
function maybeDeleteSnapshotStories() {
storyIDsToMaybeDelete.forEach(function (element, idx, array) {
request({
uri: METAVERSE_BASE + '/api/v1/user_stories/' + element,
method: 'DELETE'
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("ERROR deleting snapshot story: ", error || response.status);
return;
} else {
print("SUCCESS deleting snapshot story with ID", element);
}
})
});
storyIDsToMaybeDelete = [];
}
function onTabletScreenChanged(type, url) {
button.editProperties({ isActive: shouldActivateButton });
shouldActivateButton = false;
if (isInSnapshotReview) {
tablet.webEventReceived.disconnect(onMessage);
isInSnapshotReview = false;
}
}
function onConnected() {
function onUsernameChanged() {
if (shareAfterLogin && Account.isLoggedIn()) {
print('sharing', snapshotToShareAfterLogin.path);
print('Sharing snapshot after login:', snapshotToShareAfterLogin.path);
Window.shareSnapshot(snapshotToShareAfterLogin.path, snapshotToShareAfterLogin.href);
shareAfterLogin = false;
if (shouldOpenFeedAfterShare()) {
showFeedWindow();
}
}
}
button.clicked.connect(onClicked);
button.clicked.connect(onButtonClicked);
buttonConnected = true;
Window.snapshotShared.connect(snapshotShared);
Window.snapshotShared.connect(snapshotUploaded);
tablet.screenChanged.connect(onTabletScreenChanged);
Account.usernameChanged.connect(onConnected);
Account.usernameChanged.connect(onUsernameChanged);
Script.scriptEnding.connect(function () {
if (buttonConnected) {
button.clicked.disconnect(onClicked);
button.clicked.disconnect(onButtonClicked);
buttonConnected = false;
}
if (tablet) {
tablet.removeButton(button);
}
Window.snapshotShared.disconnect(snapshotShared);
Window.snapshotShared.disconnect(snapshotUploaded);
tablet.screenChanged.disconnect(onTabletScreenChanged);
});

View file

@ -1,4 +1,6 @@
"use strict";
/*jslint vars:true, plusplus:true, forin:true*/
/*global Window, Script, Tablet, HMD, Controller, Account, XMLHttpRequest, location, print*/
//
// goto.js
@ -11,11 +13,29 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
(function() { // BEGIN LOCAL_SCOPE
(function () { // BEGIN LOCAL_SCOPE
var gotoQmlSource = "TabletAddressDialog.qml";
var buttonName = "GOTO";
var onGotoScreen = false;
var shouldActivateButton = false;
function ignore() { }
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var NORMAL_ICON = "icons/tablet-icons/goto-i.svg";
var NORMAL_ACTIVE = "icons/tablet-icons/goto-a.svg";
var WAITING_ICON = "icons/tablet-icons/goto-msg.svg";
var button = tablet.addButton({
icon: NORMAL_ICON,
activeIcon: NORMAL_ACTIVE,
text: buttonName,
sortOrder: 8
});
function messagesWaiting(isWaiting) {
button.editProperties({
icon: isWaiting ? WAITING_ICON : NORMAL_ICON
// No need for a different activeIcon, because we issue messagesWaiting(false) when the button goes active anyway.
});
}
function onClicked() {
if (onGotoScreen) {
@ -29,29 +49,115 @@
}
function onScreenChanged(type, url) {
ignore(type);
if (url === gotoQmlSource) {
onGotoScreen = true;
shouldActivateButton = true;
button.editProperties({isActive: shouldActivateButton});
} else {
messagesWaiting(false);
} else {
shouldActivateButton = false;
onGotoScreen = false;
button.editProperties({isActive: shouldActivateButton});
}
}
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var button = tablet.addButton({
icon: "icons/tablet-icons/goto-i.svg",
activeIcon: "icons/tablet-icons/goto-a.svg",
text: buttonName,
sortOrder: 8
});
button.clicked.connect(onClicked);
tablet.screenChanged.connect(onScreenChanged);
function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request.
var httpRequest = new XMLHttpRequest(), key;
// QT bug: apparently doesn't handle onload. Workaround using readyState.
httpRequest.onreadystatechange = function () {
var READY_STATE_DONE = 4;
var HTTP_OK = 200;
if (httpRequest.readyState >= READY_STATE_DONE) {
var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText,
response = !error && httpRequest.responseText,
contentType = !error && httpRequest.getResponseHeader('content-type');
if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc.
try {
response = JSON.parse(response);
} catch (e) {
error = e;
}
}
callback(error, response);
}
};
if (typeof options === 'string') {
options = {uri: options};
}
if (options.url) {
options.uri = options.url;
}
if (!options.method) {
options.method = 'GET';
}
if (options.body && (options.method === 'GET')) { // add query parameters
var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&';
for (key in options.body) {
params.push(key + '=' + options.body[key]);
}
options.uri += appender + params.join('&');
delete options.body;
}
if (options.json) {
options.headers = options.headers || {};
options.headers["Content-type"] = "application/json";
options.body = JSON.stringify(options.body);
}
for (key in options.headers || {}) {
httpRequest.setRequestHeader(key, options.headers[key]);
}
httpRequest.open(options.method, options.uri, true);
httpRequest.send(options.body);
}
var stories = {};
var DEBUG = false;
function pollForAnnouncements() {
var actions = DEBUG ? 'snapshot' : 'announcement';
var count = DEBUG ? 10 : 100;
var options = [
'now=' + new Date().toISOString(),
'include_actions=' + actions,
'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'),
'require_online=true',
'protocol=' + encodeURIComponent(location.protocolVersion()),
'per_page=' + count
];
var url = location.metaverseServerUrl + '/api/v1/user_stories?' + options.join('&');
request({
uri: url
}, function (error, data) {
if (error || (data.status !== 'success')) {
print("Error: unable to get", url, error || data.status);
return;
}
var didNotify = false;
data.user_stories.forEach(function (story) {
if (stories[story.id]) { // already seen
return;
}
stories[story.id] = story;
var message = story.username + " says something is happening in " + story.place_name + ". Open GOTO to join them.";
Window.displayAnnouncement(message);
didNotify = true;
});
if (didNotify) {
messagesWaiting(true);
if (HMD.isHandControllerAvailable()) {
var STRENGTH = 1.0, DURATION_MS = 60, HAND = 2; // both hands
Controller.triggerHapticPulse(STRENGTH, DURATION_MS, HAND);
}
}
});
}
var ANNOUNCEMENTS_POLL_TIME_MS = (DEBUG ? 10 : 60) * 1000;
var pollTimer = Script.setInterval(pollForAnnouncements, ANNOUNCEMENTS_POLL_TIME_MS);
Script.scriptEnding.connect(function () {
Script.clearInterval(pollTimer);
button.clicked.disconnect(onClicked);
tablet.removeButton(button);
tablet.screenChanged.disconnect(onScreenChanged);