2016-12-13 09:15:28 -08:00
"use strict" ;
2016-12-12 16:37:16 -08:00
//
// pal.js
//
2023-03-20 21:46:20 -04:00
// Created by Howard Stearns on December 9th, 2016
// Copyright 2016 High Fidelity, Inc.
// Copyright 2023 Overte e.V.
2016-12-12 16:37:16 -08:00
//
// Distributed under the Apache License, Version 2.0
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
2023-03-20 21:46:20 -04:00
/* jslint vars:true, plusplus:true, forin:true */
/ * g l o b a l T a b l e t , S e t t i n g s , S c r i p t , A v a t a r L i s t , U s e r s , E n t i t i e s ,
MyAvatar , Camera , Overlays , Vec3 , Quat , HMD , Controller , Account ,
UserActivityLogger , Messages , Window , XMLHttpRequest , print , location , getControllerWorldLocation
* /
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
//
2016-12-12 16:37:16 -08:00
2018-07-16 16:28:44 -07:00
( function ( ) { // BEGIN LOCAL_SCOPE
2023-11-30 00:26:01 +01:00
var controllerStandard = Controller . Standard ;
2017-02-07 15:59:38 -08:00
2018-07-16 16:28:44 -07:00
var request = Script . require ( 'request' ) . request ;
var AppUi = Script . require ( 'appUi' ) ;
2017-05-01 13:02:18 -07:00
2017-03-20 15:19:28 -07:00
var populateNearbyUserList , color , textures , removeOverlays ,
2018-07-16 11:54:05 -07:00
controllerComputePickRay , off ,
2017-03-20 15:19:28 -07:00
receiveMessage , avatarDisconnected , clearLocalQMLDataAndClosePAL ,
2018-09-12 16:04:37 -07:00
CHANNEL , getConnectionData ,
2017-03-20 15:19:28 -07:00
avatarAdded , avatarRemoved , avatarSessionChanged ; // forward references;
2017-03-13 10:15:06 -07:00
2017-02-17 10:24:01 -07:00
// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed
2017-01-05 18:20:40 -07:00
// something, will revisit as this is sorta horrible.
2017-02-10 15:06:55 -08:00
var UNSELECTED _TEXTURES = {
"idle-D" : Script . resolvePath ( "./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png" ) ,
"idle-E" : Script . resolvePath ( "./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png" )
2017-01-05 18:20:40 -07:00
} ;
2017-02-10 15:06:55 -08:00
var SELECTED _TEXTURES = {
"idle-D" : Script . resolvePath ( "./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png" ) ,
"idle-E" : Script . resolvePath ( "./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png" )
2017-01-05 18:20:40 -07:00
} ;
2017-02-10 15:06:55 -08:00
var HOVER _TEXTURES = {
"idle-D" : Script . resolvePath ( "./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png" ) ,
"idle-E" : Script . resolvePath ( "./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png" )
2017-01-06 18:09:23 -07:00
} ;
2017-01-05 18:20:40 -07:00
2017-02-10 15:06:55 -08:00
var UNSELECTED _COLOR = { red : 0x1F , green : 0xC6 , blue : 0xA6 } ;
var SELECTED _COLOR = { red : 0xF3 , green : 0x91 , blue : 0x29 } ;
var HOVER _COLOR = { red : 0xD0 , green : 0xD0 , blue : 0xD0 } ; // almost white for now
2018-07-16 16:28:44 -07:00
var METAVERSE _BASE = Account . metaverseServerURL ;
2016-12-12 16:37:16 -08:00
2016-12-13 15:05:08 -08:00
Script . include ( "/~/system/libraries/controllers.js" ) ;
2017-02-21 13:14:45 -08:00
function projectVectorOntoPlane ( normalizedVector , planeNormal ) {
return Vec3 . cross ( planeNormal , Vec3 . cross ( normalizedVector , planeNormal ) ) ;
}
function angleBetweenVectorsInPlane ( from , to , normal ) {
var projectedFrom = projectVectorOntoPlane ( from , normal ) ;
var projectedTo = projectVectorOntoPlane ( to , normal ) ;
return Vec3 . orientedAngle ( projectedFrom , projectedTo , normal ) ;
}
2016-12-13 19:52:27 -08:00
//
// Overlays.
//
2016-12-13 09:15:28 -08:00
var overlays = { } ; // Keeps track of all our extended overlay data objects, keyed by target identifier.
2017-01-05 10:18:58 -07:00
2023-03-20 21:46:20 -04:00
function ExtendedOverlay ( key , properties , selected , hasModel ) { // A wrapper around overlays to store the key it is associated with.
2016-12-13 09:15:28 -08:00
overlays [ key ] = this ;
2017-01-05 18:20:40 -07:00
if ( hasModel ) {
var modelKey = key + "-m" ;
2023-03-20 21:46:20 -04:00
this . model = new ExtendedOverlay ( modelKey , {
"type" : "Model" ,
"modelURL" : Script . resolvePath ( "./assets/models/Avatar-Overlay-v1.fbx" ) ,
"textures" : textures ( selected ) ,
"ignorePickIntersection" : true
2017-01-05 18:20:40 -07:00
} , false , false ) ;
2017-01-05 10:18:58 -07:00
} else {
2017-01-05 18:20:40 -07:00
this . model = undefined ;
2017-01-05 10:18:58 -07:00
}
2016-12-13 09:15:28 -08:00
this . key = key ;
2016-12-15 11:00:32 -08:00
this . selected = selected || false ; // not undefined
2017-01-06 18:09:23 -07:00
this . hovering = false ;
2023-03-20 21:46:20 -04:00
this . activeOverlay = Entities . addEntity ( properties , "local" ) ; // We could use different overlays for (un)selected...
2016-12-13 09:15:28 -08:00
}
// Instance methods:
ExtendedOverlay . prototype . deleteOverlay = function ( ) { // remove display and data of this overlay
2023-03-20 21:46:20 -04:00
Entities . deleteEntity ( this . activeOverlay ) ;
2016-12-13 09:15:28 -08:00
delete overlays [ this . key ] ;
} ;
2016-12-13 13:01:45 -08:00
2016-12-13 09:15:28 -08:00
ExtendedOverlay . prototype . editOverlay = function ( properties ) { // change display of this overlay
2023-03-20 21:46:20 -04:00
Entities . editEntity ( this . activeOverlay , properties ) ;
2016-12-13 09:15:28 -08:00
} ;
2017-01-05 10:18:58 -07:00
2017-01-10 15:24:31 -07:00
function color ( selected , hovering , level ) {
var base = hovering ? HOVER _COLOR : selected ? SELECTED _COLOR : UNSELECTED _COLOR ;
2017-01-10 13:24:38 -08:00
function scale ( component ) {
var delta = 0xFF - component ;
return component + ( delta * level ) ;
}
return { red : scale ( base . red ) , green : scale ( base . green ) , blue : scale ( base . blue ) } ;
2017-01-05 10:18:58 -07:00
}
2017-01-06 18:09:23 -07:00
function textures ( selected , hovering ) {
return hovering ? HOVER _TEXTURES : selected ? SELECTED _TEXTURES : UNSELECTED _TEXTURES ;
}
// so we don't have to traverse the overlays to get the last one
var lastHoveringId = 0 ;
ExtendedOverlay . prototype . hover = function ( hovering ) {
2017-01-10 15:51:24 -07:00
this . hovering = hovering ;
2017-01-06 18:09:23 -07:00
if ( this . key === lastHoveringId ) {
if ( hovering ) {
return ;
}
2017-03-13 10:15:06 -07:00
lastHoveringId = 0 ;
2017-02-17 10:24:01 -07:00
}
2017-01-10 15:24:31 -07:00
this . editOverlay ( { color : color ( this . selected , hovering , this . audioLevel ) } ) ;
2017-01-06 18:09:23 -07:00
if ( this . model ) {
this . model . editOverlay ( { textures : textures ( this . selected , hovering ) } ) ;
}
if ( hovering ) {
2017-01-09 08:42:43 -07:00
// un-hover the last hovering overlay
2017-02-10 15:06:55 -08:00
if ( lastHoveringId && lastHoveringId !== this . key ) {
2017-01-09 08:42:43 -07:00
ExtendedOverlay . get ( lastHoveringId ) . hover ( false ) ;
}
2017-01-06 18:09:23 -07:00
lastHoveringId = this . key ;
}
2017-02-10 15:06:55 -08:00
} ;
2016-12-13 13:01:45 -08:00
ExtendedOverlay . prototype . select = function ( selected ) {
if ( this . selected === selected ) {
return ;
2016-12-13 09:15:28 -08:00
}
2017-02-17 10:24:01 -07:00
2017-01-19 17:22:39 -08:00
UserActivityLogger . palAction ( selected ? "avatar_selected" : "avatar_deselected" , this . key ) ;
2017-01-10 15:24:31 -07:00
this . editOverlay ( { color : color ( selected , this . hovering , this . audioLevel ) } ) ;
2017-01-05 18:20:40 -07:00
if ( this . model ) {
this . model . editOverlay ( { textures : textures ( selected ) } ) ;
2017-01-05 10:18:58 -07:00
}
2016-12-13 09:15:28 -08:00
this . selected = selected ;
} ;
// Class methods:
2016-12-15 11:00:32 -08:00
var selectedIds = [ ] ;
ExtendedOverlay . isSelected = function ( id ) {
return - 1 !== selectedIds . indexOf ( id ) ;
2017-01-04 11:19:32 -08:00
} ;
2016-12-13 09:15:28 -08:00
ExtendedOverlay . get = function ( key ) { // answer the extended overlay data object associated with the given avatar identifier
return overlays [ key ] ;
} ;
ExtendedOverlay . some = function ( iterator ) { // Bails early as soon as iterator returns truthy.
var key ;
for ( key in overlays ) {
if ( iterator ( ExtendedOverlay . get ( key ) ) ) {
return ;
}
}
} ;
2017-01-10 14:47:34 -07:00
ExtendedOverlay . unHover = function ( ) { // calls hover(false) on lastHoveringId (if any)
if ( lastHoveringId ) {
ExtendedOverlay . get ( lastHoveringId ) . hover ( false ) ;
}
} ;
2017-01-09 08:42:43 -07:00
// hit(overlay) on the one overlay intersected by pickRay, if any.
// noHit() if no ExtendedOverlay was intersected (helps with hover)
2017-01-10 16:12:02 -07:00
ExtendedOverlay . applyPickRay = function ( pickRay , hit , noHit ) {
2019-02-16 20:08:36 -08:00
// TODO: this could just include the necessary overlays for better performance
var pickedOverlay = Overlays . findRayIntersection ( pickRay , true ) ; // Depends on nearer coverOverlays to extend closer to us than farther ones.
2016-12-13 09:15:28 -08:00
if ( ! pickedOverlay . intersects ) {
2017-01-06 18:09:23 -07:00
if ( noHit ) {
return noHit ( ) ;
}
2016-12-13 09:15:28 -08:00
return ;
}
ExtendedOverlay . some ( function ( overlay ) { // See if pickedOverlay is one of ours.
2016-12-13 13:01:45 -08:00
if ( ( overlay . activeOverlay ) === pickedOverlay . overlayID ) {
2017-01-09 08:42:43 -07:00
hit ( overlay ) ;
2016-12-13 09:15:28 -08:00
return true ;
}
} ) ;
} ;
2017-01-04 11:19:32 -08:00
//
// Similar, for entities
//
function HighlightedEntity ( id , entityProperties ) {
this . id = id ;
2023-03-20 21:46:20 -04:00
this . overlay = Entities . addEntity ( {
"type" : "Shape" ,
"shape" : "Cube" ,
"position" : entityProperties . position ,
"rotation" : entityProperties . rotation ,
"dimensions" : entityProperties . dimensions ,
"primitiveMode" : "solid" ,
"color" : {
"red" : 0xF3 ,
"green" : 0x91 ,
"blue" : 0x29
2017-01-04 11:19:32 -08:00
} ,
2023-03-20 21:46:20 -04:00
"ignorePickIntersection" : true ,
"renderLayer" : "world" // Arguable. For now, let's not distract with mysterious wires around the scene.
} , "local" ) ;
2017-01-04 11:19:32 -08:00
HighlightedEntity . overlays . push ( this ) ;
}
HighlightedEntity . overlays = [ ] ;
HighlightedEntity . clearOverlays = function clearHighlightedEntities ( ) {
HighlightedEntity . overlays . forEach ( function ( highlighted ) {
2023-03-20 21:46:20 -04:00
Entities . deleteEntity ( highlighted . overlay ) ;
2017-01-04 11:19:32 -08:00
} ) ;
HighlightedEntity . overlays = [ ] ;
} ;
HighlightedEntity . updateOverlays = function updateHighlightedEntities ( ) {
HighlightedEntity . overlays . forEach ( function ( highlighted ) {
var properties = Entities . getEntityProperties ( highlighted . id , [ 'position' , 'rotation' , 'dimensions' ] ) ;
2023-03-20 21:46:20 -04:00
Entities . editEntity ( highlighted . overlay , {
"position" : properties . position ,
"rotation" : properties . rotation ,
"dimensions" : properties . dimensions
2017-01-04 11:19:32 -08:00
} ) ;
} ) ;
} ;
2017-02-24 10:31:55 -08:00
/ * t h i s c o n t a i n s c u r r e n t g a i n f o r a g i v e n n o d e ( b y s e s s i o n i d ) . M o r e e f f i c i e n t t h a n
2017-02-23 18:53:22 -08:00
* querying it , plus there isn ' t a getGain function so why write one * /
var sessionGains = { } ;
function convertDbToLinear ( decibels ) {
// +20db = 10x, 0dB = 1x, -10dB = 0.1x, etc...
// but, your perception is that something 2x as loud is +10db
// so we go from -60 to +20 or 1/64x to 4x. For now, we can
// maybe scale the signal this way??
2017-03-13 10:15:06 -07:00
return Math . pow ( 2 , decibels / 10.0 ) ;
2017-02-23 18:53:22 -08:00
}
2017-02-08 17:27:19 -08:00
function fromQml ( message ) { // messages are {method, params}, like json-rpc. See also sendToQml.
2018-07-16 16:28:44 -07:00
var data , connectionUserName , friendUserName ;
2016-12-13 15:05:08 -08:00
switch ( message . method ) {
case 'selected' :
2016-12-15 11:00:32 -08:00
selectedIds = message . params ;
2016-12-13 15:05:08 -08:00
ExtendedOverlay . some ( function ( overlay ) {
2016-12-15 11:00:32 -08:00
var id = overlay . key ;
var selected = ExtendedOverlay . isSelected ( id ) ;
overlay . select ( selected ) ;
2016-12-13 15:05:08 -08:00
} ) ;
2017-01-04 11:19:32 -08:00
HighlightedEntity . clearOverlays ( ) ;
if ( selectedIds . length ) {
Entities . findEntitiesInFrustum ( Camera . frustum ) . forEach ( function ( id ) {
// Because lastEditedBy is per session, the vast majority of entities won't match,
// so it would probably be worth reducing marshalling costs by asking for just we need.
// However, providing property name(s) is advisory and some additional properties are
// included anyway. As it turns out, asking for 'lastEditedBy' gives 'position', 'rotation',
// and 'dimensions', too, so we might as well make use of them instead of making a second
// getEntityProperties call.
// It would be nice if we could harden this against future changes by specifying all
// and only these four in an array, but see
// https://highfidelity.fogbugz.com/f/cases/2728/Entities-getEntityProperties-id-lastEditedBy-name-lastEditedBy-doesn-t-work
var properties = Entities . getEntityProperties ( id , 'lastEditedBy' ) ;
if ( ExtendedOverlay . isSelected ( properties . lastEditedBy ) ) {
new HighlightedEntity ( id , properties ) ;
}
} ) ;
}
2016-12-13 15:05:08 -08:00
break ;
2018-05-10 16:00:01 -07:00
case 'refresh' : // old name for refreshNearby
2017-03-13 10:15:06 -07:00
case 'refreshNearby' :
2017-03-03 16:56:12 -08:00
data = { } ;
ExtendedOverlay . some ( function ( overlay ) { // capture the audio data
data [ overlay . key ] = overlay ;
} ) ;
2016-12-23 12:22:33 -08:00
removeOverlays ( ) ;
2017-02-22 12:59:40 -08:00
// If filter is specified from .qml instead of through settings, update the settings.
if ( message . params . filter !== undefined ) {
Settings . setValue ( 'pal/filtered' , ! ! message . params . filter ) ;
}
2017-03-13 10:15:06 -07:00
populateNearbyUserList ( message . params . selected , data ) ;
UserActivityLogger . palAction ( "refresh_nearby" , "" ) ;
break ;
case 'refreshConnections' :
2017-03-14 14:05:50 -07:00
print ( 'Refreshing Connections...' ) ;
2017-03-13 10:15:06 -07:00
UserActivityLogger . palAction ( "refresh_connections" , "" ) ;
2016-12-23 12:22:33 -08:00
break ;
2017-04-05 18:16:16 -07:00
case 'removeConnection' :
connectionUserName = message . params ;
request ( {
uri : METAVERSE _BASE + '/api/v1/user/connections/' + connectionUserName ,
method : 'DELETE'
} , function ( error , response ) {
if ( error || ( response . status !== 'success' ) ) {
print ( "Error: unable to remove connection" , connectionUserName , error || response . status ) ;
return ;
}
2019-03-21 12:08:42 -07:00
sendToQml ( { method : 'connectionRemoved' , params : connectionUserName } ) ;
2017-04-05 18:16:16 -07:00
} ) ;
2018-07-16 16:28:44 -07:00
break ;
2017-04-05 18:16:16 -07:00
2017-03-31 15:05:21 -07:00
case 'removeFriend' :
friendUserName = message . params ;
2017-05-09 14:21:38 -07:00
print ( "Removing " + friendUserName + " from friends." ) ;
2017-03-31 15:05:21 -07:00
request ( {
uri : METAVERSE _BASE + '/api/v1/user/friends/' + friendUserName ,
method : 'DELETE'
} , function ( error , response ) {
if ( error || ( response . status !== 'success' ) ) {
2017-05-09 14:21:38 -07:00
print ( "Error: unable to unfriend " + friendUserName , error || response . status ) ;
2017-03-31 15:05:21 -07:00
return ;
}
2017-05-09 14:21:38 -07:00
getConnectionData ( friendUserName ) ;
2017-03-31 15:05:21 -07:00
} ) ;
2018-07-16 16:28:44 -07:00
break ;
2017-03-31 15:05:21 -07:00
case 'addFriend' :
friendUserName = message . params ;
2017-05-09 14:21:38 -07:00
print ( "Adding " + friendUserName + " to friends." ) ;
2017-03-31 15:05:21 -07:00
request ( {
uri : METAVERSE _BASE + '/api/v1/user/friends' ,
method : 'POST' ,
json : true ,
body : {
username : friendUserName ,
}
2018-07-16 16:28:44 -07:00
} , function ( error , response ) {
if ( error || ( response . status !== 'success' ) ) {
print ( "Error: unable to friend " + friendUserName , error || response . status ) ;
return ;
2017-03-31 15:05:21 -07:00
}
2018-07-16 16:28:44 -07:00
getConnectionData ( friendUserName ) ;
2018-07-18 14:03:26 -07:00
} ) ;
2017-03-31 15:05:21 -07:00
break ;
2018-06-01 13:33:45 -07:00
case 'http.request' :
2018-07-16 16:28:44 -07:00
break ; // Handled by request-service.
2018-09-14 14:37:04 -07:00
case 'hideNotificationDot' :
shouldShowDot = false ;
ui . messagesWaiting ( shouldShowDot ) ;
break ;
2016-12-13 15:05:08 -08:00
default :
2018-11-06 15:09:24 -08:00
print ( 'Unrecognized message from Pal.qml' ) ;
2016-12-13 15:05:08 -08:00
}
2017-02-08 17:27:19 -08:00
}
function sendToQml ( message ) {
2018-07-16 15:58:01 -07:00
ui . sendMessage ( message ) ;
2017-02-08 17:27:19 -08:00
}
2017-03-13 10:15:06 -07:00
function updateUser ( data ) {
print ( 'PAL update:' , JSON . stringify ( data ) ) ;
sendToQml ( { method : 'updateUsername' , params : data } ) ;
}
//
// User management services
//
// These are prototype versions that will be changed when the back end changes.
2017-03-31 15:05:21 -07:00
2017-03-13 10:15:06 -07:00
function requestJSON ( url , callback ) { // callback(data) if successfull. Logs otherwise.
2017-03-31 15:05:21 -07:00
request ( {
uri : url
} , function ( error , response ) {
2017-03-13 10:15:06 -07:00
if ( error || ( response . status !== 'success' ) ) {
2018-11-06 15:09:24 -08:00
print ( "Error: unable to get request" , error || response . status ) ;
2017-03-13 10:15:06 -07:00
return ;
}
callback ( response . data ) ;
} ) ;
}
function getProfilePicture ( username , callback ) { // callback(url) if successfull. (Logs otherwise)
// FIXME Prototype scrapes profile picture. We should include in user status, and also make available somewhere for myself
2017-03-31 15:05:21 -07:00
request ( {
uri : METAVERSE _BASE + '/users/' + username
} , function ( error , html ) {
2017-03-13 10:15:06 -07:00
var matched = ! error && html . match ( /img class="users-img" src="([^"]*)"/ ) ;
if ( ! matched ) {
print ( 'Error: Unable to get profile picture for' , username , error ) ;
2017-04-18 12:06:44 -07:00
callback ( '' ) ;
2017-03-13 10:15:06 -07:00
return ;
}
callback ( matched [ 1 ] ) ;
} ) ;
}
2018-07-26 12:01:56 -07:00
var SAFETY _LIMIT = 400 ;
2018-09-14 14:37:04 -07:00
function getAvailableConnections ( domain , callback , numResultsPerPage ) { // callback([{usename, location}...]) if successfull. (Logs otherwise)
var url = METAVERSE _BASE + '/api/v1/users?per_page=' + ( numResultsPerPage || SAFETY _LIMIT ) + '&' ;
2017-03-13 10:15:06 -07:00
if ( domain ) {
2017-03-28 16:44:16 -07:00
url += 'status=' + domain . slice ( 1 , - 1 ) ; // without curly braces
} else {
url += 'filter=connections' ; // regardless of whether online
2017-03-13 10:15:06 -07:00
}
2017-03-28 16:44:16 -07:00
requestJSON ( url , function ( connectionsData ) {
2017-05-08 12:15:03 -07:00
callback ( connectionsData . users ) ;
2017-03-28 16:44:16 -07:00
} ) ;
2017-03-13 10:15:06 -07:00
}
2017-05-09 14:21:38 -07:00
function getInfoAboutUser ( specificUsername , callback ) {
2018-07-23 15:25:06 -07:00
var url = METAVERSE _BASE + '/api/v1/users?filter=connections&per_page=' + SAFETY _LIMIT + '&search=' + encodeURIComponent ( specificUsername ) ;
2017-05-09 14:21:38 -07:00
requestJSON ( url , function ( connectionsData ) {
2018-07-23 15:25:06 -07:00
// You could have (up to SAFETY_LIMIT connections whose usernames contain the specificUsername.
// Search returns all such matches.
2017-05-09 14:21:38 -07:00
for ( user in connectionsData . users ) {
if ( connectionsData . users [ user ] . username === specificUsername ) {
callback ( connectionsData . users [ user ] ) ;
return ;
}
}
callback ( false ) ;
} ) ;
}
function getConnectionData ( specificUsername , domain ) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick.
2017-03-13 10:15:06 -07:00
function frob ( user ) { // get into the right format
2017-03-31 14:56:25 -07:00
var formattedSessionId = user . location . node _id || '' ;
if ( formattedSessionId !== '' && formattedSessionId . indexOf ( "{" ) != 0 ) {
formattedSessionId = "{" + formattedSessionId + "}" ;
}
2017-03-13 10:15:06 -07:00
return {
2017-03-31 14:56:25 -07:00
sessionId : formattedSessionId ,
2017-03-13 10:15:06 -07:00
userName : user . username ,
connection : user . connection ,
2017-05-08 12:15:03 -07:00
profileUrl : user . images . thumbnail ,
2017-03-13 10:15:06 -07:00
placeName : ( user . location . root || user . location . domain || { } ) . name || ''
} ;
}
2017-05-09 14:21:38 -07:00
if ( specificUsername ) {
getInfoAboutUser ( specificUsername , function ( user ) {
if ( user ) {
2017-03-13 10:15:06 -07:00
updateUser ( frob ( user ) ) ;
2017-05-09 14:21:38 -07:00
} else {
print ( 'Error: Unable to find information about ' + specificUsername + ' in connectionsData!' ) ;
}
} ) ;
2018-07-23 15:25:06 -07:00
} else if ( domain ) {
2017-05-09 14:21:38 -07:00
getAvailableConnections ( domain , function ( users ) {
2018-07-23 15:25:06 -07:00
users . forEach ( function ( user ) {
2018-07-26 12:01:56 -07:00
updateUser ( frob ( user ) ) ;
2018-07-23 15:25:06 -07:00
} ) ;
2017-05-09 14:21:38 -07:00
} ) ;
2018-07-23 15:25:06 -07:00
} else {
print ( "Error: unrecognized getConnectionData()" ) ;
2017-05-09 14:21:38 -07:00
}
2017-03-13 10:15:06 -07:00
}
2016-12-12 16:37:16 -08:00
2016-12-13 19:52:27 -08:00
//
// Main operations.
//
function addAvatarNode ( id ) {
2016-12-15 11:00:32 -08:00
var selected = ExtendedOverlay . isSelected ( id ) ;
2023-03-20 21:46:20 -04:00
return new ExtendedOverlay ( id , {
"type" : "Shape" ,
"shape" : "Sphere" ,
"renderLayer" : "front" ,
"primitiveMode" : "solid" ,
"alpha" : 0.8 ,
"color" : color ( selected , false , 0.0 ) ,
"ignorePickIntersection" : false
2018-06-08 17:58:06 -07:00
} , selected , true ) ;
2016-12-13 19:52:27 -08:00
}
2017-02-21 13:14:45 -08:00
// Each open/refresh will capture a stable set of avatarsOfInterest, within the specified filter.
var avatarsOfInterest = { } ;
2017-03-13 10:15:06 -07:00
function populateNearbyUserList ( selectData , oldAudioData ) {
var filter = Settings . getValue ( 'pal/filtered' ) && { distance : Settings . getValue ( 'pal/nearDistance' ) } ,
data = [ ] ,
avatars = AvatarList . getAvatarIdentifiers ( ) ,
myPosition = filter && Camera . position ,
2017-02-21 13:14:45 -08:00
frustum = filter && Camera . frustum ,
verticalHalfAngle = filter && ( frustum . fieldOfView / 2 ) ,
horizontalHalfAngle = filter && ( verticalHalfAngle * frustum . aspectRatio ) ,
orientation = filter && Camera . orientation ,
2017-03-22 17:00:12 +00:00
forward = filter && Quat . getForward ( orientation ) ,
2017-02-21 13:14:45 -08:00
verticalAngleNormal = filter && Quat . getRight ( orientation ) ,
horizontalAngleNormal = filter && Quat . getUp ( orientation ) ;
2017-03-13 10:15:06 -07:00
avatarsOfInterest = { } ;
2018-07-12 15:03:01 -07:00
2018-07-13 12:47:45 -07:00
var avatarData = AvatarList . getPalData ( ) . data ;
2018-07-12 15:03:01 -07:00
avatarData . forEach ( function ( currentAvatarData ) {
var id = currentAvatarData . sessionUUID ;
var name = currentAvatarData . sessionDisplayName ;
2017-02-21 13:14:45 -08:00
if ( ! name ) {
// Either we got a data packet but no identity yet, or something is really messed up. In any case,
// we won't be able to do anything with this user, so don't include them.
// In normal circumstances, a refresh will bring in the new user, but if we're very heavily loaded,
// we could be losing and gaining people randomly.
2018-07-12 15:03:01 -07:00
print ( 'No avatar identity data for' , currentAvatarData . sessionUUID ) ;
2017-02-21 13:14:45 -08:00
return ;
}
2018-07-12 15:03:01 -07:00
if ( id && myPosition && ( Vec3 . distance ( currentAvatarData . position , myPosition ) > filter . distance ) ) {
2017-02-21 13:14:45 -08:00
return ;
}
2018-07-12 15:03:01 -07:00
var normal = id && filter && Vec3 . normalize ( Vec3 . subtract ( currentAvatarData . position , myPosition ) ) ;
2017-03-22 17:00:12 +00:00
var horizontal = normal && angleBetweenVectorsInPlane ( normal , forward , horizontalAngleNormal ) ;
var vertical = normal && angleBetweenVectorsInPlane ( normal , forward , verticalAngleNormal ) ;
2017-02-21 13:14:45 -08:00
if ( id && filter && ( ( Math . abs ( horizontal ) > horizontalHalfAngle ) || ( Math . abs ( vertical ) > verticalHalfAngle ) ) ) {
return ;
}
2017-03-03 16:56:12 -08:00
var oldAudio = oldAudioData && oldAudioData [ id ] ;
2016-12-14 11:26:33 -08:00
var avatarPalDatum = {
2017-03-13 10:15:06 -07:00
profileUrl : '' ,
2017-02-21 13:14:45 -08:00
displayName : name ,
2016-12-16 14:01:21 -08:00
userName : '' ,
2017-03-13 10:15:06 -07:00
connection : '' ,
2016-12-22 14:01:03 -08:00
sessionId : id || '' ,
2017-03-03 16:56:12 -08:00
audioLevel : ( oldAudio && oldAudio . audioLevel ) || 0.0 ,
avgAudioLevel : ( oldAudio && oldAudio . avgAudioLevel ) || 0.0 ,
2017-02-16 10:53:02 -08:00
admin : false ,
personalMute : ! ! id && Users . getPersonalMuteStatus ( id ) , // expects proper boolean, not null
2017-03-20 17:20:03 -07:00
ignore : ! ! id && Users . getIgnoreStatus ( id ) , // ditto
2017-06-21 09:49:16 -07:00
isPresent : true ,
2018-07-12 15:03:01 -07:00
isReplicated : currentAvatarData . isReplicated
2016-12-14 11:26:33 -08:00
} ;
2017-07-06 12:15:44 -07:00
// Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin.
Users . requestUsernameFromID ( id ) ;
2018-07-13 12:47:45 -07:00
if ( id !== "" ) {
2016-12-30 09:51:43 -08:00
addAvatarNode ( id ) ; // No overlay for ourselves
2017-02-21 13:14:45 -08:00
avatarsOfInterest [ id ] = true ;
2017-03-13 10:15:06 -07:00
} else {
// Return our username from the Account API
avatarPalDatum . userName = Account . username ;
2016-12-13 09:15:28 -08:00
}
2016-12-14 11:26:33 -08:00
data . push ( avatarPalDatum ) ;
print ( 'PAL data:' , JSON . stringify ( avatarPalDatum ) ) ;
2016-12-12 16:37:16 -08:00
} ) ;
2018-01-30 20:46:27 +13:00
getConnectionData ( false , location . domainID ) ; // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain).
2017-03-13 10:15:06 -07:00
sendToQml ( { method : 'nearbyUsers' , params : data } ) ;
2017-01-19 16:03:46 -08:00
if ( selectData ) {
selectData [ 2 ] = true ;
2017-02-08 17:27:19 -08:00
sendToQml ( { method : 'select' , params : selectData } ) ;
2017-01-19 16:03:46 -08:00
}
2016-12-12 16:37:16 -08:00
}
2016-12-15 16:31:44 -08:00
2016-12-16 14:38:39 -08:00
// The function that handles the reply from the server
2017-01-17 14:16:31 -08:00
function usernameFromIDReply ( id , username , machineFingerprint , isAdmin ) {
2017-03-13 10:15:06 -07:00
var data = {
sessionId : ( MyAvatar . sessionUUID === id ) ? '' : id , // Pal.qml recognizes empty id specially.
2017-02-16 10:53:02 -08:00
// If we get username (e.g., if in future we receive it when we're friends), use it.
// Otherwise, use valid machineFingerprint (which is not valid when not an admin).
2017-03-13 10:15:06 -07:00
userName : username || ( Users . canKick && machineFingerprint ) || '' ,
admin : isAdmin
} ;
2016-12-16 14:38:39 -08:00
// Ship the data off to QML
2017-03-13 10:15:06 -07:00
updateUser ( data ) ;
2016-12-15 16:31:44 -08:00
}
2018-07-13 13:55:32 -07:00
function updateAudioLevel ( avatarData ) {
2018-07-12 15:03:01 -07:00
// the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged
// But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency
// of updating (the latter for efficiency too).
var audioLevel = 0.0 ;
var avgAudioLevel = 0.0 ;
2018-07-13 13:55:32 -07:00
var data = avatarData . sessionUUID === "" ? myData : ExtendedOverlay . get ( avatarData . sessionUUID ) ;
if ( data ) {
2018-07-12 15:16:54 -07:00
// we will do exponential moving average by taking some the last loudness and averaging
2018-07-13 13:55:32 -07:00
data . accumulatedLevel = AVERAGING _RATIO * ( data . accumulatedLevel || 0 ) + ( 1 - AVERAGING _RATIO ) * ( avatarData . audioLoudness ) ;
2018-07-12 15:03:01 -07:00
2018-07-12 15:16:54 -07:00
// add 1 to insure we don't go log() and hit -infinity. Math.log is
// natural log, so to get log base 2, just divide by ln(2).
2018-07-13 13:55:32 -07:00
audioLevel = scaleAudio ( Math . log ( data . accumulatedLevel + 1 ) / LOG2 ) ;
2018-07-12 15:03:01 -07:00
2018-07-12 15:16:54 -07:00
// decay avgAudioLevel
2018-07-13 13:55:32 -07:00
avgAudioLevel = Math . max ( ( 1 - AUDIO _PEAK _DECAY ) * ( data . avgAudioLevel || 0 ) , audioLevel ) ;
2018-07-12 15:03:01 -07:00
2018-07-13 13:55:32 -07:00
data . avgAudioLevel = avgAudioLevel ;
data . audioLevel = audioLevel ;
2018-07-12 15:03:01 -07:00
2018-07-12 15:16:54 -07:00
// now scale for the gain. Also, asked to boost the low end, so one simple way is
// to take sqrt of the value. Lets try that, see how it feels.
avgAudioLevel = Math . min ( 1.0 , Math . sqrt ( avgAudioLevel * ( sessionGains [ avatarData . sessionUUID ] || 0.75 ) ) ) ;
}
2018-07-12 15:03:01 -07:00
var param = { } ;
var level = [ audioLevel , avgAudioLevel ] ;
2018-07-13 12:47:45 -07:00
var userId = avatarData . sessionUUID ;
2018-07-12 15:03:01 -07:00
param [ userId ] = level ;
sendToQml ( { method : 'updateAudioLevel' , params : param } ) ;
}
2016-12-13 13:01:45 -08:00
var pingPong = true ;
2016-12-13 09:15:28 -08:00
function updateOverlays ( ) {
2016-12-13 13:01:45 -08:00
var eye = Camera . position ;
2018-07-12 15:03:01 -07:00
2018-07-13 12:47:45 -07:00
var avatarData = AvatarList . getPalData ( ) . data ;
2018-07-12 15:03:01 -07:00
avatarData . forEach ( function ( currentAvatarData ) {
2018-07-12 15:16:54 -07:00
2018-07-13 12:47:45 -07:00
if ( currentAvatarData . sessionUUID === "" || ! avatarsOfInterest [ currentAvatarData . sessionUUID ] ) {
2017-02-21 13:14:45 -08:00
return ; // don't update ourself, or avatars we're not interested in
2016-12-13 09:15:28 -08:00
}
2018-07-13 13:55:32 -07:00
updateAudioLevel ( currentAvatarData ) ;
2018-07-12 15:03:01 -07:00
var overlay = ExtendedOverlay . get ( currentAvatarData . sessionUUID ) ;
2016-12-13 19:52:27 -08:00
if ( ! overlay ) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back.
2018-07-12 15:03:01 -07:00
print ( 'Adding non-PAL avatar node' , currentAvatarData . sessionUUID ) ;
overlay = addAvatarNode ( currentAvatarData . sessionUUID ) ;
2016-12-13 15:05:08 -08:00
}
2018-07-12 15:03:01 -07:00
var target = currentAvatarData . position ;
2016-12-13 19:52:27 -08:00
var distance = Vec3 . distance ( target , eye ) ;
2018-07-12 15:03:01 -07:00
var offset = currentAvatarData . palOrbOffset ;
2017-03-13 10:15:06 -07:00
var diff = Vec3 . subtract ( target , eye ) ; // get diff between target and eye (a vector pointing to the eye from avatar position)
2017-01-05 15:04:40 -07:00
2017-01-05 18:20:40 -07:00
// move a bit in front, towards the camera
2017-01-06 08:44:22 -07:00
target = Vec3 . subtract ( target , Vec3 . multiply ( Vec3 . normalize ( diff ) , offset ) ) ;
2017-01-05 18:20:40 -07:00
2017-01-05 15:04:40 -07:00
// now bump it up a bit
target . y = target . y + offset ;
2017-01-05 14:22:35 -07:00
2016-12-13 19:52:27 -08:00
overlay . ping = pingPong ;
overlay . editOverlay ( {
2018-07-12 15:03:01 -07:00
color : color ( ExtendedOverlay . isSelected ( currentAvatarData . sessionUUID ) , overlay . hovering , overlay . audioLevel ) ,
2016-12-13 19:52:27 -08:00
position : target ,
2017-02-17 10:24:01 -07:00
dimensions : 0.032 * distance
2016-12-13 19:52:27 -08:00
} ) ;
2017-01-05 18:20:40 -07:00
if ( overlay . model ) {
overlay . model . ping = pingPong ;
overlay . model . editOverlay ( {
2017-02-17 10:24:01 -07:00
position : target ,
2017-01-05 18:20:40 -07:00
scale : 0.2 * distance , // constant apparent size
rotation : Camera . orientation
2017-01-05 10:18:58 -07:00
} ) ;
}
2016-12-13 09:15:28 -08:00
} ) ;
2016-12-13 13:01:45 -08:00
pingPong = ! pingPong ;
ExtendedOverlay . some ( function ( overlay ) { // Remove any that weren't updated. (User is gone.)
if ( overlay . ping === pingPong ) {
overlay . deleteOverlay ( ) ;
}
} ) ;
2017-03-13 10:15:06 -07:00
// We could re-populateNearbyUserList if anything added or removed, but not for now.
2017-01-04 11:19:32 -08:00
HighlightedEntity . updateOverlays ( ) ;
2016-12-13 09:15:28 -08:00
}
function removeOverlays ( ) {
2016-12-15 11:00:32 -08:00
selectedIds = [ ] ;
2017-01-06 18:09:23 -07:00
lastHoveringId = 0 ;
2017-01-04 11:19:32 -08:00
HighlightedEntity . clearOverlays ( ) ;
2017-02-10 15:06:55 -08:00
ExtendedOverlay . some ( function ( overlay ) {
overlay . deleteOverlay ( ) ;
} ) ;
2016-12-13 09:15:28 -08:00
}
2016-12-12 16:37:16 -08:00
2016-12-13 19:52:27 -08:00
//
// Clicks.
//
2016-12-13 15:05:08 -08:00
function handleClick ( pickRay ) {
ExtendedOverlay . applyPickRay ( pickRay , function ( overlay ) {
2016-12-15 11:00:32 -08:00
// Don't select directly. Tell qml, who will give us back a list of ids.
2017-01-19 16:03:46 -08:00
var message = { method : 'select' , params : [ [ overlay . key ] , ! overlay . selected , false ] } ;
2017-02-08 17:27:19 -08:00
sendToQml ( message ) ;
2016-12-13 15:05:08 -08:00
return true ;
} ) ;
}
function handleMouseEvent ( mousePressEvent ) { // handleClick if we get one.
if ( ! mousePressEvent . isLeftButton ) {
return ;
}
handleClick ( Camera . computePickRay ( mousePressEvent . x , mousePressEvent . y ) ) ;
}
2017-01-10 16:12:02 -07:00
function handleMouseMove ( pickRay ) { // given the pickRay, just do the hover logic
ExtendedOverlay . applyPickRay ( pickRay , function ( overlay ) {
overlay . hover ( true ) ;
} , function ( ) {
ExtendedOverlay . unHover ( ) ;
} ) ;
}
2017-01-11 14:43:37 -07:00
// handy global to keep track of which hand is the mouse (if any)
var currentHandPressed = 0 ;
2017-02-10 15:06:55 -08:00
var TRIGGER _CLICK _THRESHOLD = 0.85 ;
var TRIGGER _PRESS _THRESHOLD = 0.05 ;
2017-01-11 14:43:37 -07:00
2017-01-10 14:47:34 -07:00
function handleMouseMoveEvent ( event ) { // find out which overlay (if any) is over the mouse position
2017-02-10 15:06:55 -08:00
var pickRay ;
2017-01-10 14:47:34 -07:00
if ( HMD . active ) {
2017-02-10 15:06:55 -08:00
if ( currentHandPressed !== 0 ) {
2017-01-11 14:43:37 -07:00
pickRay = controllerComputePickRay ( currentHandPressed ) ;
2017-01-10 14:47:34 -07:00
} else {
// nothing should hover, so
ExtendedOverlay . unHover ( ) ;
return ;
}
} else {
pickRay = Camera . computePickRay ( event . x , event . y ) ;
}
handleMouseMove ( pickRay ) ;
}
2017-01-11 14:43:37 -07:00
function handleTriggerPressed ( hand , value ) {
2017-02-17 10:24:01 -07:00
// The idea is if you press one trigger, it is the one
2017-01-11 14:43:37 -07:00
// we will consider the mouse. Even if the other is pressed,
// we ignore it until this one is no longer pressed.
2017-02-10 15:06:55 -08:00
var isPressed = value > TRIGGER _PRESS _THRESHOLD ;
if ( currentHandPressed === 0 ) {
2017-01-11 14:43:37 -07:00
currentHandPressed = isPressed ? hand : 0 ;
return ;
2017-01-10 14:47:34 -07:00
}
2017-02-10 15:06:55 -08:00
if ( currentHandPressed === hand ) {
2017-01-11 14:43:37 -07:00
currentHandPressed = isPressed ? hand : 0 ;
return ;
2017-02-17 10:24:01 -07:00
}
2017-01-11 14:43:37 -07:00
// otherwise, the other hand is still triggered
// so do nothing.
2017-01-10 14:47:34 -07:00
}
2016-12-13 15:05:08 -08:00
// We get mouseMoveEvents from the handControllers, via handControllerPointer.
2016-12-13 19:52:27 -08:00
// But we don't get mousePressEvents.
2016-12-13 15:05:08 -08:00
var triggerMapping = Controller . newMapping ( Script . resolvePath ( '' ) + '-click' ) ;
2017-01-11 14:43:37 -07:00
var triggerPressMapping = Controller . newMapping ( Script . resolvePath ( '' ) + '-press' ) ;
2016-12-13 15:05:08 -08:00
function controllerComputePickRay ( hand ) {
var controllerPose = getControllerWorldLocation ( hand , true ) ;
if ( controllerPose . valid ) {
2016-12-13 15:09:46 -08:00
return { origin : controllerPose . position , direction : Quat . getUp ( controllerPose . orientation ) } ;
2016-12-13 15:05:08 -08:00
}
}
function makeClickHandler ( hand ) {
2016-12-13 19:52:27 -08:00
return function ( clicked ) {
2017-01-10 18:23:39 -07:00
if ( clicked > TRIGGER _CLICK _THRESHOLD ) {
2016-12-13 15:05:08 -08:00
var pickRay = controllerComputePickRay ( hand ) ;
handleClick ( pickRay ) ;
}
} ;
}
2017-01-11 14:43:37 -07:00
function makePressHandler ( hand ) {
return function ( value ) {
handleTriggerPressed ( hand , value ) ;
2017-02-10 15:06:55 -08:00
} ;
2017-01-11 14:43:37 -07:00
}
2023-11-30 00:26:01 +01:00
triggerMapping . from ( controllerStandard . RTClick ) . peek ( ) . to ( makeClickHandler ( controllerStandard . RightHand ) ) ;
triggerMapping . from ( controllerStandard . LTClick ) . peek ( ) . to ( makeClickHandler ( controllerStandard . LeftHand ) ) ;
triggerPressMapping . from ( controllerStandard . RT ) . peek ( ) . to ( makePressHandler ( controllerStandard . RightHand ) ) ;
triggerPressMapping . from ( controllerStandard . LT ) . peek ( ) . to ( makePressHandler ( controllerStandard . LeftHand ) ) ;
2017-02-10 15:06:55 -08:00
2018-07-16 13:06:12 -07:00
var ui ;
// Most apps can have people toggle the tablet closed and open again, and the app should remain "open" even while
// the tablet is not shown. However, for the pal, we explicitly close the app and return the tablet to it's
// home screen (so that the avatar highlighting goes away).
2017-03-29 14:53:45 -07:00
function tabletVisibilityChanged ( ) {
2018-07-16 13:06:12 -07:00
if ( ! ui . tablet . tabletShown && ui . isOpen ) {
ui . close ( ) ;
2017-03-29 14:53:45 -07:00
}
2018-09-12 15:32:51 -07:00
}
2018-07-12 15:03:01 -07:00
var UPDATE _INTERVAL _MS = 100 ;
2018-07-16 17:43:53 -07:00
var updateInterval ;
2018-07-12 15:03:01 -07:00
function createUpdateInterval ( ) {
return Script . setInterval ( function ( ) {
updateOverlays ( ) ;
} , UPDATE _INTERVAL _MS ) ;
2017-03-29 14:53:45 -07:00
}
2018-07-16 15:58:01 -07:00
var previousRequestsDomainListData = Users . requestsDomainListData ;
2018-09-14 14:37:04 -07:00
function palOpened ( ) {
ui . sendMessage ( {
method : 'changeConnectionsDotStatus' ,
shouldShowDot : shouldShowDot
} ) ;
2018-04-10 16:02:58 -07:00
2018-09-14 14:37:04 -07:00
previousRequestsDomainListData = Users . requestsDomainListData ;
2018-07-16 11:54:05 -07:00
Users . requestsDomainListData = true ;
2018-04-10 16:02:58 -07:00
2018-07-16 13:06:12 -07:00
ui . tablet . tabletShownChanged . connect ( tabletVisibilityChanged ) ;
2018-07-16 17:43:53 -07:00
updateInterval = createUpdateInterval ( ) ;
2018-07-16 11:54:05 -07:00
Controller . mousePressEvent . connect ( handleMouseEvent ) ;
Controller . mouseMoveEvent . connect ( handleMouseMoveEvent ) ;
Users . usernameFromIDReply . connect ( usernameFromIDReply ) ;
triggerMapping . enable ( ) ;
triggerPressMapping . enable ( ) ;
populateNearbyUserList ( ) ;
2016-12-12 16:37:16 -08:00
}
2016-12-13 15:05:08 -08:00
2017-01-05 13:45:03 -08:00
//
// Message from other scripts, such as edit.js
//
var CHANNEL = 'com.highfidelity.pal' ;
function receiveMessage ( channel , messageString , senderID ) {
2017-03-13 10:15:06 -07:00
if ( ( channel !== CHANNEL ) || ( senderID !== MyAvatar . sessionUUID ) ) {
2017-01-05 13:45:03 -08:00
return ;
}
var message = JSON . parse ( messageString ) ;
switch ( message . method ) {
case 'select' :
2018-07-16 13:06:12 -07:00
if ( ! ui . isOpen ) {
ui . open ( ) ;
2018-05-10 16:39:56 -07:00
Script . setTimeout ( function ( ) { sendToQml ( message ) ; } , 1000 ) ;
} else {
2018-05-10 16:00:01 -07:00
sendToQml ( message ) ; // Accepts objects, not just strings.
2018-05-10 16:39:56 -07:00
}
2017-01-05 13:45:03 -08:00
break ;
}
}
2017-01-04 11:19:32 -08:00
var AVERAGING _RATIO = 0.05 ;
2016-12-22 14:01:03 -08:00
var LOUDNESS _FLOOR = 11.0 ;
var LOUDNESS _SCALE = 2.8 / 5.0 ;
var LOG2 = Math . log ( 2.0 ) ;
2017-02-28 11:51:40 -07:00
var AUDIO _PEAK _DECAY = 0.02 ;
2017-01-10 13:08:07 -08:00
var myData = { } ; // we're not includied in ExtendedOverlay.get.
2016-12-22 14:01:03 -08:00
2017-02-17 10:24:01 -07:00
function scaleAudio ( val ) {
var audioLevel = 0.0 ;
if ( val <= LOUDNESS _FLOOR ) {
audioLevel = val / LOUDNESS _FLOOR * LOUDNESS _SCALE ;
} else {
2017-03-13 10:15:06 -07:00
audioLevel = ( val - ( LOUDNESS _FLOOR - 1 ) ) * LOUDNESS _SCALE ;
2017-02-17 10:24:01 -07:00
}
if ( audioLevel > 1.0 ) {
audioLevel = 1 ;
}
return audioLevel ;
}
2017-02-23 18:53:22 -08:00
2017-01-16 18:22:53 -08:00
function avatarDisconnected ( nodeID ) {
// remove from the pal list
2017-02-08 17:27:19 -08:00
sendToQml ( { method : 'avatarDisconnected' , params : [ nodeID ] } ) ;
2017-01-16 18:22:53 -08:00
}
2017-01-17 09:29:30 -08:00
function clearLocalQMLDataAndClosePAL ( ) {
2017-02-08 17:27:19 -08:00
sendToQml ( { method : 'clearLocalQMLData' } ) ;
2018-07-16 13:06:12 -07:00
if ( ui . isOpen ) {
ui . close ( ) ;
2017-07-07 08:08:52 -07:00
}
2017-01-04 13:31:16 -08:00
}
2016-12-12 16:37:16 -08:00
2017-03-22 15:04:44 -07:00
function avatarAdded ( avatarID ) {
2017-03-22 13:35:48 -07:00
sendToQml ( { method : 'palIsStale' , params : [ avatarID , 'avatarAdded' ] } ) ;
2017-03-20 15:19:28 -07:00
}
2017-03-20 17:20:03 -07:00
function avatarRemoved ( avatarID ) {
2017-03-22 13:35:48 -07:00
sendToQml ( { method : 'palIsStale' , params : [ avatarID , 'avatarRemoved' ] } ) ;
2017-03-20 15:19:28 -07:00
}
2017-03-22 15:04:44 -07:00
function avatarSessionChanged ( avatarID ) {
2017-03-22 13:35:48 -07:00
sendToQml ( { method : 'palIsStale' , params : [ avatarID , 'avatarSessionChanged' ] } ) ;
2017-03-20 15:19:28 -07:00
}
2018-09-14 14:37:04 -07:00
function notificationDataProcessPage ( data ) {
return data . data . users ;
}
var shouldShowDot = false ;
2018-09-18 16:35:53 -07:00
var pingPong = false ;
var storedOnlineUsers = { } ;
2018-09-17 14:41:18 -07:00
function notificationPollCallback ( connectionsArray ) {
//
// START logic for handling online/offline user changes
//
2018-09-18 16:35:53 -07:00
pingPong = ! pingPong ;
var newOnlineUsers = 0 ;
var message ;
connectionsArray . forEach ( function ( user ) {
var stored = storedOnlineUsers [ user . username ] ;
var storedOrNew = stored || user ;
storedOrNew . pingPong = pingPong ;
if ( stored ) {
return ;
}
2018-09-20 13:36:50 -07:00
newOnlineUsers ++ ;
storedOnlineUsers [ user . username ] = user ;
2018-10-31 13:23:58 -07:00
if ( ! ui . isOpen && ui . notificationInitialCallbackMade [ 0 ] ) {
2018-09-20 13:36:50 -07:00
message = user . username + " is available in " +
2018-09-20 14:34:55 -07:00
user . location . root . name + ". Open PEOPLE to join them." ;
2018-09-20 13:36:50 -07:00
ui . notificationDisplayBanner ( message ) ;
2018-09-17 14:41:18 -07:00
}
2018-09-18 16:35:53 -07:00
} ) ;
var key ;
for ( key in storedOnlineUsers ) {
if ( storedOnlineUsers [ key ] . pingPong !== pingPong ) {
delete storedOnlineUsers [ key ] ;
}
2018-09-17 14:41:18 -07:00
}
2018-09-18 16:35:53 -07:00
shouldShowDot = newOnlineUsers > 0 || ( Object . keys ( storedOnlineUsers ) . length > 0 && shouldShowDot ) ;
2018-09-17 14:41:18 -07:00
//
// END logic for handling online/offline user changes
//
2018-09-14 14:37:04 -07:00
if ( ! ui . isOpen ) {
ui . messagesWaiting ( shouldShowDot ) ;
ui . sendMessage ( {
method : 'changeConnectionsDotStatus' ,
shouldShowDot : shouldShowDot
} ) ;
2018-10-31 13:23:58 -07:00
if ( newOnlineUsers > 0 && ! ui . notificationInitialCallbackMade [ 0 ] ) {
2018-09-18 16:35:53 -07:00
message = newOnlineUsers + " of your connections " +
2018-09-20 14:34:55 -07:00
( newOnlineUsers === 1 ? "is" : "are" ) + " available online. Open PEOPLE to join them." ;
2018-09-18 16:35:53 -07:00
ui . notificationDisplayBanner ( message ) ;
2018-09-14 14:37:04 -07:00
}
}
}
function isReturnedDataEmpty ( data ) {
var usersArray = data . data . users ;
return usersArray . length === 0 ;
}
2018-04-10 16:02:58 -07:00
function startup ( ) {
2018-07-16 11:54:05 -07:00
ui = new AppUi ( {
buttonName : "PEOPLE" ,
sortOrder : 7 ,
home : "hifi/Pal.qml" ,
2018-09-14 14:37:04 -07:00
onOpened : palOpened ,
2018-07-16 11:54:05 -07:00
onClosed : off ,
2018-09-14 14:37:04 -07:00
onMessage : fromQml ,
2018-10-31 13:23:58 -07:00
notificationPollEndpoint : [ "/api/v1/users?filter=connections&status=online&per_page=10" ] ,
notificationPollTimeoutMs : [ 60000 ] ,
notificationDataProcessPage : [ notificationDataProcessPage ] ,
notificationPollCallback : [ notificationPollCallback ] ,
notificationPollStopPaginatingConditionMet : [ isReturnedDataEmpty ] ,
notificationPollCaresAboutSince : [ false ]
2018-04-10 16:02:58 -07:00
} ) ;
Window . domainChanged . connect ( clearLocalQMLDataAndClosePAL ) ;
Window . domainConnectionRefused . connect ( clearLocalQMLDataAndClosePAL ) ;
Messages . subscribe ( CHANNEL ) ;
Messages . messageReceived . connect ( receiveMessage ) ;
Users . avatarDisconnected . connect ( avatarDisconnected ) ;
AvatarList . avatarAddedEvent . connect ( avatarAdded ) ;
AvatarList . avatarRemovedEvent . connect ( avatarRemoved ) ;
AvatarList . avatarSessionChangedEvent . connect ( avatarSessionChanged ) ;
}
startup ( ) ;
function off ( ) {
2018-07-16 13:06:12 -07:00
if ( ui . isOpen ) { // i.e., only when connected
2018-07-12 15:03:01 -07:00
if ( updateInterval ) {
Script . clearInterval ( updateInterval ) ;
}
2018-04-10 16:02:58 -07:00
Controller . mousePressEvent . disconnect ( handleMouseEvent ) ;
Controller . mouseMoveEvent . disconnect ( handleMouseMoveEvent ) ;
2018-07-16 13:06:12 -07:00
ui . tablet . tabletShownChanged . disconnect ( tabletVisibilityChanged ) ;
2018-04-10 16:02:58 -07:00
Users . usernameFromIDReply . disconnect ( usernameFromIDReply ) ;
triggerMapping . disable ( ) ;
triggerPressMapping . disable ( ) ;
}
removeOverlays ( ) ;
2018-07-16 15:58:01 -07:00
Users . requestsDomainListData = previousRequestsDomainListData ;
2018-04-10 16:02:58 -07:00
}
2017-02-10 15:06:55 -08:00
function shutdown ( ) {
2017-01-17 09:29:30 -08:00
Window . domainChanged . disconnect ( clearLocalQMLDataAndClosePAL ) ;
Window . domainConnectionRefused . disconnect ( clearLocalQMLDataAndClosePAL ) ;
2017-02-10 15:06:55 -08:00
Messages . subscribe ( CHANNEL ) ;
2017-01-04 20:37:56 -08:00
Messages . messageReceived . disconnect ( receiveMessage ) ;
2017-01-17 09:53:04 -08:00
Users . avatarDisconnected . disconnect ( avatarDisconnected ) ;
2017-03-20 15:19:28 -07:00
AvatarList . avatarAddedEvent . disconnect ( avatarAdded ) ;
AvatarList . avatarRemovedEvent . disconnect ( avatarRemoved ) ;
AvatarList . avatarSessionChangedEvent . disconnect ( avatarSessionChanged ) ;
2016-12-13 15:05:08 -08:00
off ( ) ;
2017-02-10 15:06:55 -08:00
}
Script . scriptEnding . connect ( shutdown ) ;
2016-12-12 16:37:16 -08:00
2017-01-05 12:23:17 -08:00
} ( ) ) ; // END LOCAL_SCOPE