content/hifi-content/zfox/lookAtApp/lookAt.js
2022-02-14 02:04:11 +01:00

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