Editor/InteriorLayoutBuilder/RoomLayoutTool.Selection.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutTool
{
	private bool TryGetCursorPoint( out Vector2 point )
	{
		return TryGetCursorPoint( out point, out _ );
	}

	private bool TryGetCursorPoint( out Vector2 point, out bool hitGeneratedLayout )
	{
		return TryGetCursorPoint( out point, out hitGeneratedLayout, out _ );
	}

	private bool TryGetCursorPoint( out Vector2 point, out bool hitGeneratedLayout, out GameObject hitGameObject )
	{
		point = default;
		hitGeneratedLayout = false;
		hitGameObject = null;

		var trace = Trace.UseRenderMeshes( true ).UsePhysicsWorld( false ).Run();
		if ( trace.Hit && trace.GameObject.IsValid() && IsGeneratedLayoutObject( trace.GameObject ) )
		{
			hitGeneratedLayout = true;
			hitGameObject = trace.GameObject;
			point = SnapCursorPoint( trace.HitPosition );
			return true;
		}

		var plane = new Plane( Vector3.Up, ActiveFloorZ() );

		if ( !plane.TryTrace( Gizmo.CurrentRay, out var hit, true ) )
		{
			return false;
		}

		point = SnapCursorPoint( hit );
		return true;
	}

	private bool TryFindRoom( Vector2 point, out RoomLayoutRoom room )
	{
		room = document.Rooms.LastOrDefault( candidate => IsActiveFloor( candidate ) && candidate.Bounds.Contains( point ) );
		return room is not null;
	}

	private bool TryFindRoomWall( Vector2 point, out RoomLayoutRoom room )
	{
		return TryFindNearestWall( point, out room, out _, out _ );
	}

	private bool TryFindNearestWall( Vector2 point, out RoomLayoutRoom room, out RoomLayoutWallSide side, out float offset )
	{
		RoomLayoutRoom bestRoom = null;
		var bestSide = default( RoomLayoutWallSide );
		var bestOffset = 0.0f;
		var bestDistance = MathF.Max( 16.0f, document.Settings.GridSize * 0.35f );

		foreach ( var candidate in document.Rooms.Where( IsActiveFloor ) )
		{
			var bounds = candidate.Bounds;
			CheckWall( candidate, RoomLayoutWallSide.North, MathF.Abs( point.y - (bounds.Y + bounds.Height) ), point.x - bounds.X, bounds.Width );
			CheckWall( candidate, RoomLayoutWallSide.South, MathF.Abs( point.y - bounds.Y ), point.x - bounds.X, bounds.Width );
			CheckWall( candidate, RoomLayoutWallSide.East, MathF.Abs( point.x - (bounds.X + bounds.Width) ), point.y - bounds.Y, bounds.Height );
			CheckWall( candidate, RoomLayoutWallSide.West, MathF.Abs( point.x - bounds.X ), point.y - bounds.Y, bounds.Height );
		}

		room = bestRoom;
		side = bestSide;
		offset = bestOffset;
		return room is not null;

		void CheckWall( RoomLayoutRoom candidate, RoomLayoutWallSide candidateSide, float distance, float candidateOffset, float length )
		{
			if ( distance > bestDistance || candidateOffset < 0.0f || candidateOffset > length )
			{
				return;
			}

			bestDistance = distance;
			bestRoom = candidate;
			bestSide = candidateSide;
			bestOffset = candidateOffset;
		}
	}

	private bool TryFindNearestCorridorWall(
		Vector2 point,
		out RoomLayoutCorridor corridor,
		out int segmentIndex,
		out int side,
		out float offset,
		out float length )
	{
		corridor = null;
		segmentIndex = 0;
		side = 0;
		offset = 0.0f;
		length = 0.0f;
		var bestDistance = MathF.Max( 16.0f, document.Settings.GridSize * 0.35f );

		foreach ( var candidate in document.Corridors.Where( IsActiveFloor ) )
		{
			if ( !TryGetCorridorPath( candidate, out var points ) )
			{
				continue;
			}

			var width = CorridorClearWidth( candidate );
			for ( var i = 0; i < points.Count - 1; i++ )
			{
				var a = points[i];
				var b = points[i + 1];
				var delta = b - a;
				if ( delta.Length < 1.0f )
				{
					continue;
				}

				var horizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
				var minAlong = horizontal ? MathF.Min( a.x, b.x ) : MathF.Min( a.y, b.y );
				var maxAlong = horizontal ? MathF.Max( a.x, b.x ) : MathF.Max( a.y, b.y );
				var candidateLength = maxAlong - minAlong;
				var along = horizontal ? point.x : point.y;

				for ( var candidateSide = -1; candidateSide <= 1; candidateSide += 2 )
				{
					var fixedCoordinate = (horizontal ? a.y : a.x) + candidateSide * width * 0.5f;
					var distance = MathF.Abs( (horizontal ? point.y : point.x) - fixedCoordinate );
					if ( distance > bestDistance || along < minAlong || along > maxAlong )
					{
						continue;
					}

					bestDistance = distance;
					corridor = candidate;
					segmentIndex = i;
					side = candidateSide;
					offset = along - minAlong;
					length = candidateLength;
				}
			}
		}

		return corridor is not null;
	}

	private bool TryFindNearestDoor( Vector2 point, out RoomLayoutDoor door, float maxDistance, bool roomOnly = false )
	{
		door = null;
		var bestDistance = maxDistance;

		foreach ( var candidate in document.Doors.Where( IsActiveFloor ) )
		{
			if ( roomOnly && candidate.RoomId == 0 )
			{
				continue;
			}

			if ( !TryGetDoorFootprint( candidate, out var footprint ) )
			{
				continue;
			}

			var distance = DistanceToRect( point, footprint );
			if ( distance > bestDistance )
			{
				continue;
			}

			bestDistance = distance;
			door = candidate;
		}

		return door is not null;
	}

	private bool TryFindNearestWindow( Vector2 point, out RoomLayoutWindow window, float maxDistance )
	{
		window = null;
		var bestDistance = maxDistance;

		foreach ( var candidate in document.Windows.Where( IsActiveFloor ) )
		{
			if ( !TryGetWindowFootprint( candidate, out var footprint ) )
			{
				continue;
			}

			var distance = DistanceToRect( point, footprint );
			if ( distance > bestDistance )
			{
				continue;
			}

			bestDistance = distance;
			window = candidate;
		}

		return window is not null;
	}

	private bool TryFindWindowUnderCursor( out RoomLayoutWindow window )
	{
		window = null;
		var padding = MathF.Max( 4.0f, document.Settings.GridSize * 0.05f );

		for ( var i = document.Windows.Count - 1; i >= 0; i-- )
		{
			var candidate = document.Windows[i];
			if ( !IsActiveFloor( candidate ) )
			{
				continue;
			}

			if ( !TryGetWindowSegment( candidate, out var a, out var b ) )
			{
				continue;
			}

			var sillHeight = EffectiveWindowSillHeight( candidate );
			var openingHeight = EffectiveWindowHeight( candidate, sillHeight );
			var windowTop = MathF.Min( document.Settings.WallHeight, sillHeight + openingHeight );
			var horizontal = MathF.Abs( b.x - a.x ) >= MathF.Abs( b.y - a.y );
			var planeNormal = horizontal
				? new Vector3( 0.0f, 1.0f, 0.0f )
				: new Vector3( 1.0f, 0.0f, 0.0f );
			var planeDistance = horizontal ? a.y : a.x;
			var plane = new Plane( planeNormal, planeDistance );
			if ( !plane.TryTrace( Gizmo.CurrentRay, out var hit, true ) )
			{
				continue;
			}

			var along = horizontal ? hit.x : hit.y;
			var minAlong = horizontal ? MathF.Min( a.x, b.x ) : MathF.Min( a.y, b.y );
			var maxAlong = horizontal ? MathF.Max( a.x, b.x ) : MathF.Max( a.y, b.y );
			var floorZ = FloorWorldZ( document.FloorFor( candidate ) );
			if ( along < minAlong - padding ||
				along > maxAlong + padding ||
				hit.z < floorZ + sillHeight - padding ||
				hit.z > floorZ + windowTop + padding )
			{
				continue;
			}

			window = candidate;
			return true;
		}

		return false;
	}

	private bool TrySelectGeneratedLayoutObject( GameObject gameObject )
	{
		for ( var current = gameObject; current.IsValid(); current = current.Parent )
		{
			var name = current.Name ?? "";
			if ( TryReadGeneratedLayoutId( name, "Window", out var windowId ) &&
				document.FindWindow( windowId ) is { } window )
			{
				SelectWindow( window );
				SetActiveFloorFromSelection();
				return true;
			}

			if ( TryReadGeneratedLayoutId( name, "Door", out var doorId ) &&
				document.FindDoor( doorId ) is { } door )
			{
				SelectDoor( door );
				SetActiveFloorFromSelection();
				return true;
			}

			if ( TryReadGeneratedLayoutId( name, "Corridor", out var corridorId ) &&
				document.Corridors.FirstOrDefault( corridor => corridor.Id == corridorId ) is { } corridor )
			{
				SelectCorridor( corridor );
				SetActiveFloorFromSelection();
				return true;
			}
		}

		return false;
	}

	private static bool TryReadGeneratedLayoutId( string name, string marker, out int id )
	{
		id = 0;
		var index = name.IndexOf( marker, StringComparison.OrdinalIgnoreCase );
		if ( index < 0 )
		{
			return false;
		}

		index += marker.Length;
		while ( index < name.Length && char.IsWhiteSpace( name[index] ) )
		{
			index++;
		}

		var start = index;
		while ( index < name.Length && char.IsDigit( name[index] ) )
		{
			index++;
		}

		return index > start && int.TryParse( name[start..index], out id );
	}

	private bool TryFindNearestCorridor( Vector2 point, out RoomLayoutCorridor corridor )
	{
		corridor = null;
		var bestDistance = MathF.Max( 12.0f, document.Settings.GridSize * 0.25f );

		foreach ( var candidate in document.Corridors.Where( IsActiveFloor ) )
		{
			if ( !TryGetCorridorPath( candidate, out var points ) )
			{
				continue;
			}

			var width = CorridorClearWidth( candidate );
			var hitDistance = width * 0.5f + MathF.Max( 8.0f, document.Settings.GridSize * 0.15f );
			for ( var i = 0; i < points.Count - 1; i++ )
			{
				var distance = DistanceToSegment( point, points[i], points[i + 1] );
				if ( distance > hitDistance || distance > bestDistance + width * 0.5f )
				{
					continue;
				}

				bestDistance = distance;
				corridor = candidate;
			}
		}

		return corridor is not null;
	}

	private bool TryGetDoorSegment( RoomLayoutDoor door, out Vector3 a, out Vector3 b )
	{
		a = default;
		b = default;
		if ( door.CorridorId != 0 )
		{
			return TryGetCorridorOpeningSegment( door.CorridorId, door.CorridorSegmentIndex, door.CorridorSide, door.Offset, door.Width, 7.0f, out a, out b );
		}

		var room = document.FindRoom( door.RoomId );
		if ( room is null )
		{
			return false;
		}

		var half = DoorOpeningWidth( door ) * 0.5f;
		var bounds = room.Bounds;
		switch ( door.Side )
		{
			case RoomLayoutWallSide.North:
				a = new Vector3( bounds.X + door.Offset - half, bounds.Y + bounds.Height, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
				b = new Vector3( bounds.X + door.Offset + half, bounds.Y + bounds.Height, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
				return true;
			case RoomLayoutWallSide.South:
				a = new Vector3( bounds.X + door.Offset - half, bounds.Y, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
				b = new Vector3( bounds.X + door.Offset + half, bounds.Y, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
				return true;
			case RoomLayoutWallSide.East:
				a = new Vector3( bounds.X + bounds.Width, bounds.Y + door.Offset - half, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
				b = new Vector3( bounds.X + bounds.Width, bounds.Y + door.Offset + half, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
				return true;
			default:
				a = new Vector3( bounds.X, bounds.Y + door.Offset - half, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
				b = new Vector3( bounds.X, bounds.Y + door.Offset + half, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
				return true;
		}
	}

	private bool TryGetDoorFootprint( RoomLayoutDoor door, out RoomLayoutRect footprint )
	{
		footprint = default;
		return TryGetDoorSegment( door, out var a, out var b ) &&
			TryGetOpeningFootprint( a, b, out footprint );
	}

	private bool TryGetWindowSegment( RoomLayoutWindow window, out Vector3 a, out Vector3 b )
	{
		a = default;
		b = default;
		var sillHeight = EffectiveWindowSillHeight( window );
		var openingHeight = EffectiveWindowHeight( window, sillHeight );
		var localZ = Math.Clamp(
			sillHeight + openingHeight * 0.5f,
			8.0f,
			MathF.Max( 8.0f, document.Settings.WallHeight ) );
		if ( window.CorridorId != 0 )
		{
			return TryGetCorridorOpeningSegment( window.CorridorId, window.CorridorSegmentIndex, window.CorridorSide, window.Offset, window.Width, localZ, out a, out b );
		}

		var room = document.FindRoom( window.RoomId );
		if ( room is null )
		{
			return false;
		}

		var half = window.Width * 0.5f;
		var bounds = room.Bounds;
		var z = FloorWorldZ( document.FloorFor( window ) ) + localZ;

		switch ( window.Side )
		{
			case RoomLayoutWallSide.North:
				a = new Vector3( bounds.X + window.Offset - half, bounds.Y + bounds.Height, z );
				b = new Vector3( bounds.X + window.Offset + half, bounds.Y + bounds.Height, z );
				return true;
			case RoomLayoutWallSide.South:
				a = new Vector3( bounds.X + window.Offset - half, bounds.Y, z );
				b = new Vector3( bounds.X + window.Offset + half, bounds.Y, z );
				return true;
			case RoomLayoutWallSide.East:
				a = new Vector3( bounds.X + bounds.Width, bounds.Y + window.Offset - half, z );
				b = new Vector3( bounds.X + bounds.Width, bounds.Y + window.Offset + half, z );
				return true;
			default:
				a = new Vector3( bounds.X, bounds.Y + window.Offset - half, z );
				b = new Vector3( bounds.X, bounds.Y + window.Offset + half, z );
				return true;
		}
	}

	private bool TryGetWindowFootprint( RoomLayoutWindow window, out RoomLayoutRect footprint )
	{
		footprint = default;
		return TryGetWindowSegment( window, out var a, out var b ) &&
			TryGetOpeningFootprint( a, b, out footprint );
	}

	private bool TryGetOpeningFootprint( Vector3 a, Vector3 b, out RoomLayoutRect footprint )
	{
		footprint = default;
		var deltaX = MathF.Abs( b.x - a.x );
		var deltaY = MathF.Abs( b.y - a.y );
		var depth = MathF.Max( 4.0f, document.Settings.WallThickness );

		if ( MathF.Max( deltaX, deltaY ) < 1.0f )
		{
			return false;
		}

		if ( deltaX >= deltaY )
		{
			footprint = new RoomLayoutRect( MathF.Min( a.x, b.x ), a.y - depth * 0.5f, deltaX, depth );
			return true;
		}

		footprint = new RoomLayoutRect( a.x - depth * 0.5f, MathF.Min( a.y, b.y ), depth, deltaY );
		return true;
	}

	private bool TryGetCorridorOpeningSegment(
		int corridorId,
		int segmentIndex,
		int side,
		float offset,
		float width,
		float z,
		out Vector3 a,
		out Vector3 b )
	{
		a = default;
		b = default;
		var corridor = document.Corridors.FirstOrDefault( candidate => candidate.Id == corridorId );
		if ( corridor is null || !TryGetCorridorPath( corridor, out var points ) || segmentIndex < 0 || segmentIndex >= points.Count - 1 )
		{
			return false;
		}

		var start = points[segmentIndex];
		var end = points[segmentIndex + 1];
		var delta = end - start;
		if ( delta.Length < 1.0f )
		{
			return false;
		}

		var horizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
		var clearWidth = CorridorClearWidth( corridor );
		var minAlong = horizontal ? MathF.Min( start.x, end.x ) : MathF.Min( start.y, end.y );
		var maxAlong = horizontal ? MathF.Max( start.x, end.x ) : MathF.Max( start.y, end.y );
		var halfOpening = width * 0.5f;
		var centerAlong = Math.Clamp( minAlong + offset, minAlong + halfOpening, MathF.Max( minAlong + halfOpening, maxAlong - halfOpening ) );
		var fixedCoordinate = (horizontal ? start.y : start.x) + Math.Sign( side == 0 ? 1 : side ) * clearWidth * 0.5f;
		z += FloorWorldZ( document.FloorFor( corridor ) );

		if ( horizontal )
		{
			a = new Vector3( centerAlong - halfOpening, fixedCoordinate, z );
			b = new Vector3( centerAlong + halfOpening, fixedCoordinate, z );
			return true;
		}

		a = new Vector3( fixedCoordinate, centerAlong - halfOpening, z );
		b = new Vector3( fixedCoordinate, centerAlong + halfOpening, z );
		return true;
	}

	private bool TryGetDoorCenter( RoomLayoutDoor door, out Vector2 center )
	{
		center = default;
		if ( !TryGetDoorSegment( door, out var a, out var b ) )
		{
			return false;
		}

		center = new Vector2( (a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f );
		return true;
	}

	private bool TryGetWindowCenter( RoomLayoutWindow window, out Vector2 center )
	{
		center = default;
		if ( !TryGetWindowSegment( window, out var a, out var b ) )
		{
			return false;
		}

		center = new Vector2( (a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f );
		return true;
	}

	private float EffectiveDoorHeight( RoomLayoutDoor door )
	{
		var height = door.Height > 0.0f ? door.Height : document.Settings.DoorHeight;
		return Math.Clamp( height, 0.0f, document.Settings.WallHeight );
	}

	private float EffectiveWindowSillHeight( RoomLayoutWindow window )
	{
		var sillHeight = window.SillHeight >= 0.0f ? window.SillHeight : document.Settings.WindowSillHeight;
		return Math.Clamp( sillHeight, 0.0f, MathF.Max( 0.0f, document.Settings.WallHeight - 1.0f ) );
	}

	private float EffectiveWindowHeight( RoomLayoutWindow window, float sillHeight )
	{
		var height = window.Height > 0.0f ? window.Height : document.Settings.WindowHeight;
		return Math.Clamp( height, 1.0f, MathF.Max( 1.0f, document.Settings.WallHeight - sillHeight ) );
	}

	private float DoorOpeningWidth( RoomLayoutDoor door )
	{
		var connectedWidth = 0.0f;
		foreach ( var corridor in document.Corridors )
		{
			if ( corridor.StartDoorId != door.Id && corridor.EndDoorId != door.Id )
			{
				continue;
			}

			connectedWidth = MathF.Max( connectedWidth, CorridorClearWidth( corridor ) );
		}

		return connectedWidth > 0.0f
			? MathF.Min( door.Width, connectedWidth )
			: door.Width;
	}

	private bool TryGetDoorPoint( int doorId, out Vector2 point )
	{
		point = default;
		if ( !TryGetDoorAnchor( doorId, out var anchor ) )
		{
			return false;
		}

		point = anchor.Point;
		return true;
	}

	private bool TryGetCorridorPath( RoomLayoutCorridor corridor, out List<Vector2> points )
	{
		points = null;

		if ( !document.CorridorDoorsAreOnFloor( corridor ) )
		{
			return false;
		}

		if ( !TryGetDoorAnchor( corridor.StartDoorId, out var start ) ||
			!TryGetDoorAnchor( corridor.EndDoorId, out var end ) )
		{
			return false;
		}

		var width = CorridorClearWidth( corridor );
		var startPortal = CorridorPortalPoint( start );
		var endPortal = CorridorPortalPoint( end );
		var startApproach = CorridorApproachPoint( start, width );
		var endApproach = CorridorApproachPoint( end, width );

		points = new List<Vector2>();
		if ( corridor.ManualPath )
		{
			AddCorridorPoint( points, startPortal );
			AddCorridorPoint( points, startApproach );
			foreach ( var bend in corridor.BendPoints )
			{
				AddCorridorPoint( points, bend.Vector );
			}

			AddCorridorPoint( points, endApproach );
			AddCorridorPoint( points, endPortal );
			SimplifyCorridorPath( points );
			return true;
		}

		AddCorridorPoint( points, startPortal );
		AddCorridorPoint( points, startApproach );
		foreach ( var bend in corridor.BendPoints )
		{
			AddCorridorPoint( points, bend.Vector );
		}

		if ( corridor.BendPoints.Count == 0 && !startApproach.x.AlmostEqual( endApproach.x ) && !startApproach.y.AlmostEqual( endApproach.y ) )
		{
			AddCorridorPoint( points, new Vector2( endApproach.x, startApproach.y ) );
		}

		AddCorridorPoint( points, endApproach );
		AddCorridorPoint( points, endPortal );
		SimplifyCorridorPath( points );
		return true;
	}

	private Vector2 CorridorPortalPoint( RoomLayoutDoorAnchor door )
	{
		return door.Point;
	}

	private Vector2 CorridorApproachPoint( RoomLayoutDoorAnchor door, float width )
	{
		return door.Point + door.Normal * (width * 0.5f + document.Settings.WallThickness);
	}

	private static void AddCorridorPoint( List<Vector2> points, Vector2 point )
	{
		if ( points.Count == 0 || Vector2.DistanceBetween( points[^1], point ) > 0.5f )
		{
			points.Add( point );
		}
	}

	private static void SimplifyCorridorPath( List<Vector2> points )
	{
		for ( var i = 1; i < points.Count - 1; )
		{
			if ( IsRedundantCorridorPoint( points[i - 1], points[i], points[i + 1] ) )
			{
				points.RemoveAt( i );
				continue;
			}

			i++;
		}
	}

	private static bool IsRedundantCorridorPoint( Vector2 a, Vector2 b, Vector2 c )
	{
		if ( a.x.AlmostEqual( b.x ) && b.x.AlmostEqual( c.x ) )
		{
			return IsBetween( b.y, a.y, c.y );
		}

		return a.y.AlmostEqual( b.y ) && b.y.AlmostEqual( c.y ) && IsBetween( b.x, a.x, c.x );
	}

	private static bool IsBetween( float value, float a, float b )
	{
		return value >= MathF.Min( a, b ) - 0.5f && value <= MathF.Max( a, b ) + 0.5f;
	}

	private bool TryGetDoorAnchor( int doorId, out RoomLayoutDoorAnchor anchor )
	{
		anchor = default;
		var door = document.FindDoor( doorId );
		if ( door is null || !TryGetDoorCenter( door, out var point ) )
		{
			return false;
		}

		anchor = new RoomLayoutDoorAnchor( point, DoorNormal( door.Side ) );
		return true;
	}

	private static Vector2 DoorNormal( RoomLayoutWallSide side )
	{
		return side switch
		{
			RoomLayoutWallSide.North => new Vector2( 0.0f, 1.0f ),
			RoomLayoutWallSide.South => new Vector2( 0.0f, -1.0f ),
			RoomLayoutWallSide.East => new Vector2( 1.0f, 0.0f ),
			_ => new Vector2( -1.0f, 0.0f )
		};
	}

	private static float DistanceToSegment( Vector2 point, Vector2 a, Vector2 b )
	{
		var segment = b - a;
		var lengthSquared = segment.LengthSquared;
		if ( lengthSquared.AlmostEqual( 0.0f ) )
		{
			return Vector2.DistanceBetween( point, a );
		}

		var t = (((point.x - a.x) * segment.x) + ((point.y - a.y) * segment.y)) / lengthSquared;
		t = Math.Clamp( t, 0.0f, 1.0f );
		var closest = a + segment * t;
		return Vector2.DistanceBetween( point, closest );
	}

	private static float DistanceToRect( Vector2 point, RoomLayoutRect rect )
	{
		var dx = MathF.Max( MathF.Max( rect.X - point.x, 0.0f ), point.x - (rect.X + rect.Width) );
		var dy = MathF.Max( MathF.Max( rect.Y - point.y, 0.0f ), point.y - (rect.Y + rect.Height) );
		return MathF.Sqrt( dx * dx + dy * dy );
	}

	private RoomLayoutRect SnapRect( RoomLayoutRect rect )
	{
		var min = new Vector2( Snap( rect.X ), Snap( rect.Y ) );
		var max = new Vector2( Snap( rect.X + rect.Width ), Snap( rect.Y + rect.Height ) );
		return RoomLayoutRect.FromPoints( min, max );
	}

	private float Snap( float value )
	{
		if ( BypassGridSnap )
		{
			return value;
		}

		var grid = MathF.Max( 1.0f, document.Settings.GridSize );
		return MathF.Round( value / grid ) * grid;
	}

	private Vector2 SnapCursorPoint( Vector3 point )
	{
		if ( mode == RoomLayoutToolMode.Select || BypassGridSnap )
		{
			return new Vector2( point.x, point.y );
		}

		return mode is RoomLayoutToolMode.Corridors or RoomLayoutToolMode.Doors or RoomLayoutToolMode.Windows
			? new Vector2( SnapHalfGrid( point.x ), SnapHalfGrid( point.y ) )
			: new Vector2( Snap( point.x ), Snap( point.y ) );
	}

	private float SnapHalfGrid( float value )
	{
		if ( BypassGridSnap )
		{
			return value;
		}

		var grid = MathF.Max( 1.0f, document.Settings.GridSize * 0.5f );
		return MathF.Round( value / grid ) * grid;
	}

	private BBox SnapGizmoBox( BBox startBox, BBox deltaBox )
	{
		if ( !BypassGridSnap )
		{
			return Gizmo.Snap( startBox, deltaBox );
		}

		return new BBox(
			startBox.Mins + deltaBox.Mins,
			startBox.Maxs + deltaBox.Maxs );
	}

	private static bool BypassGridSnap => global::Editor.Application.IsKeyDown( KeyCode.Shift );

	private readonly record struct RoomLayoutDoorAnchor( Vector2 Point, Vector2 Normal );
}