#if UIELEMENTS_MODULE_PRESENT && UNITY_6000_2_OR_NEWER #define UITOOLKIT_WORLDSPACE_ENABLED using System.Collections.Generic; using UnityEngine.UIElements; #endif using UnityEngine.XR.Interaction.Toolkit.Filtering; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; namespace UnityEngine.XR.Interaction.Toolkit.UI { /// /// Class that handles UI Toolkit poke interactions. /// This is used by XRPokeInteractor to encapsulate UI interaction logic. /// internal class XRUIToolkitPokeHandler { #if UITOOLKIT_WORLDSPACE_ENABLED // References to debug visualizers GameObject m_VisualizersRoot; Transform m_PokePointVisualizer; Transform m_ClosestPointVisualizer; Transform m_RayOriginVisualizer; Transform m_NormalVisualizer; bool m_VisualizersCreated; #endif // Reference to the owning interactor readonly XRPokeInteractor m_Interactor; // Flag to enable/disable depth updates bool m_UpdateDepth; /// /// Whether to update depth during interactions. /// public bool updateDepth { get => m_UpdateDepth; set => m_UpdateDepth = value; } /// /// Creates a new UI Toolkit poke handler for the given interactor. /// /// The owning poke interactor. public XRUIToolkitPokeHandler(XRPokeInteractor interactor) { m_Interactor = interactor; } /// /// Clean up any resources when done /// public void Dispose() { #if UITOOLKIT_WORLDSPACE_ENABLED DestroyVisualizers(); #endif } /// /// Processes a poke interaction with a UI Toolkit document. /// /// The being interacted with. /// The being interacted with. This requires a UI Document component. /// The being interacted with. /// If , UI Toolkit will perform a multi-picking operation. /// The reference for the . public void ProcessPokeInteraction( Collider hitCollider, Transform interactableTransform, IXRInteractable interactable, bool useMultiPick, IXRPokeFilter pokeFilter = null) { #if UITOOLKIT_WORLDSPACE_ENABLED if (!interactableTransform.TryGetComponent(out UIDocument document)) return; var documentTransform = document.transform; var pokeTransform = m_Interactor.GetAttachTransform(null); var pokePoint = pokeTransform.position; // Use document's forward as our stable picking axis var documentNormal = -documentTransform.forward; // Project onto document plane var documentPlane = new Plane(documentNormal, documentTransform.position); documentPlane.Raycast(new Ray(pokePoint, -documentNormal), out float distanceToPlane); var closestPoint = pokePoint - documentNormal * distanceToPlane; // Pull back ray origin by our poke width to ensure stable picking var rayOrigin = closestPoint + documentNormal; // Update debug visuals if enabled UpdateVisualizers(pokePoint, closestPoint, rayOrigin, documentNormal, documentTransform); // Perform pick to find hit element var hitElement = PerformPick(document, rayOrigin, -documentNormal, m_Interactor.pokeWidth, useMultiPick); if (hitElement != null) { // Cache the physical hit data for use by interactables XRUIToolkitHandler.UpdateInteractorHitData(m_Interactor, new InteractorHitData { closestPoint = closestPoint, interactorOrigin = pokePoint, interactorDirection = documentNormal, hitDocument = document }); // Determine poke depth from filter if available float processResult = 1f; if (pokeFilter != null && interactable is IXRSelectInteractable selectInteractable) { // We invoke the poke filter process to obtain the poke press amount. This call is not expensive because unlike the normal process call, it just returns the cached value for the poke data. // It might be preferable to find a different solution to, assuming user's implement their own poke filter that might behave differently, but the value should be correct in those cases, which is import to maintain. // With Unity provided code it is the least invasive way of getting the poke press amount at this point in the frame. // The reason we call need to get this data here, is because we want to retrieve it before the XRUIToolkitHandler invokes the event system update, which occurs at the end of PreProcess. processResult = pokeFilter.Process(m_Interactor, selectInteractable, 0f); } bool pokeComplete = processResult > 0.99f; // Apply element depth based on poke progress if enabled if (m_UpdateDepth) { // Add better handling of detection of z depth to handle this // TODO Improve depth support - it's a bit glitchy right now XRUIToolkitHandler.SetZDepthForInteractor(hitElement, m_Interactor, 20f * (1f - processResult)); } // Update XRUIToolkitHandler pointer state XRUIToolkitHandler.HandlePointerUpdate( m_Interactor, rayOrigin, Quaternion.LookRotation((closestPoint - rayOrigin).normalized), pokeComplete, false); } else { // No hit, reset pointer state ResetPointerState(); } #endif } /// /// Reset the pointer state when no longer interacting with UI /// public void ResetPointerState() { XRUIToolkitHandler.HandlePointerUpdate( m_Interactor, Vector3.zero, Quaternion.identity, false, true); #if UITOOLKIT_WORLDSPACE_ENABLED // Clear depth if enabled if (m_UpdateDepth) { XRUIToolkitHandler.ClearZDepthForInteractor(m_Interactor); } #endif } #if UITOOLKIT_WORLDSPACE_ENABLED /// /// Perform a pick operation at the given point /// VisualElement PerformPick(UIDocument document, Vector3 center, Vector3 direction, float radius, bool useMultiPick) { // Center pick always has priority if it hits var centerElement = WorldSpaceInput.Pick3D(document, new Ray(center, direction)); if (centerElement != null) return centerElement; // If multi-pick is disabled or radius is too small, return null if (useMultiPick) return PerformMultiPick(document, center, direction, radius); else return null; } /// /// Performs multiple ray casts in a pattern around the center point to find the best UI element to interact with. /// This helps with hitting small UI elements and provides more reliable picking. /// VisualElement PerformMultiPick(UIDocument document, Vector3 center, Vector3 direction, float radius) { // Store all hits with their distances Dictionary elementDistances = new Dictionary(); // Sample points in a circle around the center const int sampleCount = 4; const float angleStep = 2f * Mathf.PI / sampleCount; // Using radians directly // Extract document's transformed axes for sampling in correct space Matrix4x4 documentTransform = document.transform.localToWorldMatrix; Vector3 xAxis = documentTransform.GetColumn(0); // Already includes proper scaling Vector3 yAxis = documentTransform.GetColumn(1); // Already includes proper scaling for (int i = 0; i < sampleCount; i++) { float angle = i * angleStep; // Calculate offset in world space using document's transformed axes Vector3 worldOffset = xAxis * (Mathf.Cos(angle) * radius) + yAxis * (Mathf.Sin(angle) * radius); var sampleOrigin = center + worldOffset; var element = WorldSpaceInput.Pick3D(document, new Ray(sampleOrigin, direction)); if (element != null) { // Use squared distance from sampling point to center as proximity metric float distanceSquared = worldOffset.sqrMagnitude; // Keep the closest detection point for each element if (!elementDistances.TryGetValue(element, out float existingDistance) || distanceSquared < existingDistance) { elementDistances[element] = distanceSquared; } } } // Find the closest element VisualElement bestMatch = null; float closestDistance = float.MaxValue; foreach (var pair in elementDistances) { if (pair.Value < closestDistance) { closestDistance = pair.Value; bestMatch = pair.Key; } } return bestMatch; } /// /// Update visualizer state based on debug settings /// public void UpdateVisualizersState() { if (m_Interactor.debugVisualizationsEnabled && m_Interactor.isActiveAndEnabled) { CreateVisualizers(); } else { DestroyVisualizers(); } } void UpdateVisualizers(Vector3 pokePoint, Vector3 closestPoint, Vector3 rayOrigin, Vector3 normal, Transform parentTransform) { if (!m_Interactor.debugVisualizationsEnabled || !m_VisualizersCreated) return; m_PokePointVisualizer.position = pokePoint; m_ClosestPointVisualizer.position = closestPoint; m_RayOriginVisualizer.position = rayOrigin; m_NormalVisualizer.position = closestPoint + normal * 0.025f; m_NormalVisualizer.up = normal; // Ensure visualizers are parented to the document for proper visibility if (m_VisualizersRoot.transform.parent != parentTransform) { m_VisualizersRoot.transform.SetParent(parentTransform, false); } } void CreateVisualizers() { if (m_VisualizersCreated) return; // Create a parent object to keep visualizers organized m_VisualizersRoot = new GameObject("UIPokeVisualizers"); // Create debug sphere for poke point (blue) GameObject pokePointSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); pokePointSphere.name = "PokePoint"; pokePointSphere.transform.SetParent(m_VisualizersRoot.transform); pokePointSphere.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f); if (pokePointSphere.TryGetComponent(out var collider1)) Object.Destroy(collider1); pokePointSphere.GetComponent().material.color = Color.blue; m_PokePointVisualizer = pokePointSphere.transform; // Create debug sphere for closest point (green) GameObject closestPointSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); closestPointSphere.name = "ClosestPoint"; closestPointSphere.transform.SetParent(m_VisualizersRoot.transform); closestPointSphere.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f); if (closestPointSphere.TryGetComponent(out var collider2)) Object.Destroy(collider2); closestPointSphere.GetComponent().material.color = Color.green; m_ClosestPointVisualizer = closestPointSphere.transform; // Create debug sphere for ray origin (yellow) GameObject rayOriginSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); rayOriginSphere.name = "RayOrigin"; rayOriginSphere.transform.SetParent(m_VisualizersRoot.transform); rayOriginSphere.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f); if (rayOriginSphere.TryGetComponent(out var collider3)) Object.Destroy(collider3); rayOriginSphere.GetComponent().material.color = Color.yellow; m_RayOriginVisualizer = rayOriginSphere.transform; // Create cylinder for document normal direction (red) GameObject normalIndicator = GameObject.CreatePrimitive(PrimitiveType.Cylinder); normalIndicator.name = "DocumentNormal"; normalIndicator.transform.SetParent(m_VisualizersRoot.transform); normalIndicator.transform.localScale = new Vector3(0.005f, 0.05f, 0.005f); if (normalIndicator.TryGetComponent(out var collider4)) Object.Destroy(collider4); normalIndicator.GetComponent().material.color = Color.red; m_NormalVisualizer = normalIndicator.transform; m_VisualizersCreated = true; } void DestroyVisualizers() { if (m_VisualizersRoot != null) { Object.Destroy(m_VisualizersRoot); m_VisualizersCreated = false; m_PokePointVisualizer = null; m_ClosestPointVisualizer = null; m_RayOriginVisualizer = null; m_NormalVisualizer = null; } } #endif } }