From 0d0f147056b734804a246af7506e23009b0c712e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 9 Aug 2017 17:48:35 -0700 Subject: [PATCH] port action-near-grab to dispatcher --- scripts/defaultScripts.js | 4 +- .../controllers/controllerDispatcher.js | 14 +- .../controllers/controllerDispatcherUtils.js | 54 ++++- .../controllerModules/nearActionGrabEntity.js | 203 ++++++++++++++++++ .../nearParentGrabEntity.js} | 102 ++++----- .../system/controllers/controllerScripts.js | 3 +- 6 files changed, 321 insertions(+), 59 deletions(-) create mode 100644 scripts/system/controllers/controllerModules/nearActionGrabEntity.js rename scripts/system/controllers/{nearGrab.js => controllerModules/nearParentGrabEntity.js} (59%) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index f4c7b42ee2..63f37f3199 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -20,11 +20,11 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/bubble.js", "system/snapshot.js", "system/help.js", - // "system/pal.js", // "system/mod.js", // older UX, if you prefer + "system/pal.js", // "system/mod.js", // older UX, if you prefer "system/makeUserConnection.js", "system/tablet-goto.js", "system/marketplaces/marketplaces.js", - // "system/edit.js", + "system/edit.js", "system/notifications.js", "system/dialTone.js", "system/firstPersonHMD.js", diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 820bfe942c..1560ac5b44 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -68,9 +68,9 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); getControllerWorldLocation(Controller.Standard.RightHand, true)]; var nearbyEntityProperties = [[], []]; - for (var i = LEFT_HAND; i <= RIGHT_HAND; i ++) { - // todo: check controllerLocations[i].valid - var controllerPosition = controllerLocations[i].position; + for (var h = LEFT_HAND; h <= RIGHT_HAND; h ++) { + // todo: check controllerLocations[h].valid + var controllerPosition = controllerLocations[h].position; var nearbyEntityIDs = Entities.findEntities(controllerPosition, NEAR_MIN_RADIUS); for (var j = 0; j < nearbyEntityIDs.length; j++) { var entityID = nearbyEntityIDs[j]; @@ -78,9 +78,15 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); props.id = entityID; props.distanceFromController = Vec3.length(Vec3.subtract(controllerPosition, props.position)); if (props.distanceFromController < NEAR_MAX_RADIUS) { - nearbyEntityProperties[i].push(props); + nearbyEntityProperties[h].push(props); } } + // sort by distance from each hand + nearbyEntityProperties[h].sort(function (a, b) { + var aDistance = Vec3.distance(a.position, controllerLocations[h]); + var bDistance = Vec3.distance(b.position, controllerLocations[h]); + return aDistance - bDistance; + }); } var controllerData = { diff --git a/scripts/system/controllers/controllerDispatcherUtils.js b/scripts/system/controllers/controllerDispatcherUtils.js index d1c8143f98..8717be34d5 100644 --- a/scripts/system/controllers/controllerDispatcherUtils.js +++ b/scripts/system/controllers/controllerDispatcherUtils.js @@ -6,8 +6,18 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global Camera, HMD, MyAvatar, getControllerJointIndex, - LEFT_HAND, RIGHT_HAND, NULL_UUID, AVATAR_SELF_ID, getGrabbableData */ +/* global Camera, HMD, MyAvatar, controllerDispatcherPlugins, + MSECS_PER_SEC, LEFT_HAND, RIGHT_HAND, NULL_UUID, AVATAR_SELF_ID, FORBIDDEN_GRAB_TYPES, + HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, + enableDispatcherModule, + disableDispatcherModule, + getGrabbableData, + entityIsGrabbable, + getControllerJointIndex, + propsArePhysical +*/ + +MSECS_PER_SEC = 1000.0; LEFT_HAND = 0; RIGHT_HAND = 1; @@ -15,6 +25,23 @@ RIGHT_HAND = 1; NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; +FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"]; + +HAPTIC_PULSE_STRENGTH = 1.0; +HAPTIC_PULSE_DURATION = 13.0; + + +enableDispatcherModule = function (moduleName, module, priority) { + if (!controllerDispatcherPlugins) { + controllerDispatcherPlugins = {}; + } + controllerDispatcherPlugins[moduleName] = module; +}; + +disableDispatcherModule = function (moduleName) { + delete controllerDispatcherPlugins[moduleName]; +}; + getGrabbableData = function (props) { // look in userData for a "grabbable" key, return the value or some defaults var grabbableData = {}; @@ -29,10 +56,25 @@ getGrabbableData = function (props) { if (!grabbableData.hasOwnProperty("grabbable")) { grabbableData.grabbable = true; } + if (!grabbableData.hasOwnProperty("ignoreIK")) { + grabbableData.ignoreIK = true; + } + if (!grabbableData.hasOwnProperty("kinematicGrab")) { + grabbableData.kinematicGrab = false; + } return grabbableData; }; +entityIsGrabbable = function (props) { + var grabbable = getGrabbableData(props).grabbable; + if (!grabbable || + props.locked || + FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0) { + return false; + } + return true; +}; getControllerJointIndex = function (hand) { if (HMD.isHandControllerAvailable()) { @@ -52,3 +94,11 @@ getControllerJointIndex = function (hand) { return MyAvatar.getJointIndex("Head"); }; + +propsArePhysical = function (props) { + if (!props.dynamic) { + return false; + } + var isPhysical = (props.shapeType && props.shapeType != 'none'); + return isPhysical; +}; diff --git a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js new file mode 100644 index 0000000000..57df123c1d --- /dev/null +++ b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js @@ -0,0 +1,203 @@ +"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 +*/ + +Script.include("/~/system/controllers/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + + function NearActionGrabEntity(hand) { + this.hand = hand; + this.grabbedThingID = null; + this.actionID = null; // action this script created... + + 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, grabbedProperties) { + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + + var grabbableData = getGrabbableData(grabbedProperties); + 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 = grabbedProperties.rotation; + this.offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); + + var currentObjectPosition = grabbedProperties.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.grabbedThingID, this.actionID); + } + this.actionID = Entities.addAction("hold", this.grabbedThingID, { + 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.grabbedThingID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + }; + + // this is for when the action creation failed, before + this.restartNearGrabAction = function (controllerData) { + var props = Entities.getEntityProperties(this.grabbedThingID, ["position", "rotation", "userData"]); + if (props && entityIsGrabbable(props)) { + this.startNearGrabAction(controllerData, props); + } + }; + + // 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.grabbedThingID, 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); + } else { + print("continueNearGrabbing -- updateAction failed"); + this.restartNearGrabAction(controllerData); + } + } + }; + + this.endNearGrabAction = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.grabbedThingID, "releaseGrab", args); + + Entities.deleteAction(this.grabbedThingID, this.actionID); + this.actionID = null; + + this.grabbedThingID = null; + }; + + this.isReady = function (controllerData) { + if (controllerData.triggerClicks[this.hand] == 0) { + return false; + } + + var grabbedProperties = null; + // nearbyEntityProperties is already sorted by length from controller + var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; + for (var i = 0; i < nearbyEntityProperties.length; i++) { + var props = nearbyEntityProperties[i]; + if (entityIsGrabbable(props)) { + grabbedProperties = props; + break; + } + } + + if (grabbedProperties) { + if (!propsArePhysical(grabbedProperties)) { + return false; // let nearParentGrabEntity handle it + } + this.grabbedThingID = grabbedProperties.id; + this.startNearGrabAction(controllerData, grabbedProperties); + return true; + } else { + return false; + } + }; + + this.run = function (controllerData) { + if (controllerData.triggerClicks[this.hand] == 0) { + this.endNearGrabAction(controllerData); + return false; + } + + if (!this.actionID) { + this.restartNearGrabAction(controllerData); + } + + this.refreshNearGrabAction(controllerData); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.grabbedThingID, "continueNearGrab", args); + + return true; + }; + } + + enableDispatcherModule("LeftNearActionGrabEntity", new NearActionGrabEntity(LEFT_HAND), 500); + enableDispatcherModule("RightNearActionGrabEntity", new NearActionGrabEntity(RIGHT_HAND), 500); + + this.cleanup = function () { + disableDispatcherModule("LeftNearActionGrabEntity"); + disableDispatcherModule("RightNearActionGrabEntity"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/nearGrab.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js similarity index 59% rename from scripts/system/controllers/nearGrab.js rename to scripts/system/controllers/controllerModules/nearParentGrabEntity.js index bf02858ada..70e8bb363a 100644 --- a/scripts/system/controllers/nearGrab.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -1,38 +1,24 @@ "use strict"; -// nearGrab.js +// nearParentGrabEntity.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, controllerDispatcherPlugins, - RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID, getControllerJointIndex, getGrabbableData, NULL_UUID */ +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID, + getControllerJointIndex, getGrabbableData, NULL_UUID, enableDispatcherModule, disableDispatcherModule, + FORBIDDEN_GRAB_TYPES, propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION +*/ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); (function() { - var HAPTIC_PULSE_STRENGTH = 1.0; - var HAPTIC_PULSE_DURATION = 13.0; - - var FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"]; - var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; - - function entityIsParentingGrabbable(props) { - var grabbable = getGrabbableData(props).grabbable; - if (!grabbable || - props.locked || - FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0 || - FORBIDDEN_GRAB_NAMES.indexOf(props.name) >= 0) { - return false; - } - return true; - } - - function NearGrabParenting(hand) { - this.priority = 5; + // XXX this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true; + // XXX this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC; + function NearParentingGrabEntity(hand) { this.hand = hand; this.grabbedThingID = null; this.previousParentID = {}; @@ -40,8 +26,9 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); this.previouslyUnhooked = {}; - // todo: does this change if the avatar changes? + // XXX does handJointIndex change if the avatar changes? this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + this.controllerJointIndex = getControllerJointIndex(this.hand); this.thisHandIsParent = function(props) { if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== AVATAR_SELF_ID) { @@ -68,12 +55,23 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); return false; }; - this.startNearGrabParenting = function (controllerData, grabbedProperties) { + this.startNearParentingGrabEntity = function (controllerData, grabbedProperties) { Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + var handJointIndex; + // if (this.ignoreIK) { + // handJointIndex = this.controllerJointIndex; + // } else { + // handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + // } + handJointIndex = this.controllerJointIndex; + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.grabbedThingID, "startNearGrab", args); + var reparentProps = { parentID: AVATAR_SELF_ID, - parentJointIndex: getControllerJointIndex(this.hand), + parentJointIndex: handJointIndex, velocity: {x: 0, y: 0, z: 0}, angularVelocity: {x: 0, y: 0, z: 0} }; @@ -87,9 +85,15 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex; } Entities.editEntity(this.grabbedThingID, reparentProps); + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'grab', + grabbedEntity: this.grabbedThingID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); }; - this.endNearGrabParenting = function (controllerData) { + this.endNearParentingGrabEntity = function (controllerData) { if (this.previousParentID[this.grabbedThingID] === NULL_UUID) { Entities.editEntity(this.grabbedThingID, { parentID: this.previousParentID[this.grabbedThingID], @@ -104,6 +108,10 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); angularVelocity: {x: 0, y: 0, z: 0} }); } + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.grabbedThingID, "releaseGrab", args); + this.grabbedThingID = null; }; @@ -113,22 +121,22 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); } var grabbedProperties = null; - var bestDistance = 1000; + // nearbyEntityProperties is already sorted by length from controller var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; - - for (var i = 0; i < nearbyEntityProperties.length; i ++) { + for (var i = 0; i < nearbyEntityProperties.length; i++) { var props = nearbyEntityProperties[i]; - if (entityIsParentingGrabbable(props)) { - if (props.distanceFromController < bestDistance) { - bestDistance = props.distanceFromController; - grabbedProperties = props; - } + if (entityIsGrabbable(props)) { + grabbedProperties = props; + break; } } if (grabbedProperties) { + if (propsArePhysical(grabbedProperties)) { + return false; // let nearActionGrabEntity handle it + } this.grabbedThingID = grabbedProperties.id; - this.startNearGrabParenting(controllerData, grabbedProperties); + this.startNearParentingGrabEntity(controllerData, grabbedProperties); return true; } else { return false; @@ -137,29 +145,23 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); this.run = function (controllerData) { if (controllerData.triggerClicks[this.hand] == 0) { - this.endNearGrabParenting(controllerData); + this.endNearParentingGrabEntity(controllerData); return false; } + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.grabbedThingID, "continueNearGrab", args); + return true; }; } - var leftNearGrabParenting = new NearGrabParenting(LEFT_HAND); - leftNearGrabParenting.name = "leftNearGrabParenting"; - - var rightNearGrabParenting = new NearGrabParenting(RIGHT_HAND); - rightNearGrabParenting.name = "rightNearGrabParenting"; - - if (!controllerDispatcherPlugins) { - controllerDispatcherPlugins = {}; - } - controllerDispatcherPlugins.leftNearGrabParenting = leftNearGrabParenting; - controllerDispatcherPlugins.rightNearGrabParenting = rightNearGrabParenting; - + enableDispatcherModule("LeftNearParentingGrabEntity", new NearParentingGrabEntity(LEFT_HAND), 500); + enableDispatcherModule("RightNearParentingGrabEntity", new NearParentingGrabEntity(RIGHT_HAND), 500); this.cleanup = function () { - delete controllerDispatcherPlugins.leftNearGrabParenting; - delete controllerDispatcherPlugins.rightNearGrabParenting; + disableDispatcherModule("LeftNearParentingGrabEntity"); + disableDispatcherModule("RightNearParentingGrabEntity"); }; Script.scriptEnding.connect(this.cleanup); }()); diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index 5adfd93745..efc187dd86 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -19,7 +19,8 @@ var CONTOLLER_SCRIPTS = [ "toggleAdvancedMovementForHandControllers.js", "ControllerDispatcher.js", - "nearGrab.js" + "controllerModules/nearParentGrabEntity.js", + "controllerModules/nearActionGrabEntity.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js";