using System.Collections; using System.Collections.Generic; using UnityEngine; public class WallSnap : MonoBehaviour // note : can have multiple snap types at the same time { static private WallSnap _instance; [SerializeField] private RectTransform _wallLineContainer; [SerializeField] private RectTransform _wallPointContainer; [SerializeField] private float _snapRangeOnPoint = 15.0f; [SerializeField] private float _snapRangeOnWall = 15.0f; [SerializeField] private float _snapOnWallAngle = 45.0f; [SerializeField] private float _snapRangeOnAngle = 15.0f; private void Awake() { if (_instance != null && _instance != this) { Destroy(gameObject); return; } else { _instance = this; } } //static public bool TrySnap(Vector2 fromPosition, out Vector2 toPosition, out RectTransform onSnapTransform) //{ // return _instance.Snap(fromPosition, null, out toPosition, out onSnapTransform); //} // //static public bool TrySnap(RectTransform fromTransform, out Vector2 toPosition, out RectTransform onSnapTransform) //{ // return _instance.Snap(fromTransform.position, fromTransform, out toPosition, out onSnapTransform); //} static public bool TrySnap(Vector2 fromPosition, RectTransform fromTransform, out Vector2 toPosition, out RectTransform onSnapTransform) { return _instance.Snap(fromPosition, fromTransform, out toPosition, out onSnapTransform); } private bool Snap(Vector2 fromPosition, RectTransform fromTransform, out Vector2 toPosition, out RectTransform onSnapTransform) { bool isSnapped = false; toPosition = fromPosition; onSnapTransform = null; bool hasWallPoint = fromTransform.TryGetComponent(out WallPoint fromWallPoint); // nearest point in range RectTransform onPointTransform = null; if (!isSnapped) { RectTransform nearestTransform = null; float nearestSqrDistance = float.MaxValue; foreach (RectTransform item in _wallPointContainer) { if (hasWallPoint && item == fromTransform) { continue; } #if false // TODO : temporary limitation of snapping to adjacent points only if (hasWallPoint) { bool isAdjacentPoint = fromWallPoint.GetWalls().Exists(x => x.ContainsWallPoint(item.GetComponent())); RectTransform roomPointStartTransform = _wallLineContainer.GetChild(0).GetComponent()._pointStart.GetComponent(); RectTransform roomPointEndTransform = _wallLineContainer.GetChild(_wallLineContainer.childCount - 1).GetComponent()._pointEnd.GetComponent(); bool isFromRoomStartPoint = fromTransform == roomPointStartTransform; bool isFromRoomEndPoint = fromTransform == roomPointEndTransform; bool isToRoomStartPoint = item == roomPointStartTransform; bool isToRoomEndPoint = item == roomPointEndTransform; if (!isAdjacentPoint && !(isFromRoomStartPoint && isToRoomEndPoint) && !(isFromRoomEndPoint && isToRoomStartPoint)) { continue; } } #endif // TODO : check if mouse position works with all elements float currentSqrDistance = Vector2.SqrMagnitude(item.anchoredPosition - fromPosition); if (currentSqrDistance >= (_snapRangeOnPoint * _snapRangeOnPoint) || currentSqrDistance >= nearestSqrDistance) { continue; } nearestTransform = item; nearestSqrDistance = currentSqrDistance; } if (nearestTransform != null) { onPointTransform = nearestTransform; toPosition = nearestTransform.anchoredPosition; isSnapped = true; } } if (isSnapped) { onSnapTransform = onPointTransform; return isSnapped; } float nearestRangeA = float.MaxValue; float nearestRangeB = float.MaxValue; Vector2 nearestPointA = Vector2.positiveInfinity; Vector2 nearestPointB = Vector2.positiveInfinity; Vector2 directionSnapA = Vector2.zero; Vector2 directionSnapB = Vector2.zero; WallSnapType snapTypeA = WallSnapType.None; // TODO : use snap priorities to prevent overrides? WallSnapType snapTypeB = WallSnapType.None; // on wall RectTransform onWallTransform = null; #if true // TODO : temporary disabling of on wall snap if (!isSnapped) { RectTransform nearestTransform = null; float nearestSqrDistance = float.MaxValue; Vector2 snapPointOnLine = Vector2.positiveInfinity; Vector2 snapDirection = Vector2.zero; foreach (RectTransform item in _wallLineContainer) { if (hasWallPoint && item.GetComponent().ContainsWallPoint(fromWallPoint)) { continue; } if (item == fromTransform) // TODO : check if valid { continue; } WallLine currentWallLine = item.GetComponent(); // TODO : make and get from list if slow Vector2 startPoint = currentWallLine._pointStart.GetAnchoredPosition(); Vector2 line = currentWallLine._pointEnd.GetAnchoredPosition() - startPoint; Vector2 from = fromPosition - startPoint; Vector2 lineDirection = line.normalized; float dot = Vector2.Dot(from, lineDirection); if (dot < 0 || dot * dot > line.sqrMagnitude) { continue; // note : can still be in range of a point } Vector2 closestPointOnLine = startPoint + lineDirection * dot; Vector2 projectionOnLine = closestPointOnLine - fromPosition; float currentSqrDistance = projectionOnLine.sqrMagnitude; // TODO : re-evaluate (near > current) for relative wall if (currentSqrDistance > nearestSqrDistance || currentSqrDistance > _snapRangeOnWall * _snapRangeOnWall) { continue; } //if (nearestSqrDistance > currentSqrDistance) { } nearestSqrDistance = currentSqrDistance; nearestTransform = item; snapPointOnLine = closestPointOnLine; snapDirection = lineDirection; } if (nearestTransform != null) { isSnapped = true; onWallTransform = nearestTransform; // TODO : choose A or B nearestRangeA = Mathf.Sqrt(nearestSqrDistance); nearestPointA = snapPointOnLine; directionSnapA = snapDirection; snapTypeA = WallSnapType.OnLine; } } #endif // angle if (hasWallPoint) { // look at wall angles in point foreach (WallLine item in fromWallPoint.GetWalls()) { WallPoint otherPoint = item.GetOtherPoint(fromWallPoint); Vector2 otherPointPosition = otherPoint.GetAnchoredPosition(); Vector2 line = fromPosition - otherPointPosition; float angle = Vector2.SignedAngle(Vector2.right, line); float nearestSnapAngle = Mathf.Round(angle / _snapOnWallAngle) * _snapOnWallAngle; Quaternion snapRotation = Quaternion.AngleAxis(nearestSnapAngle, Vector3.forward); Vector2 snapRangeDirection = snapRotation * Vector2.up; float dotAbsoluteRange = Mathf.Abs(Vector2.Dot(snapRangeDirection, line)); if (dotAbsoluteRange > _snapRangeOnAngle) { continue; } Vector2 snapLineDirection = snapRotation * Vector2.right; bool isParallelA = directionSnapA != Vector2.zero && Mathf.Abs(Vector2.Dot(directionSnapA, snapLineDirection)) > 1.0f - 0.001f; bool isParallelB = directionSnapB != Vector2.zero && Mathf.Abs(Vector2.Dot(directionSnapB, snapLineDirection)) > 1.0f - 0.001f; if (isParallelA || isParallelB) { Debug.Log("parallel angles"); continue; } Vector2 closestPointOnLine = otherPointPosition + snapLineDirection * Vector2.Dot(snapLineDirection, line); isSnapped = true; if (nearestRangeA == float.MaxValue) { nearestRangeA = dotAbsoluteRange; nearestPointA = closestPointOnLine; directionSnapA = snapLineDirection; snapTypeA = WallSnapType.Angle; } else if (nearestRangeB == float.MaxValue) { nearestRangeB = dotAbsoluteRange; nearestPointB = closestPointOnLine; directionSnapB = snapLineDirection; snapTypeB = WallSnapType.Angle; } else { bool isRangeCloserThanA = dotAbsoluteRange < nearestRangeA; bool isRangeCloserThanB = dotAbsoluteRange < nearestRangeB; if (isRangeCloserThanA && isRangeCloserThanB) { bool isCloserToA = nearestRangeA < nearestRangeB; if (isCloserToA) { nearestRangeB = dotAbsoluteRange; nearestPointB = closestPointOnLine; directionSnapB = snapLineDirection; snapTypeB = WallSnapType.Angle; } else { nearestRangeA = dotAbsoluteRange; nearestPointA = closestPointOnLine; directionSnapA = snapLineDirection; snapTypeA = WallSnapType.Angle; } } else if (isRangeCloserThanA) { nearestRangeA = dotAbsoluteRange; nearestPointA = closestPointOnLine; directionSnapA = snapLineDirection; snapTypeA = WallSnapType.Angle; } else if (isRangeCloserThanB) { nearestRangeB = dotAbsoluteRange; nearestPointB = closestPointOnLine; directionSnapB = snapLineDirection; snapTypeB = WallSnapType.Angle; } else { // do nothing } } } } // get final snap position if (nearestRangeA == float.MaxValue && nearestRangeB == float.MaxValue) { // do nothing } else if (nearestRangeA != float.MaxValue ^ nearestRangeB != float.MaxValue) { toPosition = nearestRangeA != float.MaxValue ? nearestPointA : nearestPointB; } else // intersection between two snap values { // find offset Vector2 intersectionOffset = Vector2.zero; if (directionSnapA.x != 0 && directionSnapB.x != 0) { float slopeA = directionSnapA.y / directionSnapA.x; float slopeB = directionSnapB.y / directionSnapB.x; float slopeDelta = slopeA - slopeB; if (slopeDelta != 0) { Vector2 lineAB = nearestPointB - nearestPointA; float offsetB = lineAB.y - lineAB.x * slopeB; float intersectionX = offsetB / slopeDelta; float intersectionY = intersectionX * slopeA; intersectionOffset = new Vector2(intersectionX, intersectionY); } } // find position if (directionSnapA.x == 0 ^ directionSnapB.x == 0) { bool isSnapAtZeroA = directionSnapA.x == 0; float baseX = isSnapAtZeroA ? nearestPointA.x : nearestPointB.x; float baseY = isSnapAtZeroA ? nearestPointB.y : nearestPointA.y; float deltaX = isSnapAtZeroA ? baseX - nearestPointB.x : baseX - nearestPointA.x; float slope = isSnapAtZeroA ? directionSnapB.y / directionSnapB.x : directionSnapA.y / directionSnapA.x; float offsetY = deltaX * slope; toPosition = new Vector2(baseX, baseY + offsetY); } else { toPosition = nearestPointA + intersectionOffset; } } //Debug.Log($"snapA : {snapTypeA} ; snapB : {snapTypeB}"); onSnapTransform = snapTypeA == WallSnapType.OnLine || snapTypeB == WallSnapType.OnLine ? onWallTransform : onSnapTransform; return isSnapped; } } public enum WallSnapType // IMPORTANT : ordered from lowest to highest priority { None, Angle, RelativeLine, RelativePoint, OnLine, OnPoint }