using System.Collections.Generic; using UnityEngine; public class SplintObjectClickHandler : MonoBehaviour { private const float MinColliderSize = 0.35f; private const float ScreenHitPadding = 120f; private const float ScreenCenterFallbackRadius = 160f; private MeshRenderer _outlineMesh; private bool _clicked; private float _lastClickTime; private static int _lastHandledFrame = -1; private static readonly List ActiveHandlers = new List(); public bool CanBeClicked() { return !_clicked && gameObject.activeInHierarchy; } private void OnEnable() { if (!ActiveHandlers.Contains(this)) ActiveHandlers.Add(this); EnsureCollider(); CreateOutlineMesh(); ResetState(true); } private void OnDisable() { ActiveHandlers.Remove(this); StopAllCoroutines(); if (_outlineMesh != null) _outlineMesh.enabled = false; } private void OnMouseDown() { PerformClick(); } private void Update() { if (!CanBeClicked()) return; if (!Input.GetMouseButtonDown(0)) return; if (_lastHandledFrame == Time.frameCount) return; if (UnityEngine.EventSystems.EventSystem.current != null && UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject()) return; Camera activeCamera = GetActiveCamera(); if (activeCamera == null) return; Vector2 mousePos = Input.mousePosition; float myDistance; if (!TryGetPointerDistance(activeCamera, mousePos, out myDistance)) return; if (myDistance > ScreenCenterFallbackRadius && !IsPointerInsideExpandedBounds(activeCamera, mousePos)) return; if (!IsNearestClickable(activeCamera, mousePos, myDistance)) return; PerformClick(); } public void PerformClick() { if (_clicked) return; if (_lastHandledFrame == Time.frameCount) return; if (Time.time - _lastClickTime < 0.3f) return; _lastClickTime = Time.time; _lastHandledFrame = Time.frameCount; InteractionClickGuard.MarkHandled(); _clicked = true; StopAllCoroutines(); if (_outlineMesh != null) _outlineMesh.enabled = false; SetRenderersVisible(true); ActionHistory.Push(ActionHistory.ActionType.SplintPlank, this); SplintPracticeFlowManager flow = FindObjectOfType(); if (flow != null) flow.OnPlankClicked(this); ExamFlowManager examFlow = FindObjectOfType(); if (examFlow != null) examFlow.OnObjectClicked(this); } public void ResetState(bool startBlink) { EnsureCollider(); _clicked = false; SetRenderersVisible(false); StopAllCoroutines(); if (_outlineMesh != null) _outlineMesh.enabled = startBlink; if (startBlink && isActiveAndEnabled) StartCoroutine(BlinkOutline()); } public void ForceHideOutline() { StopAllCoroutines(); if (_outlineMesh != null) _outlineMesh.enabled = false; } private void EnsureCollider() { BoxCollider box = GetComponent(); if (box == null) box = gameObject.AddComponent(); Bounds bounds; if (TryGetRendererBounds(out bounds)) { Vector3 localCenter = transform.InverseTransformPoint(bounds.center); Vector3 localSize = transform.InverseTransformVector(bounds.size); localSize = new Vector3(Mathf.Abs(localSize.x), Mathf.Abs(localSize.y), Mathf.Abs(localSize.z)); localSize = new Vector3( Mathf.Max(localSize.x * 1.8f, MinColliderSize), Mathf.Max(localSize.y * 1.8f, MinColliderSize), Mathf.Max(localSize.z * 1.8f, MinColliderSize)); box.center = localCenter; box.size = localSize; } box.isTrigger = false; box.enabled = true; } private void CreateOutlineMesh() { if (_outlineMesh != null) return; Mesh mesh = GetMesh(); if (mesh == null) return; Shader shader = Shader.Find("Custom/OutlineUnlit"); if (shader == null) return; Material mat = new Material(shader); mat.SetColor("_OutlineColor", new Color(0.2f, 0.5f, 1f, 1f)); mat.SetColor("_MainColor", new Color(0f, 0f, 0f, 0f)); mat.SetFloat("_OutlineWidth", OutlineConfig.Width); GameObject outline = new GameObject("Outline"); outline.transform.SetParent(transform, false); outline.AddComponent().sharedMesh = mesh; _outlineMesh = outline.AddComponent(); _outlineMesh.material = mat; } private Mesh GetMesh() { MeshFilter mf = GetComponent(); if (mf == null) mf = GetComponentInChildren(true); if (mf != null && mf.sharedMesh != null) return mf.sharedMesh; SkinnedMeshRenderer smr = GetComponent(); if (smr == null) smr = GetComponentInChildren(true); return smr != null ? smr.sharedMesh : null; } private bool TryGetRendererBounds(out Bounds bounds) { Renderer[] renderers = GetComponentsInChildren(true); bool found = false; bounds = new Bounds(transform.position, Vector3.zero); for (int i = 0; i < renderers.Length; i++) { Renderer renderer = renderers[i]; if (renderer == null) continue; if (_outlineMesh != null && renderer == _outlineMesh) continue; if (!found) { bounds = renderer.bounds; found = true; } else { bounds.Encapsulate(renderer.bounds); } } return found; } private Camera GetActiveCamera() { Camera activeCamera = Camera.main; foreach (Camera camera in Camera.allCameras) { if (camera != null && camera.enabled && camera.gameObject.activeInHierarchy) { activeCamera = camera; } } return activeCamera; } private bool TryGetPointerDistance(Camera activeCamera, Vector2 pointer, out float distance) { Rect screenRect; Vector2 center; if (TryGetScreenRect(activeCamera, out screenRect, out center)) { float rectDistance = DistanceToRect(pointer, screenRect); float centerDistance = Vector2.Distance(pointer, center); distance = rectDistance + centerDistance * 0.001f; return true; } Vector3 screenPosition = activeCamera.WorldToScreenPoint(transform.position); if (screenPosition.z < 0f) { distance = float.MaxValue; return false; } center = new Vector2(screenPosition.x, screenPosition.y); distance = Vector2.Distance(pointer, center); return true; } private bool IsPointerInsideExpandedBounds(Camera activeCamera, Vector2 pointer) { Rect screenRect; Vector2 center; if (!TryGetScreenRect(activeCamera, out screenRect, out center)) return false; return screenRect.Contains(pointer); } private bool TryGetScreenRect(Camera activeCamera, out Rect screenRect, out Vector2 center) { screenRect = new Rect(); center = Vector2.zero; Bounds bounds; if (!TryGetRendererBounds(out bounds)) return false; Vector3 extents = bounds.extents; Vector3 boundsCenter = bounds.center; Vector3[] corners = { boundsCenter + new Vector3(-extents.x, -extents.y, -extents.z), boundsCenter + new Vector3(-extents.x, -extents.y, extents.z), boundsCenter + new Vector3(-extents.x, extents.y, -extents.z), boundsCenter + new Vector3(-extents.x, extents.y, extents.z), boundsCenter + new Vector3(extents.x, -extents.y, -extents.z), boundsCenter + new Vector3(extents.x, -extents.y, extents.z), boundsCenter + new Vector3(extents.x, extents.y, -extents.z), boundsCenter + new Vector3(extents.x, extents.y, extents.z) }; float minX = float.MaxValue; float minY = float.MaxValue; float maxX = float.MinValue; float maxY = float.MinValue; bool found = false; for (int i = 0; i < corners.Length; i++) { Vector3 screenPoint = activeCamera.WorldToScreenPoint(corners[i]); if (screenPoint.z < 0f) continue; minX = Mathf.Min(minX, screenPoint.x); minY = Mathf.Min(minY, screenPoint.y); maxX = Mathf.Max(maxX, screenPoint.x); maxY = Mathf.Max(maxY, screenPoint.y); found = true; } if (!found) return false; minX -= ScreenHitPadding; minY -= ScreenHitPadding; maxX += ScreenHitPadding; maxY += ScreenHitPadding; screenRect = Rect.MinMaxRect(minX, minY, maxX, maxY); center = screenRect.center; return true; } private bool IsNearestClickable(Camera activeCamera, Vector2 pointer, float myDistance) { int myId = GetInstanceID(); for (int i = 0; i < ActiveHandlers.Count; i++) { SplintObjectClickHandler other = ActiveHandlers[i]; if (other == null || other == this) continue; if (!other.CanBeClicked()) continue; float otherDistance; if (!other.TryGetPointerDistance(activeCamera, pointer, out otherDistance)) continue; if (otherDistance < myDistance) return false; if (Mathf.Approximately(otherDistance, myDistance) && other.GetInstanceID() < myId) return false; } return true; } private float DistanceToRect(Vector2 point, Rect rect) { if (rect.Contains(point)) return 0f; float closestX = Mathf.Clamp(point.x, rect.xMin, rect.xMax); float closestY = Mathf.Clamp(point.y, rect.yMin, rect.yMax); return Vector2.Distance(point, new Vector2(closestX, closestY)); } private void SetRenderersVisible(bool visible) { Renderer[] renderers = GetComponentsInChildren(true); for (int i = 0; i < renderers.Length; i++) { if (_outlineMesh != null && renderers[i] == _outlineMesh) continue; renderers[i].enabled = visible; } } private System.Collections.IEnumerator BlinkOutline() { yield return new WaitForSeconds(0.4f); while (true) { if (_outlineMesh != null) _outlineMesh.enabled = !_outlineMesh.enabled; yield return new WaitForSeconds(0.4f); } } }