"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, getControllerJointIndex, enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, TEAR_AWAY_DISTANCE, Uuid */ Script.include("/~/system/libraries/Xform.js"); Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.include("/~/system/libraries/controllers.js"); Script.include("/~/system/libraries/cloneEntityUtils.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; var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}"; // 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"; this.map[hotspot.key] = overlayInfoSet; } else { 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) { 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) { // 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. 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 { overlayInfoSet.overlays.forEach(Overlays.deleteOverlay); delete this.map[keys[i]]; } } } }; (function() { var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; var EQUIP_RADIUS = 1.0; // 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; var UNEQUIP_KEY = "u"; function getWearableData(props) { var wearable = {}; try { if (!props.userDataParsed) { props.userDataParsed = JSON.parse(props.userData); } wearable = props.userDataParsed.wearable ? props.userDataParsed.wearable : {}; } catch (err) { // don't want to spam the logs } 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) { // don't want to spam the logs } return equipHotspots; } function getAttachPointSettings() { try { var str = Settings.getValue(ATTACH_POINT_SETTINGS); if (str === "false" || str === "") { 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 skeletonModelURL = MyAvatar.skeletonModelURL; var attachPointSettings = getAttachPointSettings(); var avatarSettingsData = attachPointSettings[skeletonModelURL]; if (avatarSettingsData) { var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; var joints = avatarSettingsData[hotspot.key]; if (joints) { return joints[jointName]; } } return undefined; } function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, offsetRotation) { var attachPointSettings = getAttachPointSettings(); var skeletonModelURL = MyAvatar.skeletonModelURL; var avatarSettingsData = attachPointSettings[skeletonModelURL]; if (!avatarSettingsData) { avatarSettingsData = {}; attachPointSettings[skeletonModelURL] = avatarSettingsData; } var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; var joints = avatarSettingsData[hotspot.key]; if (!joints) { joints = {}; avatarSettingsData[hotspot.key] = joints; } joints[jointName] = [offsetPosition, offsetRotation]; setAttachPointSettings(attachPointSettings); } function clearAttachPoints() { setAttachPointSettings({}); } function EquipEntity(hand) { this.hand = hand; this.targetEntityID = null; this.prevHandIsUpsideDown = false; this.triggerValue = 0; this.messageGrabEntity = false; this.grabEntityProps = null; this.shouldSendStart = false; this.equipedWithSecondary = false; this.handHasBeenRightsideUp = false; this.mouseEquip = false; this.mouseEquipAnimationHandler; this.parameters = makeDispatcherModuleParameters( 300, this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip"] : ["leftHand", "leftHandEquip"], [], 100); var equipHotspotBuddy = new EquipHotspotBuddy(); this.setMessageGrabData = function(entityProperties, mouseEquip) { if (entityProperties) { this.messageGrabEntity = true; this.grabEntityProps = entityProperties; this.mouseEquip = mouseEquip; } }; // 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); var sensorToScaleFactor = MyAvatar.sensorToWorldScale; 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 * sensorToScaleFactor, 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 === Uuid.NULL) { hasParent = false; } if (hasParent || entityHasActions(hotspot.entityID)) { return false; } return true; }; this.handToController = function() { return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; }; 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.secondaryReleased = function() { return this.rawSecondaryValue < BUMPER_ON_VALUE; }; this.secondarySmoothedSqueezed = 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.cloneHotspot = function(props, controllerData) { if (entityIsCloneable(props)) { var worldEntityProps = controllerData.nearbyEntityProperties[this.hand]; var cloneID = cloneEntity(props, worldEntityProps); return cloneID; } return null; }; 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); var grabbedProperties = Entities.getEntityProperties(this.targetEntityID); // if an object is "equipped" and has a predefined offset, use it. if (this.grabbedHotspot) { 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: MyAvatar.SELF_ID, parentJointIndex: handJointIndex, localVelocity: {x: 0, y: 0, z: 0}, localAngularVelocity: {x: 0, y: 0, z: 0}, localPosition: this.offsetPosition, localRotation: this.offsetRotation }; var isClone = false; if (entityIsCloneable(grabbedProperties)) { var cloneID = this.cloneHotspot(grabbedProperties, controllerData); this.targetEntityID = cloneID; Entities.editEntity(this.targetEntityID, reparentProps); controllerData.nearbyEntityPropertiesByID[this.targetEntityID] = grabbedProperties; isClone = true; } else if (!grabbedProperties.locked) { Entities.editEntity(this.targetEntityID, reparentProps); } else { this.grabbedHotspot = null; this.targetEntityID = null; return; } // we don't want to send startEquip message until the trigger is released. otherwise, // guns etc will fire right as they are equipped. this.shouldSendStart = true; Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'equip', grabbedEntity: this.targetEntityID, joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); var _this = this; var grabEquipCheck = function() { var args = [_this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(_this.targetEntityID, "startEquip", args); }; if (isClone) { // 100 ms seems to be sufficient time to force the check even occur after the object has been initialized. Script.setTimeout(grabEquipCheck, 100); } if (this.mouseEquip) { this.removeMouseEquipAnimation(); if (this.hand === RIGHT_HAND) { this.mouseEquipAnimationHandler = MyAvatar.addAnimationStateHandler(this.rightHandMouseEquipAnimation, []); } else { this.mouseEquipAnimationHandler = MyAvatar.addAnimationStateHandler(this.leftHandMouseEquipAnimation, []); } } }; this.endEquipEntity = function () { this.storeAttachPointInSettings(); Entities.editEntity(this.targetEntityID, { parentID: Uuid.NULL, parentJointIndex: -1 }); var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(this.targetEntityID, "releaseEquip", args); Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'release', grabbedEntity: this.targetEntityID, joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); ensureDynamic(this.targetEntityID); this.targetEntityID = null; this.messageGrabEntity = false; this.grabEntityProps = null; if (this.mouseEquip) { this.removeMouseEquipAnimation(); this.mouseEquip = false; } }; this.updateInputs = function (controllerData) { this.rawTriggerValue = controllerData.triggerValues[this.hand]; this.triggerClicked = controllerData.triggerClicks[this.hand]; this.rawSecondaryValue = controllerData.secondaryValues[this.hand]; this.updateSmoothedTrigger(controllerData); }; this.checkNearbyHotspots = function (controllerData, deltaTime, timestamp) { this.controllerJointIndex = getControllerJointIndex(this.hand); if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.waitForTriggerRelease = false; } var controllerLocation = getControllerWorldLocation(this.handToController(), true); var worldHandPosition = controllerLocation.position; var candidateEntityProps = controllerData.nearbyEntityProperties[this.hand]; var potentialEquipHotspot = null; if (this.messageGrabEntity) { var hotspots = this.collectEquipHotspots(this.grabEntityProps); if (hotspots.length > -1) { potentialEquipHotspot = hotspots[0]; } } else { potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntityProps, controllerData); } if (!this.waitForTriggerRelease) { this.updateEquipHaptics(potentialEquipHotspot, worldHandPosition); } var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData); equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); if (potentialEquipHotspot) { equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); } equipHotspotBuddy.update(deltaTime, timestamp, controllerData); // if the potentialHotspot is cloneable, clone it and return it // if the potentialHotspot os not cloneable and locked return null if (potentialEquipHotspot && (((this.triggerSmoothedSqueezed() || this.secondarySmoothedSqueezed()) && !this.waitForTriggerRelease) || this.messageGrabEntity)) { this.grabbedHotspot = potentialEquipHotspot; this.targetEntityID = this.grabbedHotspot.entityID; this.startEquipEntity(controllerData); this.messageGrabEntity = false; this.equipedWithSecondary = this.secondarySmoothedSqueezed(); return makeRunningValues(true, [potentialEquipHotspot.entityID], []); } else { return makeRunningValues(false, [], []); } }; this.isTargetIDValid = function(controllerData) { var entityProperties = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; return entityProperties && "type" in entityProperties; }; this.isReady = function (controllerData, deltaTime) { var timestamp = Date.now(); this.updateInputs(controllerData); this.handHasBeenRightsideUp = false; return this.checkNearbyHotspots(controllerData, deltaTime, timestamp); }; this.run = function (controllerData, deltaTime) { var timestamp = Date.now(); this.updateInputs(controllerData); if (!this.mouseEquip && !this.isTargetIDValid(controllerData)) { this.endEquipEntity(); return makeRunningValues(false, [], []); } if (!this.targetEntityID) { return this.checkNearbyHotspots(controllerData, deltaTime, timestamp); } if (controllerData.secondaryValues[this.hand] && !this.equipedWithSecondary) { // 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 handIsUpsideDown = this.dropGestureProcess(deltaTime); var dropDetected = false; if (this.handHasBeenRightsideUp) { dropDetected = handIsUpsideDown; } if (!handIsUpsideDown) { this.handHasBeenRightsideUp = true; } if (this.triggerSmoothedReleased() || this.secondaryReleased()) { if (this.shouldSendStart) { // we don't want to send startEquip message until the trigger is released. otherwise, // guns etc will fire right as they are equipped. var startArgs = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(this.targetEntityID, "startEquip", startArgs); this.shouldSendStart = false; } this.waitForTriggerRelease = false; if (this.secondaryReleased() && this.equipedWithSecondary) { this.equipedWithSecondary = false; } } if (dropDetected && this.prevDropDetected !== dropDetected) { this.waitForTriggerRelease = true; } // highlight the grabbed hotspot when the dropGesture is detected. if (dropDetected && this.grabbedHotspot) { equipHotspotBuddy.updateHotspot(this.grabbedHotspot, timestamp); equipHotspotBuddy.highlightHotspot(this.grabbedHotspot); } if (dropDetected && !this.waitForTriggerRelease && this.triggerSmoothedGrab()) { this.waitForTriggerRelease = true; // store the offset attach points into preferences. this.endEquipEntity(); return makeRunningValues(false, [], []); } this.prevDropDetected = dropDetected; equipHotspotBuddy.update(deltaTime, timestamp, controllerData); if (!this.shouldSendStart) { var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(this.targetEntityID, "continueEquip", args); } return makeRunningValues(true, [this.targetEntityID], []); }; this.storeAttachPointInSettings = function() { if (this.grabbedHotspot && this.targetEntityID) { var prefProps = Entities.getEntityProperties(this.targetEntityID, ["localPosition", "localRotation"]); if (prefProps && prefProps.localPosition && prefProps.localRotation) { storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, prefProps.localPosition, prefProps.localRotation); } } }; this.cleanup = function () { if (this.targetEntityID) { this.endEquipEntity(); } }; this.removeMouseEquipAnimation = function() { if (this.mouseEquipAnimationHandler) { this.mouseEquipAnimationHandler = MyAvatar.removeAnimationStateHandler(this.mouseEquipAnimationHandler); } }; this.leftHandMouseEquipAnimation = function() { var result = {}; var leftHandPosition = MyAvatar.getJointPosition("LeftHand"); var leftShoulderPosition = MyAvatar.getJointPosition("LeftShoulder"); var cameraToLeftShoulder = Vec3.subtract(leftShoulderPosition, Camera.position); var cameraToLeftShoulderNormalized = Vec3.normalize(cameraToLeftShoulder); var leftHandPositionNew = Vec3.sum(leftShoulderPosition, cameraToLeftShoulderNormalized); var leftHandPositionNewAvatarFrame = Vec3.subtract(leftHandPositionNew, MyAvatar.position); result.leftHandType = 1; result.leftHandPosition = leftHandPositionNewAvatarFrame; result.leftHandRotation = Quat.multiply(Quat.lookAtSimple(leftHandPositionNew, Camera.position), Quat.fromPitchYawRollDegrees(90, 0, -90)); return result; }; this.rightHandMouseEquipAnimation = function() { var result = {}; var rightHandPosition = MyAvatar.getJointPosition("RightHand"); var rightShoulderPosition = MyAvatar.getJointPosition("RightShoulder"); var cameraToRightShoulder = Vec3.subtract(rightShoulderPosition, Camera.position); var cameraToRightShoulderNormalized = Vec3.normalize(cameraToRightShoulder); var rightHandPositionNew = Vec3.sum(rightShoulderPosition, cameraToRightShoulderNormalized); var rightHandPositionNewAvatarFrame = Vec3.subtract(rightHandPositionNew, MyAvatar.position); result.rightHandType = 1; result.rightHandPosition = rightHandPositionNewAvatarFrame; result.rightHandRotation = Quat.multiply(Quat.lookAtSimple(rightHandPositionNew, Camera.position), Quat.fromPitchYawRollDegrees(90, 0, 90)); return result; }; } var handleMessage = function(channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Grab') { try { data = JSON.parse(message); var equipModule = (data.hand === "left") ? leftEquipEntity : rightEquipEntity; var entityProperties = Entities.getEntityProperties(data.entityID, DISPATCHER_PROPERTIES); entityProperties.id = data.entityID; var mouseEquip = false; equipModule.setMessageGrabData(entityProperties, mouseEquip); } catch (e) { print("WARNING: equipEntity.js -- error parsing Hifi-Hand-Grab message: " + message); } } else if (channel === 'Hifi-Hand-Drop') { if (message === "left") { leftEquipEntity.endEquipEntity(); } else if (message === "right") { rightEquipEntity.endEquipEntity(); } else if (message === "both") { leftEquipEntity.endEquipEntity(); rightEquipEntity.endEquipEntity(); } } } }; var clearGrabActions = function(entityID) { var actionIDs = Entities.getActionIDs(entityID); for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) { var actionID = actionIDs[actionIndex]; var actionArguments = Entities.getActionArguments(entityID, actionID); var tag = actionArguments.tag; if (tag.slice(0, 5) == "grab-") { Entities.deleteAction(entityID, actionID); } } }; var onMousePress = function(event) { if (isInEditMode()) { // ignore any mouse clicks on the entity while create/edit is open return; } var pickRay = Camera.computePickRay(event.x, event.y); var intersection = Entities.findRayIntersection(pickRay, true); if (intersection.intersects) { var entityProperties = Entities.getEntityProperties(intersection.entityID, DISPATCHER_PROPERTIES); if (entityProperties.parentID === EMPTY_PARENT_ID) { entityProperties.id = intersection.entityID; var rightHandPosition = MyAvatar.getJointPosition("RightHand"); var leftHandPosition = MyAvatar.getJointPosition("LeftHand"); var distanceToRightHand = Vec3.distance(entityProperties.position, rightHandPosition); var distanceToLeftHand = Vec3.distance(entityProperties.position, leftHandPosition); var leftHandAvailable = leftEquipEntity.targetEntityID === null; var rightHandAvailable = rightEquipEntity.targetEntityID === null; var mouseEquip = true; if (rightHandAvailable && (distanceToRightHand < distanceToLeftHand || !leftHandAvailable)) { clearGrabActions(intersection.entityID); rightEquipEntity.setMessageGrabData(entityProperties, mouseEquip); } else if (leftHandAvailable && (distanceToLeftHand < distanceToRightHand || !rightHandAvailable)) { clearGrabActions(intersection.entityID); leftEquipEntity.setMessageGrabData(entityProperties, mouseEquip); } } } }; var onKeyPress = function(event) { if (event.text === UNEQUIP_KEY) { if (rightEquipEntity.mouseEquip) { rightEquipEntity.endEquipEntity(); } if (leftEquipEntity.mouseEquip) { leftEquipEntity.endEquipEntity(); } } }; Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-Drop'); Messages.messageReceived.connect(handleMessage); Controller.mousePressEvent.connect(onMousePress); Controller.keyPressEvent.connect(onKeyPress); var leftEquipEntity = new EquipEntity(LEFT_HAND); var rightEquipEntity = new EquipEntity(RIGHT_HAND); enableDispatcherModule("LeftEquipEntity", leftEquipEntity); enableDispatcherModule("RightEquipEntity", rightEquipEntity); function cleanup() { leftEquipEntity.cleanup(); rightEquipEntity.cleanup(); disableDispatcherModule("LeftEquipEntity"); disableDispatcherModule("RightEquipEntity"); clearAttachPoints(); Messages.messageReceived.disconnect(handleMessage); Controller.mousePressEvent.disconnect(onMousePress); Controller.keyPressEvent.disconnect(onKeyPress); } Script.scriptEnding.connect(cleanup); }());