"use strict"; // nearActionGrabEntity.js // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html /* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, getControllerJointIndex, getGrabbableData, NULL_UUID, enableDispatcherModule, disableDispatcherModule, propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, entityIsGrabbable, Quat, Vec3, MSECS_PER_SEC, getControllerWorldLocation, makeDispatcherModuleParameters, makeRunningValues, TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS */ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); Script.include("/~/system/libraries/controllers.js"); (function() { function NearActionGrabEntity(hand) { this.hand = hand; this.targetEntityID = null; this.actionID = null; // action this script created... this.parameters = makeDispatcherModuleParameters( 500, this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100); var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position var ACTION_TTL = 15; // seconds var ACTION_TTL_REFRESH = 5; // XXX does handJointIndex change if the avatar changes? this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); this.controllerJointIndex = getControllerJointIndex(this.hand); // handPosition is where the avatar's hand appears to be, in-world. this.getHandPosition = function () { if (this.hand === RIGHT_HAND) { return MyAvatar.getRightPalmPosition(); } else { return MyAvatar.getLeftPalmPosition(); } }; this.getHandRotation = function () { if (this.hand === RIGHT_HAND) { return MyAvatar.getRightPalmRotation(); } else { return MyAvatar.getLeftPalmRotation(); } }; this.startNearGrabAction = function (controllerData, targetProps) { Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); var grabbableData = getGrabbableData(targetProps); this.ignoreIK = grabbableData.ignoreIK; this.kinematicGrab = grabbableData.kinematicGrab; var handRotation; var handPosition; if (this.ignoreIK) { var controllerID = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; var controllerLocation = getControllerWorldLocation(controllerID, false); handRotation = controllerLocation.orientation; handPosition = controllerLocation.position; } else { handRotation = this.getHandRotation(); handPosition = this.getHandPosition(); } var objectRotation = targetProps.rotation; this.offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); var currentObjectPosition = targetProps.position; var offset = Vec3.subtract(currentObjectPosition, handPosition); this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); var now = Date.now(); this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); if (this.actionID) { Entities.deleteAction(this.targetEntityID, this.actionID); } this.actionID = Entities.addAction("hold", this.targetEntityID, { hand: this.hand === RIGHT_HAND ? "right" : "left", timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, relativePosition: this.offsetPosition, relativeRotation: this.offsetRotation, ttl: ACTION_TTL, kinematic: this.kinematicGrab, kinematicSetVelocity: true, ignoreIK: this.ignoreIK }); if (this.actionID === NULL_UUID) { this.actionID = null; return; } Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'grab', grabbedEntity: this.targetEntityID, joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); }; // this is for when the action is going to time-out this.refreshNearGrabAction = function (controllerData) { var now = Date.now(); if (this.actionID && this.actionTimeout - now < ACTION_TTL_REFRESH * MSECS_PER_SEC) { // if less than a 5 seconds left, refresh the actions ttl var success = Entities.updateAction(this.targetEntityID, this.actionID, { hand: this.hand === RIGHT_HAND ? "right" : "left", timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, relativePosition: this.offsetPosition, relativeRotation: this.offsetRotation, ttl: ACTION_TTL, kinematic: this.kinematicGrab, kinematicSetVelocity: true, ignoreIK: this.ignoreIK }); if (success) { this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); } } }; this.endNearGrabAction = function () { var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args); Entities.deleteAction(this.targetEntityID, this.actionID); this.actionID = null; this.targetEntityID = null; }; this.getTargetProps = function (controllerData) { // nearbyEntityProperties is already sorted by distance from controller var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; for (var i = 0; i < nearbyEntityProperties.length; i++) { var props = nearbyEntityProperties[i]; var handPosition = controllerData.controllerLocations[this.hand].position; var distance = Vec3.distance(props.position, handPosition); if (distance > NEAR_GRAB_RADIUS) { break; } if (entityIsGrabbable(props)) { return props; } } return null; }; this.isReady = function (controllerData) { this.targetEntityID = null; if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) { makeRunningValues(false, [], []); } var targetProps = this.getTargetProps(controllerData); if (targetProps) { if (!propsArePhysical(targetProps)) { // XXX make sure no highlights are enabled from this module return makeRunningValues(false, [], []); // let nearParentGrabEntity handle it } else { this.targetEntityID = targetProps.id; ContextOverlay.entityWithContextOverlay = this.targetEntityID; ContextOverlay.enabled = true; // XXX highlight this.targetEntityID here return makeRunningValues(true, [this.targetEntityID], []); } } else { // XXX make sure no highlights are enabled from this module return makeRunningValues(false, [], []); } }; this.run = function (controllerData) { if (this.actionID) { if (controllerData.triggerClicks[this.hand] == 0) { this.endNearGrabAction(); return makeRunningValues(false, [], []); } this.refreshNearGrabAction(controllerData); var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args); } else { // still searching / highlighting var readiness = this.isReady (controllerData); if (!readiness.active) { return readiness; } var targetProps = this.getTargetProps(controllerData); if (targetProps) { // XXX var rayPickInfo = controllerData.rayPicks[this.hand]; var pointerEvent = { type: "Move", id: this.hand + 1, // 0 is reserved for hardware mouse pos2D: projectOntoEntityXYPlane(rayPickInfo.entityID, rayPickInfo.intersection, targetProps), pos3D: rayPickInfo.intersection, normal: rayPickInfo.normal, direction: rayPickInfo.searchRay.direction, button: "Secondary" }; if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.entityID, pointerEvent)) { } // XXX if (controllerData.triggerClicks[this.hand] == 1) { // stop highlighting, switch to grabbing // XXX stop highlight here this.startNearGrabAction(controllerData, targetProps); } } } return makeRunningValues(true, [this.targetEntityID], []); }; this.cleanup = function () { if (this.targetEntityID) { this.endNearGrabAction(); } }; } var leftNearActionGrabEntity = new NearActionGrabEntity(LEFT_HAND); var rightNearActionGrabEntity = new NearActionGrabEntity(RIGHT_HAND); enableDispatcherModule("LeftNearActionGrabEntity", leftNearActionGrabEntity); enableDispatcherModule("RightNearActionGrabEntity", rightNearActionGrabEntity); this.cleanup = function () { leftNearActionGrabEntity.cleanup(); rightNearActionGrabEntity.cleanup(); disableDispatcherModule("LeftNearActionGrabEntity"); disableDispatcherModule("RightNearActionGrabEntity"); }; Script.scriptEnding.connect(this.cleanup); }());