UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

172 lines (151 loc) 5.85 kB
import { Intersection, Object3D, SkinnedMesh } from "three"; import { type IRaycastOptions, RaycastOptions } from "../../engine/engine_physics.js"; import { serializable } from "../../engine/engine_serialization.js"; import { NeedleXRSession } from "../../engine/engine_xr.js"; import { Behaviour } from "../Component.js"; import { EventSystem } from "./EventSystem.js"; /** * [ObjectRaycaster](https://engine.needle.tools/docs/api/ObjectRaycaster) Base class for raycasters that detect pointer interactions. * Derive from this class to create custom raycasting logic. * * **Built-in raycasters:** * - {@link ObjectRaycaster} - Raycasts against 3D objects * - {@link GraphicRaycaster} - Raycasts against UI elements * - {@link SpatialGrabRaycaster} - Sphere overlap for XR grab * * **Important:** If you override `awake`, `onEnable`, or `onDisable`, * call the base class methods to ensure proper registration with {@link EventSystem}. * * @category Interactivity * @group Components * @see {@link EventSystem} for the event dispatch system */ export abstract class Raycaster extends Behaviour { awake(): void { EventSystem.createIfNoneExists(this.context); } onEnable(): void { EventSystem.get(this.context)?.register(this); } onDisable(): void { EventSystem.get(this.context)?.unregister(this); } abstract performRaycast(_opts?: IRaycastOptions | RaycastOptions | null): Intersection[] | null; } /** * ObjectRaycaster enables pointer interactions with 3D objects. * Add this component to any object that needs click/hover detection. * * **Usage:** * Objects with ObjectRaycaster will receive pointer events when * they implement interfaces like {@link IPointerClickHandler}. * * **Note:** * In older Needle Engine versions the ObjectRaycaster was required to be added to the Scene. * This is no longer the case - the EventSystem will automatically handle raycasts. * * * @category Interactivity * @group Components * @see {@link IPointerClickHandler} for click events * @see {@link DragControls} for drag interactions */ export class ObjectRaycaster extends Raycaster { private targets: Object3D[] | null = null; private raycastHits: Intersection[] = []; @serializable() ignoreSkinnedMeshes: boolean = false; start(): void { this.targets = [this.gameObject]; } performRaycast(opts: IRaycastOptions | RaycastOptions | null = null): Intersection[] | null { if (!this.targets) return null; opts ??= new RaycastOptions(); opts.targets = this.targets; opts.results = this.raycastHits; opts.useAcceleratedRaycast = true; const orig = opts.testObject; if (this.ignoreSkinnedMeshes) { opts.testObject = obj => { // if we are set to ignore skinned meshes, we return false for them if (obj instanceof SkinnedMesh) { return "continue in children"; } // call the original testObject function if (orig) return orig(obj); // otherwise allow raycasting return true; }; } const hits = this.context.physics.raycast(opts); opts.testObject = orig; return hits; } } /** * GraphicRaycaster enables pointer interactions with UI elements. * Add this to a {@link Canvas} or UI hierarchy to enable button clicks, * hover effects, and other UI interactions. * * **Requirements:** * - Must be on the same object as a Canvas or on a parent * - UI elements need proper RectTransform setup * * @example Enable UI interaction * ```ts * // Add to Canvas object * canvas.addComponent(GraphicRaycaster); * // Now buttons and other UI elements will respond to clicks * ``` * * @summary Raycaster for UI elements * @category User Interface * @group Components * @see {@link Canvas} for UI root * @see {@link Button} for clickable UI * @see {@link EventSystem} for event handling */ export class GraphicRaycaster extends ObjectRaycaster { // eventCamera: Camera | null = null; // ignoreReversedGraphics: boolean = false; // rootRaycaster: GraphicRaycaster | null = null; constructor() { super(); this.ignoreSkinnedMeshes = true; } } /** * SpatialGrabRaycaster enables direct grab interactions in VR/AR. * Uses sphere overlap detection around the controller/hand position * to allow grabbing objects by reaching into them. * * **Features:** * - Active only during XR sessions * - Can be globally disabled via `SpatialGrabRaycaster.allow` * - Works alongside ray-based interaction * * @category XR * @group Components * @see {@link WebXR} for XR session management * @see {@link DragControls} for object manipulation */ export class SpatialGrabRaycaster extends Raycaster { /** * Use to disable SpatialGrabRaycaster globally */ static allow: boolean = true; performRaycast(_opts?: IRaycastOptions | RaycastOptions | null): Intersection[] | null { // ensure we're in XR, otherwise return if (!NeedleXRSession.active) return null; if (!SpatialGrabRaycaster.allow) return null; if (!_opts?.ray) return null; // TODO this raycast should actually start from gripWorldPosition, not the ray origin, for // cases like transient-pointer on VisionOS where the ray starts at the head and not the hand const rayOrigin = _opts.ray.origin; const radius = 0.015; // TODO if needed, check if the input source is a XR controller or hand // draw gizmo around ray origin // Gizmos.DrawSphere(rayOrigin, radius, 0x00ff0022); return this.context.physics.sphereOverlap(rayOrigin, radius, false, true); } }