diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 13d71dca1c..528bb349ea 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -42,10 +42,6 @@ var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only var PICK_WITH_HAND_RAY = true; -var DRAW_GRAB_BOXES = false; -var DRAW_HAND_SPHERES = false; -var DROP_WITHOUT_SHAKE = false; - var EQUIP_SPHERE_COLOR = { red: 179, green: 120, blue: 211 }; var EQUIP_SPHERE_ALPHA = 0.15; var EQUIP_SPHERE_SCALE_FACTOR = 0.65; @@ -79,8 +75,6 @@ var LINE_ENTITY_DIMENSIONS = { var LINE_LENGTH = 500; var PICK_MAX_DISTANCE = 500; // max length of pick-ray -var EQUIP_RADIUS_EMBIGGEN_FACTOR = 1.1; - // // near grabbing // @@ -208,7 +202,8 @@ CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { CONTROLLER_STATE_MACHINE[STATE_HOLD] = { name: "hold", enterMethod: "nearGrabbingEnter", - updateMethod: "nearGrabbing" + updateMethod: "nearGrabbing", + exitMethod: "holdExit" }; CONTROLLER_STATE_MACHINE[STATE_NEAR_TRIGGER] = { name: "trigger", @@ -268,6 +263,51 @@ function propsArePhysical(props) { return isPhysical; } +// currently disabled. +var USE_ATTACH_POINT_SETTINGS = false; + +var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; +function getAttachPointSettings() { + try { + var str = Settings.getValue(ATTACH_POINT_SETTINGS); + print("getAttachPointSettings = " + str); + 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); + print("setAttachPointSettings = " + str); + 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 removeMyAvatarFromCollidesWith(origCollidesWith) { var collidesWithSplit = origCollidesWith.split(","); // remove myAvatar from the array @@ -302,20 +342,32 @@ function restore2DMode() { // EntityPropertiesCache is a helper class that contains a cache of entity properties. // the hope is to prevent excess calls to Entity.getEntityProperties() +// +// usage: +// call EntityPropertiesCache.addEntities with all the entities that you are interested in. +// This will fetch their properties. Then call EntityPropertiesCache.getProps to receive an object +// containing a cache of all the properties previously fetched. function EntityPropertiesCache() { this.cache = {}; } EntityPropertiesCache.prototype.clear = function () { this.cache = {}; }; -EntityPropertiesCache.prototype.findEntities = function (position, radius) { - var entities = Entities.findEntities(position, radius); +EntityPropertiesCache.prototype.addEntity = function (entityID) { + var cacheEntry = this.cache[entityID]; + if (cacheEntry && cacheEntry.refCount) { + cacheEntry.refCount += 1; + } else { + this._updateCacheEntry(entityID); + } +}; +EntityPropertiesCache.prototype.addEntities = function (entities) { var _this = this; - entities.forEach(function (x) { - _this.updateEntity(x); + entities.forEach(function (entityID) { + _this.addEntity(entityID); }); }; -EntityPropertiesCache.prototype.updateEntity = function (entityID) { +EntityPropertiesCache.prototype._updateCacheEntry = function (entityID) { var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); // convert props.userData from a string to an object. @@ -328,11 +380,21 @@ EntityPropertiesCache.prototype.updateEntity = function (entityID) { } } props.userData = userData; + props.refCount = 1; this.cache[entityID] = props; }; -EntityPropertiesCache.prototype.getEntities = function () { - return Object.keys(this.cache); +EntityPropertiesCache.prototype.update = function () { + // delete any cacheEntries with zero refCounts. + var entities = Object.keys(this.cache); + for (var i = 0; i < entities.length; i++) { + var props = this.cache[entities[i]]; + if (props.refCount === 0) { + delete this.cache[entities[i]]; + } else { + props.refCount = 0; + } + } }; EntityPropertiesCache.prototype.getProps = function (entityID) { var obj = this.cache[entityID]; @@ -371,6 +433,158 @@ EntityPropertiesCache.prototype.getEquipHotspotsProps = function (entityID) { } }; +// global cache +var entityPropertiesCache = new EntityPropertiesCache(); + +// 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; + + if (hotspot.modelURL) { + // override default sphere with a user specified model + overlayInfoSet.overlays.push(Overlays.addOverlay("model", { + url: hotspot.modelURL, + 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"; + } else { + // default sphere overlay + overlayInfoSet.overlays.push(Overlays.addOverlay("sphere", { + position: hotspot.worldPosition, + rotation: {x: 0, y: 0, z: 0, w: 1}, + dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, + color: EQUIP_SPHERE_COLOR, + alpha: EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + })); + overlayInfoSet.type = "sphere"; + } + + 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) { + + 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) { + // because this item timestamp has expired, it might not be in the cache anymore.... + entityPropertiesCache.addEntity(overlayInfoSet.entityID); + 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(function (overlay) { + Overlays.deleteOverlay(overlay); + }); + delete this.map[keys[i]]; + } else { + // update overlay position, rotation to follow the object it's attached to. + + var props = entityPropertiesCache.getProps(overlayInfoSet.entityID); + 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 + }); + }); + } + } +}; + +// global EquipHotspotBuddy instance +var equipHotspotBuddy = new EquipHotspotBuddy(); + function MyController(hand) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -421,8 +635,6 @@ function MyController(hand) { this.lastPickTime = 0; this.lastUnequipCheckTime = 0; - this.entityPropertyCache = new EntityPropertiesCache(); - this.equipOverlayInfoSetMap = {}; var _this = this; @@ -434,7 +646,7 @@ function MyController(hand) { return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); }; - this.update = function (deltaTime) { + this.update = function (deltaTime, timestamp) { this.updateSmoothedTrigger(); @@ -447,7 +659,7 @@ function MyController(hand) { var updateMethodName = CONTROLLER_STATE_MACHINE[this.state].updateMethod; var updateMethod = this[updateMethodName]; if (updateMethod) { - updateMethod.call(this, deltaTime); + updateMethod.call(this, deltaTime, timestamp); } else { print("WARNING: could not find updateMethod for state " + stateToName(this.state)); } @@ -907,7 +1119,7 @@ function MyController(hand) { return _this.rawThumbValue < THUMB_ON_VALUE; }; - this.off = function () { + this.off = function (deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.waitForTriggerRelease = false; } @@ -921,17 +1133,18 @@ function MyController(hand) { } } - this.entityPropertyCache.clear(); - this.entityPropertyCache.findEntities(this.getHandPosition(), EQUIP_HOTSPOT_RENDER_RADIUS); - var candidateEntities = this.entityPropertyCache.getEntities(); - + var candidateEntities = Entities.findEntities(this.getHandPosition(), EQUIP_HOTSPOT_RENDER_RADIUS); + entityPropertiesCache.addEntities(candidateEntities); var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); if (!this.waitForTriggerRelease) { this.updateEquipHaptics(potentialEquipHotspot); } var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); - this.updateEquipHotspotRendering(nearEquipHotspots, potentialEquipHotspot); + equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); + if (potentialEquipHotspot) { + equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); + } }; this.clearEquipHaptics = function () { @@ -946,95 +1159,6 @@ function MyController(hand) { this.prevPotentialEquipHotspot = potentialEquipHotspot; }; - this.clearEquipHotspotRendering = function () { - var keys = Object.keys(this.equipOverlayInfoSetMap); - for (var i = 0; i < keys.length; i++) { - var overlayInfoSet = this.equipOverlayInfoSetMap[keys[i]]; - this.deleteOverlayInfoSet(overlayInfoSet); - } - this.equipOverlayInfoSetMap = {}; - }; - - this.createOverlayInfoSet = function (hotspot, timestamp) { - var overlayInfoSet = { - timestamp: timestamp, - entityID: hotspot.entityID, - localPosition: hotspot.localPosition, - hotspot: hotspot, - overlays: [] - }; - - var diameter = hotspot.radius * 2; - - overlayInfoSet.overlays.push(Overlays.addOverlay("sphere", { - position: hotspot.worldPosition, - rotation: {x: 0, y: 0, z: 0, w: 1}, - dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, - color: EQUIP_SPHERE_COLOR, - alpha: EQUIP_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - })); - - return overlayInfoSet; - }; - - this.updateOverlayInfoSet = function (overlayInfoSet, timestamp, potentialEquipHotspot) { - overlayInfoSet.timestamp = timestamp; - - var diameter = overlayInfoSet.hotspot.radius * 2; - - // embiggen the overlays if it maches the potentialEquipHotspot - if (potentialEquipHotspot && overlayInfoSet.entityID == potentialEquipHotspot.entityID && - Vec3.equal(overlayInfoSet.localPosition, potentialEquipHotspot.localPosition)) { - diameter = diameter * EQUIP_RADIUS_EMBIGGEN_FACTOR; - } - - var props = _this.entityPropertyCache.getProps(overlayInfoSet.entityID); - var entityXform = new Xform(props.rotation, props.position); - var position = entityXform.xformPoint(overlayInfoSet.localPosition); - - overlayInfoSet.overlays.forEach(function (overlay) { - Overlays.editOverlay(overlay, { - position: position, - rotation: props.rotation, - dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR - }); - }); - }; - - this.deleteOverlayInfoSet = function (overlayInfoSet) { - overlayInfoSet.overlays.forEach(function (overlay) { - Overlays.deleteOverlay(overlay); - }); - }; - - this.updateEquipHotspotRendering = function (hotspots, potentialEquipHotspot) { - var now = Date.now(); - var _this = this; - - hotspots.forEach(function (hotspot) { - var overlayInfoSet = _this.equipOverlayInfoSetMap[hotspot.key]; - if (overlayInfoSet) { - _this.updateOverlayInfoSet(overlayInfoSet, now, potentialEquipHotspot); - } else { - _this.equipOverlayInfoSetMap[hotspot.key] = _this.createOverlayInfoSet(hotspot, now); - } - }); - - // delete sets with old timestamps. - var keys = Object.keys(this.equipOverlayInfoSetMap); - for (var i = 0; i < keys.length; i++) { - var overlayInfoSet = this.equipOverlayInfoSetMap[keys[i]]; - if (overlayInfoSet.timestamp !== now) { - this.deleteOverlayInfoSet(overlayInfoSet); - delete this.equipOverlayInfoSetMap[keys[i]]; - } - } - }; - // Performs ray pick test from the hand controller into the world // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND // @returns {object} returns object with two keys entityID and distance @@ -1093,7 +1217,7 @@ function MyController(hand) { }; this.entityWantsTrigger = function (entityID) { - var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); + var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); return grabbableProps && grabbableProps.wantsTrigger; }; @@ -1107,11 +1231,13 @@ function MyController(hand) { // * 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 (entityID) { var result = []; - var props = this.entityPropertyCache.getProps(entityID); + var props = entityPropertiesCache.getProps(entityID); var entityXform = new Xform(props.rotation, props.position); - var equipHotspotsProps = this.entityPropertyCache.getEquipHotspotsProps(entityID); + var equipHotspotsProps = entityPropertiesCache.getEquipHotspotsProps(entityID); if (equipHotspotsProps && equipHotspotsProps.length > 0) { var i, length = equipHotspotsProps.length; for (i = 0; i < length; i++) { @@ -1123,12 +1249,14 @@ function MyController(hand) { localPosition: hotspot.position, worldPosition: entityXform.xformPoint(hotspot.position), radius: hotspot.radius, - joints: hotspot.joints + joints: hotspot.joints, + modelURL: hotspot.modelURL, + modelScale: hotspot.modelScale }); } } } else { - var wearableProps = this.entityPropertyCache.getWearableProps(entityID); + var wearableProps = entityPropertiesCache.getWearableProps(entityID); if (wearableProps && wearableProps.joints) { result.push({ key: entityID.toString() + "0", @@ -1136,7 +1264,9 @@ function MyController(hand) { localPosition: {x: 0, y: 0, z: 0}, worldPosition: entityXform.pos, radius: EQUIP_RADIUS, - joints: wearableProps.joints + joints: wearableProps.joints, + modelURL: null, + modelScale: null }); } } @@ -1144,8 +1274,8 @@ function MyController(hand) { }; this.hotspotIsEquippable = function (hotspot) { - var props = this.entityPropertyCache.getProps(hotspot.entityID); - var grabProps = this.entityPropertyCache.getGrabProps(hotspot.entityID); + var props = entityPropertiesCache.getProps(hotspot.entityID); + var grabProps = entityPropertiesCache.getGrabProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; @@ -1163,9 +1293,9 @@ function MyController(hand) { }; this.entityIsGrabbable = function (entityID) { - var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); - var grabProps = this.entityPropertyCache.getGrabProps(entityID); - var props = this.entityPropertyCache.getProps(entityID); + var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); + var grabProps = entityPropertiesCache.getGrabProps(entityID); + var props = entityPropertiesCache.getProps(entityID); var physical = propsArePhysical(props); var grabbable = false; var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); @@ -1219,7 +1349,7 @@ function MyController(hand) { return false; } - var props = this.entityPropertyCache.getProps(entityID); + var props = entityPropertiesCache.getProps(entityID); var distance = Vec3.distance(props.position, handPosition); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); @@ -1257,7 +1387,7 @@ function MyController(hand) { return false; } - var props = this.entityPropertyCache.getProps(entityID); + var props = entityPropertiesCache.getProps(entityID); var distance = Vec3.distance(props.position, handPosition); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); @@ -1298,7 +1428,7 @@ function MyController(hand) { } }; - this.search = function () { + this.search = function (deltaTime, timestamp) { var _this = this; var name; @@ -1315,16 +1445,15 @@ function MyController(hand) { var handPosition = this.getHandPosition(); - this.entityPropertyCache.clear(); - this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS); - var candidateEntities = this.entityPropertyCache.getEntities(); + var candidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS); + entityPropertiesCache.addEntities(candidateEntities); var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); if (potentialEquipHotspot) { if (this.triggerSmoothedGrab()) { this.grabbedHotspot = potentialEquipHotspot; this.grabbedEntity = potentialEquipHotspot.entityID; - this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(this.grabbedEntity).name + "'"); + this.setState(STATE_HOLD, "eqipping '" + entityPropertiesCache.getProps(this.grabbedEntity).name + "'"); return; } } @@ -1336,7 +1465,7 @@ function MyController(hand) { var rayPickInfo = this.calcRayPickInfo(this.hand); if (rayPickInfo.entityID) { this.intersectionDistance = rayPickInfo.distance; - this.entityPropertyCache.updateEntity(rayPickInfo.entityID); + entityPropertiesCache.addEntity(rayPickInfo.entityID); if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) { grabbableEntities.push(rayPickInfo.entityID); } @@ -1350,12 +1479,12 @@ function MyController(hand) { if (grabbableEntities.length > 0) { // sort by distance grabbableEntities.sort(function (a, b) { - var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); - var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + var aDistance = Vec3.distance(entityPropertiesCache.getProps(a).position, handPosition); + var bDistance = Vec3.distance(entityPropertiesCache.getProps(b).position, handPosition); return aDistance - bDistance; }); entity = grabbableEntities[0]; - name = this.entityPropertyCache.getProps(entity).name; + name = entityPropertiesCache.getProps(entity).name; this.grabbedEntity = entity; if (this.entityWantsTrigger(entity)) { if (this.triggerSmoothedGrab()) { @@ -1366,8 +1495,8 @@ function MyController(hand) { } } else { if (this.triggerSmoothedGrab()) { - var props = this.entityPropertyCache.getProps(entity); - var grabProps = this.entityPropertyCache.getGrabProps(entity); + var props = entityPropertiesCache.getProps(entity); + var grabProps = entityPropertiesCache.getGrabProps(entity); var refCount = grabProps.refCount ? grabProps.refCount : 0; if (refCount >= 1) { // if another person is holding the object, remember to restore the @@ -1387,7 +1516,7 @@ function MyController(hand) { if (rayPickInfo.entityID) { entity = rayPickInfo.entityID; - name = this.entityPropertyCache.getProps(entity).name; + name = entityPropertiesCache.getProps(entity).name; if (this.entityWantsTrigger(entity)) { if (this.triggerSmoothedGrab()) { this.grabbedEntity = entity; @@ -1410,7 +1539,10 @@ function MyController(hand) { this.updateEquipHaptics(potentialEquipHotspot); var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); - this.updateEquipHotspotRendering(nearEquipHotspots, potentialEquipHotspot); + equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); + if (potentialEquipHotspot) { + equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); + } // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { @@ -1439,7 +1571,6 @@ function MyController(hand) { this.distanceHoldingEnter = function () { - this.clearEquipHotspotRendering(); this.clearEquipHaptics(); // controller pose is in avatar frame @@ -1499,7 +1630,7 @@ function MyController(hand) { this.previousControllerRotation = controllerRotation; }; - this.distanceHolding = function () { + this.distanceHolding = function (deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("releaseGrab"); this.setState(STATE_OFF, "trigger released"); @@ -1668,47 +1799,35 @@ function MyController(hand) { }; this.dropGestureReset = function () { - this.fastHandMoveDetected = false; - this.fastHandMoveTimer = 0; + this.prevHandIsUpsideDown = false; }; this.dropGestureProcess = function (deltaTime) { var standardControllerValue = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; var pose = Controller.getPoseValue(standardControllerValue); - var worldHandVelocity = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); - if (this.fastHandMoveDetected) { - this.fastHandMoveTimer -= deltaTime; - } - if (this.fastHandMoveTimer < 0) { - this.fastHandMoveDetected = false; - } - var FAST_HAND_SPEED_REST_TIME = 1; // sec - var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec - if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) { - this.fastHandMoveDetected = true; - this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME; - } - 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 ROTATION_THRESHOLD = Math.cos(Math.PI / 8); + + var DROP_ANGLE = Math.PI / 7; + 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) > ROTATION_THRESHOLD) { + if (Vec3.dot(worldHandUpAxis, DOWN) > rotationThreshold) { handIsUpsideDown = true; } - var WANT_DEBUG = false; - if (WANT_DEBUG) { - print("zAxis = " + worldHandUpAxis.x + ", " + worldHandUpAxis.y + ", " + worldHandUpAxis.z); - print("dot = " + Vec3.dot(worldHandUpAxis, DOWN) + ", ROTATION_THRESHOLD = " + ROTATION_THRESHOLD); - print("handMove = " + this.fastHandMoveDetected + ", handIsUpsideDown = " + handIsUpsideDown); + if (handIsUpsideDown != this.prevHandIsUpsideDown) { + this.prevHandIsUpsideDown = handIsUpsideDown; + Controller.triggerShortHapticPulse(0.5, this.hand); } - return (DROP_WITHOUT_SHAKE || this.fastHandMoveDetected) && handIsUpsideDown; + return handIsUpsideDown; }; this.nearGrabbingEnter = function () { @@ -1718,7 +1837,6 @@ function MyController(hand) { this.dropGestureReset(); this.clearEquipHaptics(); - this.clearEquipHotspotRendering(); Controller.triggerShortHapticPulse(1.0, this.hand); @@ -1747,11 +1865,18 @@ function MyController(hand) { // if an object is "equipped" and has a predefined offset, use it. this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false; - 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 offsets = USE_ATTACH_POINT_SETTINGS && getAttachPointForHotspotFromSettings(this.grabbedHotspot, this.hand); + if (offsets) { + this.offsetPosition = offsets[0]; + this.offsetRotation = offsets[1]; hasPresetPosition = true; + } 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]; + hasPresetPosition = true; + } } } else { this.ignoreIK = false; @@ -1820,7 +1945,7 @@ function MyController(hand) { this.currentAngularVelocity = ZERO_VEC; }; - this.nearGrabbing = function (deltaTime) { + this.nearGrabbing = function (deltaTime, timestamp) { var dropDetected = this.dropGestureProcess(deltaTime); @@ -1831,6 +1956,14 @@ function MyController(hand) { } if (this.state == STATE_HOLD) { + + // highlight the grabbed hotspot when the dropGesture is detected. + if (dropDetected) { + entityPropertiesCache.addEntity(this.grabbedHotspot.entityID); + equipHotspotBuddy.updateHotspot(this.grabbedHotspot, timestamp); + equipHotspotBuddy.highlightHotspot(this.grabbedHotspot); + } + if (dropDetected && this.triggerSmoothedGrab()) { this.callEntityMethodOnGrabbed("releaseEquip"); this.setState(STATE_OFF, "drop gesture detected"); @@ -1937,9 +2070,24 @@ function MyController(hand) { } }; + this.holdExit = function () { + // store the offset attach points into preferences. + if (USE_ATTACH_POINT_SETTINGS && this.grabbedHotspot && this.grabbedEntity) { + entityPropertiesCache.addEntity(this.grabbedEntity); + var props = entityPropertiesCache.getProps(this.grabbedEntity); + var entityXform = new Xform(props.rotation, props.position); + var avatarXform = new Xform(MyAvatar.orientation, MyAvatar.position); + var handRot = (this.hand === RIGHT_HAND) ? MyAvatar.getRightPalmRotation() : MyAvatar.getLeftPalmRotation(); + var avatarHandPos = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; + var palmXform = new Xform(handRot, avatarXform.xformPoint(avatarHandPos)); + var offsetXform = Xform.mul(palmXform.inv(), entityXform); + + storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, offsetXform.pos, offsetXform.rot); + } + }; + this.nearTriggerEnter = function () { - this.clearEquipHotspotRendering(); this.clearEquipHaptics(); Controller.triggerShortHapticPulse(1.0, this.hand); @@ -1947,13 +2095,12 @@ function MyController(hand) { }; this.farTriggerEnter = function () { - this.clearEquipHotspotRendering(); this.clearEquipHaptics(); this.callEntityMethodOnGrabbed("startFarTrigger"); }; - this.nearTrigger = function () { + this.nearTrigger = function (deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopNearTrigger"); this.setState(STATE_OFF, "trigger released"); @@ -1962,7 +2109,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("continueNearTrigger"); }; - this.farTrigger = function () { + this.farTrigger = function (deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopFarTrigger"); this.setState(STATE_OFF, "trigger released"); @@ -2010,7 +2157,7 @@ function MyController(hand) { // and rotation of the held thing to help content creators set the userData. var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); if (grabData.refCount > 1) { - grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); if (grabbedProperties && grabbedProperties.localPosition && grabbedProperties.localRotation) { print((this.hand === RIGHT_HAND ? '"LeftHand"' : '"RightHand"') + ":" + '[{"x":' + grabbedProperties.localPosition.x + ', "y":' + grabbedProperties.localPosition.y + @@ -2214,7 +2361,7 @@ function MyController(hand) { delayedDeactivateTimeout = null; _this.delayedDeactivateEntity(delayedEntityID, delayedCollidesWith); return delayedCollidesWith; - } + }; delayedDeactivateTimeout = Script.setTimeout(delayedDeactivateFunc, COLLIDE_WITH_AV_AFTER_RELEASE_DELAY * MSECS_PER_SEC); delayedDeactivateEntityID = entityID; @@ -2317,12 +2464,16 @@ Controller.enableMapping(MAPPING_NAME); var handToDisable = 'none'; function update(deltaTime) { + var timestamp = Date.now(); + if (handToDisable !== LEFT_HAND && handToDisable !== 'both') { - leftController.update(deltaTime); + leftController.update(deltaTime, timestamp); } if (handToDisable !== RIGHT_HAND && handToDisable !== 'both') { - rightController.update(deltaTime); + rightController.update(deltaTime, timestamp); } + equipHotspotBuddy.update(deltaTime, timestamp); + entityPropertiesCache.update(); } Messages.subscribe('Hifi-Hand-Disabler'); @@ -2392,38 +2543,7 @@ function cleanup() { leftController.cleanup(); Controller.disableMapping(MAPPING_NAME); Reticle.setVisible(true); - Menu.removeMenuItem("Developer > Hands", "Drop Without Shake"); } Script.scriptEnding.connect(cleanup); Script.update.connect(update); - -if (!Menu.menuExists("Developer > Grab Script")) { - Menu.addMenu("Developer > Grab Script"); -} - -Menu.addMenuItem({ - menuName: "Developer > Grab Script", - menuItemName: "Drop Without Shake", - isCheckable: true, - isChecked: DROP_WITHOUT_SHAKE -}); - -Menu.addMenuItem({ - menuName: "Developer > Grab Script", - menuItemName: "Draw Grab Boxes", - isCheckable: true, - isChecked: DRAW_GRAB_BOXES -}); - -function handleMenuItemEvent(menuItem) { - if (menuItem === "Drop Without Shake") { - DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake"); - } - if (menuItem === "Draw Grab Boxes") { - DRAW_GRAB_BOXES = Menu.isOptionChecked("Draw Grab Boxes"); - DRAW_HAND_SPHERES = DRAW_GRAB_BOXES; - } -} - -Menu.menuItemEvent.connect(handleMenuItemEvent);