using NaughtyAttributes; using System.Collections.Generic; using UnityEngine; namespace UDE_HAND_INTERACTION { public class InteractableSurface : MonoBehaviour, ISurface, IBounds, IClippedSurface { public enum NormalFacing { Backward, Forward, } public Transform Transform => transform; [SerializeField] private NormalFacing _facing = NormalFacing.Backward; [SerializeField] private bool _doubleSided = false; private InteractableSurface _interactableSurface; public ISurface BackingSurface => _interactableSurface; public enum SurfaceType { Square, Circle, Infinity } public SurfaceType surfaceType = SurfaceType.Square; [HideInInspector] public float _radius = 0.1f; [HideInInspector] public Vector3 _position = Vector3.zero; [HideInInspector] public float _width = 0.1f; [HideInInspector] public float _height = 0.1f; public Vector3 Position { get => _position; set => _position = value; } public Vector3 Size { get => new(_width, _height, 0); set { _width = value[0]; _height = value[1]; } } [HideInInspector, SerializeField, Interface(typeof(IBoundsClipper))] private List _clippers; public bool GetLocalBounds(Transform localTo, out Bounds bounds) { Vector3 localPos = localTo.InverseTransformPoint(transform.TransformPoint(Position)); Vector3 localSize = localTo.InverseTransformVector(transform.TransformVector(Size)); bounds = new Bounds(localPos, localSize); return isActiveAndEnabled; } public IEnumerable GetClippers() { foreach (var clipper in _clippers) { yield return clipper as IBoundsClipper; } } public bool ClipBounds(in Bounds bounds, out Bounds clipped) { clipped = bounds; foreach (var clipperMono in _clippers) { IBoundsClipper clipper = clipperMono as IBoundsClipper; if (clipper == null || !clipper.GetLocalBounds(transform, out Bounds clipTo)) { continue; } if (!clipped.Clip(clipTo, out clipped)) { return false; } } return true; } protected virtual void Start() { _interactableSurface = GetComponent(); _clippers.Add(gameObject); } public Vector3 Normal { get { return _facing == NormalFacing.Forward ? transform.forward : -transform.forward; } } private bool IsPointAboveSurface(Vector3 point) { Plane plane = GetPlane(); return plane.GetSide(point); } private Vector3 ClampPoint(in Vector3 point, in Bounds bounds) { Vector3 min = bounds.min; Vector3 max = bounds.max; Vector3 localPoint = Transform.InverseTransformPoint(point); Vector3 clamped = new(Mathf.Clamp(localPoint.x, min.x, max.x),Mathf.Clamp(localPoint.y, min.y, max.y),Mathf.Clamp(localPoint.z, min.z, max.z)); return Transform.TransformPoint(clamped); } public bool ClosestSurfacePoint(in Vector3 point, out SurfaceHit hit, float maxDistance) { hit = new SurfaceHit(); Plane plane = GetPlane(); if(surfaceType == SurfaceType.Circle) { Vector3 vectorFromPlane = point - Transform.position; Vector3 planeNormal = Normal; Vector3 projectedPoint = Vector3.ProjectOnPlane(vectorFromPlane, planeNormal); float distanceFromCenterSqr = projectedPoint.sqrMagnitude; float worldRadius = Transform.lossyScale.x * _radius; if (distanceFromCenterSqr > worldRadius * worldRadius) { projectedPoint = worldRadius * projectedPoint.normalized; } Vector3 closestPoint = projectedPoint + Transform.position; hit.Point = closestPoint; hit.Normal = plane.normal; hit.Distance = Vector3.Distance(point, closestPoint); if (Vector3.Distance(point, transform.position) > _radius) return false; return maxDistance <= 0 || hit.Distance <= maxDistance; } else if(surfaceType == SurfaceType.Square) { bool result = true; float hitDistance = plane.GetDistanceToPoint(point); if (maxDistance > 0 && Mathf.Abs(hitDistance) > maxDistance) { result = false; } hit.Point = plane.ClosestPointOnPlane(point); hit.Distance = IsPointAboveSurface(point) ? hitDistance : -hitDistance; hit.Normal = plane.normal; if (result && GetLocalBounds(transform, out Bounds clippedPlane)) { hit.Point = ClampPoint(hit.Point, clippedPlane); hit.Distance = Vector3.Distance(point, hit.Point); float VaildMaxDist = Mathf.Sqrt(Mathf.Pow(_width, 2) + Mathf.Pow(_height, 2)) * 0.5f; if (Vector3.Distance(point, transform.position) > VaildMaxDist) return false; return maxDistance <= 0 || hit.Distance <= maxDistance; } return false; } else { float hitDistance = plane.GetDistanceToPoint(point); if (maxDistance > 0 && Mathf.Abs(hitDistance) > maxDistance) { return false; } hit.Point = plane.ClosestPointOnPlane(point); hit.Distance = IsPointAboveSurface(point) ? hitDistance : -hitDistance; hit.Normal = plane.normal; return true; } } public Bounds Bounds { get { Vector3 size = new Vector3(Mathf.Abs(Normal.x) == 1f ? float.Epsilon : float.PositiveInfinity, Mathf.Abs(Normal.y) == 1f ? float.Epsilon : float.PositiveInfinity, Mathf.Abs(Normal.z) == 1f ? float.Epsilon : float.PositiveInfinity); return new Bounds(transform.position, size); } } public bool Raycast(in Ray ray, out SurfaceHit hit, float maxDistance) { hit = new SurfaceHit(); Plane plane = GetPlane(); if (!_doubleSided && !IsPointAboveSurface(ray.origin)) { return false; } if (plane.Raycast(ray, out float hitDistance)) { if (maxDistance > 0 && hitDistance > maxDistance) { return false; } if (surfaceType == SurfaceType.Circle) { Vector3 hitPointWorld = ray.GetPoint(hitDistance); Vector3 hitPointLocal = Transform.InverseTransformPoint(hitPointWorld); if (Mathf.Abs(hitPointLocal.x) > _radius || Mathf.Abs(hitPointLocal.y) > _radius) { return false; } hit.Point = hitPointWorld; hit.Normal = plane.normal; hit.Distance = hitDistance; } else if(surfaceType == SurfaceType.Square) { hit.Point = ray.GetPoint(hitDistance); hit.Normal = plane.normal; hit.Distance = hitDistance; Bounds InfiniteBounds = new Bounds(Vector3.zero, Vector3.one * float.PositiveInfinity); return ClipBounds(InfiniteBounds, out Bounds clipBounds) && clipBounds.size != Vector3.zero && clipBounds.Contains(Transform.InverseTransformPoint(hit.Point)); } else { hit.Point = ray.GetPoint(hitDistance); hit.Normal = plane.normal; hit.Distance = hitDistance; } return true; } return false; } public Plane GetPlane() { return new Plane(Normal, transform.position); } } }