// // Created by Sam Gondelman 10/20/2017 // Copyright 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 "PickScriptingInterface.h" #include #include "GLMHelpers.h" #include "Application.h" #include #include "StaticRayPick.h" #include "JointRayPick.h" #include "MouseRayPick.h" #include "StylusPick.h" #include "StaticParabolaPick.h" #include "JointParabolaPick.h" #include "MouseParabolaPick.h" #include "CollisionPick.h" #include "SpatialParentFinder.h" #include "PickTransformNode.h" #include "MouseTransformNode.h" #include "avatar/MyAvatarHeadTransformNode.h" #include "avatar/AvatarManager.h" #include "NestableTransformNode.h" #include "avatars-renderer/AvatarTransformNode.h" #include "ui/overlays/OverlayTransformNode.h" #include "EntityTransformNode.h" #include unsigned int PickScriptingInterface::createPick(const PickQuery::PickType type, const QVariant& properties) { switch (type) { case PickQuery::PickType::Ray: return createRayPick(properties); case PickQuery::PickType::Stylus: return createStylusPick(properties); case PickQuery::PickType::Parabola: return createParabolaPick(properties); case PickQuery::PickType::Collision: return createCollisionPick(properties); default: return PickManager::INVALID_PICK_ID; } } /**jsdoc * A set of properties that can be passed to {@link Picks.createPick} to create a new Ray Pick. * @typedef {object} Picks.RayPickProperties * @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results. * @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR. * @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid. * @property {string} [joint] Only for Joint or Mouse Ray Picks. If "Mouse", it will create a Ray Pick that follows the system mouse, in desktop or HMD. * If "Avatar", it will create a Joint Ray Pick that follows your avatar's head. Otherwise, it will create a Joint Ray Pick that follows the given joint, if it * exists on your current avatar. * @property {Vec3} [posOffset=Vec3.ZERO] Only for Joint Ray Picks. A local joint position offset, in meters. x = upward, y = forward, z = lateral * @property {Vec3} [dirOffset=Vec3.UP] Only for Joint Ray Picks. A local joint direction offset. x = upward, y = forward, z = lateral * @property {Vec3} [position] Only for Static Ray Picks. The world-space origin of the ray. * @property {Vec3} [direction=-Vec3.UP] Only for Static Ray Picks. The world-space direction of the ray. */ unsigned int PickScriptingInterface::createRayPick(const QVariant& properties) { QVariantMap propMap = properties.toMap(); bool enabled = false; if (propMap["enabled"].isValid()) { enabled = propMap["enabled"].toBool(); } PickFilter filter = PickFilter(); if (propMap["filter"].isValid()) { filter = PickFilter(propMap["filter"].toUInt()); } float maxDistance = 0.0f; if (propMap["maxDistance"].isValid()) { maxDistance = propMap["maxDistance"].toFloat(); } if (propMap["joint"].isValid()) { std::string jointName = propMap["joint"].toString().toStdString(); if (jointName != "Mouse") { // x = upward, y = forward, z = lateral glm::vec3 posOffset = Vectors::ZERO; if (propMap["posOffset"].isValid()) { posOffset = vec3FromVariant(propMap["posOffset"]); } glm::vec3 dirOffset = Vectors::UP; if (propMap["dirOffset"].isValid()) { dirOffset = vec3FromVariant(propMap["dirOffset"]); } return DependencyManager::get()->addPick(PickQuery::Ray, std::make_shared(jointName, posOffset, dirOffset, filter, maxDistance, enabled)); } else { return DependencyManager::get()->addPick(PickQuery::Ray, std::make_shared(filter, maxDistance, enabled)); } } else if (propMap["position"].isValid()) { glm::vec3 position = vec3FromVariant(propMap["position"]); glm::vec3 direction = -Vectors::UP; if (propMap["direction"].isValid()) { direction = vec3FromVariant(propMap["direction"]); } return DependencyManager::get()->addPick(PickQuery::Ray, std::make_shared(position, direction, filter, maxDistance, enabled)); } return PickManager::INVALID_PICK_ID; } /**jsdoc * A set of properties that can be passed to {@link Picks.createPick} to create a new Stylus Pick. * @typedef {object} Picks.StylusPickProperties * @property {number} [hand=-1] An integer. 0 == left, 1 == right. Invalid otherwise. * @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results. * @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR. * @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid. */ 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()); } float maxDistance = 0.0f; if (propMap["maxDistance"].isValid()) { maxDistance = propMap["maxDistance"].toFloat(); } return DependencyManager::get()->addPick(PickQuery::Stylus, std::make_shared(side, filter, maxDistance, enabled)); } /**jsdoc * A set of properties that can be passed to {@link Picks.createPick} to create a new Parabola Pick. * @typedef {object} Picks.ParabolaPickProperties * @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results. * @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR. * @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid. * @property {string} [joint] Only for Joint or Mouse Parabola Picks. If "Mouse", it will create a Parabola Pick that follows the system mouse, in desktop or HMD. * If "Avatar", it will create a Joint Parabola Pick that follows your avatar's head. Otherwise, it will create a Joint Parabola Pick that follows the given joint, if it * exists on your current avatar. * @property {Vec3} [posOffset=Vec3.ZERO] Only for Joint Parabola Picks. A local joint position offset, in meters. x = upward, y = forward, z = lateral * @property {Vec3} [dirOffset=Vec3.UP] Only for Joint Parabola Picks. A local joint direction offset. x = upward, y = forward, z = lateral * @property {Vec3} [position] Only for Static Parabola Picks. The world-space origin of the parabola segment. * @property {Vec3} [direction=-Vec3.FRONT] Only for Static Parabola Picks. The world-space direction of the parabola segment. * @property {number} [speed=1] The initial speed of the parabola, i.e. the initial speed of the projectile whose trajectory defines the parabola. * @property {Vec3} [accelerationAxis=-Vec3.UP] The acceleration of the parabola, i.e. the acceleration of the projectile whose trajectory defines the parabola, both magnitude and direction. * @property {boolean} [rotateAccelerationWithAvatar=true] Whether or not the acceleration axis should rotate with your avatar's local Y axis. * @property {boolean} [scaleWithAvatar=false] If true, the velocity and acceleration of the Pick will scale linearly with your avatar. */ unsigned int PickScriptingInterface::createParabolaPick(const QVariant& properties) { QVariantMap propMap = properties.toMap(); bool enabled = false; if (propMap["enabled"].isValid()) { enabled = propMap["enabled"].toBool(); } PickFilter filter = PickFilter(); if (propMap["filter"].isValid()) { filter = PickFilter(propMap["filter"].toUInt()); } float maxDistance = 0.0f; if (propMap["maxDistance"].isValid()) { maxDistance = propMap["maxDistance"].toFloat(); } float speed = 1.0f; if (propMap["speed"].isValid()) { speed = propMap["speed"].toFloat(); } glm::vec3 accelerationAxis = -Vectors::UP; if (propMap["accelerationAxis"].isValid()) { accelerationAxis = vec3FromVariant(propMap["accelerationAxis"]); } bool rotateAccelerationWithAvatar = true; if (propMap["rotateAccelerationWithAvatar"].isValid()) { rotateAccelerationWithAvatar = propMap["rotateAccelerationWithAvatar"].toBool(); } bool scaleWithAvatar = false; if (propMap["scaleWithAvatar"].isValid()) { scaleWithAvatar = propMap["scaleWithAvatar"].toBool(); } if (propMap["joint"].isValid()) { std::string jointName = propMap["joint"].toString().toStdString(); if (jointName != "Mouse") { // x = upward, y = forward, z = lateral glm::vec3 posOffset = Vectors::ZERO; if (propMap["posOffset"].isValid()) { posOffset = vec3FromVariant(propMap["posOffset"]); } glm::vec3 dirOffset = Vectors::UP; if (propMap["dirOffset"].isValid()) { dirOffset = vec3FromVariant(propMap["dirOffset"]); } return DependencyManager::get()->addPick(PickQuery::Parabola, std::make_shared(jointName, posOffset, dirOffset, speed, accelerationAxis, rotateAccelerationWithAvatar, scaleWithAvatar, filter, maxDistance, enabled)); } else { return DependencyManager::get()->addPick(PickQuery::Parabola, std::make_shared(speed, accelerationAxis, rotateAccelerationWithAvatar, scaleWithAvatar, filter, maxDistance, enabled)); } } else if (propMap["position"].isValid()) { glm::vec3 position = vec3FromVariant(propMap["position"]); glm::vec3 direction = -Vectors::FRONT; if (propMap["direction"].isValid()) { direction = vec3FromVariant(propMap["direction"]); } return DependencyManager::get()->addPick(PickQuery::Parabola, std::make_shared(position, direction, speed, accelerationAxis, rotateAccelerationWithAvatar, scaleWithAvatar, filter, maxDistance, enabled)); } return PickManager::INVALID_PICK_ID; } /**jsdoc * A Shape defines a physical volume. * * @typedef {object} Shape * @property {string} shapeType The type of shape to use. Can be one of the following: "box", "sphere", "capsule-x", "capsule-y", "capsule-z", "cylinder-x", "cylinder-y", "cylinder-z" * @property {Vec3} dimensions - The size to scale the shape to. */ // TODO: Add this property to the Shape jsdoc above once model picks work properly // * @property {string} modelURL - If shapeType is one of: "compound", "simple-hull", "simple-compound", or "static-mesh", this defines the model to load to generate the collision volume. /**jsdoc * A set of properties that can be passed to {@link Picks.createPick} to create a new Collision Pick. * @typedef {object} Picks.CollisionPickProperties * @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results. * @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR. * @property {Shape} shape - The information about the collision region's size and shape. Dimensions are in world space, but will scale with the parent if defined. * @property {Vec3} position - The position of the collision region, relative to a parent if defined. * @property {Quat} orientation - The orientation of the collision region, relative to a parent if defined. * @property {float} threshold - The approximate minimum penetration depth for a test object to be considered in contact with the collision region. * The depth is measured in world space, but will scale with the parent if defined. * @property {Uuid} parentID - The ID of the parent, either an avatar, an entity, or an overlay. * @property {number} parentJointIndex - The joint of the parent to parent to, for example, the joints on the model of an avatar. (default = 0, no joint) * @property {string} joint - If "Mouse," parents the pick to the mouse. If "Avatar," parents the pick to MyAvatar's head. Otherwise, parents to the joint of the given name on MyAvatar. */ unsigned int PickScriptingInterface::createCollisionPick(const QVariant& properties) { QVariantMap propMap = properties.toMap(); bool enabled = false; if (propMap["enabled"].isValid()) { enabled = propMap["enabled"].toBool(); } PickFilter filter = PickFilter(); if (propMap["filter"].isValid()) { filter = PickFilter(propMap["filter"].toUInt()); } float maxDistance = 0.0f; if (propMap["maxDistance"].isValid()) { maxDistance = propMap["maxDistance"].toFloat(); } CollisionRegion collisionRegion(propMap); auto collisionPick = std::make_shared(filter, maxDistance, enabled, collisionRegion, qApp->getPhysicsEngine()); collisionPick->parentTransform = createTransformNode(propMap); return DependencyManager::get()->addPick(PickQuery::Collision, collisionPick); } void PickScriptingInterface::enablePick(unsigned int uid) { DependencyManager::get()->enablePick(uid); } void PickScriptingInterface::disablePick(unsigned int uid) { DependencyManager::get()->disablePick(uid); } void PickScriptingInterface::removePick(unsigned int uid) { DependencyManager::get()->removePick(uid); } QVariantMap PickScriptingInterface::getPrevPickResult(unsigned int uid) { QVariantMap result; auto pickResult = DependencyManager::get()->getPrevPickResult(uid); if (pickResult) { result = pickResult->toVariantMap(); } return result; } void PickScriptingInterface::setPrecisionPicking(unsigned int uid, bool precisionPicking) { DependencyManager::get()->setPrecisionPicking(uid, precisionPicking); } void PickScriptingInterface::setIgnoreItems(unsigned int uid, const QScriptValue& ignoreItems) { DependencyManager::get()->setIgnoreItems(uid, qVectorQUuidFromScriptValue(ignoreItems)); } void PickScriptingInterface::setIncludeItems(unsigned int uid, const QScriptValue& includeItems) { DependencyManager::get()->setIncludeItems(uid, qVectorQUuidFromScriptValue(includeItems)); } bool PickScriptingInterface::isLeftHand(unsigned int uid) { return DependencyManager::get()->isLeftHand(uid); } bool PickScriptingInterface::isRightHand(unsigned int uid) { return DependencyManager::get()->isRightHand(uid); } bool PickScriptingInterface::isMouse(unsigned int uid) { return DependencyManager::get()->isMouse(uid); } QScriptValue pickTypesToScriptValue(QScriptEngine* engine, const PickQuery::PickType& pickType) { return pickType; } void pickTypesFromScriptValue(const QScriptValue& object, PickQuery::PickType& pickType) { pickType = static_cast(object.toUInt16()); } void PickScriptingInterface::registerMetaTypes(QScriptEngine* engine) { QScriptValue pickTypes = engine->newObject(); auto metaEnum = QMetaEnum::fromType(); for (int i = 0; i < PickQuery::PickType::NUM_PICK_TYPES; ++i) { pickTypes.setProperty(metaEnum.key(i), metaEnum.value(i)); } engine->globalObject().setProperty("PickType", pickTypes); qScriptRegisterMetaType(engine, pickTypesToScriptValue, pickTypesFromScriptValue); } unsigned int PickScriptingInterface::getPerFrameTimeBudget() const { return DependencyManager::get()->getPerFrameTimeBudget(); } void PickScriptingInterface::setPerFrameTimeBudget(unsigned int numUsecs) { DependencyManager::get()->setPerFrameTimeBudget(numUsecs); } std::shared_ptr PickScriptingInterface::createTransformNode(const QVariantMap& propMap) { if (propMap["parentID"].isValid()) { QUuid parentUuid = propMap["parentID"].toUuid(); if (!parentUuid.isNull()) { // Infer object type from parentID // For now, assume a QUuuid is a SpatiallyNestable. This should change when picks are converted over to QUuids. bool success; std::weak_ptr nestablePointer = DependencyManager::get()->find(parentUuid, success, nullptr); int parentJointIndex = 0; if (propMap["parentJointIndex"].isValid()) { parentJointIndex = propMap["parentJointIndex"].toInt(); } auto sharedNestablePointer = nestablePointer.lock(); if (success && sharedNestablePointer) { NestableType nestableType = sharedNestablePointer->getNestableType(); if (nestableType == NestableType::Avatar) { return std::make_shared(std::static_pointer_cast(sharedNestablePointer), parentJointIndex); } else if (nestableType == NestableType::Overlay) { return std::make_shared(std::static_pointer_cast(sharedNestablePointer), parentJointIndex); } else if (nestableType == NestableType::Entity) { return std::make_shared(std::static_pointer_cast(sharedNestablePointer), parentJointIndex); } else { return std::make_shared(nestablePointer, parentJointIndex); } } } unsigned int pickID = propMap["parentID"].toUInt(); if (pickID != 0) { return std::make_shared(pickID); } } if (propMap["joint"].isValid()) { QString joint = propMap["joint"].toString(); if (joint == "Mouse") { return std::make_shared(); } else if (joint == "Avatar") { return std::make_shared(); } else if (!joint.isNull()) { auto myAvatar = DependencyManager::get()->getMyAvatar(); int jointIndex = myAvatar->getJointIndex(joint); return std::make_shared(myAvatar, jointIndex); } } return std::shared_ptr(); }