integrated stylus into pointer system, need to update controller module

This commit is contained in:
SamGondelman 2017-11-08 17:17:00 -08:00
parent f970eb2302
commit 5a78c9ebfe
21 changed files with 645 additions and 782 deletions

View file

@ -12,7 +12,7 @@
#include "avatar/AvatarManager.h"
JointRayPick::JointRayPick(const std::string& jointName, const glm::vec3& posOffset, const glm::vec3& dirOffset, const PickFilter& filter, const float maxDistance, const bool enabled) :
JointRayPick::JointRayPick(const std::string& jointName, const glm::vec3& posOffset, const glm::vec3& dirOffset, const PickFilter& filter, float maxDistance, bool enabled) :
RayPick(filter, maxDistance, enabled),
_jointName(jointName),
_posOffset(posOffset),

View file

@ -16,7 +16,7 @@
class JointRayPick : public RayPick {
public:
JointRayPick(const std::string& jointName, const glm::vec3& posOffset, const glm::vec3& dirOffset, const PickFilter& filter, const float maxDistance = 0.0f, const bool enabled = false);
JointRayPick(const std::string& jointName, const glm::vec3& posOffset, const glm::vec3& dirOffset, const PickFilter& filter, float maxDistance = 0.0f, bool enabled = false);
PickRay getMathematicalPick() const override;

View file

@ -184,7 +184,7 @@ void LaserPointer::updateVisuals(const PickResultPointer& pickResult) {
IntersectionType type = rayPickResult ? rayPickResult->type : IntersectionType::NONE;
if (_enabled && !_currentRenderState.empty() && _renderStates.find(_currentRenderState) != _renderStates.end() &&
(type != IntersectionType::NONE || _laserLength > 0.0f || !_objectLockEnd.first.isNull())) {
PickRay pickRay{ rayPickResult->pickVariant };
PickRay pickRay(rayPickResult->pickVariant);
QUuid uid = rayPickResult->objectID;
float distance = _laserLength > 0.0f ? _laserLength : rayPickResult->distance;
updateRenderState(_renderStates[_currentRenderState], type, distance, uid, pickRay, false);
@ -292,82 +292,30 @@ RenderState LaserPointer::buildRenderState(const QVariantMap& propMap) {
PointerEvent LaserPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult) const {
QUuid pickedID;
glm::vec3 intersection, surfaceNormal, direction, origin;
if (target.type != NONE) {
auto rayPickResult = std::static_pointer_cast<RayPickResult>(pickResult);
auto rayPickResult = std::static_pointer_cast<RayPickResult>(pickResult);
if (rayPickResult) {
intersection = rayPickResult->intersection;
surfaceNormal = rayPickResult->surfaceNormal;
const QVariantMap& searchRay = rayPickResult->pickVariant;
direction = vec3FromVariant(searchRay["direction"]);
origin = vec3FromVariant(searchRay["origin"]);
pickedID = rayPickResult->objectID;;
pickedID = rayPickResult->objectID;
}
glm::vec2 pos2D;
if (pickedID != target.objectID) {
if (target.type == ENTITY) {
intersection = intersectRayWithEntityXYPlane(target.objectID, origin, direction);
intersection = RayPick::intersectRayWithEntityXYPlane(target.objectID, origin, direction);
} else if (target.type == OVERLAY) {
intersection = intersectRayWithOverlayXYPlane(target.objectID, origin, direction);
intersection = RayPick::intersectRayWithOverlayXYPlane(target.objectID, origin, direction);
}
}
if (target.type == ENTITY) {
pos2D = projectOntoEntityXYPlane(target.objectID, intersection);
pos2D = RayPick::projectOntoEntityXYPlane(target.objectID, intersection);
} else if (target.type == OVERLAY) {
pos2D = projectOntoOverlayXYPlane(target.objectID, intersection);
pos2D = RayPick::projectOntoOverlayXYPlane(target.objectID, intersection);
} else if (target.type == HUD) {
pos2D = DependencyManager::get<PickManager>()->calculatePos2DFromHUD(intersection);
}
return PointerEvent(pos2D, intersection, surfaceNormal, direction);
}
glm::vec3 LaserPointer::intersectRayWithXYPlane(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& point, const glm::quat rotation, const glm::vec3& registration) const {
glm::vec3 n = rotation * Vectors::FRONT;
float t = glm::dot(n, point - origin) / glm::dot(n, direction);
return origin + t * direction;
}
glm::vec3 LaserPointer::intersectRayWithOverlayXYPlane(const QUuid& overlayID, const glm::vec3& origin, const glm::vec3& direction) const {
glm::vec3 position = vec3FromVariant(qApp->getOverlays().getProperty(overlayID, "position").value);
glm::quat rotation = quatFromVariant(qApp->getOverlays().getProperty(overlayID, "rotation").value);
const glm::vec3 DEFAULT_REGISTRATION_POINT = glm::vec3(0.5f);
return intersectRayWithXYPlane(origin, direction, position, rotation, DEFAULT_REGISTRATION_POINT);
}
glm::vec3 LaserPointer::intersectRayWithEntityXYPlane(const QUuid& entityID, const glm::vec3& origin, const glm::vec3& direction) const {
auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
return intersectRayWithXYPlane(origin, direction, props.getPosition(), props.getRotation(), props.getRegistrationPoint());
}
glm::vec2 LaserPointer::projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint) const {
glm::quat invRot = glm::inverse(rotation);
glm::vec3 localPos = invRot * (worldPos - position);
glm::vec3 invDimensions = glm::vec3(1.0f / dimensions.x, 1.0f / dimensions.y, 1.0f / dimensions.z);
glm::vec3 normalizedPos = (localPos * invDimensions) + registrationPoint;
return glm::vec2(normalizedPos.x * dimensions.x, (1.0f - normalizedPos.y) * dimensions.y);
}
glm::vec2 LaserPointer::projectOntoOverlayXYPlane(const QUuid& overlayID, const glm::vec3& worldPos) const {
glm::vec3 position = vec3FromVariant(qApp->getOverlays().getProperty(overlayID, "position").value);
glm::quat rotation = quatFromVariant(qApp->getOverlays().getProperty(overlayID, "rotation").value);
glm::vec3 dimensions;
float dpi = qApp->getOverlays().getProperty(overlayID, "dpi").value.toFloat();
if (dpi > 0) {
// Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale.
glm::vec3 resolution = glm::vec3(vec2FromVariant(qApp->getOverlays().getProperty(overlayID, "resolution").value), 1);
glm::vec3 scale = glm::vec3(vec2FromVariant(qApp->getOverlays().getProperty(overlayID, "dimensions").value), 0.01f);
const float INCHES_TO_METERS = 1.0f / 39.3701f;
dimensions = (resolution * INCHES_TO_METERS / dpi) * scale;
} else {
dimensions = glm::vec3(vec2FromVariant(qApp->getOverlays().getProperty(overlayID, "dimensions").value), 0.01);
}
const glm::vec3 DEFAULT_REGISTRATION_POINT = glm::vec3(0.5f);
return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT);
}
glm::vec2 LaserPointer::projectOntoEntityXYPlane(const QUuid& entityID, const glm::vec3& worldPos) const {
auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
return projectOntoXYPlane(worldPos, props.getPosition(), props.getRotation(), props.getDimensions(), props.getRegistrationPoint());
}
}

View file

@ -75,8 +75,8 @@ protected:
PickedObject getHoveredObject(const PickResultPointer& pickResult) override;
Pointer::Buttons getPressedButtons() override;
bool shouldHover() override { return _currentRenderState != ""; }
bool shouldTrigger() override { return _currentRenderState != ""; }
bool shouldHover(const PickResultPointer& pickResult) override { return _currentRenderState != ""; }
bool shouldTrigger(const PickResultPointer& pickResult) override { return _currentRenderState != ""; }
private:
PointerTriggers _triggers;
@ -94,14 +94,6 @@ private:
void updateRenderState(const RenderState& renderState, const IntersectionType type, float distance, const QUuid& objectID, const PickRay& pickRay, bool defaultState);
void disableRenderState(const RenderState& renderState);
glm::vec3 intersectRayWithEntityXYPlane(const QUuid& entityID, const glm::vec3& origin, const glm::vec3& direction) const;
glm::vec3 intersectRayWithOverlayXYPlane(const QUuid& overlayID, const glm::vec3& origin, const glm::vec3& direction) const;
glm::vec3 intersectRayWithXYPlane(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& point, const glm::quat rotation, const glm::vec3& registration) const;
glm::vec2 projectOntoEntityXYPlane(const QUuid& entityID, const glm::vec3& worldPos) const;
glm::vec2 projectOntoOverlayXYPlane(const QUuid& overlayID, const glm::vec3& worldPos) const;
glm::vec2 projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint) const;
};
#endif // hifi_LaserPointer_h

View file

@ -13,7 +13,7 @@
#include "Application.h"
#include "display-plugins/CompositorHelper.h"
MouseRayPick::MouseRayPick(const PickFilter& filter, const float maxDistance, const bool enabled) :
MouseRayPick::MouseRayPick(const PickFilter& filter, float maxDistance, bool enabled) :
RayPick(filter, maxDistance, enabled)
{
}

View file

@ -16,7 +16,7 @@
class MouseRayPick : public RayPick {
public:
MouseRayPick(const PickFilter& filter, const float maxDistance = 0.0f, const bool enabled = false);
MouseRayPick(const PickFilter& filter, float maxDistance = 0.0f, bool enabled = false);
PickRay getMathematicalPick() const override;

View file

@ -16,6 +16,7 @@
#include "StaticRayPick.h"
#include "JointRayPick.h"
#include "MouseRayPick.h"
#include "StylusPick.h"
#include <pointers/Pick.h>
#include <ScriptEngine.h>
@ -24,6 +25,8 @@ unsigned int PickScriptingInterface::createPick(const PickQuery::PickType type,
switch (type) {
case PickQuery::PickType::Ray:
return createRayPick(properties);
case PickQuery::PickType::Stylus:
return createStylusPick(properties);
default:
return PickManager::INVALID_PICK_ID;
}
@ -81,6 +84,30 @@ unsigned int PickScriptingInterface::createRayPick(const QVariant& properties) {
return PickManager::INVALID_PICK_ID;
}
unsigned int PickScriptingInterface::createStylusPick(const QVariant& properties) {
QVariantMap propMap = properties.toMap();
bilateral::Side side = bilateral::Side::Invalid;
{
QVariant handVar = propMap["hand"];
if (handVar.isValid()) {
side = bilateral::side(handVar.toInt());
}
}
bool enabled = false;
if (propMap["enabled"].isValid()) {
enabled = propMap["enabled"].toBool();
}
PickFilter filter = PickFilter();
if (propMap["filter"].isValid()) {
filter = PickFilter(propMap["filter"].toUInt());
}
return DependencyManager::get<PickManager>()->addPick(PickQuery::Stylus, std::make_shared<StylusPick>(filter, side, enabled));
}
void PickScriptingInterface::enablePick(unsigned int uid) {
DependencyManager::get<PickManager>()->enablePick(uid);
}

View file

@ -34,6 +34,7 @@ class PickScriptingInterface : public QObject, public Dependency {
public:
unsigned int createRayPick(const QVariant& properties);
unsigned int createStylusPick(const QVariant& properties);
void registerMetaTypes(QScriptEngine* engine);

View file

@ -43,19 +43,19 @@ unsigned int PointerScriptingInterface::createPointer(const PickQuery::PickType&
}
unsigned int PointerScriptingInterface::createStylus(const QVariant& properties) const {
bilateral::Side side = bilateral::Side::Invalid;
{
QVariant handVar = properties.toMap()["hand"];
if (handVar.isValid()) {
side = bilateral::side(handVar.toInt());
}
QVariantMap propertyMap = properties.toMap();
bool hover = false;
if (propertyMap["hover"].isValid()) {
hover = propertyMap["hover"].toBool();
}
if (bilateral::Side::Invalid == side) {
return PointerEvent::INVALID_POINTER_ID;
bool enabled = false;
if (propertyMap["enabled"].isValid()) {
enabled = propertyMap["enabled"].toBool();
}
return DependencyManager::get<PointerManager>()->addPointer(std::make_shared<StylusPointer>(side));
return DependencyManager::get<PointerManager>()->addPointer(std::make_shared<StylusPointer>(properties, StylusPointer::buildStylusOverlay(propertyMap), hover, enabled));
}
unsigned int PointerScriptingInterface::createLaserPointer(const QVariant& properties) const {

View file

@ -48,4 +48,61 @@ PickResultPointer RayPick::getAvatarIntersection(const PickRay& pick) {
PickResultPointer RayPick::getHUDIntersection(const PickRay& pick) {
glm::vec3 hudRes = DependencyManager::get<HMDScriptingInterface>()->calculateRayUICollisionPoint(pick.origin, pick.direction);
return std::make_shared<RayPickResult>(IntersectionType::HUD, QUuid(), glm::distance(pick.origin, hudRes), hudRes, pick);
}
}
glm::vec3 RayPick::intersectRayWithXYPlane(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& point, const glm::quat& rotation, const glm::vec3& registration) {
// TODO: take into account registration
glm::vec3 n = rotation * Vectors::FRONT;
float t = glm::dot(n, point - origin) / glm::dot(n, direction);
return origin + t * direction;
}
glm::vec3 RayPick::intersectRayWithOverlayXYPlane(const QUuid& overlayID, const glm::vec3& origin, const glm::vec3& direction) {
glm::vec3 position = vec3FromVariant(qApp->getOverlays().getProperty(overlayID, "position").value);
glm::quat rotation = quatFromVariant(qApp->getOverlays().getProperty(overlayID, "rotation").value);
const glm::vec3 DEFAULT_REGISTRATION_POINT = glm::vec3(0.5f);
return intersectRayWithXYPlane(origin, direction, position, rotation, DEFAULT_REGISTRATION_POINT);
}
glm::vec3 RayPick::intersectRayWithEntityXYPlane(const QUuid& entityID, const glm::vec3& origin, const glm::vec3& direction) {
auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
return intersectRayWithXYPlane(origin, direction, props.getPosition(), props.getRotation(), props.getRegistrationPoint());
}
glm::vec2 RayPick::projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNormalized) {
glm::quat invRot = glm::inverse(rotation);
glm::vec3 localPos = invRot * (worldPos - position);
glm::vec3 normalizedPos = (localPos / dimensions) + registrationPoint;
glm::vec2 pos2D = glm::vec2(normalizedPos.x, (1.0f - normalizedPos.y));
if (unNormalized) {
pos2D *= glm::vec2(dimensions.x, dimensions.y);
}
return pos2D;
}
glm::vec2 RayPick::projectOntoOverlayXYPlane(const QUuid& overlayID, const glm::vec3& worldPos, bool unNormalized) {
glm::vec3 position = vec3FromVariant(qApp->getOverlays().getProperty(overlayID, "position").value);
glm::quat rotation = quatFromVariant(qApp->getOverlays().getProperty(overlayID, "rotation").value);
glm::vec3 dimensions;
float dpi = qApp->getOverlays().getProperty(overlayID, "dpi").value.toFloat();
if (dpi > 0) {
// Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale.
glm::vec3 resolution = glm::vec3(vec2FromVariant(qApp->getOverlays().getProperty(overlayID, "resolution").value), 1);
glm::vec3 scale = glm::vec3(vec2FromVariant(qApp->getOverlays().getProperty(overlayID, "dimensions").value), 0.01f);
const float INCHES_TO_METERS = 1.0f / 39.3701f;
dimensions = (resolution * INCHES_TO_METERS / dpi) * scale;
} else {
dimensions = glm::vec3(vec2FromVariant(qApp->getOverlays().getProperty(overlayID, "dimensions").value), 0.01);
}
const glm::vec3 DEFAULT_REGISTRATION_POINT = glm::vec3(0.5f);
return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT, unNormalized);
}
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);
}

View file

@ -18,7 +18,7 @@ class RayPickResult : public PickResult {
public:
RayPickResult() {}
RayPickResult(const QVariantMap& pickVariant) : PickResult(pickVariant) {}
RayPickResult(const IntersectionType type, const QUuid& objectID, const float distance, const glm::vec3& intersection, const PickRay& searchRay, const glm::vec3& surfaceNormal = glm::vec3(NAN)) :
RayPickResult(const IntersectionType type, const QUuid& objectID, float distance, const glm::vec3& intersection, const PickRay& searchRay, const glm::vec3& surfaceNormal = glm::vec3(NAN)) :
PickResult(searchRay.toVariantMap()), type(type), intersects(type != NONE), objectID(objectID), distance(distance), intersection(intersection), surfaceNormal(surfaceNormal) {
}
@ -67,13 +67,23 @@ public:
class RayPick : public Pick<PickRay> {
public:
RayPick(const PickFilter& filter, const float maxDistance, const bool enabled) : Pick(filter, maxDistance, enabled) {}
RayPick(const PickFilter& filter, float maxDistance, bool enabled) : Pick(filter, maxDistance, enabled) {}
PickResultPointer getDefaultResult(const QVariantMap& pickVariant) const override { return std::make_shared<RayPickResult>(pickVariant); }
PickResultPointer getEntityIntersection(const PickRay& pick) override;
PickResultPointer getOverlayIntersection(const PickRay& pick) override;
PickResultPointer getAvatarIntersection(const PickRay& pick) override;
PickResultPointer getHUDIntersection(const PickRay& pick) override;
// These are helper functions for projecting and intersecting rays
static glm::vec3 intersectRayWithEntityXYPlane(const QUuid& entityID, const glm::vec3& origin, const glm::vec3& direction);
static glm::vec3 intersectRayWithOverlayXYPlane(const QUuid& overlayID, const glm::vec3& origin, const glm::vec3& direction);
static glm::vec2 projectOntoEntityXYPlane(const QUuid& entityID, const glm::vec3& worldPos, bool unNormalized = true);
static glm::vec2 projectOntoOverlayXYPlane(const QUuid& overlayID, const glm::vec3& worldPos, bool unNormalized = true);
private:
static glm::vec3 intersectRayWithXYPlane(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& point, const glm::quat& rotation, const glm::vec3& registration);
static glm::vec2 projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNormalized);
};
#endif // hifi_RayPick_h

View file

@ -7,7 +7,7 @@
//
#include "StaticRayPick.h"
StaticRayPick::StaticRayPick(const glm::vec3& position, const glm::vec3& direction, const PickFilter& filter, const float maxDistance, const bool enabled) :
StaticRayPick::StaticRayPick(const glm::vec3& position, const glm::vec3& direction, const PickFilter& filter, float maxDistance, bool enabled) :
RayPick(filter, maxDistance, enabled),
_pickRay(position, direction)
{

View file

@ -13,7 +13,7 @@
class StaticRayPick : public RayPick {
public:
StaticRayPick(const glm::vec3& position, const glm::vec3& direction, const PickFilter& filter, const float maxDistance = 0.0f, const bool enabled = false);
StaticRayPick(const glm::vec3& position, const glm::vec3& direction, const PickFilter& filter, float maxDistance = 0.0f, bool enabled = false);
PickRay getMathematicalPick() const override;

View file

@ -0,0 +1,230 @@
//
// 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 "StylusPick.h"
#include "RayPick.h"
#include <glm/glm.hpp>
#include "ui/overlays/Base3DOverlay.h"
#include "Application.h"
#include <DependencyManager.h>
#include "avatar/AvatarManager.h"
#include <controllers/StandardControls.h>
#include <controllers/UserInputMapper.h>
using namespace bilateral;
// TODO: make these configurable per pick
static Setting::Handle<double> USE_FINGER_AS_STYLUS("preferAvatarFingerOverStylus", false);
static const float WEB_STYLUS_LENGTH = 0.2f;
static const float WEB_TOUCH_Y_OFFSET = 0.105f; // how far forward (or back with a negative number) to slide stylus in hand
static const glm::vec3 TIP_OFFSET{ 0.0f, WEB_STYLUS_LENGTH - WEB_TOUCH_Y_OFFSET, 0.0f };
struct SideData {
QString avatarJoint;
QString cameraJoint;
controller::StandardPoseChannel channel;
controller::Hand hand;
glm::vec3 grabPointSphereOffset;
int getJointIndex(bool finger) {
const auto& jointName = finger ? avatarJoint : cameraJoint;
return DependencyManager::get<AvatarManager>()->getMyAvatar()->getJointIndex(jointName);
}
};
static const std::array<SideData, 2> SIDES{ { { "LeftHandIndex4",
"_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
controller::StandardPoseChannel::LEFT_HAND,
controller::Hand::LEFT,
{ -0.04f, 0.13f, 0.039f } },
{ "RightHandIndex4",
"_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND",
controller::StandardPoseChannel::RIGHT_HAND,
controller::Hand::RIGHT,
{ 0.04f, 0.13f, 0.039f } } } };
std::shared_ptr<PickResult> StylusPickResult::compareAndProcessNewResult(const std::shared_ptr<PickResult>& newRes) {
auto newStylusResult = std::static_pointer_cast<StylusPickResult>(newRes);
if (newStylusResult && newStylusResult->distance < distance) {
return std::make_shared<StylusPickResult>(*newStylusResult);
} else {
return std::make_shared<StylusPickResult>(*this);
}
}
bool StylusPickResult::checkOrFilterAgainstMaxDistance(float maxDistance) {
return distance < maxDistance;
}
StylusPick::StylusPick(const PickFilter& filter, Side side, bool enabled) :
Pick(filter, 0.0f, enabled),
_side(side)
{
}
static StylusTip getFingerWorldLocation(Side side) {
const auto& sideData = SIDES[index(side)];
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto fingerJointIndex = myAvatar->getJointIndex(sideData.avatarJoint);
if (fingerJointIndex == -1) {
return StylusTip();
}
auto fingerPosition = myAvatar->getAbsoluteJointTranslationInObjectFrame(fingerJointIndex);
auto fingerRotation = myAvatar->getAbsoluteJointRotationInObjectFrame(fingerJointIndex);
auto avatarOrientation = myAvatar->getOrientation();
auto avatarPosition = myAvatar->getPosition();
StylusTip result;
result.side = side;
result.orientation = avatarOrientation * fingerRotation;
result.position = avatarPosition + (avatarOrientation * fingerPosition);
return result;
}
// controllerWorldLocation is where the controller would be, in-world, with an added offset
static StylusTip getControllerWorldLocation(Side side) {
static const std::array<controller::Input, 2> INPUTS{ { UserInputMapper::makeStandardInput(SIDES[0].channel),
UserInputMapper::makeStandardInput(SIDES[1].channel) } };
const auto sideIndex = index(side);
const auto& input = INPUTS[sideIndex];
const auto pose = DependencyManager::get<UserInputMapper>()->getPose(input);
const auto& valid = pose.valid;
StylusTip result;
if (valid) {
result.side = side;
const auto& sideData = SIDES[sideIndex];
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
float sensorScaleFactor = myAvatar->getSensorToWorldScale();
auto controllerJointIndex = myAvatar->getJointIndex(sideData.cameraJoint);
const auto avatarOrientation = myAvatar->getOrientation();
const auto avatarPosition = myAvatar->getPosition();
result.orientation = avatarOrientation * myAvatar->getAbsoluteJointRotationInObjectFrame(controllerJointIndex);
result.position = avatarPosition + (avatarOrientation * myAvatar->getAbsoluteJointTranslationInObjectFrame(controllerJointIndex));
// add to the real position so the grab-point is out in front of the hand, a bit
result.position += result.orientation * (sideData.grabPointSphereOffset * sensorScaleFactor);
// move the stylus forward a bit
result.position += result.orientation * (TIP_OFFSET * sensorScaleFactor);
auto worldControllerPos = avatarPosition + avatarOrientation * pose.translation;
// compute tip velocity from hand controller motion, it is more accurate than computing it from previous positions.
auto worldControllerLinearVel = avatarOrientation * pose.velocity;
auto worldControllerAngularVel = avatarOrientation * pose.angularVelocity;
result.velocity = worldControllerLinearVel + glm::cross(worldControllerAngularVel, result.position - worldControllerPos);
}
return result;
}
StylusTip StylusPick::getMathematicalPick() const {
StylusTip result;
if (USE_FINGER_AS_STYLUS.get()) {
result = getFingerWorldLocation(_side);
} else {
result = getControllerWorldLocation(_side);
}
return result;
}
PickResultPointer StylusPick::getDefaultResult(const QVariantMap& pickVariant) const {
return std::make_shared<StylusPickResult>(pickVariant);
}
PickResultPointer StylusPick::getEntityIntersection(const StylusTip& pick) {
std::vector<StylusPickResult> results;
for (const auto& target : getIncludeItems()) {
if (target.isNull()) {
continue;
}
auto entity = qApp->getEntities()->getTree()->findEntityByEntityItemID(target);
// Don't interact with non-3D or invalid overlays
if (!entity) {
continue;
}
if (!entity->getVisible() && !getFilter().doesPickInvisible()) {
continue;
}
const auto entityRotation = entity->getRotation();
const auto entityPosition = entity->getPosition();
glm::vec3 normal = entityRotation * Vectors::UNIT_Z;
float distance = glm::dot(pick.position - entityPosition, normal);
glm::vec3 intersection = pick.position - (normal * distance);
glm::vec2 pos2D = RayPick::projectOntoEntityXYPlane(target, intersection, false);
if (pos2D == glm::clamp(pos2D, glm::vec2(0), glm::vec2(1))) {
results.push_back(StylusPickResult(IntersectionType::ENTITY, target, distance, intersection, pick, normal));
}
}
StylusPickResult nearestTarget(pick.toVariantMap());
for (const auto& result : results) {
if (result.distance < nearestTarget.distance) {
nearestTarget = result;
}
}
return std::make_shared<StylusPickResult>(nearestTarget);
}
PickResultPointer StylusPick::getOverlayIntersection(const StylusTip& pick) {
std::vector<StylusPickResult> results;
for (const auto& target : getIncludeItems()) {
if (target.isNull()) {
continue;
}
auto overlay = qApp->getOverlays().getOverlay(target);
// Don't interact with non-3D or invalid overlays
if (!overlay || !overlay->is3D()) {
continue;
}
if (!overlay->getVisible() && !getFilter().doesPickInvisible()) {
continue;
}
auto overlay3D = std::static_pointer_cast<Base3DOverlay>(overlay);
const auto overlayRotation = overlay3D->getRotation();
const auto overlayPosition = overlay3D->getPosition();
glm::vec3 normal = overlayRotation * Vectors::UNIT_Z;
float distance = glm::dot(pick.position - overlayPosition, normal);
glm::vec3 intersection = pick.position - (normal * distance);
glm::vec2 pos2D = RayPick::projectOntoOverlayXYPlane(target, intersection, false);
if (pos2D == glm::clamp(pos2D, glm::vec2(0), glm::vec2(1))) {
results.push_back(StylusPickResult(IntersectionType::OVERLAY, target, distance, intersection, pick, normal));
}
}
StylusPickResult nearestTarget(pick.toVariantMap());
for (const auto& result : results) {
if (result.distance < nearestTarget.distance) {
nearestTarget = result;
}
}
return std::make_shared<StylusPickResult>(nearestTarget);
}
PickResultPointer StylusPick::getAvatarIntersection(const StylusTip& pick) {
return std::make_shared<StylusPickResult>(pick.toVariantMap());
}
PickResultPointer StylusPick::getHUDIntersection(const StylusTip& pick) {
return std::make_shared<StylusPickResult>(pick.toVariantMap());
}

View file

@ -0,0 +1,78 @@
//
// 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
//
#ifndef hifi_StylusPick_h
#define hifi_StylusPick_h
#include "pointers/Pick.h"
#include "RegisteredMetaTypes.h"
class StylusPickResult : public PickResult {
using Side = bilateral::Side;
public:
StylusPickResult() {}
StylusPickResult(const QVariantMap& pickVariant) : PickResult(pickVariant) {}
StylusPickResult(const IntersectionType type, const QUuid& objectID, float distance, const glm::vec3& intersection, const StylusTip& stylusTip,
const glm::vec3& surfaceNormal = glm::vec3(NAN)) :
PickResult(stylusTip.toVariantMap()), type(type), intersects(type != NONE), objectID(objectID), distance(distance), intersection(intersection), surfaceNormal(surfaceNormal) {
}
StylusPickResult(const StylusPickResult& stylusPickResult) : PickResult(stylusPickResult.pickVariant) {
type = stylusPickResult.type;
intersects = stylusPickResult.intersects;
objectID = stylusPickResult.objectID;
distance = stylusPickResult.distance;
intersection = stylusPickResult.intersection;
surfaceNormal = stylusPickResult.surfaceNormal;
}
IntersectionType type { NONE };
bool intersects { false };
QUuid objectID;
float distance { FLT_MAX };
glm::vec3 intersection { NAN };
glm::vec3 surfaceNormal { NAN };
virtual QVariantMap toVariantMap() const override {
QVariantMap toReturn;
toReturn["type"] = type;
toReturn["intersects"] = intersects;
toReturn["objectID"] = objectID;
toReturn["distance"] = distance;
toReturn["intersection"] = vec3toVariant(intersection);
toReturn["surfaceNormal"] = vec3toVariant(surfaceNormal);
toReturn["stylusTip"] = PickResult::toVariantMap();
return toReturn;
}
bool doesIntersect() const override { return intersects; }
std::shared_ptr<PickResult> compareAndProcessNewResult(const std::shared_ptr<PickResult>& newRes) override;
bool checkOrFilterAgainstMaxDistance(float maxDistance) override;
};
class StylusPick : public Pick<StylusTip> {
using Side = bilateral::Side;
public:
StylusPick(const PickFilter& filter, Side side, bool enabled);
StylusTip getMathematicalPick() const override;
PickResultPointer getDefaultResult(const QVariantMap& pickVariant) const override;
PickResultPointer getEntityIntersection(const StylusTip& pick) override;
PickResultPointer getOverlayIntersection(const StylusTip& pick) override;
PickResultPointer getAvatarIntersection(const StylusTip& pick) override;
PickResultPointer getHUDIntersection(const StylusTip& pick) override;
private:
const Side _side;
};
#endif // hifi_StylusPick_h

View file

@ -7,621 +7,200 @@
//
#include "StylusPointer.h"
#include <array>
#include <QtCore/QThread>
#include <DependencyManager.h>
#include <pointers/PickManager.h>
#include <GLMHelpers.h>
#include <Transform.h>
#include <shared/QtHelpers.h>
#include <controllers/StandardControls.h>
#include <controllers/UserInputMapper.h>
#include <RegisteredMetaTypes.h>
#include <MessagesClient.h>
#include <EntityItemID.h>
#include "RayPick.h"
#include "Application.h"
#include "avatar/AvatarManager.h"
#include "avatar/MyAvatar.h"
#include "scripting/HMDScriptingInterface.h"
#include "ui/overlays/Web3DOverlay.h"
#include "ui/overlays/Sphere3DOverlay.h"
#include "avatar/AvatarManager.h"
#include "InterfaceLogging.h"
#include <DependencyManager.h>
#include "PickScriptingInterface.h"
#include <pointers/PickManager.h>
using namespace controller;
using namespace bilateral;
static Setting::Handle<double> USE_FINGER_AS_STYLUS("preferAvatarFingerOverStylus", false);
// TODO: make these configurable per pointer
static const float WEB_STYLUS_LENGTH = 0.2f;
static const float WEB_TOUCH_Y_OFFSET = 0.105f; // how far forward (or back with a negative number) to slide stylus in hand
static const vec3 TIP_OFFSET{ 0.0f, WEB_STYLUS_LENGTH - WEB_TOUCH_Y_OFFSET, 0.0f };
static const float TABLET_MIN_HOVER_DISTANCE = 0.01f;
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.05f;
static const float TABLET_MAX_TOUCH_DISTANCE = TABLET_MIN_HOVER_DISTANCE;
static const float EDGE_BORDER = 0.075f;
static const float TABLET_MIN_TOUCH_DISTANCE = -0.1f;
static const float TABLET_MAX_TOUCH_DISTANCE = 0.01f;
static const float HOVER_HYSTERESIS = 0.01f;
static const float NEAR_HYSTERESIS = 0.05f;
static const float TOUCH_HYSTERESIS = 0.002f;
static const float TOUCH_HYSTERESIS = 0.02f;
// triggered when stylus presses a web overlay/entity
static const float HAPTIC_STYLUS_STRENGTH = 1.0f;
static const float HAPTIC_STYLUS_DURATION = 20.0f;
static const float POINTER_PRESS_TO_MOVE_DELAY = 0.33f; // seconds
static const float WEB_DISPLAY_STYLUS_DISTANCE = 0.5f;
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;
std::array<StylusPointer*, 2> STYLUSES;
static OverlayID getHomeButtonID() {
return DependencyManager::get<HMDScriptingInterface>()->getCurrentHomeButtonID();
}
static OverlayID getTabletScreenID() {
return DependencyManager::get<HMDScriptingInterface>()->getCurrentTabletScreenID();
}
struct SideData {
QString avatarJoint;
QString cameraJoint;
controller::StandardPoseChannel channel;
controller::Hand hand;
vec3 grabPointSphereOffset;
int getJointIndex(bool finger) {
const auto& jointName = finger ? avatarJoint : cameraJoint;
return DependencyManager::get<AvatarManager>()->getMyAvatar()->getJointIndex(jointName);
}
};
static const std::array<SideData, 2> SIDES{ { { "LeftHandIndex4",
"_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
StandardPoseChannel::LEFT_HAND,
Hand::LEFT,
{ -0.04f, 0.13f, 0.039f } },
{ "RightHandIndex4",
"_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND",
StandardPoseChannel::RIGHT_HAND,
Hand::RIGHT,
{ 0.04f, 0.13f, 0.039f } } } };
static StylusTip getFingerWorldLocation(Side side) {
const auto& sideData = SIDES[index(side)];
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto fingerJointIndex = myAvatar->getJointIndex(sideData.avatarJoint);
if (-1 == fingerJointIndex) {
return StylusTip();
}
auto fingerPosition = myAvatar->getAbsoluteJointTranslationInObjectFrame(fingerJointIndex);
auto fingerRotation = myAvatar->getAbsoluteJointRotationInObjectFrame(fingerJointIndex);
auto avatarOrientation = myAvatar->getOrientation();
auto avatarPosition = myAvatar->getPosition();
StylusTip result;
result.side = side;
result.orientation = avatarOrientation * fingerRotation;
result.position = avatarPosition + (avatarOrientation * fingerPosition);
return result;
}
// controllerWorldLocation is where the controller would be, in-world, with an added offset
static StylusTip getControllerWorldLocation(Side side, float sensorToWorldScale) {
static const std::array<Input, 2> INPUTS{ { UserInputMapper::makeStandardInput(SIDES[0].channel),
UserInputMapper::makeStandardInput(SIDES[1].channel) } };
const auto sideIndex = index(side);
const auto& input = INPUTS[sideIndex];
const auto pose = DependencyManager::get<UserInputMapper>()->getPose(input);
const auto& valid = pose.valid;
StylusTip result;
if (valid) {
result.side = side;
const auto& sideData = SIDES[sideIndex];
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto controllerJointIndex = myAvatar->getJointIndex(sideData.cameraJoint);
const auto avatarOrientation = myAvatar->getOrientation();
const auto avatarPosition = myAvatar->getPosition();
result.orientation = avatarOrientation * myAvatar->getAbsoluteJointRotationInObjectFrame(controllerJointIndex);
result.position =
avatarPosition + (avatarOrientation * myAvatar->getAbsoluteJointTranslationInObjectFrame(controllerJointIndex));
// add to the real position so the grab-point is out in front of the hand, a bit
result.position += result.orientation * (sideData.grabPointSphereOffset * sensorToWorldScale);
auto worldControllerPos = avatarPosition + avatarOrientation * pose.translation;
// compute tip velocity from hand controller motion, it is more accurate than computing it from previous positions.
auto worldControllerLinearVel = avatarOrientation * pose.velocity;
auto worldControllerAngularVel = avatarOrientation * pose.angularVelocity;
result.velocity =
worldControllerLinearVel + glm::cross(worldControllerAngularVel, result.position - worldControllerPos);
}
return result;
}
bool StylusPickResult::isNormalized() const {
return valid && (normalizedPosition == glm::clamp(normalizedPosition, vec3(0), vec3(1)));
}
bool StylusPickResult::isNearNormal(float min, float max, float hystersis) const {
return valid && (distance == glm::clamp(distance, min - hystersis, max + hystersis));
}
bool StylusPickResult::isNear2D(float border, float hystersis) const {
return valid && position2D == glm::clamp(position2D, vec2(0) - border - hystersis, vec2(dimensions) + border + hystersis);
}
bool StylusPickResult::isNear(float min, float max, float border, float hystersis) const {
// check to see if the projected stylusTip is within within the 2d border
return isNearNormal(min, max, hystersis) && isNear2D(border, hystersis);
}
StylusPickResult::operator bool() const {
return valid;
}
bool StylusPickResult::hasKeyboardFocus() const {
if (!overlayID.isNull()) {
return qApp->getOverlays().getKeyboardFocusOverlay() == overlayID;
}
#if 0
if (!entityID.isNull()) {
return qApp->getKeyboardFocusEntity() == entityID;
}
#endif
return false;
}
void StylusPickResult::setKeyboardFocus() const {
if (!overlayID.isNull()) {
qApp->getOverlays().setKeyboardFocusOverlay(overlayID);
qApp->setKeyboardFocusEntity(EntityItemID());
#if 0
} else if (!entityID.isNull()) {
qApp->getOverlays().setKeyboardFocusOverlay(OverlayID());
qApp->setKeyboardFocusEntity(entityID);
#endif
}
}
void StylusPickResult::sendHoverOverEvent() const {
if (!overlayID.isNull()) {
qApp->getOverlays().hoverOverOverlay(overlayID, PointerEvent{ PointerEvent::Move, deviceId(), position2D, position,
normal, -normal });
}
// FIXME support entity
}
void StylusPickResult::sendHoverEnterEvent() const {
if (!overlayID.isNull()) {
qApp->getOverlays().hoverEnterOverlay(overlayID, PointerEvent{ PointerEvent::Move, deviceId(), position2D, position,
normal, -normal });
}
// FIXME support entity
}
void StylusPickResult::sendTouchStartEvent() const {
if (!overlayID.isNull()) {
qApp->getOverlays().sendMousePressOnOverlay(overlayID, PointerEvent{ PointerEvent::Press, deviceId(), position2D, position,
normal, -normal, PointerEvent::PrimaryButton,
PointerEvent::PrimaryButton });
}
// FIXME support entity
}
void StylusPickResult::sendTouchEndEvent() const {
if (!overlayID.isNull()) {
qApp->getOverlays().sendMouseReleaseOnOverlay(overlayID,
PointerEvent{ PointerEvent::Release, deviceId(), position2D, position, normal,
-normal, PointerEvent::PrimaryButton });
}
// FIXME support entity
}
void StylusPickResult::sendTouchMoveEvent() const {
if (!overlayID.isNull()) {
qApp->getOverlays().sendMouseMoveOnOverlay(overlayID, PointerEvent{ PointerEvent::Move, deviceId(), position2D, position,
normal, -normal, PointerEvent::PrimaryButton,
PointerEvent::PrimaryButton });
}
// FIXME support entity
}
bool StylusPickResult::doesIntersect() const {
return true;
}
// for example: if we want the closest result, compare based on distance
// if we want all results, combine them
// must return a new pointer
std::shared_ptr<PickResult> StylusPickResult::compareAndProcessNewResult(const std::shared_ptr<PickResult>& newRes) {
auto newStylusResult = std::static_pointer_cast<StylusPickResult>(newRes);
if (newStylusResult && newStylusResult->distance < distance) {
return std::make_shared<StylusPickResult>(*newStylusResult);
} else {
return std::make_shared<StylusPickResult>(*this);
}
}
// returns true if this result contains any valid results with distance < maxDistance
// can also filter out results with distance >= maxDistance
bool StylusPickResult::checkOrFilterAgainstMaxDistance(float maxDistance) {
return distance < maxDistance;
}
uint32_t StylusPickResult::deviceId() const {
// 0 is reserved for hardware mouse
return index(tip.side) + 1;
}
StylusPick::StylusPick(Side side)
: Pick(PickFilter(PickScriptingInterface::PICK_OVERLAYS()), FLT_MAX, true)
, _side(side) {
}
StylusTip StylusPick::getMathematicalPick() const {
StylusTip result;
if (_useFingerInsteadOfStylus) {
result = getFingerWorldLocation(_side);
} else {
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
float sensorScaleFactor = myAvatar->getSensorToWorldScale();
result = getControllerWorldLocation(_side, sensorScaleFactor);
result.position += result.orientation * (TIP_OFFSET * sensorScaleFactor);
}
return result;
}
PickResultPointer StylusPick::getDefaultResult(const QVariantMap& pickVariant) const {
return std::make_shared<StylusPickResult>();
}
PickResultPointer StylusPick::getEntityIntersection(const StylusTip& pick) {
return PickResultPointer();
}
PickResultPointer StylusPick::getOverlayIntersection(const StylusTip& pick) {
if (!getFilter().doesPickOverlays()) {
return PickResultPointer();
}
std::vector<StylusPickResult> results;
for (const auto& target : getIncludeItems()) {
if (target.isNull()) {
continue;
}
auto overlay = qApp->getOverlays().getOverlay(target);
// Don't interact with non-3D or invalid overlays
if (!overlay || !overlay->is3D()) {
continue;
}
if (!overlay->getVisible() && !getFilter().doesPickInvisible()) {
continue;
}
auto overlayType = overlay->getType();
auto overlay3D = std::static_pointer_cast<Base3DOverlay>(overlay);
const auto overlayRotation = overlay3D->getRotation();
const auto overlayPosition = overlay3D->getPosition();
StylusPickResult result;
result.tip = pick;
result.overlayID = target;
result.normal = overlayRotation * Vectors::UNIT_Z;
result.distance = glm::dot(pick.position - overlayPosition, result.normal);
result.position = pick.position - (result.normal * result.distance);
if (overlayType == Web3DOverlay::TYPE) {
result.dimensions = vec3(std::static_pointer_cast<Web3DOverlay>(overlay3D)->getSize(), 0.01f);
} else if (overlayType == Sphere3DOverlay::TYPE) {
result.dimensions = std::static_pointer_cast<Sphere3DOverlay>(overlay3D)->getDimensions();
} else {
result.dimensions = vec3(0.01f);
}
auto tipRelativePosition = result.position - overlayPosition;
auto localPos = glm::inverse(overlayRotation) * tipRelativePosition;
auto normalizedPosition = localPos / result.dimensions;
result.normalizedPosition = normalizedPosition + 0.5f;
result.position2D = { result.normalizedPosition.x * result.dimensions.x,
(1.0f - result.normalizedPosition.y) * result.dimensions.y };
result.valid = true;
results.push_back(result);
}
StylusPickResult nearestTarget;
for (const auto& result : results) {
if (result && result.isNormalized() && result.distance < nearestTarget.distance) {
nearestTarget = result;
}
}
return std::make_shared<StylusPickResult>(nearestTarget);
}
PickResultPointer StylusPick::getAvatarIntersection(const StylusTip& pick) {
return PickResultPointer();
}
PickResultPointer StylusPick::getHUDIntersection(const StylusTip& pick) {
return PickResultPointer();
}
StylusPointer::StylusPointer(Side side)
: Pointer(DependencyManager::get<PickManager>()->addPick(PickQuery::Stylus, std::make_shared<StylusPick>(side)),
false,
true)
, _side(side)
, _sideData(SIDES[index(side)]) {
setIncludeItems({ { getHomeButtonID(), getTabletScreenID() } });
STYLUSES[index(_side)] = this;
StylusPointer::StylusPointer(const QVariant& props, const OverlayID& stylusOverlay, bool hover, bool enabled) :
Pointer(DependencyManager::get<PickScriptingInterface>()->createStylusPick(props), enabled, hover),
_stylusOverlay(stylusOverlay)
{
}
StylusPointer::~StylusPointer() {
if (!_stylusOverlay.isNull()) {
qApp->getOverlays().deleteOverlay(_stylusOverlay);
}
STYLUSES[index(_side)] = nullptr;
}
StylusPointer* StylusPointer::getOtherStylus() {
return STYLUSES[((index(_side) + 1) % 2)];
}
void StylusPointer::enable() {
Parent::enable();
withWriteLock([&] { _renderingEnabled = true; });
}
void StylusPointer::disable() {
Parent::disable();
withWriteLock([&] { _renderingEnabled = false; });
}
void StylusPointer::updateStylusTarget() {
const float minNearDistance = TABLET_MIN_TOUCH_DISTANCE * _sensorScaleFactor;
const float maxNearDistance = WEB_DISPLAY_STYLUS_DISTANCE * _sensorScaleFactor;
const float edgeBorder = EDGE_BORDER * _sensorScaleFactor;
auto pickResult = DependencyManager::get<PickManager>()->getPrevPickResultTyped<StylusPickResult>(_pickUID);
if (pickResult) {
_state.target = *pickResult;
float hystersis = 0.0f;
// If we're already near the target, add hystersis to ensure we don't rapidly toggle between near and not near
// but only for the current near target
if (_previousState.nearTarget && pickResult->overlayID == _previousState.target.overlayID) {
hystersis = _nearHysteresis;
}
_state.nearTarget = pickResult->isNear(minNearDistance, maxNearDistance, edgeBorder, hystersis);
}
// Not near anything, short circuit the rest
if (!_state.nearTarget) {
relinquishTouchFocus();
hide();
return;
}
show();
auto minTouchDistance = TABLET_MIN_TOUCH_DISTANCE * _sensorScaleFactor;
auto maxTouchDistance = TABLET_MAX_TOUCH_DISTANCE * _sensorScaleFactor;
auto maxHoverDistance = TABLET_MAX_HOVER_DISTANCE * _sensorScaleFactor;
float hystersis = 0.0f;
if (_previousState.nearTarget && _previousState.target.overlayID == _previousState.target.overlayID) {
hystersis = _nearHysteresis;
}
// If we're in hover distance (calculated as the normal distance from the XY plane of the overlay)
if ((getOtherStylus() && getOtherStylus()->_state.touchingTarget) || !_state.target.isNearNormal(minTouchDistance, maxHoverDistance, hystersis)) {
relinquishTouchFocus();
return;
}
requestTouchFocus(_state.target);
if (!_state.target.hasKeyboardFocus()) {
_state.target.setKeyboardFocus();
}
if (hasTouchFocus(_state.target) && !_previousState.touchingTarget) {
_state.target.sendHoverOverEvent();
}
hystersis = 0.0f;
if (_previousState.touchingTarget && _previousState.target.overlayID == _state.target.overlayID) {
hystersis = _touchHysteresis;
}
// If we're in touch distance
if (_state.target.isNearNormal(minTouchDistance, maxTouchDistance, _touchHysteresis) && _state.target.isNormalized()) {
_state.touchingTarget = true;
}
}
void StylusPointer::update(unsigned int pointerID, float deltaTime) {
// This only needs to be a read lock because update won't change any of the properties that can be modified from scripts
withReadLock([&] {
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
// Store and reset the state
{
_previousState = _state;
_state = State();
}
#if 0
// Update finger as stylus setting
{
useFingerInsteadOfStylus = (USE_FINGER_AS_STYLUS.get() && myAvatar->getJointIndex(sideData.avatarJoint) != -1);
}
#endif
// Update scale factor
{
_sensorScaleFactor = myAvatar->getSensorToWorldScale();
_hoverHysteresis = HOVER_HYSTERESIS * _sensorScaleFactor;
_nearHysteresis = NEAR_HYSTERESIS * _sensorScaleFactor;
_touchHysteresis = TOUCH_HYSTERESIS * _sensorScaleFactor;
}
// Identify the current near or touching target
updateStylusTarget();
// If we stopped touching, or if the target overlay ID changed, send a touching exit to the previous touch target
if (_previousState.touchingTarget &&
(!_state.touchingTarget || _state.target.overlayID != _previousState.target.overlayID)) {
stylusTouchingExit();
}
// Handle new or continuing touch
if (_state.touchingTarget) {
// If we were previously not touching, or we were touching a different overlay, add a touch enter
if (!_previousState.touchingTarget || _previousState.target.overlayID != _state.target.overlayID) {
stylusTouchingEnter();
} else {
_touchingEnterTimer += deltaTime;
}
stylusTouching();
}
});
setIncludeItems({ { getHomeButtonID(), getTabletScreenID() } });
}
void StylusPointer::show() {
if (!_stylusOverlay.isNull()) {
return;
}
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
// FIXME perhaps instantiate a stylus and use show / hide instead of create / destroy
// however, the current design doesn't really allow for this because it assumes that
// hide / show are idempotent and low cost, but constantly querying the visibility
OverlayID StylusPointer::buildStylusOverlay(const QVariantMap& properties) {
QVariantMap overlayProperties;
// TODO: make these configurable per pointer
overlayProperties["name"] = "stylus";
overlayProperties["url"] = PathUtils::resourcesPath() + "/meshes/tablet-stylus-fat.fbx";
overlayProperties["loadPriority"] = 10.0f;
overlayProperties["dimensions"] = vec3toVariant(_sensorScaleFactor * vec3(0.01f, 0.01f, WEB_STYLUS_LENGTH));
overlayProperties["solid"] = true;
overlayProperties["visible"] = true;
overlayProperties["visible"] = false;
overlayProperties["ignoreRayIntersection"] = true;
overlayProperties["drawInFront"] = false;
overlayProperties["parentID"] = AVATAR_SELF_ID;
overlayProperties["parentJointIndex"] = myAvatar->getJointIndex(_sideData.cameraJoint);
static const glm::quat X_ROT_NEG_90{ 0.70710678f, -0.70710678f, 0.0f, 0.0f };
auto modelOrientation = _state.target.tip.orientation * X_ROT_NEG_90;
auto modelPositionOffset = modelOrientation * (vec3(0.0f, 0.0f, -WEB_STYLUS_LENGTH / 2.0f) * _sensorScaleFactor);
overlayProperties["position"] = vec3toVariant(_state.target.tip.position + modelPositionOffset);
overlayProperties["rotation"] = quatToVariant(modelOrientation);
_stylusOverlay = qApp->getOverlays().addOverlay("model", overlayProperties);
return qApp->getOverlays().addOverlay("model", overlayProperties);
}
void StylusPointer::updateVisuals(const PickResultPointer& pickResult) {
auto stylusPickResult = std::static_pointer_cast<const StylusPickResult>(pickResult);
if (_enabled && _renderState != DISABLED && stylusPickResult) {
StylusTip tip(stylusPickResult->pickVariant);
if (tip.side != bilateral::Side::Invalid) {
show(tip);
return;
}
}
hide();
}
void StylusPointer::show(const StylusTip& tip) {
if (!_stylusOverlay.isNull()) {
QVariantMap props;
static const glm::quat X_ROT_NEG_90{ 0.70710678f, -0.70710678f, 0.0f, 0.0f };
auto modelOrientation = tip.orientation * X_ROT_NEG_90;
auto sensorToWorldScale = DependencyManager::get<AvatarManager>()->getMyAvatar()->getSensorToWorldScale();
auto modelPositionOffset = modelOrientation * (vec3(0.0f, 0.0f, -WEB_STYLUS_LENGTH / 2.0f) * sensorToWorldScale);
props["position"] = vec3toVariant(tip.position + modelPositionOffset);
props["rotation"] = quatToVariant(modelOrientation);
props["dimensions"] = vec3toVariant(sensorToWorldScale * vec3(0.01f, 0.01f, WEB_STYLUS_LENGTH));
props["visible"] = true;
qApp->getOverlays().editOverlay(_stylusOverlay, props);
}
}
void StylusPointer::hide() {
if (_stylusOverlay.isNull()) {
return;
if (!_stylusOverlay.isNull()) {
QVariantMap props;
props.insert("visible", false);
qApp->getOverlays().editOverlay(_stylusOverlay, props);
}
}
bool StylusPointer::shouldHover(const PickResultPointer& pickResult) {
auto stylusPickResult = std::static_pointer_cast<const StylusPickResult>(pickResult);
if (_renderState == EVENTS_ON && stylusPickResult && stylusPickResult->intersects) {
auto sensorScaleFactor = DependencyManager::get<AvatarManager>()->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;
}
qApp->getOverlays().deleteOverlay(_stylusOverlay);
_stylusOverlay = OverlayID();
_state.hovering = false;
return false;
}
#if 0
void pointFinger(bool value) {
static const QString HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index";
static const std::array<QString, 2> KEYS{ { "pointLeftIndex", "pointLeftIndex" } };
if (fingerPointing != value) {
QString message = QJsonDocument(QJsonObject{ { KEYS[index(side)], value } }).toJson();
DependencyManager::get<MessagesClient>()->sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, message);
fingerPointing = value;
bool StylusPointer::shouldTrigger(const PickResultPointer& pickResult) {
auto stylusPickResult = std::static_pointer_cast<const StylusPickResult>(pickResult);
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
if (!_state.triggeredObject.objectID.isNull() && stylusPickResult->objectID != _state.triggeredObject.objectID) {
glm::vec3 intersection;
glm::vec3 origin = vec3FromVariant(stylusPickResult->pickVariant["position"]);
glm::vec3 direction = -_state.surfaceNormal;
if (_state.triggeredObject.type == ENTITY) {
intersection = RayPick::intersectRayWithEntityXYPlane(_state.triggeredObject.objectID, origin, direction);
} else if (_state.triggeredObject.type == OVERLAY) {
intersection = RayPick::intersectRayWithOverlayXYPlane(_state.triggeredObject.objectID, origin, direction);
}
distance = glm::dot(intersection - 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()) {
_state.triggeredObject = PickedObject(stylusPickResult->objectID, stylusPickResult->type);
_state.surfaceNormal = stylusPickResult->surfaceNormal;
_state.triggering = true;
}
return true;
}
#endif
void StylusPointer::requestTouchFocus(const StylusPickResult& pickResult) {
if (!pickResult) {
return;
}
// send hover events to target if we can.
// record the entity or overlay we are hovering over.
if (!pickResult.overlayID.isNull() && pickResult.overlayID != _hoverOverlay &&
getOtherStylus() && pickResult.overlayID != getOtherStylus()->_hoverOverlay) {
_hoverOverlay = pickResult.overlayID;
pickResult.sendHoverEnterEvent();
}
_state.triggeredObject = PickedObject();
_state.surfaceNormal = glm::vec3(NAN);
_state.triggering = false;
return false;
}
bool StylusPointer::hasTouchFocus(const StylusPickResult& pickResult) {
return (!pickResult.overlayID.isNull() && pickResult.overlayID == _hoverOverlay);
Pointer::PickedObject StylusPointer::getHoveredObject(const PickResultPointer& pickResult) {
auto stylusPickResult = std::static_pointer_cast<const StylusPickResult>(pickResult);
if (!stylusPickResult) {
return PickedObject();
}
return PickedObject(stylusPickResult->objectID, stylusPickResult->type);
}
void StylusPointer::relinquishTouchFocus() {
// send hover leave event.
if (!_hoverOverlay.isNull()) {
PointerEvent pointerEvent{ PointerEvent::Move, (uint32_t)(index(_side) + 1) };
auto& overlays = qApp->getOverlays();
overlays.sendMouseMoveOnOverlay(_hoverOverlay, pointerEvent);
overlays.sendHoverOverOverlay(_hoverOverlay, pointerEvent);
overlays.sendHoverLeaveOverlay(_hoverOverlay, pointerEvent);
_hoverOverlay = OverlayID();
}
};
void StylusPointer::stealTouchFocus() {
// send hover events to target
if (getOtherStylus() && _state.target.overlayID == getOtherStylus()->_hoverOverlay) {
getOtherStylus()->relinquishTouchFocus();
}
requestTouchFocus(_state.target);
Pointer::Buttons StylusPointer::getPressedButtons() {
// TODO: custom buttons for styluses
Pointer::Buttons toReturn({ "Primary", "Focus" });
return toReturn;
}
void StylusPointer::stylusTouchingEnter() {
stealTouchFocus();
_state.target.sendTouchStartEvent();
DependencyManager::get<UserInputMapper>()->triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION,
_sideData.hand);
_touchingEnterTimer = 0;
_touchingEnterPosition = _state.target.position2D;
_deadspotExpired = false;
PointerEvent StylusPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult) const {
QUuid pickedID;
glm::vec3 intersection, surfaceNormal, direction, origin;
auto stylusPickResult = std::static_pointer_cast<StylusPickResult>(pickResult);
if (stylusPickResult) {
intersection = stylusPickResult->intersection;
surfaceNormal = _state.surfaceNormal;
const QVariantMap& stylusTip = stylusPickResult->pickVariant;
origin = vec3FromVariant(stylusTip["position"]);
direction = -_state.surfaceNormal;
pickedID = stylusPickResult->objectID;
}
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);
}
}
glm::vec2 pos2D;
if (target.type == ENTITY) {
pos2D = RayPick::projectOntoEntityXYPlane(target.objectID, origin);
} else if (target.type == OVERLAY) {
pos2D = RayPick::projectOntoOverlayXYPlane(target.objectID, origin);
} else if (target.type == HUD) {
pos2D = DependencyManager::get<PickManager>()->calculatePos2DFromHUD(origin);
}
return PointerEvent(pos2D, intersection, surfaceNormal, direction);
}
void StylusPointer::stylusTouchingExit() {
if (!_previousState.target) {
return;
}
// special case to handle home button.
if (_previousState.target.overlayID == getHomeButtonID()) {
DependencyManager::get<MessagesClient>()->sendLocalMessage("home", _previousState.target.overlayID.toString());
}
// send touch end event
_state.target.sendTouchEndEvent();
bool StylusPointer::isWithinBounds(float distance, float min, float max, float hysteresis) {
return (distance == glm::clamp(distance, min - hysteresis, max + hysteresis));
}
void StylusPointer::stylusTouching() {
qDebug() << "QQQ " << __FUNCTION__;
if (_state.target.overlayID.isNull()) {
return;
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;
}
if (!_deadspotExpired) {
_deadspotExpired =
(_touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY) ||
glm::distance2(_state.target.position2D, _touchingEnterPosition) > TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED;
}
// Only send moves if the target moves more than the deadspot position or if we've timed out the deadspot
if (_deadspotExpired) {
_state.target.sendTouchMoveEvent();
}
}
}

View file

@ -8,136 +8,65 @@
#ifndef hifi_StylusPointer_h
#define hifi_StylusPointer_h
#include <QString>
#include <glm/glm.hpp>
#include <pointers/Pointer.h>
#include <pointers/Pick.h>
#include <shared/Bilateral.h>
#include <RegisteredMetaTypes.h>
#include <pointers/Pick.h>
#include "ui/overlays/Overlay.h"
class StylusPick : public Pick<StylusTip> {
using Side = bilateral::Side;
public:
StylusPick(Side side);
StylusTip getMathematicalPick() const override;
PickResultPointer getDefaultResult(const QVariantMap& pickVariant) const override;
PickResultPointer getEntityIntersection(const StylusTip& pick) override;
PickResultPointer getOverlayIntersection(const StylusTip& pick) override;
PickResultPointer getAvatarIntersection(const StylusTip& pick) override;
PickResultPointer getHUDIntersection(const StylusTip& pick) override;
private:
const Side _side;
const bool _useFingerInsteadOfStylus{ false };
};
struct SideData;
struct StylusPickResult : public PickResult {
using Side = bilateral::Side;
// FIXME make into a single ID
OverlayID overlayID;
// FIXME restore entity functionality
#if 0
EntityItemID entityID;
#endif
StylusTip tip;
float distance{ FLT_MAX };
vec3 position;
vec2 position2D;
vec3 normal;
vec3 normalizedPosition;
vec3 dimensions;
bool valid{ false };
virtual bool doesIntersect() const override;
virtual std::shared_ptr<PickResult> compareAndProcessNewResult(const std::shared_ptr<PickResult>& newRes) override;
virtual bool checkOrFilterAgainstMaxDistance(float maxDistance) override;
bool isNormalized() const;
bool isNearNormal(float min, float max, float hystersis) const;
bool isNear2D(float border, float hystersis) const;
bool isNear(float min, float max, float border, float hystersis) const;
operator bool() const;
bool hasKeyboardFocus() const;
void setKeyboardFocus() const;
void sendHoverOverEvent() const;
void sendHoverEnterEvent() const;
void sendTouchStartEvent() const;
void sendTouchEndEvent() const;
void sendTouchMoveEvent() const;
private:
uint32_t deviceId() const;
};
#include "StylusPick.h"
class StylusPointer : public Pointer {
using Parent = Pointer;
using Side = bilateral::Side;
using Ptr = std::shared_ptr<StylusPointer>;
public:
StylusPointer(Side side);
StylusPointer(const QVariant& props, const OverlayID& stylusOverlay, bool hover, bool enabled);
~StylusPointer();
void enable() override;
void disable() override;
void update(unsigned int pointerID, float deltaTime) override;
void updateVisuals(const PickResultPointer& pickResult) override;
// Styluses have three render states:
// default: "events on" -> render and hover/trigger
// "events off" -> render, don't hover/trigger
// "disabled" -> don't render, don't hover/trigger
void setRenderState(const std::string& state) override;
void editRenderState(const std::string& state, const QVariant& startProps, const QVariant& pathProps, const QVariant& endProps) override {}
static OverlayID buildStylusOverlay(const QVariantMap& properties);
protected:
PickedObject getHoveredObject(const PickResultPointer& pickResult) override;
Buttons getPressedButtons() override;
bool shouldHover(const PickResultPointer& pickResult) override;
bool shouldTrigger(const PickResultPointer& pickResult) override;
PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult) const override;
private:
virtual void setRenderState(const std::string& state) override {}
virtual void editRenderState(const std::string& state, const QVariant& startProps, const QVariant& pathProps, const QVariant& endProps) override {}
virtual PickedObject getHoveredObject(const PickResultPointer& pickResult) override { return PickedObject(); }
virtual Buttons getPressedButtons() override { return {}; }
bool shouldHover() override { return true; }
bool shouldTrigger() override { return true; }
virtual PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult) const override { return PointerEvent(); }
StylusPointer* getOtherStylus();
void updateStylusTarget();
void requestTouchFocus(const StylusPickResult& pickResult);
bool hasTouchFocus(const StylusPickResult& pickResult);
void relinquishTouchFocus();
void stealTouchFocus();
void stylusTouchingEnter();
void stylusTouchingExit();
void stylusTouching();
void show();
void show(const StylusTip& tip);
void hide();
struct State {
StylusPickResult target;
bool nearTarget{ false };
bool touchingTarget{ false };
struct TriggerState {
PickedObject triggeredObject;
glm::vec3 surfaceNormal { NAN };
bool hovering { false };
bool triggering { false };
};
State _state;
State _previousState;
TriggerState _state;
float _nearHysteresis{ 0.0f };
float _touchHysteresis{ 0.0f };
float _hoverHysteresis{ 0.0f };
enum RenderState {
EVENTS_ON = 0,
EVENTS_OFF,
DISABLED
};
float _sensorScaleFactor{ 1.0f };
float _touchingEnterTimer{ 0 };
vec2 _touchingEnterPosition;
bool _deadspotExpired{ false };
RenderState _renderState { EVENTS_ON };
bool _renderingEnabled;
OverlayID _stylusOverlay;
OverlayID _hoverOverlay;
const Side _side;
const SideData& _sideData;
const OverlayID _stylusOverlay;
static bool isWithinBounds(float distance, float min, float max, float hysteresis);
};
#endif // hifi_StylusPointer_h

View file

@ -78,7 +78,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin
Buttons sameButtons;
const std::string PRIMARY_BUTTON = "Primary";
bool primaryPressed = false;
if (_enabled && shouldTrigger()) {
if (_enabled && shouldTrigger(pickResult)) {
buttons = getPressedButtons();
primaryPressed = buttons.find(PRIMARY_BUTTON) != buttons.end();
for (const std::string& button : buttons) {
@ -92,7 +92,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin
}
// Hover events
bool doHover = shouldHover();
bool doHover = shouldHover(pickResult);
Pointer::PickedObject hoveredObject = getHoveredObject(pickResult);
PointerEvent hoveredEvent = buildPointerEvent(hoveredObject, pickResult);
hoveredEvent.setType(PointerEvent::Move);

View file

@ -62,8 +62,8 @@ public:
virtual void setLength(float length) {}
virtual void setLockEndUUID(const QUuid& objectID, bool isOverlay) {}
virtual void update(unsigned int pointerID, float deltaTime);
virtual void updateVisuals(const PickResultPointer& pickResult) {}
void update(unsigned int pointerID, float deltaTime);
virtual void updateVisuals(const PickResultPointer& pickResult) = 0;
void generatePointerEvents(unsigned int pointerID, const PickResultPointer& pickResult);
struct PickedObject {
@ -87,8 +87,8 @@ protected:
virtual PickedObject getHoveredObject(const PickResultPointer& pickResult) = 0;
virtual Buttons getPressedButtons() = 0;
virtual bool shouldHover() = 0;
virtual bool shouldTrigger() = 0;
virtual bool shouldHover(const PickResultPointer& pickResult) { return true; }
virtual bool shouldTrigger(const PickResultPointer& pickResult) { return true; }
private:
PickedObject _prevHoveredObject;

View file

@ -154,18 +154,30 @@ public:
}
};
struct StylusTip : public MathPick {
bilateral::Side side{ bilateral::Side::Invalid };
class StylusTip : public MathPick {
public:
StylusTip() : position(NAN), velocity(NAN) {}
StylusTip(const QVariantMap& pickVariant) : side(bilateral::Side(pickVariant["side"].toInt())), position(vec3FromVariant(pickVariant["position"])),
orientation(quatFromVariant(pickVariant["orientation"])), velocity(vec3FromVariant(pickVariant["velocity"])) {}
bilateral::Side side { bilateral::Side::Invalid };
glm::vec3 position;
glm::quat orientation;
glm::vec3 velocity;
virtual operator bool() const override { return side != bilateral::Side::Invalid; }
operator bool() const override { return side != bilateral::Side::Invalid; }
bool operator==(const StylusTip& other) const {
return (side == other.side && position == other.position && orientation == other.orientation && velocity == other.velocity);
}
QVariantMap toVariantMap() const override {
QVariantMap pickRay;
pickRay["position"] = vec3toVariant(position);
pickRay["orientation"] = quatToVariant(orientation);
pickRay["velocity"] = vec3toVariant(velocity);
return pickRay;
QVariantMap stylusTip;
stylusTip["side"] = (int)side;
stylusTip["position"] = vec3toVariant(position);
stylusTip["orientation"] = quatToVariant(orientation);
stylusTip["velocity"] = vec3toVariant(velocity);
return stylusTip;
}
};

View file

@ -128,16 +128,16 @@ Script.include("/~/system/libraries/utils.js");
LaserPointers.enableLaserPointer(this.laserPointer);
LaserPointers.setRenderState(this.laserPointer, this.mode);
if (HMD.tabletID !== this.tabletID || HMD.tabletButtonID !== this.tabletButtonID || HMD.tabletScreenID !== this.tabletScreenID) {
if (HMD.tabletID !== this.tabletID || HMD.homeButtonID !== this.homeButtonID || HMD.tabletScreenID !== this.tabletScreenID) {
this.tabletID = HMD.tabletID;
this.tabletButtonID = HMD.tabletButtonID;
this.homeButtonID = HMD.homeButtonID;
this.tabletScreenID = HMD.tabletScreenID;
LaserPointers.setIgnoreItems(this.laserPointer, [HMD.tabletID, HMD.tabletButtonID, HMD.tabletScreenID]);
LaserPointers.setIgnoreItems(this.laserPointer, [HMD.tabletID, HMD.homeButtonID, HMD.tabletScreenID]);
}
};
this.pointingAtTablet = function(objectID) {
if (objectID === HMD.tabletScreenID || objectID === HMD.tabletButtonID) {
if (objectID === HMD.tabletScreenID || objectID === HMD.homeButtonID) {
return true;
}
return false;
@ -240,7 +240,7 @@ Script.include("/~/system/libraries/utils.js");
defaultRenderStates: defaultRenderStates
});
LaserPointers.setIgnoreItems(this.laserPointer, [HMD.tabletID, HMD.tabletButtonID, HMD.tabletScreenID]);
LaserPointers.setIgnoreItems(this.laserPointer, [HMD.tabletID, HMD.homeButtonID, HMD.tabletScreenID]);
}
var leftHandInEditMode = new InEditMode(LEFT_HAND);