diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 6b81ad7ff3..a857dc39e0 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -43,7 +43,6 @@ ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) : void ModelOverlay::update(float deltatime) { if (_updateModel) { _updateModel = false; - _model->setSnapModelToCenter(true); _model->setScaleToFit(true, getDimensions()); _model->setRotation(getRotation()); @@ -87,23 +86,15 @@ void ModelOverlay::render(RenderArgs* args) { void ModelOverlay::setProperties(const QVariantMap& properties) { auto position = getPosition(); auto rotation = getRotation(); - auto scale = getDimensions(); - + Volume3DOverlay::setProperties(properties); - + if (position != getPosition() || rotation != getRotation()) { _updateModel = true; } - if (scale != getDimensions()) { - auto newScale = getDimensions(); - if (newScale.x <= 0 || newScale.y <= 0 || newScale.z <= 0) { - setDimensions(scale); - } else { - _updateModel = true; - } - } - + _updateModel = true; + auto urlValue = properties["url"]; if (urlValue.isValid() && urlValue.canConvert()) { _url = urlValue.toString(); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 4e4269df28..2cef48290c 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -26,9 +26,11 @@ var WANT_DEBUG_SEARCH_NAME = null; // these tune time-averaging and "on" value for analog trigger // +var SPARK_MODEL_SCALE_FACTOR = 0.75; + var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab -var TRIGGER_GRAB_VALUE = 0.75; // Squeezed far enough to complete distant grab +var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab var TRIGGER_OFF_VALUE = 0.15; var BUMPER_ON_VALUE = 0.5; @@ -43,6 +45,10 @@ 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; + // // distant manipulation // @@ -72,11 +78,14 @@ 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 // var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. +var EQUIP_HOTSPOT_RENDER_RADIUS = 0.3; // radius used for palm vs equip-hotspot for rendering hot-spots var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position @@ -179,9 +188,7 @@ CONTROLLER_STATE_MACHINE[STATE_OFF] = { }; CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { name: "searching", - updateMethod: "search", - enterMethod: "searchEnter", - exitMethod: "searchExit" + updateMethod: "search" }; CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { name: "distance_holding", @@ -261,7 +268,8 @@ function propsArePhysical(props) { var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; var EDIT_SETTING = "io.highfidelity.isEditting"; function isEditing() { - return EXTERNALLY_MANAGED_2D_MINOR_MODE && Settings.getValue(EDIT_SETTING); + var actualSettingValue = Settings.getValue(EDIT_SETTING) === "false" ? false : !!Settings.getValue(EDIT_SETTING); + return EXTERNALLY_MANAGED_2D_MINOR_MODE && actualSettingValue; } function isIn2DMode() { // In this version, we make our own determination of whether we're aimed a HUD element, @@ -281,17 +289,17 @@ function restore2DMode() { function EntityPropertiesCache() { this.cache = {}; } -EntityPropertiesCache.prototype.clear = function() { +EntityPropertiesCache.prototype.clear = function () { this.cache = {}; }; -EntityPropertiesCache.prototype.findEntities = function(position, radius) { +EntityPropertiesCache.prototype.findEntities = function (position, radius) { var entities = Entities.findEntities(position, radius); var _this = this; - entities.forEach(function(x) { + entities.forEach(function (x) { _this.updateEntity(x); }); }; -EntityPropertiesCache.prototype.updateEntity = function(entityID) { +EntityPropertiesCache.prototype.updateEntity = function (entityID) { var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); // convert props.userData from a string to an object. @@ -307,14 +315,14 @@ EntityPropertiesCache.prototype.updateEntity = function(entityID) { this.cache[entityID] = props; }; -EntityPropertiesCache.prototype.getEntities = function() { +EntityPropertiesCache.prototype.getEntities = function () { return Object.keys(this.cache); }; -EntityPropertiesCache.prototype.getProps = function(entityID) { +EntityPropertiesCache.prototype.getProps = function (entityID) { var obj = this.cache[entityID]; return obj ? obj : undefined; }; -EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { +EntityPropertiesCache.prototype.getGrabbableProps = function (entityID) { var props = this.cache[entityID]; if (props) { return props.userData.grabbableKey ? props.userData.grabbableKey : DEFAULT_GRABBABLE_DATA; @@ -322,7 +330,7 @@ EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { return undefined; } }; -EntityPropertiesCache.prototype.getGrabProps = function(entityID) { +EntityPropertiesCache.prototype.getGrabProps = function (entityID) { var props = this.cache[entityID]; if (props) { return props.userData.grabKey ? props.userData.grabKey : {}; @@ -330,7 +338,7 @@ EntityPropertiesCache.prototype.getGrabProps = function(entityID) { return undefined; } }; -EntityPropertiesCache.prototype.getWearableProps = function(entityID) { +EntityPropertiesCache.prototype.getWearableProps = function (entityID) { var props = this.cache[entityID]; if (props) { return props.userData.wearable ? props.userData.wearable : {}; @@ -338,7 +346,7 @@ EntityPropertiesCache.prototype.getWearableProps = function(entityID) { return undefined; } }; -EntityPropertiesCache.prototype.getEquipHotspotsProps = function(entityID) { +EntityPropertiesCache.prototype.getEquipHotspotsProps = function (entityID) { var props = this.cache[entityID]; if (props) { return props.userData.equipHotspots ? props.userData.equipHotspots : {}; @@ -356,7 +364,7 @@ function MyController(hand) { this.getHandPosition = MyAvatar.getLeftPalmPosition; // this.getHandRotation = MyAvatar.getLeftPalmRotation; } - this.getHandRotation = function() { + this.getHandRotation = function () { var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; return Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(controllerHandInput).rotation); }; @@ -398,16 +406,18 @@ function MyController(hand) { this.entityPropertyCache = new EntityPropertiesCache(); + this.equipOverlayInfoSetMap = {}; + var _this = this; var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; - this.ignoreInput = function() { + this.ignoreInput = function () { // We've made the decision to use 'this' for new code, even though it is fragile, // in order to keep/ the code uniform without making any no-op line changes. return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); }; - this.update = function(deltaTime) { + this.update = function (deltaTime) { this.updateSmoothedTrigger(); @@ -429,12 +439,12 @@ function MyController(hand) { } }; - this.callEntityMethodOnGrabbed = function(entityMethodName) { + this.callEntityMethodOnGrabbed = function (entityMethodName) { var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(this.grabbedEntity, entityMethodName, args); }; - this.setState = function(newState, reason) { + this.setState = function (newState, reason) { if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); @@ -467,7 +477,7 @@ function MyController(hand) { } }; - this.debugLine = function(closePoint, farPoint, color) { + this.debugLine = function (closePoint, farPoint, color) { Entities.addEntity({ type: "Line", name: "Grab Debug Entity", @@ -487,7 +497,7 @@ function MyController(hand) { }); }; - this.lineOn = function(closePoint, farPoint, color) { + this.lineOn = function (closePoint, farPoint, color) { // draw a line if (this.pointer === null) { this.pointer = Entities.addEntity({ @@ -519,7 +529,7 @@ function MyController(hand) { }; var SEARCH_SPHERE_ALPHA = 0.5; - this.searchSphereOn = function(location, size, color) { + this.searchSphereOn = function (location, size, color) { if (this.searchSphere === null) { var sphereProperties = { position: location, @@ -542,10 +552,10 @@ function MyController(hand) { } }; - this.overlayLineOn = function(closePoint, farPoint, color) { + this.overlayLineOn = function (closePoint, farPoint, color) { if (this.overlayLine === null) { var lineProperties = { - lineWidth: 5, + glow: 1.0, start: closePoint, end: farPoint, color: color, @@ -570,7 +580,7 @@ function MyController(hand) { } }; - this.searchIndicatorOn = function(distantPickRay) { + this.searchIndicatorOn = function (distantPickRay) { var handPosition = distantPickRay.origin; var SEARCH_SPHERE_SIZE = 0.011; var SEARCH_SPHERE_FOLLOW_RATE = 0.50; @@ -591,7 +601,7 @@ function MyController(hand) { } }; - this.handleDistantParticleBeam = function(handPosition, objectPosition, color) { + this.handleDistantParticleBeam = function (handPosition, objectPosition, color) { var handToObject = Vec3.subtract(objectPosition, handPosition); var finalRotationObject = Quat.rotationBetween(Vec3.multiply(-1, Vec3.UP), handToObject); @@ -607,7 +617,7 @@ function MyController(hand) { } }; - this.createParticleBeam = function(positionObject, orientationObject, color, speed, spread, lifespan) { + this.createParticleBeam = function (positionObject, orientationObject, color, speed, spread, lifespan) { var particleBeamPropertiesObject = { type: "ParticleEffect", @@ -661,7 +671,7 @@ function MyController(hand) { this.particleBeamObject = Entities.addEntity(particleBeamPropertiesObject); }; - this.updateParticleBeam = function(positionObject, orientationObject, color, speed, spread, lifespan) { + this.updateParticleBeam = function (positionObject, orientationObject, color, speed, spread, lifespan) { Entities.editEntity(this.particleBeamObject, { rotation: orientationObject, position: positionObject, @@ -673,7 +683,7 @@ function MyController(hand) { }); }; - this.evalLightWorldTransform = function(modelPos, modelRot) { + this.evalLightWorldTransform = function (modelPos, modelRot) { var MODEL_LIGHT_POSITION = { x: 0, @@ -693,7 +703,7 @@ function MyController(hand) { }; }; - this.handleSpotlight = function(parentID) { + this.handleSpotlight = function (parentID) { var LIFETIME = 100; var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']); @@ -730,7 +740,7 @@ function MyController(hand) { } }; - this.handlePointLight = function(parentID) { + this.handlePointLight = function (parentID) { var LIFETIME = 100; var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']); @@ -762,21 +772,21 @@ function MyController(hand) { } }; - this.lineOff = function() { + this.lineOff = function () { if (this.pointer !== null) { Entities.deleteEntity(this.pointer); } this.pointer = null; }; - this.overlayLineOff = function() { + this.overlayLineOff = function () { if (this.overlayLine !== null) { Overlays.deleteOverlay(this.overlayLine); } this.overlayLine = null; }; - this.searchSphereOff = function() { + this.searchSphereOff = function () { if (this.searchSphere !== null) { Overlays.deleteOverlay(this.searchSphere); this.searchSphere = null; @@ -785,14 +795,14 @@ function MyController(hand) { } }; - this.particleBeamOff = function() { + this.particleBeamOff = function () { if (this.particleBeamObject !== null) { Entities.deleteEntity(this.particleBeamObject); this.particleBeamObject = null; } }; - this.turnLightsOff = function() { + this.turnLightsOff = function () { if (this.spotlight !== null) { Entities.deleteEntity(this.spotlight); this.spotlight = null; @@ -804,7 +814,7 @@ function MyController(hand) { } }; - this.turnOffVisualizations = function() { + this.turnOffVisualizations = function () { if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); } @@ -821,63 +831,62 @@ function MyController(hand) { }; - this.triggerPress = function(value) { + this.triggerPress = function (value) { _this.rawTriggerValue = value; }; - this.secondaryPress = function(value) { + this.secondaryPress = function (value) { _this.rawSecondaryValue = value; }; - this.updateSmoothedTrigger = function() { + this.updateSmoothedTrigger = function () { var triggerValue = this.rawTriggerValue; // smooth out trigger value this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); }; - this.triggerSmoothedGrab = function() { + this.triggerSmoothedGrab = function () { return this.triggerValue > TRIGGER_GRAB_VALUE; }; - this.triggerSmoothedSqueezed = function() { + this.triggerSmoothedSqueezed = function () { return this.triggerValue > TRIGGER_ON_VALUE; }; - this.triggerSmoothedReleased = function() { + this.triggerSmoothedReleased = function () { return this.triggerValue < TRIGGER_OFF_VALUE; }; - this.secondarySqueezed = function() { + this.secondarySqueezed = function () { return _this.rawSecondaryValue > BUMPER_ON_VALUE; }; - this.secondaryReleased = function() { + this.secondaryReleased = function () { return _this.rawSecondaryValue < BUMPER_ON_VALUE; }; - // this.triggerOrsecondarySqueezed = function() { + // this.triggerOrsecondarySqueezed = function () { // return triggerSmoothedSqueezed() || secondarySqueezed(); // } - // this.triggerAndSecondaryReleased = function() { + // this.triggerAndSecondaryReleased = function () { // return triggerSmoothedReleased() && secondaryReleased(); // } - this.thumbPress = function(value) { + this.thumbPress = function (value) { _this.rawThumbValue = value; }; - this.thumbPressed = function() { + this.thumbPressed = function () { return _this.rawThumbValue > THUMB_ON_VALUE; }; - this.thumbReleased = function() { + this.thumbReleased = function () { return _this.rawThumbValue < THUMB_ON_VALUE; }; - this.off = function() { - + this.off = function () { if (this.triggerSmoothedReleased()) { this.waitForTriggerRelease = false; } @@ -887,165 +896,129 @@ function MyController(hand) { this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; if (this.triggerSmoothedSqueezed()) { this.setState(STATE_SEARCHING, "trigger squeeze detected"); + return; } } - }; - this.createHotspots = function() { - var _this = this; - - var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; - var HAND_EQUIP_SPHERE_ALPHA = 0.7; - var HAND_EQUIP_SPHERE_RADIUS = 0.01; - - var HAND_GRAB_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; - var HAND_GRAB_SPHERE_ALPHA = 0.3; - var HAND_GRAB_SPHERE_RADIUS = NEAR_GRAB_RADIUS; - - var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; - var EQUIP_SPHERE_ALPHA = 0.3; - - var GRAB_BOX_COLOR = { red: 90, green: 90, blue: 255 }; - var GRAB_BOX_ALPHA = 0.1; - - this.hotspotOverlays = []; - - var overlay; - if (DRAW_HAND_SPHERES) { - // add tiny green sphere around the palm. - var handPosition = this.getHandPosition(); - overlay = Overlays.addOverlay("sphere", { - position: handPosition, - size: HAND_EQUIP_SPHERE_RADIUS * 2, - color: HAND_EQUIP_SPHERE_COLOR, - alpha: HAND_EQUIP_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - }); - this.hotspotOverlays.push({ - entityID: undefined, - overlay: overlay, - type: "hand" - }); - - // add larger blue sphere around the palm. - overlay = Overlays.addOverlay("sphere", { - position: handPosition, - size: HAND_GRAB_SPHERE_RADIUS * 2, - color: HAND_GRAB_SPHERE_COLOR, - alpha: HAND_GRAB_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - }); - this.hotspotOverlays.push({ - entityID: undefined, - overlay: overlay, - type: "hand", - localPosition: {x: 0, y: 0, z: 0} - }); - } - - // find entities near the avatar that might be equipable. this.entityPropertyCache.clear(); - this.entityPropertyCache.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); + this.entityPropertyCache.findEntities(this.getHandPosition(), EQUIP_HOTSPOT_RENDER_RADIUS); + var candidateEntities = this.entityPropertyCache.getEntities(); - if (DRAW_GRAB_BOXES) { - // add blue box overlays for grabbable entities. - this.entityPropertyCache.getEntities().forEach(function(entityID) { - var props = _this.entityPropertyCache.getProps(entityID); - if (_this.entityIsGrabbable(entityID)) { - var overlay = Overlays.addOverlay("cube", { - rotation: props.rotation, - position: props.position, - size: props.dimensions, - color: GRAB_BOX_COLOR, - alpha: GRAB_BOX_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - }); - _this.hotspotOverlays.push({ - entityID: entityID, - overlay: overlay, - type: "near", - localPosition: {x: 0, y: 0, z: 0} - }); - } - }); + var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); + if (!this.waitForTriggerRelease) { + this.updateEquipHaptics(potentialEquipHotspot); } - // add green spheres for each equippable hotspot. - flatten(this.entityPropertyCache.getEntities().map(function(entityID) { - return _this.collectEquipHotspots(entityID); - })).filter(function(hotspot) { - return _this.hotspotIsEquippable(hotspot); - }).forEach(function(hotspot) { - var overlay = Overlays.addOverlay("sphere", { - position: hotspot.worldPosition, - size: hotspot.radius * 2, - color: EQUIP_SPHERE_COLOR, - alpha: EQUIP_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false - }); - _this.hotspotOverlays.push({ - entityID: hotspot.entityID, - overlay: overlay, - type: "equip", - localPosition: hotspot.localPosition + var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); + this.updateEquipHotspotRendering(nearEquipHotspots, potentialEquipHotspot); + }; + + this.clearEquipHaptics = function () { + this.prevPotentialEquipHotspot = null; + }; + + this.updateEquipHaptics = function (potentialEquipHotspot) { + if (potentialEquipHotspot && !this.prevPotentialEquipHotspot || + !potentialEquipHotspot && this.prevPotentialEquipHotspot) { + Controller.triggerShortHapticPulse(0.5, this.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.updateHotspots = function() { + this.deleteOverlayInfoSet = function (overlayInfoSet) { + overlayInfoSet.overlays.forEach(function (overlay) { + Overlays.deleteOverlay(overlay); + }); + }; + + this.updateEquipHotspotRendering = function (hotspots, potentialEquipHotspot) { + var now = Date.now(); var _this = this; - var props; - this.hotspotOverlays.forEach(function(overlayInfo) { - if (overlayInfo.type === "hand") { - Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); - } else if (overlayInfo.type === "equip") { - _this.entityPropertyCache.updateEntity(overlayInfo.entityID); - props = _this.entityPropertyCache.getProps(overlayInfo.entityID); - var entityXform = new Xform(props.rotation, props.position); - Overlays.editOverlay(overlayInfo.overlay, { - position: entityXform.xformPoint(overlayInfo.localPosition), - rotation: props.rotation - }); - } else if (overlayInfo.type === "near") { - _this.entityPropertyCache.updateEntity(overlayInfo.entityID); - props = _this.entityPropertyCache.getProps(overlayInfo.entityID); - Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + + 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); } }); - }; - this.destroyHotspots = function() { - this.hotspotOverlays.forEach(function(overlayInfo) { - Overlays.deleteOverlay(overlayInfo.overlay); - }); - this.hotspotOverlays = []; - }; - - this.searchEnter = function() { - this.createHotspots(); - }; - - this.searchExit = function() { - this.destroyHotspots(); + // 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 // - this.calcRayPickInfo = function(hand) { + this.calcRayPickInfo = function (hand) { var standardControllerValue = (hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; var pose = Controller.getPoseValue(standardControllerValue); @@ -1098,7 +1071,7 @@ function MyController(hand) { } }; - this.entityWantsTrigger = function(entityID) { + this.entityWantsTrigger = function (entityID) { var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); return grabbableProps && grabbableProps.wantsTrigger; }; @@ -1106,13 +1079,14 @@ function MyController(hand) { // 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. - this.collectEquipHotspots = function(entityID) { + this.collectEquipHotspots = function (entityID) { var result = []; var props = this.entityPropertyCache.getProps(entityID); var entityXform = new Xform(props.rotation, props.position); @@ -1123,6 +1097,7 @@ function MyController(hand) { 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), @@ -1135,6 +1110,7 @@ function MyController(hand) { var wearableProps = this.entityPropertyCache.getWearableProps(entityID); if (wearableProps && wearableProps.joints) { result.push({ + key: entityID.toString() + "0", entityID: entityID, localPosition: {x: 0, y: 0, z: 0}, worldPosition: entityXform.pos, @@ -1146,7 +1122,7 @@ function MyController(hand) { return result; }; - this.hotspotIsEquippable = function(hotspot) { + this.hotspotIsEquippable = function (hotspot) { var props = this.entityPropertyCache.getProps(hotspot.entityID); var grabProps = this.entityPropertyCache.getGrabProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); @@ -1165,7 +1141,7 @@ function MyController(hand) { return true; }; - this.entityIsGrabbable = function(entityID) { + this.entityIsGrabbable = function (entityID) { var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); var grabProps = this.entityPropertyCache.getGrabProps(entityID); var props = this.entityPropertyCache.getProps(entityID); @@ -1217,7 +1193,7 @@ function MyController(hand) { return true; }; - this.entityIsDistanceGrabbable = function(entityID, handPosition) { + this.entityIsDistanceGrabbable = function (entityID, handPosition) { if (!this.entityIsGrabbable(entityID)) { return false; } @@ -1254,7 +1230,7 @@ function MyController(hand) { return true; }; - this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { + this.entityIsNearGrabbable = function (entityID, handPosition, maxDistance) { if (!this.entityIsGrabbable(entityID)) { return false; @@ -1275,12 +1251,36 @@ function MyController(hand) { return true; }; - this.search = function() { + this.chooseNearEquipHotspots = function (candidateEntities, distance) { + var equippableHotspots = flatten(candidateEntities.map(function (entityID) { + return _this.collectEquipHotspots(entityID); + })).filter(function (hotspot) { + return (_this.hotspotIsEquippable(hotspot) && + Vec3.distance(hotspot.worldPosition, _this.getHandPosition()) < hotspot.radius + distance); + }); + return equippableHotspots; + }; + + this.chooseBestEquipHotspot = function (candidateEntities) { + var DISTANCE = 0; + var equippableHotspots = this.chooseNearEquipHotspots(candidateEntities, DISTANCE); + if (equippableHotspots.length > 0) { + // sort by distance + equippableHotspots.sort(function (a, b) { + var aDistance = Vec3.distance(a.worldPosition, this.getHandPosition()); + var bDistance = Vec3.distance(b.worldPosition, this.getHandPosition()); + return aDistance - bDistance; + }); + return equippableHotspots[0]; + } else { + return null; + } + }; + + this.search = function () { var _this = this; var name; - this.updateHotspots(); - this.grabbedEntity = null; this.isInitialGrab = false; this.shouldResetParentOnRelease = false; @@ -1298,31 +1298,17 @@ function MyController(hand) { this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS); var candidateEntities = this.entityPropertyCache.getEntities(); - var equippableHotspots = flatten(candidateEntities.map(function(entityID) { - return _this.collectEquipHotspots(entityID); - })).filter(function(hotspot) { - return _this.hotspotIsEquippable(hotspot) && Vec3.distance(hotspot.worldPosition, handPosition) < hotspot.radius; - }); - - var entity; - if (equippableHotspots.length > 0) { - // sort by distance - equippableHotspots.sort(function(a, b) { - var aDistance = Vec3.distance(a.worldPosition, handPosition); - var bDistance = Vec3.distance(b.worldPosition, handPosition); - return aDistance - bDistance; - }); + var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); + if (potentialEquipHotspot) { if (this.triggerSmoothedGrab()) { - this.grabbedHotspot = equippableHotspots[0]; - this.grabbedEntity = equippableHotspots[0].entityID; + this.grabbedHotspot = potentialEquipHotspot; + this.grabbedEntity = potentialEquipHotspot.entityID; this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(this.grabbedEntity).name + "'"); return; - } else { - // TODO: highlight the equippable object? } } - var grabbableEntities = candidateEntities.filter(function(entity) { + var grabbableEntities = candidateEntities.filter(function (entity) { return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_MAX_DISTANCE); }); @@ -1339,9 +1325,10 @@ function MyController(hand) { this.intersectionDistance = 0; } + var entity; if (grabbableEntities.length > 0) { // sort by distance - grabbableEntities.sort(function(a, b) { + 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); return aDistance - bDistance; @@ -1354,11 +1341,10 @@ function MyController(hand) { this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); return; } else { - // TODO: highlight the near-triggerable object? + // potentialNearTriggerEntity = entity; } } else { if (this.triggerSmoothedGrab()) { - var props = this.entityPropertyCache.getProps(entity); var grabProps = this.entityPropertyCache.getGrabProps(entity); var refCount = grabProps.refCount ? grabProps.refCount : 0; @@ -1373,10 +1359,9 @@ function MyController(hand) { this.setState(STATE_NEAR_GRABBING, "near grab '" + name + "'"); return; } else { - // TODO: highlight the grabbable object? + // potentialNearGrabEntity = entity; } } - return; } if (rayPickInfo.entityID) { @@ -1388,7 +1373,7 @@ function MyController(hand) { this.setState(STATE_FAR_TRIGGER, "far trigger '" + name + "'"); return; } else { - // TODO: highlight the far-triggerable object? + // potentialFarTriggerEntity = entity; } } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { if (this.triggerSmoothedGrab() && !isEditing()) { @@ -1396,11 +1381,16 @@ function MyController(hand) { this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); return; } else { - // TODO: highlight the far-grabbable object? + // potentialFarGrabEntity = entity; } } } + this.updateEquipHaptics(potentialEquipHotspot); + + var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); + this.updateEquipHotspotRendering(nearEquipHotspots, potentialEquipHotspot); + // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { this.lineOn(rayPickInfo.searchRay.origin, @@ -1412,7 +1402,7 @@ function MyController(hand) { Reticle.setVisible(false); }; - this.distanceGrabTimescale = function(mass, distance) { + this.distanceGrabTimescale = function (mass, distance) { var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass / DISTANCE_HOLDING_UNITY_MASS * distance / DISTANCE_HOLDING_UNITY_DISTANCE; @@ -1422,11 +1412,14 @@ function MyController(hand) { return timeScale; }; - this.getMass = function(dimensions, density) { + this.getMass = function (dimensions, density) { return (dimensions.x * dimensions.y * dimensions.z) * density; }; - this.distanceHoldingEnter = function() { + this.distanceHoldingEnter = function () { + + this.clearEquipHotspotRendering(); + this.clearEquipHaptics(); // controller pose is in avatar frame var avatarControllerPose = @@ -1485,7 +1478,7 @@ function MyController(hand) { this.previousControllerRotation = controllerRotation; }; - this.distanceHolding = function() { + this.distanceHolding = function () { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("releaseGrab"); this.setState(STATE_OFF, "trigger released"); @@ -1617,7 +1610,7 @@ function MyController(hand) { this.previousControllerRotation = controllerRotation; }; - this.setupHoldAction = function() { + this.setupHoldAction = function () { this.actionID = Entities.addAction("hold", this.grabbedEntity, { hand: this.hand === RIGHT_HAND ? "right" : "left", timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, @@ -1637,7 +1630,7 @@ function MyController(hand) { return true; }; - this.projectVectorAlongAxis = function(position, axisStart, axisEnd) { + this.projectVectorAlongAxis = function (position, axisStart, axisEnd) { var aPrime = Vec3.subtract(position, axisStart); var bPrime = Vec3.subtract(axisEnd, axisStart); var bPrimeMagnitude = Vec3.length(bPrime); @@ -1653,12 +1646,12 @@ function MyController(hand) { return projection; }; - this.dropGestureReset = function() { + this.dropGestureReset = function () { this.fastHandMoveDetected = false; this.fastHandMoveTimer = 0; }; - this.dropGestureProcess = function(deltaTime) { + 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); @@ -1697,12 +1690,16 @@ function MyController(hand) { return (DROP_WITHOUT_SHAKE || this.fastHandMoveDetected) && handIsUpsideDown; }; - this.nearGrabbingEnter = function() { + this.nearGrabbingEnter = function () { this.lineOff(); this.overlayLineOff(); this.dropGestureReset(); + this.clearEquipHaptics(); + this.clearEquipHotspotRendering(); + + Controller.triggerShortHapticPulse(1.0, this.hand); if (this.entityActivated) { var saveGrabbedID = this.grabbedEntity; @@ -1802,7 +1799,7 @@ function MyController(hand) { this.currentAngularVelocity = ZERO_VEC; }; - this.nearGrabbing = function(deltaTime) { + this.nearGrabbing = function (deltaTime) { var dropDetected = this.dropGestureProcess(deltaTime); @@ -1919,15 +1916,23 @@ function MyController(hand) { } }; - this.nearTriggerEnter = function() { + this.nearTriggerEnter = function () { + + this.clearEquipHotspotRendering(); + this.clearEquipHaptics(); + + Controller.triggerShortHapticPulse(1.0, this.hand); this.callEntityMethodOnGrabbed("startNearTrigger"); }; - this.farTriggerEnter = function() { + this.farTriggerEnter = function () { + this.clearEquipHotspotRendering(); + this.clearEquipHaptics(); + this.callEntityMethodOnGrabbed("startFarTrigger"); }; - this.nearTrigger = function() { + this.nearTrigger = function () { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopNearTrigger"); this.setState(STATE_OFF, "trigger released"); @@ -1936,7 +1941,7 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("continueNearTrigger"); }; - this.farTrigger = function() { + this.farTrigger = function () { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopFarTrigger"); this.setState(STATE_OFF, "trigger released"); @@ -1969,11 +1974,11 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("continueFarTrigger"); }; - this.offEnter = function() { + this.offEnter = function () { this.release(); }; - this.release = function() { + this.release = function () { this.turnLightsOff(); this.turnOffVisualizations(); @@ -2011,14 +2016,14 @@ function MyController(hand) { } }; - this.cleanup = function() { + this.cleanup = function () { this.release(); Entities.deleteEntity(this.particleBeamObject); Entities.deleteEntity(this.spotLight); Entities.deleteEntity(this.pointLight); }; - this.heartBeat = function(entityID) { + this.heartBeat = function (entityID) { var now = Date.now(); if (now - this.lastHeartBeat > HEART_BEAT_INTERVAL) { var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); @@ -2028,7 +2033,7 @@ function MyController(hand) { } }; - this.resetAbandonedGrab = function(entityID) { + this.resetAbandonedGrab = function (entityID) { print("cleaning up abandoned grab on " + entityID); var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); data["refCount"] = 1; @@ -2036,7 +2041,7 @@ function MyController(hand) { this.deactivateEntity(entityID, false); }; - this.activateEntity = function(entityID, grabbedProperties, wasLoaded) { + this.activateEntity = function (entityID, grabbedProperties, wasLoaded) { if (this.entityActivated) { return; } @@ -2098,18 +2103,18 @@ function MyController(hand) { return data; }; - this.checkForStrayChildren = function() { + this.checkForStrayChildren = function () { // sometimes things can get parented to a hand and this script is unaware. Search for such entities and // unhook them. var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); - children.forEach(function(childID) { + children.forEach(function (childID) { print("disconnecting stray child of hand: (" + _this.hand + ") " + childID); Entities.editEntity(childID, {parentID: NULL_UUID}); }); }; - this.deactivateEntity = function(entityID, noVelocity) { + this.deactivateEntity = function (entityID, noVelocity) { var deactiveProps; if (!this.entityActivated) { @@ -2194,7 +2199,7 @@ function MyController(hand) { setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); }; - this.getOtherHandController = function() { + this.getOtherHandController = function () { return (this.hand === RIGHT_HAND) ? leftController : rightController; }; } @@ -2236,7 +2241,7 @@ Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Object-Manipulation'); -var handleHandMessages = function(channel, message, sender) { +var handleHandMessages = function (channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') {