"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