// // Created by Sam Gondelman 10/20/2017 // Copyright 2017 High Fidelity, Inc. // Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // SPDX-License-Identifier: Apache-2.0 // #ifndef hifi_PointerScriptingInterface_h #define hifi_PointerScriptingInterface_h #include #include "DependencyManager.h" #include "RegisteredMetaTypes.h" #include #include #include class ScriptValue; /*@jsdoc * The Pointers API lets you create, manage, and visually represent objects for repeatedly calculating * intersections with avatars, entities, and overlays. Pointers can also be configured to generate events on entities and * overlays intersected. * * @namespace Pointers * * @hifi-interface * @hifi-client-entity * @hifi-avatar */ class PointerProperties { public: QVariantMap properties; QList entityProperties; }; class RayPointerProperties : public PointerProperties { }; class ParabolaPointerProperties : public PointerProperties { }; class StylusPointerProperties : public PointerProperties { }; Q_DECLARE_METATYPE(RayPointerProperties); Q_DECLARE_METATYPE(StylusPointerProperties); Q_DECLARE_METATYPE(ParabolaPointerProperties); Q_DECLARE_METATYPE(PointerProperties); class PointerScriptingInterface : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY public: // The purpose of registering different classes is to let the script engine know what data structure it has to // expect in JS object that will be converted to PointerProperties /*@jsdoc * Specifies that a {@link Controller} action or function should trigger events on the entity or overlay currently * intersected by a {@link Pointers.RayPointerProperties|Ray} or {@link Pointers.ParabolaPointerProperties|Parabola} * pointer. * @typedef {object} Pointers.Trigger * @property {Controller.Standard|Controller.Actions|function} action - The controller output or function that triggers the * events on the entity or overlay. If a function, it must return a number >= 1.0 to start the action * and < 1.0 to terminate the action. * @property {string} button - Which button to trigger: *
    *
  • "Primary", "Secondary", and "Tertiary" cause {@link Entities} and * {@link Overlays} mouse pointer events. Other button names also cause mouse events but the button * property in the event will be "None".
  • *
  • "Focus" will try to give focus to the entity or overlay which the pointer is intersecting.
  • *
*/ /*@jsdoc * Creates a new ray, parabola, or stylus pointer. The pointer can have a wide range of behaviors depending on the * properties specified. For example, a ray pointer may be a static ray pointer, a mouse ray pointer, or joint ray * pointer. *

Warning: Pointers created using this method currently always intersect at least visible and * collidable things but this may not always be the case.

* @function Pointers.createPointer * @param {PickType} type - The type of pointer to create. Cannot be {@link PickType|PickType.Collision}. * @param {Pointers.RayPointerProperties|Pointers.ParabolaPointerProperties|Pointers.StylusPointerProperties} properties - * The properties of the pointer, per the pointer type, including the properties of the underlying pick * that the pointer uses to do its picking. * @returns {number} The ID of the pointer if successfully created, otherwise 0. * * @example Create a ray pointer on the left hand that changes color when it's intersecting and that triggers * events.
* Note: Stop controllerScripts.js from running to disable similar behavior from it. * var intersectEnd = { * type: "sphere", * dimensions: { x: 0.2, y: 0.2, z: 0.2 }, * solid: true, * color: { red: 0, green: 255, blue: 0 }, * ignorePickIntersection: true * }; * var intersectedPath = { * type: "line3d", * color: { red: 0, green: 255, blue: 0 }, * }; * var searchEnd = { * type: "sphere", * dimensions: { x: 0.2, y: 0.2, z: 0.2 }, * solid: true, * color: { red: 255, green: 0, blue: 0 }, * ignorePickIntersection: true * }; * var searchPath = { * type: "line3d", * color: { red: 255, green: 0, blue: 0 }, * }; * * var renderStates = [{ name: "example", path: intersectedPath, end: intersectEnd }]; * var defaultRenderStates = [{ name: "example", distance: 20.0, path: searchPath, end: searchEnd }]; * * // Create the pointer. * var rayPointer = Pointers.createPointer(PickType.Ray, { * joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", * filter: Picks.PICK_LOCAL_ENTITIES | Picks.PICK_DOMAIN_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, * renderStates: renderStates, * defaultRenderStates: defaultRenderStates, * hover: true, // Generate hover events. * triggers: [ * { action: Controller.Standard.LTClick, button: "Primary" }, // Generate mouse events. * { action: Controller.Standard.LTClick, button: "Focus" } // Focus on web entities. * ], * enabled: true * }); * Pointers.setRenderState(rayPointer, "example"); * * // Hover events. * Entities.hoverEnterEntity.connect(function (entityID, event) { * print("hoverEnterEntity() : " + entityID); * }); * Entities.hoverLeaveEntity.connect(function (entityID, event) { * print("hoverLeaveEntity() : " + entityID); * }); * * // Mouse events. * Entities.mousePressOnEntity.connect(function (entityID, event) { * print("mousePressOnEntity() : " + entityID + " , " + event.button); * }); * Entities.mouseReleaseOnEntity.connect(function (entityID, event) { * print("mouseReleaseOnEntity() : " + entityID + " , " + event.button); * }); * * // Tidy up. * Script.scriptEnding.connect(function () { * Pointers.removePointer(rayPointer); * }); */ // TODO: expand Pointers to be able to be fully configurable with PickFilters // V8TODO: add documentation Q_INVOKABLE unsigned int createRayPointer(RayPointerProperties properties); Q_INVOKABLE unsigned int createStylusPointer(StylusPointerProperties properties); Q_INVOKABLE unsigned int createParabolaPointer(ParabolaPointerProperties properties); /*@jsdoc * Enables and shows a pointer. Enabled pointers update their pick results and generate events. * @function Pointers.enablePointer * @param {number} id - The ID of the pointer. */ Q_INVOKABLE void enablePointer(unsigned int uid) const { DependencyManager::get()->enablePointer(uid); } /*@jsdoc * Disables and hides a pointer. Disabled pointers do not update their pick results or generate events. * @function Pointers.disablePointer * @param {number} id - The ID of the pointer. */ Q_INVOKABLE void disablePointer(unsigned int uid) const { DependencyManager::get()->disablePointer(uid); } /*@jsdoc * Gets the enabled status of a pointer. Enabled pointers update their pick results and generate events. * @function Pointers.isPointerEnabled * @param {number} id - The ID of the pointer. * @returns {boolean} enabled - Whether or not the pointer is enabled. */ Q_INVOKABLE bool isPointerEnabled(unsigned int uid) const; /*@jsdoc * Removes (deletes) a pointer. * @function Pointers.removePointer * @param {number} id - The ID of the pointer. */ Q_INVOKABLE void removePointer(unsigned int uid) const { DependencyManager::get()->removePointer(uid); } /*@jsdoc * Gets the parameters that were passed in to {@link Pointers.createPointer} to create the pointer when the pointer was * created through a script. *

Note: These properties do not reflect the current state of the pointer. To get the current state * of the pointer, see {@link Pointers.getPointerProperties}. * @function Pointers.getPointerScriptParameters * @param {number} id - The ID of the pointer. * @returns {Pointers.RayPointerProperties|Pointers.ParabolaPointerProperties|Pointers.StylusPointerProperties} * Script-provided properties, per the pointer type. */ Q_INVOKABLE QVariantMap getPointerScriptParameters(unsigned int uid) const; /*@jsdoc * Gets all pointers which currently exist, including disabled pointers. * @function Pointers.getPointers * @returns {number[]} pointers - The IDs of the pointers. */ Q_INVOKABLE QVector getPointers() const; /*@jsdoc * Edits a render state of a {@link Pointers.RayPointerProperties|ray} or * {@link Pointers.ParabolaPointerProperties|parabola} pointer, to change its visual appearance for the state when the * pointer is intersecting something. *

Note: You can only edit the properties of the existing parts of the pointer; you cannot change the * type of any part.

*

Note: You cannot use this method to change the appearance of a default render state.

*

Note: Not able to be used with stylus pointers.

* @function Pointers.editRenderState * @param {number} id - The ID of the pointer. * @param {string} renderState - The name of the render state to edit. * @param {Pointers.RayPointerRenderState|Pointers.ParabolaPointerRenderState} properties - The new properties for the * render state. Only the overlay properties to change need be specified. * @example Change the dimensions of a ray pointer's intersecting end overlay. * var intersectEnd = { * type: "sphere", * dimensions: { x: 0.2, y: 0.2, z: 0.2 }, * solid: true, * color: { red: 0, green: 255, blue: 0 }, * ignorePickIntersection: true * }; * var intersectedPath = { * type: "line3d", * color: { red: 0, green: 255, blue: 0 }, * }; * var searchEnd = { * type: "sphere", * dimensions: { x: 0.2, y: 0.2, z: 0.2 }, * solid: true, * color: { red: 255, green: 0, blue: 0 }, * ignorePickIntersection: true * }; * var searchPath = { * type: "line3d", * color: { red: 255, green: 0, blue: 0 }, * }; * * var renderStates = [ { name: "example", path: intersectedPath, end: intersectEnd } ]; * var defaultRenderStates = [ { name: "example", distance: 20.0, path: searchPath, end: searchEnd } ]; * * // Create the pointer. * var rayPointer = Pointers.createPointer(PickType.Ray, { * joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", * filter: Picks.PICK_LOCAL_ENTITIES | Picks.PICK_DOMAIN_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, * renderStates: renderStates, * defaultRenderStates: defaultRenderStates, * enabled: true * }); * Pointers.setRenderState(rayPointer, "example"); * * // Edit the intersecting render state. * Script.setTimeout(function () { * print("Edit render state"); * Pointers.editRenderState(rayPointer, "example", { * end: { dimensions: { x: 0.5, y: 0.5, z: 0.5 } } * }); * }, 10000); * * Script.setTimeout(function () { * print("Edit render state"); * Pointers.editRenderState(rayPointer, "example", { * end: { dimensions: { x: 0.2, y: 0.2, z: 0.2 } } * }); * }, 15000); * * // Tidy up. * Script.scriptEnding.connect(function () { * Pointers.removePointer(rayPointer); * }); */ Q_INVOKABLE void editRenderState(unsigned int uid, const QString& renderState, const QVariant& properties) const; /*@jsdoc * Sets the render state of a pointer, to change its visual appearance and possibly disable or enable it. * @function Pointers.setRenderState * @param {number} id - The ID of the pointer. * @param {string} renderState -

The name of the render state to set the pointer to.

*

For {@link Pointers.RayPointerProperties|ray} and {@link Pointers.ParabolaPointerProperties|parabola} pointers, * this may be:

*
    *
  • The name of one of the render states set in the pointer's properties.
  • *
  • "", to hide the pointer and disable emitting of events.
  • *
*

For {@link Pointers.StylusPointerProperties|stylus} pointers, the values may be:

*
    *
  • "events on", to render and emit events (the default).
  • *
  • "events off", to render but don't emit events.
  • *
  • "disabled", to not render and not emit events.
  • *
* @example Switch a ray pointer between having a path and not having a path. * var intersectEnd = { * type: "sphere", * dimensions: { x: 0.2, y: 0.2, z: 0.2 }, * solid: true, * color: { red: 0, green: 255, blue: 0 }, * ignorePickIntersection: true * }; * var intersectedPath = { * type: "line3d", * color: { red: 0, green: 255, blue: 0 }, * }; * var searchEnd = { * type: "sphere", * dimensions: { x: 0.2, y: 0.2, z: 0.2 }, * solid: true, * color: { red: 255, green: 0, blue: 0 }, * ignorePickIntersection: true * }; * var searchPath = { * type: "line3d", * color: { red: 255, green: 0, blue: 0 }, * }; * * var renderStates = [ * { name: "examplePath", path: intersectedPath, end: intersectEnd }, * { name: "exampleNoPath", end: intersectEnd } * ]; * var defaultRenderStates = [ * { name: "examplePath", distance: 20.0, path: searchPath, end: searchEnd }, * { name: "exampleNoPath", distance: 20.0, end: searchEnd } * ]; * * // Create the pointer. * var rayPointer = Pointers.createPointer(PickType.Ray, { * joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", * filter: Picks.PICK_LOCAL_ENTITIES | Picks.PICK_DOMAIN_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, * renderStates: renderStates, * defaultRenderStates: defaultRenderStates, * enabled: true * }); * Pointers.setRenderState(rayPointer, "examplePath"); * * // Change states. * Script.setTimeout(function () { * print("Without path"); * Pointers.setRenderState(rayPointer, "exampleNoPath"); * }, 10000); * * Script.setTimeout(function () { * print("With path"); * Pointers.setRenderState(rayPointer, "examplePath"); * }, 15000); * * // Tidy up. * Script.scriptEnding.connect(function () { * Pointers.removePointer(rayPointer); * }); */ Q_INVOKABLE void setRenderState(unsigned int uid, const QString& renderState) const { DependencyManager::get()->setRenderState(uid, renderState.toStdString()); } /*@jsdoc * Gets the most recent intersection of a pointer. A pointer continues to be updated ready to return a result, as long as * it is enabled, regardless of the render state. * @function Pointers.getPrevPickResult * @param {number} id - The ID of the pointer. * @returns {RayPickResult|ParabolaPickResult|StylusPickResult} The most recent intersection of the pointer. */ Q_INVOKABLE QVariantMap getPrevPickResult(unsigned int uid) const; /*@jsdoc * Sets whether or not a pointer should use precision picking, i.e., whether it should pick against precise meshes or * coarse meshes. This has the same effect as using the PICK_PRECISE or PICK_COARSE filter flags. * @function Pointers.setPrecisionPicking * @param {number} id - The ID of the pointer. * @param {boolean} precisionPicking - true to use precision picking, false to use coarse picking. */ Q_INVOKABLE void setPrecisionPicking(unsigned int uid, bool precisionPicking) const { DependencyManager::get()->setPrecisionPicking(uid, precisionPicking); } /*@jsdoc * Sets the length of a pointer. *

Note: Not used by stylus pointers.

* @function Pointers.setLength * @param {number} id - The ID of the pointer. * @param {number} length - The desired length of the pointer. */ Q_INVOKABLE void setLength(unsigned int uid, float length) const { DependencyManager::get()->setLength(uid, length); } /*@jsdoc * Sets a list of entity and avatar IDs that a pointer should ignore during intersection. *

Note: Not used by stylus pointers.

* @function Pointers.setIgnoreItems * @param {number} id - The ID of the pointer. * @param {Uuid[]} ignoreItems - A list of IDs to ignore. */ Q_INVOKABLE void setIgnoreItems(unsigned int uid, const ScriptValue& ignoreEntities) const; /*@jsdoc * Sets a list of entity and avatar IDs that a pointer should include during intersection, instead of intersecting with * everything. *

Note: Stylus pointers only intersect with items in their include list.

* @function Pointers.setIncludeItems * @param {number} id - The ID of the pointer. * @param {Uuid[]} includeItems - A list of IDs to include. */ Q_INVOKABLE void setIncludeItems(unsigned int uid, const ScriptValue& includeEntities) const; /*@jsdoc * Locks a pointer onto a specific entity or avatar. *

Note: Not used by stylus pointers.

* @function Pointers.setLockEndUUID * @param {number} id - The ID of the pointer. * @param {Uuid} targetID - The ID of the entity or avatar to lock the pointer on to. * @param {boolean} isAvatar - true if the target is an avatar, false if it is an entity. * @param {Mat4} [offset] - The offset of the target point from the center of the target item. If not specified, the * pointer locks on to the center of the target item. */ Q_INVOKABLE void setLockEndUUID(unsigned int uid, const QUuid& objectID, bool isAvatar, const glm::mat4& offsetMat = glm::mat4()) const { DependencyManager::get()->setLockEndUUID(uid, objectID, isAvatar, offsetMat); } /*@jsdoc * Checks if a pointer is associated with the left hand: a ray or parabola pointer with joint property set to * "_CONTROLLER_LEFTHAND" or "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", or a stylus pointer with * hand property set to 0. * @function Pointers.isLeftHand * @param {number} id - The ID of the pointer. * @returns {boolean} true if the pointer is associated with the left hand, false if it isn't. */ Q_INVOKABLE bool isLeftHand(unsigned int uid) { return DependencyManager::get()->isLeftHand(uid); } /*@jsdoc * Checks if a pointer is associated with the right hand: a ray or parabola pointer with joint property set to * "_CONTROLLER_RIGHTHAND" or "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND", or a stylus pointer with * hand property set to 1. * @function Pointers.isRightHand * @param {number} id - The ID of the pointer. * @returns {boolean} true if the pointer is associated with the right hand, false if it isn't. */ Q_INVOKABLE bool isRightHand(unsigned int uid) { return DependencyManager::get()->isRightHand(uid); } /*@jsdoc * Checks if a pointer is associated with the system mouse: a ray or parabola pointer with joint property set * to "Mouse". * @function Pointers.isMouse * @param {number} id - The ID of the pointer. * @returns {boolean} true if the pointer is associated with the system mouse, false if it isn't. */ Q_INVOKABLE bool isMouse(unsigned int uid) { return DependencyManager::get()->isMouse(uid); } /*@jsdoc * Gets information about a pointer. * @function Pointers.getPointerProperties * @param {number} id - The ID of the pointer. * @returns {Pointers.RayPointerProperties|Pointers.ParabolaPointerProperties|object} The renderStates and * defaultRenderStates for ray and parabola pointers, {} for stylus pointers. * @example Report the properties of a parabola pointer. * var intersectEnd = { * type: "sphere", * dimensions: { x: 0.2, y: 0.2, z: 0.2 }, * solid: true, * color: { red: 0, green: 255, blue: 0 }, * ignorePickIntersection: true * }; * var intersectedPath = { * color: { red: 0, green: 255, blue: 0 }, * }; * var searchEnd = { * type: "sphere", * dimensions: { x: 0.2, y: 0.2, z: 0.2 }, * solid: true, * color: { red: 255, green: 0, blue: 0 }, * ignorePickIntersection: true * }; * var searchPath = { * color: { red: 255, green: 0, blue: 0 }, * }; * * var renderStates = [{ name: "example", path: intersectedPath, end: intersectEnd }]; * var defaultRenderStates = [{ name: "example", distance: 20.0, path: searchPath, end: searchEnd }]; * * // Create the pointer. * var parabolaPointer = Pointers.createPointer(PickType.Parabola, { * joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", * filter: Picks.PICK_LOCAL_ENTITIES | Picks.PICK_DOMAIN_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, * renderStates: renderStates, * defaultRenderStates: defaultRenderStates, * enabled: true * }); * Pointers.setRenderState(parabolaPointer, "example"); * * // Report the pointer properties. * Script.setTimeout(function () { * var properties = Pointers.getPointerProperties(parabolaPointer); * print("Pointer properties:" + JSON.stringify(properties)); * }, 500); * * // Tidy up. * Script.scriptEnding.connect(function () { * Pointers.removePointer(parabolaPointer); * }); */ Q_INVOKABLE QVariantMap getPointerProperties(unsigned int uid) const; protected: static std::shared_ptr buildLaserPointer(const PointerProperties& properties); static std::shared_ptr buildStylus(const PointerProperties& properties); static std::shared_ptr buildParabolaPointer(const PointerProperties& properties); private: Q_INVOKABLE unsigned int createPointerInternal(const PickQuery::PickType& type, const PointerProperties& properties); }; ScriptValue rayPointerPropertiesToScriptValue(ScriptEngine* engine, const RayPointerProperties& in); ScriptValue stylusPointerPropertiesToScriptValue(ScriptEngine* engine, const StylusPointerProperties& in); ScriptValue parabolaPointerPropertiesToScriptValue(ScriptEngine* engine, const ParabolaPointerProperties& in); bool rayPointerPropertiesFromScriptValue(const ScriptValue& value, RayPointerProperties& out); bool stylusPointerPropertiesFromScriptValue(const ScriptValue& value, StylusPointerProperties& out); bool parabolaPointerPropertiesFromScriptValue(const ScriptValue& value, ParabolaPointerProperties& out); #endif // hifi_PointerScriptingInterface_h