"use strict"; // equipEntity.js//// Created by Seth Alves, August 14th, 2017.// Copyright 2017 High Fidelity, Inc.// Copyright 2023, Overte e.V.//// 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, Camera, print, getControllerJointIndex, enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, isInEditMode, getGrabbableData, entityIsEquippable, HMD*/ Script.include("/~/system/libraries/Xform.js");Script.include("/~/system/libraries/controllerDispatcherUtils.js");Script.include("/~/system/libraries/controllers.js");Script.include("/~/system/libraries/cloneEntityUtils.js");Script.include("/~/system/libraries/utils.js"); var controllerStandard = Controller.Standard;var DEFAULT_SPHERE_MODEL_URL = Script.resolvePath("../../assets/models/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 dimensions = hotspot.radius * 2 * EQUIP_SPHERE_SCALE_FACTOR; if (hotspot.indicatorURL) { dimensions = hotspot.indicatorScale; } // override default sphere with a user specified model, if it exists. overlayInfoSet.overlays.push(Entities.addEntity({ "type": "Model", "name": "hotspot overlay", "modelURL": hotspot.indicatorURL ? hotspot.indicatorURL : DEFAULT_SPHERE_MODEL_URL, "position": hotspot.worldPosition, "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 }, "dimensions": dimensions, "ignorePickIntersection": true, "grab": { "grabbable": false }, }, "local")); 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(Entities.deleteEntity); 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.hotspot.indicatorURL) { var ratio = overlayInfoSet.currentSize / overlayInfoSet.targetSize; dimensions = { x: overlayInfoSet.hotspot.dimensions.x * ratio, y: overlayInfoSet.hotspot.dimensions.y * ratio, z: overlayInfoSet.hotspot.dimensions.z * ratio }; } else { dimensions = (overlayInfoSet.hotspot.radius / 2) * overlayInfoSet.currentSize; } overlayInfoSet.overlays.forEach(function(overlay) { Entities.editEntity(overlay, { "position": position, "rotation": props.rotation, "dimensions": dimensions }); }); } else { overlayInfoSet.overlays.forEach(Entities.deleteEntity); delete this.map[keys[i]]; } } }}; (function() { var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; 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 ATTACHPOINT_MAX_DISTANCE = 3.0; // var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}"; var UNEQUIP_KEY = "u"; function getWearableData(props) { if (props.grab.equippable) { return { joints: { LeftHand: [ props.grab.equippableLeftPosition, props.grab.equippableLeftRotation ], RightHand: [ props.grab.equippableRightPosition, props.grab.equippableRightRotation ] }, indicatorURL: props.grab.equippableIndicatorURL, indicatorScale: props.grab.equippableIndicatorScale, indicatorOffset: props.grab.equippableIndicatorOffset }; } else { return null; } } 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) { // make sure they are reasonable if (joints[jointName] && joints[jointName][0] && Vec3.length(joints[jointName][0]) > ATTACHPOINT_MAX_DISTANCE) { print("equipEntity -- Warning: rejecting settings attachPoint " + Vec3.length(joints[jointName][0])); return undefined; } 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.parameters = makeDispatcherModuleParameters( 115, this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip"] : ["leftHand", "leftHandEquip"], [], 100); var equipHotspotBuddy = new EquipHotspotBuddy(); this.setMessageGrabData = function(entityProperties) { if (entityProperties) { this.messageGrabEntity = true; this.grabEntityProps = entityProperties; } }; // 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. // * indicatorURL {string} url for model to use instead of default sphere. // * indicatorScale {Vec3} scale factor for model this.collectEquipHotspots = function(props) { var result = []; var entityID = props.id; var entityXform = new Xform(props.rotation, props.position); var wearableProps = getWearableData(props); var sensorToScaleFactor = MyAvatar.sensorToWorldScale; if (wearableProps && wearableProps.joints) { result.push({ key: entityID.toString() + "0", entityID: entityID, localPosition: wearableProps.indicatorOffset, worldPosition: entityXform.pos, radius: ((wearableProps.indicatorScale.x + wearableProps.indicatorScale.y + wearableProps.indicatorScale.z) / 3) * sensorToScaleFactor, dimensions: wearableProps.indicatorScale, joints: wearableProps.joints, indicatorURL: wearableProps.indicatorURL, indicatorScale: wearableProps.indicatorScale, }); } return result; }; this.hotspotIsEquippable = function(hotspot, controllerData) { var props = controllerData.nearbyEntityPropertiesByID[hotspot.entityID]; var hasParent = true; if (props.parentID === Uuid.NONE) { hasParent = false; } if (hasParent || entityHasActions(hotspot.entityID)) { return false; } return true; }; this.handToController = function() { return (this.hand === RIGHT_HAND) ? controllerStandard.RightHand : controllerStandard.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 cloneID = cloneEntity(props); 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) { var _this = this; this.dropGestureReset(); this.clearEquipHaptics(); Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); var grabbedProperties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES); var grabData = getGrabbableData(grabbedProperties); // 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 (HMD.mounted && HMD.isHandControllerAvailable() && grabData.grabFollowsController) { handJointIndex = this.controllerJointIndex;Lines 1–500 of 875. Load more
"use strict"; // equipEntity.js // // Created by Seth Alves, August 14th, 2017. // Copyright 2017 High Fidelity, Inc. // Copyright 2023, Overte e.V. // // 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, Camera, print, getControllerJointIndex, enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, isInEditMode, getGrabbableData, entityIsEquippable, HMD */ Script.include("/~/system/libraries/Xform.js"); Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.include("/~/system/libraries/controllers.js"); Script.include("/~/system/libraries/cloneEntityUtils.js"); Script.include("/~/system/libraries/utils.js"); var controllerStandard = Controller.Standard; var DEFAULT_SPHERE_MODEL_URL = Script.resolvePath("../../assets/models/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 dimensions = hotspot.radius * 2 * EQUIP_SPHERE_SCALE_FACTOR; if (hotspot.indicatorURL) { dimensions = hotspot.indicatorScale; } // override default sphere with a user specified model, if it exists. overlayInfoSet.overlays.push(Entities.addEntity({ "type": "Model", "name": "hotspot overlay", "modelURL": hotspot.indicatorURL ? hotspot.indicatorURL : DEFAULT_SPHERE_MODEL_URL, "position": hotspot.worldPosition, "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 }, "dimensions": dimensions, "ignorePickIntersection": true, "grab": { "grabbable": false }, }, "local")); 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(Entities.deleteEntity); 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.hotspot.indicatorURL) { var ratio = overlayInfoSet.currentSize / overlayInfoSet.targetSize; dimensions = { x: overlayInfoSet.hotspot.dimensions.x * ratio, y: overlayInfoSet.hotspot.dimensions.y * ratio, z: overlayInfoSet.hotspot.dimensions.z * ratio }; } else { dimensions = (overlayInfoSet.hotspot.radius / 2) * overlayInfoSet.currentSize; } overlayInfoSet.overlays.forEach(function(overlay) { Entities.editEntity(overlay, { "position": position, "rotation": props.rotation, "dimensions": dimensions }); }); } else { overlayInfoSet.overlays.forEach(Entities.deleteEntity); delete this.map[keys[i]]; } } } }; (function() { var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; 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 ATTACHPOINT_MAX_DISTANCE = 3.0; // var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}"; var UNEQUIP_KEY = "u"; function getWearableData(props) { if (props.grab.equippable) { return { joints: { LeftHand: [ props.grab.equippableLeftPosition, props.grab.equippableLeftRotation ], RightHand: [ props.grab.equippableRightPosition, props.grab.equippableRightRotation ] }, indicatorURL: props.grab.equippableIndicatorURL, indicatorScale: props.grab.equippableIndicatorScale, indicatorOffset: props.grab.equippableIndicatorOffset }; } else { return null; } } 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) { // make sure they are reasonable if (joints[jointName] && joints[jointName][0] && Vec3.length(joints[jointName][0]) > ATTACHPOINT_MAX_DISTANCE) { print("equipEntity -- Warning: rejecting settings attachPoint " + Vec3.length(joints[jointName][0])); return undefined; } 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.parameters = makeDispatcherModuleParameters( 115, this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip"] : ["leftHand", "leftHandEquip"], [], 100); var equipHotspotBuddy = new EquipHotspotBuddy(); this.setMessageGrabData = function(entityProperties) { if (entityProperties) { this.messageGrabEntity = true; this.grabEntityProps = entityProperties; } }; // 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. // * indicatorURL {string} url for model to use instead of default sphere. // * indicatorScale {Vec3} scale factor for model this.collectEquipHotspots = function(props) { var result = []; var entityID = props.id; var entityXform = new Xform(props.rotation, props.position); var wearableProps = getWearableData(props); var sensorToScaleFactor = MyAvatar.sensorToWorldScale; if (wearableProps && wearableProps.joints) { result.push({ key: entityID.toString() + "0", entityID: entityID, localPosition: wearableProps.indicatorOffset, worldPosition: entityXform.pos, radius: ((wearableProps.indicatorScale.x + wearableProps.indicatorScale.y + wearableProps.indicatorScale.z) / 3) * sensorToScaleFactor, dimensions: wearableProps.indicatorScale, joints: wearableProps.joints, indicatorURL: wearableProps.indicatorURL, indicatorScale: wearableProps.indicatorScale, }); } return result; }; this.hotspotIsEquippable = function(hotspot, controllerData) { var props = controllerData.nearbyEntityPropertiesByID[hotspot.entityID]; var hasParent = true; if (props.parentID === Uuid.NONE) { hasParent = false; } if (hasParent || entityHasActions(hotspot.entityID)) { return false; } return true; }; this.handToController = function() { return (this.hand === RIGHT_HAND) ? controllerStandard.RightHand : controllerStandard.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 cloneID = cloneEntity(props); 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) { var _this = this; this.dropGestureReset(); this.clearEquipHaptics(); Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); var grabbedProperties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES); var grabData = getGrabbableData(grabbedProperties); // 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 (HMD.mounted && HMD.isHandControllerAvailable() && grabData.grabFollowsController) { handJointIndex = this.controllerJointIndex;