// // Created by Bradley Austin Davis on 2017/10/24 // Copyright 2013-2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "StylusPointer.h" #include "RayPick.h" #include "Application.h" #include "avatar/AvatarManager.h" #include "avatar/MyAvatar.h" #include #include "PickScriptingInterface.h" #include static const float TABLET_MIN_HOVER_DISTANCE = -0.1f; static const float TABLET_MAX_HOVER_DISTANCE = 0.1f; static const float TABLET_MIN_TOUCH_DISTANCE = -0.1f; 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 QString DEFAULT_STYLUS_MODEL_URL = PathUtils::resourcesUrl() + "/meshes/tablet-stylus-fat.fbx"; StylusPointer::StylusPointer(const QVariant& props, const OverlayID& stylusOverlay, bool hover, bool enabled, const glm::vec3& modelPositionOffset, const glm::quat& modelRotationOffset, const glm::vec3& modelDimensions) : Pointer(DependencyManager::get()->createStylusPick(props), enabled, hover), _stylusOverlay(stylusOverlay), _modelPositionOffset(modelPositionOffset), _modelDimensions(modelDimensions), _modelRotationOffset(modelRotationOffset) { } StylusPointer::~StylusPointer() { if (!_stylusOverlay.isNull()) { qApp->getOverlays().deleteOverlay(_stylusOverlay); } } OverlayID StylusPointer::buildStylusOverlay(const QVariantMap& properties) { QVariantMap overlayProperties; QString modelUrl = DEFAULT_STYLUS_MODEL_URL; if (properties["model"].isValid()) { QVariantMap modelData = properties["model"].toMap(); if (modelData["url"].isValid()) { modelUrl = modelData["url"].toString(); } } // TODO: make these configurable per pointer overlayProperties["name"] = "stylus"; overlayProperties["url"] = modelUrl; overlayProperties["loadPriority"] = 10.0f; overlayProperties["solid"] = true; overlayProperties["visible"] = false; overlayProperties["ignorePickIntersection"] = true; overlayProperties["drawInFront"] = false; return qApp->getOverlays().addOverlay("model", overlayProperties); } void StylusPointer::updateVisuals(const PickResultPointer& pickResult) { auto stylusPickResult = std::static_pointer_cast(pickResult); if (_enabled && !qApp->getPreferAvatarFingerOverStylus() && _renderState != DISABLED && stylusPickResult) { StylusTip tip(stylusPickResult->pickVariant); if (tip.side != bilateral::Side::Invalid) { show(tip); return; } } if (_showing) { hide(); } } void StylusPointer::show(const StylusTip& tip) { if (!_stylusOverlay.isNull()) { QVariantMap props; auto modelOrientation = tip.orientation * _modelRotationOffset; auto sensorToWorldScale = DependencyManager::get()->getMyAvatar()->getSensorToWorldScale(); auto modelPositionOffset = modelOrientation * (_modelPositionOffset * sensorToWorldScale); props["position"] = vec3toVariant(tip.position + modelPositionOffset); props["rotation"] = quatToVariant(modelOrientation); props["dimensions"] = vec3toVariant(sensorToWorldScale * _modelDimensions); props["visible"] = true; qApp->getOverlays().editOverlay(_stylusOverlay, props); } _showing = true; } void StylusPointer::hide() { if (!_stylusOverlay.isNull()) { QVariantMap props; props.insert("visible", false); qApp->getOverlays().editOverlay(_stylusOverlay, props); } _showing = false; } bool StylusPointer::shouldHover(const PickResultPointer& pickResult) { auto stylusPickResult = std::static_pointer_cast(pickResult); if (_renderState == EVENTS_ON && stylusPickResult && stylusPickResult->intersects) { auto sensorScaleFactor = DependencyManager::get()->getMyAvatar()->getSensorToWorldScale(); float hysteresis = _state.hovering ? HOVER_HYSTERESIS * sensorScaleFactor : 0.0f; bool hovering = isWithinBounds(stylusPickResult->distance, TABLET_MIN_HOVER_DISTANCE * sensorScaleFactor, TABLET_MAX_HOVER_DISTANCE * sensorScaleFactor, hysteresis); _state.hovering = hovering; return hovering; } _state.hovering = false; return false; } bool StylusPointer::shouldTrigger(const PickResultPointer& pickResult) { auto stylusPickResult = std::static_pointer_cast(pickResult); bool wasTriggering = false; if (_renderState == EVENTS_ON && stylusPickResult) { auto sensorScaleFactor = DependencyManager::get()->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.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)) { _state.wasTriggering = _state.triggering; if (!_state.triggering) { _state.triggeredObject = PickedObject(stylusPickResult->objectID, stylusPickResult->type); _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.wasTriggering = wasTriggering; _state.triggering = false; return false; } PickResultPointer StylusPointer::getPickResultCopy(const PickResultPointer& pickResult) const { auto stylusPickResult = std::dynamic_pointer_cast(pickResult); if (!stylusPickResult) { return std::make_shared(); } return std::make_shared(*stylusPickResult.get()); } Pointer::PickedObject StylusPointer::getHoveredObject(const PickResultPointer& pickResult) { auto stylusPickResult = std::static_pointer_cast(pickResult); if (!stylusPickResult) { return PickedObject(); } return PickedObject(stylusPickResult->objectID, stylusPickResult->type); } 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, const std::string& button, bool hover) { QUuid pickedID; glm::vec2 pos2D; glm::vec3 intersection, surfaceNormal, direction, origin; auto stylusPickResult = std::static_pointer_cast(pickResult); if (stylusPickResult) { intersection = stylusPickResult->intersection; surfaceNormal = hover ? stylusPickResult->surfaceNormal : _state.surfaceNormal; const QVariantMap& stylusTip = stylusPickResult->pickVariant; origin = vec3FromVariant(stylusTip["position"]); direction = -surfaceNormal; pos2D = findPos2D(target, origin); pickedID = stylusPickResult->objectID; } // If we just started triggering and we haven't moved too much, don't update intersection and pos2D float sensorToWorldScale = DependencyManager::get()->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)); } void StylusPointer::setRenderState(const std::string& state) { if (state == "events on") { _renderState = EVENTS_ON; } else if (state == "events off") { _renderState = EVENTS_OFF; } else if (state == "disabled") { _renderState = DISABLED; } } QVariantMap StylusPointer::toVariantMap() const { return QVariantMap(); } glm::vec3 StylusPointer::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 StylusPointer::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()->calculatePos2DFromHUD(origin); default: return glm::vec2(NAN); } }