diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7adde0e406..f3c41565f8 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -738,6 +738,7 @@ const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 100.0f; const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; +const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false; const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; const QString DEFAULT_CURSOR_NAME = "DEFAULT"; @@ -757,6 +758,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), + _preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER), _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), @@ -2580,6 +2582,10 @@ void Application::setHmdTabletBecomesToolbarSetting(bool value) { updateSystemTabletMode(); } +void Application::setPreferStylusOverLaser(bool value) { + _preferStylusOverLaserSetting.set(value); +} + void Application::setPreferAvatarFingerOverStylus(bool value) { _preferAvatarFingerOverStylusSetting.set(value); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 5267d50cf6..479ce919a3 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -207,7 +207,11 @@ public: void setDesktopTabletBecomesToolbarSetting(bool value); bool getHmdTabletBecomesToolbarSetting() { return _hmdTabletBecomesToolbarSetting.get(); } void setHmdTabletBecomesToolbarSetting(bool value); - bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); } + bool getPreferStylusOverLaser() { return _preferStylusOverLaserSetting.get(); } + void setPreferStylusOverLaser(bool value); + // FIXME: Remove setting completely or make available through JavaScript API? + //bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); } + bool getPreferAvatarFingerOverStylus() { return false; } void setPreferAvatarFingerOverStylus(bool value); float getSettingConstrainToolbarPosition() { return _constrainToolbarPosition.get(); } @@ -551,6 +555,7 @@ private: Setting::Handle<float> _desktopTabletScale; Setting::Handle<bool> _desktopTabletBecomesToolbarSetting; Setting::Handle<bool> _hmdTabletBecomesToolbarSetting; + Setting::Handle<bool> _preferStylusOverLaserSetting; Setting::Handle<bool> _preferAvatarFingerOverStylusSetting; Setting::Handle<bool> _constrainToolbarPosition; Setting::Handle<QString> _preferredCursor; diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp index 35f3a44227..a4fe516590 100644 --- a/interface/src/raypick/LaserPointer.cpp +++ b/interface/src/raypick/LaserPointer.cpp @@ -218,14 +218,40 @@ Pointer::PickedObject LaserPointer::getHoveredObject(const PickResultPointer& pi return PickedObject(rayPickResult->objectID, rayPickResult->type); } -Pointer::Buttons LaserPointer::getPressedButtons() { +Pointer::Buttons LaserPointer::getPressedButtons(const PickResultPointer& pickResult) { std::unordered_set<std::string> toReturn; - for (const PointerTrigger& trigger : _triggers) { - // TODO: right now, LaserPointers don't support axes, only on/off buttons - if (trigger.getEndpoint()->peek() >= 1.0f) { - toReturn.insert(trigger.getButton()); + auto rayPickResult = std::static_pointer_cast<const RayPickResult>(pickResult); + + if (rayPickResult) { + for (const PointerTrigger& trigger : _triggers) { + std::string button = trigger.getButton(); + TriggerState& state = _states[button]; + // TODO: right now, LaserPointers don't support axes, only on/off buttons + if (trigger.getEndpoint()->peek() >= 1.0f) { + toReturn.insert(button); + + if (_previousButtons.find(button) == _previousButtons.end()) { + // start triggering for buttons that were just pressed + state.triggeredObject = PickedObject(rayPickResult->objectID, rayPickResult->type); + state.intersection = rayPickResult->intersection; + state.triggerPos2D = findPos2D(state.triggeredObject, rayPickResult->intersection); + state.triggerStartTime = usecTimestampNow(); + state.surfaceNormal = rayPickResult->surfaceNormal; + state.deadspotExpired = false; + state.wasTriggering = true; + state.triggering = true; + _latestState = state; + } + } else { + // stop triggering for buttons that aren't pressed + state.wasTriggering = state.triggering; + state.triggering = false; + _latestState = state; + } } + _previousButtons = toReturn; } + return toReturn; } @@ -303,7 +329,7 @@ RenderState LaserPointer::buildRenderState(const QVariantMap& propMap) { return RenderState(startID, pathID, endID); } -PointerEvent LaserPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, bool hover) const { +PointerEvent LaserPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button, bool hover) { QUuid pickedID; glm::vec3 intersection, surfaceNormal, direction, origin; auto rayPickResult = std::static_pointer_cast<RayPickResult>(pickResult); @@ -316,20 +342,48 @@ PointerEvent LaserPointer::buildPointerEvent(const PickedObject& target, const P pickedID = rayPickResult->objectID; } - glm::vec2 pos2D; if (pickedID != target.objectID) { - if (target.type == ENTITY) { - intersection = RayPick::intersectRayWithEntityXYPlane(target.objectID, origin, direction); - } else if (target.type == OVERLAY) { - intersection = RayPick::intersectRayWithOverlayXYPlane(target.objectID, origin, direction); - } + intersection = findIntersection(target, origin, direction); } - if (target.type == ENTITY) { - pos2D = RayPick::projectOntoEntityXYPlane(target.objectID, intersection); - } else if (target.type == OVERLAY) { - pos2D = RayPick::projectOntoOverlayXYPlane(target.objectID, intersection); - } else if (target.type == HUD) { - pos2D = DependencyManager::get<PickManager>()->calculatePos2DFromHUD(intersection); + glm::vec2 pos2D = findPos2D(target, intersection); + + // If we just started triggering and we haven't moved too much, don't update intersection and pos2D + TriggerState& state = hover ? _latestState : _states[button]; + float sensorToWorldScale = DependencyManager::get<AvatarManager>()->getMyAvatar()->getSensorToWorldScale(); + float deadspotSquared = TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED * sensorToWorldScale * sensorToWorldScale; + bool withinDeadspot = usecTimestampNow() - state.triggerStartTime < POINTER_MOVE_DELAY && glm::distance2(pos2D, state.triggerPos2D) < deadspotSquared; + if ((state.triggering || state.wasTriggering) && !state.deadspotExpired && withinDeadspot) { + pos2D = state.triggerPos2D; + intersection = state.intersection; + surfaceNormal = state.surfaceNormal; } + if (!withinDeadspot) { + state.deadspotExpired = true; + } + return PointerEvent(pos2D, intersection, surfaceNormal, direction); +} + +glm::vec3 LaserPointer::findIntersection(const PickedObject& pickedObject, const glm::vec3& origin, const glm::vec3& direction) { + switch (pickedObject.type) { + case ENTITY: + return RayPick::intersectRayWithEntityXYPlane(pickedObject.objectID, origin, direction); + case OVERLAY: + return RayPick::intersectRayWithOverlayXYPlane(pickedObject.objectID, origin, direction); + default: + return glm::vec3(NAN); + } +} + +glm::vec2 LaserPointer::findPos2D(const PickedObject& pickedObject, const glm::vec3& origin) { + switch (pickedObject.type) { + case ENTITY: + return RayPick::projectOntoEntityXYPlane(pickedObject.objectID, origin); + case OVERLAY: + return RayPick::projectOntoOverlayXYPlane(pickedObject.objectID, origin); + case HUD: + return DependencyManager::get<PickManager>()->calculatePos2DFromHUD(origin); + default: + return glm::vec2(NAN); + } } \ No newline at end of file diff --git a/interface/src/raypick/LaserPointer.h b/interface/src/raypick/LaserPointer.h index 5cc83749bd..efc5c02729 100644 --- a/interface/src/raypick/LaserPointer.h +++ b/interface/src/raypick/LaserPointer.h @@ -80,10 +80,10 @@ public: static RenderState buildRenderState(const QVariantMap& propMap); protected: - PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, bool hover = true) const override; + PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button = "", bool hover = true) override; PickedObject getHoveredObject(const PickResultPointer& pickResult) override; - Pointer::Buttons getPressedButtons() override; + Pointer::Buttons getPressedButtons(const PickResultPointer& pickResult) override; bool shouldHover(const PickResultPointer& pickResult) override { return _currentRenderState != ""; } bool shouldTrigger(const PickResultPointer& pickResult) override { return _currentRenderState != ""; } @@ -105,6 +105,23 @@ private: void updateRenderState(const RenderState& renderState, const IntersectionType type, float distance, const QUuid& objectID, const PickRay& pickRay, bool defaultState); void disableRenderState(const RenderState& renderState); + struct TriggerState { + PickedObject triggeredObject; + glm::vec3 intersection { NAN }; + glm::vec3 surfaceNormal { NAN }; + glm::vec2 triggerPos2D { NAN }; + quint64 triggerStartTime { 0 }; + bool deadspotExpired { true }; + bool triggering { false }; + bool wasTriggering { false }; + }; + + Pointer::Buttons _previousButtons; + std::unordered_map<std::string, TriggerState> _states; + TriggerState _latestState; + static glm::vec3 findIntersection(const PickedObject& pickedObject, const glm::vec3& origin, const glm::vec3& direction); + static glm::vec2 findPos2D(const PickedObject& pickedObject, const glm::vec3& origin); + }; #endif // hifi_LaserPointer_h diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp index 501e8d1e42..4a39d5df59 100644 --- a/interface/src/raypick/RayPick.cpp +++ b/interface/src/raypick/RayPick.cpp @@ -92,4 +92,4 @@ glm::vec2 RayPick::projectOntoOverlayXYPlane(const QUuid& overlayID, const glm:: glm::vec2 RayPick::projectOntoEntityXYPlane(const QUuid& entityID, const glm::vec3& worldPos, bool unNormalized) { auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID); return projectOntoXYPlane(worldPos, props.getPosition(), props.getRotation(), props.getDimensions(), props.getRegistrationPoint(), unNormalized); -} +} \ No newline at end of file diff --git a/interface/src/raypick/StylusPointer.cpp b/interface/src/raypick/StylusPointer.cpp index 5afbc2058a..fad6fe47ae 100644 --- a/interface/src/raypick/StylusPointer.cpp +++ b/interface/src/raypick/StylusPointer.cpp @@ -28,10 +28,6 @@ static const float TABLET_MAX_TOUCH_DISTANCE = 0.005f; static const float HOVER_HYSTERESIS = 0.01f; static const float TOUCH_HYSTERESIS = 0.001f; -static const float STYLUS_MOVE_DELAY = 0.33f * USECS_PER_SECOND; -static const float TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0481f; -static const float TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED = TOUCH_PRESS_TO_MOVE_DEADSPOT * TOUCH_PRESS_TO_MOVE_DEADSPOT; - StylusPointer::StylusPointer(const QVariant& props, const OverlayID& stylusOverlay, bool hover, bool enabled) : Pointer(DependencyManager::get<PickScriptingInterface>()->createStylusPick(props), enabled, hover), _stylusOverlay(stylusOverlay) @@ -112,37 +108,37 @@ bool StylusPointer::shouldHover(const PickResultPointer& pickResult) { bool StylusPointer::shouldTrigger(const PickResultPointer& pickResult) { auto stylusPickResult = std::static_pointer_cast<const StylusPickResult>(pickResult); + bool wasTriggering = false; if (_renderState == EVENTS_ON && stylusPickResult) { auto sensorScaleFactor = DependencyManager::get<AvatarManager>()->getMyAvatar()->getSensorToWorldScale(); float distance = stylusPickResult->distance; // If we're triggering on an object, recalculate the distance instead of using the pickResult glm::vec3 origin = vec3FromVariant(stylusPickResult->pickVariant["position"]); - glm::vec3 direction = -_state.surfaceNormal; - if (!_state.triggeredObject.objectID.isNull() && stylusPickResult->objectID != _state.triggeredObject.objectID) { + glm::vec3 direction = _state.triggering ? -_state.surfaceNormal : -stylusPickResult->surfaceNormal; + if ((_state.triggering || _state.wasTriggering) && stylusPickResult->objectID != _state.triggeredObject.objectID) { distance = glm::dot(findIntersection(_state.triggeredObject, origin, direction) - origin, direction); } float hysteresis = _state.triggering ? TOUCH_HYSTERESIS * sensorScaleFactor : 0.0f; if (isWithinBounds(distance, TABLET_MIN_TOUCH_DISTANCE * sensorScaleFactor, - TABLET_MAX_TOUCH_DISTANCE * sensorScaleFactor, hysteresis)) { - if (_state.triggeredObject.objectID.isNull()) { + TABLET_MAX_TOUCH_DISTANCE * sensorScaleFactor, hysteresis)) { + _state.wasTriggering = _state.triggering; + if (!_state.triggering) { _state.triggeredObject = PickedObject(stylusPickResult->objectID, stylusPickResult->type); - _state.intersection = findIntersection(_state.triggeredObject, origin, direction); + _state.intersection = stylusPickResult->intersection; _state.triggerPos2D = findPos2D(_state.triggeredObject, origin); _state.triggerStartTime = usecTimestampNow(); _state.surfaceNormal = stylusPickResult->surfaceNormal; + _state.deadspotExpired = false; _state.triggering = true; } return true; } + wasTriggering = _state.triggering; } - _state.triggeredObject = PickedObject(); - _state.intersection = glm::vec3(NAN); - _state.triggerPos2D = glm::vec2(NAN); - _state.triggerStartTime = 0; - _state.surfaceNormal = glm::vec3(NAN); + _state.wasTriggering = wasTriggering; _state.triggering = false; return false; } @@ -155,13 +151,13 @@ Pointer::PickedObject StylusPointer::getHoveredObject(const PickResultPointer& p return PickedObject(stylusPickResult->objectID, stylusPickResult->type); } -Pointer::Buttons StylusPointer::getPressedButtons() { +Pointer::Buttons StylusPointer::getPressedButtons(const PickResultPointer& pickResult) { // TODO: custom buttons for styluses Pointer::Buttons toReturn({ "Primary", "Focus" }); return toReturn; } -PointerEvent StylusPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, bool hover) const { +PointerEvent StylusPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button, bool hover) { QUuid pickedID; glm::vec2 pos2D; glm::vec3 intersection, surfaceNormal, direction, origin; @@ -177,18 +173,22 @@ PointerEvent StylusPointer::buildPointerEvent(const PickedObject& target, const } // If we just started triggering and we haven't moved too much, don't update intersection and pos2D - if (!_state.triggeredObject.objectID.isNull() && usecTimestampNow() - _state.triggerStartTime < STYLUS_MOVE_DELAY && - glm::distance2(pos2D, _state.triggerPos2D) < TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED) { + float sensorToWorldScale = DependencyManager::get<AvatarManager>()->getMyAvatar()->getSensorToWorldScale(); + float deadspotSquared = TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED * sensorToWorldScale * sensorToWorldScale; + bool withinDeadspot = usecTimestampNow() - _state.triggerStartTime < POINTER_MOVE_DELAY && glm::distance2(pos2D, _state.triggerPos2D) < deadspotSquared; + if ((_state.triggering || _state.wasTriggering) && !_state.deadspotExpired && withinDeadspot) { pos2D = _state.triggerPos2D; intersection = _state.intersection; } else if (pickedID != target.objectID) { intersection = findIntersection(target, origin, direction); } + if (!withinDeadspot) { + _state.deadspotExpired = true; + } return PointerEvent(pos2D, intersection, surfaceNormal, direction); } - bool StylusPointer::isWithinBounds(float distance, float min, float max, float hysteresis) { return (distance == glm::clamp(distance, min - hysteresis, max + hysteresis)); } diff --git a/interface/src/raypick/StylusPointer.h b/interface/src/raypick/StylusPointer.h index 9c69915108..950b03b7c9 100644 --- a/interface/src/raypick/StylusPointer.h +++ b/interface/src/raypick/StylusPointer.h @@ -37,11 +37,11 @@ public: protected: PickedObject getHoveredObject(const PickResultPointer& pickResult) override; - Buttons getPressedButtons() override; + Buttons getPressedButtons(const PickResultPointer& pickResult) override; bool shouldHover(const PickResultPointer& pickResult) override; bool shouldTrigger(const PickResultPointer& pickResult) override; - PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, bool hover = true) const override; + PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button = "", bool hover = true) override; private: void show(const StylusTip& tip); @@ -53,7 +53,9 @@ private: glm::vec2 triggerPos2D { NAN }; glm::vec3 surfaceNormal { NAN }; quint64 triggerStartTime { 0 }; + bool deadspotExpired { true }; bool triggering { false }; + bool wasTriggering { false }; bool hovering { false }; }; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 023d3e6e17..1a09af07ab 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -90,11 +90,19 @@ void setupPreferences() { preference->setMax(500); preferences->addPreference(preference); } + { + auto getter = []()->bool { return qApp->getPreferStylusOverLaser(); }; + auto setter = [](bool value) { qApp->setPreferStylusOverLaser(value); }; + preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Stylus Over Laser", getter, setter)); + } + // FIXME: Remove setting completely or make available through JavaScript API? + /* { auto getter = []()->bool { return qApp->getPreferAvatarFingerOverStylus(); }; auto setter = [](bool value) { qApp->setPreferAvatarFingerOverStylus(value); }; preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Avatar Finger Over Stylus", getter, setter)); } + */ { static const QString RETICLE_ICON_NAME = { Cursor::Manager::getIconName(Cursor::Icon::RETICLE) }; auto getter = []()->bool { return qApp->getPreferredCursor() == RETICLE_ICON_NAME; }; diff --git a/libraries/pointers/src/Pointer.cpp b/libraries/pointers/src/Pointer.cpp index fcebb2a23f..5307e17355 100644 --- a/libraries/pointers/src/Pointer.cpp +++ b/libraries/pointers/src/Pointer.cpp @@ -11,6 +11,12 @@ #include "PickManager.h" #include "PointerManager.h" +#include "NumericalConstants.h" + +const float Pointer::POINTER_MOVE_DELAY = 0.33f * USECS_PER_SECOND; +const float TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0481f; +const float Pointer::TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED = TOUCH_PRESS_TO_MOVE_DEADSPOT * TOUCH_PRESS_TO_MOVE_DEADSPOT; + Pointer::~Pointer() { DependencyManager::get<PickManager>()->removePick(_pickUID); } @@ -77,7 +83,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin Buttons newButtons; Buttons sameButtons; if (_enabled && shouldTrigger(pickResult)) { - buttons = getPressedButtons(); + buttons = getPressedButtons(pickResult); for (const std::string& button : buttons) { if (_prevButtons.find(button) == _prevButtons.end()) { newButtons.insert(button); @@ -175,17 +181,6 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin } } - // send hoverEnd events if we disable the pointer or disable hovering - if (_hover && ((!_enabled && _prevEnabled) || (!doHover && _prevDoHover))) { - if (_prevHoveredObject.type == ENTITY) { - emit pointerManager->hoverEndEntity(_prevHoveredObject.objectID, hoveredEvent); - } else if (_prevHoveredObject.type == OVERLAY) { - emit pointerManager->hoverEndOverlay(_prevHoveredObject.objectID, hoveredEvent); - } else if (_prevHoveredObject.type == HUD) { - emit pointerManager->hoverEndHUD(hoveredEvent); - } - } - // Trigger begin const std::string SHOULD_FOCUS_BUTTON = "Focus"; for (const std::string& button : newButtons) { @@ -204,7 +199,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin // Trigger continue for (const std::string& button : sameButtons) { - PointerEvent triggeredEvent = buildPointerEvent(_triggeredObjects[button], pickResult, false); + PointerEvent triggeredEvent = buildPointerEvent(_triggeredObjects[button], pickResult, button, false); triggeredEvent.setID(pointerID); triggeredEvent.setType(PointerEvent::Move); triggeredEvent.setButton(chooseButton(button)); @@ -219,7 +214,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin // Trigger end for (const std::string& button : _prevButtons) { - PointerEvent triggeredEvent = buildPointerEvent(_triggeredObjects[button], pickResult, false); + PointerEvent triggeredEvent = buildPointerEvent(_triggeredObjects[button], pickResult, button, false); triggeredEvent.setID(pointerID); triggeredEvent.setType(PointerEvent::Release); triggeredEvent.setButton(chooseButton(button)); @@ -233,6 +228,17 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin _triggeredObjects.erase(button); } + // if we disable the pointer or disable hovering, send hoverEnd events after triggerEnd + if (_hover && ((!_enabled && _prevEnabled) || (!doHover && _prevDoHover))) { + if (_prevHoveredObject.type == ENTITY) { + emit pointerManager->hoverEndEntity(_prevHoveredObject.objectID, hoveredEvent); + } else if (_prevHoveredObject.type == OVERLAY) { + emit pointerManager->hoverEndOverlay(_prevHoveredObject.objectID, hoveredEvent); + } else if (_prevHoveredObject.type == HUD) { + emit pointerManager->hoverEndHUD(hoveredEvent); + } + } + _prevHoveredObject = hoveredObject; _prevButtons = buttons; _prevEnabled = _enabled; diff --git a/libraries/pointers/src/Pointer.h b/libraries/pointers/src/Pointer.h index 5ee55e7aeb..3197c80cad 100644 --- a/libraries/pointers/src/Pointer.h +++ b/libraries/pointers/src/Pointer.h @@ -82,14 +82,17 @@ protected: bool _enabled; bool _hover; - virtual PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, bool hover = true) const = 0; + virtual PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button = "", bool hover = true) = 0; virtual PickedObject getHoveredObject(const PickResultPointer& pickResult) = 0; - virtual Buttons getPressedButtons() = 0; + virtual Buttons getPressedButtons(const PickResultPointer& pickResult) = 0; virtual bool shouldHover(const PickResultPointer& pickResult) { return true; } virtual bool shouldTrigger(const PickResultPointer& pickResult) { return true; } + static const float POINTER_MOVE_DELAY; + static const float TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED; + private: PickedObject _prevHoveredObject; Buttons _prevButtons; diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 51f927f224..16f1d086b7 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -145,11 +145,11 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); return deltaTime; }; - this.setIgnoreTablet = function() { + this.setIgnorePointerItems = function() { if (HMD.tabletID !== this.tabletID) { this.tabletID = HMD.tabletID; - Pointers.setIgnoreItems(_this.leftPointer, _this.blacklist.concat([HMD.tabletID])); - Pointers.setIgnoreItems(_this.rightPointer, _this.blacklist.concat([HMD.tabletID])); + Pointers.setIgnoreItems(_this.leftPointer, _this.blacklist); + Pointers.setIgnoreItems(_this.rightPointer, _this.blacklist); } }; @@ -168,7 +168,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } var sensorScaleFactor = MyAvatar.sensorToWorldScale; var deltaTime = _this.updateTimings(); - _this.setIgnoreTablet(); + _this.setIgnorePointerItems(); if (controllerDispatcherPluginsNeedSort) { _this.orderedPluginNames = []; @@ -182,16 +182,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); 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; } @@ -388,8 +378,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }; this.setBlacklist = function() { - RayPick.setIgnoreItems(_this.leftControllerRayPick, this.blacklist.concat(HMD.tabletID)); - RayPick.setIgnoreItems(_this.rightControllerRayPick, this.blacklist.concat(HMD.tabletID)); + RayPick.setIgnoreItems(_this.leftControllerRayPick, this.blacklist); + RayPick.setIgnoreItems(_this.rightControllerRayPick, this.blacklist); }; var MAPPING_NAME = "com.highfidelity.controllerDispatcher"; diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 0d421bbaec..5e12252bc3 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -14,7 +14,7 @@ PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic, getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Reticle, Overlays, isPointingAtUI - Picks, makeLaserLockInfo Xform + Picks, makeLaserLockInfo Xform, makeLaserParams */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -119,7 +119,7 @@ Script.include("/~/system/libraries/Xform.js"); this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100, - this.hand); + makeLaserParams(this.hand, false)); this.handToController = function() { diff --git a/scripts/system/controllers/controllerModules/farTrigger.js b/scripts/system/controllers/controllerModules/farTrigger.js index 24f336d581..70e9ceff16 100644 --- a/scripts/system/controllers/controllerModules/farTrigger.js +++ b/scripts/system/controllers/controllerModules/farTrigger.js @@ -9,7 +9,7 @@ /* global Script, Controller, LaserPointers, RayPick, RIGHT_HAND, LEFT_HAND, MyAvatar, getGrabPointSphereOffset, makeRunningValues, Entities, enableDispatcherModule, disableDispatcherModule, makeDispatcherModuleParameters, PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, - DEFAULT_SEARCH_SPHERE_DISTANCE, getGrabbableData + DEFAULT_SEARCH_SPHERE_DISTANCE, getGrabbableData, makeLaserParams */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -34,7 +34,7 @@ Script.include("/~/system/libraries/controllers.js"); this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100, - this.hand); + makeLaserParams(this.hand, false)); this.getTargetProps = function (controllerData) { // nearbyEntityProperties is already sorted by length from controller diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index a1faacbdd6..a7a186b07a 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -16,7 +16,8 @@ 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, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic, - getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Reticle, Overlays, isPointingAtUI + getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Reticle, Overlays, isPointingAtUI, + makeLaserParams */ (function() { @@ -36,7 +37,7 @@ this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100, - (this.hand + HUD_LASER_OFFSET)); + makeLaserParams((this.hand + HUD_LASER_OFFSET), false)); this.getOtherHandController = function() { return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; diff --git a/scripts/system/controllers/controllerModules/inEditMode.js b/scripts/system/controllers/controllerModules/inEditMode.js index f79b2db1e2..763258573d 100644 --- a/scripts/system/controllers/controllerModules/inEditMode.js +++ b/scripts/system/controllers/controllerModules/inEditMode.js @@ -10,7 +10,7 @@ /* global Script, Controller, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, makeRunningValues, Messages, makeDispatcherModuleParameters, HMD, getGrabPointSphereOffset, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_ON_VALUE, - getEnabledModuleByName, PICK_MAX_DISTANCE, isInEditMode, LaserPointers, RayPick, Picks + getEnabledModuleByName, PICK_MAX_DISTANCE, isInEditMode, LaserPointers, RayPick, Picks, makeLaserParams */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -28,7 +28,7 @@ Script.include("/~/system/libraries/utils.js"); this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], [], 100, - this.hand); + makeLaserParams(this.hand, false)); this.nearTablet = function(overlays) { for (var i = 0; i < overlays.length; i++) { @@ -44,10 +44,7 @@ Script.include("/~/system/libraries/utils.js"); }; this.pointingAtTablet = function(objectID) { - if (objectID === HMD.tabletScreenID || objectID === HMD.homeButtonID) { - return true; - } - return false; + return objectID === HMD.tabletScreenID || objectID === HMD.homeButtonID; }; this.sendPickData = function(controllerData) { @@ -99,8 +96,8 @@ Script.include("/~/system/libraries/utils.js"); var overlayLaser = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightWebSurfaceLaserInput" : "LeftWebSurfaceLaserInput"); if (overlayLaser) { var overlayLaserReady = overlayLaser.isReady(controllerData); - - if (overlayLaserReady.active && this.pointingAtTablet(overlayLaser.target)) { + var target = controllerData.rayPicks[this.hand].objectID; + if (overlayLaserReady.active && this.pointingAtTablet(target)) { return this.exitModule(); } } diff --git a/scripts/system/controllers/controllerModules/inVREditMode.js b/scripts/system/controllers/controllerModules/inVREditMode.js index e3035b26f2..38eca65dd3 100644 --- a/scripts/system/controllers/controllerModules/inVREditMode.js +++ b/scripts/system/controllers/controllerModules/inVREditMode.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html /* global Script, MyAvatar, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, - makeDispatcherModuleParameters, makeRunningValues, getEnabledModuleByName + makeDispatcherModuleParameters, makeRunningValues, getEnabledModuleByName, makeLaserParams */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -19,15 +19,21 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); function InVREditMode(hand) { this.hand = hand; this.disableModules = false; + var NO_HAND_LASER = -1; // Invalid hand parameter so that default laser is not displayed. this.parameters = makeDispatcherModuleParameters( 200, // Not too high otherwise the tablet laser doesn't work. this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], [], - 100 + 100, + makeLaserParams(NO_HAND_LASER, false) ); + this.pointingAtTablet = function (objectID) { + return objectID === HMD.tabletScreenID || objectID === HMD.homeButtonID; + }; + this.isReady = function (controllerData) { if (this.disableModules) { return makeRunningValues(true, [], []); @@ -42,7 +48,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } // Tablet stylus. - // Includes the tablet laser. var tabletStylusInput = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightTabletStylusInput" : "LeftTabletStylusInput"); @@ -53,6 +58,18 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } } + // Tablet surface. + var overlayLaser = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightWebSurfaceLaserInput" + : "LeftWebSurfaceLaserInput"); + if (overlayLaser) { + var overlayLaserReady = overlayLaser.isReady(controllerData); + var target = controllerData.rayPicks[this.hand].objectID; + if (overlayLaserReady.active && this.pointingAtTablet(target)) { + return makeRunningValues(false, [], []); + } + } + // Tablet grabbing. var nearOverlay = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js index d9fa0f76a9..aa65135289 100644 --- a/scripts/system/controllers/controllerModules/stylusInput.js +++ b/scripts/system/controllers/controllerModules/stylusInput.js @@ -142,7 +142,10 @@ Script.include("/~/system/libraries/controllers.js"); }; this.isReady = function (controllerData) { - if (this.processStylus(controllerData)) { + var PREFER_STYLUS_OVER_LASER = "preferStylusOverLaser"; + var isUsingStylus = Settings.getValue(PREFER_STYLUS_OVER_LASER, false); + + if (isUsingStylus && this.processStylus(controllerData)) { Pointers.enablePointer(this.pointer); return makeRunningValues(true, [], []); } else { diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index bf90124cab..3d9d7979d5 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -9,7 +9,7 @@ makeRunningValues, Messages, Quat, Vec3, makeDispatcherModuleParameters, Overlays, ZERO_VEC, HMD, INCHES_TO_METERS, DEFAULT_REGISTRATION_POINT, 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, LaserPointers, RayPick, ContextOverlay, Picks + TRIGGER_OFF_VALUE, getEnabledModuleByName, PICK_MAX_DISTANCE, LaserPointers, RayPick, ContextOverlay, Picks, makeLaserParams */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -18,6 +18,7 @@ Script.include("/~/system/libraries/controllers.js"); (function() { function WebSurfaceLaserInput(hand) { this.hand = hand; + this.otherHand = this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND; this.running = false; this.parameters = makeDispatcherModuleParameters( @@ -25,7 +26,7 @@ Script.include("/~/system/libraries/controllers.js"); this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100, - this.hand); + makeLaserParams(hand, true)); this.grabModuleWantsNearbyOverlay = function(controllerData) { if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { @@ -65,7 +66,8 @@ Script.include("/~/system/libraries/controllers.js"); }; this.deleteContextOverlay = function() { - var farGrabModule = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightFarActionGrabEntity" : "LeftFarActionGrabEntity"); + var farGrabModule = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightFarActionGrabEntity" : "LeftFarActionGrabEntity"); if (farGrabModule) { var entityWithContextOverlay = farGrabModule.entityWithContextOverlay; @@ -76,25 +78,50 @@ Script.include("/~/system/libraries/controllers.js"); } }; - this.isReady = function (controllerData) { + this.updateAllwaysOn = function() { + var PREFER_STYLUS_OVER_LASER = "preferStylusOverLaser"; + this.parameters.handLaser.allwaysOn = !Settings.getValue(PREFER_STYLUS_OVER_LASER, false); + }; + + this.getDominantHand = function() { + return MyAvatar.getDominantHand() === "right" ? 1 : 0; + }; + + this.dominantHandOverride = false; + + this.isReady = function(controllerData) { var otherModuleRunning = this.getOtherModule().running; - if ((this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData)) && - !otherModuleRunning) { - if (controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE) { + otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand. + var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE + && controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE; + if ((!otherModuleRunning || isTriggerPressed) + && (this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData))) { + this.updateAllwaysOn(); + if (isTriggerPressed) { + this.dominantHandOverride = true; // Override dominant hand. + this.getOtherModule().dominantHandOverride = false; + } + if (this.parameters.handLaser.allwaysOn || isTriggerPressed) { return makeRunningValues(true, [], []); } } return makeRunningValues(false, [], []); }; - this.run = function (controllerData, deltaTime) { + this.run = function(controllerData, deltaTime) { + var otherModuleRunning = this.getOtherModule().running; + otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand. + otherModuleRunning = otherModuleRunning || this.getOtherModule().dominantHandOverride; // Override dominant hand. var grabModuleNeedsToRun = this.grabModuleWantsNearbyOverlay(controllerData); - if (controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE && !grabModuleNeedsToRun) { + if (!otherModuleRunning && !grabModuleNeedsToRun && (controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE + || this.parameters.handLaser.allwaysOn + && (this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData)))) { this.running = true; return makeRunningValues(true, [], []); } this.deleteContextOverlay(); this.running = false; + this.dominantHandOverride = false; return makeRunningValues(false, [], []); }; } diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 4217ec503e..05b4963280 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -118,7 +118,8 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { Overlays.deleteOverlay(this.webOverlayID); } - var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) / sensorScaleFactor; + var RAYPICK_OFFSET = 0.0001; // Sufficient for raypick to reliably intersect tablet screen before tablet model. + var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) / sensorScaleFactor + RAYPICK_OFFSET; var WEB_ENTITY_Y_OFFSET = 0.004; var screenWidth = 0.82 * tabletWidth; var screenHeight = 0.81 * tabletHeight; diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index e0971201e6..915cfc05fb 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -45,6 +45,7 @@ BUMPER_ON_VALUE:true, getEntityParents:true, findHandChildEntities:true, + makeLaserParams:true, TEAR_AWAY_DISTANCE:true, TEAR_AWAY_COUNT:true, TEAR_AWAY_CHECK_TIME:true, @@ -134,6 +135,17 @@ makeLaserLockInfo = function(targetID, isOverlay, hand, offset) { }; }; +makeLaserParams = function(hand, allwaysOn) { + if (allwaysOn === undefined) { + allwaysOn = false; + } + + return { + hand: hand, + allwaysOn: allwaysOn + }; +}; + makeRunningValues = function (active, targets, requiredDataForRun, laserLockInfo) { return { active: active, diff --git a/scripts/system/libraries/pointersUtils.js b/scripts/system/libraries/pointersUtils.js index 896ef094b8..2af563f8d4 100644 --- a/scripts/system/libraries/pointersUtils.js +++ b/scripts/system/libraries/pointersUtils.js @@ -95,6 +95,7 @@ Pointer = function(hudLayer, pickType, pointerData) { this.pointerID = null; this.visible = false; this.locked = false; + this.allwaysOn = false; this.hand = pointerData.hand; delete pointerData.hand; @@ -150,7 +151,7 @@ Pointer = function(hudLayer, pickType, pointerData) { mode = "hold"; } else if (triggerClicks[this.hand]) { mode = "full"; - } else if (triggerValues[this.hand] > TRIGGER_ON_VALUE) { + } else if (triggerValues[this.hand] > TRIGGER_ON_VALUE || this.allwaysOn) { mode = "half"; } } @@ -172,19 +173,23 @@ PointerManager = function() { return pointer.pointerID; }; - this.makePointerVisible = function(index) { + this.makePointerVisible = function(laserParams) { + var index = laserParams.hand; if (index < this.pointers.length && index >= 0) { this.pointers[index].makeVisible(); + this.pointers[index].allwaysOn = laserParams.allwaysOn; } }; - this.makePointerInvisible = function(index) { + this.makePointerInvisible = function(laserParams) { + var index = laserParams.hand; if (index < this.pointers.length && index >= 0) { this.pointers[index].makeInvisible(); } }; - this.lockPointerEnd = function(index, lockData) { + this.lockPointerEnd = function(laserParams, lockData) { + var index = laserParams.hand; if (index < this.pointers.length && index >= 0) { this.pointers[index].lockEnd(lockData); } diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index 4a1fcdf301..a6e2751a83 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -400,7 +400,8 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride) }); // update webOverlay - var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) * sensorScaleOffsetOverride; + var RAYPICK_OFFSET = 0.0001; // Sufficient for raypick to reliably intersect tablet screen before tablet model. + var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) * sensorScaleOffsetOverride + RAYPICK_OFFSET; var WEB_ENTITY_Y_OFFSET = 0.004 * sensorScaleFactor * sensorScaleOffsetOverride; var screenWidth = 0.82 * tabletWidth; var screenHeight = 0.81 * tabletHeight; @@ -417,11 +418,13 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride) var homeButtonDim = 4.0 * tabletScaleFactor / 3.0; Overlays.editOverlay(HMD.homeButtonID, { localPosition: { x: 0, y: -HOME_BUTTON_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, + localRotation: Quat.angleAxis(180, Vec3.UNIT_Y), dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim } }); Overlays.editOverlay(HMD.homeButtonHighlightID, { localPosition: { x: 0, y: -HOME_BUTTON_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, + localRotation: Quat.angleAxis(180, Vec3.UNIT_Y), dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim } }); };