488 lines
17 KiB
JavaScript
488 lines
17 KiB
JavaScript
"use strict";
|
|
/*jslint vars:true, plusplus:true, forin:true*/
|
|
/*global Tablet, Script, */
|
|
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
|
|
//
|
|
// lookAt.js
|
|
//
|
|
// Created by Zach Fox on 2018-07-30
|
|
// Copyright 2018 High Fidelity, Inc
|
|
//
|
|
// Distributed under the Apache License, Version 2.0
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
(function () { // BEGIN LOCAL_SCOPE
|
|
var AppUi = Script.require('appUi');
|
|
|
|
/********************************
|
|
// START Debug Functions
|
|
********************************/
|
|
var DEBUG_UNIMPORTANT = 0;
|
|
var DEBUG_IMPORTANT = 1;
|
|
var DEBUG_URGENT = 2;
|
|
|
|
var DEBUG_ONLY_PRINT_URGENT = 0;
|
|
var DEBUG_PRINT_URGENT_AND_IMPORTANT = 1;
|
|
var DEBUG_PRINT_EVERYTHING = 2;
|
|
var debugLevel = DEBUG_PRINT_URGENT_AND_IMPORTANT;
|
|
|
|
function maybePrint(string, importance) {
|
|
if (importance >= (DEBUG_URGENT - debugLevel)) {
|
|
console.log(string);
|
|
}
|
|
}
|
|
/********************************
|
|
// END Debug Functions
|
|
********************************/
|
|
|
|
/********************************
|
|
// START Shared Math Utility Functions
|
|
********************************/
|
|
// Function Name: inFrontOf()
|
|
//
|
|
// Description:
|
|
// -Returns the position in front of the given "position" argument, where the forward vector is based off
|
|
// the "orientation" argument and the amount in front is based off the "distance" argument.
|
|
function inFrontOf(distance, position, orientation) {
|
|
return Vec3.sum(position,
|
|
Vec3.multiply(distance, Quat.getForward(orientation)));
|
|
}
|
|
/********************************
|
|
// END Shared Math Utility Functions
|
|
********************************/
|
|
|
|
/********************************
|
|
// START Hover Handler Functions
|
|
********************************/
|
|
var hoverHandlersConnected = false;
|
|
|
|
var isHoveringOverLookAtOverlay = false;
|
|
function handleHoverEnterOverlay(overlayID, event) {
|
|
isHoveringOverLookAtOverlay = (lookAtOverlay && overlayID === lookAtOverlay);
|
|
}
|
|
|
|
function handleHoverLeaveOverlay(overlayID, event) {
|
|
var wasHoveringOverLookAtOverlay = isHoveringOverLookAtOverlay;
|
|
justStoppedHoveringOverLookAtOverlay = wasHoveringOverLookAtOverlay && (!lookAtOverlay || overlayID === lookAtOverlay);
|
|
if (!overlayIntersection.intersects &&
|
|
ID_currentLookAt !== ID_actionTakenOn &&
|
|
justStoppedHoveringOverLookAtOverlay &&
|
|
!reverseActionTimer) {
|
|
startReverseActionTimer();
|
|
}
|
|
isHoveringOverLookAtOverlay = false;
|
|
}
|
|
/********************************
|
|
// END Hover Handler Functions
|
|
********************************/
|
|
|
|
/********************************
|
|
// START Click Handler Functions
|
|
********************************/
|
|
function takeMuteButtonAction() {
|
|
if (actionTakenOnAvatar) {
|
|
Users.personalMute(ID_currentLookAt);
|
|
} else {
|
|
Entities.editEntity(ID_currentLookAt, {
|
|
color: {
|
|
red: 30,
|
|
green: 30,
|
|
blue: 30
|
|
}
|
|
});
|
|
}
|
|
reverseAction(true);
|
|
}
|
|
|
|
function takeIgnoreButtonAction() {
|
|
if (actionTakenOnAvatar) {
|
|
Users.ignore(ID_currentLookAt);
|
|
} else {
|
|
Entities.editEntity(ID_currentLookAt, {
|
|
color: {
|
|
red: 255,
|
|
green: 0,
|
|
blue: 0
|
|
}
|
|
});
|
|
}
|
|
reverseAction(true);
|
|
}
|
|
/********************************
|
|
// END Click Handler Functions
|
|
********************************/
|
|
|
|
/********************************
|
|
// START Global Detection Start/Stop/Update functions
|
|
********************************/
|
|
function maybeDeleteOverlays() {
|
|
if (lookAtOverlayObject) {
|
|
lookAtOverlayObject.webEventReceived.disconnect(lookAtOverlayWebEventReceived);
|
|
lookAtOverlayObject = false;
|
|
}
|
|
|
|
if (lookAtOverlay) {
|
|
Overlays.deleteOverlay(lookAtOverlay);
|
|
lookAtOverlay = false;
|
|
}
|
|
|
|
if (updateLookAtOverlayConnected) {
|
|
Script.update.disconnect(updateLookAtOverlay);
|
|
updateLookAtOverlayConnected = false;
|
|
}
|
|
|
|
if (hoverHandlersConnected) {
|
|
Overlays.hoverEnterOverlay.disconnect(handleHoverEnterOverlay);
|
|
Overlays.hoverLeaveOverlay.disconnect(handleHoverLeaveOverlay);
|
|
hoverHandlersConnected = false;
|
|
}
|
|
}
|
|
|
|
function lookAtOverlayWebEventReceived(event) {
|
|
event = JSON.parse(event);
|
|
switch (event.method) {
|
|
case 'lookAt-Overlay-Ready':
|
|
lookAtOverlayObject.emitScriptEvent(JSON.stringify({
|
|
method: 'lookAt-Overlay-initializeUI',
|
|
entityName: (actionTakenOnAvatar ? AvatarList.getAvatar(ID_actionTakenOn).sessionDisplayName : ID_actionTakenOn)
|
|
}));
|
|
break;
|
|
case 'lookAt-Overlay-Mute':
|
|
takeMuteButtonAction();
|
|
break;
|
|
case 'lookAt-Overlay-Ignore':
|
|
takeIgnoreButtonAction();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function updateLookAtOverlay() {
|
|
var editProps = {
|
|
rotation: Camera.orientation
|
|
}
|
|
|
|
if (actionTakenOnAvatar && !isHoveringOverLookAtOverlay && !overlayIntersection.intersects) {
|
|
editProps.position = AvatarList.getAvatar(ID_actionTakenOn).getJointPosition("Head");
|
|
editProps.position.y += 0.66;
|
|
}
|
|
|
|
Overlays.editOverlay(lookAtOverlay, editProps);
|
|
}
|
|
|
|
var ID_actionTakenOn = false;
|
|
var actionTakenOnAvatar = false;
|
|
var lookAtOverlay = false;
|
|
var updateLookAtOverlayConnected = false;
|
|
var lookAtOverlayObject = false;
|
|
var OVERLAY_DIMENSIONS = { x: 1.0, y: 0.4, z: 0.1 };
|
|
var OVERLAY_Y_OFFSET_M = 0.1;
|
|
function takeLookAtAction(isAvatarAction) {
|
|
actionTakenOnAvatar = isAvatarAction;
|
|
|
|
var timestamp = new Date();
|
|
sendToQml({
|
|
method: 'tookLookAtAction',
|
|
timestamp: timestamp.getHours() + ":" + timestamp.getMinutes() + ":" + timestamp.getSeconds()
|
|
});
|
|
ID_actionTakenOn = ID_currentLookAt;
|
|
|
|
var overlayPosition;
|
|
if (isAvatarAction) {
|
|
overlayPosition = AvatarList.getAvatar(ID_currentLookAt).getJointPosition("Head");
|
|
overlayPosition.y += 0.66;
|
|
} else {
|
|
Entities.editEntity(ID_currentLookAt, {
|
|
color: {
|
|
red: Math.random() * 255,
|
|
green: Math.random() * 255,
|
|
blue: Math.random() * 255
|
|
}
|
|
});
|
|
|
|
var lookAtEntityProps = Entities.getEntityProperties(ID_actionTakenOn, ["position", "dimensions"]);
|
|
overlayPosition = lookAtEntityProps.position;
|
|
overlayPosition.y += lookAtEntityProps.dimensions.y / 2 + OVERLAY_DIMENSIONS.y / 2 + OVERLAY_Y_OFFSET_M;
|
|
}
|
|
|
|
maybeDeleteOverlays();
|
|
|
|
var overlayOrientation = Camera.orientation;
|
|
|
|
var lookAtOverlayProps = {
|
|
name: "LookAt Overlay",
|
|
url: "https://hifi-content.s3.amazonaws.com/zfox/lookAtApp/lookAtOverlay.html",
|
|
position: overlayPosition,
|
|
rotation: overlayOrientation,
|
|
dimensions: OVERLAY_DIMENSIONS,
|
|
dpi: 16,
|
|
color: { red: 255, green: 255, blue: 255 },
|
|
alpha: 1.0,
|
|
showKeyboardFocusHighlight: false,
|
|
visible: true,
|
|
}
|
|
lookAtOverlay = Overlays.addOverlay("web3d", lookAtOverlayProps);
|
|
|
|
if (!updateLookAtOverlayConnected) {
|
|
Script.update.connect(updateLookAtOverlay);
|
|
updateLookAtOverlayConnected = true;
|
|
}
|
|
|
|
if (!hoverHandlersConnected) {
|
|
Overlays.hoverEnterOverlay.connect(handleHoverEnterOverlay);
|
|
Overlays.hoverLeaveOverlay.connect(handleHoverLeaveOverlay);
|
|
hoverHandlersConnected = true;
|
|
}
|
|
|
|
lookAtOverlayObject = Overlays.getOverlayObject(lookAtOverlay);
|
|
lookAtOverlayObject.webEventReceived.connect(lookAtOverlayWebEventReceived);
|
|
}
|
|
|
|
function reverseAction(force) {
|
|
reverseActionTimer = false;
|
|
if (force || ((!overlayIntersection || !overlayIntersection.intersects) &&
|
|
ID_actionTakenOn &&
|
|
ID_currentLookAt !== ID_actionTakenOn &&
|
|
!isHoveringOverLookAtOverlay)) {
|
|
maybeDeleteOverlays();
|
|
|
|
var timestamp = new Date();
|
|
sendToQml({
|
|
method: 'reversedLookAtAction',
|
|
timestamp: timestamp.getHours() + ":" + timestamp.getMinutes() + ":" + timestamp.getSeconds()
|
|
});
|
|
ID_actionTakenOn = false;
|
|
isHoveringOverLookAtOverlay = false;
|
|
}
|
|
}
|
|
|
|
var reverseActionTimer = false;
|
|
var REVERSE_ACTION_TIMEOUT_MS = 750;
|
|
function startReverseActionTimer() {
|
|
var timestamp = Date.now();
|
|
sendToQml({
|
|
method: 'reverseActionTimerStarted',
|
|
timestamp: timestamp
|
|
});
|
|
|
|
if (reverseActionTimer) {
|
|
Script.clearTimeout(reverseActionTimer);
|
|
reverseActionTimer = false;
|
|
}
|
|
|
|
reverseActionTimer = Script.setTimeout(function () {
|
|
reverseAction();
|
|
}, REVERSE_ACTION_TIMEOUT_MS);
|
|
}
|
|
|
|
var overlayID_currentLookAt = false;
|
|
var ID_currentLookAt = false;
|
|
var overlayIntersection = false;
|
|
var lookAtTimeout = false;
|
|
var PICK_RAY_MAX_DISTANCE = 5;
|
|
var LOOK_AT_TIMEOUT_MS = 1000;
|
|
function handlePickRay(isAvatarPickRay) {
|
|
var pickRay = {
|
|
origin: MyAvatar.position,
|
|
direction: Quat.getFront(Camera.orientation),
|
|
length: PICK_RAY_MAX_DISTANCE
|
|
}
|
|
|
|
var entityIntersection = false;
|
|
var avatarIntersection = false;
|
|
if (isAvatarPickRay) {
|
|
avatarIntersection = AvatarList.findRayIntersection(pickRay, [], [MyAvatar.sessionUUID]);
|
|
} else {
|
|
entityIntersection = Entities.findRayIntersection(pickRay, true);
|
|
}
|
|
|
|
var lookAtIDChanged = false;
|
|
|
|
if ((entityIntersection && entityIntersection.intersects) ||
|
|
(avatarIntersection && avatarIntersection.intersects)) {
|
|
var intersectID = entityIntersection.entityID || avatarIntersection.avatarID;
|
|
|
|
if (ID_currentLookAt !== intersectID) {
|
|
lookAtIDChanged = true;
|
|
if (!isHoveringOverLookAtOverlay && ID_currentLookAt) {
|
|
startReverseActionTimer();
|
|
}
|
|
ID_currentLookAt = intersectID;
|
|
|
|
if (lookAtTimeout) {
|
|
Script.clearTimeout(lookAtTimeout);
|
|
lookAtTimeout = false;
|
|
}
|
|
|
|
lookAtTimeout = Script.setTimeout(function () {
|
|
lookAtTimeout = false;
|
|
if (ID_actionTakenOn !== ID_currentLookAt) {
|
|
takeLookAtAction(avatarIntersection);
|
|
}
|
|
}, LOOK_AT_TIMEOUT_MS);
|
|
}
|
|
} else {
|
|
if (ID_currentLookAt) {
|
|
lookAtIDChanged = true;
|
|
startReverseActionTimer();
|
|
}
|
|
ID_currentLookAt = false;
|
|
|
|
if (lookAtTimeout) {
|
|
Script.clearTimeout(lookAtTimeout);
|
|
lookAtTimeout = false;
|
|
}
|
|
}
|
|
|
|
overlayIntersection = false;
|
|
if (lookAtOverlay) {
|
|
pickRay.origin = Camera.position;
|
|
overlayIntersection = Overlays.findRayIntersection(pickRay, true, [lookAtOverlay]);
|
|
|
|
if (!overlayIntersection.intersects &&
|
|
(ID_currentLookAt !== ID_actionTakenOn || !ID_currentLookAt) &&
|
|
!isHoveringOverLookAtOverlay &&
|
|
!reverseActionTimer) {
|
|
startReverseActionTimer();
|
|
}
|
|
}
|
|
|
|
if (lookAtIDChanged) {
|
|
sendToQml({
|
|
method: 'lookAtIDChanged',
|
|
id: ID_currentLookAt || "",
|
|
});
|
|
}
|
|
}
|
|
|
|
function lookAtDetectionUpdateLoop() {
|
|
handlePickRay(true);
|
|
}
|
|
|
|
var isDetectingLookAt = false;
|
|
function startDetectionLoop() {
|
|
if (isDetectingLookAt) {
|
|
Script.update.disconnect(lookAtDetectionUpdateLoop);
|
|
isDetectingLookAt = false;
|
|
}
|
|
|
|
isDetectingLookAt = true;
|
|
Script.update.connect(lookAtDetectionUpdateLoop);
|
|
}
|
|
|
|
function stopDetectionLoop() {
|
|
if (isDetectingLookAt) {
|
|
Script.update.disconnect(lookAtDetectionUpdateLoop);
|
|
isDetectingLookAt = false;
|
|
}
|
|
}
|
|
|
|
var lookAtDetectionStatus = false
|
|
function enableOrDisableLookAtDetection() {
|
|
if (lookAtDetectionStatus) {
|
|
startDetectionLoop();
|
|
} else {
|
|
stopDetectionLoop();
|
|
}
|
|
}
|
|
|
|
/********************************
|
|
// END Global Detection Start/Stop/Update functions
|
|
********************************/
|
|
|
|
/********************************
|
|
// START App-Related Functions
|
|
********************************/
|
|
|
|
|
|
// Function Name: sendToQml()
|
|
//
|
|
// Description:
|
|
// -Use this function to send a message to the app's QML (i.e. to change appearances). The "message" argument is what is sent to
|
|
// the app's QML in the format "{method, params}", like json-rpc. See also fromQml().
|
|
function sendToQml(message) {
|
|
ui.sendMessage(message);
|
|
}
|
|
|
|
// Function Name: fromQml()
|
|
//
|
|
// Description:
|
|
// -Called when a message is received from the app QML. The "message" argument is what is sent from the app QML
|
|
// in the format "{method, params}", like json-rpc. See also sendToQml().
|
|
function fromQml(message) {
|
|
switch (message.method) {
|
|
case 'masterSwitchChanged':
|
|
lookAtDetectionStatus = message.status;
|
|
Settings.setValue('lookAt/enableDetection', lookAtDetectionStatus);
|
|
enableOrDisableLookAtDetection();
|
|
break;
|
|
default:
|
|
maybePrint('Unrecognized message from LookAt.qml: ' + JSON.stringify(message), DEBUG_URGENT);
|
|
}
|
|
}
|
|
|
|
// Function Name: appUiOpened()
|
|
//
|
|
// Description:
|
|
// - Called when the app's UI is opened
|
|
//
|
|
var APP_INITIALIZE_UI_DELAY = 500; // MS
|
|
function appUiOpened() {
|
|
// In the case of a remote QML app, it takes a bit of time
|
|
// for the event bridge to actually connect, so we have to wait...
|
|
Script.setTimeout(function () {
|
|
sendToQml({
|
|
method: 'initializeUI',
|
|
masterSwitchOn: !!lookAtDetectionStatus,
|
|
timeout: LOOK_AT_TIMEOUT_MS
|
|
});
|
|
}, APP_INITIALIZE_UI_DELAY);
|
|
}
|
|
|
|
// Function Name: appUiClosed()
|
|
//
|
|
// Description:
|
|
// - Called when the app's UI is closed
|
|
//
|
|
function appUiClosed() {
|
|
}
|
|
|
|
// Function Name: startup()
|
|
//
|
|
// Description:
|
|
// -startup() will be called when the script is loaded.
|
|
//
|
|
var ui;
|
|
function startup() {
|
|
ui = new AppUi({
|
|
buttonName: "LOOKAT",
|
|
home: Script.resolvePath('./LookAt.qml'),
|
|
onOpened: appUiOpened,
|
|
onClosed: appUiClosed,
|
|
onMessage: fromQml,
|
|
sortOrder: 15,
|
|
normalButton: Script.resourcesPath() + "icons/tablet-icons/avatar-record-i.svg",
|
|
activeButton: Script.resourcesPath() + "icons/tablet-icons/avatar-record-a.svg"
|
|
});
|
|
lookAtDetectionStatus = Settings.getValue('lookAt/enableDetection', false);
|
|
enableOrDisableLookAtDetection();
|
|
}
|
|
|
|
// Function Name: shutdown()
|
|
//
|
|
// Description:
|
|
// - Called when the script ends (i.e. is stopped).
|
|
//
|
|
function shutdown() {
|
|
appUiClosed();
|
|
maybeDeleteOverlays();
|
|
}
|
|
|
|
var SOUND_LOOKAT_DETECTED = SoundCache.getSound(Script.resolvePath("lookAtDetected.wav"));
|
|
startup();
|
|
Script.scriptEnding.connect(shutdown);
|
|
/********************************
|
|
// END App-Related Functions
|
|
********************************/
|
|
|
|
}()); // END LOCAL_SCOPE
|