using System; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace InteractiveVideo { /// /// Sits on the video RawImage. Turns pointer events (mouse, touch, XR ray via the EventSystem) into a hit test /// against the tracking manager and updates the selection. /// /// Needs the usual UI plumbing: an EventSystem in the scene, a GraphicRaycaster on the Canvas (or a /// TrackedDeviceGraphicRaycaster for XR), and raycastTarget on the RawImage (forced on in Awake). /// [DisallowMultipleComponent] [RequireComponent(typeof(RectTransform))] [AddComponentMenu("Interactive Video/Video Click Handler")] public sealed class VideoClickHandler : MonoBehaviour, IPointerClickHandler, IPointerDownHandler { public enum SelectTrigger { Click, PointerDown } public enum EmptyClickBehavior { KeepSelection, ClearSelection } [SerializeField] private InteractiveVideoPlayer player; [SerializeField] private VideoTrackingManager trackingManager; [Header("Behaviour")] [Tooltip("Click = press and release on the image (ignores drags). PointerDown = react immediately on press.")] [SerializeField] private SelectTrigger trigger = SelectTrigger.Click; [Tooltip("What a click on the background (or on a letterbox bar) does to the current selection.")] [SerializeField] private EmptyClickBehavior emptyClick = EmptyClickBehavior.ClearSelection; [Tooltip("Clicking the already selected object deselects it.")] [SerializeField] private bool toggleOnReselect; [Tooltip("Only react to the primary pointer (left mouse button / first finger).")] [SerializeField] private bool primaryButtonOnly = true; /// Raised on every handled click. hit is null for background/letterbox clicks; point is in TRACKING coordinates. public event Action VideoClicked; public EmptyClickBehavior EmptyClick { get => emptyClick; set => emptyClick = value; } public SelectTrigger Trigger { get => trigger; set => trigger = value; } private RectTransform _rectTransform; private void Reset() { player = GetComponentInParent(); if (player == null) player = FindAnyObjectByType(); if (player != null) trackingManager = player.TrackingManager; } private void Awake() { _rectTransform = (RectTransform)transform; var graphic = GetComponent(); if (graphic == null) Debug.LogWarning("[InteractiveVideo] VideoClickHandler needs a Graphic (RawImage) on the same object to receive pointer events.", this); else graphic.raycastTarget = true; if (player == null) Debug.LogWarning("[InteractiveVideo] VideoClickHandler has no InteractiveVideoPlayer assigned.", this); if (trackingManager == null && player != null) trackingManager = player.TrackingManager; } public void OnPointerClick(PointerEventData eventData) { if (trigger == SelectTrigger.Click) Handle(eventData); } public void OnPointerDown(PointerEventData eventData) { if (trigger == SelectTrigger.PointerDown) Handle(eventData); } private void Handle(PointerEventData eventData) { if (primaryButtonOnly && eventData.button != PointerEventData.InputButton.Left) return; if (player == null || trackingManager == null) return; bool onVideo = TryEventToTracking(eventData, out Vector2 trackingPoint); TrackedObjectInstance hit = onVideo ? trackingManager.GetObjectAtPoint(trackingPoint) : null; if (hit != null) { if (toggleOnReselect && trackingManager.IsSelected(hit.TrackId)) trackingManager.Deselect(hit.TrackId); else trackingManager.Select(hit.TrackId); } else if (emptyClick == EmptyClickBehavior.ClearSelection) { trackingManager.ClearSelection(); } VideoClicked?.Invoke(trackingPoint, hit); } /// /// Pointer event to tracking coordinates. Screen-space events go through the screen point; XR ray events /// (raycaster is not a GraphicRaycaster) use the world hit position so world-space canvases work too. /// public bool TryEventToTracking(PointerEventData eventData, out Vector2 trackingPoint) { trackingPoint = default; if (player == null) return false; var raycast = eventData.pointerCurrentRaycast; bool worldHit = raycast.isValid && !(raycast.module is GraphicRaycaster) && raycast.worldPosition != Vector3.zero; if (worldHit) { Vector2 local = _rectTransform.InverseTransformPoint(raycast.worldPosition); return player.TryLocalPointToTracking(local, out trackingPoint); } Camera cam = eventData.pressEventCamera != null ? eventData.pressEventCamera : eventData.enterEventCamera; return player.TryScreenPointToTracking(eventData.position, cam, out trackingPoint); } } }