344 lines
12 KiB
JavaScript
Raw Normal View History

2016-12-13 09:15:28 -08:00
"use strict";
/*jslint vars: true, plusplus: true, forin: true*/
/*globals Script, AvatarList, Camera, Overlays, OverlayWindow, Toolbars, Vec3, Quat, Controller, print, getControllerWorldLocation */
2016-12-12 16:37:16 -08:00
//
// pal.js
//
// Created by Howard Stearns on December 9, 2016
// 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
//
2016-12-13 13:01:45 -08:00
// FIXME when we make this a defaultScript: (function() { // BEGIN LOCAL_SCOPE
2016-12-12 16:37:16 -08:00
2016-12-13 15:05:08 -08:00
Script.include("/~/system/libraries/controllers.js");
//
// Overlays.
//
2016-12-13 09:15:28 -08:00
var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier.
2016-12-15 11:00:32 -08:00
function ExtendedOverlay(key, type, properties, selected) { // A wrapper around overlays to store the key it is associated with.
2016-12-13 09:15:28 -08:00
overlays[key] = this;
this.key = key;
2016-12-15 11:00:32 -08:00
this.selected = selected || false; // not undefined
2016-12-13 13:01:45 -08:00
this.activeOverlay = Overlays.addOverlay(type, properties); // 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
2016-12-13 13:01:45 -08:00
Overlays.deleteOverlay(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
2016-12-13 13:01:45 -08:00
Overlays.editOverlay(this.activeOverlay, properties);
2016-12-13 09:15:28 -08:00
};
2016-12-15 11:00:32 -08:00
const UNSELECTED_COLOR = {red: 20, green: 250, blue: 20};
const SELECTED_COLOR = {red: 250, green: 20, blue: 20};
function color(selected) { return selected ? SELECTED_COLOR : UNSELECTED_COLOR; }
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
}
this.editOverlay({color: color(selected)});
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);
}
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;
}
}
};
ExtendedOverlay.applyPickRay = function (pickRay, cb) { // cb(overlay) on the one overlay intersected by pickRay, if any.
var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones.
if (!pickedOverlay.intersects) {
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) {
2016-12-13 09:15:28 -08:00
cb(overlay);
return true;
}
});
};
//
// The qml window and communications.
//
2016-12-12 16:37:16 -08:00
var pal = new OverlayWindow({
2016-12-13 09:15:28 -08:00
title: 'People Action List',
2016-12-13 13:01:45 -08:00
source: 'hifi/Pal.qml',
width: 580,
2016-12-13 09:15:28 -08:00
height: 640,
2016-12-12 16:37:16 -08:00
visible: false
});
pal.fromQml.connect(function (message) { // messages are {method, params}, like json-rpc. See also sendToQml.
print('From PAL QML:', JSON.stringify(message));
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
});
break;
2016-12-23 12:22:33 -08:00
case 'refresh':
removeOverlays();
populateUserList();
break;
2016-12-13 15:05:08 -08:00
default:
print('Unrecognized message from Pal.qml:', JSON.stringify(message));
}
2016-12-13 13:01:45 -08:00
});
2016-12-12 16:37:16 -08:00
//
// Main operations.
//
function addAvatarNode(id) {
2016-12-15 11:00:32 -08:00
var selected = ExtendedOverlay.isSelected(id);
return new ExtendedOverlay(id, "sphere", { // 3d so we don't go cross-eyed looking at it, but on top of everything
solid: true,
alpha: 0.8,
2016-12-15 11:00:32 -08:00
color: color(selected),
drawInFront: true
2016-12-15 11:00:32 -08:00
}, selected);
}
2016-12-12 16:37:16 -08:00
function populateUserList() {
2016-12-13 09:15:28 -08:00
var data = [];
2016-12-12 16:37:16 -08:00
var counter = 1;
2016-12-13 16:52:18 -08:00
AvatarList.getAvatarIdentifiers().sort().forEach(function (id) { // sorting the identifiers is just an aid for debugging
2016-12-12 16:37:16 -08:00
var avatar = AvatarList.getAvatar(id);
var avatarPalDatum = {
2016-12-16 14:59:54 -08:00
displayName: avatar.sessionDisplayName,
2016-12-16 14:01:21 -08:00
userName: '',
sessionId: id || '',
2016-12-22 18:09:19 -08:00
audioLevel: 0.0
};
2016-12-16 14:38:39 -08:00
// If the current user is an admin OR
// they're requesting their own username ("id" is blank)...
if (Users.canKick || !id) {
// Request the username from the given UUID
2016-12-15 18:16:45 -08:00
Users.requestUsernameFromID(id);
2016-12-15 16:31:44 -08:00
}
data.push(avatarPalDatum);
if (id) { // No overlay for ourself.
addAvatarNode(id);
2016-12-13 09:15:28 -08:00
}
print('PAL data:', JSON.stringify(avatarPalDatum));
2016-12-12 16:37:16 -08:00
});
2016-12-13 15:05:08 -08:00
pal.sendToQml({method: 'users', params: data});
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
2016-12-19 14:19:39 -08:00
function usernameFromIDReply(id, username, machineFingerprint) {
var data;
2016-12-16 14:38:39 -08:00
// If the ID we've received is our ID...
2016-12-20 17:18:28 -08:00
if (MyAvatar.sessionUUID === id) {
2016-12-16 14:38:39 -08:00
// Set the data to contain specific strings.
2016-12-19 15:31:43 -08:00
data = ['', username]
} else {
// Set the data to contain the ID and the username (if we have one)
// or fingerprint (if we don't have a username) string.
2016-12-19 15:41:28 -08:00
data = [id, username || machineFingerprint];
}
2016-12-15 16:31:44 -08:00
print('Username Data:', JSON.stringify(data));
2016-12-16 14:38:39 -08:00
// Ship the data off to QML
2016-12-15 16:31:44 -08:00
pal.sendToQml({ method: 'updateUsername', params: data });
}
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;
2016-12-13 09:15:28 -08:00
AvatarList.getAvatarIdentifiers().forEach(function (id) {
if (!id) {
return; // don't update ourself
}
2016-12-13 13:01:45 -08:00
var overlay = ExtendedOverlay.get(id);
if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back.
print('Adding non-PAL avatar node', id);
overlay = addAvatarNode(id);
2016-12-13 15:05:08 -08:00
}
var avatar = AvatarList.getAvatar(id);
var target = avatar.position;
var distance = Vec3.distance(target, eye);
overlay.ping = pingPong;
overlay.editOverlay({
position: target,
dimensions: 0.05 * distance // constant apparent size
});
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();
}
});
// We could re-populateUserList if anything added or removed, but not for now.
2016-12-13 09:15:28 -08:00
}
function removeOverlays() {
2016-12-15 11:00:32 -08:00
selectedIds = [];
2016-12-13 09:15:28 -08:00
ExtendedOverlay.some(function (overlay) { overlay.deleteOverlay(); });
}
2016-12-12 16:37:16 -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.
2016-12-13 15:05:08 -08:00
var message = {method: 'select', params: [overlay.key, !overlay.selected]};
pal.sendToQml(message);
return true;
});
}
function handleMouseEvent(mousePressEvent) { // handleClick if we get one.
if (!mousePressEvent.isLeftButton) {
return;
}
handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y));
}
// We get mouseMoveEvents from the handControllers, via handControllerPointer.
// But we don't get mousePressEvents.
2016-12-13 15:05:08 -08:00
var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click');
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) {
return function (clicked) {
2016-12-13 15:05:08 -08:00
if (clicked > 0.85) {
var pickRay = controllerComputePickRay(hand);
handleClick(pickRay);
}
};
}
triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand));
triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand));
//
2016-12-13 09:15:28 -08:00
// Manage the connection between the button and the window.
//
2016-12-12 16:37:16 -08:00
var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system");
2016-12-13 09:15:28 -08:00
var buttonName = "pal";
2016-12-12 16:37:16 -08:00
var button = toolBar.addButton({
objectName: buttonName,
2016-12-13 13:01:45 -08:00
imageURL: Script.resolvePath("assets/images/tools/people.svg"),
2016-12-12 16:37:16 -08:00
visible: true,
hoverState: 2,
defaultState: 1,
buttonState: 1,
alpha: 0.9
});
var isWired = false;
2016-12-13 15:05:08 -08:00
function off() {
if (isWired) { // It is not ok to disconnect these twice, hence guard.
Script.update.disconnect(updateOverlays);
Controller.mousePressEvent.disconnect(handleMouseEvent);
isWired = false;
}
triggerMapping.disable(); // It's ok if we disable twice.
2016-12-13 15:05:08 -08:00
removeOverlays();
}
2016-12-12 16:37:16 -08:00
function onClicked() {
if (!pal.visible) {
populateUserList();
pal.raise();
isWired = true;
2016-12-13 09:15:28 -08:00
Script.update.connect(updateOverlays);
2016-12-13 15:05:08 -08:00
Controller.mousePressEvent.connect(handleMouseEvent);
triggerMapping.enable();
2016-12-15 11:00:32 -08:00
} else {
off();
}
2016-12-12 16:37:16 -08:00
pal.setVisible(!pal.visible);
}
2016-12-13 15:05:08 -08:00
var AVERAGING_RATIO = 0.05
var LOUDNESS_FLOOR = 11.0;
var LOUDNESS_SCALE = 2.8 / 5.0;
var LOG2 = Math.log(2.0);
var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too)
2016-12-23 13:11:42 -08:00
var accumulatedLevels = {};
function getAudioLevel(id) {
// 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 avatar = AvatarList.getAvatar(id);
var audioLevel = 0.0;
// we will do exponential moving average by taking some the last loudness and averaging
2016-12-23 13:11:42 -08:00
accumulatedLevels[id] = AVERAGING_RATIO * (accumulatedLevels[id] || 0 ) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness);
// 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).
2016-12-23 13:11:42 -08:00
var logLevel = Math.log(accumulatedLevels[id] + 1) / LOG2;
if (logLevel <= LOUDNESS_FLOOR) {
audioLevel = logLevel / LOUDNESS_FLOOR * LOUDNESS_SCALE;
} else {
audioLevel = (logLevel - (LOUDNESS_FLOOR - 1.0)) * LOUDNESS_SCALE;
}
if (audioLevel > 1.0) {
audioLevel = 1;
}
return audioLevel;
}
// we will update the audioLevels periodically
// TODO: tune for efficiency - expecially with large numbers of avatars
Script.setInterval(function () {
if (pal.visible) {
2016-12-22 18:09:19 -08:00
var param = {};
AvatarList.getAvatarIdentifiers().sort().forEach(function (id) {
var level = getAudioLevel(id);
2016-12-22 18:09:19 -08:00
// qml didn't like an object with null/empty string for a key, so...
var userId = id || 0;
param[userId]= level;
});
2016-12-22 18:09:19 -08:00
pal.sendToQml({method: 'updateAudioLevel', params: param});
}
}, AUDIO_LEVEL_UPDATE_INTERVAL_MS);
//
// Button state.
//
2016-12-23 12:22:33 -08:00
function onVisibleChanged() {
2016-12-12 16:37:16 -08:00
button.writeProperty('buttonState', pal.visible ? 0 : 1);
button.writeProperty('defaultState', pal.visible ? 0 : 1);
button.writeProperty('hoverState', pal.visible ? 2 : 3);
}
button.clicked.connect(onClicked);
2016-12-23 12:22:33 -08:00
pal.visibleChanged.connect(onVisibleChanged);
2016-12-15 11:00:32 -08:00
pal.closed.connect(off);
2016-12-16 14:38:39 -08:00
Users.usernameFromIDReply.connect(usernameFromIDReply);
2016-12-12 16:37:16 -08:00
//
// Cleanup.
//
2016-12-12 16:37:16 -08:00
Script.scriptEnding.connect(function () {
2016-12-13 15:05:08 -08:00
button.clicked.disconnect(onClicked);
2016-12-12 16:37:16 -08:00
toolBar.removeButton(buttonName);
2016-12-23 12:22:33 -08:00
pal.visibleChanged.disconnect(onVisibleChanged);
2016-12-15 11:00:32 -08:00
pal.closed.disconnect(off);
2016-12-16 14:38:39 -08:00
Users.usernameFromIDReply.disconnect(usernameFromIDReply);
2016-12-13 15:05:08 -08:00
off();
2016-12-12 16:37:16 -08:00
});
// FIXME: }()); // END LOCAL_SCOPE