mirror of
https://github.com/overte-org/overte.git
synced 2025-04-21 04:03:59 +02:00
Merge branch 'master' of https://github.com/highfidelity/hifi into ambient-bis
This commit is contained in:
commit
bb0bdac864
50 changed files with 3036 additions and 1460 deletions
8
BUILD.md
8
BUILD.md
|
@ -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
|
||||
|
||||
|
|
10
BUILD_OSX.md
10
BUILD_OSX.md
|
@ -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.
|
||||
|
|
12
BUILD_WIN.md
12
BUILD_WIN.md
|
@ -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.
|
||||
|
||||
|
|
|
@ -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": []
|
||||
|
|
18
interface/resources/icons/tablet-icons/goto-msg.svg
Normal file
18
interface/resources/icons/tablet-icons/goto-msg.svg
Normal 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 |
|
@ -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;
|
||||
|
|
218
interface/resources/qml/hifi/Feed.qml
Normal file
218
interface/resources/qml/hifi/Feed.qml
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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] : '');
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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); };
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
118
scripts/developer/tests/hipsIkTest.js
Normal file
118
scripts/developer/tests/hipsIkTest.js
Normal 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);
|
||||
});
|
||||
|
307
scripts/developer/tests/viveMotionCapture.js
Normal file
307
scripts/developer/tests/viveMotionCapture.js
Normal 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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
170
scripts/system/html/css/hifi-style.css
Normal file
170
scripts/system/html/css/hifi-style.css
Normal 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);
|
||||
}
|
BIN
scripts/system/html/img/fb_logo.png
Normal file
BIN
scripts/system/html/img/fb_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
scripts/system/html/img/shareIcon.png
Normal file
BIN
scripts/system/html/img/shareIcon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
scripts/system/html/img/shareToFeed.png
Normal file
BIN
scripts/system/html/img/shareToFeed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
scripts/system/html/img/snapshotIcon.png
Normal file
BIN
scripts/system/html/img/snapshotIcon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
scripts/system/html/img/twitter_logo.png
Normal file
BIN
scripts/system/html/img/twitter_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 552 B |
|
@ -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
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue