using UnityEngine; [DisallowMultipleComponent] public sealed class MapCoordinateSystem : MonoBehaviour { public const float CoordinateMaximum = 10f; [Header("Coordinate Bounds")] [SerializeField] private Transform bottomLeft = null; [SerializeField] private Transform bottomRight = null; [SerializeField] private Transform topLeft = null; [SerializeField] private Transform topRight = null; [Header("Square Bounds")] [SerializeField, Min(0.001f)] private float squareSize = 5.14925f; [Header("Scene View Visualization")] [SerializeField] private bool showSceneVisualization = true; [SerializeField, Range(1, 20)] private int gridDivisions = 10; [SerializeField] private bool showCoordinateLabels = true; [SerializeField] private Color boundaryColor = new(0f, 0.85f, 1f, 1f); [SerializeField] private Color gridColor = new(0f, 0.85f, 1f, 0.3f); [SerializeField, Min(0.001f)] private float markerSize = 0.06f; public Vector2 MaximumCoordinates => new(CoordinateMaximum, CoordinateMaximum); public float SquareSize => Mathf.Max(0.001f, squareSize); public bool ShowSceneVisualization => showSceneVisualization; public int GridDivisions => Mathf.Max(1, gridDivisions); public bool ShowCoordinateLabels => showCoordinateLabels; public Color BoundaryColor => boundaryColor; public Color GridColor => gridColor; public float MarkerSize => Mathf.Max(0.001f, markerSize); public Transform BottomLeft => bottomLeft; public Transform BottomRight => bottomRight; public Transform TopLeft => topLeft; public Transform TopRight => topRight; private void OnValidate() { squareSize = Mathf.Max(0.001f, squareSize); ApplySquareBounds(); } public void ApplySquareBounds() { if (!HasValidCorners()) return; Vector3 origin = bottomLeft.position; Vector3 rightDirection = bottomRight.position - origin; Vector3 upDirection = topLeft.position - origin; if (rightDirection.sqrMagnitude <= Mathf.Epsilon || upDirection.sqrMagnitude <= Mathf.Epsilon) return; rightDirection.Normalize(); upDirection = Vector3.ProjectOnPlane(upDirection, rightDirection); if (upDirection.sqrMagnitude <= Mathf.Epsilon) return; upDirection.Normalize(); float size = SquareSize; bottomRight.position = origin + rightDirection * size; topLeft.position = origin + upDirection * size; topRight.position = origin + (rightDirection + upDirection) * size; } public Vector2 ClampCoordinates(Vector2 coordinates) { return new Vector2( Mathf.Clamp(coordinates.x, 0f, CoordinateMaximum), Mathf.Clamp(coordinates.y, 0f, CoordinateMaximum)); } public Vector3 CoordinatesToWorldPosition(Vector2 coordinates) { if (!HasValidCorners()) return transform.position; Vector2 clamped = ClampCoordinates(coordinates); float horizontal = clamped.x / CoordinateMaximum; float vertical = clamped.y / CoordinateMaximum; Vector3 bottom = Vector3.Lerp(bottomLeft.position, bottomRight.position, horizontal); Vector3 top = Vector3.Lerp(topLeft.position, topRight.position, horizontal); return Vector3.Lerp(bottom, top, vertical); } public Vector2 WorldPositionToCoordinates(Vector3 worldPosition) { if (!HasValidCorners()) return Vector2.zero; Vector3 horizontalEdge = bottomRight.position - bottomLeft.position; Vector3 verticalEdge = topLeft.position - bottomLeft.position; Vector3 fromBottomLeft = worldPosition - bottomLeft.position; float horizontal = horizontalEdge.sqrMagnitude > Mathf.Epsilon ? Vector3.Dot(fromBottomLeft, horizontalEdge) / horizontalEdge.sqrMagnitude : 0f; float vertical = verticalEdge.sqrMagnitude > Mathf.Epsilon ? Vector3.Dot(fromBottomLeft, verticalEdge) / verticalEdge.sqrMagnitude : 0f; for (int iteration = 0; iteration < 6; iteration++) { Vector3 current = BilinearPosition(horizontal, vertical); Vector3 error = worldPosition - current; Vector3 horizontalDerivative = Vector3.Lerp( bottomRight.position - bottomLeft.position, topRight.position - topLeft.position, vertical); Vector3 verticalDerivative = Vector3.Lerp( topLeft.position - bottomLeft.position, topRight.position - bottomRight.position, horizontal); float horizontalLength = Vector3.Dot(horizontalDerivative, horizontalDerivative); float mixedLength = Vector3.Dot(horizontalDerivative, verticalDerivative); float verticalLength = Vector3.Dot(verticalDerivative, verticalDerivative); float determinant = horizontalLength * verticalLength - mixedLength * mixedLength; if (Mathf.Abs(determinant) <= Mathf.Epsilon) break; float horizontalError = Vector3.Dot(horizontalDerivative, error); float verticalError = Vector3.Dot(verticalDerivative, error); horizontal += (horizontalError * verticalLength - verticalError * mixedLength) / determinant; vertical += (verticalError * horizontalLength - horizontalError * mixedLength) / determinant; } Vector2 coordinates = new( horizontal * CoordinateMaximum, vertical * CoordinateMaximum); return ClampCoordinates(coordinates); } private Vector3 BilinearPosition(float horizontal, float vertical) { Vector3 bottom = Vector3.LerpUnclamped(bottomLeft.position, bottomRight.position, horizontal); Vector3 top = Vector3.LerpUnclamped(topLeft.position, topRight.position, horizontal); return Vector3.LerpUnclamped(bottom, top, vertical); } public bool HasValidCorners() { return bottomLeft != null && bottomRight != null && topLeft != null && topRight != null; } }