diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 3398d535f4..b35891e71e 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -90,23 +90,28 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); this.leftTriggerClicked = 0; this.rightTriggerValue = 0; this.rightTriggerClicked = 0; - + this.leftSecondaryValue = 0; + this.rightSecondaryValue = 0; this.leftTriggerPress = function (value) { _this.leftTriggerValue = value; }; - this.leftTriggerClick = function (value) { _this.leftTriggerClicked = value; }; - this.rightTriggerPress = function (value) { _this.rightTriggerValue = value; }; - this.rightTriggerClick = function (value) { _this.rightTriggerClicked = value; }; + this.leftSecondaryPress = function (value) { + _this.leftSecondaryValue = value; + }; + this.rightSecondaryPress = function (value) { + _this.rightSecondaryValue = value; + }; + this.dataGatherers = {}; this.dataGatherers.leftControllerLocation = function () { @@ -150,10 +155,21 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); } } this.orderedPluginNames.sort(function (a, b) { - return controllerDispatcherPlugins[a].priority < controllerDispatcherPlugins[b].priority; + return controllerDispatcherPlugins[a].parameters.priority - + controllerDispatcherPlugins[b].parameters.priority; }); - print("controllerDispatcher: new plugin order: " + JSON.stringify(this.orderedPluginNames)); + // print("controllerDispatcher -- new plugin order: " + JSON.stringify(this.orderedPluginNames)); + var output = "controllerDispatcher -- new plugin order: "; + for (var k = 0; k < this.orderedPluginNames.length; k++) { + var dbgPluginName = this.orderedPluginNames[k]; + var priority = controllerDispatcherPlugins[dbgPluginName].parameters.priority; + output += dbgPluginName + ":" + priority; + if (k + 1 < this.orderedPluginNames.length) { + output += ", "; + } + } + print(output); controllerDispatcherPluginsNeedSort = false; } @@ -166,44 +182,31 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); var h; for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { // todo: check controllerLocations[h].valid - var nearbyOverlays = Overlays.findOverlays(controllerLocations[h].position, NEAR_MIN_RADIUS); - var makeOverlaySorter = function (handIndex) { - return function (a, b) { - var aPosition = Overlays.getProperty(a, "position"); - var aDistance = Vec3.distance(aPosition, controllerLocations[handIndex]); - var bPosition = Overlays.getProperty(b, "position"); - var bDistance = Vec3.distance(bPosition, controllerLocations[handIndex]); - return aDistance - bDistance; - }; - }; - nearbyOverlays.sort(makeOverlaySorter(h)); + var nearbyOverlays = Overlays.findOverlays(controllerLocations[h].position, NEAR_MAX_RADIUS); + nearbyOverlays.sort(function (a, b) { + var aPosition = Overlays.getProperty(a, "position"); + var aDistance = Vec3.distance(aPosition, controllerLocations[h].position); + var bPosition = Overlays.getProperty(b, "position"); + var bDistance = Vec3.distance(bPosition, controllerLocations[h].position); + return aDistance - bDistance; + }); nearbyOverlayIDs.push(nearbyOverlays); } // find entities near each hand var nearbyEntityProperties = [[], []]; + var nearbyEntityPropertiesByID = {}; for (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); + var nearbyEntityIDs = Entities.findEntities(controllerPosition, NEAR_MAX_RADIUS); for (var j = 0; j < nearbyEntityIDs.length; j++) { var entityID = nearbyEntityIDs[j]; var props = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); props.id = entityID; - props.distanceFromController = Vec3.length(Vec3.subtract(controllerPosition, props.position)); - if (props.distanceFromController < NEAR_MAX_RADIUS) { - nearbyEntityProperties[h].push(props); - } + nearbyEntityPropertiesByID[entityID] = props; + nearbyEntityProperties[h].push(props); } - // sort by distance from each hand - var makeSorter = function (handIndex) { - return function (a, b) { - var aDistance = Vec3.distance(a.position, controllerLocations[handIndex]); - var bDistance = Vec3.distance(b.position, controllerLocations[handIndex]); - return aDistance - bDistance; - }; - }; - nearbyEntityProperties[h].sort(makeSorter(h)); } // raypick for each controller @@ -221,25 +224,33 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); length: 1000 }; - var nearEntityID = rayPicks[h].entityID; - if (nearEntityID) { + if (rayPicks[h].type == RayPick.INTERSECTED_ENTITY) { // XXX check to make sure this one isn't already in nearbyEntityProperties? if (rayPicks[h].distance < NEAR_GRAB_PICK_RADIUS) { + var nearEntityID = rayPicks[h].objectID; var nearbyProps = Entities.getEntityProperties(nearEntityID, DISPATCHER_PROPERTIES); nearbyProps.id = nearEntityID; - if (entityIsGrabbable(nearbyProps)) { - nearbyEntityProperties[h].push(nearbyProps); - } + nearbyEntityPropertiesByID[nearEntityID] = nearbyProps; + nearbyEntityProperties[h].push(nearbyProps); } } + + // sort by distance from each hand + nearbyEntityProperties[h].sort(function (a, b) { + var aDistance = Vec3.distance(a.position, controllerLocations[h].position); + var bDistance = Vec3.distance(b.position, controllerLocations[h].position); + return aDistance - bDistance; + }); } // bundle up all the data about the current situation var controllerData = { triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue], triggerClicks: [_this.leftTriggerClicked, _this.rightTriggerClicked], + secondaryValues: [_this.leftSecondaryValue, _this.rightSecondaryValue], controllerLocations: controllerLocations, nearbyEntityProperties: nearbyEntityProperties, + nearbyEntityPropertiesByID: nearbyEntityPropertiesByID, nearbyOverlayIDs: nearbyOverlayIDs, rayPicks: rayPicks }; @@ -288,6 +299,12 @@ Script.include("/~/system/controllers/controllerDispatcherUtils.js"); mapping.from([Controller.Standard.RTClick]).peek().to(_this.rightTriggerClick); mapping.from([Controller.Standard.LT]).peek().to(_this.leftTriggerPress); mapping.from([Controller.Standard.LTClick]).peek().to(_this.leftTriggerClick); + + mapping.from([Controller.Standard.RB]).peek().to(_this.rightSecondaryPress); + mapping.from([Controller.Standard.LB]).peek().to(_this.leftSecondaryPress); + mapping.from([Controller.Standard.LeftGrip]).peek().to(_this.leftSecondaryPress); + mapping.from([Controller.Standard.RightGrip]).peek().to(_this.rightSecondaryPress); + Controller.enableMapping(MAPPING_NAME); diff --git a/scripts/system/controllers/controllerDispatcherUtils.js b/scripts/system/controllers/controllerDispatcherUtils.js index 72cf9362d1..daf6b667ed 100644 --- a/scripts/system/controllers/controllerDispatcherUtils.js +++ b/scripts/system/controllers/controllerDispatcherUtils.js @@ -17,6 +17,7 @@ COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, + Entities, makeDispatcherModuleParameters, makeRunningValues, enableDispatcherModule, @@ -30,7 +31,8 @@ controllerDispatcherPluginsNeedSort, projectOntoXYPlane, projectOntoEntityXYPlane, - projectOntoOverlayXYPlane + projectOntoOverlayXYPlane, + entityHasActions */ MSECS_PER_SEC = 1000.0; @@ -111,10 +113,14 @@ getGrabbableData = function (props) { var grabbableData = {}; var userDataParsed = null; try { - userDataParsed = JSON.parse(props.userData); + if (!props.userDataParsed) { + props.userDataParsed = JSON.parse(props.userData); + } + userDataParsed = props.userDataParsed; } catch (err) { + userDataParsed = {}; } - if (userDataParsed && userDataParsed.grabbable) { + if (userDataParsed.grabbable) { grabbableData = userDataParsed.grabbable; } if (!grabbableData.hasOwnProperty("grabbable")) { @@ -230,3 +236,7 @@ projectOntoOverlayXYPlane = function projectOntoOverlayXYPlane(overlayID, worldP return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT); }; + +entityHasActions = function (entityID) { + return Entities.getActionIDs(entityID).length > 0; +}; diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js new file mode 100644 index 0000000000..65668f0d23 --- /dev/null +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -0,0 +1,634 @@ +"use strict"; + +// equipEntity.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, AVATAR_SELF_ID, + getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule, + Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, + Vec3, Overlays, flatten, Xform, getControllerWorldLocation +*/ + +Script.include("/~/system/libraries/Xform.js"); +Script.include("/~/system/controllers/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + + +var DEFAULT_SPHERE_MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx"; +var EQUIP_SPHERE_SCALE_FACTOR = 0.65; + + +// Each overlayInfoSet describes a single equip hotspot. +// It is an object with the following keys: +// timestamp - last time this object was updated, used to delete stale hotspot overlays. +// entityID - entity assosicated with this hotspot +// localPosition - position relative to the entity +// hotspot - hotspot object +// overlays - array of overlay objects created by Overlay.addOverlay() +// currentSize - current animated scale value +// targetSize - the target of our scale animations +// type - "sphere" or "model". +function EquipHotspotBuddy() { + // holds map from {string} hotspot.key to {object} overlayInfoSet. + this.map = {}; + + // array of all hotspots that are highlighed. + this.highlightedHotspots = []; +} +EquipHotspotBuddy.prototype.clear = function() { + var keys = Object.keys(this.map); + for (var i = 0; i < keys.length; i++) { + var overlayInfoSet = this.map[keys[i]]; + this.deleteOverlayInfoSet(overlayInfoSet); + } + this.map = {}; + this.highlightedHotspots = []; +}; +EquipHotspotBuddy.prototype.highlightHotspot = function(hotspot) { + this.highlightedHotspots.push(hotspot.key); +}; +EquipHotspotBuddy.prototype.updateHotspot = function(hotspot, timestamp) { + var overlayInfoSet = this.map[hotspot.key]; + if (!overlayInfoSet) { + // create a new overlayInfoSet + overlayInfoSet = { + timestamp: timestamp, + entityID: hotspot.entityID, + localPosition: hotspot.localPosition, + hotspot: hotspot, + currentSize: 0, + targetSize: 1, + overlays: [] + }; + + var diameter = hotspot.radius * 2; + + // override default sphere with a user specified model, if it exists. + overlayInfoSet.overlays.push(Overlays.addOverlay("model", { + name: "hotspot overlay", + url: hotspot.modelURL ? hotspot.modelURL : DEFAULT_SPHERE_MODEL_URL, + position: hotspot.worldPosition, + rotation: { + x: 0, + y: 0, + z: 0, + w: 1 + }, + dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, + scale: hotspot.modelScale, + ignoreRayIntersection: true + })); + overlayInfoSet.type = "model"; + print("QQQ adding hopspot: " + hotspot.key); + this.map[hotspot.key] = overlayInfoSet; + } else { + print("QQQ updating hopspot: " + hotspot.key); + overlayInfoSet.timestamp = timestamp; + } +}; +EquipHotspotBuddy.prototype.updateHotspots = function(hotspots, timestamp) { + var _this = this; + hotspots.forEach(function(hotspot) { + _this.updateHotspot(hotspot, timestamp); + }); + this.highlightedHotspots = []; +}; +EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerData) { + + var HIGHLIGHT_SIZE = 1.1; + var NORMAL_SIZE = 1.0; + + var keys = Object.keys(this.map); + for (var i = 0; i < keys.length; i++) { + var overlayInfoSet = this.map[keys[i]]; + + // this overlayInfo is highlighted. + if (this.highlightedHotspots.indexOf(keys[i]) != -1) { + overlayInfoSet.targetSize = HIGHLIGHT_SIZE; + } else { + overlayInfoSet.targetSize = NORMAL_SIZE; + } + + // start to fade out this hotspot. + if (overlayInfoSet.timestamp != timestamp) { + print("QQQ fading " + overlayInfoSet.entityID + " " + overlayInfoSet.timestamp + " != " + timestamp); + overlayInfoSet.targetSize = 0; + } + + // animate the size. + var SIZE_TIMESCALE = 0.1; + var tau = deltaTime / SIZE_TIMESCALE; + if (tau > 1.0) { + tau = 1.0; + } + overlayInfoSet.currentSize += (overlayInfoSet.targetSize - overlayInfoSet.currentSize) * tau; + + if (overlayInfoSet.timestamp != timestamp && overlayInfoSet.currentSize <= 0.05) { + print("QQQ deleting " + overlayInfoSet.entityID + " " + overlayInfoSet.timestamp + " != " + timestamp); + + // this is an old overlay, that has finished fading out, delete it! + overlayInfoSet.overlays.forEach(Overlays.deleteOverlay); + delete this.map[keys[i]]; + } else { + // update overlay position, rotation to follow the object it's attached to. + + print("QQQ grew " + overlayInfoSet.entityID + " " + overlayInfoSet.timestamp + " != " + timestamp); + + var props = controllerData.nearbyEntityPropertiesByID[overlayInfoSet.entityID]; + if (props) { + var entityXform = new Xform(props.rotation, props.position); + var position = entityXform.xformPoint(overlayInfoSet.localPosition); + + var dimensions; + if (overlayInfoSet.type == "sphere") { + dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize * EQUIP_SPHERE_SCALE_FACTOR; + } else { + dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize; + } + + overlayInfoSet.overlays.forEach(function(overlay) { + Overlays.editOverlay(overlay, { + position: position, + rotation: props.rotation, + dimensions: dimensions + }); + }); + } else { + print("QQQ but no props for " + overlayInfoSet.entityID); + overlayInfoSet.overlays.forEach(Overlays.deleteOverlay); + delete this.map[keys[i]]; + } + } + } +}; + + + +(function() { + + var debug = true; + var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; + + var EQUIP_RADIUS = 0.2; // radius used for palm vs equip-hotspot for equipping. + + var HAPTIC_PULSE_STRENGTH = 1.0; + var HAPTIC_PULSE_DURATION = 13.0; + var HAPTIC_TEXTURE_STRENGTH = 0.1; + var HAPTIC_TEXTURE_DURATION = 3.0; + var HAPTIC_TEXTURE_DISTANCE = 0.002; + var HAPTIC_DEQUIP_STRENGTH = 0.75; + var HAPTIC_DEQUIP_DURATION = 50.0; + + var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing + var TRIGGER_OFF_VALUE = 0.1; + var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab + var BUMPER_ON_VALUE = 0.5; + + + function getWearableData(props) { + var wearable = {}; + try { + if (!props.userDataParsed) { + props.userDataParsed = JSON.parse(props.userData); + } + wearable = props.userDataParsed.wearable ? props.userDataParsed.wearable : {}; + } catch (err) { + } + return wearable; + } + function getEquipHotspotsData(props) { + var equipHotspots = []; + try { + if (!props.userDataParsed) { + props.userDataParsed = JSON.parse(props.userData); + } + equipHotspots = props.userDataParsed.equipHotspots ? props.userDataParsed.equipHotspots : []; + } catch (err) { + } + return equipHotspots; + } + + function getAttachPointSettings() { + try { + var str = Settings.getValue(ATTACH_POINT_SETTINGS); + if (str === "false") { + return {}; + } else { + return JSON.parse(str); + } + } catch (err) { + print("Error parsing attachPointSettings: " + err); + return {}; + } + } + + function setAttachPointSettings(attachPointSettings) { + var str = JSON.stringify(attachPointSettings); + Settings.setValue(ATTACH_POINT_SETTINGS, str); + } + + function getAttachPointForHotspotFromSettings(hotspot, hand) { + var attachPointSettings = getAttachPointSettings(); + var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; + var joints = attachPointSettings[hotspot.key]; + if (joints) { + return joints[jointName]; + } else { + return undefined; + } + } + + function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, offsetRotation) { + var attachPointSettings = getAttachPointSettings(); + var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; + var joints = attachPointSettings[hotspot.key]; + if (!joints) { + joints = {}; + attachPointSettings[hotspot.key] = joints; + } + joints[jointName] = [offsetPosition, offsetRotation]; + setAttachPointSettings(attachPointSettings); + } + + function EquipEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.prevHandIsUpsideDown = false; + + this.parameters = makeDispatcherModuleParameters( + 300, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + var equipHotspotBuddy = new EquipHotspotBuddy(); + + // returns a list of all equip-hotspots assosiated with this entity. + // @param {UUID} entityID + // @returns {Object[]} array of objects with the following fields. + // * key {string} a string that can be used to uniquely identify this hotspot + // * entityID {UUID} + // * localPosition {Vec3} position of the hotspot in object space. + // * worldPosition {vec3} position of the hotspot in world space. + // * radius {number} radius of equip hotspot + // * joints {Object} keys are joint names values are arrays of two elements: + // offset position {Vec3} and offset rotation {Quat}, both are in the coordinate system of the joint. + // * modelURL {string} url for model to use instead of default sphere. + // * modelScale {Vec3} scale factor for model + this.collectEquipHotspots = function(props) { + var result = []; + var entityID = props.id; + var entityXform = new Xform(props.rotation, props.position); + + var equipHotspotsProps = getEquipHotspotsData(props); + if (equipHotspotsProps && equipHotspotsProps.length > 0) { + var i, length = equipHotspotsProps.length; + for (i = 0; i < length; i++) { + var hotspot = equipHotspotsProps[i]; + if (hotspot.position && hotspot.radius && hotspot.joints) { + result.push({ + key: entityID.toString() + i.toString(), + entityID: entityID, + localPosition: hotspot.position, + worldPosition: entityXform.xformPoint(hotspot.position), + radius: hotspot.radius, + joints: hotspot.joints, + modelURL: hotspot.modelURL, + modelScale: hotspot.modelScale + }); + } + } + } else { + var wearableProps = getWearableData(props); + if (wearableProps && wearableProps.joints) { + result.push({ + key: entityID.toString() + "0", + entityID: entityID, + localPosition: { + x: 0, + y: 0, + z: 0 + }, + worldPosition: entityXform.pos, + radius: EQUIP_RADIUS, + joints: wearableProps.joints, + modelURL: null, + modelScale: null + }); + } + } + return result; + }; + + this.hotspotIsEquippable = function(hotspot, controllerData) { + var props = controllerData.nearbyEntityPropertiesByID[hotspot.entityID]; + + var hasParent = true; + if (props.parentID === NULL_UUID) { + hasParent = false; + } + if (hasParent || entityHasActions(hotspot.entityID)) { + if (debug) { + print("equip is skipping '" + props.name + "': grabbed by someone else: " + + hasParent + " : " + entityHasActions(hotspot.entityID)); + } + return false; + } + + return true; + }; + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + + + this.triggerPress = function(value) { + this.rawTriggerValue = value; + }; + + this.triggerClick = function(value) { + this.triggerClicked = value; + }; + + this.secondaryPress = function(value) { + this.rawSecondaryValue = value; + }; + + this.updateSmoothedTrigger = function(controllerData) { + var triggerValue = controllerData.triggerValues[this.hand]; + // smooth out trigger value + this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); + }; + + this.triggerSmoothedGrab = function() { + return this.triggerClicked; + }; + + this.triggerSmoothedSqueezed = function() { + return this.triggerValue > TRIGGER_ON_VALUE; + }; + + this.triggerSmoothedReleased = function() { + return this.triggerValue < TRIGGER_OFF_VALUE; + }; + + this.secondarySqueezed = function() { + return this.rawSecondaryValue > BUMPER_ON_VALUE; + }; + + this.secondaryReleased = function() { + return this.rawSecondaryValue < BUMPER_ON_VALUE; + }; + + + + this.chooseNearEquipHotspots = function(candidateEntityProps, controllerData) { + var _this = this; + var collectedHotspots = flatten(candidateEntityProps.map(function(props) { + return _this.collectEquipHotspots(props); + })); + var controllerLocation = controllerData.controllerLocations[_this.hand]; + var worldControllerPosition = controllerLocation.position; + var equippableHotspots = collectedHotspots.filter(function(hotspot) { + var hotspotDistance = Vec3.distance(hotspot.worldPosition, worldControllerPosition); + return _this.hotspotIsEquippable(hotspot, controllerData) && + hotspotDistance < hotspot.radius; + }); + return equippableHotspots; + }; + + this.chooseBestEquipHotspot = function(candidateEntityProps, controllerData) { + var equippableHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData); + if (equippableHotspots.length > 0) { + // sort by distance; + var controllerLocation = controllerData.controllerLocations[this.hand]; + var worldControllerPosition = controllerLocation.position; + equippableHotspots.sort(function(a, b) { + var aDistance = Vec3.distance(a.worldPosition, worldControllerPosition); + var bDistance = Vec3.distance(b.worldPosition, worldControllerPosition); + return aDistance - bDistance; + }); + return equippableHotspots[0]; + } else { + return null; + } + }; + + this.dropGestureReset = function() { + this.prevHandIsUpsideDown = false; + }; + + this.dropGestureProcess = function (deltaTime) { + var worldHandRotation = getControllerWorldLocation(this.handToController(), true).orientation; + var localHandUpAxis = this.hand === RIGHT_HAND ? { x: 1, y: 0, z: 0 } : { x: -1, y: 0, z: 0 }; + var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); + var DOWN = { x: 0, y: -1, z: 0 }; + + var DROP_ANGLE = Math.PI / 3; + var HYSTERESIS_FACTOR = 1.1; + var ROTATION_ENTER_THRESHOLD = Math.cos(DROP_ANGLE); + var ROTATION_EXIT_THRESHOLD = Math.cos(DROP_ANGLE * HYSTERESIS_FACTOR); + var rotationThreshold = this.prevHandIsUpsideDown ? ROTATION_EXIT_THRESHOLD : ROTATION_ENTER_THRESHOLD; + + var handIsUpsideDown = false; + if (Vec3.dot(worldHandUpAxis, DOWN) > rotationThreshold) { + handIsUpsideDown = true; + } + + if (handIsUpsideDown != this.prevHandIsUpsideDown) { + this.prevHandIsUpsideDown = handIsUpsideDown; + Controller.triggerHapticPulse(HAPTIC_DEQUIP_STRENGTH, HAPTIC_DEQUIP_DURATION, this.hand); + } + + return handIsUpsideDown; + }; + + this.clearEquipHaptics = function() { + this.prevPotentialEquipHotspot = null; + }; + + this.updateEquipHaptics = function(potentialEquipHotspot, currentLocation) { + if (potentialEquipHotspot && !this.prevPotentialEquipHotspot || + !potentialEquipHotspot && this.prevPotentialEquipHotspot) { + Controller.triggerHapticPulse(HAPTIC_TEXTURE_STRENGTH, HAPTIC_TEXTURE_DURATION, this.hand); + this.lastHapticPulseLocation = currentLocation; + } else if (potentialEquipHotspot && + Vec3.distance(this.lastHapticPulseLocation, currentLocation) > HAPTIC_TEXTURE_DISTANCE) { + Controller.triggerHapticPulse(HAPTIC_TEXTURE_STRENGTH, HAPTIC_TEXTURE_DURATION, this.hand); + this.lastHapticPulseLocation = currentLocation; + } + this.prevPotentialEquipHotspot = potentialEquipHotspot; + }; + + this.startEquipEntity = function (controllerData) { + this.dropGestureReset(); + this.clearEquipHaptics(); + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + + // if an object is "equipped" and has a predefined offset, use it. + var offsets = getAttachPointForHotspotFromSettings(this.grabbedHotspot, this.hand); + if (offsets) { + this.offsetPosition = offsets[0]; + this.offsetRotation = offsets[1]; + } else { + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (this.grabbedHotspot.joints[handJointName]) { + this.offsetPosition = this.grabbedHotspot.joints[handJointName][0]; + this.offsetRotation = this.grabbedHotspot.joints[handJointName][1]; + } + } + + var handJointIndex; + if (this.ignoreIK) { + handJointIndex = this.controllerJointIndex; + } else { + handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + } + + var reparentProps = { + parentID: AVATAR_SELF_ID, + parentJointIndex: handJointIndex, + velocity: {x: 0, y: 0, z: 0}, + angularVelocity: {x: 0, y: 0, z: 0}, + localPosition: this.offsetPosition, + localRotation: this.offsetRotation + }; + Entities.editEntity(this.grabbedThingID, reparentProps); + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'equip', + grabbedEntity: this.grabbedThingID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + }; + + this.endEquipEntity = function () { + Entities.editEntity(this.targetEntityID, { + parentID: NULL_UUID, + parentJointIndex: -1 + }); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "releaseEquip", args); + + this.targetEntityID = null; + }; + + this.isReady = function (controllerData, deltaTime) { + + this.rawTriggerValue = controllerData.triggerValues[this.hand]; + this.triggerClicked = controllerData.triggerClicks[this.hand]; + this.rawSecondaryValue = controllerData.secondaryValues[this.hand]; + this.updateSmoothedTrigger(controllerData); + + this.controllerJointIndex = getControllerJointIndex(this.hand); + + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { + this.waitForTriggerRelease = false; + } + + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldHandPosition = controllerLocation.position; + + // var candidateEntities = controllerData.nearbyEntityProperties[this.hand].map(function (props) { + // return props.id; + // }); + + var candidateEntityProps = controllerData.nearbyEntityProperties[this.hand]; + + var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntityProps, controllerData); + if (!this.waitForTriggerRelease) { + this.updateEquipHaptics(potentialEquipHotspot, worldHandPosition); + } + + var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData); + var timestamp = Date.now(); + equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); + if (potentialEquipHotspot) { + equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); + } + + equipHotspotBuddy.update(deltaTime, timestamp, controllerData); + + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData, deltaTime) { + + if (controllerData.secondaryValues[this.hand]) { + // this.secondaryReleased() will always be true when not depressed + // so we cannot simply rely on that for release - ensure that the + // trigger was first "prepared" by being pushed in before the release + this.preparingHoldRelease = true; + } + + if (this.preparingHoldRelease && !controllerData.secondaryValues[this.hand]) { + // we have an equipped object and the secondary trigger was released + // short-circuit the other checks and release it + this.preparingHoldRelease = false; + this.endEquipEntity(); + return makeRunningValues(false, [], []); + } + + var dropDetected = this.dropGestureProcess(deltaTime); + + if (this.triggerSmoothedReleased()) { + this.waitForTriggerRelease = false; + } + + if (dropDetected && this.prevDropDetected != dropDetected) { + this.waitForTriggerRelease = true; + } + + // highlight the grabbed hotspot when the dropGesture is detected. + var timestamp = Date.now(); + if (dropDetected) { + equipHotspotBuddy.updateHotspot(this.grabbedHotspot, timestamp); + equipHotspotBuddy.highlightHotspot(this.grabbedHotspot); + } + + if (dropDetected && !this.waitForTriggerRelease && controllerData.triggerClicks[this.hand]) { + // store the offset attach points into preferences. + if (this.grabbedHotspot && this.grabbedThingID) { + var prefprops = Entities.getEntityProperties(this.grabbedThingID, ["localPosition", "localRotation"]); + if (prefprops && prefprops.localPosition && prefprops.localRotation) { + storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, + prefprops.localPosition, prefprops.localRotation); + } + } + + this.endEquipEntity(); + return makeRunningValues(false, [], []); + } + this.prevDropDetected = dropDetected; + + return makeRunningValues(true, [this.targetEntityID], []); + }; + + this.cleanup = function () { + if (this.targetEntityID) { + this.endEquipEntity(); + } + }; + } + + var leftEquipEntity = new EquipEntity(LEFT_HAND); + var rightEquipEntity = new EquipEntity(RIGHT_HAND); + + enableDispatcherModule("LeftEquipEntity", leftEquipEntity); + enableDispatcherModule("RightEquipEntity", rightEquipEntity); + + this.cleanup = function () { + leftEquipEntity.cleanup(); + rightEquipEntity.cleanup(); + disableDispatcherModule("LeftEquipEntity"); + disableDispatcherModule("RightEquipEntity"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index f16b6c52c2..a2edab4102 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -397,7 +397,8 @@ Script.include("/~/system/libraries/controllers.js"); if (entityIsDistanceGrabbable(targetProps)) { this.grabbedThingID = entityID; this.grabbedDistance = rayPickInfo.distance; - var otherModuleName = this.hand == RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity"; + var otherModuleName = + this.hand == RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity"; var otherFarGrabModule = getEnabledModuleByName(otherModuleName); if (otherFarGrabModule.grabbedThingID == this.grabbedThingID) { this.distanceRotating = true; diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index 552aec20a4..ea45f0a2c6 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -23,7 +23,8 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/nearParentGrabOverlay.js", "controllerModules/nearActionGrabEntity.js", "controllerModules/farActionGrabEntity.js", - "controllerModules/tabletStylusInput.js" + "controllerModules/tabletStylusInput.js", + "controllerModules/equipEntity.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js";