diff --git a/.eslintrc.js b/.eslintrc.js index 535abb807e..54ff0a1268 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,6 +55,9 @@ module.exports = { "XMLHttpRequest": false, "location": false, "print": false, + "RayPick": false, + "LaserPointers": false, + "ContextOverlay": false, "module": false }, "rules": { diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index fc15a8539b..caf9cae341 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -176,6 +176,10 @@ void ZoneEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scen _lastRotation = entity->getRotation(); _lastDimensions = entity->getDimensions(); + _keyLightProperties = entity->getKeyLightProperties(); + _stageProperties = entity->getStageProperties(); + _skyboxProperties = entity->getSkyboxProperties(); + #if 0 if (_lastShapeURL != _typedEntity->getCompoundShapeURL()) { @@ -196,14 +200,14 @@ void ZoneEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scen } #endif - updateKeyZoneItemFromEntity(entity); + updateKeyZoneItemFromEntity(); if (sunChanged) { - updateKeySunFromEntity(entity); + updateKeySunFromEntity(); } if (sunChanged || skyboxChanged) { - updateKeyAmbientFromEntity(entity); + updateKeyAmbientFromEntity(); } if (backgroundChanged || skyboxChanged) { updateKeyBackgroundFromEntity(entity); @@ -265,19 +269,19 @@ bool ZoneEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoint return false; } -void ZoneEntityRenderer::updateKeySunFromEntity(const TypedEntityPointer& entity) { +void ZoneEntityRenderer::updateKeySunFromEntity() { const auto& sunLight = editSunLight(); sunLight->setType(model::Light::SUN); sunLight->setPosition(_lastPosition); sunLight->setOrientation(_lastRotation); // Set the keylight - sunLight->setColor(ColorUtils::toVec3(entity->getKeyLightProperties().getColor())); - sunLight->setIntensity(entity->getKeyLightProperties().getIntensity()); - sunLight->setDirection(entity->getKeyLightProperties().getDirection()); + sunLight->setColor(ColorUtils::toVec3(_keyLightProperties.getColor())); + sunLight->setIntensity(_keyLightProperties.getIntensity()); + sunLight->setDirection(_keyLightProperties.getDirection()); } -void ZoneEntityRenderer::updateKeyAmbientFromEntity(const TypedEntityPointer& entity) { +void ZoneEntityRenderer::updateKeyAmbientFromEntity() { const auto& ambientLight = editAmbientLight(); ambientLight->setType(model::Light::AMBIENT); ambientLight->setPosition(_lastPosition); @@ -285,24 +289,24 @@ void ZoneEntityRenderer::updateKeyAmbientFromEntity(const TypedEntityPointer& en // Set the keylight - ambientLight->setAmbientIntensity(entity->getKeyLightProperties().getAmbientIntensity()); + ambientLight->setAmbientIntensity(_keyLightProperties.getAmbientIntensity()); - if (entity->getKeyLightProperties().getAmbientURL().isEmpty()) { - setAmbientURL(entity->getSkyboxProperties().getURL()); + if (_keyLightProperties.getAmbientURL().isEmpty()) { + setAmbientURL(_skyboxProperties.getURL()); } else { - setAmbientURL(entity->getKeyLightProperties().getAmbientURL()); + setAmbientURL(_keyLightProperties.getAmbientURL()); } } void ZoneEntityRenderer::updateKeyBackgroundFromEntity(const TypedEntityPointer& entity) { editBackground(); setBackgroundMode(entity->getBackgroundMode()); - setSkyboxColor(entity->getSkyboxProperties().getColorVec3()); + setSkyboxColor(_skyboxProperties.getColorVec3()); setProceduralUserData(entity->getUserData()); - setSkyboxURL(entity->getSkyboxProperties().getURL()); + setSkyboxURL(_skyboxProperties.getURL()); } -void ZoneEntityRenderer::updateKeyZoneItemFromEntity(const TypedEntityPointer& entity) { +void ZoneEntityRenderer::updateKeyZoneItemFromEntity() { /* TODO: Implement the sun model behavior / Keep this code here for reference, this is how we { // Set the stage diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.h b/libraries/entities-renderer/src/RenderableZoneEntityItem.h index babd35c0d6..80fe393f48 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.h +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.h @@ -41,9 +41,9 @@ protected: virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; private: - void updateKeyZoneItemFromEntity(const TypedEntityPointer& entity); - void updateKeySunFromEntity(const TypedEntityPointer& entity); - void updateKeyAmbientFromEntity(const TypedEntityPointer& entity); + void updateKeyZoneItemFromEntity(); + void updateKeySunFromEntity(); + void updateKeyAmbientFromEntity(); void updateKeyBackgroundFromEntity(const TypedEntityPointer& entity); void updateAmbientMap(); void updateSkyboxMap(); @@ -89,6 +89,10 @@ private: bool _needAmbientUpdate{ true }; bool _needBackgroundUpdate{ true }; + KeyLightPropertyGroup _keyLightProperties; + StagePropertyGroup _stageProperties; + SkyboxPropertyGroup _skyboxProperties; + // More attributes used for rendering: QString _ambientTextureURL; NetworkTexturePointer _ambientTexture; diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 1b220565cd..88e4f3c9e6 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -49,8 +49,10 @@ ZoneEntityItem::ZoneEntityItem(const EntityItemID& entityItemID) : EntityItem(en EntityItemProperties ZoneEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class - - _keyLightProperties.getProperties(properties); + // Contains a QString property, must be synchronized + withReadLock([&] { + _keyLightProperties.getProperties(properties); + }); _stageProperties.getProperties(properties); @@ -58,7 +60,10 @@ EntityItemProperties ZoneEntityItem::getProperties(EntityPropertyFlags desiredPr COPY_ENTITY_PROPERTY_TO_PROPERTIES(compoundShapeURL, getCompoundShapeURL); COPY_ENTITY_PROPERTY_TO_PROPERTIES(backgroundMode, getBackgroundMode); - _skyboxProperties.getProperties(properties); + // Contains a QString property, must be synchronized + withReadLock([&] { + _skyboxProperties.getProperties(properties); + }); COPY_ENTITY_PROPERTY_TO_PROPERTIES(flyingAllowed, getFlyingAllowed); COPY_ENTITY_PROPERTY_TO_PROPERTIES(ghostingAllowed, getGhostingAllowed); @@ -88,8 +93,10 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) { bool ZoneEntityItem::setSubClassProperties(const EntityItemProperties& properties) { bool somethingChanged = EntityItem::setSubClassProperties(properties); // set the properties in our base class - - _keyLightPropertiesChanged = _keyLightProperties.setProperties(properties); + // Contains a QString property, must be synchronized + withWriteLock([&] { + _keyLightPropertiesChanged = _keyLightProperties.setProperties(properties); + }); _stagePropertiesChanged = _stageProperties.setProperties(properties); @@ -101,11 +108,13 @@ bool ZoneEntityItem::setSubClassProperties(const EntityItemProperties& propertie SET_ENTITY_PROPERTY_FROM_PROPERTIES(ghostingAllowed, setGhostingAllowed); SET_ENTITY_PROPERTY_FROM_PROPERTIES(filterURL, setFilterURL); - _skyboxPropertiesChanged = _skyboxProperties.setProperties(properties); + // Contains a QString property, must be synchronized + withWriteLock([&] { + _skyboxPropertiesChanged = _skyboxProperties.setProperties(properties); + }); somethingChanged = somethingChanged || _keyLightPropertiesChanged || _stagePropertiesChanged || _skyboxPropertiesChanged; - return somethingChanged; } @@ -116,8 +125,12 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesRead = 0; const unsigned char* dataAt = data; - int bytesFromKeylight = _keyLightProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, - propertyFlags, overwriteLocalData, _keyLightPropertiesChanged); + int bytesFromKeylight; + withWriteLock([&] { + bytesFromKeylight = _keyLightProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, + propertyFlags, overwriteLocalData, _keyLightPropertiesChanged); + }); + somethingChanged = somethingChanged || _keyLightPropertiesChanged; bytesRead += bytesFromKeylight; dataAt += bytesFromKeylight; @@ -132,8 +145,11 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY(PROP_BACKGROUND_MODE, BackgroundMode, setBackgroundMode); - int bytesFromSkybox = _skyboxProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, - propertyFlags, overwriteLocalData, _skyboxPropertiesChanged); + int bytesFromSkybox; + withWriteLock([&] { + bytesFromSkybox = _skyboxProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, + propertyFlags, overwriteLocalData, _skyboxPropertiesChanged); + }); somethingChanged = somethingChanged || _skyboxPropertiesChanged; bytesRead += bytesFromSkybox; dataAt += bytesFromSkybox; @@ -150,13 +166,18 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); - requestedProperties += _keyLightProperties.getEntityProperties(params); + withReadLock([&] { + requestedProperties += _keyLightProperties.getEntityProperties(params); + }); requestedProperties += PROP_SHAPE_TYPE; requestedProperties += PROP_COMPOUND_SHAPE_URL; requestedProperties += PROP_BACKGROUND_MODE; requestedProperties += _stageProperties.getEntityProperties(params); - requestedProperties += _skyboxProperties.getEntityProperties(params); + + withReadLock([&] { + requestedProperties += _skyboxProperties.getEntityProperties(params); + }); requestedProperties += PROP_FLYING_ALLOWED; requestedProperties += PROP_GHOSTING_ALLOWED; diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 093c2edb64..14e7cd2f40 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -63,12 +63,12 @@ public: QString getCompoundShapeURL() const; virtual void setCompoundShapeURL(const QString& url); - const KeyLightPropertyGroup& getKeyLightProperties() const { return _keyLightProperties; } + KeyLightPropertyGroup getKeyLightProperties() const { return resultWithReadLock([&] { return _keyLightProperties; }); } void setBackgroundMode(BackgroundMode value) { _backgroundMode = value; _backgroundPropertiesChanged = true; } BackgroundMode getBackgroundMode() const { return _backgroundMode; } - const SkyboxPropertyGroup& getSkyboxProperties() const { return _skyboxProperties; } + SkyboxPropertyGroup getSkyboxProperties() const { return resultWithReadLock([&] { return _skyboxProperties; }); } const StagePropertyGroup& getStageProperties() const { return _stageProperties; } bool getFlyingAllowed() const { return _flyingAllowed; } diff --git a/scripts/developer/tests/controllerTableTest.js b/scripts/developer/tests/controllerTableTest.js new file mode 100644 index 0000000000..239c7fd0ce --- /dev/null +++ b/scripts/developer/tests/controllerTableTest.js @@ -0,0 +1,278 @@ +"use strict"; + +/* jslint bitwise: true */ +/* global Script, Entities, MyAvatar, Vec3, Quat, Mat4 */ + +(function() { // BEGIN LOCAL_SCOPE + + // var lifetime = -1; + var lifetime = 600; + var tableSections = 32; + var tableRadius = 9; + + var sectionRelativeRotation = 0; + var sectionRotation = 0; + var sectionRelativeCenterA = 0; + var sectionRelativeCenterB = 0; + var sectionRelativeCenterSign = 0; + var sectionCenterA = 0; + var sectionCenterB = 0; + var sectionCenterSign = 0; + var yFlip = 0; + + var objects = []; + var overlays = []; + + var testNames = [ + "FarActionGrab", + "NearParentGrabEntity", + "NearParentGrabOverlay", + "Clone Entity (dynamic)", + "Clone Entity (non-dynamic" + ]; + + function createCloneDynamicEntity(index) { + createPropsCube(index, false, false, true, true); + createPropsModel(index, false, false, true, true); + createSign(index, "Clone Dynamic Entity"); + }; + + function createCloneEntity(index) { + createPropsCube(index, false, false, true, false); + createPropsModel(index, false, false, true, false); + createSign(index, "Clone Non-Dynamic Entity"); + }; + + function createNearGrabOverlay(index) { + createPropsCubeOverlay(index, false, false, true, true); + createPropsModelOverlay(index, false, false, true, true); + createSign(index, "Near Grab Overlay"); + }; + + function createNearGrabEntity(index) { + createPropsCube(index, false, false, false, false); + createPropsModel(index, false, false, false, false); + createSign(index, "Near Grab Entity"); + }; + + function createFarGrabEntity(index) { + createPropsCube(index, true, false, false, false); + createPropsModel(index, true, false, false, false); + createSign(index, "Far Grab Entity"); + }; + + function createPropsModel(i, dynamic, collisionless, clone, cloneDynamic) { + var propsModel = { + name: "controller-tests model object " + i, + type: "Model", + modelURL: "http://headache.hungry.com/~seth/hifi/controller-tests/color-cube.obj", + + position: sectionCenterA, + rotation: sectionRotation, + + gravity: (dynamic && !collisionless) ? { x: 0, y: -1, z: 0 } : { x: 0, y: 0, z: 0 }, + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true, + cloneLimit: 10, + cloneable: clone, + cloneDynamic: cloneDynamic + }, + controllerTestEntity: true + }), + lifetime: lifetime, + shapeType: "box", + dynamic: dynamic, + collisionless: collisionless + }; + objects.push(Entities.addEntity(propsModel)); + } + + function createPropsModelOverlay(i, dynamic, collisionless, clone, cloneDynamic) { + var propsModel = { + name: "controller-tests model object " + i, + type: "Model", + modelURL: "http://headache.hungry.com/~seth/hifi/controller-tests/color-cube.obj", + url: "http://headache.hungry.com/~seth/hifi/controller-tests/color-cube.obj", + grabbable: true, + position: sectionCenterA, + rotation: sectionRotation, + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true, + }, + controllerTestEntity: true + }), + lifetime: lifetime, + visible: true, + }; + overlays.push(Overlays.addOverlay("model", propsModel)); + } + + + function createPropsCubeOverlay(i, dynamic, collisionless, clone, cloneDynamic) { + var propsCube = { + name: "controller-tests cube object " + i, + type: "Box", + color: { "blue": 200, "green": 10, "red": 20 }, + position: sectionCenterB, + rotation: sectionRotation, + grabbable: true, + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true, + }, + controllerTestEntity: true + }), + lifetime: lifetime, + solid: true, + visible: true, + }; + overlays.push(Overlays.addOverlay("cube", propsCube)); + } + + function createPropsCube(i, dynamic, collisionless, clone, cloneDynamic) { + var propsCube = { + name: "controller-tests cube object " + i, + type: "Box", + shape: "Cube", + color: { "blue": 200, "green": 10, "red": 20 }, + position: sectionCenterB, + rotation: sectionRotation, + gravity: dynamic ? { x: 0, y: -1, z: 0 } : { x: 0, y: 0, z: 0 }, + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true, + cloneLimit: 10, + cloneable: clone, + cloneDynamic: cloneDynamic + }, + controllerTestEntity: true + }), + lifetime: lifetime, + shapeType: "box", + dynamic: dynamic, + collisionless: collisionless + }; + objects.push(Entities.addEntity(propsCube)); + } + + function createSign(i, signText) { + var propsLabel = { + name: "controller-tests sign " + i, + type: "Text", + lineHeight: 0.125, + position: sectionCenterSign, + rotation: Quat.multiply(sectionRotation, yFlip), + text: signText, + dimensions: { x: 1, y: 1, z: 0.01 }, + lifetime: lifetime, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false, + }, + controllerTestEntity: true + }) + }; + objects.push(Entities.addEntity(propsLabel)); + } + + function chooseType(index) { + switch (index) { + case 0: + createFarGrabEntity(index); + break; + case 1: + createNearGrabEntity(index); + break; + case 2: + createNearGrabOverlay(index); + break; + case 3: + createCloneDynamicEntity(); + break; + case 4: + createCloneEntity(index); + break; + } + } + + function setupControllerTests(testBaseTransform) { + // var tableID = + objects.push(Entities.addEntity({ + name: "controller-tests table", + type: "Model", + modelURL: "http://headache.hungry.com/~seth/hifi/controller-tests/controller-tests-table.obj.gz", + position: Mat4.transformPoint(testBaseTransform, { x: 0, y: 1, z: 0 }), + rotation: Mat4.extractRotation(testBaseTransform), + userData: JSON.stringify({ + grabbableKey: { grabbable: false }, + soundKey: { + url: "http://headache.hungry.com/~seth/hifi/sound/clock-ticking-3.wav", + volume: 0.4, + loop: true, + playbackGap: 0, + playbackGapRange: 0 + }, + controllerTestEntity: true + }), + shapeType: "static-mesh", + lifetime: lifetime + })); + + var Xdynamic = 1; + var Xcollisionless = 2; + var Xkinematic = 4; + var XignoreIK = 8; + + yFlip = Quat.fromPitchYawRollDegrees(0, 180, 0); + + for (var i = 0; i < 16; i++) { + sectionRelativeRotation = Quat.fromPitchYawRollDegrees(0, -360 * i / tableSections, 0); + sectionRotation = Quat.multiply(Mat4.extractRotation(testBaseTransform), sectionRelativeRotation); + sectionRelativeCenterA = Vec3.multiplyQbyV(sectionRotation, { x: -0.2, y: 1.25, z: tableRadius - 0.8 }); + sectionRelativeCenterB = Vec3.multiplyQbyV(sectionRotation, { x: 0.2, y: 1.25, z: tableRadius - 0.8 }); + sectionRelativeCenterSign = Vec3.multiplyQbyV(sectionRotation, { x: 0, y: 1.5, z: tableRadius + 1.0 }); + sectionCenterA = Mat4.transformPoint(testBaseTransform, sectionRelativeCenterA); + sectionCenterB = Mat4.transformPoint(testBaseTransform, sectionRelativeCenterB); + sectionCenterSign = Mat4.transformPoint(testBaseTransform, sectionRelativeCenterSign); + + var dynamic = (i & Xdynamic) ? true : false; + var collisionless = (i & Xcollisionless) ? true : false; + var kinematic = (i & Xkinematic) ? true : false; + var ignoreIK = (i & XignoreIK) ? true : false; + + chooseType(i); + } + } + + // This assumes the avatar is standing on a flat floor with plenty of space. + // Find the floor: + var pickRay = { + origin: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 2, z: -1 })), + direction: { x: 0, y: -1, z: 0 }, + length: 20 + }; + var intersection = Entities.findRayIntersection(pickRay, true, [], [], true); + + if (intersection.intersects) { + var testBaseTransform = Mat4.createFromRotAndTrans(MyAvatar.rotation, intersection.intersection); + setupControllerTests(testBaseTransform); + } + + Script.scriptEnding.connect(function () { + for (var i = 0; i < objects.length; i++) { + var nearbyID = objects[i]; + Entities.deleteEntity(nearbyID); + } + + for (var i = 0; i < overlays.length; i++) { + var overlayID = overlays[i]; + Overlays.deleteOverlay(overlayID); + } + }); +}()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js new file mode 100644 index 0000000000..990f156ba8 --- /dev/null +++ b/scripts/system/controllers/controllerDispatcher.js @@ -0,0 +1,439 @@ +"use strict"; + +// controllerDispatcher.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* jslint bitwise: true */ + +/* global Script, Entities, Overlays, Controller, Vec3, Quat, getControllerWorldLocation, RayPick, + controllerDispatcherPlugins:true, controllerDispatcherPluginsNeedSort:true, + LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES, + getGrabPointSphereOffset, HMD, MyAvatar, Messages +*/ + +controllerDispatcherPlugins = {}; +controllerDispatcherPluginsNeedSort = false; + +Script.include("/~/system/libraries/utils.js"); +Script.include("/~/system/libraries/controllers.js"); +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +(function() { + var NEAR_MAX_RADIUS = 0.1; + + var TARGET_UPDATE_HZ = 60; // 50hz good enough, but we're using update + var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ; + + var PROFILE = false; + + if (typeof Test !== "undefined") { + PROFILE = true; + } + + function ControllerDispatcher() { + var _this = this; + this.lastInterval = Date.now(); + this.intervalCount = 0; + this.totalDelta = 0; + this.totalVariance = 0; + this.highVarianceCount = 0; + this.veryhighVarianceCount = 0; + this.tabletID = null; + this.blacklist = []; + + // a module can occupy one or more "activity" slots while it's running. If all the required slots for a module are + // not set to false (not in use), a module cannot start. When a module is using a slot, that module's name + // is stored as the value, rather than false. + this.activitySlots = { + leftHand: false, + rightHand: false, + rightHandTrigger: false, + leftHandTrigger: false, + rightHandEquip: false, + leftHandEquip: false, + mouse: false + }; + + this.slotsAreAvailableForPlugin = function (plugin) { + for (var i = 0; i < plugin.parameters.activitySlots.length; i++) { + if (_this.activitySlots[plugin.parameters.activitySlots[i]]) { + return false; // something is already using a slot which _this plugin requires + } + } + return true; + }; + + this.markSlots = function (plugin, pluginName) { + for (var i = 0; i < plugin.parameters.activitySlots.length; i++) { + _this.activitySlots[plugin.parameters.activitySlots[i]] = pluginName; + } + }; + + this.unmarkSlotsForPluginName = function (runningPluginName) { + // this is used to free activity-slots when a plugin is deactivated while it's running. + for (var activitySlot in _this.activitySlots) { + if (activitySlot.hasOwnProperty(activitySlot) && _this.activitySlots[activitySlot] === runningPluginName) { + _this.activitySlots[activitySlot] = false; + } + } + }; + + this.runningPluginNames = {}; + this.leftTriggerValue = 0; + this.leftTriggerClicked = 0; + this.rightTriggerValue = 0; + this.rightTriggerClicked = 0; + this.leftSecondaryValue = 0; + this.rightSecondaryValue = 0; + + this.leftTriggerPress = function (value) { + _this.leftTriggerValue = value; + }; + this.leftTriggerClick = function (value) { + _this.leftTriggerClicked = value; + }; + this.rightTriggerPress = function (value) { + _this.rightTriggerValue = value; + }; + this.rightTriggerClick = function (value) { + _this.rightTriggerClicked = value; + }; + this.leftSecondaryPress = function (value) { + _this.leftSecondaryValue = value; + }; + this.rightSecondaryPress = function (value) { + _this.rightSecondaryValue = value; + }; + + + this.dataGatherers = {}; + this.dataGatherers.leftControllerLocation = function () { + return getControllerWorldLocation(Controller.Standard.LeftHand, true); + }; + this.dataGatherers.rightControllerLocation = function () { + return getControllerWorldLocation(Controller.Standard.RightHand, true); + }; + + this.updateTimings = function () { + _this.intervalCount++; + var thisInterval = Date.now(); + var deltaTimeMsec = thisInterval - _this.lastInterval; + var deltaTime = deltaTimeMsec / 1000; + _this.lastInterval = thisInterval; + + _this.totalDelta += deltaTimeMsec; + + var variance = Math.abs(deltaTimeMsec - BASIC_TIMER_INTERVAL_MS); + _this.totalVariance += variance; + + if (variance > 1) { + _this.highVarianceCount++; + } + + if (variance > 5) { + _this.veryhighVarianceCount++; + } + + return deltaTime; + }; + + this.setIgnoreTablet = function() { + if (HMD.tabletID !== _this.tabletID) { + RayPick.setIgnoreOverlays(_this.leftControllerRayPick, [HMD.tabletID]); + RayPick.setIgnoreOverlays(_this.rightControllerRayPick, [HMD.tabletID]); + } + }; + + this.update = function () { + if (PROFILE) { + Script.beginProfileRange("dispatch.pre"); + } + var deltaTime = _this.updateTimings(); + _this.setIgnoreTablet(); + + if (controllerDispatcherPluginsNeedSort) { + _this.orderedPluginNames = []; + for (var pluginName in controllerDispatcherPlugins) { + if (controllerDispatcherPlugins.hasOwnProperty(pluginName)) { + _this.orderedPluginNames.push(pluginName); + } + } + _this.orderedPluginNames.sort(function (a, b) { + return controllerDispatcherPlugins[a].parameters.priority - + controllerDispatcherPlugins[b].parameters.priority; + }); + + var output = "controllerDispatcher -- new plugin order: "; + for (var k = 0; k < _this.orderedPluginNames.length; k++) { + var dbgPluginName = _this.orderedPluginNames[k]; + var priority = controllerDispatcherPlugins[dbgPluginName].parameters.priority; + output += dbgPluginName + ":" + priority; + if (k + 1 < _this.orderedPluginNames.length) { + output += ", "; + } + } + + controllerDispatcherPluginsNeedSort = false; + } + + if (PROFILE) { + Script.endProfileRange("dispatch.pre"); + } + + if (PROFILE) { + Script.beginProfileRange("dispatch.gather"); + } + + var controllerLocations = [ + _this.dataGatherers.leftControllerLocation(), + _this.dataGatherers.rightControllerLocation() + ]; + + // find 3d overlays near each hand + var nearbyOverlayIDs = []; + var h; + for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { + if (controllerLocations[h].valid) { + var nearbyOverlays = Overlays.findOverlays(controllerLocations[h].position, NEAR_MAX_RADIUS); + nearbyOverlays.sort(function (a, b) { + var aPosition = Overlays.getProperty(a, "position"); + var aDistance = Vec3.distance(aPosition, controllerLocations[h].position); + var bPosition = Overlays.getProperty(b, "position"); + var bDistance = Vec3.distance(bPosition, controllerLocations[h].position); + return aDistance - bDistance; + }); + nearbyOverlayIDs.push(nearbyOverlays); + } else { + nearbyOverlayIDs.push([]); + } + } + + // find entities near each hand + var nearbyEntityProperties = [[], []]; + var nearbyEntityPropertiesByID = {}; + for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { + if (controllerLocations[h].valid) { + var controllerPosition = controllerLocations[h].position; + var nearbyEntityIDs = Entities.findEntities(controllerPosition, NEAR_MAX_RADIUS); + for (var j = 0; j < nearbyEntityIDs.length; j++) { + var entityID = nearbyEntityIDs[j]; + var props = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); + props.id = entityID; + props.distance = Vec3.distance(props.position, controllerLocations[h].position); + nearbyEntityPropertiesByID[entityID] = props; + nearbyEntityProperties[h].push(props); + } + } + } + + // raypick for each controller + var rayPicks = [ + RayPick.getPrevRayPickResult(_this.leftControllerRayPick), + RayPick.getPrevRayPickResult(_this.rightControllerRayPick) + ]; + var hudRayPicks = [ + RayPick.getPrevRayPickResult(_this.leftControllerHudRayPick), + RayPick.getPrevRayPickResult(_this.rightControllerHudRayPick) + ]; + // if the pickray hit something very nearby, put it into the nearby entities list + for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { + + // XXX find a way to extract searchRay from samuel's stuff + rayPicks[h].searchRay = { + origin: controllerLocations[h].position, + direction: Quat.getUp(controllerLocations[h].orientation), + length: 1000 + }; + + if (rayPicks[h].type === RayPick.INTERSECTED_ENTITY) { + // XXX check to make sure this one isn't already in nearbyEntityProperties? + if (rayPicks[h].distance < NEAR_GRAB_PICK_RADIUS) { + var nearEntityID = rayPicks[h].objectID; + var nearbyProps = Entities.getEntityProperties(nearEntityID, DISPATCHER_PROPERTIES); + nearbyProps.id = nearEntityID; + nearbyProps.distance = rayPicks[h].distance; + nearbyEntityPropertiesByID[nearEntityID] = nearbyProps; + nearbyEntityProperties[h].push(nearbyProps); + } + } + + // sort by distance from each hand + nearbyEntityProperties[h].sort(function (a, b) { + return a.distance - b.distance; + }); + } + + // bundle up all the data about the current situation + var controllerData = { + triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue], + triggerClicks: [_this.leftTriggerClicked, _this.rightTriggerClicked], + secondaryValues: [_this.leftSecondaryValue, _this.rightSecondaryValue], + controllerLocations: controllerLocations, + nearbyEntityProperties: nearbyEntityProperties, + nearbyEntityPropertiesByID: nearbyEntityPropertiesByID, + nearbyOverlayIDs: nearbyOverlayIDs, + rayPicks: rayPicks, + hudRayPicks: hudRayPicks + }; + if (PROFILE) { + Script.endProfileRange("dispatch.gather"); + } + + if (PROFILE) { + Script.beginProfileRange("dispatch.isReady"); + } + // check for plugins that would like to start. ask in order of increasing priority value + for (var pluginIndex = 0; pluginIndex < _this.orderedPluginNames.length; pluginIndex++) { + var orderedPluginName = _this.orderedPluginNames[pluginIndex]; + var candidatePlugin = controllerDispatcherPlugins[orderedPluginName]; + + if (_this.slotsAreAvailableForPlugin(candidatePlugin)) { + if (PROFILE) { + Script.beginProfileRange("dispatch.isReady." + orderedPluginName); + } + var readiness = candidatePlugin.isReady(controllerData, deltaTime); + if (readiness.active) { + // this plugin will start. add it to the list of running plugins and mark the + // activity-slots which this plugin consumes as "in use" + _this.runningPluginNames[orderedPluginName] = true; + _this.markSlots(candidatePlugin, orderedPluginName); + } + if (PROFILE) { + Script.endProfileRange("dispatch.isReady." + orderedPluginName); + } + } + } + if (PROFILE) { + Script.endProfileRange("dispatch.isReady"); + } + + if (PROFILE) { + Script.beginProfileRange("dispatch.run"); + } + // give time to running plugins + for (var runningPluginName in _this.runningPluginNames) { + if (_this.runningPluginNames.hasOwnProperty(runningPluginName)) { + var plugin = controllerDispatcherPlugins[runningPluginName]; + if (!plugin) { + // plugin was deactivated while running. find the activity-slots it was using and make + // them available. + delete _this.runningPluginNames[runningPluginName]; + _this.unmarkSlotsForPluginName(runningPluginName); + } else { + if (PROFILE) { + Script.beginProfileRange("dispatch.run." + runningPluginName); + } + var runningness = plugin.run(controllerData, deltaTime); + if (!runningness.active) { + // plugin is finished running, for now. remove it from the list + // of running plugins and mark its activity-slots as "not in use" + delete _this.runningPluginNames[runningPluginName]; + _this.markSlots(plugin, false); + } + if (PROFILE) { + Script.endProfileRange("dispatch.run." + runningPluginName); + } + } + } + } + if (PROFILE) { + Script.endProfileRange("dispatch.run"); + } + }; + + this.setBlacklist = function() { + RayPick.setIgnoreEntities(_this.leftControllerRayPick, this.blacklist); + RayPick.setIgnoreEntities(_this.rightControllerRayPick, this.blacklist); + + }; + + var MAPPING_NAME = "com.highfidelity.controllerDispatcher"; + var mapping = Controller.newMapping(MAPPING_NAME); + mapping.from([Controller.Standard.RT]).peek().to(_this.rightTriggerPress); + mapping.from([Controller.Standard.RTClick]).peek().to(_this.rightTriggerClick); + mapping.from([Controller.Standard.LT]).peek().to(_this.leftTriggerPress); + mapping.from([Controller.Standard.LTClick]).peek().to(_this.leftTriggerClick); + + mapping.from([Controller.Standard.RB]).peek().to(_this.rightSecondaryPress); + mapping.from([Controller.Standard.LB]).peek().to(_this.leftSecondaryPress); + mapping.from([Controller.Standard.LeftGrip]).peek().to(_this.leftSecondaryPress); + mapping.from([Controller.Standard.RightGrip]).peek().to(_this.rightSecondaryPress); + + Controller.enableMapping(MAPPING_NAME); + + this.leftControllerRayPick = RayPick.createRayPick({ + joint: "_CONTROLLER_LEFTHAND", + filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS, + enabled: true, + maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE, + posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand) + }); + this.leftControllerHudRayPick = RayPick.createRayPick({ + joint: "_CONTROLLER_LEFTHAND", + filter: RayPick.PICK_HUD, + enabled: true, + maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE, + posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand) + }); + this.rightControllerRayPick = RayPick.createRayPick({ + joint: "_CONTROLLER_RIGHTHAND", + filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS, + enabled: true, + maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE, + posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand) + }); + this.rightControllerHudRayPick = RayPick.createRayPick({ + joint: "_CONTROLLER_RIGHTHAND", + filter: RayPick.PICK_HUD, + enabled: true, + maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE, + posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand) + }); + + this.handleHandMessage = function(channel, message, sender) { + var data; + if (sender === MyAvatar.sessionUUID) { + try { + if (channel === 'Hifi-Hand-RayPick-Blacklist') { + data = JSON.parse(message); + var action = data.action; + var id = data.id; + var index = _this.blacklis.indexOf(id); + + if (action === 'add' && index === -1) { + _this.blacklist.push(id); + _this.setBlacklist(); + } + + if (action === 'remove') { + if (index > -1) { + _this.blacklist.splice(index, 1); + _this.setBlacklist(); + } + } + } + + } catch (e) { + print("WARNING: handControllerGrab.js -- error parsing Hifi-Hand-RayPick-Blacklist message: " + message); + } + } + }; + + this.cleanup = function () { + Script.update.disconnect(_this.update); + Controller.disableMapping(MAPPING_NAME); + RayPick.removeRayPick(_this.leftControllerRayPick); + RayPick.removeRayPick(_this.rightControllerRayPick); + RayPick.removeRayPick(_this.rightControllerHudRayPick); + RayPick.removeRayPick(_this.leftControllerHudRayPick); + }; + } + + var controllerDispatcher = new ControllerDispatcher(); + Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); + Messages.messageReceived.connect(controllerDispatcher.handleHandMessage); + Script.scriptEnding.connect(controllerDispatcher.cleanup); + Script.update.connect(controllerDispatcher.update); +}()); diff --git a/scripts/system/controllers/controllerModules/disableOtherModule.js b/scripts/system/controllers/controllerModules/disableOtherModule.js new file mode 100644 index 0000000000..d6079ffafb --- /dev/null +++ b/scripts/system/controllers/controllerModules/disableOtherModule.js @@ -0,0 +1,86 @@ +"use strict"; + +// nearTrigger.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, + enableDispatcherModule, disableDispatcherModule, getGrabbableData, Vec3, + TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, makeRunningValues, NEAR_GRAB_RADIUS, + getEnabledModuleByName +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +(function() { + function DisableModules(hand) { + this.hand = hand; + this.disableModules = false; + this.parameters = makeDispatcherModuleParameters( + 90, + this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], + [], + 100); + + this.isReady = function(controllerData) { + if (this.disableModules) { + return makeRunningValues(true, [], []); + } + return false; + }; + + this.run = function(controllerData) { + var teleportModuleName = this.hand === RIGHT_HAND ? "RightTeleporter" : "LeftTeleporter"; + var teleportModule = getEnabledModuleByName(teleportModuleName); + + if (teleportModule) { + var ready = teleportModule.isReady(controllerData); + if (ready) { + return makeRunningValues(false, [], []); + } + } + if (!this.disablemodules) { + return makeRunningValues(false, [], []); + } + return makeRunningValues(true, [], []); + }; + } + + var leftDisableModules = new DisableModules(LEFT_HAND); + var rightDisableModules = new DisableModules(RIGHT_HAND); + + enableDispatcherModule("LeftDisableModules", leftDisableModules); + enableDispatcherModule("RightDisableModules", rightDisableModules); + this.handleMessage = function(channel, message, sender) { + if (sender === MyAvatar.sessionUUID) { + if (channel === 'Hifi-Hand-Disabler') { + if (message === 'left') { + leftDisableModules.disableModules = true; + } + if (message === 'right') { + rightDisableModules.disableModules = true; + + } + if (message === 'both' || message === 'none') { + if (message === 'both') { + leftDisableModules.disableModules = true; + rightDisableModules.disableModules = true; + } else if (message === 'none') { + leftDisableModules.disableModules = false; + rightDisableModules.disableModules = false; + } + } + } + } + }; + + Messages.subscribe('Hifi-Hand-Disabler'); + this.cleanup = function() { + disableDispatcherModule("LeftDisableModules"); + disableDispatcherModule("RightDisableModules"); + }; + Messages.messageReceived.connect(this.handleMessage); + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js new file mode 100644 index 0000000000..fa1321b168 --- /dev/null +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -0,0 +1,737 @@ +"use strict"; + +// equipEntity.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID, + getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule, + Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, + Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, entityIsCloneable, + cloneEntity, DISPATCHER_PROPERTIES +*/ + +Script.include("/~/system/libraries/Xform.js"); +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); +Script.include("/~/system/libraries/cloneEntityUtils.js"); + + +var DEFAULT_SPHERE_MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx"; +var EQUIP_SPHERE_SCALE_FACTOR = 0.65; + + +// Each overlayInfoSet describes a single equip hotspot. +// It is an object with the following keys: +// timestamp - last time this object was updated, used to delete stale hotspot overlays. +// entityID - entity assosicated with this hotspot +// localPosition - position relative to the entity +// hotspot - hotspot object +// overlays - array of overlay objects created by Overlay.addOverlay() +// currentSize - current animated scale value +// targetSize - the target of our scale animations +// type - "sphere" or "model". +function EquipHotspotBuddy() { + // holds map from {string} hotspot.key to {object} overlayInfoSet. + this.map = {}; + + // array of all hotspots that are highlighed. + this.highlightedHotspots = []; +} +EquipHotspotBuddy.prototype.clear = function() { + var keys = Object.keys(this.map); + for (var i = 0; i < keys.length; i++) { + var overlayInfoSet = this.map[keys[i]]; + this.deleteOverlayInfoSet(overlayInfoSet); + } + this.map = {}; + this.highlightedHotspots = []; +}; +EquipHotspotBuddy.prototype.highlightHotspot = function(hotspot) { + this.highlightedHotspots.push(hotspot.key); +}; +EquipHotspotBuddy.prototype.updateHotspot = function(hotspot, timestamp) { + var overlayInfoSet = this.map[hotspot.key]; + if (!overlayInfoSet) { + // create a new overlayInfoSet + overlayInfoSet = { + timestamp: timestamp, + entityID: hotspot.entityID, + localPosition: hotspot.localPosition, + hotspot: hotspot, + currentSize: 0, + targetSize: 1, + overlays: [] + }; + + var diameter = hotspot.radius * 2; + + // override default sphere with a user specified model, if it exists. + overlayInfoSet.overlays.push(Overlays.addOverlay("model", { + name: "hotspot overlay", + url: hotspot.modelURL ? hotspot.modelURL : DEFAULT_SPHERE_MODEL_URL, + position: hotspot.worldPosition, + rotation: { + x: 0, + y: 0, + z: 0, + w: 1 + }, + dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, + scale: hotspot.modelScale, + ignoreRayIntersection: true + })); + overlayInfoSet.type = "model"; + this.map[hotspot.key] = overlayInfoSet; + } else { + overlayInfoSet.timestamp = timestamp; + } +}; +EquipHotspotBuddy.prototype.updateHotspots = function(hotspots, timestamp) { + var _this = this; + hotspots.forEach(function(hotspot) { + _this.updateHotspot(hotspot, timestamp); + }); + this.highlightedHotspots = []; +}; +EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerData) { + + var HIGHLIGHT_SIZE = 1.1; + var NORMAL_SIZE = 1.0; + + var keys = Object.keys(this.map); + for (var i = 0; i < keys.length; i++) { + var overlayInfoSet = this.map[keys[i]]; + + // this overlayInfo is highlighted. + if (this.highlightedHotspots.indexOf(keys[i]) !== -1) { + overlayInfoSet.targetSize = HIGHLIGHT_SIZE; + } else { + overlayInfoSet.targetSize = NORMAL_SIZE; + } + + // start to fade out this hotspot. + if (overlayInfoSet.timestamp !== timestamp) { + overlayInfoSet.targetSize = 0; + } + + // animate the size. + var SIZE_TIMESCALE = 0.1; + var tau = deltaTime / SIZE_TIMESCALE; + if (tau > 1.0) { + tau = 1.0; + } + overlayInfoSet.currentSize += (overlayInfoSet.targetSize - overlayInfoSet.currentSize) * tau; + + if (overlayInfoSet.timestamp !== timestamp && overlayInfoSet.currentSize <= 0.05) { + // this is an old overlay, that has finished fading out, delete it! + overlayInfoSet.overlays.forEach(Overlays.deleteOverlay); + delete this.map[keys[i]]; + } else { + // update overlay position, rotation to follow the object it's attached to. + var props = controllerData.nearbyEntityPropertiesByID[overlayInfoSet.entityID]; + if (props) { + var entityXform = new Xform(props.rotation, props.position); + var position = entityXform.xformPoint(overlayInfoSet.localPosition); + + var dimensions; + if (overlayInfoSet.type === "sphere") { + dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize * EQUIP_SPHERE_SCALE_FACTOR; + } else { + dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize; + } + + overlayInfoSet.overlays.forEach(function(overlay) { + Overlays.editOverlay(overlay, { + position: position, + rotation: props.rotation, + dimensions: dimensions + }); + }); + } else { + overlayInfoSet.overlays.forEach(Overlays.deleteOverlay); + delete this.map[keys[i]]; + } + } + } +}; + +(function() { + + var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; + + var EQUIP_RADIUS = 0.2; // radius used for palm vs equip-hotspot for equipping. + + var HAPTIC_PULSE_STRENGTH = 1.0; + var HAPTIC_PULSE_DURATION = 13.0; + var HAPTIC_TEXTURE_STRENGTH = 0.1; + var HAPTIC_TEXTURE_DURATION = 3.0; + var HAPTIC_TEXTURE_DISTANCE = 0.002; + var HAPTIC_DEQUIP_STRENGTH = 0.75; + var HAPTIC_DEQUIP_DURATION = 50.0; + + var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing + var TRIGGER_OFF_VALUE = 0.1; + var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab + var BUMPER_ON_VALUE = 0.5; + + + function getWearableData(props) { + var wearable = {}; + try { + if (!props.userDataParsed) { + props.userDataParsed = JSON.parse(props.userData); + } + + wearable = props.userDataParsed.wearable ? props.userDataParsed.wearable : {}; + } catch (err) { + // don't want to spam the logs + } + return wearable; + } + function getEquipHotspotsData(props) { + var equipHotspots = []; + try { + if (!props.userDataParsed) { + props.userDataParsed = JSON.parse(props.userData); + } + + equipHotspots = props.userDataParsed.equipHotspots ? props.userDataParsed.equipHotspots : []; + } catch (err) { + // don't want to spam the logs + } + return equipHotspots; + } + + function getAttachPointSettings() { + try { + var str = Settings.getValue(ATTACH_POINT_SETTINGS); + if (str === "false" || str === "") { + return {}; + } else { + return JSON.parse(str); + } + } catch (err) { + print("Error parsing attachPointSettings: " + err); + return {}; + } + } + + function setAttachPointSettings(attachPointSettings) { + var str = JSON.stringify(attachPointSettings); + Settings.setValue(ATTACH_POINT_SETTINGS, str); + } + + function getAttachPointForHotspotFromSettings(hotspot, hand) { + var attachPointSettings = getAttachPointSettings(); + var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; + var joints = attachPointSettings[hotspot.key]; + if (joints) { + return joints[jointName]; + } else { + return undefined; + } + } + + function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, offsetRotation) { + var attachPointSettings = getAttachPointSettings(); + var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; + var joints = attachPointSettings[hotspot.key]; + if (!joints) { + joints = {}; + attachPointSettings[hotspot.key] = joints; + } + joints[jointName] = [offsetPosition, offsetRotation]; + setAttachPointSettings(attachPointSettings); + } + + function EquipEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.prevHandIsUpsideDown = false; + this.triggerValue = 0; + this.messageGrabEntity = false; + this.grabEntityProps = null; + + this.parameters = makeDispatcherModuleParameters( + 300, + 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. + // * modelURL {string} url for model to use instead of default sphere. + // * modelScale {Vec3} scale factor for model + this.collectEquipHotspots = function(props) { + var result = []; + var entityID = props.id; + var entityXform = new Xform(props.rotation, props.position); + + var equipHotspotsProps = getEquipHotspotsData(props); + if (equipHotspotsProps && equipHotspotsProps.length > 0) { + var i, length = equipHotspotsProps.length; + for (i = 0; i < length; i++) { + var hotspot = equipHotspotsProps[i]; + if (hotspot.position && hotspot.radius && hotspot.joints) { + result.push({ + key: entityID.toString() + i.toString(), + entityID: entityID, + localPosition: hotspot.position, + worldPosition: entityXform.xformPoint(hotspot.position), + radius: hotspot.radius, + joints: hotspot.joints, + modelURL: hotspot.modelURL, + modelScale: hotspot.modelScale + }); + } + } + } else { + var wearableProps = getWearableData(props); + if (wearableProps && wearableProps.joints) { + result.push({ + key: entityID.toString() + "0", + entityID: entityID, + localPosition: { + x: 0, + y: 0, + z: 0 + }, + worldPosition: entityXform.pos, + radius: EQUIP_RADIUS, + joints: wearableProps.joints, + modelURL: null, + modelScale: null + }); + } + } + return result; + }; + + this.hotspotIsEquippable = function(hotspot, controllerData) { + var props = controllerData.nearbyEntityPropertiesByID[hotspot.entityID]; + + var hasParent = true; + if (props.parentID === NULL_UUID) { + hasParent = false; + } + + if (hasParent || entityHasActions(hotspot.entityID)) { + return false; + } + + return true; + }; + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.updateSmoothedTrigger = function(controllerData) { + var triggerValue = controllerData.triggerValues[this.hand]; + // smooth out trigger value + this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); + }; + + this.triggerSmoothedGrab = function() { + return this.triggerClicked; + }; + + this.triggerSmoothedSqueezed = function() { + return this.triggerValue > TRIGGER_ON_VALUE; + }; + + this.triggerSmoothedReleased = function() { + return this.triggerValue < TRIGGER_OFF_VALUE; + }; + + this.secondaryReleased = function() { + return this.rawSecondaryValue < BUMPER_ON_VALUE; + }; + + this.chooseNearEquipHotspots = function(candidateEntityProps, controllerData) { + var _this = this; + var collectedHotspots = flatten(candidateEntityProps.map(function(props) { + return _this.collectEquipHotspots(props); + })); + var controllerLocation = controllerData.controllerLocations[_this.hand]; + var worldControllerPosition = controllerLocation.position; + var equippableHotspots = collectedHotspots.filter(function(hotspot) { + var hotspotDistance = Vec3.distance(hotspot.worldPosition, worldControllerPosition); + return _this.hotspotIsEquippable(hotspot, controllerData) && + hotspotDistance < hotspot.radius; + }); + return equippableHotspots; + }; + + this.cloneHotspot = function(props, controllerData) { + if (entityIsCloneable(props)) { + var worldEntityProps = controllerData.nearbyEntityProperties[this.hand]; + var cloneID = cloneEntity(props, worldEntityProps); + return cloneID; + } + + return null; + }; + + this.chooseBestEquipHotspot = function(candidateEntityProps, controllerData) { + var equippableHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData); + if (equippableHotspots.length > 0) { + // sort by distance; + var controllerLocation = controllerData.controllerLocations[this.hand]; + var worldControllerPosition = controllerLocation.position; + equippableHotspots.sort(function(a, b) { + var aDistance = Vec3.distance(a.worldPosition, worldControllerPosition); + var bDistance = Vec3.distance(b.worldPosition, worldControllerPosition); + return aDistance - bDistance; + }); + return equippableHotspots[0]; + } else { + return null; + } + }; + + this.dropGestureReset = function() { + this.prevHandIsUpsideDown = false; + }; + + this.dropGestureProcess = function (deltaTime) { + var worldHandRotation = getControllerWorldLocation(this.handToController(), true).orientation; + var localHandUpAxis = this.hand === RIGHT_HAND ? { x: 1, y: 0, z: 0 } : { x: -1, y: 0, z: 0 }; + var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); + var DOWN = { x: 0, y: -1, z: 0 }; + + var DROP_ANGLE = Math.PI / 3; + var HYSTERESIS_FACTOR = 1.1; + var ROTATION_ENTER_THRESHOLD = Math.cos(DROP_ANGLE); + var ROTATION_EXIT_THRESHOLD = Math.cos(DROP_ANGLE * HYSTERESIS_FACTOR); + var rotationThreshold = this.prevHandIsUpsideDown ? ROTATION_EXIT_THRESHOLD : ROTATION_ENTER_THRESHOLD; + + var handIsUpsideDown = false; + if (Vec3.dot(worldHandUpAxis, DOWN) > rotationThreshold) { + handIsUpsideDown = true; + } + + if (handIsUpsideDown !== this.prevHandIsUpsideDown) { + this.prevHandIsUpsideDown = handIsUpsideDown; + Controller.triggerHapticPulse(HAPTIC_DEQUIP_STRENGTH, HAPTIC_DEQUIP_DURATION, this.hand); + } + + return handIsUpsideDown; + }; + + this.clearEquipHaptics = function() { + this.prevPotentialEquipHotspot = null; + }; + + this.updateEquipHaptics = function(potentialEquipHotspot, currentLocation) { + if (potentialEquipHotspot && !this.prevPotentialEquipHotspot || + !potentialEquipHotspot && this.prevPotentialEquipHotspot) { + Controller.triggerHapticPulse(HAPTIC_TEXTURE_STRENGTH, HAPTIC_TEXTURE_DURATION, this.hand); + this.lastHapticPulseLocation = currentLocation; + } else if (potentialEquipHotspot && + Vec3.distance(this.lastHapticPulseLocation, currentLocation) > HAPTIC_TEXTURE_DISTANCE) { + Controller.triggerHapticPulse(HAPTIC_TEXTURE_STRENGTH, HAPTIC_TEXTURE_DURATION, this.hand); + this.lastHapticPulseLocation = currentLocation; + } + this.prevPotentialEquipHotspot = potentialEquipHotspot; + }; + + this.startEquipEntity = function (controllerData) { + this.dropGestureReset(); + this.clearEquipHaptics(); + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + + var grabbedProperties = Entities.getEntityProperties(this.targetEntityID); + + // if an object is "equipped" and has a predefined offset, use it. + var offsets = getAttachPointForHotspotFromSettings(this.grabbedHotspot, this.hand); + if (offsets) { + this.offsetPosition = offsets[0]; + this.offsetRotation = offsets[1]; + } else { + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (this.grabbedHotspot.joints[handJointName]) { + this.offsetPosition = this.grabbedHotspot.joints[handJointName][0]; + this.offsetRotation = this.grabbedHotspot.joints[handJointName][1]; + } + } + + var handJointIndex; + if (this.ignoreIK) { + handJointIndex = this.controllerJointIndex; + } else { + handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + } + + var reparentProps = { + parentID: AVATAR_SELF_ID, + parentJointIndex: handJointIndex, + localVelocity: {x: 0, y: 0, z: 0}, + localAngularVelocity: {x: 0, y: 0, z: 0}, + localPosition: this.offsetPosition, + localRotation: this.offsetRotation + }; + + var isClone = false; + if (entityIsCloneable(grabbedProperties)) { + var cloneID = this.cloneHotspot(grabbedProperties, controllerData); + this.targetEntityID = cloneID; + Entities.editEntity(this.targetEntityID, reparentProps); + isClone = true; + } else if (!grabbedProperties.locked) { + Entities.editEntity(this.targetEntityID, reparentProps); + } else { + this.grabbedHotspot = null; + this.targetEntityID = null; + return; + } + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "startEquip", args); + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'equip', + grabbedEntity: this.targetEntityID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + + var _this = this; + var grabEquipCheck = function() { + var args = [_this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(_this.targetEntityID, "startEquip", args); + }; + + if (isClone) { + // 100 ms seems to be sufficient time to force the check even occur after the object has been initialized. + Script.setTimeout(grabEquipCheck, 100); + } + + }; + + this.endEquipEntity = function () { + Entities.editEntity(this.targetEntityID, { + parentID: NULL_UUID, + parentJointIndex: -1 + }); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "releaseEquip", args); + + ensureDynamic(this.targetEntityID); + this.targetEntityID = null; + this.messageGrabEntity = false; + this.grabEntityProps = null; + }; + + this.updateInputs = function (controllerData) { + this.rawTriggerValue = controllerData.triggerValues[this.hand]; + this.triggerClicked = controllerData.triggerClicks[this.hand]; + this.rawSecondaryValue = controllerData.secondaryValues[this.hand]; + this.updateSmoothedTrigger(controllerData); + }; + + this.checkNearbyHotspots = function (controllerData, deltaTime, timestamp) { + this.controllerJointIndex = getControllerJointIndex(this.hand); + + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { + this.waitForTriggerRelease = false; + } + + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldHandPosition = controllerLocation.position; + var candidateEntityProps = controllerData.nearbyEntityProperties[this.hand]; + + + var potentialEquipHotspot = null; + if (this.messageGrabEntity) { + var hotspots = this.collectEquipHotspots(this.grabEntityProps); + if (hotspots.length > -1) { + potentialEquipHotspot = hotspots[0]; + } + } else { + potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntityProps, controllerData); + } + + if (!this.waitForTriggerRelease) { + this.updateEquipHaptics(potentialEquipHotspot, worldHandPosition); + } + + var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData); + equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); + if (potentialEquipHotspot) { + equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); + } + + equipHotspotBuddy.update(deltaTime, timestamp, controllerData); + + // if the potentialHotspot is cloneable, clone it and return it + // if the potentialHotspot os not cloneable and locked return null + + if (potentialEquipHotspot) { + if ((this.triggerSmoothedSqueezed() && !this.waitForTriggerRelease) || this.messageGrabEntity) { + this.grabbedHotspot = potentialEquipHotspot; + this.targetEntityID = this.grabbedHotspot.entityID; + this.startEquipEntity(controllerData); + this.messageGrabEnity = false; + } + return makeRunningValues(true, [potentialEquipHotspot.entityID], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.isTargetIDValid = function() { + var entityProperties = Entities.getEntityProperties(this.targetEntityID); + for (var propertry in entityProperties) { + return true; + } + return false; + }; + + this.isReady = function (controllerData, deltaTime) { + var timestamp = Date.now(); + this.updateInputs(controllerData); + return this.checkNearbyHotspots(controllerData, deltaTime, timestamp); + }; + + this.run = function (controllerData, deltaTime) { + var timestamp = Date.now(); + this.updateInputs(controllerData); + + if (!this.isTargetIDValid()) { + this.endEquipEntity(); + return makeRunningValues(false, [], []); + } + + if (!this.targetEntityID) { + return this.checkNearbyHotspots(controllerData, deltaTime, timestamp); + } + + if (controllerData.secondaryValues[this.hand]) { + // this.secondaryReleased() will always be true when not depressed + // so we cannot simply rely on that for release - ensure that the + // trigger was first "prepared" by being pushed in before the release + this.preparingHoldRelease = true; + } + + if (this.preparingHoldRelease && !controllerData.secondaryValues[this.hand]) { + // we have an equipped object and the secondary trigger was released + // short-circuit the other checks and release it + this.preparingHoldRelease = false; + this.endEquipEntity(); + return makeRunningValues(false, [], []); + } + + var dropDetected = this.dropGestureProcess(deltaTime); + + if (this.triggerSmoothedReleased()) { + this.waitForTriggerRelease = false; + } + + if (dropDetected && this.prevDropDetected !== dropDetected) { + this.waitForTriggerRelease = true; + } + + // highlight the grabbed hotspot when the dropGesture is detected. + if (dropDetected) { + equipHotspotBuddy.updateHotspot(this.grabbedHotspot, timestamp); + equipHotspotBuddy.highlightHotspot(this.grabbedHotspot); + } + + if (dropDetected && !this.waitForTriggerRelease && this.triggerSmoothedGrab()) { + this.waitForTriggerRelease = true; + // store the offset attach points into preferences. + if (this.grabbedHotspot && this.targetEntityID) { + var prefprops = Entities.getEntityProperties(this.targetEntityID, ["localPosition", "localRotation"]); + if (prefprops && prefprops.localPosition && prefprops.localRotation) { + storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, + prefprops.localPosition, prefprops.localRotation); + } + } + + this.endEquipEntity(); + return makeRunningValues(false, [], []); + } + this.prevDropDetected = dropDetected; + + equipHotspotBuddy.update(deltaTime, timestamp, controllerData); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueEquip", args); + + return makeRunningValues(true, [this.targetEntityID], []); + }; + + this.cleanup = function () { + if (this.targetEntityID) { + this.endEquipEntity(); + } + }; + } + + var handleMessage = function(channel, message, sender) { + var data; + if (sender === MyAvatar.sessionUUID) { + if (channel === 'Hifi-Hand-Grab') { + try { + data = JSON.parse(message); + var equipModule = (data.hand === "left") ? leftEquipEntity : rightEquipEntity; + var entityProperties = Entities.getEntityProperties(data.entityID, DISPATCHER_PROPERTIES); + entityProperties.id = data.entityID; + equipModule.setMessageGrabData(entityProperties); + + } catch (e) { + print("WARNING: equipEntity.js -- error parsing Hifi-Hand-Grab message: " + message); + } + } else if (channel === 'Hifi-Hand-Drop') { + if (message === "left") { + leftEquipEntity.endEquipEntity(); + } else if (message === "right") { + rightEquipEntity.endEquipEntity(); + } else if (message === "both") { + leftEquipEntity.endEquipEntity(); + rightEquipEntity.endEquipEntity(); + } + } + } + }; + + Messages.subscribe('Hifi-Hand-Grab'); + Messages.subscribe('Hifi-Hand-Drop'); + Messages.messageReceived.connect(handleMessage); + + var leftEquipEntity = new EquipEntity(LEFT_HAND); + var rightEquipEntity = new EquipEntity(RIGHT_HAND); + + enableDispatcherModule("LeftEquipEntity", leftEquipEntity); + enableDispatcherModule("RightEquipEntity", rightEquipEntity); + + this.cleanup = function () { + leftEquipEntity.cleanup(); + rightEquipEntity.cleanup(); + disableDispatcherModule("LeftEquipEntity"); + disableDispatcherModule("RightEquipEntity"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js new file mode 100644 index 0000000000..80718bc68d --- /dev/null +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -0,0 +1,578 @@ +"use strict"; + +// farActionGrabEntity.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* jslint bitwise: true */ + +/* global Script, Controller, LaserPointers, RayPick, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat, + getGrabPointSphereOffset, getEnabledModuleByName, makeRunningValues, Entities, NULL_UUID, + enableDispatcherModule, disableDispatcherModule, entityIsDistanceGrabbable, + makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, + PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, + AVATAR_SELF_ID, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic, + getControllerWorldLocation, projectOntoEntityXYPlane + +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + + var PICK_WITH_HAND_RAY = true; + + var halfPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var halfEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. + visible: true + }; + var fullPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var fullEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. + visible: true + }; + var holdPath = { + type: "line3d", + color: COLORS_GRAB_DISTANCE_HOLD, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + + var renderStates = [ + {name: "half", path: halfPath, end: halfEnd}, + {name: "full", path: fullPath, end: fullEnd}, + {name: "hold", path: holdPath} + ]; + + var defaultRenderStates = [ + {name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath}, + {name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath}, + {name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath} + ]; + + var GRABBABLE_PROPERTIES = [ + "position", + "registrationPoint", + "rotation", + "gravity", + "collidesWith", + "dynamic", + "collisionless", + "locked", + "name", + "shapeType", + "parentID", + "parentJointIndex", + "density", + "dimensions", + "userData" + ]; + + + function FarActionGrabEntity(hand) { + this.hand = hand; + this.grabbedThingID = null; + this.actionID = null; // action this script created... + this.entityWithContextOverlay = false; + this.contextOverlayTimer = false; + + var ACTION_TTL = 15; // seconds + + var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object + var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position + var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified + var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified + + this.parameters = makeDispatcherModuleParameters( + 550, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.updateLaserPointer = function(controllerData) { + var SEARCH_SPHERE_SIZE = 0.011; + var MIN_SPHERE_SIZE = 0.0005; + var radius = Math.max(1.2 * SEARCH_SPHERE_SIZE * this.intersectionDistance, MIN_SPHERE_SIZE); + var dim = {x: radius, y: radius, z: radius}; + var mode = "hold"; + if (!this.distanceHolding && !this.distanceRotating) { + if (controllerData.triggerClicks[this.hand]) { + mode = "full"; + } else { + mode = "half"; + } + } + + var laserPointerID = PICK_WITH_HAND_RAY ? this.laserPointer : this.headLaserPointer; + if (mode === "full") { + var fullEndToEdit = PICK_WITH_HAND_RAY ? this.fullEnd : fullEnd; + fullEndToEdit.dimensions = dim; + LaserPointers.editRenderState(laserPointerID, mode, {path: fullPath, end: fullEndToEdit}); + } else if (mode === "half") { + var halfEndToEdit = PICK_WITH_HAND_RAY ? this.halfEnd : halfEnd; + halfEndToEdit.dimensions = dim; + LaserPointers.editRenderState(laserPointerID, mode, {path: halfPath, end: halfEndToEdit}); + } + LaserPointers.enableLaserPointer(laserPointerID); + LaserPointers.setRenderState(laserPointerID, mode); + if (this.distanceHolding || this.distanceRotating) { + LaserPointers.setLockEndUUID(laserPointerID, this.grabbedThingID, this.grabbedIsOverlay); + } else { + LaserPointers.setLockEndUUID(laserPointerID, null, false); + } + }; + + this.laserPointerOff = function() { + LaserPointers.disableLaserPointer(this.laserPointer); + LaserPointers.disableLaserPointer(this.headLaserPointer); + }; + + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.distanceGrabTimescale = function(mass, distance) { + var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass / + DISTANCE_HOLDING_UNITY_MASS * distance / + DISTANCE_HOLDING_UNITY_DISTANCE; + if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) { + timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; + } + return timeScale; + }; + + this.getMass = function(dimensions, density) { + return (dimensions.x * dimensions.y * dimensions.z) * density; + }; + + this.startFarGrabAction = function (controllerData, grabbedProperties) { + var controllerLocation = controllerData.controllerLocations[this.hand]; + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + + // transform the position into room space + var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); + var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); + + var now = Date.now(); + + // add the action and initialize some variables + this.currentObjectPosition = grabbedProperties.position; + this.currentObjectRotation = grabbedProperties.rotation; + this.currentObjectTime = now; + this.currentCameraOrientation = Camera.orientation; + + this.grabRadius = this.grabbedDistance; + this.grabRadialVelocity = 0.0; + + // offset between controller vector at the grab radius and the entity position + var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + targetPosition = Vec3.sum(targetPosition, worldControllerPosition); + this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); + + // compute a constant based on the initial conditions which we use below to exaggerate hand motion + // onto the held object + this.radiusScalar = Math.log(this.grabRadius + 1.0); + if (this.radiusScalar < 1.0) { + this.radiusScalar = 1.0; + } + + // compute the mass for the purpose of energy and how quickly to move object + this.mass = this.getMass(grabbedProperties.dimensions, grabbedProperties.density); + var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, grabbedProperties.position)); + var timeScale = this.distanceGrabTimescale(this.mass, distanceToObject); + this.linearTimeScale = timeScale; + this.actionID = Entities.addAction("far-grab", this.grabbedThingID, { + targetPosition: this.currentObjectPosition, + linearTimeScale: timeScale, + targetRotation: this.currentObjectRotation, + angularTimeScale: timeScale, + tag: "far-grab-" + MyAvatar.sessionUUID, + ttl: ACTION_TTL + }); + if (this.actionID === NULL_UUID) { + this.actionID = null; + } + + // XXX + // if (this.actionID !== null) { + // this.callEntityMethodOnGrabbed("startDistanceGrab"); + // } + + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + this.previousRoomControllerPosition = roomControllerPosition; + }; + + this.continueDistanceHolding = function(controllerData) { + var controllerLocation = controllerData.controllerLocations[this.hand]; + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + + // also transform the position into room space + var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); + var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); + + var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, ["position"]); + var now = Date.now(); + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + this.currentObjectTime = now; + + // the action was set up when this.distanceHolding was called. update the targets. + var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) * + this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR; + if (radius < 1.0) { + radius = 1.0; + } + + var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition); + var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta); + var handMoved = Vec3.multiply(worldHandDelta, radius); + this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); + + // XXX + // this.callEntityMethodOnGrabbed("continueDistantGrab"); + + // Update radialVelocity + var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime); + var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition)); + var newRadialVelocity = Vec3.dot(lastVelocity, delta); + + var VELOCITY_AVERAGING_TIME = 0.016; + var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME; + if (blendFactor < 0.0) { + blendFactor = 0.0; + } else if (blendFactor > 1.0) { + blendFactor = 1.0; + } + this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity; + + var RADIAL_GRAB_AMPLIFIER = 10.0; + if (Math.abs(this.grabRadialVelocity) > 0.0) { + this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * + this.grabRadius * RADIAL_GRAB_AMPLIFIER); + } + + // don't let grabRadius go all the way to zero, because it can't come back from that + var MINIMUM_GRAB_RADIUS = 0.1; + if (this.grabRadius < MINIMUM_GRAB_RADIUS) { + this.grabRadius = MINIMUM_GRAB_RADIUS; + } + var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition); + newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition); + + // XXX + // this.maybeScale(grabbedProperties); + + // visualizations + this.updateLaserPointer(controllerData); + + var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition)); + + this.linearTimeScale = (this.linearTimeScale / 2); + if (this.linearTimeScale <= DISTANCE_HOLDING_ACTION_TIMEFRAME) { + this.linearTimeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; + } + var success = Entities.updateAction(this.grabbedThingID, this.actionID, { + targetPosition: newTargetPosition, + linearTimeScale: this.linearTimeScale, + targetRotation: this.currentObjectRotation, + angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject), + ttl: ACTION_TTL + }); + if (!success) { + print("continueDistanceHolding -- updateAction failed: " + this.actionID); + this.actionID = null; + } + + this.previousRoomControllerPosition = roomControllerPosition; + }; + + this.endNearGrabAction = function () { + ensureDynamic(this.grabbedThingID); + this.distanceHolding = false; + this.distanceRotating = false; + Entities.deleteAction(this.grabbedThingID, this.actionID); + this.actionID = null; + this.grabbedThingID = null; + }; + + this.notPointingAtEntity = function(controllerData) { + var intersection = controllerData.rayPicks[this.hand]; + var entityProperty = Entities.getEntityProperties(intersection.objectID); + var entityType = entityProperty.type; + if ((intersection.type === RayPick.INTERSECTED_ENTITY && entityType === "Web") || intersection.type === RayPick.INTERSECTED_OVERLAY) { + return true; + } + return false; + }; + + this.distanceRotate = function(otherFarGrabModule) { + this.distanceRotating = true; + this.distanceHolding = false; + + var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation; + var controllerRotationDelta = Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation)); + // Rotate entity by twice the delta rotation. + controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta); + + // Perform the rotation in the translation controller's action update. + otherFarGrabModule.currentObjectRotation = Quat.multiply(controllerRotationDelta, + otherFarGrabModule.currentObjectRotation); + + // Rotate about the translation controller's target position. + this.offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, this.offsetPosition); + otherFarGrabModule.offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, + otherFarGrabModule.offsetPosition); + + this.previousWorldControllerRotation = worldControllerRotation; + }; + + this.prepareDistanceRotatingData = function(controllerData) { + var intersection = controllerData.rayPicks[this.hand]; + + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + + var grabbedProperties = Entities.getEntityProperties(intersection.objectID, GRABBABLE_PROPERTIES); + this.currentObjectPosition = grabbedProperties.position; + this.grabRadius = intersection.distance; + + // Offset between controller vector at the grab radius and the entity position. + var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + targetPosition = Vec3.sum(targetPosition, worldControllerPosition); + this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); + + // Initial controller rotation. + this.previousWorldControllerRotation = worldControllerRotation; + }; + + this.destroyContextOverlay = function(controllerData) { + if (this.entityWithContextOverlay) { + ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay); + this.entityWithContextOverlay = false; + } + }; + + this.isReady = function (controllerData) { + if (this.notPointingAtEntity(controllerData)) { + return makeRunningValues(false, [], []); + } + + this.distanceHolding = false; + this.distanceRotating = false; + + if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { + this.updateLaserPointer(controllerData); + this.prepareDistanceRotatingData(controllerData); + return makeRunningValues(true, [], []); + } else { + this.destroyContextOverlay(); + return makeRunningValues(false, [], []); + } + }; + + this.isPointingAtUI = function(controllerData) { + var hudRayPickInfo = controllerData.hudRayPicks[this.hand]; + var hudPoint2d = HMD.overlayFromWorldPoint(hudRayPickInfo.intersection); + if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(hudPoint2d || Reticle.position)) { + return true; + } + + return false; + }; + + this.run = function (controllerData) { + if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.notPointingAtEntity(controllerData) || this.isPointingAtUI(controllerData)) { + this.endNearGrabAction(); + this.laserPointerOff(); + return makeRunningValues(false, [], []); + } + + this.updateLaserPointer(controllerData); + + var otherModuleName =this.hand === RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity"; + var otherFarGrabModule = getEnabledModuleByName(otherModuleName); + + // gather up the readiness of the near-grab modules + var nearGrabNames = [ + this.hand === RIGHT_HAND ? "RightScaleAvatar" : "LeftScaleAvatar", + this.hand === RIGHT_HAND ? "RightFarTriggerEntity" : "LeftFarTriggerEntity", + this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity", + this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity", + this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay" + ]; + + var nearGrabReadiness = []; + for (var i = 0; i < nearGrabNames.length; i++) { + var nearGrabModule = getEnabledModuleByName(nearGrabNames[i]); + var ready = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []); + nearGrabReadiness.push(ready); + } + + if (this.actionID) { + // if we are doing a distance grab and the object gets close enough to the controller, + // stop the far-grab so the near-grab or equip can take over. + for (var k = 0; k < nearGrabReadiness.length; k++) { + if (nearGrabReadiness[k].active && nearGrabReadiness[k].targets[0] === this.grabbedThingID) { + this.laserPointerOff(); + this.endNearGrabAction(); + return makeRunningValues(false, [], []); + } + } + + this.continueDistanceHolding(controllerData); + } else { + // if we are doing a distance search and this controller moves into a position + // where it could near-grab something, stop searching. + for (var j = 0; j < nearGrabReadiness.length; j++) { + if (nearGrabReadiness[j].active) { + this.laserPointerOff(); + return makeRunningValues(false, [], []); + } + } + + var rayPickInfo = controllerData.rayPicks[this.hand]; + if (rayPickInfo.type === RayPick.INTERSECTED_ENTITY) { + if (controllerData.triggerClicks[this.hand]) { + var entityID = rayPickInfo.objectID; + var targetProps = Entities.getEntityProperties(entityID, [ + "dynamic", "shapeType", "position", + "rotation", "dimensions", "density", + "userData", "locked", "type" + ]); + + if (entityID !== this.entityWithContextOverlay) { + this.destroyContextOverlay(); + } + + if (entityIsDistanceGrabbable(targetProps)) { + if (!this.distanceRotating) { + this.grabbedThingID = entityID; + this.grabbedDistance = rayPickInfo.distance; + } + + if (otherFarGrabModule.grabbedThingID === this.grabbedThingID && otherFarGrabModule.distanceHolding) { + this.distanceRotate(otherFarGrabModule); + } else { + this.distanceHolding = true; + this.distanceRotating = false; + this.startFarGrabAction(controllerData, targetProps); + } + } + } else if (!this.entityWithContextOverlay && !this.contextOverlayTimer) { + var _this = this; + _this.contextOverlayTimer = Script.setTimeout(function () { + if (!_this.entityWithContextOverlay && _this.contextOverlayTimer) { + var props = Entities.getEntityProperties(rayPickInfo.objectID); + var pointerEvent = { + type: "Move", + id: this.hand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID, rayPickInfo.intersection, props), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.surfaceNormal, + direction: Vec3.subtract(ZERO_VEC, rayPickInfo.surfaceNormal), + button: "Secondary" + }; + if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.objectID, pointerEvent)) { + _this.entityWithContextOverlay = rayPickInfo.objectID; + } + } + _this.contextOverlayTimer = false; + }, 500); + } + } else if (this.distanceRotating) { + this.distanceRotate(otherFarGrabModule); + } + } + return this.exitIfDisabled(); + }; + + this.exitIfDisabled = function() { + var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules"; + var disableModule = getEnabledModuleByName(moduleName); + if (disableModule) { + if (disableModule.disableModules) { + this.laserPointerOff(); + this.endNearGrabAction(); + return makeRunningValues(false, [], []); + } + } + return makeRunningValues(true, [], []); + }; + + this.cleanup = function () { + LaserPointers.disableLaserPointer(this.laserPointer); + LaserPointers.removeLaserPointer(this.laserPointer); + }; + + this.halfEnd = halfEnd; + this.fullEnd = fullEnd; + this.laserPointer = LaserPointers.createLaserPointer({ + joint: (this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", + filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS, + maxDistance: PICK_MAX_DISTANCE, + posOffset: getGrabPointSphereOffset(this.handToController()), + renderStates: renderStates, + faceAvatar: true, + defaultRenderStates: defaultRenderStates + }); + } + + var leftFarActionGrabEntity = new FarActionGrabEntity(LEFT_HAND); + var rightFarActionGrabEntity = new FarActionGrabEntity(RIGHT_HAND); + + enableDispatcherModule("LeftFarActionGrabEntity", leftFarActionGrabEntity); + enableDispatcherModule("RightFarActionGrabEntity", rightFarActionGrabEntity); + + this.cleanup = function () { + leftFarActionGrabEntity.cleanup(); + rightFarActionGrabEntity.cleanup(); + disableDispatcherModule("LeftFarActionGrabEntity"); + disableDispatcherModule("RightFarActionGrabEntity"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/farTrigger.js b/scripts/system/controllers/controllerModules/farTrigger.js new file mode 100644 index 0000000000..38152aac91 --- /dev/null +++ b/scripts/system/controllers/controllerModules/farTrigger.js @@ -0,0 +1,234 @@ +"use strict"; + +// farTrigger.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, Controller, LaserPointers, RayPick, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat, + getGrabPointSphereOffset, getEnabledModuleByName, makeRunningValues, Entities, NULL_UUID, + enableDispatcherModule, disableDispatcherModule, entityIsDistanceGrabbable, + makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, + PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, + AVATAR_SELF_ID, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic, + getControllerWorldLocation, projectOntoEntityXYPlane, getGrabbableData + +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + var halfPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var halfEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. + visible: true + }; + var fullPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var fullEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. + visible: true + }; + var holdPath = { + type: "line3d", + color: COLORS_GRAB_DISTANCE_HOLD, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + + var renderStates = [ + {name: "half", path: halfPath, end: halfEnd}, + {name: "full", path: fullPath, end: fullEnd}, + {name: "hold", path: holdPath} + ]; + + var defaultRenderStates = [ + {name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath}, + {name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath}, + {name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath} + ]; + + function entityWantsNearTrigger(props) { + var grabbableData = getGrabbableData(props); + return grabbableData.triggerable || grabbableData.wantsTrigger; + } + + function FarTriggerEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.grabbing = false; + this.previousParentID = {}; + this.previousParentJointIndex = {}; + this.previouslyUnhooked = {}; + + this.parameters = makeDispatcherModuleParameters( + 520, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.updateLaserPointer = function(controllerData) { + var SEARCH_SPHERE_SIZE = 0.011; + var MIN_SPHERE_SIZE = 0.0005; + var radius = Math.max(1.2 * SEARCH_SPHERE_SIZE * this.intersectionDistance, MIN_SPHERE_SIZE); + var dim = {x: radius, y: radius, z: radius}; + var mode = "none"; + if (controllerData.triggerClicks[this.hand]) { + mode = "full"; + } else { + mode = "half"; + } + + var laserPointerID = this.laserPointer; + if (mode === "full") { + var fullEndToEdit = this.fullEnd; + fullEndToEdit.dimensions = dim; + LaserPointers.editRenderState(laserPointerID, mode, {path: fullPath, end: fullEndToEdit}); + } else if (mode === "half") { + var halfEndToEdit = this.halfEnd; + halfEndToEdit.dimensions = dim; + LaserPointers.editRenderState(laserPointerID, mode, {path: halfPath, end: halfEndToEdit}); + } + LaserPointers.enableLaserPointer(laserPointerID); + LaserPointers.setRenderState(laserPointerID, mode); + }; + + this.laserPointerOff = function() { + LaserPointers.disableLaserPointer(this.laserPointer); + }; + + this.getTargetProps = function (controllerData) { + // nearbyEntityProperties is already sorted by length from controller + var targetEntity = controllerData.rayPicks[this.hand].objectID; + if (targetEntity) { + var targetProperties = Entities.getEntityProperties(targetEntity); + if (entityWantsNearTrigger(targetProperties)) { + return targetProperties; + } + } + return null; + }; + + this.startFarTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "startFarTrigger", args); + }; + + this.continueFarTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueFarTrigger", args); + }; + + this.endFarTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "stopFarTrigger", args); + this.laserPointerOff(); + }; + + this.isReady = function (controllerData) { + this.targetEntityID = null; + if (controllerData.triggerClicks[this.hand] === 0) { + return makeRunningValues(false, [], []); + } + + var targetProps = this.getTargetProps(controllerData); + if (targetProps) { + this.targetEntityID = targetProps.id; + this.startFarTrigger(controllerData); + return makeRunningValues(true, [this.targetEntityID], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData) { + var targetEntity = controllerData.rayPicks[this.hand].objectID; + if (controllerData.triggerClicks[this.hand] === 0 || this.targetEntityID !== targetEntity) { + this.endFarTrigger(controllerData); + return makeRunningValues(false, [], []); + } + + this.updateLaserPointer(controllerData); + this.continueFarTrigger(controllerData); + return makeRunningValues(true, [this.targetEntityID], []); + }; + + this.halfEnd = halfEnd; + this.fullEnd = fullEnd; + this.laserPointer = LaserPointers.createLaserPointer({ + joint: (this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", + filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS, + maxDistance: PICK_MAX_DISTANCE, + posOffset: getGrabPointSphereOffset(this.handToController()), + renderStates: renderStates, + faceAvatar: true, + defaultRenderStates: defaultRenderStates + }); + + this.cleanup = function () { + if (this.targetEntityID) { + this.endFarTrigger(); + } + + LaserPointers.disableLaserPointer(this.laserPointer); + LaserPointers.removeLaserPointer(this.laserPointer); + }; + } + + var leftFarTriggerEntity = new FarTriggerEntity(LEFT_HAND); + var rightFarTriggerEntity = new FarTriggerEntity(RIGHT_HAND); + + enableDispatcherModule("LeftFarTriggerEntity", leftFarTriggerEntity); + enableDispatcherModule("RightFarTriggerEntity", rightFarTriggerEntity); + + this.cleanup = function () { + leftFarTriggerEntity.cleanup(); + rightFarTriggerEntity.cleanup(); + disableDispatcherModule("LeftFarTriggerEntity"); + disableDispatcherModule("RightFarTriggerEntity"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/inEditMode.js b/scripts/system/controllers/controllerModules/inEditMode.js new file mode 100644 index 0000000000..916487277a --- /dev/null +++ b/scripts/system/controllers/controllerModules/inEditMode.js @@ -0,0 +1,265 @@ +"use strict"; + +// inEditMode.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, + NULL_UUID, enableDispatcherModule, disableDispatcherModule, makeRunningValues, + Messages, Quat, Vec3, getControllerWorldLocation, makeDispatcherModuleParameters, Overlays, ZERO_VEC, + AVATAR_SELF_ID, HMD, INCHES_TO_METERS, DEFAULT_REGISTRATION_POINT, Settings, getGrabPointSphereOffset, + COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, + DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_ON_VALUE, TRIGGER_OFF_VALUE, getEnabledModuleByName, PICK_MAX_DISTANCE, + isInEditMode +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); +Script.include("/~/system/libraries/utils.js"); + +(function () { + var halfPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var halfEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. + visible: true + }; + var fullPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var fullEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. + visible: true + }; + var holdPath = { + type: "line3d", + color: COLORS_GRAB_DISTANCE_HOLD, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + + var renderStates = [ + {name: "half", path: halfPath, end: halfEnd}, + {name: "full", path: fullPath, end: fullEnd}, + {name: "hold", path: holdPath} + ]; + + var defaultRenderStates = [ + {name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath}, + {name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath}, + {name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath} + ]; + + function InEditMode(hand) { + this.hand = hand; + this.triggerClicked = false; + this.mode = "none"; + + this.parameters = makeDispatcherModuleParameters( + 160, + this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], + [], + 100); + + this.nearTablet = function(overlays) { + for (var i = 0; i < overlays.length; i++) { + if (overlays[i] === HMD.tabletID) { + return true; + } + } + return false; + }; + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + + this.processControllerTriggers = function(controllerData) { + if (controllerData.triggerClicks[this.hand]) { + this.mode = "full"; + } else if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { + this.mode = "half"; + } else { + this.mode = "none"; + } + }; + + this.updateLaserPointer = function(controllerData) { + var RADIUS = 0.005; + var dim = { x: RADIUS, y: RADIUS, z: RADIUS }; + + if (this.mode === "full") { + this.fullEnd.dimensions = dim; + LaserPointers.editRenderState(this.laserPointer, this.mode, {path: fullPath, end: this.fullEnd}); + } else if (this.mode === "half") { + this.halfEnd.dimensions = dim; + LaserPointers.editRenderState(this.laserPointer, this.mode, {path: halfPath, end: this.halfEnd}); + } + + LaserPointers.enableLaserPointer(this.laserPointer); + LaserPointers.setRenderState(this.laserPointer, this.mode); + }; + + this.pointingAtTablet = function(objectID) { + if (objectID === HMD.tabletScreenID || objectID === HMD.tabletButtonID) { + return true; + } + return false; + }; + + this.sendPickData = function(controllerData) { + if (controllerData.triggerClicks[this.hand] && !this.triggerClicked) { + var intersection = controllerData.rayPicks[this.hand]; + if (intersection.type === RayPick.INTERSECTED_ENTITY) { + Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ + method: "selectEntity", + entityID: intersection.objectID + })); + } else if (intersection.type === RayPick.INTERSECTED_OVERLAY) { + Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ + method: "selectOverlay", + overlayID: intersection.objectID + })); + } + + this.triggerClicked = true; + } else { + this.triggerClicked = false; + } + }; + + this.exitModule = function() { + this.disableLasers(); + return makeRunningValues(false, [], []); + }; + + this.disableLasers = function() { + LaserPointers.disableLaserPointer(this.laserPointer); + }; + + this.isReady = function(controllerData) { + if (isInEditMode()) { + this.triggerClicked = false; + return makeRunningValues(true, [], []); + } + return this.exitModule(); + }; + + this.run = function(controllerData) { + var tabletStylusInput = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightTabletStylusInput" : "LeftTabletStylusInput"); + if (tabletStylusInput) { + var tabletReady = tabletStylusInput.isReady(controllerData); + + if (tabletReady.active) { + return this.exitModule(); + } + } + + var overlayLaser = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightOverlayLaserInput" : "LeftOverlayLaserInput"); + if (overlayLaser) { + var overlayLaserReady = overlayLaser.isReady(controllerData); + + if (overlayLaserReady.active && this.pointingAtTablet(overlayLaser.target)) { + return this.exitModule(); + } + } + + var nearOverlay = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"); + if (nearOverlay) { + var nearOverlayReady = nearOverlay.isReady(controllerData); + + if (nearOverlayReady.active && nearOverlay.grabbedThingID === HMD.tabletID) { + return this.exitModule(); + } + } + + var teleport = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightTeleporter" : "LeftTeleporter"); + if (teleport) { + var teleportReady = teleport.isReady(controllerData); + if (teleportReady.active) { + return this.exitModule(); + } + } + + this.processControllerTriggers(controllerData); + this.updateLaserPointer(controllerData); + this.sendPickData(controllerData); + + + return this.isReady(controllerData); + }; + + this.cleanup = function() { + LaserPointers.disableLaserPointer(this.laserPointer); + LaserPointers.removeLaserPointer(this.laserPointer); + }; + + + this.halfEnd = halfEnd; + this.fullEnd = fullEnd; + + this.laserPointer = LaserPointers.createLaserPointer({ + joint: (this.hand === RIGHT_HAND) ? "_CONTROLLER_RIGHTHAND" : "_CONTROLLER_LEFTHAND", + filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS, + maxDistance: PICK_MAX_DISTANCE, + posOffset: getGrabPointSphereOffset(this.handToController()), + renderStates: renderStates, + faceAvatar: true, + defaultRenderStates: defaultRenderStates + }); + + LaserPointers.setIgnoreOverlays(this.laserPointer, [HMD.tabletID, HMD.tabletButtonID, HMD.tabletScreenID]); + } + + var leftHandInEditMode = new InEditMode(LEFT_HAND); + var rightHandInEditMode = new InEditMode(RIGHT_HAND); + + enableDispatcherModule("LeftHandInEditMode", leftHandInEditMode); + enableDispatcherModule("RightHandInEditMode", rightHandInEditMode); + + this.cleanup = function() { + leftHandInEditMode.cleanup(); + rightHandInEditMode.cleanup(); + disableDispatcherModule("LeftHandInEditMode"); + disableDispatcherModule("RightHandInEditMode"); + }; + + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js new file mode 100644 index 0000000000..399388d614 --- /dev/null +++ b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js @@ -0,0 +1,253 @@ +"use strict"; + +// nearActionGrabEntity.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, + getControllerJointIndex, getGrabbableData, NULL_UUID, enableDispatcherModule, disableDispatcherModule, + propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, entityIsGrabbable, + Quat, Vec3, MSECS_PER_SEC, getControllerWorldLocation, makeDispatcherModuleParameters, makeRunningValues, + TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, findGroupParent, entityIsCloneable, propsAreCloneDynamic, cloneEntity, + HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); +Script.include("/~/system/libraries/cloneEntityUtils.js"); + +(function() { + + function NearActionGrabEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.actionID = null; // action this script created... + this.hapticTargetID = null; + + this.parameters = makeDispatcherModuleParameters( + 500, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position + var ACTION_TTL = 15; // seconds + var ACTION_TTL_REFRESH = 5; + + // XXX does handJointIndex change if the avatar changes? + this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + this.controllerJointIndex = getControllerJointIndex(this.hand); + + + // handPosition is where the avatar's hand appears to be, in-world. + this.getHandPosition = function () { + if (this.hand === RIGHT_HAND) { + return MyAvatar.getRightPalmPosition(); + } else { + return MyAvatar.getLeftPalmPosition(); + } + }; + + this.getHandRotation = function () { + if (this.hand === RIGHT_HAND) { + return MyAvatar.getRightPalmRotation(); + } else { + return MyAvatar.getLeftPalmRotation(); + } + }; + + + this.startNearGrabAction = function (controllerData, targetProps) { + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + + var grabbableData = getGrabbableData(targetProps); + this.ignoreIK = grabbableData.ignoreIK; + this.kinematicGrab = grabbableData.kinematic; + + var handRotation; + var handPosition; + if (this.ignoreIK) { + var controllerID = + (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + var controllerLocation = getControllerWorldLocation(controllerID, false); + handRotation = controllerLocation.orientation; + handPosition = controllerLocation.position; + } else { + handRotation = this.getHandRotation(); + handPosition = this.getHandPosition(); + } + + var objectRotation = targetProps.rotation; + this.offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); + + var currentObjectPosition = targetProps.position; + var offset = Vec3.subtract(currentObjectPosition, handPosition); + this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); + + var now = Date.now(); + this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); + + if (this.actionID) { + Entities.deleteAction(this.targetEntityID, this.actionID); + } + this.actionID = Entities.addAction("hold", this.targetEntityID, { + hand: this.hand === RIGHT_HAND ? "right" : "left", + timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, + relativePosition: this.offsetPosition, + relativeRotation: this.offsetRotation, + ttl: ACTION_TTL, + kinematic: this.kinematicGrab, + kinematicSetVelocity: true, + ignoreIK: this.ignoreIK + }); + if (this.actionID === NULL_UUID) { + this.actionID = null; + return; + } + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'grab', + grabbedEntity: this.targetEntityID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + }; + + // this is for when the action is going to time-out + this.refreshNearGrabAction = function (controllerData) { + var now = Date.now(); + if (this.actionID && this.actionTimeout - now < ACTION_TTL_REFRESH * MSECS_PER_SEC) { + // if less than a 5 seconds left, refresh the actions ttl + var success = Entities.updateAction(this.targetEntityID, this.actionID, { + hand: this.hand === RIGHT_HAND ? "right" : "left", + timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, + relativePosition: this.offsetPosition, + relativeRotation: this.offsetRotation, + ttl: ACTION_TTL, + kinematic: this.kinematicGrab, + kinematicSetVelocity: true, + ignoreIK: this.ignoreIK + }); + if (success) { + this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); + } + } + }; + + this.endNearGrabAction = function () { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args); + + Entities.deleteAction(this.targetEntityID, this.actionID); + this.actionID = null; + + this.targetEntityID = null; + }; + + this.getTargetProps = function (controllerData) { + // nearbyEntityProperties is already sorted by distance from controller + var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; + for (var i = 0; i < nearbyEntityProperties.length; i++) { + var props = nearbyEntityProperties[i]; + var handPosition = controllerData.controllerLocations[this.hand].position; + if (props.distance > NEAR_GRAB_RADIUS) { + break; + } + if (entityIsGrabbable(props) || entityIsCloneable(props)) { + if (props.id !== this.hapticTargetID) { + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + this.hapticTargetID = props.id; + } + // if we've attempted to grab a child, roll up to the root of the tree + var groupRootProps = findGroupParent(controllerData, props); + if (entityIsGrabbable(groupRootProps)) { + return groupRootProps; + } + return props; + } + } + return null; + }; + + this.isReady = function (controllerData) { + this.targetEntityID = null; + + var targetProps = this.getTargetProps(controllerData); + if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { + return makeRunningValues(false, [], []); + } + + if (targetProps) { + if (!propsArePhysical(targetProps) && !propsAreCloneDynamic(targetProps)) { + return makeRunningValues(false, [], []); // let nearParentGrabEntity handle it + } else { + this.targetEntityID = targetProps.id; + return makeRunningValues(true, [this.targetEntityID], []); + } + } else { + this.hapticTargetID = null; + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData) { + if (this.actionID) { + if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { + this.endNearGrabAction(); + this.hapticTargetID = null; + return makeRunningValues(false, [], []); + } + + this.refreshNearGrabAction(controllerData); + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args); + } else { + + // still searching / highlighting + var readiness = this.isReady (controllerData); + if (!readiness.active) { + return readiness; + } + + var targetProps = this.getTargetProps(controllerData); + if (targetProps) { + if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) { + // switch to grabbing + var targetCloneable = entityIsCloneable(targetProps); + if (targetCloneable) { + var worldEntityProps = controllerData.nearbyEntityProperties[this.hand]; + var cloneID = cloneEntity(targetProps, worldEntityProps); + var cloneProps = Entities.getEntityProperties(cloneID); + this.targetEntityID = cloneID; + this.startNearGrabAction(controllerData, cloneProps); + } else { + this.startNearGrabAction(controllerData, targetProps); + } + } + } + } + + return makeRunningValues(true, [this.targetEntityID], []); + }; + + this.cleanup = function () { + if (this.targetEntityID) { + this.endNearGrabAction(); + } + }; + } + + var leftNearActionGrabEntity = new NearActionGrabEntity(LEFT_HAND); + var rightNearActionGrabEntity = new NearActionGrabEntity(RIGHT_HAND); + + enableDispatcherModule("LeftNearActionGrabEntity", leftNearActionGrabEntity); + enableDispatcherModule("RightNearActionGrabEntity", rightNearActionGrabEntity); + + this.cleanup = function () { + leftNearActionGrabEntity.cleanup(); + rightNearActionGrabEntity.cleanup(); + disableDispatcherModule("LeftNearActionGrabEntity"); + disableDispatcherModule("RightNearActionGrabEntity"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js new file mode 100644 index 0000000000..6cd52fef07 --- /dev/null +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -0,0 +1,264 @@ +"use strict"; + +// nearParentGrabEntity.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID, + getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule, + propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, + makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, + findGroupParent, Vec3, cloneEntity, entityIsCloneable, propsAreCloneDynamic, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/cloneEntityUtils.js"); + +(function() { + + // XXX this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true; + // XXX this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC; + + function NearParentingGrabEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.grabbing = false; + this.previousParentID = {}; + this.previousParentJointIndex = {}; + this.previouslyUnhooked = {}; + this.hapticTargetID = null; + + this.parameters = makeDispatcherModuleParameters( + 500, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + + // XXX does handJointIndex change if the avatar changes? + this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + this.controllerJointIndex = getControllerJointIndex(this.hand); + + this.getOtherModule = function() { + return (this.hand === RIGHT_HAND) ? leftNearParentingGrabEntity : rightNearParentingGrabEntity; + }; + + this.otherHandIsParent = function(props) { + return this.getOtherModule().thisHandIsParent(props); + }; + + this.thisHandIsParent = function(props) { + if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== AVATAR_SELF_ID) { + return false; + } + + var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + if (props.parentJointIndex === handJointIndex) { + return true; + } + + var controllerJointIndex = this.controllerJointIndex; + if (props.parentJointIndex === controllerJointIndex) { + return true; + } + + var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + + if (props.parentJointIndex === controllerCRJointIndex) { + return true; + } + + return false; + }; + + this.startNearParentingGrabEntity = function (controllerData, targetProps) { + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + + var handJointIndex; + // if (this.ignoreIK) { + // handJointIndex = this.controllerJointIndex; + // } else { + // handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + // } + handJointIndex = this.controllerJointIndex; + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(targetProps.id, "startNearGrab", args); + + var reparentProps = { + parentID: AVATAR_SELF_ID, + parentJointIndex: handJointIndex, + localVelocity: {x: 0, y: 0, z: 0}, + localAngularVelocity: {x: 0, y: 0, z: 0} + }; + + if (this.thisHandIsParent(targetProps)) { + // this should never happen, but if it does, don't set previous parent to be this hand. + // this.previousParentID[targetProps.id] = NULL; + // this.previousParentJointIndex[targetProps.id] = -1; + } else if (this.otherHandIsParent(targetProps)) { + // the other hand is parent. Steal the object and information + var otherModule = this.getOtherModule(); + this.previousParentID[targetProps.id] = otherModule.previousParentID[targetProps.id]; + this.previousParentJointIndex[targetProps.id] = otherModule.previousParentJointIndex[targetProps.id]; + otherModule.endNearParentingGrabEntity(); + } else { + this.previousParentID[targetProps.id] = targetProps.parentID; + this.previousParentJointIndex[targetProps.id] = targetProps.parentJointIndex; + } + + this.targetEntityID = targetProps.id; + Entities.editEntity(targetProps.id, reparentProps); + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'grab', + grabbedEntity: targetProps.id, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + this.grabbing = true; + }; + + this.endNearParentingGrabEntity = function () { + if (this.previousParentID[this.targetEntityID] === NULL_UUID || this.previousParentID === undefined) { + Entities.editEntity(this.targetEntityID, { + parentID: this.previousParentID[this.targetEntityID], + parentJointIndex: this.previousParentJointIndex[this.targetEntityID] + }); + } else { + // we're putting this back as a child of some other parent, so zero its velocity + Entities.editEntity(this.targetEntityID, { + parentID: this.previousParentID[this.targetEntityID], + parentJointIndex: this.previousParentJointIndex[this.targetEntityID], + localVelocity: {x: 0, y: 0, z: 0}, + localAngularVelocity: {x: 0, y: 0, z: 0} + }); + } + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args); + this.grabbing = false; + this.targetEntityID = null; + }; + + this.getTargetProps = function (controllerData) { + // nearbyEntityProperties is already sorted by length from controller + var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; + for (var i = 0; i < nearbyEntityProperties.length; i++) { + var props = nearbyEntityProperties[i]; + var handPosition = controllerData.controllerLocations[this.hand].position; + var distance = Vec3.distance(props.position, handPosition); + if (distance > NEAR_GRAB_RADIUS) { + continue; + } + if (entityIsGrabbable(props)) { + // give haptic feedback + if (props.id !== this.hapticTargetID) { + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + this.hapticTargetID = props.id; + } + // if we've attempted to grab a child, roll up to the root of the tree + var groupRootProps = findGroupParent(controllerData, props); + if (entityIsGrabbable(groupRootProps)) { + return groupRootProps; + } + return props; + } + } + return null; + }; + + this.isReady = function (controllerData, deltaTime) { + this.targetEntityID = null; + this.grabbing = false; + + var targetProps = this.getTargetProps(controllerData); + if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { + return makeRunningValues(false, [], []); + } + + if (targetProps) { + if (propsArePhysical(targetProps) || propsAreCloneDynamic(targetProps)) { + return makeRunningValues(false, [], []); // let nearActionGrabEntity handle it + } else { + this.targetEntityID = targetProps.id; + return makeRunningValues(true, [this.targetEntityID], []); + } + } else { + this.hapticTargetID = null; + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData, deltaTime) { + if (this.grabbing) { + if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { + this.endNearParentingGrabEntity(); + this.hapticTargetID = null; + return makeRunningValues(false, [], []); + } + + var props = Entities.getEntityProperties(this.targetEntityID); + if (!this.thisHandIsParent(props)) { + this.grabbing = false; + this.targetEntityID = null; + this.hapticTargetID = null; + return makeRunningValues(false, [], []); + } + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args); + } else { + // still searching / highlighting + var readiness = this.isReady (controllerData); + if (!readiness.active) { + return readiness; + } + if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) { + // switch to grab + var targetProps = this.getTargetProps(controllerData); + var targetCloneable = entityIsCloneable(targetProps); + + if (targetCloneable) { + var worldEntityProps = controllerData.nearbyEntityProperties[this.hand]; + var cloneID = cloneEntity(targetProps, worldEntityProps); + var cloneProps = Entities.getEntityProperties(cloneID); + + this.grabbing = true; + this.targetEntityID = cloneID; + this.startNearParentingGrabEntity(controllerData, cloneProps); + + } else if (targetProps) { + this.grabbing = true; + this.startNearParentingGrabEntity(controllerData, targetProps); + } + } + } + + return makeRunningValues(true, [this.targetEntityID], []); + }; + + this.cleanup = function () { + if (this.targetEntityID) { + this.endNearParentingGrabEntity(); + } + }; + } + + var leftNearParentingGrabEntity = new NearParentingGrabEntity(LEFT_HAND); + var rightNearParentingGrabEntity = new NearParentingGrabEntity(RIGHT_HAND); + + enableDispatcherModule("LeftNearParentingGrabEntity", leftNearParentingGrabEntity); + enableDispatcherModule("RightNearParentingGrabEntity", rightNearParentingGrabEntity); + + this.cleanup = function () { + leftNearParentingGrabEntity.cleanup(); + rightNearParentingGrabEntity.cleanup(); + disableDispatcherModule("LeftNearParentingGrabEntity"); + disableDispatcherModule("RightNearParentingGrabEntity"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js new file mode 100644 index 0000000000..54257ea6af --- /dev/null +++ b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js @@ -0,0 +1,218 @@ +"use strict"; + +// nearParentGrabOverlay.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID, + getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule, + Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, + makeDispatcherModuleParameters, Overlays, makeRunningValues +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +var GRAB_RADIUS = 0.35; + +(function() { + + // XXX this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true; + // XXX this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC; + + function NearParentingGrabOverlay(hand) { + this.hand = hand; + this.grabbedThingID = null; + this.previousParentID = {}; + this.previousParentJointIndex = {}; + this.previouslyUnhooked = {}; + + this.parameters = makeDispatcherModuleParameters( + 90, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + + // XXX does handJointIndex change if the avatar changes? + this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + this.controllerJointIndex = getControllerJointIndex(this.hand); + + this.getOtherModule = function() { + return (this.hand === RIGHT_HAND) ? leftNearParentingGrabOverlay : rightNearParentingGrabOverlay; + }; + + this.otherHandIsParent = function(props) { + return this.getOtherModule().thisHandIsParent(props); + }; + + this.thisHandIsParent = function(props) { + if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== AVATAR_SELF_ID) { + return false; + } + + var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + if (props.parentJointIndex === handJointIndex) { + return true; + } + + var controllerJointIndex = this.controllerJointIndex; + if (props.parentJointIndex === controllerJointIndex) { + return true; + } + + var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + + if (props.parentJointIndex === controllerCRJointIndex) { + return true; + } + + return false; + }; + + this.getGrabbedProperties = function() { + return { + position: Overlays.getProperty(this.grabbedThingID, "position"), + rotation: Overlays.getProperty(this.grabbedThingID, "rotation"), + parentID: Overlays.getProperty(this.grabbedThingID, "parentID"), + parentJointIndex: Overlays.getProperty(this.grabbedThingID, "parentJointIndex"), + dynamic: false, + shapeType: "none" + }; + }; + + + this.startNearParentingGrabOverlay = function (controllerData) { + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + + var handJointIndex; + // if (this.ignoreIK) { + // handJointIndex = this.controllerJointIndex; + // } else { + // handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + // } + handJointIndex = this.controllerJointIndex; + + var grabbedProperties = this.getGrabbedProperties(); + + var reparentProps = { + parentID: AVATAR_SELF_ID, + parentJointIndex: handJointIndex, + velocity: {x: 0, y: 0, z: 0}, + angularVelocity: {x: 0, y: 0, z: 0} + }; + + if (this.thisHandIsParent(grabbedProperties)) { + // this should never happen, but if it does, don't set previous parent to be this hand. + // this.previousParentID[this.grabbedThingID] = NULL; + // this.previousParentJointIndex[this.grabbedThingID] = -1; + } else if (this.otherHandIsParent(grabbedProperties)) { + // the other hand is parent. Steal the object and information + var otherModule = this.getOtherModule(); + this.previousParentID[this.grabbedThingID] = otherModule.previousParentID[this.garbbedThingID]; + this.previousParentJointIndex[this.grabbedThingID] = otherModule.previousParentJointIndex[this.grabbedThingID]; + + } else { + this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID; + this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex; + } + Overlays.editOverlay(this.grabbedThingID, reparentProps); + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'grab', + grabbedEntity: this.grabbedThingID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + }; + + this.endNearParentingGrabOverlay = function () { + var previousParentID = this.previousParentID[this.grabbedThingID]; + if (previousParentID === NULL_UUID || previousParentID === null || previousParentID === undefined) { + Overlays.editOverlay(this.grabbedThingID, { + parentID: NULL_UUID, + parentJointIndex: -1 + }); + } else { + // before we grabbed it, overlay was a child of something; put it back. + Overlays.editOverlay(this.grabbedThingID, { + parentID: this.previousParentID[this.grabbedThingID], + parentJointIndex: this.previousParentJointIndex[this.grabbedThingID] + }); + } + + this.grabbedThingID = null; + }; + + this.getTargetID = function(overlays, controllerData) { + for (var i = 0; i < overlays.length; i++) { + var overlayPosition = Overlays.getProperty(overlays[i], "position"); + var handPosition = controllerData.controllerLocations[this.hand].position; + var distance = Vec3.distance(overlayPosition, handPosition); + if (distance <= GRAB_RADIUS) { + return overlays[i]; + } + } + return null; + }; + + + this.isReady = function (controllerData) { + if (controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0) { + return makeRunningValues(false, [], []); + } + + this.grabbedThingID = null; + + var candidateOverlays = controllerData.nearbyOverlayIDs[this.hand]; + var grabbableOverlays = candidateOverlays.filter(function(overlayID) { + return Overlays.getProperty(overlayID, "grabbable"); + }); + + var targetID = this.getTargetID(grabbableOverlays, controllerData); + if (targetID) { + this.grabbedThingID = targetID; + this.startNearParentingGrabOverlay(controllerData); + return makeRunningValues(true, [this.grabbedThingID], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData) { + if (controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0) { + this.endNearParentingGrabOverlay(); + return makeRunningValues(false, [], []); + } else { + // check if someone stole the target from us + var grabbedProperties = this.getGrabbedProperties(); + if (!this.thisHandIsParent(grabbedProperties)) { + return makeRunningValues(false, [], []); + } + + return makeRunningValues(true, [this.grabbedThingID], []); + } + }; + + this.cleanup = function () { + if (this.grabbedThingID) { + this.endNearParentingGrabOverlay(); + } + }; + } + + var leftNearParentingGrabOverlay = new NearParentingGrabOverlay(LEFT_HAND); + var rightNearParentingGrabOverlay = new NearParentingGrabOverlay(RIGHT_HAND); + + enableDispatcherModule("LeftNearParentingGrabOverlay", leftNearParentingGrabOverlay); + enableDispatcherModule("RightNearParentingGrabOverlay", rightNearParentingGrabOverlay); + + this.cleanup = function () { + leftNearParentingGrabOverlay.cleanup(); + rightNearParentingGrabOverlay.cleanup(); + disableDispatcherModule("LeftNearParentingGrabOverlay"); + disableDispatcherModule("RightNearParentingGrabOverlay"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/nearTrigger.js b/scripts/system/controllers/controllerModules/nearTrigger.js new file mode 100644 index 0000000000..03fc7f8f64 --- /dev/null +++ b/scripts/system/controllers/controllerModules/nearTrigger.js @@ -0,0 +1,116 @@ +"use strict"; + +// nearTrigger.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, + enableDispatcherModule, disableDispatcherModule, getGrabbableData, Vec3, + TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, makeRunningValues, NEAR_GRAB_RADIUS +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +(function() { + + function entityWantsNearTrigger(props) { + var grabbableData = getGrabbableData(props); + return grabbableData.triggerable || grabbableData.wantsTrigger; + } + + function NearTriggerEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.grabbing = false; + this.previousParentID = {}; + this.previousParentJointIndex = {}; + this.previouslyUnhooked = {}; + + this.parameters = makeDispatcherModuleParameters( + 520, + this.hand === RIGHT_HAND ? ["rightHandTrigger", "rightHand"] : ["leftHandTrigger", "leftHand"], + [], + 100); + + this.getTargetProps = function (controllerData) { + // nearbyEntityProperties is already sorted by length from controller + var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; + for (var i = 0; i < nearbyEntityProperties.length; i++) { + var props = nearbyEntityProperties[i]; + var handPosition = controllerData.controllerLocations[this.hand].position; + var distance = Vec3.distance(props.position, handPosition); + if (distance > NEAR_GRAB_RADIUS) { + continue; + } + if (entityWantsNearTrigger(props)) { + return props; + } + } + return null; + }; + + this.startNearTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "startNearTrigger", args); + }; + + this.continueNearTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueNearTrigger", args); + }; + + this.endNearTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "endNearTrigger", args); + }; + + this.isReady = function (controllerData) { + this.targetEntityID = null; + + if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) { + return makeRunningValues(false, [], []); + } + + var targetProps = this.getTargetProps(controllerData); + if (targetProps) { + this.targetEntityID = targetProps.id; + this.startNearTrigger(controllerData); + return makeRunningValues(true, [this.targetEntityID], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData) { + if (controllerData.triggerClicks[this.hand] === 0) { + this.endNearTrigger(controllerData); + return makeRunningValues(false, [], []); + } + + this.continueNearTrigger(controllerData); + return makeRunningValues(true, [this.targetEntityID], []); + }; + + this.cleanup = function () { + if (this.targetEntityID) { + this.endNearTrigger(); + } + }; + } + + var leftNearTriggerEntity = new NearTriggerEntity(LEFT_HAND); + var rightNearTriggerEntity = new NearTriggerEntity(RIGHT_HAND); + + enableDispatcherModule("LeftNearTriggerEntity", leftNearTriggerEntity); + enableDispatcherModule("RightNearTriggerEntity", rightNearTriggerEntity); + + this.cleanup = function () { + leftNearTriggerEntity.cleanup(); + rightNearTriggerEntity.cleanup(); + disableDispatcherModule("LeftNearTriggerEntity"); + disableDispatcherModule("RightNearTriggerEntity"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/overlayLaserInput.js b/scripts/system/controllers/controllerModules/overlayLaserInput.js new file mode 100644 index 0000000000..2c950fd4df --- /dev/null +++ b/scripts/system/controllers/controllerModules/overlayLaserInput.js @@ -0,0 +1,553 @@ +"use strict"; + +// overlayLaserInput.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, + NULL_UUID, enableDispatcherModule, disableDispatcherModule, makeRunningValues, + Messages, Quat, Vec3, getControllerWorldLocation, makeDispatcherModuleParameters, Overlays, ZERO_VEC, + AVATAR_SELF_ID, HMD, INCHES_TO_METERS, DEFAULT_REGISTRATION_POINT, Settings, getGrabPointSphereOffset, + COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, + DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_ON_VALUE, TRIGGER_OFF_VALUE, getEnabledModuleByName, PICK_MAX_DISTANCE, + DISPATCHER_PROPERTIES +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + var halfPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var halfEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. + visible: true + }; + var fullPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var fullEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. + visible: true + }; + var holdPath = { + type: "line3d", + color: COLORS_GRAB_DISTANCE_HOLD, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + + var renderStates = [ + {name: "half", path: halfPath, end: halfEnd}, + {name: "full", path: fullPath, end: fullEnd}, + {name: "hold", path: holdPath} + ]; + + var defaultRenderStates = [ + {name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath}, + {name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath}, + {name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath} + ]; + + + // triggered when stylus presses a web overlay/entity + var HAPTIC_STYLUS_STRENGTH = 1.0; + var HAPTIC_STYLUS_DURATION = 20.0; + + function laserTargetHasKeyboardFocus(laserTarget) { + if (laserTarget && laserTarget !== NULL_UUID) { + return Overlays.keyboardFocusOverlay === laserTarget; + } + } + + function setKeyboardFocusOnLaserTarget(laserTarget) { + if (laserTarget && laserTarget !== NULL_UUID) { + Overlays.keyboardFocusOverlay = laserTarget; + Entities.keyboardFocusEntity = NULL_UUID; + } + } + + function sendHoverEnterEventToLaserTarget(hand, laserTarget) { + if (!laserTarget) { + return; + } + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: laserTarget.position2D, + pos3D: laserTarget.position, + normal: laserTarget.normal, + direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), + button: "None" + }; + + if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) { + Overlays.sendHoverEnterOverlay(laserTarget.overlayID, pointerEvent); + } + } + + function sendHoverOverEventToLaserTarget(hand, laserTarget) { + + if (!laserTarget) { + return; + } + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: laserTarget.position2D, + pos3D: laserTarget.position, + normal: laserTarget.normal, + direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), + button: "None" + }; + + if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseMoveOnOverlay(laserTarget.overlayID, pointerEvent); + Overlays.sendHoverOverOverlay(laserTarget.overlayID, pointerEvent); + } + } + + function sendTouchStartEventToLaserTarget(hand, laserTarget) { + if (!laserTarget) { + return; + } + + var pointerEvent = { + type: "Press", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: laserTarget.position2D, + pos3D: laserTarget.position, + normal: laserTarget.normal, + direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) { + Overlays.sendMousePressOnOverlay(laserTarget.overlayID, pointerEvent); + } + } + + function sendTouchEndEventToLaserTarget(hand, laserTarget) { + if (!laserTarget) { + return; + } + var pointerEvent = { + type: "Release", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: laserTarget.position2D, + pos3D: laserTarget.position, + normal: laserTarget.normal, + direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), + button: "Primary" + }; + + if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseReleaseOnOverlay(laserTarget.overlayID, pointerEvent); + Overlays.sendMouseReleaseOnOverlay(laserTarget.overlayID, pointerEvent); + } + } + + function sendTouchMoveEventToLaserTarget(hand, laserTarget) { + if (!laserTarget) { + return; + } + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: laserTarget.position2D, + pos3D: laserTarget.position, + normal: laserTarget.normal, + direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (laserTarget.overlayID && laserTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseReleaseOnOverlay(laserTarget.overlayID, pointerEvent); + } + } + + // will return undefined if overlayID does not exist. + function calculateLaserTargetFromOverlay(worldPos, overlayID) { + var overlayPosition = Overlays.getProperty(overlayID, "position"); + if (overlayPosition === undefined) { + return null; + } + + // project stylusTip onto overlay plane. + var overlayRotation = Overlays.getProperty(overlayID, "rotation"); + if (overlayRotation === undefined) { + return null; + } + var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1}); + var distance = Vec3.dot(Vec3.subtract(worldPos, overlayPosition), normal); + + // calclulate normalized position + var invRot = Quat.inverse(overlayRotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, overlayPosition)); + var dpi = Overlays.getProperty(overlayID, "dpi"); + + var dimensions; + if (dpi) { + // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property + // is used as a scale. + var resolution = Overlays.getProperty(overlayID, "resolution"); + if (resolution === undefined) { + return null; + } + resolution.z = 1;// Circumvent divide-by-zero. + var scale = Overlays.getProperty(overlayID, "dimensions"); + if (scale === undefined) { + return null; + } + scale.z = 0.01;// overlay dimensions are 2D, not 3D. + dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); + } else { + dimensions = Overlays.getProperty(overlayID, "dimensions"); + if (dimensions === undefined) { + return null; + } + if (!dimensions.z) { + dimensions.z = 0.01;// sometimes overlay dimensions are 2D, not 3D. + } + } + var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z }; + var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT); + + // 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner. + var position2D = { + x: normalizedPosition.x * dimensions.x, + y: (1 - normalizedPosition.y) * dimensions.y // flip y-axis + }; + + return { + entityID: null, + overlayID: overlayID, + distance: distance, + position: worldPos, + position2D: position2D, + normal: normal, + normalizedPosition: normalizedPosition, + dimensions: dimensions, + valid: true + }; + } + + function distance2D(a, b) { + var dx = (a.x - b.x); + var dy = (a.y - b.y); + return Math.sqrt(dx * dx + dy * dy); + } + + function OverlayLaserInput(hand) { + this.hand = hand; + this.active = false; + this.previousLaserClikcedTarget = false; + this.laserPressingTarget = false; + this.tabletScreenID = null; + this.mode = "none"; + this.laserTargetID = null; + this.laserTarget = null; + this.pressEnterLaserTarget = null; + this.hover = false; + this.target = null; + this.lastValidTargetID = this.tabletTargetID; + + + this.parameters = makeDispatcherModuleParameters( + 120, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.getOtherHandController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; + }; + + this.getOtherModule = function() { + return (this.hand === RIGHT_HAND) ? leftOverlayLaserInput : rightOverlayLaserInput; + }; + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.stealTouchFocus = function(laserTarget) { + this.requestTouchFocus(laserTarget); + }; + + this.requestTouchFocus = function(laserTarget) { + if (laserTarget !== null || laserTarget !== undefined) { + sendHoverEnterEventToLaserTarget(this.hand, this.laserTarget); + this.lastValidTargetID = laserTarget; + } + }; + + this.relinquishTouchFocus = function() { + // send hover leave event. + var pointerEvent = { type: "Move", id: this.hand + 1 }; + Overlays.sendMouseMoveOnOverlay(this.lastValidTargetID, pointerEvent); + Overlays.sendHoverOverOverlay(this.lastValidTargetID, pointerEvent); + Overlays.sendHoverLeaveOverlay(this.lastValidID, pointerEvent); + }; + + this.updateLaserPointer = function(controllerData) { + var RADIUS = 0.005; + var dim = { x: RADIUS, y: RADIUS, z: RADIUS }; + + if (this.mode === "full") { + this.fullEnd.dimensions = dim; + LaserPointers.editRenderState(this.laserPointer, this.mode, {path: fullPath, end: this.fullEnd}); + } else if (this.mode === "half") { + this.halfEnd.dimensions = dim; + LaserPointers.editRenderState(this.laserPointer, this.mode, {path: halfPath, end: this.halfEnd}); + } + + LaserPointers.enableLaserPointer(this.laserPointer); + LaserPointers.setRenderState(this.laserPointer, this.mode); + }; + + this.processControllerTriggers = function(controllerData) { + if (controllerData.triggerClicks[this.hand]) { + this.mode = "full"; + this.laserPressingTarget = true; + this.hover = false; + } else if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { + this.mode = "half"; + this.laserPressingTarget = false; + this.hover = true; + this.requestTouchFocus(this.laserTargetID); + } else { + this.mode = "none"; + this.laserPressingTarget = false; + this.hover = false; + this.relinquishTouchFocus(); + + } + }; + + this.hovering = function() { + if (!laserTargetHasKeyboardFocus(this.laserTagetID)) { + setKeyboardFocusOnLaserTarget(this.laserTargetID); + } + sendHoverOverEventToLaserTarget(this.hand, this.laserTarget); + }; + + this.laserPressEnter = function () { + sendTouchStartEventToLaserTarget(this.hand, this.laserTarget); + Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); + + this.touchingEnterTimer = 0; + this.pressEnterLaserTarget = this.laserTarget; + this.deadspotExpired = false; + + var LASER_PRESS_TO_MOVE_DEADSPOT = 0.026; + this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT) * this.laserTarget.distance; + }; + + this.laserPressExit = function () { + if (this.laserTarget === null) { + return; + } + + // special case to handle home button. + if (this.laserTargetID === HMD.homeButtonID) { + Messages.sendLocalMessage("home", this.laserTargetID); + } + + // send press event + if (this.deadspotExpired) { + sendTouchEndEventToLaserTarget(this.hand, this.laserTarget); + } else { + sendTouchEndEventToLaserTarget(this.hand, this.pressEnterLaserTarget); + } + }; + + this.laserPressing = function (controllerData, dt) { + this.touchingEnterTimer += dt; + + if (this.laserTarget) { + var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds + if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || + distance2D( this.laserTarget.position2D, + this.pressEnterLaserTarget.position2D) > this.deadspotRadius) { + sendTouchMoveEventToLaserTarget(this.hand, this.laserTarget); + this.deadspotExpired = true; + } + } else { + this.laserPressingTarget = false; + } + }; + + this.releaseTouchEvent = function() { + sendTouchEndEventToLaserTarget(this.hand, this.pressEnterLaserTarget); + }; + + + this.updateLaserTargets = function(controllerData) { + var intersection = controllerData.rayPicks[this.hand]; + this.laserTargetID = intersection.objectID; + this.laserTarget = calculateLaserTargetFromOverlay(intersection.intersection, intersection.objectID); + }; + + this.shouldExit = function(controllerData) { + var intersection = controllerData.rayPicks[this.hand]; + var nearGrabName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"; + var nearGrabModule = getEnabledModuleByName(nearGrabName); + var status = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []); + var offOverlay = (intersection.type !== RayPick.INTERSECTED_OVERLAY); + var triggerOff = (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE); + return offOverlay || status.active || triggerOff; + }; + + this.exitModule = function() { + this.releaseTouchEvent(); + this.relinquishTouchFocus(); + this.reset(); + this.updateLaserPointer(); + LaserPointers.disableLaserPointer(this.laserPointer); + }; + + this.reset = function() { + this.hover = false; + this.pressEnterLaserTarget = null; + this.laserTarget = null; + this.laserTargetID = null; + this.laserPressingTarget = false; + this.previousLaserClickedTarget = null; + this.mode = "none"; + this.active = false; + }; + + this.deleteContextOverlay = function() { + var farGrabModule = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightFarActionGrabEntity" : "LeftFarActionGrabEntity"); + if (farGrabModule) { + var entityWithContextOverlay = farGrabModule.entityWithContextOverlay; + + if (entityWithContextOverlay) { + ContextOverlay.destroyContextOverlay(entityWithContextOverlay); + farGrabModule.entityWithContextOverlay = false; + } + } + }; + + this.isReady = function (controllerData) { + this.target = null; + var intersection = controllerData.rayPicks[this.hand]; + if (intersection.type === RayPick.INTERSECTED_OVERLAY) { + if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE && !this.getOtherModule().active) { + this.target = intersection.objectID; + this.active = true; + return makeRunningValues(true, [], []); + } else { + this.deleteContextOverlay(); + } + } + this.reset(); + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData, deltaTime) { + if (this.shouldExit(controllerData)) { + this.exitModule(); + return makeRunningValues(false, [], []); + } + + if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) { + this.deleteContextOverlay(); + } + + this.updateLaserTargets(controllerData); + this.processControllerTriggers(controllerData); + this.updateLaserPointer(controllerData); + + if (!this.previousLaserClickedTarget && this.laserPressingTarget) { + this.laserPressEnter(); + } + if (this.previousLaserClickedTarget && !this.laserPressingTarget) { + this.laserPressExit(); + } + this.previousLaserClickedTarget = this.laserPressingTarget; + + if (this.laserPressingTarget) { + this.laserPressing(controllerData, deltaTime); + } + + if (this.hover) { + this.hovering(); + } + + return makeRunningValues(true, [], []); + }; + + this.cleanup = function () { + LaserPointers.disableLaserPointer(this.laserPointer); + LaserPointers.removeLaserPointer(this.laserPointer); + }; + + this.halfEnd = halfEnd; + this.fullEnd = fullEnd; + this.laserPointer = LaserPointers.createLaserPointer({ + joint: (this.hand === RIGHT_HAND) ? "_CONTROLLER_RIGHTHAND" : "_CONTROLLER_LEFTHAND", + filter: RayPick.PICK_OVERLAYS, + maxDistance: PICK_MAX_DISTANCE, + posOffset: getGrabPointSphereOffset(this.handToController()), + renderStates: renderStates, + faceAvatar: true, + defaultRenderStates: defaultRenderStates + }); + + LaserPointers.setIgnoreOverlays(this.laserPointer, [HMD.tabletID]); + } + + var leftOverlayLaserInput = new OverlayLaserInput(LEFT_HAND); + var rightOverlayLaserInput = new OverlayLaserInput(RIGHT_HAND); + + enableDispatcherModule("LeftOverlayLaserInput", leftOverlayLaserInput); + enableDispatcherModule("RightOverlayLaserInput", rightOverlayLaserInput); + + this.cleanup = function () { + leftOverlayLaserInput.cleanup(); + rightOverlayLaserInput.cleanup(); + disableDispatcherModule("LeftOverlayLaserInput"); + disableDispatcherModule("RightOverlayLaserInput"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/scaleAvatar.js b/scripts/system/controllers/controllerModules/scaleAvatar.js new file mode 100644 index 0000000000..b98e26b1da --- /dev/null +++ b/scripts/system/controllers/controllerModules/scaleAvatar.js @@ -0,0 +1,83 @@ +// handControllerGrab.js +// +// Created by Dante Ruiz on 9/11/17 +// +// Grabs physically moveable entities with hydra-like controllers; it works for either near or far objects. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, + Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, + setGrabCommunications, Menu, HMD, isInEditMode, AvatarList */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ + +(function () { + var dispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); + + function ScaleAvatar(hand) { + this.hand = hand; + this.scalingStartAvatarScale = 0; + this.scalingStartDistance = 0; + + this.parameters = dispatcherUtils.makeDispatcherModuleParameters( + 120, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100 + ); + + this.otherHand = function() { + return this.hand === dispatcherUtils.RIGHT_HAND ? dispatcherUtils.LEFT_HAND : dispatcherUtils.RIGHT_HAND; + }; + + this.getOtherModule = function() { + var otherModule = this.hand === dispatcherUtils.RIGHT_HAND ? leftScaleAvatar : rightScaleAvatar; + return otherModule; + }; + + this.triggersPressed = function(controllerData) { + if (controllerData.triggerClicks[this.hand] && controllerData.secondaryValues[this.hand] > dispatcherUtils.BUMPER_ON_VALUE) { + return true; + } + return false; + }; + + this.isReady = function(controllerData) { + var otherModule = this.getOtherModule(); + if (this.triggersPressed(controllerData) && otherModule.triggersPressed(controllerData)) { + this.scalingStartAvatarScale = MyAvatar.scale; + this.scalingStartDistance = Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position, + controllerData.controllerLocations[this.otherHand()].position)); + return dispatcherUtils.makeRunningValues(true, [], []); + } + return dispatcherUtils.makeRunningValues(false, [], []); + }; + + this.run = function(controllerData) { + var otherModule = this.getOtherModule(); + if (this.triggersPressed(controllerData) && otherModule.triggersPressed(controllerData)) { + if (this.hand === dispatcherUtils.RIGHT_HAND) { + var scalingCurrentDistance = Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position, + controllerData.controllerLocations[this.otherHand()].position)); + + var newAvatarScale = (scalingCurrentDistance / this.scalingStartDistance) * this.scalingStartAvatarScale; + MyAvatar.scale = newAvatarScale; + } + return dispatcherUtils.makeRunningValues(true, [], []); + } + return dispatcherUtils.makeRunningValues(false, [], []); + }; + } + + var leftScaleAvatar = new ScaleAvatar(dispatcherUtils.LEFT_HAND); + var rightScaleAvatar = new ScaleAvatar(dispatcherUtils.RIGHT_HAND); + + dispatcherUtils.enableDispatcherModule("LeftScaleAvatar", leftScaleAvatar); + dispatcherUtils.enableDispatcherModule("RightScaleAvatar", rightScaleAvatar); + + this.cleanup = function() { + dispatcherUtils.disableDispatcherModule("LeftScaleAvatar"); + dispatcherUtils.disableDispatcherModule("RightScaleAvatar"); + }; +})(); diff --git a/scripts/system/controllers/controllerModules/tabletStylusInput.js b/scripts/system/controllers/controllerModules/tabletStylusInput.js new file mode 100644 index 0000000000..0d4f4ff78a --- /dev/null +++ b/scripts/system/controllers/controllerModules/tabletStylusInput.js @@ -0,0 +1,704 @@ +"use strict"; + +// tabletStylusInput.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, + NULL_UUID, enableDispatcherModule, disableDispatcherModule, makeRunningValues, + Messages, Quat, Vec3, getControllerWorldLocation, makeDispatcherModuleParameters, Overlays, ZERO_VEC, + AVATAR_SELF_ID, HMD, INCHES_TO_METERS, DEFAULT_REGISTRATION_POINT, Settings, getGrabPointSphereOffset +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + + // triggered when stylus presses a web overlay/entity + var HAPTIC_STYLUS_STRENGTH = 1.0; + var HAPTIC_STYLUS_DURATION = 20.0; + + var WEB_DISPLAY_STYLUS_DISTANCE = 0.5; + var WEB_STYLUS_LENGTH = 0.2; + var WEB_TOUCH_Y_OFFSET = 0.05; // how far forward (or back with a negative number) to slide stylus in hand + + + function stylusTargetHasKeyboardFocus(stylusTarget) { + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + return Entities.keyboardFocusEntity === stylusTarget.entityID; + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + return Overlays.keyboardFocusOverlay === stylusTarget.overlayID; + } + } + + function setKeyboardFocusOnStylusTarget(stylusTarget) { + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID && + Entities.wantsHandControllerPointerEvents(stylusTarget.entityID)) { + Overlays.keyboardFocusOverlay = NULL_UUID; + Entities.keyboardFocusEntity = stylusTarget.entityID; + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.keyboardFocusOverlay = stylusTarget.overlayID; + Entities.keyboardFocusEntity = NULL_UUID; + } + } + + function sendHoverEnterEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "None" + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendHoverEnterEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendHoverEnterOverlay(stylusTarget.overlayID, pointerEvent); + } + } + + function sendHoverOverEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "None" + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendHoverOverEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent); + Overlays.sendHoverOverOverlay(stylusTarget.overlayID, pointerEvent); + } + } + + function sendTouchStartEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Press", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMousePressOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendClickDownOnEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMousePressOnOverlay(stylusTarget.overlayID, pointerEvent); + } + } + + function sendTouchEndEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Release", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "Primary" + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMouseReleaseOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendClickReleaseOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendHoverLeaveEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseReleaseOnOverlay(stylusTarget.overlayID, pointerEvent); + } + } + + function sendTouchMoveEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendHoldingClickOnEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent); + } + } + + // will return undefined if overlayID does not exist. + function calculateStylusTargetFromOverlay(stylusTip, overlayID) { + var overlayPosition = Overlays.getProperty(overlayID, "position"); + if (overlayPosition === undefined) { + return; + } + + // project stylusTip onto overlay plane. + var overlayRotation = Overlays.getProperty(overlayID, "rotation"); + if (overlayRotation === undefined) { + return; + } + var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1}); + var distance = Vec3.dot(Vec3.subtract(stylusTip.position, overlayPosition), normal); + var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance)); + + // calclulate normalized position + var invRot = Quat.inverse(overlayRotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, overlayPosition)); + var dpi = Overlays.getProperty(overlayID, "dpi"); + + var dimensions; + if (dpi) { + // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property + // is used as a scale. + var resolution = Overlays.getProperty(overlayID, "resolution"); + if (resolution === undefined) { + return; + } + resolution.z = 1; // Circumvent divide-by-zero. + var scale = Overlays.getProperty(overlayID, "dimensions"); + if (scale === undefined) { + return; + } + scale.z = 0.01; // overlay dimensions are 2D, not 3D. + dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); + } else { + dimensions = Overlays.getProperty(overlayID, "dimensions"); + if (dimensions === undefined) { + return; + } + if (!dimensions.z) { + dimensions.z = 0.01; // sometimes overlay dimensions are 2D, not 3D. + } + } + var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z }; + var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT); + + // 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner. + var position2D = { + x: normalizedPosition.x * dimensions.x, + y: (1 - normalizedPosition.y) * dimensions.y // flip y-axis + }; + + return { + entityID: null, + overlayID: overlayID, + distance: distance, + position: position, + position2D: position2D, + normal: normal, + normalizedPosition: normalizedPosition, + dimensions: dimensions, + valid: true + }; + } + + // will return undefined if entity does not exist. + function calculateStylusTargetFromEntity(stylusTip, props) { + if (props.rotation === undefined) { + // if rotation is missing from props object, then this entity has probably been deleted. + return; + } + + // project stylus tip onto entity plane. + var normal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1}); + Vec3.multiplyQbyV(props.rotation, {x: 0, y: 1, z: 0}); + var distance = Vec3.dot(Vec3.subtract(stylusTip.position, props.position), normal); + var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance)); + + // generate normalized coordinates + var invRot = Quat.inverse(props.rotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, props.position)); + var invDimensions = { x: 1 / props.dimensions.x, y: 1 / props.dimensions.y, z: 1 / props.dimensions.z }; + var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint); + + // 2D position on entity plane in meters, relative to the bounding box upper-left hand corner. + var position2D = { + x: normalizedPosition.x * props.dimensions.x, + y: (1 - normalizedPosition.y) * props.dimensions.y // flip y-axis + }; + + return { + entityID: props.id, + entityProps: props, + overlayID: null, + distance: distance, + position: position, + position2D: position2D, + normal: normal, + normalizedPosition: normalizedPosition, + dimensions: props.dimensions, + valid: true + }; + } + + function isNearStylusTarget(stylusTargets, edgeBorder, minNormalDistance, maxNormalDistance) { + for (var i = 0; i < stylusTargets.length; i++) { + var stylusTarget = stylusTargets[i]; + + // check to see if the projected stylusTip is within within the 2d border + var borderMin = {x: -edgeBorder, y: -edgeBorder}; + var borderMax = {x: stylusTarget.dimensions.x + edgeBorder, y: stylusTarget.dimensions.y + edgeBorder}; + if (stylusTarget.distance >= minNormalDistance && stylusTarget.distance <= maxNormalDistance && + stylusTarget.position2D.x >= borderMin.x && stylusTarget.position2D.y >= borderMin.y && + stylusTarget.position2D.x <= borderMax.x && stylusTarget.position2D.y <= borderMax.y) { + return true; + } + } + return false; + } + + function calculateNearestStylusTarget(stylusTargets) { + var nearestStylusTarget; + + for (var i = 0; i < stylusTargets.length; i++) { + var stylusTarget = stylusTargets[i]; + + if ((!nearestStylusTarget || stylusTarget.distance < nearestStylusTarget.distance) && + stylusTarget.normalizedPosition.x >= 0 && stylusTarget.normalizedPosition.y >= 0 && + stylusTarget.normalizedPosition.x <= 1 && stylusTarget.normalizedPosition.y <= 1) { + nearestStylusTarget = stylusTarget; + } + } + + return nearestStylusTarget; + } + + function getFingerWorldLocation(hand) { + var fingerJointName = (hand === RIGHT_HAND) ? "RightHandIndex4" : "LeftHandIndex4"; + + var fingerJointIndex = MyAvatar.getJointIndex(fingerJointName); + var fingerPosition = MyAvatar.getAbsoluteJointTranslationInObjectFrame(fingerJointIndex); + var fingerRotation = MyAvatar.getAbsoluteJointRotationInObjectFrame(fingerJointIndex); + var worldFingerRotation = Quat.multiply(MyAvatar.orientation, fingerRotation); + var worldFingerPosition = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, fingerPosition)); + + return { + position: worldFingerPosition, + orientation: worldFingerRotation, + rotation: worldFingerRotation, + valid: true + }; + } + + function distance2D(a, b) { + var dx = (a.x - b.x); + var dy = (a.y - b.y); + return Math.sqrt(dx * dx + dy * dy); + } + + function TabletStylusInput(hand) { + this.hand = hand; + this.previousStylusTouchingTarget = false; + this.stylusTouchingTarget = false; + + this.useFingerInsteadOfStylus = false; + this.fingerPointing = false; + + // initialize stylus tip + var DEFAULT_STYLUS_TIP = { + position: {x: 0, y: 0, z: 0}, + orientation: {x: 0, y: 0, z: 0, w: 0}, + rotation: {x: 0, y: 0, z: 0, w: 0}, + velocity: {x: 0, y: 0, z: 0}, + valid: false + }; + this.stylusTip = DEFAULT_STYLUS_TIP; + + + this.parameters = makeDispatcherModuleParameters( + 100, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.getOtherHandController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; + }; + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.updateFingerAsStylusSetting = function () { + var DEFAULT_USE_FINGER_AS_STYLUS = false; + var USE_FINGER_AS_STYLUS = Settings.getValue("preferAvatarFingerOverStylus"); + if (USE_FINGER_AS_STYLUS === "") { + USE_FINGER_AS_STYLUS = DEFAULT_USE_FINGER_AS_STYLUS; + } + if (USE_FINGER_AS_STYLUS && MyAvatar.getJointIndex("LeftHandIndex4") !== -1) { + this.useFingerInsteadOfStylus = true; + } else { + this.useFingerInsteadOfStylus = false; + } + }; + + this.updateStylusTip = function() { + if (this.useFingerInsteadOfStylus) { + this.stylusTip = getFingerWorldLocation(this.hand); + } else { + this.stylusTip = getControllerWorldLocation(this.handToController(), true); + + // translate tip forward according to constant. + var TIP_OFFSET = {x: 0, y: WEB_STYLUS_LENGTH - WEB_TOUCH_Y_OFFSET, z: 0}; + this.stylusTip.position = Vec3.sum(this.stylusTip.position, + Vec3.multiplyQbyV(this.stylusTip.orientation, TIP_OFFSET)); + } + + // compute tip velocity from hand controller motion, it is more accurate than computing it from previous positions. + var pose = Controller.getPoseValue(this.handToController()); + if (pose.valid) { + var worldControllerPos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation)); + var worldControllerLinearVel = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); + var worldControllerAngularVel = Vec3.multiplyQbyV(MyAvatar.orientation, pose.angularVelocity); + var tipVelocity = Vec3.sum(worldControllerLinearVel, Vec3.cross(worldControllerAngularVel, + Vec3.subtract(this.stylusTip.position, worldControllerPos))); + this.stylusTip.velocity = tipVelocity; + } else { + this.stylusTip.velocity = {x: 0, y: 0, z: 0}; + } + }; + + this.showStylus = function() { + if (this.stylus) { + return; + } + + var stylusProperties = { + name: "stylus", + url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx", + loadPriority: 10.0, + localPosition: Vec3.sum({ + x: 0.0, + y: WEB_TOUCH_Y_OFFSET, + z: 0.0 + }, getGrabPointSphereOffset(this.handToController())), + localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), + dimensions: { x: 0.01, y: 0.01, z: WEB_STYLUS_LENGTH }, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false, + parentID: AVATAR_SELF_ID, + parentJointIndex: MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND") + }; + this.stylus = Overlays.addOverlay("model", stylusProperties); + }; + + this.hideStylus = function() { + if (!this.stylus) { + return; + } + Overlays.deleteOverlay(this.stylus); + this.stylus = null; + }; + + this.stealTouchFocus = function(stylusTarget) { + // send hover events to target + // record the entity or overlay we are hovering over. + if ((stylusTarget.entityID === this.getOtherHandController().hoverEntity) || + (stylusTarget.overlayID === this.getOtherHandController().hoverOverlay)) { + this.getOtherHandController().relinquishTouchFocus(); + } + this.requestTouchFocus(stylusTarget); + }; + + this.requestTouchFocus = function(stylusTarget) { + + // send hover events to target if we can. + // record the entity or overlay we are hovering over. + if (stylusTarget.entityID && + stylusTarget.entityID !== this.hoverEntity && + stylusTarget.entityID !== this.getOtherHandController().hoverEntity) { + this.hoverEntity = stylusTarget.entityID; + sendHoverEnterEventToStylusTarget(this.hand, stylusTarget); + } else if (stylusTarget.overlayID && + stylusTarget.overlayID !== this.hoverOverlay && + stylusTarget.overlayID !== this.getOtherHandController().hoverOverlay) { + this.hoverOverlay = stylusTarget.overlayID; + sendHoverEnterEventToStylusTarget(this.hand, stylusTarget); + } + }; + + this.hasTouchFocus = function(stylusTarget) { + return ((stylusTarget.entityID && stylusTarget.entityID === this.hoverEntity) || + (stylusTarget.overlayID && stylusTarget.overlayID === this.hoverOverlay)); + }; + + this.relinquishTouchFocus = function() { + // send hover leave event. + var pointerEvent = { type: "Move", id: this.hand + 1 }; + if (this.hoverEntity) { + Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); + this.hoverEntity = null; + } else if (this.hoverOverlay) { + Overlays.sendMouseMoveOnOverlay(this.hoverOverlay, pointerEvent); + Overlays.sendHoverOverOverlay(this.hoverOverlay, pointerEvent); + Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); + this.hoverOverlay = null; + } + }; + + this.pointFinger = function(value) { + var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index"; + if (this.fingerPointing !== value) { + var message; + if (this.hand === RIGHT_HAND) { + message = { pointRightIndex: value }; + } else { + message = { pointLeftIndex: value }; + } + Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify(message), true); + this.fingerPointing = value; + } + }; + + this.processStylus = function(controllerData) { + this.updateStylusTip(); + + if (!this.stylusTip.valid || this.overlayLaserActive(controllerData)) { + this.pointFinger(false); + this.hideStylus(); + return false; + } + + if (this.useFingerInsteadOfStylus) { + this.hideStylus(); + } + + // build list of stylus targets, near the stylusTip + var stylusTargets = []; + var candidateEntities = controllerData.nearbyEntityProperties; + var i, props, stylusTarget; + for (i = 0; i < candidateEntities.length; i++) { + props = candidateEntities[i]; + if (props && props.type === "Web") { + stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, candidateEntities[i]); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + } + + // add the tabletScreen, if it is valid + if (HMD.tabletScreenID && HMD.tabletScreenID !== NULL_UUID && + Overlays.getProperty(HMD.tabletScreenID, "visible")) { + stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.tabletScreenID); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + + // add the tablet home button. + if (HMD.homeButtonID && HMD.homeButtonID !== NULL_UUID && + Overlays.getProperty(HMD.homeButtonID, "visible")) { + stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.homeButtonID); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + + var TABLET_MIN_HOVER_DISTANCE = 0.01; + var TABLET_MAX_HOVER_DISTANCE = 0.1; + var TABLET_MIN_TOUCH_DISTANCE = -0.05; + var TABLET_MAX_TOUCH_DISTANCE = TABLET_MIN_HOVER_DISTANCE; + var EDGE_BORDER = 0.075; + + var hysteresisOffset = 0.0; + if (this.isNearStylusTarget) { + hysteresisOffset = 0.05; + } + + this.isNearStylusTarget = isNearStylusTarget(stylusTargets, EDGE_BORDER + hysteresisOffset, + TABLET_MIN_TOUCH_DISTANCE - hysteresisOffset, + WEB_DISPLAY_STYLUS_DISTANCE + hysteresisOffset); + + if (this.isNearStylusTarget) { + if (!this.useFingerInsteadOfStylus) { + this.showStylus(); + } else { + this.pointFinger(true); + } + } else { + this.hideStylus(); + this.pointFinger(false); + } + + var nearestStylusTarget = calculateNearestStylusTarget(stylusTargets); + + if (nearestStylusTarget && nearestStylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && + nearestStylusTarget.distance < TABLET_MAX_HOVER_DISTANCE) { + + this.requestTouchFocus(nearestStylusTarget); + + if (!stylusTargetHasKeyboardFocus(nearestStylusTarget)) { + setKeyboardFocusOnStylusTarget(nearestStylusTarget); + } + + if (this.hasTouchFocus(nearestStylusTarget)) { + sendHoverOverEventToStylusTarget(this.hand, nearestStylusTarget); + } + + // filter out presses when tip is moving away from tablet. + // ensure that stylus is within bounding box by checking normalizedPosition + if (nearestStylusTarget.valid && nearestStylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && + nearestStylusTarget.distance < TABLET_MAX_TOUCH_DISTANCE && + Vec3.dot(this.stylusTip.velocity, nearestStylusTarget.normal) < 0 && + nearestStylusTarget.normalizedPosition.x >= 0 && nearestStylusTarget.normalizedPosition.x <= 1 && + nearestStylusTarget.normalizedPosition.y >= 0 && nearestStylusTarget.normalizedPosition.y <= 1) { + + this.stylusTarget = nearestStylusTarget; + this.stylusTouchingTarget = true; + } + } else { + this.relinquishTouchFocus(); + } + + this.homeButtonTouched = false; + + if (this.isNearStylusTarget) { + return true; + } else { + this.pointFinger(false); + this.hideStylus(); + return false; + } + }; + + this.stylusTouchingEnter = function () { + this.stealTouchFocus(this.stylusTarget); + sendTouchStartEventToStylusTarget(this.hand, this.stylusTarget); + Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); + + this.touchingEnterTimer = 0; + this.touchingEnterStylusTarget = this.stylusTarget; + this.deadspotExpired = false; + + var TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0381; + this.deadspotRadius = TOUCH_PRESS_TO_MOVE_DEADSPOT; + }; + + this.stylusTouchingExit = function () { + + if (this.stylusTarget === undefined) { + return; + } + + // special case to handle home button. + if (this.stylusTarget.overlayID === HMD.homeButtonID) { + Messages.sendLocalMessage("home", this.stylusTarget.overlayID); + } + + // send press event + if (this.deadspotExpired) { + sendTouchEndEventToStylusTarget(this.hand, this.stylusTarget); + } else { + sendTouchEndEventToStylusTarget(this.hand, this.touchingEnterStylusTarget); + } + }; + + this.stylusTouching = function (controllerData, dt) { + + this.touchingEnterTimer += dt; + + if (this.stylusTarget.entityID) { + this.stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, this.stylusTarget.entityProps); + } else if (this.stylusTarget.overlayID) { + this.stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, this.stylusTarget.overlayID); + } + + var TABLET_MIN_TOUCH_DISTANCE = -0.1; + var TABLET_MAX_TOUCH_DISTANCE = 0.01; + + if (this.stylusTarget) { + if (this.stylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && + this.stylusTarget.distance < TABLET_MAX_TOUCH_DISTANCE) { + var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds + if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || + distance2D(this.stylusTarget.position2D, + this.touchingEnterStylusTarget.position2D) > this.deadspotRadius) { + sendTouchMoveEventToStylusTarget(this.hand, this.stylusTarget); + this.deadspotExpired = true; + } + } else { + this.stylusTouchingTarget = false; + } + } else { + this.stylusTouchingTarget = false; + } + }; + + this.overlayLaserActive = function(controllerData) { + var overlayLaserModule = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightOverlayLaserInput" : "LeftOverlayLaserInput"); + if (overlayLaserModule) { + return overlayLaserModule.isReady(controllerData).active; + } + return false; + }; + + this.isReady = function (controllerData) { + if (this.processStylus(controllerData)) { + return makeRunningValues(true, [], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData, deltaTime) { + this.updateFingerAsStylusSetting(); + + if (!this.previousStylusTouchingTarget && this.stylusTouchingTarget) { + this.stylusTouchingEnter(); + } + if (this.previousStylusTouchingTarget && !this.stylusTouchingTarget) { + this.stylusTouchingExit(); + } + this.previousStylusTouchingTarget = this.stylusTouchingTarget; + + if (this.stylusTouchingTarget) { + this.stylusTouching(controllerData, deltaTime); + } + if (this.processStylus(controllerData)) { + return makeRunningValues(true, [], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.cleanup = function () { + this.hideStylus(); + }; + } + + var leftTabletStylusInput = new TabletStylusInput(LEFT_HAND); + var rightTabletStylusInput = new TabletStylusInput(RIGHT_HAND); + + enableDispatcherModule("LeftTabletStylusInput", leftTabletStylusInput); + enableDispatcherModule("RightTabletStylusInput", rightTabletStylusInput); + + this.cleanup = function () { + leftTabletStylusInput.cleanup(); + rightTabletStylusInput.cleanup(); + disableDispatcherModule("LeftTabletStylusInput"); + disableDispatcherModule("RightTabletStylusInput"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js new file mode 100644 index 0000000000..9bb3bcb2f9 --- /dev/null +++ b/scripts/system/controllers/controllerModules/teleport.js @@ -0,0 +1,522 @@ +"use strict"; + +// Created by james b. pollack @imgntn on 7/2/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Creates a beam and target and then teleports you there. Release when its close to you to cancel. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, AVATAR_SELF_ID, + getControllerJointIndex, NULL_UUID, enableDispatcherModule, disableDispatcherModule, + Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, + Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic +*/ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ + +Script.include("/~/system/libraries/Xform.js"); +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { // BEGIN LOCAL_SCOPE + +var inTeleportMode = false; + +var SMOOTH_ARRIVAL_SPACING = 33; +var NUMBER_OF_STEPS = 6; + +var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleport-destination.fbx"); +var TOO_CLOSE_MODEL_URL = Script.resolvePath("../../assets/models/teleport-cancel.fbx"); +var SEAT_MODEL_URL = Script.resolvePath("../../assets/models/teleport-seat.fbx"); + +var TARGET_MODEL_DIMENSIONS = { + x: 1.15, + y: 0.5, + z: 1.15 +}; + +var COLORS_TELEPORT_SEAT = { + red: 255, + green: 0, + blue: 170 +}; + +var COLORS_TELEPORT_CAN_TELEPORT = { + red: 97, + green: 247, + blue: 255 +}; + +var COLORS_TELEPORT_CANCEL = { + red: 255, + green: 184, + blue: 73 +}; + +var TELEPORT_CANCEL_RANGE = 1; +var COOL_IN_DURATION = 500; + +var handInfo = { + right: { + controllerInput: Controller.Standard.RightHand + }, + left: { + controllerInput: Controller.Standard.LeftHand + } +}; + +var cancelPath = { + type: "line3d", + color: COLORS_TELEPORT_CANCEL, + ignoreRayIntersection: true, + alpha: 1, + solid: true, + drawInFront: true, + glow: 1.0 +}; +var teleportPath = { + type: "line3d", + color: COLORS_TELEPORT_CAN_TELEPORT, + ignoreRayIntersection: true, + alpha: 1, + solid: true, + drawInFront: true, + glow: 1.0 +}; +var seatPath = { + type: "line3d", + color: COLORS_TELEPORT_SEAT, + ignoreRayIntersection: true, + alpha: 1, + solid: true, + drawInFront: true, + glow: 1.0 +}; +var cancelEnd = { + type: "model", + url: TOO_CLOSE_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + ignoreRayIntersection: true +}; +var teleportEnd = { + type: "model", + url: TARGET_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + ignoreRayIntersection: true +}; +var seatEnd = { + type: "model", + url: SEAT_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + ignoreRayIntersection: true +} + +var teleportRenderStates = [{name: "cancel", path: cancelPath, end: cancelEnd}, + {name: "teleport", path: teleportPath, end: teleportEnd}, + {name: "seat", path: seatPath, end: seatEnd}]; + +var DEFAULT_DISTANCE = 50; +var teleportDefaultRenderStates = [{name: "cancel", distance: DEFAULT_DISTANCE, path: cancelPath}]; + +function ThumbPad(hand) { + this.hand = hand; + var _thisPad = this; + + this.buttonPress = function(value) { + _thisPad.buttonValue = value; + }; +} + +function Trigger(hand) { + this.hand = hand; + var _this = this; + + this.buttonPress = function(value) { + _this.buttonValue = value; + }; + + this.down = function() { + var down = _this.buttonValue === 1 ? 1.0 : 0.0; + return down; + }; +} + +var coolInTimeout = null; +var ignoredEntities = []; + +var TELEPORTER_STATES = { + IDLE: 'idle', + COOL_IN: 'cool_in', + TARGETTING: 'targetting', + TARGETTING_INVALID: 'targetting_invalid', +}; + +var TARGET = { + NONE: 'none', // Not currently targetting anything + INVISIBLE: 'invisible', // The current target is an invvsible surface + INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.) + SURFACE: 'surface', // The current target is a valid surface + SEAT: 'seat', // The current target is a seat +}; + +function Teleporter(hand) { + var _this = this; + this.hand = hand; + this.buttonValue = 0; + this.active = false; + this.state = TELEPORTER_STATES.IDLE; + this.currentTarget = TARGET.INVALID; + this.currentResult = null; + + this.getOtherModule = function() { + var otherModule = this.hand === RIGHT_HAND ? leftTeleporter : rightTeleporter; + return otherModule; + }; + + this.teleportRayHandVisible = LaserPointers.createLaserPointer({ + joint: (_this.hand === RIGHT_HAND) ? "RightHand" : "LeftHand", + filter: RayPick.PICK_ENTITIES, + faceAvatar: true, + centerEndY: false, + renderStates: teleportRenderStates, + defaultRenderStates: teleportDefaultRenderStates + }); + this.teleportRayHandInvisible = LaserPointers.createLaserPointer({ + joint: (_this.hand === RIGHT_HAND) ? "RightHand" : "LeftHand", + filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + centerEndY: false, + renderStates: teleportRenderStates + }); + this.teleportRayHeadVisible = LaserPointers.createLaserPointer({ + joint: "Avatar", + filter: RayPick.PICK_ENTITIES, + faceAvatar: true, + centerEndY: false, + renderStates: teleportRenderStates, + defaultRenderStates: teleportDefaultRenderStates + }); + this.teleportRayHeadInvisible = LaserPointers.createLaserPointer({ + joint: "Avatar", + filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + centerEndY: false, + renderStates: teleportRenderStates + }); + + this.teleporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); + this.teleportMappingInternal = Controller.newMapping(this.teleporterMappingInternalName); + + this.enableMappings = function() { + Controller.enableMapping(this.teleporterMappingInternalName); + }; + + this.disableMappings = function() { + Controller.disableMapping(teleporter.teleporterMappingInternalName); + }; + + this.cleanup = function() { + this.disableMappings(); + + LaserPointers.removeLaserPointer(this.teleportRayHandVisible); + LaserPointers.removeLaserPointer(this.teleportRayHandInvisible); + LaserPointers.removeLaserPointer(this.teleportRayHeadVisible); + LaserPointers.removeLaserPointer(this.teleportRayHeadInvisible); + }; + + this.buttonPress = function(value) { + _this.buttonValue = value; + } + + this.parameters = makeDispatcherModuleParameters( + 80, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.enterTeleport = function() { + if (coolInTimeout !== null) { + Script.clearTimeout(coolInTimeout); + } + + this.state = TELEPORTER_STATES.COOL_IN; + coolInTimeout = Script.setTimeout(function() { + if (_this.state === TELEPORTER_STATES.COOL_IN) { + _this.state = TELEPORTER_STATES.TARGETTING; + } + }, COOL_IN_DURATION); + }; + + + this.isReady = function(controllerData, deltaTime) { + var otherModule = this.getOtherModule(); + if (_this.buttonValue !== 0 && !otherModule.active) { + this.active = true; + this.enterTeleport(); + return makeRunningValues(true, [], []); + } + return makeRunningValues(false, [], []); + }; + + this.run = function(controllerData, deltaTime) { + //_this.state = TELEPORTER_STATES.TARGETTING; + + // Get current hand pose information to see if the pose is valid + var pose = Controller.getPoseValue(handInfo[(_this.hand === RIGHT_HAND) ? 'right' : 'left'].controllerInput); + var mode = pose.valid ? _this.hand : 'head'; + if (!pose.valid) { + LaserPointers.disableLaserPointer(_this.teleportRayHandVisible); + LaserPointers.disableLaserPointer(_this.teleportRayHandInvisible); + LaserPointers.enableLaserPointer(_this.teleportRayHeadVisible); + LaserPointers.enableLaserPointer(_this.teleportRayHeadInvisible); + } else { + LaserPointers.enableLaserPointer(_this.teleportRayHandVisible); + LaserPointers.enableLaserPointer(_this.teleportRayHandInvisible); + LaserPointers.disableLaserPointer(_this.teleportRayHeadVisible); + LaserPointers.disableLaserPointer(_this.teleportRayHeadInvisible); + } + + // We do up to 2 ray picks to find a teleport location. + // There are 2 types of teleport locations we are interested in: + // 1. A visible floor. This can be any entity surface that points within some degree of "up" + // 2. A seat. The seat can be visible or invisible. + // + // * In the first pass we pick against visible and invisible entities so that we can find invisible seats. + // We might hit an invisible entity that is not a seat, so we need to do a second pass. + // * In the second pass we pick against visible entities only. + // + var result; + if (mode === 'head') { + result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadInvisible); + } else { + result = LaserPointers.getPrevRayPickResult(_this.teleportRayHandInvisible); + } + + var teleportLocationType = getTeleportTargetType(result); + if (teleportLocationType === TARGET.INVISIBLE) { + if (mode === 'head') { + result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadVisible); + } else { + result = LaserPointers.getPrevRayPickResult(_this.teleportRayHandVisible); + } + + teleportLocationType = getTeleportTargetType(result); + } + + if (teleportLocationType === TARGET.NONE) { + // Use the cancel default state + this.setTeleportState(mode, "cancel", ""); + } else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) { + this.setTeleportState(mode, "", "cancel"); + } else if (teleportLocationType === TARGET.SURFACE) { + if (this.state === TELEPORTER_STATES.COOL_IN) { + this.setTeleportState(mode, "cancel", ""); + } else { + this.setTeleportState(mode, "teleport", ""); + } + } else if (teleportLocationType === TARGET.SEAT) { + this.setTeleportState(mode, "", "seat"); + } + return this.teleport(result, teleportLocationType); + }; + + this.teleport = function(newResult, target) { + var result = newResult; + if (_this.buttonValue !== 0) { + return makeRunningValues(true, [], []); + } + + if (target === TARGET.NONE || target === TARGET.INVALID || this.state === TELEPORTER_STATES.COOL_IN) { + // Do nothing + } else if (target === TARGET.SEAT) { + Entities.callEntityMethod(result.objectID, 'sit'); + } else if (target === TARGET.SURFACE) { + var offset = getAvatarFootOffset(); + result.intersection.y += offset; + MyAvatar.goToLocation(result.intersection, false, {x: 0, y: 0, z: 0, w: 1}, false); + HMD.centerUI(); + MyAvatar.centerBody(); + } + + this.disableLasers(); + this.active = false; + return makeRunningValues(false, [], []); + }; + + this.disableLasers = function() { + LaserPointers.disableLaserPointer(_this.teleportRayHandVisible); + LaserPointers.disableLaserPointer(_this.teleportRayHandInvisible); + LaserPointers.disableLaserPointer(_this.teleportRayHeadVisible); + LaserPointers.disableLaserPointer(_this.teleportRayHeadInvisible); + }; + + this.setTeleportState = function(mode, visibleState, invisibleState) { + if (mode === 'head') { + LaserPointers.setRenderState(_this.teleportRayHeadVisible, visibleState); + LaserPointers.setRenderState(_this.teleportRayHeadInvisible, invisibleState); + } else { + LaserPointers.setRenderState(_this.teleportRayHandVisible, visibleState); + LaserPointers.setRenderState(_this.teleportRayHandInvisible, invisibleState); + } + }; +} + + // related to repositioning the avatar after you teleport + var FOOT_JOINT_NAMES = ["RightToe_End", "RightToeBase", "RightFoot"]; + var DEFAULT_ROOT_TO_FOOT_OFFSET = 0.5; + function getAvatarFootOffset() { + + // find a valid foot jointIndex + var footJointIndex = -1; + var i, l = FOOT_JOINT_NAMES.length; + for (i = 0; i < l; i++) { + footJointIndex = MyAvatar.getJointIndex(FOOT_JOINT_NAMES[i]); + if (footJointIndex != -1) { + break; + } + } + if (footJointIndex != -1) { + // default vertical offset from foot to avatar root. + var footPos = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(footJointIndex); + if (footPos.x === 0 && footPos.y === 0 && footPos.z === 0.0) { + // if footPos is exactly zero, it's probably wrong because avatar is currently loading, fall back to default. + return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale; + } else { + return -footPos.y; + } + } else { + return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale; + } + } + + var leftPad = new ThumbPad('left'); + var rightPad = new ThumbPad('right'); + + var mappingName, teleportMapping; + + var TELEPORT_DELAY = 0; + + function isMoving() { + var LY = Controller.getValue(Controller.Standard.LY); + var LX = Controller.getValue(Controller.Standard.LX); + if (LY !== 0 || LX !== 0) { + return true; + } else { + return false; + } + } + + function parseJSON(json) { + try { + return JSON.parse(json); + } catch (e) { + return undefined; + } + } + // When determininig whether you can teleport to a location, the normal of the + // point that is being intersected with is looked at. If this normal is more + // than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from <0, 1, 0> (straight up), then + // you can't teleport there. + var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; + function getTeleportTargetType(result) { + if (result.type == RayPick.INTERSECTED_NONE) { + return TARGET.NONE; + } + + var props = Entities.getEntityProperties(result.objectID, ['userData', 'visible']); + var data = parseJSON(props.userData); + if (data !== undefined && data.seat !== undefined) { + var avatarUuid = Uuid.fromString(data.seat.user); + if (Uuid.isNull(avatarUuid) || !AvatarList.getAvatar(avatarUuid)) { + return TARGET.SEAT; + } else { + return TARGET.INVALID; + } + } + + if (!props.visible) { + return TARGET.INVISIBLE; + } + + var surfaceNormal = result.surfaceNormal; + var adj = Math.sqrt(surfaceNormal.x * surfaceNormal.x + surfaceNormal.z * surfaceNormal.z); + var angleUp = Math.atan2(surfaceNormal.y, adj) * (180 / Math.PI); + + if (angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) || + angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) || + Vec3.distance(MyAvatar.position, result.intersection) <= TELEPORT_CANCEL_RANGE) { + return TARGET.INVALID; + } else { + return TARGET.SURFACE; + } + } + + function registerMappings() { + mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); + teleportMapping = Controller.newMapping(mappingName); + + teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightTeleporter.buttonPress); + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftTeleporter.buttonPress); + } + + var leftTeleporter = new Teleporter(LEFT_HAND); + var rightTeleporter = new Teleporter(RIGHT_HAND); + + enableDispatcherModule("LeftTeleporter", leftTeleporter); + enableDispatcherModule("RightTeleporter", rightTeleporter); + registerMappings(); + Controller.enableMapping(mappingName); + + function cleanup() { + teleportMapping.disable(); + disableDispatcherModule("LeftTeleporter"); + disableDispatcherModule("RightTeleporter"); + } + Script.scriptEnding.connect(cleanup); + + var setIgnoreEntities = function() { + LaserPointers.setIgnoreEntities(teleporter.teleportRayRightVisible, ignoredEntities); + LaserPointers.setIgnoreEntities(teleporter.teleportRayRightInvisible, ignoredEntities); + LaserPointers.setIgnoreEntities(teleporter.teleportRayLeftVisible, ignoredEntities); + LaserPointers.setIgnoreEntities(teleporter.teleportRayLeftInvisible, ignoredEntities); + LaserPointers.setIgnoreEntities(teleporter.teleportRayHeadVisible, ignoredEntities); + LaserPointers.setIgnoreEntities(teleporter.teleportRayHeadInvisible, ignoredEntities); + } + + var isDisabled = false; + var handleTeleportMessages = function(channel, message, sender) { + if (sender === MyAvatar.sessionUUID) { + if (channel === 'Hifi-Teleport-Disabler') { + if (message === 'both') { + isDisabled = 'both'; + } + if (message === 'left') { + isDisabled = 'left'; + } + if (message === 'right') { + isDisabled = 'right'; + } + if (message === 'none') { + isDisabled = false; + } + } else if (channel === 'Hifi-Teleport-Ignore-Add' && !Uuid.isNull(message) && ignoredEntities.indexOf(message) === -1) { + ignoredEntities.push(message); + setIgnoreEntities(); + } else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) { + var removeIndex = ignoredEntities.indexOf(message); + if (removeIndex > -1) { + ignoredEntities.splice(removeIndex, 1); + setIgnoreEntities(); + } + } + } + }; + + Messages.subscribe('Hifi-Teleport-Disabler'); + Messages.subscribe('Hifi-Teleport-Ignore-Add'); + Messages.subscribe('Hifi-Teleport-Ignore-Remove'); + Messages.messageReceived.connect(handleTeleportMessages); + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/controllerModules/webEntityLaserInput.js b/scripts/system/controllers/controllerModules/webEntityLaserInput.js new file mode 100644 index 0000000000..1e954d5917 --- /dev/null +++ b/scripts/system/controllers/controllerModules/webEntityLaserInput.js @@ -0,0 +1,484 @@ +"use strict"; + +// webEntityLaserInput.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* jslint bitwise: true */ + +/* global Script, Controller, LaserPointers, RayPick, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat, + getGrabPointSphereOffset, getEnabledModuleByName, makeRunningValues, Entities, NULL_UUID, + enableDispatcherModule, disableDispatcherModule, entityIsDistanceGrabbable, + makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, + PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, + AVATAR_SELF_ID, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC + +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + var halfPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var halfEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. + visible: true + }; + var fullPath = { + type: "line3d", + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + var fullEnd = { + type: "sphere", + solid: true, + color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, + alpha: 0.9, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. + visible: true + }; + var holdPath = { + type: "line3d", + color: COLORS_GRAB_DISTANCE_HOLD, + visible: true, + alpha: 1, + solid: true, + glow: 1.0, + lineWidth: 5, + ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. + parentID: AVATAR_SELF_ID + }; + + var renderStates = [ + {name: "half", path: halfPath, end: halfEnd}, + {name: "full", path: fullPath, end: fullEnd}, + {name: "hold", path: holdPath} + ]; + + var defaultRenderStates = [ + {name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath}, + {name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath}, + {name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath} + ]; + + + // triggered when stylus presses a web overlay/entity + var HAPTIC_STYLUS_STRENGTH = 1.0; + var HAPTIC_STYLUS_DURATION = 20.0; + + function laserTargetHasKeyboardFocus(laserTarget) { + if (laserTarget && laserTarget !== NULL_UUID) { + return Entities.keyboardFocusOverlay === laserTarget; + } + } + + function setKeyboardFocusOnLaserTarget(laserTarget) { + if (laserTarget && laserTarget !== NULL_UUID) { + Entities.wantsHandControllerPointerEvents(laserTarget); + Overlays.keyboardFocusOverlay = NULL_UUID; + Entities.keyboardFocusEntity = laserTarget; + } + } + + function sendHoverEnterEventToLaserTarget(hand, laserTarget) { + if (!laserTarget) { + return; + } + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: laserTarget.position2D, + pos3D: laserTarget.position, + normal: laserTarget.normal, + direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), + button: "None" + }; + + if (laserTarget.entityID && laserTarget.entityID !== NULL_UUID) { + Entities.sendHoverEnterEntity(laserTarget.entityID, pointerEvent); + } + } + + function sendHoverOverEventToLaserTarget(hand, laserTarget) { + + if (!laserTarget) { + return; + } + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: laserTarget.position2D, + pos3D: laserTarget.position, + normal: laserTarget.normal, + direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), + button: "None" + }; + + if (laserTarget.entityID && laserTarget.entityID !== NULL_UUID) { + Entities.sendMouseMoveOnEntity(laserTarget.entityID, pointerEvent); + Entities.sendHoverOverEntity(laserTarget.entityID, pointerEvent); + } + } + + + function sendTouchStartEventToLaserTarget(hand, laserTarget) { + var pointerEvent = { + type: "Press", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: laserTarget.position2D, + pos3D: laserTarget.position, + normal: laserTarget.normal, + direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (laserTarget.entityID && laserTarget.entityID !== NULL_UUID) { + Entities.sendMousePressOnEntity(laserTarget.entityID, pointerEvent); + Entities.sendClickDownOnEntity(laserTarget.entityID, pointerEvent); + } + } + + function sendTouchEndEventToLaserTarget(hand, laserTarget) { + var pointerEvent = { + type: "Release", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: laserTarget.position2D, + pos3D: laserTarget.position, + normal: laserTarget.normal, + direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), + button: "Primary" + }; + + if (laserTarget.entityID && laserTarget.entityID !== NULL_UUID) { + Entities.sendMouseReleaseOnEntity(laserTarget.entityID, pointerEvent); + Entities.sendClickReleaseOnEntity(laserTarget.entityID, pointerEvent); + Entities.sendHoverLeaveEntity(laserTarget.entityID, pointerEvent); + } + } + + function sendTouchMoveEventToLaserTarget(hand, laserTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: laserTarget.position2D, + pos3D: laserTarget.position, + normal: laserTarget.normal, + direction: Vec3.subtract(ZERO_VEC, laserTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (laserTarget.entityID && laserTarget.entityID !== NULL_UUID) { + Entities.sendMouseMoveOnEntity(laserTarget.entityID, pointerEvent); + Entities.sendHoldingClickOnEntity(laserTarget.entityID, pointerEvent); + } + } + + function calculateTargetFromEntity(intersection, props) { + if (props.rotation === undefined) { + // if rotation is missing from props object, then this entity has probably been deleted. + return null; + } + + // project stylus tip onto entity plane. + var normal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1}); + Vec3.multiplyQbyV(props.rotation, {x: 0, y: 1, z: 0}); + var distance = Vec3.dot(Vec3.subtract(intersection, props.position), normal); + var position = Vec3.subtract(intersection, Vec3.multiply(normal, distance)); + + // generate normalized coordinates + var invRot = Quat.inverse(props.rotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, props.position)); + var invDimensions = { x: 1 / props.dimensions.x, y: 1 / props.dimensions.y, z: 1 / props.dimensions.z }; + var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint); + + // 2D position on entity plane in meters, relative to the bounding box upper-left hand corner. + var position2D = { + x: normalizedPosition.x * props.dimensions.x, + y: (1 - normalizedPosition.y) * props.dimensions.y // flip y-axis + }; + + return { + entityID: props.id, + entityProps: props, + overlayID: null, + distance: distance, + position: position, + position2D: position2D, + normal: normal, + normalizedPosition: normalizedPosition, + dimensions: props.dimensions, + valid: true + }; + } + + function distance2D(a, b) { + var dx = (a.x - b.x); + var dy = (a.y - b.y); + return Math.sqrt(dx * dx + dy * dy); + } + + function WebEntityLaserInput(hand) { + this.hand = hand; + this.active = false; + this.previousLaserClickedTarget = false; + this.laserPressingTarget = false; + this.hover = false; + this.mode = "none"; + this.pressEnterLaserTarget = null; + this.laserTarget = null; + this.laserTargetID = null; + this.lastValidTargetID = null; + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.getOtherModule = function() { + return (this.hand === RIGHT_HAND) ? leftWebEntityLaserInput : rightWebEntityLaserInput; + }; + + this.parameters = makeDispatcherModuleParameters( + 550, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.requestTouchFocus = function(laserTarget) { + if (laserTarget !== null || laserTarget !== undefined) { + sendHoverEnterEventToLaserTarget(this.hand, this.laserTarget); + this.lastValidTargetID = laserTarget; + } + }; + + this.relinquishTouchFocus = function() { + // send hover leave event. + var pointerEvent = { type: "Move", id: this.hand + 1 }; + Entities.sendMouseMoveOnEntity(this.lastValidTargetID, pointerEvent); + Entities.sendHoverOverEntity(this.lastValidTargetID, pointerEvent); + Entities.sendHoverLeaveEntity(this.lastValidID, pointerEvent); + }; + + this.updateLaserTargets = function(controllerData) { + var intersection = controllerData.rayPicks[this.hand]; + this.laserTargetID = intersection.objectID; + var props = Entities.getEntityProperties(intersection.objectID); + this.laserTarget = calculateTargetFromEntity(intersection.intersection, props); + }; + + this.processControllerTriggers = function(controllerData) { + if (controllerData.triggerClicks[this.hand]) { + this.mode = "full"; + this.laserPressingTarget = true; + this.hover = false; + } else if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { + this.mode = "half"; + this.laserPressingTarget = false; + this.hover = true; + this.requestTouchFocus(this.laserTargetID); + } else { + this.mode = "none"; + this.laserPressingTarget = false; + this.hover = false; + this.relinquishTouchFocus(); + + } + }; + + this.hovering = function() { + if (!laserTargetHasKeyboardFocus(this.laserTagetID)) { + setKeyboardFocusOnLaserTarget(this.laserTargetID); + } + sendHoverOverEventToLaserTarget(this.hand, this.laserTarget); + }; + + this.laserPressEnter = function () { + sendTouchStartEventToLaserTarget(this.hand, this.laserTarget); + Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); + + this.touchingEnterTimer = 0; + this.pressEnterLaserTarget = this.laserTarget; + this.deadspotExpired = false; + + var LASER_PRESS_TO_MOVE_DEADSPOT = 0.026; + this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT) * this.laserTarget.distance; + }; + + this.laserPressExit = function () { + if (this.laserTarget === null) { + return; + } + + // send press event + if (this.deadspotExpired) { + sendTouchEndEventToLaserTarget(this.hand, this.laserTarget); + } else { + sendTouchEndEventToLaserTarget(this.hand, this.pressEnterLaserTarget); + } + }; + + this.laserPressing = function (controllerData, dt) { + this.touchingEnterTimer += dt; + + if (this.laserTarget) { + var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds + if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || + distance2D(this.laserTarget.position2D, + this.pressEnterLaserTarget.position2D) > this.deadspotRadius) { + sendTouchMoveEventToLaserTarget(this.hand, this.laserTarget); + this.deadspotExpired = true; + } + } else { + this.laserPressingTarget = false; + } + }; + + this.releaseTouchEvent = function() { + if (this.pressEnterLaserTarget === null) { + return; + } + + sendTouchEndEventToLaserTarget(this.hand, this.pressEnterLaserTarget); + }; + + this.updateLaserPointer = function(controllerData) { + var RADIUS = 0.005; + var dim = { x: RADIUS, y: RADIUS, z: RADIUS }; + + if (this.mode === "full") { + fullEnd.dimensions = dim; + LaserPointers.editRenderState(this.laserPointer, this.mode, {path: fullPath, end: fullEnd}); + } else if (this.mode === "half") { + halfEnd.dimensions = dim; + LaserPointers.editRenderState(this.laserPointer, this.mode, {path: halfPath, end: halfEnd}); + } + + LaserPointers.enableLaserPointer(this.laserPointer); + LaserPointers.setRenderState(this.laserPointer, this.mode); + }; + + this.isPointingAtWebEntity = function(controllerData) { + var intersection = controllerData.rayPicks[this.hand]; + var entityProperty = Entities.getEntityProperties(intersection.objectID); + var entityType = entityProperty.type; + + if ((intersection.type === RayPick.INTERSECTED_ENTITY && entityType === "Web")) { + return true; + } + return false; + }; + + this.exitModule = function() { + this.releaseTouchEvent(); + this.relinquishTouchFocus(); + this.reset(); + this.updateLaserPointer(); + LaserPointers.disableLaserPointer(this.laserPointer); + }; + + this.reset = function() { + this.pressEnterLaserTarget = null; + this.laserTarget = null; + this.laserTargetID = null; + this.laserPressingTarget = false; + this.previousLaserClickedTarget = null; + this.mode = "none"; + this.active = false; + }; + + this.isReady = function(controllerData) { + var otherModule = this.getOtherModule(); + if (this.isPointingAtWebEntity(controllerData) && !otherModule.active) { + return makeRunningValues(true, [], []); + } + + return makeRunningValues(false, [], []); + }; + + this.run = function(controllerData, deltaTime) { + if (!this.isPointingAtWebEntity(controllerData)) { + this.exitModule(); + return makeRunningValues(false, [], []); + } + + this.updateLaserTargets(controllerData); + this.processControllerTriggers(controllerData); + this.updateLaserPointer(controllerData); + + if (!this.previousLaserClickedTarget && this.laserPressingTarget) { + this.laserPressEnter(); + } + if (this.previousLaserClickedTarget && !this.laserPressingTarget) { + this.laserPressExit(); + } + this.previousLaserClickedTarget = this.laserPressingTarget; + + if (this.laserPressingTarget) { + this.laserPressing(controllerData, deltaTime); + } + + if (this.hover) { + this.hovering(); + } + return makeRunningValues(true, [], []); + }; + + this.cleanup = function() { + LaserPointers.disableLaserPointer(this.laserPointer); + LaserPointers.removeLaserPointer(this.laserPointer); + }; + + this.laserPointer = LaserPointers.createLaserPointer({ + joint: (this.hand === RIGHT_HAND) ? "_CONTROLLER_RIGHTHAND" : "_CONTROLLER_LEFTHAND", + filter: RayPick.PICK_ENTITIES, + maxDistance: PICK_MAX_DISTANCE, + posOffset: getGrabPointSphereOffset(this.handToController()), + renderStates: renderStates, + faceAvatar: true, + defaultRenderStates: defaultRenderStates + }); + } + + + var leftWebEntityLaserInput = new WebEntityLaserInput(LEFT_HAND); + var rightWebEntityLaserInput = new WebEntityLaserInput(RIGHT_HAND); + + enableDispatcherModule("LeftWebEntityLaserInput", leftWebEntityLaserInput); + enableDispatcherModule("RightWebEntityLaserInput", rightWebEntityLaserInput); + + this.cleanup = function() { + leftWebEntityLaserInput.cleanup(); + rightWebEntityLaserInput.cleanup(); + disableDispatcherModule("LeftWebEntityLaserInput"); + disableDispatcherModule("RightWebEntityLaserInput"); + }; + Script.scriptEnding.connect(this.cleanup); + +}()); diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index df11a1e5be..e8b07c623d 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -12,11 +12,24 @@ var CONTOLLER_SCRIPTS = [ "squeezeHands.js", "controllerDisplayManager.js", - "handControllerGrab.js", "handControllerPointer.js", "grab.js", - "teleport.js", "toggleAdvancedMovementForHandControllers.js", + "controllerDispatcher.js", + "controllerModules/nearParentGrabEntity.js", + "controllerModules/nearParentGrabOverlay.js", + "controllerModules/nearActionGrabEntity.js", + "controllerModules/farActionGrabEntity.js", + "controllerModules/tabletStylusInput.js", + "controllerModules/equipEntity.js", + "controllerModules/nearTrigger.js", + "controllerModules/overlayLaserInput.js", + "controllerModules/webEntityLaserInput.js", + "controllerModules/inEditMode.js", + "controllerModules/disableOtherModule.js", + "controllerModules/farTrigger.js", + "controllerModules/teleport.js", + "controllerModules/scaleAvatar.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js deleted file mode 100644 index f6eec7ab76..0000000000 --- a/scripts/system/controllers/handControllerGrab.js +++ /dev/null @@ -1,4192 +0,0 @@ -"use strict"; - -// handControllerGrab.js -// -// Created by Eric Levin on 9/2/15 -// Additions by James B. Pollack @imgntn on 9/24/2015 -// Additions By Seth Alves on 10/20/2015 -// Copyright 2015 High Fidelity, Inc. -// -// Grabs physically moveable entities with hydra-like controllers; it works for either near or far objects. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -/* global getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, - Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, - setGrabCommunications, Menu, HMD, isInEditMode, AvatarList */ -/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ - -(function() { // BEGIN LOCAL_SCOPE - -Script.include("/~/system/libraries/utils.js"); -Script.include("/~/system/libraries/Xform.js"); -Script.include("/~/system/libraries/controllers.js"); - -// -// add lines where the hand ray picking is happening -// - -var WANT_DEBUG = false; -var WANT_DEBUG_STATE = false; -var WANT_DEBUG_SEARCH_NAME = null; - -var UPDATE_SLEEP_MS = 16; // how many milliseconds to wait between "update" calls - -var FORCE_IGNORE_IK = false; -var SHOW_GRAB_POINT_SPHERE = false; - -// -// these tune time-averaging and "on" value for analog trigger -// - -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 THUMB_ON_VALUE = 0.5; - -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; - -// triggered when stylus presses a web overlay/entity -var HAPTIC_STYLUS_STRENGTH = 1.0; -var HAPTIC_STYLUS_DURATION = 20.0; - -// triggerd when ui laser presses a web overlay/entity -var HAPTIC_LASER_UI_STRENGTH = 1.0; -var HAPTIC_LASER_UI_DURATION = 20.0; - -var PICK_WITH_HAND_RAY = true; - -var EQUIP_SPHERE_SCALE_FACTOR = 0.65; - -var WEB_DISPLAY_STYLUS_DISTANCE = 0.5; -var WEB_STYLUS_LENGTH = 0.2; -var WEB_TOUCH_Y_OFFSET = 0.05; // how far forward (or back with a negative number) to slide stylus in hand - -// -// distant manipulation -// -var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object -var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position -var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified -var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified -var MOVE_WITH_HEAD = true; // experimental head-control of distantly held objects - -var COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { - red: 10, - green: 10, - blue: 255 -}; - -var COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { - red: 250, - green: 10, - blue: 10 -}; - -var COLORS_GRAB_DISTANCE_HOLD = { - red: 238, - green: 75, - blue: 214 -}; - -var PICK_MAX_DISTANCE = 500; // max length of pick-ray - -// -// near grabbing -// - -var EQUIP_RADIUS = 0.2; // radius used for palm vs equip-hotspot for equipping. -// if EQUIP_HOTSPOT_RENDER_RADIUS is greater than zero, the hotspot will appear before the hand -// has reached the required position, and then grow larger once the hand is close enough to equip. -var EQUIP_HOTSPOT_RENDER_RADIUS = 0.0; // radius used for palm vs equip-hotspot for rendering hot-spots -var MAX_EQUIP_HOTSPOT_RADIUS = 1.0; -var MAX_FAR_TO_NEAR_EQUIP_HOTSPOT_RADIUS = 0.5; // radius used for far to near equipping object. -var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position - -var NEAR_GRAB_RADIUS = 0.1; // radius used for palm vs object for near grabbing. -var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand -var FAR_TO_NEAR_GRAB_MAX_DISTANCE = 0.3; // In far to near grabbing conversion,grab the object if distancetoObject from hand is less than this. -var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. -var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed - -// if an equipped item is "adjusted" to be too far from the hand it's in, it will be unequipped. -var CHECK_TOO_FAR_UNEQUIP_TIME = 0.3; // seconds, duration between checks - - -var GRAB_POINT_SPHERE_RADIUS = NEAR_GRAB_RADIUS; -var GRAB_POINT_SPHERE_COLOR = { red: 240, green: 240, blue: 240 }; -var GRAB_POINT_SPHERE_ALPHA = 0.85; - -// -// other constants -// -var RIGHT_HAND = 1; -var LEFT_HAND = 0; - -var ZERO_VEC = { - x: 0, - y: 0, - z: 0 -}; - -var ONE_VEC = { - x: 1, - y: 1, - z: 1 -}; - -var NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; -var AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; - -var DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 }; -var INCHES_TO_METERS = 1.0 / 39.3701; - - -// these control how long an abandoned pointer line or action will hang around -var ACTION_TTL = 15; // seconds -var ACTION_TTL_REFRESH = 5; -var MSECS_PER_SEC = 1000.0; -var GRABBABLE_PROPERTIES = [ - "position", - "registrationPoint", - "rotation", - "gravity", - "collidesWith", - "dynamic", - "collisionless", - "locked", - "name", - "shapeType", - "parentID", - "parentJointIndex", - "density", - "dimensions", - "userData" -]; - -var GRABBABLE_DATA_KEY = "grabbableKey"; // shared with grab.js - -var DEFAULT_GRABBABLE_DATA = { - disableReleaseVelocity: false -}; - -// sometimes we want to exclude objects from being picked -var USE_BLACKLIST = true; -var blacklist = []; - -var hoveredEntityID = false; -var contextOverlayTimer = false; -var entityWithContextOverlay = false; -var contextualHand = -1; - -var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; -var FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"]; - -var holdEnabled = true; -var nearGrabEnabled = true; -var farGrabEnabled = true; -var myAvatarScalingEnabled = true; -var objectScalingEnabled = true; -var mostRecentSearchingHand = RIGHT_HAND; -var DEFAULT_SPHERE_MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx"; -var HARDWARE_MOUSE_ID = 0; // Value reserved for hardware mouse. - -// states for the state machine -var STATE_OFF = 0; -var STATE_SEARCHING = 1; -var STATE_DISTANCE_HOLDING = 2; -var STATE_DISTANCE_ROTATING = 3; -var STATE_NEAR_GRABBING = 4; -var STATE_NEAR_TRIGGER = 5; -var STATE_FAR_TRIGGER = 6; -var STATE_HOLD = 7; -var STATE_ENTITY_LASER_TOUCHING = 8; -var STATE_OVERLAY_LASER_TOUCHING = 9; -var STATE_STYLUS_TOUCHING = 10; - -var CONTROLLER_STATE_MACHINE = {}; - -CONTROLLER_STATE_MACHINE[STATE_OFF] = { - name: "off", - enterMethod: "offEnter", - updateMethod: "off" -}; -CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { - name: "searching", - enterMethod: "searchEnter", - exitMethod: "searchExit", - updateMethod: "search" -}; -CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { - name: "distance_holding", - enterMethod: "distanceHoldingEnter", - updateMethod: "distanceHolding" -}; -CONTROLLER_STATE_MACHINE[STATE_DISTANCE_ROTATING] = { - name: "distance_rotating", - enterMethod: "distanceRotatingEnter", - updateMethod: "distanceRotating" -}; -CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { - name: "near_grabbing", - enterMethod: "nearGrabbingEnter", - updateMethod: "nearGrabbing" -}; -CONTROLLER_STATE_MACHINE[STATE_HOLD] = { - name: "hold", - enterMethod: "nearGrabbingEnter", - updateMethod: "nearGrabbing" -}; -CONTROLLER_STATE_MACHINE[STATE_NEAR_TRIGGER] = { - name: "trigger", - enterMethod: "nearTriggerEnter", - updateMethod: "nearTrigger" -}; -CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = { - name: "far_trigger", - enterMethod: "farTriggerEnter", - updateMethod: "farTrigger" -}; -CONTROLLER_STATE_MACHINE[STATE_ENTITY_LASER_TOUCHING] = { - name: "entityLaserTouching", - enterMethod: "entityLaserTouchingEnter", - exitMethod: "entityLaserTouchingExit", - updateMethod: "entityLaserTouching" -}; -CONTROLLER_STATE_MACHINE[STATE_OVERLAY_LASER_TOUCHING] = { - name: "overlayLaserTouching", - enterMethod: "overlayLaserTouchingEnter", - exitMethod: "overlayLaserTouchingExit", - updateMethod: "overlayLaserTouching" -}; -CONTROLLER_STATE_MACHINE[STATE_STYLUS_TOUCHING] = { - name: "stylusTouching", - enterMethod: "stylusTouchingEnter", - exitMethod: "stylusTouchingExit", - updateMethod: "stylusTouching" -}; - -function distance2D(a, b) { - var dx = (a.x - b.x); - var dy = (a.y - b.y); - return Math.sqrt(dx * dx + dy * dy); -} - -function getFingerWorldLocation(hand) { - var fingerJointName = (hand === RIGHT_HAND) ? "RightHandIndex4" : "LeftHandIndex4"; - - var fingerJointIndex = MyAvatar.getJointIndex(fingerJointName); - var fingerPosition = MyAvatar.getAbsoluteJointTranslationInObjectFrame(fingerJointIndex); - var fingerRotation = MyAvatar.getAbsoluteJointRotationInObjectFrame(fingerJointIndex); - var worldFingerRotation = Quat.multiply(MyAvatar.orientation, fingerRotation); - var worldFingerPosition = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, fingerPosition)); - - return { - position: worldFingerPosition, - orientation: worldFingerRotation, - rotation: worldFingerRotation, - valid: true - }; -} - -// Object assign polyfill -if (typeof Object.assign != 'function') { - Object.assign = function(target, varArgs) { - if (target === null) { - throw new TypeError('Cannot convert undefined or null to object'); - } - var to = Object(target); - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - if (nextSource !== null) { - for (var nextKey in nextSource) { - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - return to; - }; -} - -function distanceBetweenPointAndEntityBoundingBox(point, entityProps) { - var entityXform = new Xform(entityProps.rotation, entityProps.position); - var localPoint = entityXform.inv().xformPoint(point); - var minOffset = Vec3.multiplyVbyV(entityProps.registrationPoint, entityProps.dimensions); - var maxOffset = Vec3.multiplyVbyV(Vec3.subtract(ONE_VEC, entityProps.registrationPoint), entityProps.dimensions); - var localMin = Vec3.subtract(entityXform.trans, minOffset); - var localMax = Vec3.sum(entityXform.trans, maxOffset); - - var v = {x: localPoint.x, y: localPoint.y, z: localPoint.z}; - v.x = Math.max(v.x, localMin.x); - v.x = Math.min(v.x, localMax.x); - v.y = Math.max(v.y, localMin.y); - v.y = Math.min(v.y, localMax.y); - v.z = Math.max(v.z, localMin.z); - v.z = Math.min(v.z, localMax.z); - - return Vec3.distance(v, localPoint); -} - -function projectOntoXYPlane(worldPos, position, rotation, dimensions, registrationPoint) { - var invRot = Quat.inverse(rotation); - var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, position)); - var invDimensions = { x: 1 / dimensions.x, - y: 1 / dimensions.y, - z: 1 / dimensions.z }; - var normalizedPos = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), registrationPoint); - return { x: normalizedPos.x * dimensions.x, - y: (1 - normalizedPos.y) * dimensions.y }; // flip y-axis -} - -function projectOntoEntityXYPlane(entityID, worldPos) { - var props = entityPropertiesCache.getProps(entityID); - if (props && props.position && props.rotation && props.dimensions && props.registrationPoint) { - return projectOntoXYPlane(worldPos, props.position, props.rotation, props.dimensions, props.registrationPoint); - } -} - -function projectOntoOverlayXYPlane(overlayID, worldPos) { - var position = Overlays.getProperty(overlayID, "position"); - var rotation = Overlays.getProperty(overlayID, "rotation"); - var dimensions; - - var dpi = Overlays.getProperty(overlayID, "dpi"); - if (dpi) { - // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale. - var resolution = Overlays.getProperty(overlayID, "resolution"); - resolution.z = 1; // Circumvent divide-by-zero. - var scale = Overlays.getProperty(overlayID, "dimensions"); - if (scale) { - scale.z = 0.01; // overlay dimensions are 2D, not 3D. - dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); - } - } else { - dimensions = Overlays.getProperty(overlayID, "dimensions"); - if (dimensions && dimensions.z) { - dimensions.z = 0.01; // overlay dimensions are 2D, not 3D. - } - } - - if (position && rotation && dimensions) { - return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT); - } -} - -function handLaserIntersectItem(position, rotation, start) { - var worldHandPosition = start.position; - var worldHandRotation = start.orientation; - - if (position) { - var planePosition = position; - var planeNormal = Vec3.multiplyQbyV(rotation, {x: 0, y: 0, z: 1.0}); - var rayStart = worldHandPosition; - var rayDirection = Quat.getUp(worldHandRotation); - var intersectionInfo = rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection); - - var intersectionPoint = planePosition; - if (intersectionInfo.hit && intersectionInfo.distance > 0) { - intersectionPoint = Vec3.sum(rayStart, Vec3.multiply(intersectionInfo.distance, rayDirection)); - } else { - intersectionPoint = planePosition; - } - intersectionInfo.point = intersectionPoint; - intersectionInfo.normal = planeNormal; - intersectionInfo.searchRay = { - origin: rayStart, - direction: rayDirection, - length: PICK_MAX_DISTANCE - }; - - return intersectionInfo; - } else { - // entity has been destroyed? or is no longer in cache - return null; - } -} - -function handLaserIntersectEntity(entityID, start) { - var props = entityPropertiesCache.getProps(entityID); - return handLaserIntersectItem(props.position, props.rotation, start); -} - -function handLaserIntersectOverlay(overlayID, start) { - var position = Overlays.getProperty(overlayID, "position"); - var rotation = Overlays.getProperty(overlayID, "rotation"); - return handLaserIntersectItem(position, rotation, start); -} - -function rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection) { - var rayDirectionDotPlaneNormal = Vec3.dot(rayDirection, planeNormal); - if (rayDirectionDotPlaneNormal > 0.00001 || rayDirectionDotPlaneNormal < -0.00001) { - var rayStartDotPlaneNormal = Vec3.dot(Vec3.subtract(planePosition, rayStart), planeNormal); - var distance = rayStartDotPlaneNormal / rayDirectionDotPlaneNormal; - return {hit: true, distance: distance}; - } else { - // ray is parallel to the plane - return {hit: false, distance: 0}; - } -} - -function stateToName(state) { - return CONTROLLER_STATE_MACHINE[state] ? CONTROLLER_STATE_MACHINE[state].name : "???"; -} - -function getTag() { - return "grab-" + MyAvatar.sessionUUID; -} - -function colorPow(color, power) { - return { - red: Math.pow(color.red / 255.0, power) * 255, - green: Math.pow(color.green / 255.0, power) * 255, - blue: Math.pow(color.blue / 255.0, power) * 255 - }; -} - -function entityHasActions(entityID) { - return Entities.getActionIDs(entityID).length > 0; -} - -function entityIsGrabbedByOther(entityID) { - // by convention, a distance grab sets the tag of its action to be grab-*owner-session-id*. - var actionIDs = Entities.getActionIDs(entityID); - for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) { - var actionID = actionIDs[actionIndex]; - var actionArguments = Entities.getActionArguments(entityID, actionID); - var tag = actionArguments.tag; - if (tag === getTag()) { - // we see a grab-*uuid* shaped tag, but it's our tag, so that's okay. - continue; - } - var GRAB_PREFIX_LENGTH = 5; - var UUID_LENGTH = 38; - if (tag && tag.slice(0, GRAB_PREFIX_LENGTH) == "grab-") { - // we see a grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it. - return tag.slice(GRAB_PREFIX_LENGTH, GRAB_PREFIX_LENGTH + UUID_LENGTH - 1); - } - } - return null; -} - -function propsArePhysical(props) { - if (!props.dynamic) { - return false; - } - var isPhysical = (props.shapeType && props.shapeType != 'none'); - return isPhysical; -} - -var USE_ATTACH_POINT_SETTINGS = true; - -var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; - -function getAttachPointSettings() { - try { - var str = Settings.getValue(ATTACH_POINT_SETTINGS); - if (str === "false") { - return {}; - } else { - return JSON.parse(str); - } - } catch (err) { - print("Error parsing attachPointSettings: " + err); - return {}; - } -} - -function setAttachPointSettings(attachPointSettings) { - var str = JSON.stringify(attachPointSettings); - Settings.setValue(ATTACH_POINT_SETTINGS, str); -} - -function getAttachPointForHotspotFromSettings(hotspot, hand) { - var attachPointSettings = getAttachPointSettings(); - var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; - var joints = attachPointSettings[hotspot.key]; - if (joints) { - return joints[jointName]; - } else { - return undefined; - } -} - -function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, offsetRotation) { - var attachPointSettings = getAttachPointSettings(); - var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; - var joints = attachPointSettings[hotspot.key]; - if (!joints) { - joints = {}; - attachPointSettings[hotspot.key] = joints; - } - joints[jointName] = [offsetPosition, offsetRotation]; - setAttachPointSettings(attachPointSettings); -} - -// If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, -// and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. -var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; - -function isEditing() { - return EXTERNALLY_MANAGED_2D_MINOR_MODE && isInEditMode(); -} - -function isIn2DMode() { - // In this version, we make our own determination of whether we're aimed a HUD element, - // because other scripts (such as handControllerPointer) might be using some other visualization - // instead of setting Reticle.visible. - return (EXTERNALLY_MANAGED_2D_MINOR_MODE && - (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position))); -} - -function restore2DMode() { - if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) { - Reticle.setVisible(true); - } -} - -function stylusTargetHasKeyboardFocus(stylusTarget) { - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - return Entities.keyboardFocusEntity === stylusTarget.entityID; - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - return Overlays.keyboardFocusOverlay === stylusTarget.overlayID; - } -} - -function setKeyboardFocusOnStylusTarget(stylusTarget) { - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID && Entities.wantsHandControllerPointerEvents(stylusTarget.entityID)) { - Overlays.keyboardFocusOverlay = NULL_UUID; - Entities.keyboardFocusEntity = stylusTarget.entityID; - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.keyboardFocusOverlay = stylusTarget.overlayID; - Entities.keyboardFocusEntity = NULL_UUID; - } -} - -function sendHoverEnterEventToStylusTarget(hand, stylusTarget) { - var pointerEvent = { - type: "Move", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: stylusTarget.position2D, - pos3D: stylusTarget.position, - normal: stylusTarget.normal, - direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), - button: "None" - }; - - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - Entities.sendHoverEnterEntity(stylusTarget.entityID, pointerEvent); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.sendHoverEnterOverlay(stylusTarget.overlayID, pointerEvent); - } -} - -function sendHoverOverEventToStylusTarget(hand, stylusTarget) { - var pointerEvent = { - type: "Move", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: stylusTarget.position2D, - pos3D: stylusTarget.position, - normal: stylusTarget.normal, - direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), - button: "None" - }; - - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent); - Entities.sendHoverOverEntity(stylusTarget.entityID, pointerEvent); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent); - Overlays.sendHoverOverOverlay(stylusTarget.overlayID, pointerEvent); - } -} - -function sendTouchStartEventToStylusTarget(hand, stylusTarget) { - var pointerEvent = { - type: "Press", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: stylusTarget.position2D, - pos3D: stylusTarget.position, - normal: stylusTarget.normal, - direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), - button: "Primary", - isPrimaryHeld: true - }; - - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - Entities.sendMousePressOnEntity(stylusTarget.entityID, pointerEvent); - Entities.sendClickDownOnEntity(stylusTarget.entityID, pointerEvent); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.sendMousePressOnOverlay(stylusTarget.overlayID, pointerEvent); - } -} - -function sendTouchEndEventToStylusTarget(hand, stylusTarget) { - var pointerEvent = { - type: "Release", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: stylusTarget.position2D, - pos3D: stylusTarget.position, - normal: stylusTarget.normal, - direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), - button: "Primary" - }; - - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - Entities.sendMouseReleaseOnEntity(stylusTarget.entityID, pointerEvent); - Entities.sendClickReleaseOnEntity(stylusTarget.entityID, pointerEvent); - Entities.sendHoverLeaveEntity(stylusTarget.entityID, pointerEvent); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.sendMouseReleaseOnOverlay(stylusTarget.overlayID, pointerEvent); - } -} - -function sendTouchMoveEventToStylusTarget(hand, stylusTarget) { - var pointerEvent = { - type: "Move", - id: hand + 1, // 0 is reserved for hardware mouse - pos2D: stylusTarget.position2D, - pos3D: stylusTarget.position, - normal: stylusTarget.normal, - direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), - button: "Primary", - isPrimaryHeld: true - }; - - if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { - Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent); - Entities.sendHoldingClickOnEntity(stylusTarget.entityID, pointerEvent); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { - Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent); - } -} - -// will return undefined if entity does not exist. -function calculateStylusTargetFromEntity(stylusTip, entityID) { - var props = entityPropertiesCache.getProps(entityID); - if (props.rotation === undefined) { - // if rotation is missing from props object, then this entity has probably been deleted. - return; - } - - // project stylus tip onto entity plane. - var normal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1}); - Vec3.multiplyQbyV(props.rotation, {x: 0, y: 1, z: 0}); - var distance = Vec3.dot(Vec3.subtract(stylusTip.position, props.position), normal); - var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance)); - - // generate normalized coordinates - var invRot = Quat.inverse(props.rotation); - var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, props.position)); - var invDimensions = { x: 1 / props.dimensions.x, y: 1 / props.dimensions.y, z: 1 / props.dimensions.z }; - var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint); - - // 2D position on entity plane in meters, relative to the bounding box upper-left hand corner. - var position2D = { x: normalizedPosition.x * props.dimensions.x, y: (1 - normalizedPosition.y) * props.dimensions.y }; // flip y-axis - - return { - entityID: entityID, - overlayID: null, - distance: distance, - position: position, - position2D: position2D, - normal: normal, - normalizedPosition: normalizedPosition, - dimensions: props.dimensions, - valid: true - }; -} - -// will return undefined if overlayID does not exist. -function calculateStylusTargetFromOverlay(stylusTip, overlayID) { - var overlayPosition = Overlays.getProperty(overlayID, "position"); - if (overlayPosition === undefined) { - return; - } - - // project stylusTip onto overlay plane. - var overlayRotation = Overlays.getProperty(overlayID, "rotation"); - if (overlayRotation === undefined) { - return; - } - var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1}); - var distance = Vec3.dot(Vec3.subtract(stylusTip.position, overlayPosition), normal); - var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance)); - - // calclulate normalized position - var invRot = Quat.inverse(overlayRotation); - var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, overlayPosition)); - var dpi = Overlays.getProperty(overlayID, "dpi"); - - var dimensions; - if (dpi) { - // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale. - var resolution = Overlays.getProperty(overlayID, "resolution"); - if (resolution === undefined) { - return; - } - resolution.z = 1; // Circumvent divide-by-zero. - var scale = Overlays.getProperty(overlayID, "dimensions"); - if (scale === undefined) { - return; - } - scale.z = 0.01; // overlay dimensions are 2D, not 3D. - dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); - } else { - dimensions = Overlays.getProperty(overlayID, "dimensions"); - if (dimensions === undefined) { - return; - } - if (!dimensions.z) { - dimensions.z = 0.01; // sometimes overlay dimensions are 2D, not 3D. - } - } - var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z }; - var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT); - - // 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner. - var position2D = { x: normalizedPosition.x * dimensions.x, y: (1 - normalizedPosition.y) * dimensions.y }; // flip y-axis - - return { - entityID: null, - overlayID: overlayID, - distance: distance, - position: position, - position2D: position2D, - normal: normal, - normalizedPosition: normalizedPosition, - dimensions: dimensions, - valid: true - }; -} - -function isNearStylusTarget(stylusTargets, edgeBorder, minNormalDistance, maxNormalDistance) { - for (var i = 0; i < stylusTargets.length; i++) { - var stylusTarget = stylusTargets[i]; - - // check to see if the projected stylusTip is within within the 2d border - var borderMin = {x: -edgeBorder, y: -edgeBorder}; - var borderMax = {x: stylusTarget.dimensions.x + edgeBorder, y: stylusTarget.dimensions.y + edgeBorder}; - if (stylusTarget.distance >= minNormalDistance && stylusTarget.distance <= maxNormalDistance && - stylusTarget.position2D.x >= borderMin.x && stylusTarget.position2D.y >= borderMin.y && - stylusTarget.position2D.x <= borderMax.x && stylusTarget.position2D.y <= borderMax.y) { - return true; - } - } - return false; -} - -function calculateNearestStylusTarget(stylusTargets) { - var nearestStylusTarget; - - for (var i = 0; i < stylusTargets.length; i++) { - var stylusTarget = stylusTargets[i]; - - if ((!nearestStylusTarget || stylusTarget.distance < nearestStylusTarget.distance) && - stylusTarget.normalizedPosition.x >= 0 && stylusTarget.normalizedPosition.y >= 0 && - stylusTarget.normalizedPosition.x <= 1 && stylusTarget.normalizedPosition.y <= 1) { - nearestStylusTarget = stylusTarget; - } - } - - return nearestStylusTarget; -} - -// 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.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(entityID) { - _this.addEntity(entityID); - }); -}; -EntityPropertiesCache.prototype._updateCacheEntry = function(entityID) { - var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); - - // convert props.userData from a string to an object. - var userData = {}; - if (props.userData) { - try { - userData = JSON.parse(props.userData); - } catch (err) { - print("WARNING: malformed userData on " + entityID + ", name = " + props.name + ", error = " + err); - } - } - props.userData = userData; - props.refCount = 1; - - this.cache[entityID] = props; -}; -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]; - return obj ? obj : undefined; -}; -EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { - var props = this.cache[entityID]; - if (props) { - return props.userData.grabbableKey ? props.userData.grabbableKey : DEFAULT_GRABBABLE_DATA; - } else { - return undefined; - } -}; -EntityPropertiesCache.prototype.getGrabProps = function(entityID) { - var props = this.cache[entityID]; - if (props) { - return props.userData.grabKey ? props.userData.grabKey : {}; - } else { - return undefined; - } -}; -EntityPropertiesCache.prototype.getWearableProps = function(entityID) { - var props = this.cache[entityID]; - if (props) { - return props.userData.wearable ? props.userData.wearable : {}; - } else { - return undefined; - } -}; -EntityPropertiesCache.prototype.getEquipHotspotsProps = function(entityID) { - var props = this.cache[entityID]; - if (props) { - return props.userData.equipHotspots ? props.userData.equipHotspots : {}; - } else { - return undefined; - } -}; - -// 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; - - // override default sphere with a user specified model, if it exists. - overlayInfoSet.overlays.push(Overlays.addOverlay("model", { - name: "hotspot overlay", - url: hotspot.modelURL ? hotspot.modelURL : DEFAULT_SPHERE_MODEL_URL, - position: hotspot.worldPosition, - rotation: { - x: 0, - y: 0, - z: 0, - w: 1 - }, - dimensions: diameter * EQUIP_SPHERE_SCALE_FACTOR, - scale: hotspot.modelScale, - ignoreRayIntersection: true - })); - overlayInfoSet.type = "model"; - this.map[hotspot.key] = overlayInfoSet; - } else { - overlayInfoSet.timestamp = timestamp; - } -}; -EquipHotspotBuddy.prototype.updateHotspots = function(hotspots, timestamp) { - var _this = this; - hotspots.forEach(function(hotspot) { - _this.updateHotspot(hotspot, timestamp); - }); - this.highlightedHotspots = []; -}; -EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp) { - - 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(Overlays.deleteOverlay); - 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 - }); - }); - } - } -}; - -function getControllerJointIndex(hand) { - if (HMD.isHandControllerAvailable()) { - var controllerJointIndex = -1; - if (Camera.mode === "first person") { - controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND"); - } else if (Camera.mode === "third person") { - controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? - "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : - "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); - } - - return controllerJointIndex; - } - - return MyAvatar.getJointIndex("Head"); -} - -// global EquipHotspotBuddy instance -var equipHotspotBuddy = new EquipHotspotBuddy(); - -var halfPath = { - type: "line3d", - color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, - visible: true, - alpha: 1, - solid: true, - glow: 1.0, - lineWidth: 5, - ignoreRayIntersection: true, // always ignore this - drawInFront: true, // Even when burried inside of something, show it. - parentID: AVATAR_SELF_ID -} -var halfEnd = { - type: "sphere", - solid: true, - color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE, - alpha: 0.9, - ignoreRayIntersection: true, - drawInFront: true, // Even when burried inside of something, show it. - visible: true -} -var fullPath = { - type: "line3d", - color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, - visible: true, - alpha: 1, - solid: true, - glow: 1.0, - lineWidth: 5, - ignoreRayIntersection: true, // always ignore this - drawInFront: true, // Even when burried inside of something, show it. - parentID: AVATAR_SELF_ID -} -var fullEnd = { - type: "sphere", - solid: true, - color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE, - alpha: 0.9, - ignoreRayIntersection: true, - drawInFront: true, // Even when burried inside of something, show it. - visible: true -} -var holdPath = { - type: "line3d", - color: COLORS_GRAB_DISTANCE_HOLD, - visible: true, - alpha: 1, - solid: true, - glow: 1.0, - lineWidth: 5, - ignoreRayIntersection: true, // always ignore this - drawInFront: true, // Even when burried inside of something, show it. - parentID: AVATAR_SELF_ID -} - -var renderStates = [{name: "half", path: halfPath, end: halfEnd}, - {name: "full", path: fullPath, end: fullEnd}, - {name: "hold", path: holdPath}]; -var headRenderStates = [{name: "half", end: halfEnd}, - {name: "full", end: fullEnd}, - {name: "hold", path: holdPath}]; - -// how far from camera to search intersection? -var DEFAULT_SEARCH_SPHERE_DISTANCE = 1000; -var defaultRenderStates = [{name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: halfPath}, - {name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: fullPath}, - {name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: holdPath}]; - -function MyController(hand) { - this.hand = hand; - this.autoUnequipCounter = 0; - this.grabPointIntersectsEntity = false; - this.stylus = null; - this.homeButtonTouched = false; - this.editTriggered = false; - - // Until there is some reliable way to keep track of a "stack" of parentIDs, we'll have problems - // when more than one avatar does parenting grabs on things. This script tries to work - // around this with two associative arrays: previousParentID and previousParentJointIndex. If - // (1) avatar-A does a parenting grab on something, and then (2) avatar-B takes it, and (3) avatar-A - // releases it and then (4) avatar-B releases it, then avatar-B will set the parent back to - // avatar-A's hand. Avatar-A is no longer grabbing it, so it will end up triggering avatar-A's - // checkForUnexpectedChildren which will put it back to wherever it was when avatar-A initially grabbed it. - // this will work most of the time, unless avatar-A crashes or logs out while avatar-B is grabbing the - // entity. This can also happen when a single avatar passes something from hand to hand. - this.previousParentID = {}; - this.previousParentJointIndex = {}; - this.previouslyUnhooked = {}; - - this.shouldScale = false; - this.isScalingAvatar = false; - - // handPosition is where the avatar's hand appears to be, in-world. - this.getHandPosition = function () { - if (this.hand === RIGHT_HAND) { - return MyAvatar.getRightPalmPosition(); - } else { - return MyAvatar.getLeftPalmPosition(); - } - }; - this.getHandRotation = function () { - if (this.hand === RIGHT_HAND) { - return MyAvatar.getRightPalmRotation(); - } else { - return MyAvatar.getLeftPalmRotation(); - } - }; - - this.handToController = function() { - return (hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; - }; - - this.actionID = null; // action this script created... - this.grabbedThingID = null; // on this entity. - this.grabbedOverlay = null; - this.state = STATE_OFF; - - this.triggerValue = 0; // rolling average of trigger value - this.triggerClicked = false; - this.rawTriggerValue = 0; - this.rawSecondaryValue = 0; - this.rawThumbValue = 0; - - // for visualizations - this.halfEnd = halfEnd; - this.fullEnd = fullEnd; - this.laserPointer = LaserPointers.createLaserPointer({ - joint: (hand == RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", - filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS | RayPick.PICK_INCLUDE_NONCOLLIDABLE, - maxDistance: PICK_MAX_DISTANCE, - posOffset: getGrabPointSphereOffset(this.handToController()), - renderStates: renderStates, - faceAvatar: true, - defaultRenderStates: defaultRenderStates - }); - this.headLaserPointer = LaserPointers.createLaserPointer({ - joint: "Avatar", - filter: RayPick.PICK_ENTITIES | RayPick.PICK_OVERLAYS | RayPick.PICK_INCLUDE_NONCOLLIDABLE, - maxDistance: PICK_MAX_DISTANCE, - renderStates: headRenderStates, - faceAvatar: true, - defaultRenderStates: defaultRenderStates - }); - LaserPointers.setIgnoreOverlays(this.laserPointer, [HMD.tabletID]); - LaserPointers.setIgnoreOverlays(this.headLaserPointer, [HMD.tabletID]); - - this.otherGrabbingUUID = null; - - this.waitForTriggerRelease = false; - - this.intersectionDistance = 0.0; - - this.ignoreIK = false; - this.offsetPosition = Vec3.ZERO; - this.offsetRotation = Quat.IDENTITY; - - this.lastPickTime = 0; - this.lastUnequipCheckTime = 0; - - this.equipOverlayInfoSetMap = {}; - - this.tabletStabbed = false; - this.tabletStabbedPos2D = null; - this.tabletStabbedPos3D = null; - - this.useFingerInsteadOfStylus = false; - this.fingerPointing = false; - - // initialize stylus tip - var DEFAULT_STYLUS_TIP = { - position: {x: 0, y: 0, z: 0}, - orientation: {x: 0, y: 0, z: 0, w: 0}, - rotation: {x: 0, y: 0, z: 0, w: 0}, - velocity: {x: 0, y: 0, z: 0}, - valid: false - }; - this.stylusTip = DEFAULT_STYLUS_TIP; - - var _this = this; - - var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; - 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.updateStylusTip = function() { - if (this.useFingerInsteadOfStylus) { - this.stylusTip = getFingerWorldLocation(this.hand); - } else { - this.stylusTip = getControllerWorldLocation(this.handToController(), true); - - // translate tip forward according to constant. - var TIP_OFFSET = {x: 0, y: WEB_STYLUS_LENGTH - WEB_TOUCH_Y_OFFSET, z: 0}; - this.stylusTip.position = Vec3.sum(this.stylusTip.position, Vec3.multiplyQbyV(this.stylusTip.orientation, TIP_OFFSET)); - } - - // compute tip velocity from hand controller motion, it is more accurate then computing it from previous positions. - var pose = Controller.getPoseValue(this.handToController()); - if (pose.valid) { - var worldControllerPos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation)); - var worldControllerLinearVel = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); - var worldControllerAngularVel = Vec3.multiplyQbyV(MyAvatar.orientation, pose.angularVelocity); - var tipVelocity = Vec3.sum(worldControllerLinearVel, Vec3.cross(worldControllerAngularVel, Vec3.subtract(this.stylusTip.position, worldControllerPos))); - this.stylusTip.velocity = tipVelocity; - } else { - this.stylusTip.velocity = {x: 0, y: 0, z: 0}; - } - }; - - this.update = function(deltaTime, timestamp) { - this.updateSmoothedTrigger(); - this.maybeScaleMyAvatar(); - - this.updateStylusTip(); - - var DEFAULT_USE_FINGER_AS_STYLUS = false; - var USE_FINGER_AS_STYLUS = Settings.getValue("preferAvatarFingerOverStylus"); - if (USE_FINGER_AS_STYLUS === "") { - USE_FINGER_AS_STYLUS = DEFAULT_USE_FINGER_AS_STYLUS; - } - if (USE_FINGER_AS_STYLUS && MyAvatar.getJointIndex("LeftHandIndex4") !== -1) { - this.useFingerInsteadOfStylus = true; - } else { - this.useFingerInsteadOfStylus = false; - } - - if (this.ignoreInput()) { - - // Most hand input is disabled, because we are interacting with the 2d hud. - // However, we still should check for collisions of the stylus with the web overlay. - this.processStylus(); - this.turnOffVisualizations(); - return; - } - - if (CONTROLLER_STATE_MACHINE[this.state]) { - var updateMethodName = CONTROLLER_STATE_MACHINE[this.state].updateMethod; - var updateMethod = this[updateMethodName]; - if (updateMethod) { - updateMethod.call(this, deltaTime, timestamp); - } else { - print("WARNING: could not find updateMethod for state " + stateToName(this.state)); - } - } else { - print("WARNING: could not find state " + this.state + " in state machine"); - } - }; - - this.callEntityMethodOnGrabbed = function(entityMethodName) { - if (this.grabbedIsOverlay) { - return; - } - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.grabbedThingID, entityMethodName, args); - }; - - this.setState = function(newState, reason) { - if ((isInEditMode() && this.grabbedThingID !== HMD.tabletID) && - (newState !== STATE_OFF && - newState !== STATE_SEARCHING && - newState !== STATE_STYLUS_TOUCHING && - newState !== STATE_OVERLAY_LASER_TOUCHING)) { - return; - } - setGrabCommunications((newState === STATE_DISTANCE_HOLDING) || - (newState === STATE_DISTANCE_ROTATING) || - (newState === STATE_NEAR_GRABBING)); - if (WANT_DEBUG || WANT_DEBUG_STATE) { - var oldStateName = stateToName(this.state); - var newStateName = stateToName(newState); - print("STATE (" + this.hand + "): " + this.state + "-" + newStateName + - " <-- " + oldStateName + ", reason = " + reason); - } - - // exit the old state - if (CONTROLLER_STATE_MACHINE[this.state]) { - var exitMethodName = CONTROLLER_STATE_MACHINE[this.state].exitMethod; - var exitMethod = this[exitMethodName]; - if (exitMethod) { - exitMethod.call(this); - } - } else { - print("WARNING: could not find state " + this.state + " in state machine"); - } - - this.state = newState; - - // enter the new state - if (CONTROLLER_STATE_MACHINE[newState]) { - var enterMethodName = CONTROLLER_STATE_MACHINE[newState].enterMethod; - var enterMethod = this[enterMethodName]; - if (enterMethod) { - enterMethod.call(this); - } - } else { - print("WARNING: could not find newState " + newState + " in state machine"); - } - }; - - this.grabPointSphereOn = function() { - if (!SHOW_GRAB_POINT_SPHERE) { - return; - } - - if (!this.grabPointSphere) { - this.grabPointSphere = Overlays.addOverlay("sphere", { - name: "grabPointSphere", - localPosition: getGrabPointSphereOffset(this.handToController()), - localRotation: { x: 0, y: 0, z: 0, w: 1 }, - dimensions: GRAB_POINT_SPHERE_RADIUS * 2, - color: GRAB_POINT_SPHERE_COLOR, - alpha: GRAB_POINT_SPHERE_ALPHA, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false, - parentID: AVATAR_SELF_ID, - parentJointIndex: this.controllerJointIndex - }); - } - }; - - this.grabPointSphereOff = function() { - if (this.grabPointSphere) { - Overlays.deleteOverlay(this.grabPointSphere); - this.grabPointSphere = null; - } - }; - - this.showStylus = function() { - if (this.stylus) { - return; - } - - var stylusProperties = { - name: "stylus", - url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx", - loadPriority: 10.0, - localPosition: Vec3.sum({ x: 0.0, - y: WEB_TOUCH_Y_OFFSET, - z: 0.0 }, - getGrabPointSphereOffset(this.handToController())), - localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), - dimensions: { x: 0.01, y: 0.01, z: WEB_STYLUS_LENGTH }, - solid: true, - visible: true, - ignoreRayIntersection: true, - drawInFront: false, - parentID: AVATAR_SELF_ID, - parentJointIndex: MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : - "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND") - }; - this.stylus = Overlays.addOverlay("model", stylusProperties); - }; - - this.hideStylus = function() { - if (!this.stylus) { - return; - } - Overlays.deleteOverlay(this.stylus); - this.stylus = null; - }; - - this.updateLaserPointer = function() { - var SEARCH_SPHERE_SIZE = 0.011; - var MIN_SPHERE_SIZE = 0.0005; - var radius = Math.max(1.2 * SEARCH_SPHERE_SIZE * this.intersectionDistance, MIN_SPHERE_SIZE); - var dim = {x: radius, y: radius, z: radius}; - var mode = "hold"; - if (this.state !== STATE_DISTANCE_HOLDING && this.state !== STATE_DISTANCE_ROTATING) { - mode = (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? "full" : "half"; - } - - var laserPointerID = PICK_WITH_HAND_RAY ? this.laserPointer : this.headLaserPointer; - if (mode === "full") { - var fullEndToEdit = PICK_WITH_HAND_RAY ? this.fullEnd : fullEnd; - fullEndToEdit.dimensions = dim; - LaserPointers.editRenderState(laserPointerID, mode, {path: fullPath, end: fullEndToEdit}); - } else if (mode === "half") { - var halfEndToEdit = PICK_WITH_HAND_RAY ? this.halfEnd : halfEnd; - halfEndToEdit.dimensions = dim; - LaserPointers.editRenderState(laserPointerID, mode, {path: halfPath, end: halfEndToEdit}); - } - LaserPointers.enableLaserPointer(laserPointerID); - LaserPointers.setRenderState(laserPointerID, mode); - if (this.state === STATE_DISTANCE_HOLDING || this.state === STATE_DISTANCE_ROTATING) { - LaserPointers.setLockEndUUID(laserPointerID, this.grabbedThingID, this.grabbedIsOverlay); - } else { - LaserPointers.setLockEndUUID(laserPointerID, null, false); - } - }; - - this.laserPointerOff = function() { - var laserPointerID = PICK_WITH_HAND_RAY ? this.laserPointer : this.headLaserPointer; - LaserPointers.disableLaserPointer(laserPointerID); - }; - - this.evalLightWorldTransform = function(modelPos, modelRot) { - - var MODEL_LIGHT_POSITION = { - x: 0, - y: -0.3, - z: 0 - }; - - var MODEL_LIGHT_ROTATION = Quat.angleAxis(-90, { - x: 1, - y: 0, - z: 0 - }); - - return { - p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, MODEL_LIGHT_POSITION)), - q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION) - }; - }; - - this.turnOffVisualizations = function() { - this.grabPointSphereOff(); - this.laserPointerOff(); - restore2DMode(); - }; - - this.triggerPress = function(value) { - _this.rawTriggerValue = value; - }; - - this.triggerClick = function(value) { - _this.triggerClicked = value; - }; - - this.secondaryPress = function(value) { - _this.rawSecondaryValue = value; - }; - - 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() { - return this.triggerClicked; - }; - - this.triggerSmoothedSqueezed = function() { - return this.triggerValue > TRIGGER_ON_VALUE; - }; - - this.triggerSmoothedReleased = function() { - return this.triggerValue < TRIGGER_OFF_VALUE; - }; - - this.secondarySqueezed = function() { - return _this.rawSecondaryValue > BUMPER_ON_VALUE; - }; - - this.secondaryReleased = function() { - return _this.rawSecondaryValue < BUMPER_ON_VALUE; - }; - - // this.triggerOrsecondarySqueezed = function () { - // return triggerSmoothedSqueezed() || secondarySqueezed(); - // } - - // this.triggerAndSecondaryReleased = function () { - // return triggerSmoothedReleased() && secondaryReleased(); - // } - - this.thumbPress = function(value) { - _this.rawThumbValue = value; - }; - - this.thumbPressed = function() { - return _this.rawThumbValue > THUMB_ON_VALUE; - }; - - this.thumbReleased = function() { - return _this.rawThumbValue < THUMB_ON_VALUE; - }; - - this.stealTouchFocus = function(stylusTarget) { - // send hover events to target - // record the entity or overlay we are hovering over. - if ((stylusTarget.entityID === this.getOtherHandController().hoverEntity) || - (stylusTarget.overlayID === this.getOtherHandController().hoverOverlay)) { - this.getOtherHandController().relinquishTouchFocus(); - } - this.requestTouchFocus(stylusTarget); - }; - - this.requestTouchFocus = function(stylusTarget) { - - // send hover events to target if we can. - // record the entity or overlay we are hovering over. - if (stylusTarget.entityID && stylusTarget.entityID !== this.hoverEntity && stylusTarget.entityID !== this.getOtherHandController().hoverEntity) { - this.hoverEntity = stylusTarget.entityID; - sendHoverEnterEventToStylusTarget(this.hand, stylusTarget); - } else if (stylusTarget.overlayID && stylusTarget.overlayID !== this.hoverOverlay && stylusTarget.overlayID !== this.getOtherHandController().hoverOverlay) { - this.hoverOverlay = stylusTarget.overlayID; - sendHoverEnterEventToStylusTarget(this.hand, stylusTarget); - } - }; - - this.hasTouchFocus = function(stylusTarget) { - return ((stylusTarget.entityID && stylusTarget.entityID === this.hoverEntity) || - (stylusTarget.overlayID && stylusTarget.overlayID === this.hoverOverlay)); - }; - - this.relinquishTouchFocus = function() { - - // send hover leave event. - var pointerEvent = { type: "Move", id: this.hand + 1 }; - if (this.hoverEntity) { - Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); - this.hoverEntity = null; - } else if (this.hoverOverlay) { - Overlays.sendMouseMoveOnOverlay(this.hoverOverlay, pointerEvent); - Overlays.sendHoverOverOverlay(this.hoverOverlay, pointerEvent); - Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); - this.hoverOverlay = null; - } - }; - - this.pointFinger = function(value) { - var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index"; - if (this.fingerPointing !== value) { - var message; - if (this.hand === RIGHT_HAND) { - message = { pointRightIndex: value }; - } else { - message = { pointLeftIndex: value }; - } - Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify(message), true); - this.fingerPointing = value; - } - }; - - this.processStylus = function() { - if (!this.stylusTip.valid) { - this.pointFinger(false); - this.hideStylus(); - return; - } - - if (this.useFingerInsteadOfStylus) { - this.hideStylus(); - } - - var tipPosition = this.stylusTip.position; - - // build list of stylus targets, near the stylusTip - var stylusTargets = []; - var candidateEntities = Entities.findEntities(tipPosition, WEB_DISPLAY_STYLUS_DISTANCE); - entityPropertiesCache.addEntities(candidateEntities); - var i, props, stylusTarget; - for (i = 0; i < candidateEntities.length; i++) { - props = entityPropertiesCache.getProps(candidateEntities[i]); - if (props && (props.type === "Web" || this.isTablet(candidateEntities[i]))) { - stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, candidateEntities[i]); - if (stylusTarget) { - stylusTargets.push(stylusTarget); - } - } - } - - // add the tabletScreen, if it is valid - if (HMD.tabletScreenID && HMD.tabletScreenID !== NULL_UUID && Overlays.getProperty(HMD.tabletScreenID, "visible")) { - stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.tabletScreenID); - if (stylusTarget) { - stylusTargets.push(stylusTarget); - } - } - - // add the tablet home button. - if (HMD.homeButtonID && HMD.homeButtonID !== NULL_UUID && Overlays.getProperty(HMD.homeButtonID, "visible")) { - stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.homeButtonID); - if (stylusTarget) { - stylusTargets.push(stylusTarget); - } - } - - var TABLET_MIN_HOVER_DISTANCE = 0.01; - var TABLET_MAX_HOVER_DISTANCE = 0.1; - var TABLET_MIN_TOUCH_DISTANCE = -0.05; - var TABLET_MAX_TOUCH_DISTANCE = TABLET_MIN_HOVER_DISTANCE; - var EDGE_BORDER = 0.075; - - var hysteresisOffset = 0.0; - if (this.isNearStylusTarget) { - hysteresisOffset = 0.05; - } - - this.isNearStylusTarget = isNearStylusTarget(stylusTargets, EDGE_BORDER + hysteresisOffset, - TABLET_MIN_TOUCH_DISTANCE - hysteresisOffset, WEB_DISPLAY_STYLUS_DISTANCE + hysteresisOffset); - - if (this.isNearStylusTarget) { - if (!this.useFingerInsteadOfStylus) { - this.showStylus(); - } else { - this.pointFinger(true); - } - } else { - this.hideStylus(); - this.pointFinger(false); - } - - var nearestStylusTarget = calculateNearestStylusTarget(stylusTargets); - - if (nearestStylusTarget && nearestStylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && - nearestStylusTarget.distance < TABLET_MAX_HOVER_DISTANCE) { - - this.requestTouchFocus(nearestStylusTarget); - - if (!stylusTargetHasKeyboardFocus(nearestStylusTarget)) { - setKeyboardFocusOnStylusTarget(nearestStylusTarget); - } - - if (this.hasTouchFocus(nearestStylusTarget)) { - sendHoverOverEventToStylusTarget(this.hand, nearestStylusTarget); - } - - // filter out presses when tip is moving away from tablet. - // ensure that stylus is within bounding box by checking normalizedPosition - if (nearestStylusTarget.valid && nearestStylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && - nearestStylusTarget.distance < TABLET_MAX_TOUCH_DISTANCE && Vec3.dot(this.stylusTip.velocity, nearestStylusTarget.normal) < 0 && - nearestStylusTarget.normalizedPosition.x >= 0 && nearestStylusTarget.normalizedPosition.x <= 1 && - nearestStylusTarget.normalizedPosition.y >= 0 && nearestStylusTarget.normalizedPosition.y <= 1) { - - var name; - if (nearestStylusTarget.entityID) { - name = entityPropertiesCache.getProps(nearestStylusTarget.entityID).name; - this.stylusTarget = nearestStylusTarget; - this.setState(STATE_STYLUS_TOUCHING, "begin touching entity '" + name + "'"); - } else if (nearestStylusTarget.overlayID) { - name = Overlays.getProperty(nearestStylusTarget.overlayID, "name"); - this.stylusTarget = nearestStylusTarget; - this.setState(STATE_STYLUS_TOUCHING, "begin touching overlay '" + name + "'"); - } - } - } else { - this.relinquishTouchFocus(); - } - - this.homeButtonTouched = false; - }; - - this.off = function(deltaTime, timestamp) { - - this.controllerJointIndex = getControllerJointIndex(this.hand); - this.checkForUnexpectedChildren(); - - if (this.editTriggered) { - this.editTriggered = false; - } - - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.waitForTriggerRelease = false; - } - if (!this.waitForTriggerRelease && (this.triggerSmoothedSqueezed() || this.secondarySqueezed())) { - this.lastPickTime = 0; - this.startingHandRotation = getControllerWorldLocation(this.handToController(), true).orientation; - this.searchStartTime = Date.now(); - this.setState(STATE_SEARCHING, "trigger squeeze detected"); - return; - } - - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var worldHandPosition = controllerLocation.position; - - var candidateEntities = Entities.findEntities(worldHandPosition, MAX_EQUIP_HOTSPOT_RADIUS); - entityPropertiesCache.addEntities(candidateEntities); - var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntities); - if (!this.waitForTriggerRelease) { - this.updateEquipHaptics(potentialEquipHotspot, worldHandPosition); - } - - var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); - equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); - if (potentialEquipHotspot) { - equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); - } - - // when the grab-point enters a grabable entity, give a haptic pulse - candidateEntities = Entities.findEntities(worldHandPosition, NEAR_GRAB_RADIUS); - var grabbableEntities = candidateEntities.filter(function(entity) { - return _this.entityIsNearGrabbable(entity, worldHandPosition, NEAR_GRAB_MAX_DISTANCE); - }); - if (grabbableEntities.length > 0) { - if (!this.grabPointIntersectsEntity) { - // don't do haptic pulse for tablet - var nonTabletEntities = grabbableEntities.filter(function(entityID) { - return entityID != HMD.tabletID && entityID != HMD.homeButtonID; - }); - if (nonTabletEntities.length > 0) { - Controller.triggerHapticPulse(1, 20, this.hand); - } - this.grabPointIntersectsEntity = true; - this.grabPointSphereOn(); - } - } else { - this.grabPointIntersectsEntity = false; - this.grabPointSphereOff(); - } - - this.processStylus(); - - if (isInEditMode() && !this.isNearStylusTarget && HMD.isHandControllerAvailable()) { - // Always showing lasers while in edit mode and hands/stylus is not active. - - var rayPickInfo = this.calcRayPickInfo(this.hand); - if (rayPickInfo.isValid) { - this.intersectionDistance = (rayPickInfo.entityID || rayPickInfo.overlayID) ? rayPickInfo.distance : 0; - this.updateLaserPointer(); - } else { - this.laserPointerOff(); - } - } else { - this.laserPointerOff(); - } - }; - - this.handleLaserOnHomeButton = function(rayPickInfo) { - if (rayPickInfo.overlayID && this.triggerSmoothedGrab()) { - var homeButton = rayPickInfo.overlayID; - var hmdHomeButton = HMD.homeButtonID; - if (homeButton === hmdHomeButton) { - if (this.homeButtonTouched === false) { - this.homeButtonTouched = true; - Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); - Messages.sendLocalMessage("home", homeButton); - } - } else { - this.homeButtonTouched = false; - } - } else { - this.homeButtonTouched = false; - } - }; - - 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; - }; - - // 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) { - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var worldHandPosition = controllerLocation.position; - var worldHandRotation = controllerLocation.orientation; - - var pickRay; - var valid = true - - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var worldHandPosition = controllerLocation.position; - var worldHandRotation = controllerLocation.orientation; - valid = !(worldHandPosition === undefined); - - pickRay = { - origin: PICK_WITH_HAND_RAY ? worldHandPosition : MyAvatar.getHeadPosition(), - direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Quat.getFront(Camera.orientation), - length: PICK_MAX_DISTANCE - }; - - var result = { - entityID: null, - overlayID: null, - searchRay: pickRay, - distance: PICK_MAX_DISTANCE, - isValid: valid - }; - - var laserPointerID = PICK_WITH_HAND_RAY ? this.laserPointer : this.headLaserPointer; - var intersection = LaserPointers.getPrevRayPickResult(laserPointerID); - - if (intersection.type != RayPick.INTERSECTED_NONE) { - return { - entityID: intersection.type == RayPick.INTERSECTED_ENTITY ? intersection.objectID : null, - overlayID: intersection.type == RayPick.INTERSECTED_OVERLAY ? intersection.objectID : null, - searchRay: pickRay, - distance: intersection.distance, - intersection: intersection.intersection, - normal: intersection.surfaceNormal - }; - } else { - return result; - } - }; - - this.entityWantsTrigger = function(entityID) { - var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); - return grabbableProps && grabbableProps.wantsTrigger; - }; - - // returns a list of all equip-hotspots assosiated with this entity. - // @param {UUID} entityID - // @returns {Object[]} array of objects with the following fields. - // * key {string} a string that can be used to uniquely identify this hotspot - // * entityID {UUID} - // * localPosition {Vec3} position of the hotspot in object space. - // * worldPosition {vec3} position of the hotspot in world space. - // * radius {number} radius of equip hotspot - // * joints {Object} keys are joint names values are arrays of two elements: - // offset position {Vec3} and offset rotation {Quat}, both are in the coordinate system of the joint. - // * modelURL {string} url for model to use instead of default sphere. - // * modelScale {Vec3} scale factor for model - this.collectEquipHotspots = function(entityID) { - var result = []; - var props = entityPropertiesCache.getProps(entityID); - var entityXform = new Xform(props.rotation, props.position); - var equipHotspotsProps = entityPropertiesCache.getEquipHotspotsProps(entityID); - if (equipHotspotsProps && equipHotspotsProps.length > 0) { - var i, length = equipHotspotsProps.length; - for (i = 0; i < length; i++) { - var hotspot = equipHotspotsProps[i]; - if (hotspot.position && hotspot.radius && hotspot.joints) { - result.push({ - key: entityID.toString() + i.toString(), - entityID: entityID, - localPosition: hotspot.position, - worldPosition: entityXform.xformPoint(hotspot.position), - radius: hotspot.radius, - joints: hotspot.joints, - modelURL: hotspot.modelURL, - modelScale: hotspot.modelScale - }); - } - } - } else { - var wearableProps = entityPropertiesCache.getWearableProps(entityID); - if (wearableProps && wearableProps.joints) { - result.push({ - key: entityID.toString() + "0", - entityID: entityID, - localPosition: { - x: 0, - y: 0, - z: 0 - }, - worldPosition: entityXform.pos, - radius: EQUIP_RADIUS, - joints: wearableProps.joints, - modelURL: null, - modelScale: null - }); - } - } - return result; - }; - - this.hotspotIsEquippable = function(hotspot) { - var props = entityPropertiesCache.getProps(hotspot.entityID); - var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - - var otherHandControllerState = this.getOtherHandController().state; - var okToEquipFromOtherHand = ((otherHandControllerState === STATE_NEAR_GRABBING || - otherHandControllerState === STATE_DISTANCE_HOLDING || - otherHandControllerState === STATE_DISTANCE_ROTATING) && - this.getOtherHandController().grabbedThingID === hotspot.entityID); - var hasParent = true; - if (props.parentID === NULL_UUID) { - hasParent = false; - } - if ((hasParent || entityHasActions(hotspot.entityID)) && !okToEquipFromOtherHand) { - if (debug) { - print("equip is skipping '" + props.name + "': grabbed by someone else"); - } - return false; - } - - return true; - }; - this.entityIsCloneable = function(entityID) { - var entityProps = entityPropertiesCache.getGrabbableProps(entityID); - var props = entityPropertiesCache.getProps(entityID); - if (!props) { - return false; - } - - if (entityProps.hasOwnProperty("cloneable")) { - return entityProps.cloneable; - } - return false; - }; - this.entityIsGrabbable = function(entityID) { - var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); - var props = entityPropertiesCache.getProps(entityID); - if (!props) { - return false; - } - var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - var grabbable = propsArePhysical(props); - if (grabbableProps.hasOwnProperty("grabbable")) { - grabbable = grabbableProps.grabbable; - } - - if (!grabbable && !grabbableProps.wantsTrigger) { - if (debug) { - print("grab is skipping '" + props.name + "': not grabbable."); - } - return false; - } - if (FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0) { - if (debug) { - print("grab is skipping '" + props.name + "': forbidden entity type."); - } - return false; - } - if (props.locked && !grabbableProps.wantsTrigger) { - if (debug) { - print("grab is skipping '" + props.name + "': locked and not triggerable."); - } - return false; - } - if (FORBIDDEN_GRAB_NAMES.indexOf(props.name) >= 0) { - if (debug) { - print("grab is skipping '" + props.name + "': forbidden name."); - } - return false; - } - - return true; - }; - - this.entityIsDistanceGrabbable = function(entityID, handPosition) { - if (!this.entityIsGrabbable(entityID)) { - return false; - } - - var props = entityPropertiesCache.getProps(entityID); - var distance = Vec3.distance(props.position, handPosition); - var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - - // we can't distance-grab non-physical - var isPhysical = propsArePhysical(props); - if (!isPhysical) { - if (debug) { - print("distance grab is skipping '" + props.name + "': not physical"); - } - return false; - } - - if (distance > PICK_MAX_DISTANCE) { - // too far away, don't grab - if (debug) { - print("distance grab is skipping '" + props.name + "': too far away."); - } - return false; - } - - this.otherGrabbingUUID = entityIsGrabbedByOther(entityID); - if (this.otherGrabbingUUID !== null) { - // don't distance grab something that is already grabbed. - if (debug) { - print("distance grab is skipping '" + props.name + "': already grabbed by another."); - } - return false; - } - - return true; - }; - - this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { - - if (!this.entityIsCloneable(entityID) && !this.entityIsGrabbable(entityID)) { - return false; - } - - var props = entityPropertiesCache.getProps(entityID); - var distance = Vec3.distance(props.position, handPosition); - var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - - if (distance > maxDistance) { - // too far away, don't grab - if (debug) { - print(" grab is skipping '" + props.name + "': too far away."); - } - return false; - } - - return true; - }; - - this.entityIsFarToNearGrabbable = function (objectPosition, handPosition, maxDistance) { - var distanceToObjectFromHand = Vec3.length(Vec3.subtract(handPosition, objectPosition)); - - if (distanceToObjectFromHand > maxDistance) { - return false; - } - return true; - }; - - 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, getControllerWorldLocation(_this.handToController(), true).position) < - hotspot.radius + distance); - }); - return equippableHotspots; - }; - - this.chooseBestEquipHotspot = function(candidateEntities) { - var DISTANCE = 0; - var equippableHotspots = this.chooseNearEquipHotspots(candidateEntities, DISTANCE); - var _this = this; - if (equippableHotspots.length > 0) { - // sort by distance - equippableHotspots.sort(function(a, b) { - var handControllerLocation = getControllerWorldLocation(_this.handToController(), true); - var aDistance = Vec3.distance(a.worldPosition, handControllerLocation.position); - var bDistance = Vec3.distance(b.worldPosition, handControllerLocation.position); - return aDistance - bDistance; - }); - return equippableHotspots[0]; - } else { - return null; - } - }; - - this.chooseNearEquipHotspotsForFarToNearEquip = function(candidateEntities, distance) { - var equippableHotspots = flatten(candidateEntities.map(function(entityID) { - return _this.collectEquipHotspots(entityID); - })).filter(function(hotspot) { - return (Vec3.distance(hotspot.worldPosition, getControllerWorldLocation(_this.handToController(), true).position) < - hotspot.radius + distance); - }); - return equippableHotspots; - }; - - this.chooseBestEquipHotspotForFarToNearEquip = function(candidateEntities) { - var DISTANCE = 1; - var equippableHotspots = this.chooseNearEquipHotspotsForFarToNearEquip(candidateEntities, DISTANCE); - var _this = this; - if (equippableHotspots.length > 0) { - // sort by distance - equippableHotspots.sort(function(a, b) { - var handControllerLocation = getControllerWorldLocation(_this.handToController(), true); - var aDistance = Vec3.distance(a.worldPosition, handControllerLocation.position); - var bDistance = Vec3.distance(b.worldPosition, handControllerLocation.position); - return aDistance - bDistance; - }); - return equippableHotspots[0]; - } else { - return null; - } - }; - - this.searchEnter = function() { - mostRecentSearchingHand = this.hand; - var rayPickInfo = this.calcRayPickInfo(this.hand); - if (rayPickInfo.entityID || rayPickInfo.overlayID) { - this.intersectionDistance = rayPickInfo.distance; - } - }; - - this.searchExit = function () { - contextualHand = -1; - if (hoveredEntityID) { - Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); - } - hoveredEntityID = false; - }; - - this.search = function(deltaTime, timestamp) { - var _this = this; - var name; - var FAR_SEARCH_DELAY = 0; // msecs before search beam appears - - var farSearching = this.triggerSmoothedSqueezed() && (Date.now() - this.searchStartTime > FAR_SEARCH_DELAY); - - this.grabbedThingID = null; - this.grabbedOverlay = null; - this.isInitialGrab = false; - this.preparingHoldRelease = false; - - this.checkForUnexpectedChildren(); - - if ((this.triggerSmoothedReleased() && this.secondaryReleased())) { - this.grabbedThingID = null; - this.setState(STATE_OFF, "trigger released"); - return; - } - - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var handPosition = controllerLocation.position; - - var rayPickInfo = this.calcRayPickInfo(this.hand); - - if (rayPickInfo.entityID) { - entityPropertiesCache.addEntity(rayPickInfo.entityID); - } - - pointerEvent = { - type: "Move", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(rayPickInfo.entityID, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - if (rayPickInfo.entityID) { - if (hoveredEntityID !== rayPickInfo.entityID) { - if (contextOverlayTimer) { - Script.clearTimeout(contextOverlayTimer); - contextOverlayTimer = false; - } - if (hoveredEntityID) { - Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); - } - hoveredEntityID = rayPickInfo.entityID; - Entities.sendHoverEnterEntity(hoveredEntityID, pointerEvent); - } - - // If we already have a context overlay, we don't want to move it to - // another entity while we're searching. - if (!entityWithContextOverlay && !contextOverlayTimer) { - contextOverlayTimer = Script.setTimeout(function () { - if (rayPickInfo.entityID === hoveredEntityID && - !entityWithContextOverlay && - contextualHand !== -1 && - contextOverlayTimer) { - var pointerEvent = { - type: "Move", - id: contextualHand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(rayPickInfo.entityID, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "Secondary" - }; - if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.entityID, pointerEvent)) { - entityWithContextOverlay = rayPickInfo.entityID; - hoveredEntityID = false; - } - } - contextOverlayTimer = false; - }, 500); - contextualHand = this.hand; - } - } else { - if (hoveredEntityID) { - Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); - hoveredEntityID = false; - } - if (contextOverlayTimer) { - Script.clearTimeout(contextOverlayTimer); - contextOverlayTimer = false; - } - } - - var candidateHotSpotEntities = Entities.findEntities(handPosition, MAX_EQUIP_HOTSPOT_RADIUS); - entityPropertiesCache.addEntities(candidateHotSpotEntities); - - var potentialEquipHotspot = this.chooseBestEquipHotspot(candidateHotSpotEntities); - if (potentialEquipHotspot) { - if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && holdEnabled) { - this.grabbedHotspot = potentialEquipHotspot; - this.grabbedThingID = potentialEquipHotspot.entityID; - this.grabbedIsOverlay = false; - this.setState(STATE_HOLD, "equipping '" + entityPropertiesCache.getProps(this.grabbedThingID).name + "'"); - - return; - } - } - - var candidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS); - var grabbableEntities = candidateEntities.filter(function(entity) { - return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_MAX_DISTANCE); - }); - - var candidateOverlays = Overlays.findOverlays(handPosition, NEAR_GRAB_RADIUS); - var grabbableOverlays = candidateOverlays.filter(function(overlayID) { - return Overlays.getProperty(overlayID, "grabbable"); - }); - - if (rayPickInfo.entityID) { - this.intersectionDistance = rayPickInfo.distance; - if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) { - grabbableEntities.push(rayPickInfo.entityID); - } - } else if (rayPickInfo.overlayID) { - this.intersectionDistance = rayPickInfo.distance; - } else { - this.intersectionDistance = 0; - } - - if (grabbableOverlays.length > 0) { - grabbableOverlays.sort(function(a, b) { - var aPosition = Overlays.getProperty(a, "position"); - var aDistance = Vec3.distance(aPosition, handPosition); - var bPosition = Overlays.getProperty(b, "position"); - var bDistance = Vec3.distance(bPosition, handPosition); - return aDistance - bDistance; - }); - this.grabbedThingID = grabbableOverlays[0]; - this.grabbedIsOverlay = true; - if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && nearGrabEnabled) { - this.setState(STATE_NEAR_GRABBING, "near grab overlay '" + - Overlays.getProperty(this.grabbedThingID, "name") + "'"); - return; - } - } - - var entity; - if (grabbableEntities.length > 0) { - // sort by distance - grabbableEntities.sort(function(a, b) { - var aDistance = Vec3.distance(entityPropertiesCache.getProps(a).position, handPosition); - var bDistance = Vec3.distance(entityPropertiesCache.getProps(b).position, handPosition); - return aDistance - bDistance; - }); - entity = grabbableEntities[0]; - if (!isInEditMode() || entity == HMD.tabletID) { // tablet is grabbable, even when editing - name = entityPropertiesCache.getProps(entity).name; - this.grabbedThingID = entity; - this.grabbedIsOverlay = false; - if (this.entityWantsTrigger(entity)) { - if (this.triggerSmoothedGrab()) { - this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); - return; - } - } else { - // If near something grabbable, grab it! - if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && nearGrabEnabled) { - this.setState(STATE_NEAR_GRABBING, "near grab entity '" + name + "'"); - return; - } - } - } - } - - if (rayPickInfo.distance >= WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET) { - this.handleLaserOnHomeButton(rayPickInfo); - if (this.handleLaserOnWebEntity(rayPickInfo)) { - return; - } - if (this.handleLaserOnWebOverlay(rayPickInfo)) { - return; - } - } - - if (isInEditMode()) { - this.updateLaserPointer(); - if (this.triggerSmoothedGrab()) { - if (!this.editTriggered){ - if (rayPickInfo.entityID) { - Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ - method: "selectEntity", - entityID: rayPickInfo.entityID - })); - } else if (rayPickInfo.overlayID) { - Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ - method: "selectOverlay", - overlayID: rayPickInfo.overlayID - })); - } - } - this.editTriggered = true; - } - Reticle.setVisible(false); - return; - } - - if (rayPickInfo.entityID) { - entity = rayPickInfo.entityID; - name = entityPropertiesCache.getProps(entity).name; - if (this.entityWantsTrigger(entity)) { - if (this.triggerSmoothedGrab()) { - this.grabbedThingID = entity; - this.grabbedIsOverlay = false; - this.setState(STATE_FAR_TRIGGER, "far trigger '" + name + "'"); - return; - } else { - // potentialFarTriggerEntity = entity; - } - this.laserPointerOff(); - } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { - if (this.triggerSmoothedGrab() && !isEditing() && farGrabEnabled && farSearching) { - this.grabbedThingID = entity; - this.grabbedIsOverlay = false; - this.grabbedDistance = rayPickInfo.distance; - if (this.getOtherHandController().state === STATE_DISTANCE_HOLDING) { - this.setState(STATE_DISTANCE_ROTATING, "distance rotate '" + name + "'"); - } else { - this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); - } - return; - } else { - // potentialFarGrabEntity = entity; - } - this.laserPointerOff(); - } else if (this.otherGrabbingUUID !== null) { - if (this.triggerSmoothedGrab() && !isEditing() && farGrabEnabled && farSearching) { - this.updateLaserPointer(); - } else { - this.laserPointerOff(); - } - } else { - this.laserPointerOff(); - } - } else { - this.laserPointerOff(); - } - - this.updateEquipHaptics(potentialEquipHotspot, handPosition); - - var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); - equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); - if (potentialEquipHotspot) { - equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); - } - - if (farGrabEnabled && farSearching) { - this.updateLaserPointer(); - } - Reticle.setVisible(false); - }; - - this.isTablet = function (entityID) { - if (entityID === HMD.tabletID) { - return true; - } - return false; - }; - - this.handleLaserOnWebEntity = function (rayPickInfo) { - var pointerEvent; - - if (rayPickInfo.entityID && Entities.wantsHandControllerPointerEvents(rayPickInfo.entityID)) { - var entity = rayPickInfo.entityID; - var name = entityPropertiesCache.getProps(entity).name; - - if (Entities.keyboardFocusEntity != entity) { - Overlays.keyboardFocusOverlay = 0; - Entities.keyboardFocusEntity = entity; - - pointerEvent = { - type: "Move", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - if (this.hoverEntity !== entity) { - Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); - this.hoverEntity = entity; - Entities.sendHoverEnterEntity(this.hoverEntity, pointerEvent); - } - } - - // send mouse events for button highlights and tooltips. - if (this.hand == mostRecentSearchingHand || - (this.hand !== mostRecentSearchingHand && - this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_STYLUS_TOUCHING && - this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { - - // most recently searching hand has priority over other hand, for the purposes of button highlighting. - pointerEvent = { - type: "Move", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - Entities.sendMouseMoveOnEntity(entity, pointerEvent); - Entities.sendHoverOverEntity(entity, pointerEvent); - } - - if (this.triggerSmoothedGrab()) { - this.grabbedThingID = entity; - this.grabbedIsOverlay = false; - this.setState(STATE_ENTITY_LASER_TOUCHING, "begin touching entity '" + name + "'"); - return true; - } - - } else if (this.hoverEntity) { - pointerEvent = { - type: "Move", - id: this.hand + 1 - }; - Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); - this.hoverEntity = null; - } - - return false; - }; - - this.handleLaserOnWebOverlay = function (rayPickInfo) { - var pointerEvent; - if (rayPickInfo.overlayID) { - var overlay = rayPickInfo.overlayID; - if ((Overlays.getProperty(overlay, "type") === "web3d") && Overlays.keyboardFocusOverlay != overlay) { - Entities.keyboardFocusEntity = null; - Overlays.keyboardFocusOverlay = overlay; - } - - pointerEvent = { - type: "Move", - id: HARDWARE_MOUSE_ID, - pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - if (this.hoverOverlay !== overlay) { - Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); - this.hoverOverlay = overlay; - Overlays.sendHoverEnterOverlay(this.hoverOverlay, pointerEvent); - } - - // Send mouse events for button highlights and tooltips. - if (this.hand == mostRecentSearchingHand || - (this.hand !== mostRecentSearchingHand && - this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_STYLUS_TOUCHING && - this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { - - // most recently searching hand has priority over other hand, for the purposes of button highlighting. - pointerEvent = { - type: "Move", - id: HARDWARE_MOUSE_ID, - pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - Overlays.sendMouseMoveOnOverlay(overlay, pointerEvent); - Overlays.sendHoverOverOverlay(overlay, pointerEvent); - } - - if (this.triggerSmoothedGrab()) { - this.grabbedOverlay = overlay; - this.setState(STATE_OVERLAY_LASER_TOUCHING, "begin touching overlay '" + overlay + "'"); - return true; - } - - } else if (this.hoverOverlay) { - pointerEvent = { - type: "Move", - id: HARDWARE_MOUSE_ID - }; - Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); - this.hoverOverlay = null; - } - - return false; - }; - - this.distanceGrabTimescale = function(mass, distance) { - var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass / - DISTANCE_HOLDING_UNITY_MASS * distance / - DISTANCE_HOLDING_UNITY_DISTANCE; - if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) { - timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; - } - return timeScale; - }; - - this.getMass = function(dimensions, density) { - return (dimensions.x * dimensions.y * dimensions.z) * density; - }; - - this.ensureDynamic = function () { - // if we distance hold something and keep it very still before releasing it, it ends up - // non-dynamic in bullet. If it's too still, give it a little bounce so it will fall. - var props = Entities.getEntityProperties(this.grabbedThingID, ["velocity", "dynamic", "parentID"]); - if (props.dynamic && props.parentID == NULL_UUID) { - var velocity = props.velocity; - if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD - velocity = { x: 0.0, y: 0.2, z: 0.0 }; - Entities.editEntity(this.grabbedThingID, { velocity: velocity }); - } - } - }; - - this.distanceHoldingEnter = function() { - this.clearEquipHaptics(); - this.grabPointSphereOff(); - - this.shouldScale = false; - - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var worldControllerPosition = controllerLocation.position; - var worldControllerRotation = controllerLocation.orientation; - - // transform the position into room space - var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); - var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); - - var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); - var now = Date.now(); - - // add the action and initialize some variables - this.currentObjectPosition = grabbedProperties.position; - this.currentObjectRotation = grabbedProperties.rotation; - this.currentObjectTime = now; - this.currentCameraOrientation = Camera.orientation; - - this.grabRadius = this.grabbedDistance; - this.grabRadialVelocity = 0.0; - - // offset between controller vector at the grab radius and the entity position - var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); - targetPosition = Vec3.sum(targetPosition, worldControllerPosition); - this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); - - // compute a constant based on the initial conditions which we use below to exaggerate hand motion - // onto the held object - this.radiusScalar = Math.log(this.grabRadius + 1.0); - if (this.radiusScalar < 1.0) { - this.radiusScalar = 1.0; - } - - // compute the mass for the purpose of energy and how quickly to move object - this.mass = this.getMass(grabbedProperties.dimensions, grabbedProperties.density); - var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, grabbedProperties.position)); - var timeScale = this.distanceGrabTimescale(this.mass, distanceToObject); - this.linearTimeScale = timeScale; - this.actionID = NULL_UUID; - this.actionID = Entities.addAction("far-grab", this.grabbedThingID, { - targetPosition: this.currentObjectPosition, - linearTimeScale: timeScale, - targetRotation: this.currentObjectRotation, - angularTimeScale: timeScale, - tag: getTag(), - ttl: ACTION_TTL - }); - if (this.actionID === NULL_UUID) { - this.actionID = null; - } - this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); - - if (this.actionID !== null) { - this.callEntityMethodOnGrabbed("startDistanceGrab"); - } - - Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - this.turnOffVisualizations(); - this.previousRoomControllerPosition = roomControllerPosition; - }; - - this.distanceHolding = function(deltaTime, timestamp) { - - if (!this.triggerClicked) { - this.callEntityMethodOnGrabbed("releaseGrab"); - this.ensureDynamic(); - this.setState(STATE_OFF, "trigger released"); - if (this.getOtherHandController().state === STATE_DISTANCE_ROTATING) { - this.getOtherHandController().setState(STATE_SEARCHING, "trigger released on holding controller"); - // Can't set state of other controller to STATE_DISTANCE_HOLDING because then either: - // (a) The entity would jump to line up with the formerly rotating controller's orientation, or - // (b) The grab beam would need an orientation offset to the controller's true orientation. - // Neither of these options is good, so instead set STATE_SEARCHING and subsequently let the formerly distance - // rotating controller start distance holding the entity if it happens to be pointing at the entity. - } - return; - } - - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var worldControllerPosition = controllerLocation.position; - var worldControllerRotation = controllerLocation.orientation; - - // also transform the position into room space - var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); - var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); - - var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); - var now = Date.now(); - var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds - this.currentObjectTime = now; - - // the action was set up when this.distanceHolding was called. update the targets. - var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) * - this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR; - if (radius < 1.0) { - radius = 1.0; - } - - var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition); - var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta); - var handMoved = Vec3.multiply(worldHandDelta, radius); - this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); - - this.callEntityMethodOnGrabbed("continueDistantGrab"); - - var defaultMoveWithHeadData = { - disableMoveWithHead: false - }; - - // Update radialVelocity - var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime); - var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition)); - var newRadialVelocity = Vec3.dot(lastVelocity, delta); - - var VELOCITY_AVERAGING_TIME = 0.016; - var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME; - if (blendFactor < 0.0) { - blendFactor = 0.0; - } else if (blendFactor > 1.0) { - blendFactor = 1.0; - } - this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity; - - var RADIAL_GRAB_AMPLIFIER = 10.0; - if (Math.abs(this.grabRadialVelocity) > 0.0) { - this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * - this.grabRadius * RADIAL_GRAB_AMPLIFIER); - } - - // don't let grabRadius go all the way to zero, because it can't come back from that - var MINIMUM_GRAB_RADIUS = 0.1; - if (this.grabRadius < MINIMUM_GRAB_RADIUS) { - this.grabRadius = MINIMUM_GRAB_RADIUS; - } - var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); - newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition); - newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition); - var objectToAvatar = Vec3.subtract(this.currentObjectPosition, MyAvatar.position); - var handControllerData = getEntityCustomData('handControllerKey', this.grabbedThingID, defaultMoveWithHeadData); - if (handControllerData.disableMoveWithHead !== true) { - // mix in head motion - if (MOVE_WITH_HEAD) { - var objDistance = Vec3.length(objectToAvatar); - var before = Vec3.multiplyQbyV(this.currentCameraOrientation, { - x: 0.0, - y: 0.0, - z: objDistance - }); - var after = Vec3.multiplyQbyV(Camera.orientation, { - x: 0.0, - y: 0.0, - z: objDistance - }); - var change = Vec3.subtract(before, after) * (PICK_WITH_HAND_RAY ? 0.0 : 1.0); - this.currentCameraOrientation = Camera.orientation; - this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, change); - } - } - - this.maybeScale(grabbedProperties); - - // visualizations - this.updateLaserPointer(); - - var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition)); - - var candidateHotSpotEntities = - Entities.findEntities(controllerLocation.position,MAX_FAR_TO_NEAR_EQUIP_HOTSPOT_RADIUS); - entityPropertiesCache.addEntities(candidateHotSpotEntities); - - var potentialEquipHotspot = this.chooseBestEquipHotspotForFarToNearEquip(candidateHotSpotEntities); - if (potentialEquipHotspot && (potentialEquipHotspot.entityID == this.grabbedThingID)) { - if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && holdEnabled) { - this.grabbedHotspot = potentialEquipHotspot; - this.grabbedThingID = potentialEquipHotspot.entityID; - this.grabbedIsOverlay = false; - - Entities.deleteAction(this.grabbedThingID, this.actionID); - this.actionID = null; - - this.setState(STATE_HOLD, "equipping '" + entityPropertiesCache.getProps(this.grabbedThingID).name + "'"); - return; - } - } - var rayPositionOnEntity = Vec3.subtract(grabbedProperties.position, this.offsetPosition); - //Far to Near Grab: If object is draw by user inside FAR_TO_NEAR_GRAB_MAX_DISTANCE, grab it - if (this.entityIsFarToNearGrabbable(rayPositionOnEntity, - controllerLocation.position, - FAR_TO_NEAR_GRAB_MAX_DISTANCE)) { - this.farToNearGrab = true; - - Entities.deleteAction(this.grabbedThingID, this.actionID); - this.actionID = null; - - this.setState(STATE_NEAR_GRABBING , "near grab entity '" + this.grabbedThingID + "'"); - return; - } - - this.linearTimeScale = (this.linearTimeScale / 2); - if (this.linearTimeScale <= DISTANCE_HOLDING_ACTION_TIMEFRAME) { - this.linearTimeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; - } - var success = Entities.updateAction(this.grabbedThingID, this.actionID, { - targetPosition: newTargetPosition, - linearTimeScale: this.linearTimeScale, - targetRotation: this.currentObjectRotation, - angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject), - ttl: ACTION_TTL - }); - if (success) { - this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); - } else { - print("continueDistanceHolding -- updateAction failed"); - } - - this.previousRoomControllerPosition = roomControllerPosition; - }; - - this.distanceRotatingEnter = function() { - this.clearEquipHaptics(); - this.grabPointSphereOff(); - - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var worldControllerPosition = controllerLocation.position; - var worldControllerRotation = controllerLocation.orientation; - - var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); - this.currentObjectPosition = grabbedProperties.position; - this.grabRadius = this.grabbedDistance; - - // Offset between controller vector at the grab radius and the entity position. - var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); - targetPosition = Vec3.sum(targetPosition, worldControllerPosition); - this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); - - // Initial controller rotation. - this.previousWorldControllerRotation = worldControllerRotation; - - Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - this.turnOffVisualizations(); - }; - - this.distanceRotating = function(deltaTime, timestamp) { - - if (!this.triggerClicked) { - this.callEntityMethodOnGrabbed("releaseGrab"); - this.ensureDynamic(); - this.setState(STATE_OFF, "trigger released"); - return; - } - - var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); - - // Delta rotation of grabbing controller since last update. - var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation; - var controllerRotationDelta = Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation)); - - // Rotate entity by twice the delta rotation. - controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta); - - // Perform the rotation in the translation controller's action update. - this.getOtherHandController().currentObjectRotation = Quat.multiply(controllerRotationDelta, - this.getOtherHandController().currentObjectRotation); - - // Rotate about the translation controller's target position. - this.offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, this.offsetPosition); - this.getOtherHandController().offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, - this.getOtherHandController().offsetPosition); - - this.updateLaserPointer(); - - this.previousWorldControllerRotation = worldControllerRotation; - }; - - this.setupHoldAction = function() { - this.actionID = Entities.addAction("hold", this.grabbedThingID, { - hand: this.hand === RIGHT_HAND ? "right" : "left", - timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, - relativePosition: this.offsetPosition, - relativeRotation: this.offsetRotation, - ttl: ACTION_TTL, - kinematic: this.kinematicGrab, - kinematicSetVelocity: true, - ignoreIK: this.ignoreIK - }); - if (this.actionID === NULL_UUID) { - this.actionID = null; - return false; - } - var now = Date.now(); - this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); - return true; - }; - - this.projectVectorAlongAxis = function(position, axisStart, axisEnd) { - var aPrime = Vec3.subtract(position, axisStart); - var bPrime = Vec3.subtract(axisEnd, axisStart); - var bPrimeMagnitude = Vec3.length(bPrime); - var dotProduct = Vec3.dot(aPrime, bPrime); - var scalar = dotProduct / bPrimeMagnitude; - if (scalar < 0) { - scalar = 0; - } - if (scalar > 1) { - scalar = 1; - } - var projection = Vec3.sum(axisStart, Vec3.multiply(scalar, Vec3.normalize(bPrime))); - return projection; - }; - - 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.nearGrabbingEnter = function() { - this.grabPointSphereOff(); - this.laserPointerOff(); - - this.dropGestureReset(); - this.clearEquipHaptics(); - - this.shouldScale = false; - - Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - - var grabbedProperties; - if (this.grabbedIsOverlay) { - grabbedProperties = { - position: Overlays.getProperty(this.grabbedThingID, "position"), - rotation: Overlays.getProperty(this.grabbedThingID, "rotation"), - parentID: Overlays.getProperty(this.grabbedThingID, "parentID"), - parentJointIndex: Overlays.getProperty(this.grabbedThingID, "parentJointIndex"), - dynamic: false, - shapeType: "none" - }; - this.ignoreIK = true; - } else { - grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); - var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedThingID, DEFAULT_GRABBABLE_DATA); - if (FORCE_IGNORE_IK) { - this.ignoreIK = true; - } else { - this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true; - } - - this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC; - } - - var handRotation; - var handPosition; - if (this.ignoreIK) { - var controllerLocation = getControllerWorldLocation(this.handToController(), false); - handRotation = controllerLocation.orientation; - handPosition = controllerLocation.position; - } else { - handRotation = this.getHandRotation(); - handPosition = this.getHandPosition(); - } - - var hasPresetPosition = false; - if (this.state == STATE_HOLD && this.grabbedHotspot) { - // if an object is "equipped" and has a predefined offset, use it. - 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 { - var objectRotation = grabbedProperties.rotation; - this.offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); - - var currentObjectPosition = grabbedProperties.position; - var offset = Vec3.subtract(currentObjectPosition, handPosition); - this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); - } - - // This boolean is used to check if the object that is grabbed has just been cloned - // It is only set true, if the object that is grabbed creates a new clone. - var isClone = false; - var isPhysical = propsArePhysical(grabbedProperties) || - (!this.grabbedIsOverlay && entityHasActions(this.grabbedThingID)); - if (isPhysical && this.state == STATE_NEAR_GRABBING && grabbedProperties.parentID === NULL_UUID) { - // grab entity via action - if (!this.setupHoldAction()) { - return; - } - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'grab', - grabbedEntity: this.grabbedThingID, - joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" - })); - } else { - // grab entity via parenting - this.actionID = null; - var handJointIndex; - if (this.ignoreIK) { - handJointIndex = this.controllerJointIndex; - } else { - handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - } - - var reparentProps = { - parentID: AVATAR_SELF_ID, - parentJointIndex: handJointIndex, - velocity: {x: 0, y: 0, z: 0}, - angularVelocity: {x: 0, y: 0, z: 0} - }; - if (hasPresetPosition) { - reparentProps.localPosition = this.offsetPosition; - reparentProps.localRotation = this.offsetRotation; - } - - if (this.grabbedIsOverlay) { - Overlays.editOverlay(this.grabbedThingID, reparentProps); - } else { - if (grabbedProperties.userData.length > 0) { - try{ - var userData = JSON.parse(grabbedProperties.userData); - var grabInfo = userData.grabbableKey; - if (grabInfo && grabInfo.cloneable) { - var worldEntities = Entities.findEntities(MyAvatar.position, 50); - var count = 0; - worldEntities.forEach(function(item) { - var itemWE = Entities.getEntityProperties(item, ["name"]); - if (itemWE.name.indexOf('-clone-' + grabbedProperties.id) !== -1) { - count++; - } - }); - - var limit = grabInfo.cloneLimit ? grabInfo.cloneLimit : 0; - if (count >= limit && limit !== 0) { - return; - } - - var cloneableProps = Entities.getEntityProperties(grabbedProperties.id); - cloneableProps.name = cloneableProps.name + '-clone-' + grabbedProperties.id; - var lifetime = grabInfo.cloneLifetime ? grabInfo.cloneLifetime : 300; - var dynamic = grabInfo.cloneDynamic ? grabInfo.cloneDynamic : false; - var cUserData = Object.assign({}, userData); - var cProperties = Object.assign({}, cloneableProps); - isClone = true; - - delete cUserData.grabbableKey.cloneLifetime; - delete cUserData.grabbableKey.cloneable; - delete cUserData.grabbableKey.cloneDynamic; - delete cUserData.grabbableKey.cloneLimit; - delete cProperties.id; - - cProperties.dynamic = dynamic; - cProperties.locked = false; - cUserData.grabbableKey.triggerable = true; - cUserData.grabbableKey.grabbable = true; - cProperties.lifetime = lifetime; - cProperties.userData = JSON.stringify(cUserData); - var cloneID = Entities.addEntity(cProperties); - this.grabbedThingID = cloneID; - grabbedProperties = Entities.getEntityProperties(cloneID); - } - }catch(e) {} - } - Entities.editEntity(this.grabbedThingID, reparentProps); - } - - if (this.thisHandIsParent(grabbedProperties)) { - // this should never happen, but if it does, don't set previous parent to be this hand. - // this.previousParentID[this.grabbedThingID] = NULL; - // this.previousParentJointIndex[this.grabbedThingID] = -1; - } else { - this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID; - this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex; - } - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'equip', - grabbedEntity: this.grabbedThingID, - joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" - })); - } - - if (!this.grabbedIsOverlay) { - Entities.editEntity(this.grabbedThingID, { - velocity: { x: 0, y: 0, z: 0 }, - angularVelocity: { x: 0, y: 0, z: 0 }, - // dynamic: false - }); - } - - var _this = this; - /* - * Setting context for function that is either called via timer or directly, depending if - * if the object in question is a clone. If it is a clone, we need to make sure that the intial equipment event - * is called correctly, as these just freshly created entity may not have completely initialized. - */ - var grabEquipCheck = function () { - if (_this.state == STATE_NEAR_GRABBING) { - _this.callEntityMethodOnGrabbed("startNearGrab"); - } else { // this.state == STATE_HOLD - _this.callEntityMethodOnGrabbed("startEquip"); - } - - // don't block teleport raypick with equipped entity - Messages.sendMessage('Hifi-Teleport-Ignore-Add', _this.grabbedThingID); - - _this.currentHandControllerTipPosition = - (_this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; - _this.currentObjectTime = Date.now(); - - _this.currentObjectPosition = grabbedProperties.position; - _this.currentObjectRotation = grabbedProperties.rotation; - _this.currentVelocity = ZERO_VEC; - _this.currentAngularVelocity = ZERO_VEC; - - _this.prevDropDetected = false; - }; - - if (isClone) { - // 100 ms seems to be sufficient time to force the check even occur after the object has been initialized. - Script.setTimeout(grabEquipCheck, 100); - } else { - grabEquipCheck(); - } - }; - - this.nearGrabbing = function(deltaTime, timestamp) { - this.grabPointSphereOff(); - - var ttl = ACTION_TTL; - - if (this.farToNearGrab) { - if(!this.triggerClicked){ - this.farToNearGrab = false; - } - } - - if (this.state == STATE_NEAR_GRABBING && (!this.triggerClicked && this.secondaryReleased())) { - this.callEntityMethodOnGrabbed("releaseGrab"); - this.setState(STATE_OFF, "trigger released"); - return; - } - - if (this.state == STATE_HOLD) { - - if (this.secondarySqueezed()) { - // this.secondaryReleased() will always be true when not depressed - // so we cannot simply rely on that for release - ensure that the - // trigger was first "prepared" by being pushed in before the release - this.preparingHoldRelease = true; - } - - if (this.preparingHoldRelease && this.secondaryReleased()) { - // we have an equipped object and the secondary trigger was released - // short-circuit the other checks and release it - this.preparingHoldRelease = false; - this.callEntityMethodOnGrabbed("releaseEquip"); - this.setState(STATE_OFF, "equipping ended via secondary press"); - return; - } - - var dropDetected = this.dropGestureProcess(deltaTime); - - if (this.triggerSmoothedReleased()) { - this.waitForTriggerRelease = false; - } - - if (dropDetected && this.prevDropDetected != dropDetected) { - this.waitForTriggerRelease = true; - } - - // highlight the grabbed hotspot when the dropGesture is detected. - if (dropDetected) { - entityPropertiesCache.addEntity(this.grabbedHotspot.entityID); - equipHotspotBuddy.updateHotspot(this.grabbedHotspot, timestamp); - equipHotspotBuddy.highlightHotspot(this.grabbedHotspot); - } - - if (dropDetected && !this.waitForTriggerRelease && this.triggerSmoothedGrab()) { - // store the offset attach points into preferences. - if (USE_ATTACH_POINT_SETTINGS && this.grabbedHotspot && this.grabbedThingID) { - var prefprops = Entities.getEntityProperties(this.grabbedThingID, ["localPosition", "localRotation"]); - if (prefprops && prefprops.localPosition && prefprops.localRotation) { - storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, - prefprops.localPosition, prefprops.localRotation); - } - } - - var grabbedEntity = this.grabbedThingID; - this.release(); - this.grabbedThingID = grabbedEntity; - this.setState(STATE_NEAR_GRABBING, "drop gesture detected"); - return; - } - this.prevDropDetected = dropDetected; - } - - var props; - if (this.grabbedIsOverlay) { - props = { - localPosition: Overlays.getProperty(this.grabbedThingID, "localPosition"), - parentID: Overlays.getProperty(this.grabbedThingID, "parentID"), - parentJointIndex: Overlays.getProperty(this.grabbedThingID, "parentJointIndex"), - position: Overlays.getProperty(this.grabbedThingID, "position"), - rotation: Overlays.getProperty(this.grabbedThingID, "rotation"), - dimensions: Overlays.getProperty(this.grabbedThingID, "dimensions"), - registrationPoint: { x: 0.5, y: 0.5, z: 0.5 } - }; - } else { - props = Entities.getEntityProperties(this.grabbedThingID, ["localPosition", "parentID", "parentJointIndex", - "position", "rotation", "dimensions", - "registrationPoint"]); - } - if (!props.position) { - // server may have reset, taking our equipped entity with it. move back to "off" state - this.callEntityMethodOnGrabbed("releaseGrab"); - this.setState(STATE_OFF, "entity has no position property"); - return; - } - - if (this.state == STATE_NEAR_GRABBING && this.actionID === null && !this.thisHandIsParent(props)) { - // someone took it from us or otherwise edited the parentID. end the grab. We don't do this - // for equipped things so that they can be adjusted while equipped. - this.callEntityMethodOnGrabbed("releaseGrab"); - this.grabbedThingID = null; - this.setState(STATE_OFF, "someone took it"); - return; - } - - var now = Date.now(); - if (this.state == STATE_HOLD && now - this.lastUnequipCheckTime > MSECS_PER_SEC * CHECK_TOO_FAR_UNEQUIP_TIME) { - this.lastUnequipCheckTime = now; - - if (props.parentID == AVATAR_SELF_ID) { - var handPosition; - if (this.ignoreIK) { - handPosition = getControllerWorldLocation(this.handToController(), false).position; - } else { - handPosition = this.getHandPosition(); - } - - var TEAR_AWAY_DISTANCE = 0.1; - var dist = distanceBetweenPointAndEntityBoundingBox(handPosition, props); - if (dist > TEAR_AWAY_DISTANCE) { - this.autoUnequipCounter += deltaTime; - } else { - this.autoUnequipCounter = 0; - } - - if (this.autoUnequipCounter > 0.25) { - // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. - print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + - props.parentID + ", dist = " + dist); - - if (this.state == STATE_NEAR_GRABBING) { - this.callEntityMethodOnGrabbed("releaseGrab"); - } else { // this.state == STATE_HOLD - this.callEntityMethodOnGrabbed("releaseEquip"); - } - this.setState(STATE_OFF, "held object too far away"); - return; - } - } - } - - // Keep track of the fingertip velocity to impart when we release the object. - // Note that the idea of using a constant 'tip' velocity regardless of the - // object's actual held offset is an idea intended to make it easier to throw things: - // Because we might catch something or transfer it between hands without a good idea - // of it's actual offset, let's try imparting a velocity which is at a fixed radius - // from the palm. - - var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; - - var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds - - if (deltaObjectTime > 0.0) { - var worldDeltaPosition = Vec3.subtract(props.position, this.currentObjectPosition); - - var previousEulers = Quat.safeEulerAngles(this.currentObjectRotation); - var newEulers = Quat.safeEulerAngles(props.rotation); - var worldDeltaRotation = Vec3.subtract(newEulers, previousEulers); - - this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaObjectTime); - this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaObjectTime * 180.0)); - - this.currentObjectPosition = props.position; - this.currentObjectRotation = props.rotation; - } - - this.currentHandControllerTipPosition = handControllerPosition; - this.currentObjectTime = now; - - if (this.state === STATE_HOLD) { - this.callEntityMethodOnGrabbed("continueEquip"); - } - if (this.state == STATE_NEAR_GRABBING) { - this.callEntityMethodOnGrabbed("continueNearGrab"); - } - - if (this.state == STATE_NEAR_GRABBING) { - this.maybeScale(props); - } - - if (this.actionID && this.actionTimeout - now < ACTION_TTL_REFRESH * MSECS_PER_SEC) { - // if less than a 5 seconds left, refresh the actions ttl - var success = Entities.updateAction(this.grabbedThingID, this.actionID, { - hand: this.hand === RIGHT_HAND ? "right" : "left", - timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, - relativePosition: this.offsetPosition, - relativeRotation: this.offsetRotation, - ttl: ttl, - kinematic: this.kinematicGrab, - kinematicSetVelocity: true, - ignoreIK: this.ignoreIK - }); - if (success) { - this.actionTimeout = now + (ttl * MSECS_PER_SEC); - } else { - print("continueNearGrabbing -- updateAction failed"); - Entities.deleteAction(this.grabbedThingID, this.actionID); - this.setupHoldAction(); - } - } - }; - - this.maybeScale = function(props) { - if (!objectScalingEnabled || this.isTablet(this.grabbedThingID) || this.grabbedIsOverlay) { - return; - } - - if (!this.shouldScale) { - // If both secondary triggers squeezed, and the non-holding hand is empty, start scaling - if (this.secondarySqueezed() && - this.getOtherHandController().secondarySqueezed() && - this.grabbedThingID && this.getOtherHandController().grabbedThingID && - this.grabbedThingID == this.getOtherHandController().grabbedThingID) { - this.scalingStartDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), - this.getOtherHandController().getHandPosition())); - this.scalingStartDimensions = props.dimensions; - this.shouldScale = true; - } - } else if (!this.secondarySqueezed() || !this.getOtherHandController().secondarySqueezed()) { - this.shouldScale = false; - } - if (this.shouldScale) { - var scalingCurrentDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), - this.getOtherHandController().getHandPosition())); - var currentRescale = scalingCurrentDistance / this.scalingStartDistance; - var newDimensions = Vec3.multiply(currentRescale, this.scalingStartDimensions); - Entities.editEntity(this.grabbedThingID, { dimensions: newDimensions }); - } - }; - - this.maybeScaleMyAvatar = function() { - if (!myAvatarScalingEnabled || this.shouldScale || this.hand === LEFT_HAND) { - // If scaling disabled, or if we are currently scaling an entity, don't scale avatar - // and only rescale avatar for one hand (so we're not doing it twice) - return; - } - - // Only scale avatar if both triggers and grips are squeezed - var tryingToScale = this.secondarySqueezed() && this.getOtherHandController().secondarySqueezed() && - this.triggerSmoothedSqueezed() && this.getOtherHandController().triggerSmoothedSqueezed(); - - - if (!this.isScalingAvatar) { - // If both secondary triggers squeezed, start scaling - if (tryingToScale) { - this.scalingStartDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), - this.getOtherHandController().getHandPosition())); - this.scalingStartAvatarScale = MyAvatar.scale; - this.isScalingAvatar = true; - } - } else if (!tryingToScale) { - this.isScalingAvatar = false; - } - if (this.isScalingAvatar) { - var scalingCurrentDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), - this.getOtherHandController().getHandPosition())); - var newAvatarScale = (scalingCurrentDistance / this.scalingStartDistance) * this.scalingStartAvatarScale; - MyAvatar.scale = newAvatarScale; - } - }; - - this.nearTriggerEnter = function() { - this.clearEquipHaptics(); - this.grabPointSphereOff(); - Controller.triggerShortHapticPulse(1.0, this.hand); - this.callEntityMethodOnGrabbed("startNearTrigger"); - }; - - this.farTriggerEnter = function() { - this.clearEquipHaptics(); - this.grabPointSphereOff(); - this.callEntityMethodOnGrabbed("startFarTrigger"); - }; - - this.nearTrigger = function(deltaTime, timestamp) { - if (this.triggerSmoothedReleased()) { - this.callEntityMethodOnGrabbed("stopNearTrigger"); - this.grabbedThingID = null; - this.setState(STATE_OFF, "trigger released"); - return; - } - this.callEntityMethodOnGrabbed("continueNearTrigger"); - }; - - this.farTrigger = function(deltaTime, timestamp) { - if (this.triggerSmoothedReleased()) { - this.callEntityMethodOnGrabbed("stopFarTrigger"); - this.grabbedThingID = null; - this.setState(STATE_OFF, "trigger released"); - return; - } - - var laserPointerID = PICK_WITH_HAND_RAY ? this.laserPointer : this.headLaserPointer; - var intersection = LaserPointers.getPrevRayPickResult(laserPointerID); - if (intersection.type != RayPick.INTERSECTED_NONE) { - if (intersection.objectID != this.grabbedThingID) { - this.callEntityMethodOnGrabbed("stopFarTrigger"); - this.grabbedThingID = null; - this.setState(STATE_OFF, "laser moved off of entity"); - return; - } - this.intersectionDistance = intersection.distance; - if (farGrabEnabled) { - this.updateLaserPointer(); - } - } - - this.callEntityMethodOnGrabbed("continueFarTrigger"); - }; - - this.offEnter = function() { - var existingSearchDistance = this.searchSphereDistance; - this.release(); - - if (hoveredEntityID) { - Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); - hoveredEntityID = false; - } - if (entityWithContextOverlay) { - ContextOverlay.destroyContextOverlay(entityWithContextOverlay); - entityWithContextOverlay = false; - } - - if (isInEditMode()) { - this.searchSphereDistance = existingSearchDistance; - } - }; - - this.entityLaserTouchingEnter = function() { - // test for intersection between controller laser and web entity plane. - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation); - if (intersectInfo) { - var pointerEvent = { - type: "Press", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(this.grabbedThingID, intersectInfo.point), - pos3D: intersectInfo.point, - normal: intersectInfo.normal, - direction: intersectInfo.searchRay.direction, - button: "Primary", - isPrimaryHeld: true - }; - - Entities.sendMousePressOnEntity(this.grabbedThingID, pointerEvent); - Entities.sendClickDownOnEntity(this.grabbedThingID, pointerEvent); - - this.touchingEnterTimer = 0; - this.touchingEnterPointerEvent = pointerEvent; - this.touchingEnterPointerEvent.button = "None"; - this.deadspotExpired = false; - - var LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.026; // radians ~ 1.2 degrees - this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE) * intersectInfo.distance; // dead spot radius in meters - } - - Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); - }; - - this.entityLaserTouchingExit = function() { - // test for intersection between controller laser and web entity plane. - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation); - if (intersectInfo) { - var pointerEvent; - if (this.deadspotExpired) { - pointerEvent = { - type: "Release", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(this.grabbedThingID, intersectInfo.point), - pos3D: intersectInfo.point, - normal: intersectInfo.normal, - direction: intersectInfo.searchRay.direction, - button: "Primary" - }; - } else { - pointerEvent = this.touchingEnterPointerEvent; - pointerEvent.type = "Release"; - pointerEvent.button = "Primary"; - pointerEvent.isPrimaryHeld = false; - } - - Entities.sendMouseReleaseOnEntity(this.grabbedThingID, pointerEvent); - Entities.sendClickReleaseOnEntity(this.grabbedThingID, pointerEvent); - Entities.sendHoverLeaveEntity(this.grabbedThingID, pointerEvent); - } - this.grabbedThingID = null; - this.grabbedOverlay = null; - }; - - this.entityLaserTouching = function(dt) { - - this.touchingEnterTimer += dt; - - entityPropertiesCache.addEntity(this.grabbedThingID); - - if (this.state == STATE_ENTITY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { // AJT: - this.setState(STATE_OFF, "released trigger"); - return; - } - - // test for intersection between controller laser and web entity plane. - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation); - if (intersectInfo) { - - if (Entities.keyboardFocusEntity != this.grabbedThingID) { - Overlays.keyboardFocusOverlay = 0; - Entities.keyboardFocusEntity = this.grabbedThingID; - } - - var pointerEvent = { - type: "Move", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(this.grabbedThingID, intersectInfo.point), - pos3D: intersectInfo.point, - normal: intersectInfo.normal, - direction: intersectInfo.searchRay.direction, - button: "NoButtons", - isPrimaryHeld: true - }; - - var POINTER_PRESS_TO_MOVE_DELAY = 0.25; // seconds - if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || - Vec3.distance(intersectInfo.point, this.touchingEnterPointerEvent.pos3D) > this.deadspotRadius) { - Entities.sendMouseMoveOnEntity(this.grabbedThingID, pointerEvent); - Entities.sendHoldingClickOnEntity(this.grabbedThingID, pointerEvent); - this.deadspotExpired = true; - } - - this.intersectionDistance = intersectInfo.distance; - if (this.state == STATE_ENTITY_LASER_TOUCHING) { - this.updateLaserPointer(); - } - Reticle.setVisible(false); - } else { - this.grabbedThingID = null; - this.setState(STATE_OFF, "grabbed entity was destroyed"); - return; - } - }; - - this.overlayLaserTouchingEnter = function () { - // Test for intersection between controller laser and overlay plane. - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); - if (intersectInfo) { - var pointerEvent = { - type: "Press", - id: this.hand + 1, - pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point), - pos3D: intersectInfo.point, - normal: intersectInfo.normal, - direction: intersectInfo.searchRay.direction, - button: "Primary", - isPrimaryHeld: true - }; - - Overlays.sendMousePressOnOverlay(this.grabbedOverlay, pointerEvent); - - this.touchingEnterTimer = 0; - this.touchingEnterPointerEvent = pointerEvent; - this.touchingEnterPointerEvent.button = "None"; - this.deadspotExpired = false; - - var LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.026; // radians ~ 1.2 degrees - this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE) * intersectInfo.distance; // dead spot radius in meters - } - - Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); - }; - - this.overlayLaserTouchingExit = function () { - // Test for intersection between controller laser and Web overlay plane. - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); - if (intersectInfo) { - var pointerEvent; - - var pos2D; - var pos3D; - if (this.tabletStabbed) { - // Some people like to jam the stylus a long ways into the tablet when clicking on a button. - // They almost always move out of the deadzone when they do this. We detect if the stylus - // has gone far through the tablet and suppress any further faux mouse events until the - // stylus is withdrawn. Once it has withdrawn, we do a release click wherever the stylus was - // when it was pushed into the tablet. - this.tabletStabbed = false; - pos2D = this.tabletStabbedPos2D; - pos3D = this.tabletStabbedPos3D; - } else { - pos2D = projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point); - pos3D = intersectInfo.point; - } - - if (this.deadspotExpired) { - pointerEvent = { - type: "Release", - id: this.hand + 1, - pos2D: pos2D, - pos3D: pos3D, - normal: intersectInfo.normal, - direction: intersectInfo.searchRay.direction, - button: "Primary" - }; - } else { - pointerEvent = this.touchingEnterPointerEvent; - pointerEvent.type = "Release"; - pointerEvent.button = "Primary"; - pointerEvent.isPrimaryHeld = false; - } - - Overlays.sendMouseReleaseOnOverlay(this.grabbedOverlay, pointerEvent); - Overlays.sendHoverLeaveOverlay(this.grabbedOverlay, pointerEvent); - } - this.grabbedThingID = null; - this.grabbedOverlay = null; - }; - - this.overlayLaserTouching = function (dt) { - this.touchingEnterTimer += dt; - - if (this.state == STATE_OVERLAY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { - this.setState(STATE_OFF, "released trigger"); - return; - } - - // Test for intersection between controller laser and Web overlay plane. - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); - if (intersectInfo) { - - var pos2D = projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point); - var pos3D = intersectInfo.point; - - if (Overlays.keyboardFocusOverlay != this.grabbedOverlay) { - Entities.keyboardFocusEntity = null; - Overlays.keyboardFocusOverlay = this.grabbedOverlay; - } - - var pointerEvent = { - type: "Move", - id: this.hand + 1, - pos2D: pos2D, - pos3D: pos3D, - normal: intersectInfo.normal, - direction: intersectInfo.searchRay.direction, - button: "NoButtons", - isPrimaryHeld: true - }; - - var POINTER_PRESS_TO_MOVE_DELAY = 0.25; // seconds - if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || - Vec3.distance(intersectInfo.point, this.touchingEnterPointerEvent.pos3D) > this.deadspotRadius) { - Overlays.sendMouseMoveOnOverlay(this.grabbedOverlay, pointerEvent); - this.deadspotExpired = true; - } - - this.intersectionDistance = intersectInfo.distance; - if (this.state == STATE_OVERLAY_LASER_TOUCHING) { - this.updateLaserPointer(); - } - Reticle.setVisible(false); - } else { - this.grabbedThingID = null; - this.setState(STATE_OFF, "grabbed overlay was destroyed"); - return; - } - }; - - this.stylusTouchingEnter = function () { - this.stealTouchFocus(this.stylusTarget); - sendTouchStartEventToStylusTarget(this.hand, this.stylusTarget); - Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); - - this.touchingEnterTimer = 0; - this.touchingEnterStylusTarget = this.stylusTarget; - this.deadspotExpired = false; - - var TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0381; - this.deadspotRadius = TOUCH_PRESS_TO_MOVE_DEADSPOT; - }; - - this.stylusTouchingExit = function () { - - if (this.stylusTarget === undefined) { - return; - } - - // special case to handle home button. - if (this.stylusTarget.overlayID === HMD.homeButtonID) { - Messages.sendLocalMessage("home", this.stylusTarget.overlayID); - } - - // send press event - if (this.deadspotExpired) { - sendTouchEndEventToStylusTarget(this.hand, this.stylusTarget); - } else { - sendTouchEndEventToStylusTarget(this.hand, this.touchingEnterStylusTarget); - } - }; - - this.stylusTouching = function (dt) { - - this.touchingEnterTimer += dt; - - if (this.stylusTarget.entityID) { - entityPropertiesCache.addEntity(this.stylusTarget.entityID); - this.stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, this.stylusTarget.entityID); - } else if (this.stylusTarget.overlayID) { - this.stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, this.stylusTarget.overlayID); - } - - var TABLET_MIN_TOUCH_DISTANCE = -0.1; - var TABLET_MAX_TOUCH_DISTANCE = 0.01; - - if (this.stylusTarget) { - if (this.stylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && - this.stylusTarget.distance < TABLET_MAX_TOUCH_DISTANCE) { - var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds - if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || - distance2D(this.stylusTarget.position2D, - this.touchingEnterStylusTarget.position2D) > this.deadspotRadius) { - sendTouchMoveEventToStylusTarget(this.hand, this.stylusTarget); - this.deadspotExpired = true; - } - } else { - this.setState(STATE_OFF, "hand moved away from touch surface"); - } - } else { - this.setState(STATE_OFF, "touch surface was destroyed"); - } - }; - - this.release = function() { - this.turnOffVisualizations(); - - if (this.grabbedThingID !== null) { - - Messages.sendMessage('Hifi-Teleport-Ignore-Remove', this.grabbedThingID); - - if (this.state === STATE_HOLD) { - this.callEntityMethodOnGrabbed("releaseEquip"); - } - - // Make a small release haptic pulse if we really were holding something - Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - if (this.actionID !== null) { - Entities.deleteAction(this.grabbedThingID, this.actionID); - } else { - // no action, so it's a parenting grab - if (this.previousParentID[this.grabbedThingID] === NULL_UUID) { - if (this.grabbedIsOverlay) { - Overlays.editOverlay(this.grabbedThingID, { - parentID: NULL_UUID, - parentJointIndex: -1 - }); - } else { - Entities.editEntity(this.grabbedThingID, { - parentID: this.previousParentID[this.grabbedThingID], - parentJointIndex: this.previousParentJointIndex[this.grabbedThingID] - }); - this.ensureDynamic(); - } - } else { - if (this.grabbedIsOverlay) { - Overlays.editOverlay(this.grabbedThingID, { - parentID: this.previousParentID[this.grabbedThingID], - parentJointIndex: this.previousParentJointIndex[this.grabbedThingID], - }); - } else { - // we're putting this back as a child of some other parent, so zero its velocity - Entities.editEntity(this.grabbedThingID, { - parentID: this.previousParentID[this.grabbedThingID], - parentJointIndex: this.previousParentJointIndex[this.grabbedThingID], - velocity: {x: 0, y: 0, z: 0}, - angularVelocity: {x: 0, y: 0, z: 0} - }); - } - } - } - - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'release', - grabbedEntity: this.grabbedThingID, - joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" - })); - } - - this.actionID = null; - this.grabbedThingID = null; - this.grabbedOverlay = null; - this.grabbedHotspot = null; - - if (this.triggerSmoothedGrab() || this.secondarySqueezed()) { - this.waitForTriggerRelease = true; - } - }; - - this.cleanup = function() { - this.release(); - this.grabPointSphereOff(); - this.hideStylus(); - LaserPointers.removeLaserPointer(this.laserPointer); - LaserPointers.removeLaserPointer(this.headLaserPointer); - }; - - this.thisHandIsParent = function(props) { - if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== AVATAR_SELF_ID) { - return false; - } - - var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - if (props.parentJointIndex == handJointIndex) { - return true; - } - - var controllerJointIndex = this.controllerJointIndex; - if (props.parentJointIndex == controllerJointIndex) { - return true; - } - - var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : - "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); - if (props.parentJointIndex == controllerCRJointIndex) { - return true; - } - - return false; - }; - - this.checkForUnexpectedChildren = function() { - var _this = this; - // sometimes things can get parented to a hand and this script is unaware. Search for such entities and - // unhook them. - - // find children of avatar's hand joint - var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); - children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, handJointIndex)); - - // find children of faux controller joint - var controllerJointIndex = this.controllerJointIndex; - children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerJointIndex)); - children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerJointIndex)); - - // find children of faux camera-relative controller joint - var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : - "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); - children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerCRJointIndex)); - children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerCRJointIndex)); - - children.forEach(function(childID) { - if (childID !== _this.stylus) { - // we appear to be holding something and this script isn't in a state that would be holding something. - // unhook it. if we previously took note of this entity's parent, put it back where it was. This - // works around some problems that happen when more than one hand or avatar is passing something around. - var childType = Entities.getNestableType(childID); - if (_this.previousParentID[childID]) { - var previousParentID = _this.previousParentID[childID]; - var previousParentJointIndex = _this.previousParentJointIndex[childID]; - - // The main flaw with keeping track of previous parantage in individual scripts is: - // (1) A grabs something (2) B takes it from A (3) A takes it from B (4) A releases it - // now A and B will take turns passing it back to the other. Detect this and stop the loop here... - var UNHOOK_LOOP_DETECT_MS = 200; - var now = Date.now(); - if (_this.previouslyUnhooked[childID]) { - if (now - _this.previouslyUnhooked[childID] < UNHOOK_LOOP_DETECT_MS) { - previousParentID = NULL_UUID; - previousParentJointIndex = -1; - } - } - _this.previouslyUnhooked[childID] = now; - - if (childType == "overlay" && Overlays.getProperty(childID, "grabbable")) { - // only auto-unhook overlays that were flagged as grabbable. this avoids unhooking overlays - // used in tutorial. - Overlays.editOverlay(childID, { - parentID: previousParentID, - parentJointIndex: previousParentJointIndex - }); - } - if (childType == "entity") { - Entities.editEntity(childID, { - parentID: previousParentID, - parentJointIndex: previousParentJointIndex - }); - } - - } else { - if (childType == "entity") { - Entities.editEntity(childID, { parentID: NULL_UUID }); - } else if (childType == "overlay") { - if (Overlays.getProperty(childID, "grabbable")) { - Overlays.editOverlay(childID, { parentID: NULL_UUID }); - } - } - } - } - }); - }; - - this.getOtherHandController = function() { - return (this.hand === RIGHT_HAND) ? leftController : rightController; - }; -} - -var rightController = new MyController(RIGHT_HAND); -var leftController = new MyController(LEFT_HAND); - -var MAPPING_NAME = "com.highfidelity.handControllerGrab"; - -var mapping = Controller.newMapping(MAPPING_NAME); -mapping.from([Controller.Standard.RT]).peek().to(rightController.triggerPress); -mapping.from([Controller.Standard.RTClick]).peek().to(rightController.triggerClick); - -mapping.from([Controller.Standard.LT]).peek().to(leftController.triggerPress); -mapping.from([Controller.Standard.LTClick]).peek().to(leftController.triggerClick); - -mapping.from([Controller.Standard.RB]).peek().to(rightController.secondaryPress); -mapping.from([Controller.Standard.LB]).peek().to(leftController.secondaryPress); -mapping.from([Controller.Standard.LeftGrip]).peek().to(leftController.secondaryPress); -mapping.from([Controller.Standard.RightGrip]).peek().to(rightController.secondaryPress); - -mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); -mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); - -Controller.enableMapping(MAPPING_NAME); - -function handleMenuEvent(menuItem) { - if (menuItem === "Show Grab Sphere") { - SHOW_GRAB_POINT_SPHERE = Menu.isOptionChecked("Show Grab Sphere"); - } -} - -Menu.addMenuItem({ menuName: "Developer", menuItemName: "Show Grab Sphere", isCheckable: true, isChecked: false }); -Menu.menuItemEvent.connect(handleMenuEvent); - -// the section below allows the grab script to listen for messages -// that disable either one or both hands. useful for two handed items -var handToDisable = 'none'; - -function update(deltaTime) { - var timestamp = Date.now(); - - if (handToDisable !== LEFT_HAND && handToDisable !== 'both') { - leftController.update(deltaTime, timestamp); - } else { - leftController.release(); - } - if (handToDisable !== RIGHT_HAND && handToDisable !== 'both') { - rightController.update(deltaTime, timestamp); - } else { - rightController.release(); - } - equipHotspotBuddy.update(deltaTime, timestamp); - entityPropertiesCache.update(); -} - -Messages.subscribe('Hifi-Grab-Disable'); -Messages.subscribe('Hifi-Hand-Disabler'); -Messages.subscribe('Hifi-Hand-Grab'); -Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); -Messages.subscribe('Hifi-Object-Manipulation'); -Messages.subscribe('Hifi-Hand-Drop'); - -var setBlacklist = function() { - if (USE_BLACKLIST) { - LaserPointers.setIgnoreEntities(leftController.laserPointer, blacklist); - LaserPointers.setIgnoreEntities(leftController.headLaserPointer, blacklist); - LaserPointers.setIgnoreEntities(rightController.laserPointer, blacklist); - LaserPointers.setIgnoreEntities(rightController.headLaserPointer, blacklist); - } -} - -var handleHandMessages = function(channel, message, sender) { - var data; - if (sender === MyAvatar.sessionUUID) { - if (channel === 'Hifi-Hand-Disabler') { - if (message === 'left') { - handToDisable = LEFT_HAND; - leftController.turnOffVisualizations(); - } - if (message === 'right') { - handToDisable = RIGHT_HAND; - rightController.turnOffVisualizations(); - } - if (message === 'both' || message === 'none') { - if (message === 'both') { - rightController.turnOffVisualizations(); - leftController.turnOffVisualizations(); - - } - handToDisable = message; - } - } else if (channel === 'Hifi-Grab-Disable') { - data = JSON.parse(message); - if (data.holdEnabled !== undefined) { - print("holdEnabled: ", data.holdEnabled); - holdEnabled = data.holdEnabled; - } - if (data.nearGrabEnabled !== undefined) { - print("nearGrabEnabled: ", data.nearGrabEnabled); - nearGrabEnabled = data.nearGrabEnabled; - } - if (data.farGrabEnabled !== undefined) { - print("farGrabEnabled: ", data.farGrabEnabled); - farGrabEnabled = data.farGrabEnabled; - } - if (data.myAvatarScalingEnabled !== undefined) { - print("myAvatarScalingEnabled: ", data.myAvatarScalingEnabled); - myAvatarScalingEnabled = data.myAvatarScalingEnabled; - } - if (data.objectScalingEnabled !== undefined) { - print("objectScalingEnabled: ", data.objectScalingEnabled); - objectScalingEnabled = data.objectScalingEnabled; - } - } else if (channel === 'Hifi-Hand-Grab') { - try { - data = JSON.parse(message); - var selectedController = (data.hand === 'left') ? leftController : rightController; - var hotspotIndex = data.hotspotIndex !== undefined ? parseInt(data.hotspotIndex) : 0; - selectedController.release(); - var wearableEntity = data.entityID; - entityPropertiesCache.addEntity(wearableEntity); - selectedController.grabbedThingID = wearableEntity; - var hotspots = selectedController.collectEquipHotspots(selectedController.grabbedThingID); - if (hotspots.length > 0) { - if (hotspotIndex >= hotspots.length) { - hotspotIndex = 0; - } - selectedController.grabbedHotspot = hotspots[hotspotIndex]; - } - selectedController.setState(STATE_HOLD, "Hifi-Hand-Grab msg received"); - selectedController.nearGrabbingEnter(); - - } catch (e) { - print("WARNING: handControllerGrab.js -- error parsing Hifi-Hand-Grab message: " + message); - } - - } else if (channel === 'Hifi-Hand-RayPick-Blacklist') { - try { - data = JSON.parse(message); - var action = data.action; - var id = data.id; - var index = blacklist.indexOf(id); - - if (action === 'add' && index === -1) { - blacklist.push(id); - setBlacklist(); - } - if (action === 'remove') { - if (index > -1) { - blacklist.splice(index, 1); - setBlacklist(); - } - } - - } catch (e) { - print("WARNING: handControllerGrab.js -- error parsing Hifi-Hand-RayPick-Blacklist message: " + message); - } - } else if (channel === 'Hifi-Hand-Drop') { - if (message === 'left') { - leftController.release(); - } else if (message === 'right') { - rightController.release(); - } else if (message === 'both') { - leftController.release(); - rightController.release(); - } - } - } -}; - -Messages.messageReceived.connect(handleHandMessages); - -var TARGET_UPDATE_HZ = 60; // 50hz good enough, but we're using update -var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ; -var lastInterval = Date.now(); - -var intervalCount = 0; -var totalDelta = 0; -var totalVariance = 0; -var highVarianceCount = 0; -var veryhighVarianceCount = 0; -var updateTotalWork = 0; - -var UPDATE_PERFORMANCE_DEBUGGING = false; - -var updateWrapper = function () { - - intervalCount++; - var thisInterval = Date.now(); - var deltaTimeMsec = thisInterval - lastInterval; - var deltaTime = deltaTimeMsec / 1000; - lastInterval = thisInterval; - - totalDelta += deltaTimeMsec; - - var variance = Math.abs(deltaTimeMsec - BASIC_TIMER_INTERVAL_MS); - totalVariance += variance; - - if (variance > 1) { - highVarianceCount++; - } - - if (variance > 5) { - veryhighVarianceCount++; - } - - // will call update for both hands - var preWork = Date.now(); - update(deltaTime); - var postWork = Date.now(); - var workDelta = postWork - preWork; - updateTotalWork += workDelta; - - if (intervalCount == 100) { - - if (UPDATE_PERFORMANCE_DEBUGGING) { - print("handControllerGrab.js -- For " + intervalCount + " samples average= " + - totalDelta/intervalCount + " ms" + - " average variance:" + totalVariance/intervalCount + " ms" + - " high variance count:" + highVarianceCount + " [ " + (highVarianceCount/intervalCount) * 100 + "% ] " + - " VERY high variance count:" + veryhighVarianceCount + - " [ " + (veryhighVarianceCount/intervalCount) * 100 + "% ] " + - " average work:" + updateTotalWork/intervalCount + " ms"); - } - - intervalCount = 0; - totalDelta = 0; - totalVariance = 0; - highVarianceCount = 0; - veryhighVarianceCount = 0; - updateTotalWork = 0; - } - - Script.setTimeout(updateWrapper, UPDATE_SLEEP_MS); -}; - -Script.setTimeout(updateWrapper, UPDATE_SLEEP_MS); -function cleanup() { - Menu.removeMenuItem("Developer", "Show Grab Sphere"); - rightController.cleanup(); - leftController.cleanup(); - Controller.disableMapping(MAPPING_NAME); - Reticle.setVisible(true); -} - -Script.scriptEnding.connect(cleanup); - -}()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js deleted file mode 100644 index 17ca2f91c5..0000000000 --- a/scripts/system/controllers/teleport.js +++ /dev/null @@ -1,575 +0,0 @@ -"use strict"; - -// Created by james b. pollack @imgntn on 7/2/2016 -// Copyright 2016 High Fidelity, Inc. -// -// Creates a beam and target and then teleports you there. Release when its close to you to cancel. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -(function() { // BEGIN LOCAL_SCOPE - -var inTeleportMode = false; - -var SMOOTH_ARRIVAL_SPACING = 33; -var NUMBER_OF_STEPS = 6; - -var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport-destination.fbx"); -var TOO_CLOSE_MODEL_URL = Script.resolvePath("../assets/models/teleport-cancel.fbx"); -var SEAT_MODEL_URL = Script.resolvePath("../assets/models/teleport-seat.fbx"); - -var TARGET_MODEL_DIMENSIONS = { - x: 1.15, - y: 0.5, - z: 1.15 -}; - -var COLORS_TELEPORT_SEAT = { - red: 255, - green: 0, - blue: 170 -}; - -var COLORS_TELEPORT_CAN_TELEPORT = { - red: 97, - green: 247, - blue: 255 -}; - -var COLORS_TELEPORT_CANCEL = { - red: 255, - green: 184, - blue: 73 -}; - -var TELEPORT_CANCEL_RANGE = 1; -var COOL_IN_DURATION = 500; - -var handInfo = { - right: { - controllerInput: Controller.Standard.RightHand - }, - left: { - controllerInput: Controller.Standard.LeftHand - } -}; - -var cancelPath = { - type: "line3d", - color: COLORS_TELEPORT_CANCEL, - ignoreRayIntersection: true, - alpha: 1, - solid: true, - drawInFront: true, - glow: 1.0 -}; -var teleportPath = { - type: "line3d", - color: COLORS_TELEPORT_CAN_TELEPORT, - ignoreRayIntersection: true, - alpha: 1, - solid: true, - drawInFront: true, - glow: 1.0 -}; -var seatPath = { - type: "line3d", - color: COLORS_TELEPORT_SEAT, - ignoreRayIntersection: true, - alpha: 1, - solid: true, - drawInFront: true, - glow: 1.0 -}; -var cancelEnd = { - type: "model", - url: TOO_CLOSE_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - ignoreRayIntersection: true -}; -var teleportEnd = { - type: "model", - url: TARGET_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - ignoreRayIntersection: true -}; -var seatEnd = { - type: "model", - url: SEAT_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - ignoreRayIntersection: true -} - -var teleportRenderStates = [{name: "cancel", path: cancelPath, end: cancelEnd}, - {name: "teleport", path: teleportPath, end: teleportEnd}, - {name: "seat", path: seatPath, end: seatEnd}]; - -var DEFAULT_DISTANCE = 50; -var teleportDefaultRenderStates = [{name: "cancel", distance: DEFAULT_DISTANCE, path: cancelPath}]; - -function ThumbPad(hand) { - this.hand = hand; - var _thisPad = this; - - this.buttonPress = function(value) { - _thisPad.buttonValue = value; - }; -} - -function Trigger(hand) { - this.hand = hand; - var _this = this; - - this.buttonPress = function(value) { - _this.buttonValue = value; - }; - - this.down = function() { - var down = _this.buttonValue === 1 ? 1.0 : 0.0; - return down; - }; -} - -var coolInTimeout = null; -var ignoredEntities = []; - -var TELEPORTER_STATES = { - IDLE: 'idle', - COOL_IN: 'cool_in', - TARGETTING: 'targetting', - TARGETTING_INVALID: 'targetting_invalid', -}; - -var TARGET = { - NONE: 'none', // Not currently targetting anything - INVISIBLE: 'invisible', // The current target is an invvsible surface - INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.) - SURFACE: 'surface', // The current target is a valid surface - SEAT: 'seat', // The current target is a seat -}; - -function Teleporter() { - var _this = this; - this.active = false; - this.state = TELEPORTER_STATES.IDLE; - this.currentTarget = TARGET.INVALID; - - this.teleportRayLeftVisible = LaserPointers.createLaserPointer({ - joint: "LeftHand", - filter: RayPick.PICK_ENTITIES, - faceAvatar: true, - centerEndY: false, - renderStates: teleportRenderStates, - defaultRenderStates: teleportDefaultRenderStates - }); - this.teleportRayLeftInvisible = LaserPointers.createLaserPointer({ - joint: "LeftHand", - filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE, - faceAvatar: true, - centerEndY: false, - renderStates: teleportRenderStates - }); - this.teleportRayRightVisible = LaserPointers.createLaserPointer({ - joint: "RightHand", - filter: RayPick.PICK_ENTITIES, - faceAvatar: true, - centerEndY: false, - renderStates: teleportRenderStates, - defaultRenderStates: teleportDefaultRenderStates - }); - this.teleportRayRightInvisible = LaserPointers.createLaserPointer({ - joint: "RightHand", - filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE, - faceAvatar: true, - centerEndY: false, - renderStates: teleportRenderStates - }); - - this.teleportRayHeadVisible = LaserPointers.createLaserPointer({ - joint: "Avatar", - filter: RayPick.PICK_ENTITIES, - faceAvatar: true, - centerEndY: false, - renderStates: teleportRenderStates, - defaultRenderStates: teleportDefaultRenderStates - }); - this.teleportRayHeadInvisible = LaserPointers.createLaserPointer({ - joint: "Avatar", - filter: RayPick.PICK_ENTITIES | RayPick.PICK_INCLUDE_INVISIBLE, - faceAvatar: true, - centerEndY: false, - renderStates: teleportRenderStates - }); - - this.updateConnected = null; - this.activeHand = null; - - this.teleporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); - this.teleportMappingInternal = Controller.newMapping(this.teleporterMappingInternalName); - - this.enableMappings = function() { - Controller.enableMapping(this.teleporterMappingInternalName); - }; - - this.disableMappings = function() { - Controller.disableMapping(teleporter.teleporterMappingInternalName); - }; - - this.cleanup = function() { - this.disableMappings(); - - LaserPointers.removeLaserPointer(this.teleportRayLeftVisible); - LaserPointers.removeLaserPointer(this.teleportRayLeftInvisible); - LaserPointers.removeLaserPointer(this.teleportRayRightVisible); - LaserPointers.removeLaserPointer(this.teleportRayRightInvisible); - LaserPointers.removeLaserPointer(this.teleportRayHeadVisible); - LaserPointers.removeLaserPointer(this.teleportRayHeadInvisible); - - if (this.updateConnected === true) { - Script.update.disconnect(this, this.update); - } - }; - - this.enterTeleportMode = function(hand) { - if (inTeleportMode === true) { - return; - } - if (isDisabled === 'both' || isDisabled === hand) { - return; - } - - inTeleportMode = true; - - if (coolInTimeout !== null) { - Script.clearTimeout(coolInTimeout); - } - - this.state = TELEPORTER_STATES.COOL_IN; - coolInTimeout = Script.setTimeout(function() { - if (_this.state === TELEPORTER_STATES.COOL_IN) { - _this.state = TELEPORTER_STATES.TARGETTING; - } - }, COOL_IN_DURATION); - - this.activeHand = hand; - this.enableMappings(); - Script.update.connect(this, this.update); - this.updateConnected = true; - }; - - this.exitTeleportMode = function(value) { - if (this.updateConnected === true) { - Script.update.disconnect(this, this.update); - } - - this.disableMappings(); - LaserPointers.disableLaserPointer(this.teleportRayLeftVisible); - LaserPointers.disableLaserPointer(this.teleportRayLeftInvisible); - LaserPointers.disableLaserPointer(this.teleportRayRightVisible); - LaserPointers.disableLaserPointer(this.teleportRayRightInvisible); - LaserPointers.disableLaserPointer(this.teleportRayHeadVisible); - LaserPointers.disableLaserPointer(this.teleportRayHeadInvisible); - - this.updateConnected = null; - this.state = TELEPORTER_STATES.IDLE; - inTeleportMode = false; - }; - - this.update = function() { - if (_this.state === TELEPORTER_STATES.IDLE) { - return; - } - - // Get current hand pose information to see if the pose is valid - var pose = Controller.getPoseValue(handInfo[_this.activeHand].controllerInput); - var mode = pose.valid ? _this.activeHand : 'head'; - if (!pose.valid) { - if (mode === 'right') { - LaserPointers.disableLaserPointer(_this.teleportRayRightVisible); - LaserPointers.disableLaserPointer(_this.teleportRayRightInvisible); - } else { - LaserPointers.disableLaserPointer(_this.teleportRayLeftVisible); - LaserPointers.disableLaserPointer(_this.teleportRayLeftInvisible); - } - LaserPointers.enableLaserPointer(_this.teleportRayHeadVisible); - LaserPointers.enableLaserPointer(_this.teleportRayHeadInvisible); - } else { - if (mode === 'right') { - LaserPointers.enableLaserPointer(_this.teleportRayRightVisible); - LaserPointers.enableLaserPointer(_this.teleportRayRightInvisible); - } else { - LaserPointers.enableLaserPointer(_this.teleportRayLeftVisible); - LaserPointers.enableLaserPointer(_this.teleportRayLeftInvisible); - } - LaserPointers.disableLaserPointer(_this.teleportRayHeadVisible); - LaserPointers.disableLaserPointer(_this.teleportRayHeadInvisible); - } - - // We do up to 2 ray picks to find a teleport location. - // There are 2 types of teleport locations we are interested in: - // 1. A visible floor. This can be any entity surface that points within some degree of "up" - // 2. A seat. The seat can be visible or invisible. - // - // * In the first pass we pick against visible and invisible entities so that we can find invisible seats. - // We might hit an invisible entity that is not a seat, so we need to do a second pass. - // * In the second pass we pick against visible entities only. - // - var result; - if (mode === 'right') { - result = LaserPointers.getPrevRayPickResult(_this.teleportRayRightInvisible); - } else if (mode === 'left') { - result = LaserPointers.getPrevRayPickResult(_this.teleportRayLeftInvisible); - } else { - result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadInvisible); - } - - var teleportLocationType = getTeleportTargetType(result); - if (teleportLocationType === TARGET.INVISIBLE) { - if (mode === 'right') { - result = LaserPointers.getPrevRayPickResult(_this.teleportRayRightVisible); - } else if (mode === 'left') { - result = LaserPointers.getPrevRayPickResult(_this.teleportRayLeftVisible); - } else { - result = LaserPointers.getPrevRayPickResult(_this.teleportRayHeadVisible); - } - teleportLocationType = getTeleportTargetType(result); - } - - if (teleportLocationType === TARGET.NONE) { - // Use the cancel default state - this.setTeleportState(mode, "cancel", ""); - } else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) { - this.setTeleportState(mode, "", "cancel"); - } else if (teleportLocationType === TARGET.SURFACE) { - if (this.state === TELEPORTER_STATES.COOL_IN) { - this.setTeleportState(mode, "cancel", ""); - } else { - this.setTeleportState(mode, "teleport", ""); - } - } else if (teleportLocationType === TARGET.SEAT) { - this.setTeleportState(mode, "", "seat"); - } - - - if (((_this.activeHand === 'left' ? leftPad : rightPad).buttonValue === 0) && inTeleportMode === true) { - // remember the state before we exit teleport mode and set it back to IDLE - var previousState = this.state; - this.exitTeleportMode(); - - if (teleportLocationType === TARGET.NONE || teleportLocationType === TARGET.INVALID || previousState === TELEPORTER_STATES.COOL_IN) { - // Do nothing - } else if (teleportLocationType === TARGET.SEAT) { - Entities.callEntityMethod(result.objectID, 'sit'); - } else if (teleportLocationType === TARGET.SURFACE) { - var offset = getAvatarFootOffset(); - result.intersection.y += offset; - MyAvatar.goToLocation(result.intersection, false, {x: 0, y: 0, z: 0, w: 1}, false); - HMD.centerUI(); - MyAvatar.centerBody(); - } - } - }; - - this.setTeleportState = function(mode, visibleState, invisibleState) { - if (mode === 'right') { - LaserPointers.setRenderState(_this.teleportRayRightVisible, visibleState); - LaserPointers.setRenderState(_this.teleportRayRightInvisible, invisibleState); - } else if (mode === 'left') { - LaserPointers.setRenderState(_this.teleportRayLeftVisible, visibleState); - LaserPointers.setRenderState(_this.teleportRayLeftInvisible, invisibleState); - } else { - LaserPointers.setRenderState(_this.teleportRayHeadVisible, visibleState); - LaserPointers.setRenderState(_this.teleportRayHeadInvisible, invisibleState); - } - }; -} - -// related to repositioning the avatar after you teleport -var FOOT_JOINT_NAMES = ["RightToe_End", "RightToeBase", "RightFoot"]; -var DEFAULT_ROOT_TO_FOOT_OFFSET = 0.5; -function getAvatarFootOffset() { - - // find a valid foot jointIndex - var footJointIndex = -1; - var i, l = FOOT_JOINT_NAMES.length; - for (i = 0; i < l; i++) { - footJointIndex = MyAvatar.getJointIndex(FOOT_JOINT_NAMES[i]); - if (footJointIndex != -1) { - break; - } - } - if (footJointIndex != -1) { - // default vertical offset from foot to avatar root. - var footPos = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(footJointIndex); - if (footPos.x === 0 && footPos.y === 0 && footPos.z === 0.0) { - // if footPos is exactly zero, it's probably wrong because avatar is currently loading, fall back to default. - return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale; - } else { - return -footPos.y; - } - } else { - return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale; - } -} - -var leftPad = new ThumbPad('left'); -var rightPad = new ThumbPad('right'); -var leftTrigger = new Trigger('left'); -var rightTrigger = new Trigger('right'); - -var mappingName, teleportMapping; - -var TELEPORT_DELAY = 0; - -function isMoving() { - var LY = Controller.getValue(Controller.Standard.LY); - var LX = Controller.getValue(Controller.Standard.LX); - if (LY !== 0 || LX !== 0) { - return true; - } else { - return false; - } -} - -function parseJSON(json) { - try { - return JSON.parse(json); - } catch (e) { - return undefined; - } -} -// When determininig whether you can teleport to a location, the normal of the -// point that is being intersected with is looked at. If this normal is more -// than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from <0, 1, 0> (straight up), then -// you can't teleport there. -var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; -function getTeleportTargetType(result) { - if (result.type == RayPick.INTERSECTED_NONE) { - return TARGET.NONE; - } - - var props = Entities.getEntityProperties(result.objectID, ['userData', 'visible']); - var data = parseJSON(props.userData); - if (data !== undefined && data.seat !== undefined) { - var avatarUuid = Uuid.fromString(data.seat.user); - if (Uuid.isNull(avatarUuid) || !AvatarList.getAvatar(avatarUuid)) { - return TARGET.SEAT; - } else { - return TARGET.INVALID; - } - } - - if (!props.visible) { - return TARGET.INVISIBLE; - } - - var surfaceNormal = result.surfaceNormal; - var adj = Math.sqrt(surfaceNormal.x * surfaceNormal.x + surfaceNormal.z * surfaceNormal.z); - var angleUp = Math.atan2(surfaceNormal.y, adj) * (180 / Math.PI); - - if (angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) || - angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) || - Vec3.distance(MyAvatar.position, result.intersection) <= TELEPORT_CANCEL_RANGE) { - return TARGET.INVALID; - } else { - return TARGET.SURFACE; - } -} - -function registerMappings() { - mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); - teleportMapping = Controller.newMapping(mappingName); - teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); - teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); - - teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); - teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - - teleportMapping.from(Controller.Standard.LeftPrimaryThumb) - .to(function(value) { - if (isDisabled === 'left' || isDisabled === 'both') { - return; - } - if (leftTrigger.down()) { - return; - } - if (isMoving() === true) { - return; - } - teleporter.enterTeleportMode('left'); - return; - }); - teleportMapping.from(Controller.Standard.RightPrimaryThumb) - .to(function(value) { - if (isDisabled === 'right' || isDisabled === 'both') { - return; - } - if (rightTrigger.down()) { - return; - } - if (isMoving() === true) { - return; - } - - teleporter.enterTeleportMode('right'); - return; - }); -} - -registerMappings(); - -var teleporter = new Teleporter(); - -Controller.enableMapping(mappingName); - -function cleanup() { - teleportMapping.disable(); - teleporter.cleanup(); -} -Script.scriptEnding.connect(cleanup); - -var setIgnoreEntities = function() { - LaserPointers.setIgnoreEntities(teleporter.teleportRayRightVisible, ignoredEntities); - LaserPointers.setIgnoreEntities(teleporter.teleportRayRightInvisible, ignoredEntities); - LaserPointers.setIgnoreEntities(teleporter.teleportRayLeftVisible, ignoredEntities); - LaserPointers.setIgnoreEntities(teleporter.teleportRayLeftInvisible, ignoredEntities); - LaserPointers.setIgnoreEntities(teleporter.teleportRayHeadVisible, ignoredEntities); - LaserPointers.setIgnoreEntities(teleporter.teleportRayHeadInvisible, ignoredEntities); -} - -var isDisabled = false; -var handleTeleportMessages = function(channel, message, sender) { - if (sender === MyAvatar.sessionUUID) { - if (channel === 'Hifi-Teleport-Disabler') { - if (message === 'both') { - isDisabled = 'both'; - } - if (message === 'left') { - isDisabled = 'left'; - } - if (message === 'right') { - isDisabled = 'right'; - } - if (message === 'none') { - isDisabled = false; - } - } else if (channel === 'Hifi-Teleport-Ignore-Add' && !Uuid.isNull(message) && ignoredEntities.indexOf(message) === -1) { - ignoredEntities.push(message); - setIgnoreEntities(); - } else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) { - var removeIndex = ignoredEntities.indexOf(message); - if (removeIndex > -1) { - ignoredEntities.splice(removeIndex, 1); - setIgnoreEntities(); - } - } - } -}; - -Messages.subscribe('Hifi-Teleport-Disabler'); -Messages.subscribe('Hifi-Teleport-Ignore-Add'); -Messages.subscribe('Hifi-Teleport-Ignore-Remove'); -Messages.messageReceived.connect(handleTeleportMessages); - -}()); // END LOCAL_SCOPE diff --git a/scripts/system/libraries/cloneEntityUtils.js b/scripts/system/libraries/cloneEntityUtils.js new file mode 100644 index 0000000000..500c6a0696 --- /dev/null +++ b/scripts/system/libraries/cloneEntityUtils.js @@ -0,0 +1,95 @@ +"use strict"; + +// cloneEntity.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global entityIsCloneable:true, getGrabbableData:true, cloneEntity:true propsAreCloneDynamic:true */ + +Script.include("/~/system/controllers/controllerDispatcherUtils.js"); + + +// Object assign polyfill +if (typeof Object.assign !== 'function') { + Object.assign = function(target, varArgs) { + if (target === null) { + throw new TypeError('Cannot convert undefined or null to object'); + } + var to = Object(target); + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + if (nextSource !== null) { + for (var nextKey in nextSource) { + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} + +entityIsCloneable = function(props) { + if (props) { + var grabbableData = getGrabbableData(props); + return grabbableData.cloneable; + } + return false; +}; + +propsAreCloneDynamic = function(props) { + var cloneable = entityIsCloneable(props); + if (cloneable) { + var grabInfo = getGrabbableData(props); + if (grabInfo.cloneDynamic) { + return true; + } + } + return false; +}; + + +cloneEntity = function(props, worldEntityProps) { + // we need all the properties, for this + var cloneableProps = Entities.getEntityProperties(props.id); + + var count = 0; + worldEntityProps.forEach(function(itemWE) { + if (itemWE.name.indexOf('-clone-' + cloneableProps.id) !== -1) { + count++; + } + }); + + var grabInfo = getGrabbableData(cloneableProps); + var limit = grabInfo.cloneLimit ? grabInfo.cloneLimit : 0; + if (count >= limit && limit !== 0) { + return null; + } + + cloneableProps.name = cloneableProps.name + '-clone-' + cloneableProps.id; + var lifetime = grabInfo.cloneLifetime ? grabInfo.cloneLifetime : 300; + var dynamic = grabInfo.cloneDynamic ? grabInfo.cloneDynamic : false; + var triggerable = grabInfo.triggerable ? grabInfo.triggerable : false; + var cUserData = Object.assign({}, JSON.parse(cloneableProps.userData)); + var cProperties = Object.assign({}, cloneableProps); + + + delete cUserData.grabbableKey.cloneLifetime; + delete cUserData.grabbableKey.cloneable; + delete cUserData.grabbableKey.cloneDynamic; + delete cUserData.grabbableKey.cloneLimit; + delete cProperties.id; + + + cProperties.dynamic = dynamic; + cProperties.locked = false; + cUserData.grabbableKey.triggerable = triggerable; + cUserData.grabbableKey.grabbable = true; + cProperties.lifetime = lifetime; + cProperties.userData = JSON.stringify(cUserData); + + var cloneID = Entities.addEntity(cProperties); + return cloneID; +}; diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js new file mode 100644 index 0000000000..715d520501 --- /dev/null +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -0,0 +1,322 @@ +"use strict"; + +// controllerDispatcherUtils.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, + MSECS_PER_SEC:true , LEFT_HAND:true, RIGHT_HAND:true, NULL_UUID:true, AVATAR_SELF_ID:true, FORBIDDEN_GRAB_TYPES:true, + HAPTIC_PULSE_STRENGTH:true, HAPTIC_PULSE_DURATION:true, ZERO_VEC:true, ONE_VEC:true, + DEFAULT_REGISTRATION_POINT:true, INCHES_TO_METERS:true, + TRIGGER_OFF_VALUE:true, + TRIGGER_ON_VALUE:true, + PICK_MAX_DISTANCE:true, + DEFAULT_SEARCH_SPHERE_DISTANCE:true, + NEAR_GRAB_PICK_RADIUS:true, + COLORS_GRAB_SEARCHING_HALF_SQUEEZE:true, + COLORS_GRAB_SEARCHING_FULL_SQUEEZE:true, + COLORS_GRAB_DISTANCE_HOLD:true, + NEAR_GRAB_RADIUS:true, + DISPATCHER_PROPERTIES:true, + HAPTIC_PULSE_STRENGTH:true, + HAPTIC_PULSE_DURATION:true, + Entities, + makeDispatcherModuleParameters:true, + makeRunningValues:true, + enableDispatcherModule:true, + disableDispatcherModule:true, + getEnabledModuleByName:true, + getGrabbableData:true, + entityIsGrabbable:true, + entityIsDistanceGrabbable:true, + getControllerJointIndex:true, + propsArePhysical:true, + controllerDispatcherPluginsNeedSort:true, + projectOntoXYPlane:true, + projectOntoEntityXYPlane:true, + projectOntoOverlayXYPlane:true, + entityHasActions:true, + ensureDynamic:true, + findGroupParent:true +*/ + +MSECS_PER_SEC = 1000.0; +INCHES_TO_METERS = 1.0 / 39.3701; + +HAPTIC_PULSE_STRENGTH = 1.0; +HAPTIC_PULSE_DURATION = 13.0; + +ZERO_VEC = { x: 0, y: 0, z: 0 }; +ONE_VEC = { x: 1, y: 1, z: 1 }; + +LEFT_HAND = 0; +RIGHT_HAND = 1; + +NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; +AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; + +FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"]; + +HAPTIC_PULSE_STRENGTH = 1.0; +HAPTIC_PULSE_DURATION = 13.0; + +DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 }; + +TRIGGER_OFF_VALUE = 0.1; +TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab +BUMPER_ON_VALUE = 0.5; + +PICK_MAX_DISTANCE = 500; // max length of pick-ray +DEFAULT_SEARCH_SPHERE_DISTANCE = 1000; // how far from camera to search intersection? +NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. + +COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }; +COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }; +COLORS_GRAB_DISTANCE_HOLD = { red: 238, green: 75, blue: 214 }; + +NEAR_GRAB_RADIUS = 1.0; + +DISPATCHER_PROPERTIES = [ + "position", + "registrationPoint", + "rotation", + "gravity", + "collidesWith", + "dynamic", + "collisionless", + "locked", + "name", + "shapeType", + "parentID", + "parentJointIndex", + "density", + "dimensions", + "userData" +]; + +// priority -- a lower priority means the module will be asked sooner than one with a higher priority in a given update step +// activitySlots -- indicates which "slots" must not yet be in use for this module to start +// requiredDataForReady -- which "situation" parts this module looks at to decide if it will start +// sleepMSBetweenRuns -- how long to wait between calls to this module's "run" method +makeDispatcherModuleParameters = function (priority, activitySlots, requiredDataForReady, sleepMSBetweenRuns) { + return { + priority: priority, + activitySlots: activitySlots, + requiredDataForReady: requiredDataForReady, + sleepMSBetweenRuns: sleepMSBetweenRuns + }; +}; + +makeRunningValues = function (active, targets, requiredDataForRun) { + return { + active: active, + targets: targets, + requiredDataForRun: requiredDataForRun + }; +}; + +enableDispatcherModule = function (moduleName, module, priority) { + if (!controllerDispatcherPlugins) { + controllerDispatcherPlugins = {}; + } + controllerDispatcherPlugins[moduleName] = module; + controllerDispatcherPluginsNeedSort = true; +}; + +disableDispatcherModule = function (moduleName) { + delete controllerDispatcherPlugins[moduleName]; + controllerDispatcherPluginsNeedSort = true; +}; + +getEnabledModuleByName = function (moduleName) { + if (controllerDispatcherPlugins.hasOwnProperty(moduleName)) { + return controllerDispatcherPlugins[moduleName]; + } + return null; +}; + +getGrabbableData = function (props) { + // look in userData for a "grabbable" key, return the value or some defaults + var grabbableData = {}; + var userDataParsed = null; + try { + if (!props.userDataParsed) { + props.userDataParsed = JSON.parse(props.userData); + } + userDataParsed = props.userDataParsed; + } catch (err) { + userDataParsed = {}; + } + if (userDataParsed.grabbableKey) { + grabbableData = userDataParsed.grabbableKey; + } + if (!grabbableData.hasOwnProperty("grabbable")) { + grabbableData.grabbable = true; + } + if (!grabbableData.hasOwnProperty("ignoreIK")) { + grabbableData.ignoreIK = true; + } + if (!grabbableData.hasOwnProperty("kinematic")) { + grabbableData.kinematic = true; + } + if (!grabbableData.hasOwnProperty("wantsTrigger")) { + grabbableData.wantsTrigger = false; + } + if (!grabbableData.hasOwnProperty("triggerable")) { + grabbableData.triggerable = false; + } + + return grabbableData; +}; + +entityIsGrabbable = function (props) { + var grabbable = getGrabbableData(props).grabbable; + if (!grabbable || + props.locked || + FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0) { + return false; + } + return true; +}; + +entityIsDistanceGrabbable = function(props) { + if (!entityIsGrabbable(props)) { + return false; + } + + // we can't distance-grab non-physical + var isPhysical = propsArePhysical(props); + if (!isPhysical) { + return false; + } + + // XXX + // var distance = Vec3.distance(props.position, handPosition); + // this.otherGrabbingUUID = entityIsGrabbedByOther(entityID); + // if (this.otherGrabbingUUID !== null) { + // // don't distance grab something that is already grabbed. + // if (debug) { + // print("distance grab is skipping '" + props.name + "': already grabbed by another."); + // } + // return false; + // } + + return true; +}; + + +getControllerJointIndex = function (hand) { + if (HMD.isHandControllerAvailable()) { + var controllerJointIndex = -1; + if (Camera.mode === "first person") { + controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? + "_CONTROLLER_RIGHTHAND" : + "_CONTROLLER_LEFTHAND"); + } else if (Camera.mode === "third person") { + controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + } + + return controllerJointIndex; + } + + return MyAvatar.getJointIndex("Head"); +}; + +propsArePhysical = function (props) { + if (!props.dynamic) { + return false; + } + var isPhysical = (props.shapeType && props.shapeType !== 'none'); + return isPhysical; +}; + +projectOntoXYPlane = function (worldPos, position, rotation, dimensions, registrationPoint) { + var invRot = Quat.inverse(rotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, position)); + var invDimensions = { + x: 1 / dimensions.x, + y: 1 / dimensions.y, + z: 1 / dimensions.z + }; + + var normalizedPos = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), registrationPoint); + return { + x: normalizedPos.x * dimensions.x, + y: (1 - normalizedPos.y) * dimensions.y // flip y-axis + }; +}; + +projectOntoEntityXYPlane = function (entityID, worldPos, props) { + return projectOntoXYPlane(worldPos, props.position, props.rotation, props.dimensions, props.registrationPoint); +}; + +projectOntoOverlayXYPlane = function projectOntoOverlayXYPlane(overlayID, worldPos) { + var position = Overlays.getProperty(overlayID, "position"); + var rotation = Overlays.getProperty(overlayID, "rotation"); + var dimensions; + + var dpi = Overlays.getProperty(overlayID, "dpi"); + if (dpi) { + // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale. + var resolution = Overlays.getProperty(overlayID, "resolution"); + resolution.z = 1; // Circumvent divide-by-zero. + var scale = Overlays.getProperty(overlayID, "dimensions"); + scale.z = 0.01; // overlay dimensions are 2D, not 3D. + dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); + } else { + dimensions = Overlays.getProperty(overlayID, "dimensions"); + if (dimensions.z) { + dimensions.z = 0.01; // overlay dimensions are 2D, not 3D. + } + } + + return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT); +}; + +entityHasActions = function (entityID) { + return Entities.getActionIDs(entityID).length > 0; +}; + +ensureDynamic = function (entityID) { + // if we distance hold something and keep it very still before releasing it, it ends up + // non-dynamic in bullet. If it's too still, give it a little bounce so it will fall. + var props = Entities.getEntityProperties(entityID, ["velocity", "dynamic", "parentID"]); + if (props.dynamic && props.parentID === NULL_UUID) { + var velocity = props.velocity; + if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + velocity = { x: 0.0, y: 0.2, z: 0.0 }; + Entities.editEntity(entityID, { velocity: velocity }); + } + } +}; + +findGroupParent = function (controllerData, targetProps) { + while (targetProps.parentID && targetProps.parentID !== NULL_UUID) { + // XXX use controllerData.nearbyEntityPropertiesByID ? + var parentProps = Entities.getEntityProperties(targetProps.parentID, DISPATCHER_PROPERTIES); + if (!parentProps) { + break; + } + parentProps.id = targetProps.parentID; + targetProps = parentProps; + controllerData.nearbyEntityPropertiesByID[targetProps.id] = targetProps; + } + + return targetProps; +}; + +if (typeof module !== 'undefined') { + module.exports = { + makeDispatcherModuleParameters: makeDispatcherModuleParameters, + enableDispatcherModule: enableDispatcherModule, + disableDispatcherModule: disableDispatcherModule, + makeRunningValues: makeRunningValues, + LEFT_HAND: LEFT_HAND, + RIGHT_HAND: RIGHT_HAND, + BUMPER_ON_VALUE: BUMPER_ON_VALUE + }; +}