Editor/Services/ArchFootprint.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public readonly struct ArchBox {
	public Vector2 Min { get; init; }
	public Vector2 Max { get; init; }

	public ArchBox Grown( float amount ) {
		var margin = new Vector2( amount, amount );

		return new ArchBox { Min = Min - margin, Max = Max + margin };
	}
}

public static class ArchFootprint {
	const float Grain = 0.02f;

	public const int LeastSegments = 8;

	public static List<Vector2> Turned( IReadOnlyList<Vector2> loop, Vector2 about, float degrees ) {
		var turned = new List<Vector2>( loop.Count );

		foreach ( var point in loop ) {
			turned.Add( Turned( point, about, degrees ) );
		}

		return turned;
	}

	public static List<List<Vector2>> Turned( IEnumerable<IReadOnlyList<Vector2>> loops, Vector2 about, float degrees ) {
		return loops.Select( loop => Turned( loop, about, degrees ) ).ToList();
	}

	public static Vector2 Turned( Vector2 point, Vector2 about, float degrees ) {
		var radians = degrees.DegreeToRadian();
		var local = point - about;
		var cos = MathF.Cos( radians );
		var sin = MathF.Sin( radians );

		return about + new Vector2( local.x * cos - local.y * sin, local.x * sin + local.y * cos );
	}

	public static List<Vector2> Rect( Vector2 min, Vector2 max ) {
		return new List<Vector2> { min, new( max.x, min.y ), max, new( min.x, max.y ) };
	}

	public static List<Vector2> OrRect( IReadOnlyList<Vector2> loop, Vector2 min, Vector2 max ) {
		return loop is { Count: >= 4 } ? new List<Vector2>( loop ) : Rect( min, max );
	}

	public static List<Vector2> Ellipse( Vector2 min, Vector2 max, int segments ) {
		var sides = Math.Max( 3, segments );
		var centre = (min + max) * 0.5f;
		var radius = (max - min) * 0.5f;
		var loop = new List<Vector2>( sides );

		// Half-step offset so flats face the axes, not points.
		var lead = MathF.PI / sides;

		for ( var index = 0; index < sides; index++ ) {
			var angle = lead + index * MathF.Tau / sides;

			loop.Add( centre + new Vector2( MathF.Cos( angle ) * radius.x, MathF.Sin( angle ) * radius.y ) );
		}

		return Wind( loop );
	}

	public static bool IsRectilinear( IReadOnlyList<Vector2> loop ) => IsRectilinear( loop, 0f );

	public static bool IsRectilinear( IReadOnlyList<Vector2> loop, float degrees ) {
		if ( loop is null || loop.Count < 4 ) {
			return false;
		}

		var radians = degrees.DegreeToRadian();
		var along = new Vector2( MathF.Cos( radians ), MathF.Sin( radians ) );
		var across = new Vector2( -along.y, along.x );

		for ( var index = 0; index < loop.Count; index++ ) {
			var span = loop[(index + 1) % loop.Count] - loop[index];

			if ( MathF.Abs( Vector2.Dot( span, along ) ) > Grain && MathF.Abs( Vector2.Dot( span, across ) ) > Grain ) {
				return false;
			}
		}

		return true;
	}

	public static bool Bearing( IEnumerable<IReadOnlyList<Vector2>> loops, out float degrees ) {
		degrees = 0f;

		var filled = loops?.Where( loop => loop is { Count: >= 3 } ).ToList();

		if ( filled is not { Count: > 0 } || filled.All( loop => IsRectilinear( loop, 0f ) ) ) {
			return true;
		}

		var turned = Longest( filled );

		if ( !filled.All( loop => IsRectilinear( loop, turned ) ) ) {
			return false;
		}

		degrees = turned;

		return true;
	}

	public static bool Bearing( IReadOnlyList<Vector2> loop, out float degrees ) {
		return Bearing( new[] { loop }, out degrees );
	}

	// Longest edge names the frame, folded into [0, 90) — a stub must not speak for the whole plan.
	static float Longest( IReadOnlyList<IReadOnlyList<Vector2>> loops ) {
		var reach = 0f;
		var degrees = 0f;

		foreach ( var loop in loops ) {
			for ( var index = 0; index < loop.Count; index++ ) {
				var span = loop[(index + 1) % loop.Count] - loop[index];

				if ( span.Length <= reach ) {
					continue;
				}

				reach = span.Length;
				degrees = MathF.Atan2( span.y, span.x ).RadianToDegree();
			}
		}

		return degrees % 90f;
	}

	public static Vector2 Pivot( IEnumerable<IReadOnlyList<Vector2>> loops ) {
		return loops?.FirstOrDefault( loop => loop is { Count: >= 1 } )?[0] ?? Vector2.Zero;
	}

	static bool Framed( IEnumerable<IReadOnlyList<Vector2>> loops, out Vector2 pivot, out float degrees ) {
		var filled = loops?.Where( loop => loop is { Count: >= 3 } ).ToList() ?? new List<IReadOnlyList<Vector2>>();

		pivot = Pivot( filled );

		return Bearing( filled, out degrees ) && MathF.Abs( degrees ) > 0.001f;
	}

	public static bool IsSimple( IReadOnlyList<Vector2> loop ) {
		if ( loop is null || loop.Count < 4 || loop.Distinct().Count() != loop.Count ) {
			return false;
		}

		for ( var first = 0; first < loop.Count; first++ ) {
			var firstEnd = (first + 1) % loop.Count;

			for ( var second = first + 1; second < loop.Count; second++ ) {
				var secondEnd = (second + 1) % loop.Count;

				if ( first == second || firstEnd == second || secondEnd == first ) {
					continue;
				}

				if ( Intersects( loop[first], loop[firstEnd], loop[second], loop[secondEnd] ) ) {
					return false;
				}
			}
		}

		return true;
	}

	// Repairs, winds, and bounds — the canonical footprint from a drag.
	public static (List<Vector2> Loop, Vector2 Min, Vector2 Max) Authored( IReadOnlyList<Vector2> outline ) {
		var loop = Wind( Repair( outline ) );

		Bounds( loop, out var min, out var max );

		return (loop, min, max);
	}

	public static List<Vector2> Repair( IReadOnlyList<Vector2> loop ) {
		if ( loop is not { Count: >= 3 } ) {
			return new List<Vector2>();
		}

		if ( IsSimple( loop ) ) {
			return loop.ToList();
		}

		Bounds( loop, out var min, out var max );

		return Rect( min, max );
	}

	public static void Bounds( IReadOnlyList<Vector2> loop, out Vector2 min, out Vector2 max ) {
		min = new Vector2( float.MaxValue, float.MaxValue );
		max = new Vector2( float.MinValue, float.MinValue );

		foreach ( var point in loop ) {
			min = new Vector2( MathF.Min( min.x, point.x ), MathF.Min( min.y, point.y ) );
			max = new Vector2( MathF.Max( max.x, point.x ), MathF.Max( max.y, point.y ) );
		}
	}

	// Floor side as a sign: -1 = floor on right (outer), +1 = floor on left (hole).
	public static float FloorSide( IReadOnlyList<Vector2> loop ) {
		return SignedArea( loop ) > 0f ? -1f : 1f;
	}

	public static float SignedArea( IReadOnlyList<Vector2> loop ) {
		var total = 0f;

		for ( var index = 0; index < loop.Count; index++ ) {
			var current = loop[index];
			var next = loop[(index + 1) % loop.Count];

			total += current.x * next.y - next.x * current.y;
		}

		return total * 0.5f;
	}

	static bool Intersects( Vector2 a, Vector2 b, Vector2 c, Vector2 d ) {
		return MathF.Max( MathF.Min( a.x, b.x ), MathF.Min( c.x, d.x ) ) <= MathF.Min( MathF.Max( a.x, b.x ), MathF.Max( c.x, d.x ) ) + Grain
			&& MathF.Max( MathF.Min( a.y, b.y ), MathF.Min( c.y, d.y ) ) <= MathF.Min( MathF.Max( a.y, b.y ), MathF.Max( c.y, d.y ) ) + Grain
			&& Side( a, b, c ) * Side( a, b, d ) <= Grain
			&& Side( c, d, a ) * Side( c, d, b ) <= Grain;
	}

	static float Side( Vector2 a, Vector2 b, Vector2 point ) => (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x);

	static bool ProperlyCrosses( Vector2 a, Vector2 b, Vector2 c, Vector2 d ) {
		var first = Side( a, b, c );
		var second = Side( a, b, d );
		var third = Side( c, d, a );
		var fourth = Side( c, d, b );

		return (first > Grain && second < -Grain || first < -Grain && second > Grain)
			&& (third > Grain && fourth < -Grain || third < -Grain && fourth > Grain);
	}

	public static List<Vector2> Wind( List<Vector2> loop ) {
		if ( loop.Count < 3 || SignedArea( loop ) >= 0f ) {
			return loop;
		}

		loop.Reverse();

		return loop;
	}

	// Even-odd coverage cancels overlaps.
	public static List<List<Vector2>> Union( IEnumerable<IReadOnlyList<Vector2>> loops ) {
		var filled = Distinct( loops.Where( loop => loop is { Count: >= 3 } ).ToList() );

		// Rotated into the set's own frame where the cell grid is exact.
		if ( Framed( filled, out var pivot, out var bearing ) ) {
			return Turned( Union( Turned( filled, pivot, -bearing ) ), pivot, bearing );
		}

		if ( Slanted( filled ) ) {
			return Carved( filled, null );
		}

		var region = new List<IReadOnlyList<Vector2>>();

		foreach ( var loop in filled ) {
			region.AddRange( Subtract( new[] { loop }, region ) );
		}

		return Trace( Cells( region, null ) );
	}

	public static List<List<Vector2>> Intersect( IEnumerable<IReadOnlyList<Vector2>> loops, IReadOnlyList<Vector2> outline ) {
		var filled = loops.Where( loop => loop is { Count: >= 3 } ).ToList();

		if ( outline is not { Count: >= 3 } || filled.Count == 0 ) {
			return filled.Select( loop => loop.ToList() ).ToList();
		}

		if ( Framed( filled.Append( outline ), out var pivot, out var bearing ) ) {
			return Turned(
				Intersect( Turned( filled, pivot, -bearing ), Turned( outline, pivot, -bearing ) ),
				pivot, bearing );
		}

		if ( Slanted( filled ) || !IsRectilinear( outline ) ) {
			var spill = Subtract( filled, new[] { outline } )
				.Select( loop => (IReadOnlyList<Vector2>)loop )
				.ToList();

			return Subtract( filled, spill );
		}

		var donors = filled.SelectMany( loop => loop ).Concat( outline ).ToList();
		var xs = Axis( donors.Select( point => point.x ) );
		var ys = Axis( donors.Select( point => point.y ) );
		var cells = new List<ArchBox>();

		for ( var ix = 0; ix + 1 < xs.Count; ix++ ) {
			for ( var iy = 0; iy + 1 < ys.Count; iy++ ) {
				var min = new Vector2( xs[ix], ys[iy] );
				var max = new Vector2( xs[ix + 1], ys[iy + 1] );
				var centre = (min + max) * 0.5f;

				if ( Encloses( filled, centre ) && Contains( outline, centre ) ) {
					cells.Add( new ArchBox { Min = min, Max = max } );
				}
			}
		}

		return Trace( cells );
	}

	public static List<List<Vector2>> Subtract( IEnumerable<IReadOnlyList<Vector2>> loops, IReadOnlyList<IReadOnlyList<Vector2>> holes ) {
		var filled = loops.Where( loop => loop is { Count: >= 3 } ).ToList();

		if ( holes is not { Count: > 0 } ) {
			return filled.Select( loop => loop.ToList() ).ToList();
		}

		if ( Framed( filled.Concat( holes ), out var pivot, out var bearing ) ) {
			return Turned(
				Subtract( Turned( filled, pivot, -bearing ), Turned( holes, pivot, -bearing ) ),
				pivot, bearing );
		}

		if ( Slanted( filled ) || Slanted( holes ) ) {
			return Carved( filled, holes );
		}

		return Trace( Cells( filled, holes ) );
	}

	// Duplicate loops are dropped — coincident slanted rings break the carve's boundary walk.
	static List<IReadOnlyList<Vector2>> Distinct( List<IReadOnlyList<Vector2>> filled ) {
		var kept = new List<IReadOnlyList<Vector2>>();

		foreach ( var loop in filled ) {
			if ( !kept.Any( standing => Coincident( standing, loop ) ) ) {
				kept.Add( loop );
			}
		}

		return kept;
	}

	static bool Coincident( IReadOnlyList<Vector2> first, IReadOnlyList<Vector2> second ) {
		if ( first.Count != second.Count ) {
			return false;
		}

		for ( var index = 0; index < first.Count; index++ ) {
			if ( (first[index] - second[index]).Length > Grain ) {
				return false;
			}
		}

		return true;
	}

	// Diagonal edges fall through to ArchCarve — cell grid only handles axis-aligned.
	static bool Slanted( IEnumerable<IReadOnlyList<Vector2>> loops ) {
		return loops.Any( loop => loop is { Count: >= 3 } && !IsRectilinear( loop ) );
	}

	static List<List<Vector2>> Carved( IReadOnlyList<IReadOnlyList<Vector2>> filled, IReadOnlyList<IReadOnlyList<Vector2>> holes ) {
		var carve = new ArchCarve();

		foreach ( var loop in filled ) {
			carve.Plus( ArchCarveVolume.Over( loop, 0f, 1f ) );
		}

		foreach ( var loop in holes ?? Array.Empty<IReadOnlyList<Vector2>>() ) {
			carve.Less( ArchCarveVolume.Over( loop, -1f, 2f ) );
		}

		return carve.Resolve().Outline( 0.5f );
	}

	// Axis-aligned: cell dilation. Slanted: per-edge offset with mitred corners.
	public static List<List<Vector2>> Grow( IEnumerable<IReadOnlyList<Vector2>> loops, float amount ) {
		var filled = loops.Where( loop => loop is { Count: >= 3 } ).ToList();

		if ( filled.Count == 1 && !IsRectilinear( filled[0] ) ) {
			// Zero grow must return untouched — cell decomposition would square off diagonal edges.
			return amount <= Grain
				? new List<List<Vector2>> { filled[0].ToList() }
				: Offset( filled[0], amount );
		}

		if ( Framed( filled, out var pivot, out var bearing ) ) {
			return Turned( Grow( Turned( filled, pivot, -bearing ), amount ), pivot, bearing );
		}

		var cells = Cells( filled, null );

		if ( amount <= Grain ) {
			return Trace( cells );
		}

		return Trace( cells.Select( cell => cell.Grown( amount ) ).ToList() );
	}

	// Offset direction is off the material — outers grow outward, holes shrink inward.
	static List<List<Vector2>> Offset( IReadOnlyList<Vector2> loop, float amount ) {
		var moved = new List<Vector2>( loop.Count );

		for ( var index = 0; index < loop.Count; index++ ) {
			var behind = loop[(index - 1 + loop.Count) % loop.Count];
			var here = loop[index];
			var ahead = loop[(index + 1) % loop.Count];

			moved.Add( Mitred( behind, here, ahead, amount ) );
		}

		var offset = Simplify( moved );

		return offset.Count >= 3 && SignedArea( offset ) * SignedArea( loop ) > 0f
			? new List<List<Vector2>> { offset }
			: new List<List<Vector2>>();
	}

	// Mitre at the intersection of two offset edges; bevelled if too sharp.
	static Vector2 Mitred( Vector2 behind, Vector2 here, Vector2 ahead, float amount ) {
		var into = ArchRegion.Outward( behind, here ) * amount;
		var away = ArchRegion.Outward( here, ahead ) * amount;
		var turn = (here - behind).Normal;
		var next = (ahead - here).Normal;
		var cross = turn.x * next.y - turn.y * next.x;

		if ( MathF.Abs( cross ) < 0.0001f ) {
			return here + into;
		}

		var reach = ((here + away) - (here + into)).x * next.y - ((here + away) - (here + into)).y * next.x;
		var mitre = here + into + turn * (reach / cross);

		return (mitre - here).Length > amount * MitreReach ? here + (into + away) * 0.5f : mitre;
	}

	const float MitreReach = 4f;

	public static List<List<Vector2>> Shrink( IEnumerable<IReadOnlyList<Vector2>> loops, float amount ) {
		var filled = loops.Where( loop => loop is { Count: >= 3 } ).ToList();

		if ( filled.Count == 0 || amount <= Grain ) {
			return filled.Select( loop => loop.ToList() ).ToList();
		}

		Bounds( filled.SelectMany( loop => loop ).ToList(), out var min, out var max );

		var margin = new Vector2( amount * 3f, amount * 3f );
		var around = Subtract( new[] { Rect( min - margin, max + margin ) }, filled );

		return Subtract( filled, Grow( around, amount ) );
	}

	public static IEnumerable<List<Vector2>> Outer( IEnumerable<List<Vector2>> region ) {
		return region.Where( loop => loop.Count >= 3 && SignedArea( loop ) > 0f );
	}

	public static List<Vector2> Merge( IReadOnlyList<Vector2> outline, IReadOnlyList<Vector2> addition ) {
		var merged = Union( new[] { outline, addition } );
		var outer = Outer( merged ).Where( IsSimple ).ToList();

		if ( outer.Count == 1 ) {
			return outer[0];
		}

		return null;
	}

	public static bool Overlaps( ArchBox first, ArchBox second ) {
		return first.Min.x < second.Max.x - Grain && first.Max.x > second.Min.x + Grain
			&& first.Min.y < second.Max.y - Grain && first.Max.y > second.Min.y + Grain;
	}

	public static bool Overlaps( IReadOnlyList<Vector2> first, IReadOnlyList<Vector2> second ) {
		if ( first is not { Count: >= 3 } || second is not { Count: >= 3 } ) {
			return false;
		}

		Bounds( first, out var firstMin, out var firstMax );
		Bounds( second, out var secondMin, out var secondMax );

		if ( firstMax.x <= secondMin.x + Grain || secondMax.x <= firstMin.x + Grain
			|| firstMax.y <= secondMin.y + Grain || secondMax.y <= firstMin.y + Grain ) {
			return false;
		}

		if ( first.Any( point => Contains( second, point ) ) || second.Any( point => Contains( first, point ) ) ) {
			return true;
		}

		for ( var firstIndex = 0; firstIndex < first.Count; firstIndex++ ) {
			var firstFrom = first[firstIndex];
			var firstTo = first[(firstIndex + 1) % first.Count];

			for ( var secondIndex = 0; secondIndex < second.Count; secondIndex++ ) {
				var secondFrom = second[secondIndex];
				var secondTo = second[(secondIndex + 1) % second.Count];

				if ( ProperlyCrosses( firstFrom, firstTo, secondFrom, secondTo ) ) {
					return true;
				}
			}
		}

		return false;
	}

	public static List<ArchBox> Cells( IEnumerable<IReadOnlyList<Vector2>> loops, IReadOnlyList<IReadOnlyList<Vector2>> holes ) {
		var filled = loops.Where( loop => loop is { Count: >= 3 } ).ToList();
		var removed = (holes ?? Array.Empty<IReadOnlyList<Vector2>>()).Where( loop => loop is { Count: >= 3 } ).ToList();
		var cells = new List<ArchBox>();

		if ( filled.Count == 0 ) {
			return cells;
		}

		var donors = filled.Concat( removed ).SelectMany( loop => loop ).ToList();
		var xs = Axis( donors.Select( point => point.x ) );
		var ys = Axis( donors.Select( point => point.y ) );

		for ( var ix = 0; ix + 1 < xs.Count; ix++ ) {
			for ( var iy = 0; iy + 1 < ys.Count; iy++ ) {
				var min = new Vector2( xs[ix], ys[iy] );
				var max = new Vector2( xs[ix + 1], ys[iy + 1] );
				var centre = (min + max) * 0.5f;

				if ( Encloses( filled, centre ) && !Encloses( removed, centre ) ) {
					cells.Add( new ArchBox { Min = min, Max = max } );
				}
			}
		}

		return cells;
	}

	public static bool Contains( IReadOnlyList<Vector2> loop, Vector2 point ) {
		if ( loop is not { Count: >= 3 } ) {
			return false;
		}

		var inside = false;

		for ( int index = 0, previous = loop.Count - 1; index < loop.Count; previous = index++ ) {
			var a = loop[index];
			var b = loop[previous];

			if ( a.y > point.y != b.y > point.y
				&& point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x ) {
				inside = !inside;
			}
		}

		return inside;
	}

	public static bool Covered(
		IReadOnlyList<IReadOnlyList<Vector2>> region,
		IReadOnlyList<ArchFloorCutout> cutouts,
		Vector2 point ) {
		return Encloses( region, point ) && !cutouts.Any( cutout => cutout.Contains( point ) );
	}

	public static bool Encloses( IReadOnlyList<IReadOnlyList<Vector2>> loops, Vector2 point ) {
		var inside = false;

		foreach ( var loop in loops ) {
			if ( Contains( loop, point ) ) {
				inside = !inside;
			}
		}

		return inside;
	}

	// Solved, not sampled — stepping quantises boundaries.
	public static IEnumerable<float> Crossings( IReadOnlyList<Vector2> loop, Vector2 from, Vector2 to ) {
		var span = to - from;

		for ( var index = 0; index < loop.Count; index++ ) {
			var corner = loop[index];
			var edge = loop[(index + 1) % loop.Count] - corner;
			var turn = span.x * edge.y - span.y * edge.x;

			if ( MathF.Abs( turn ) < 1e-6f ) {
				continue;
			}

			var offset = corner - from;
			var alongSpan = (offset.x * edge.y - offset.y * edge.x) / turn;
			var alongEdge = (offset.x * span.y - offset.y * span.x) / turn;

			if ( alongSpan > 0f && alongSpan < 1f && alongEdge >= 0f && alongEdge <= 1f ) {
				yield return alongSpan;
			}
		}
	}

	public static IEnumerable<(float From, float To)> Inside( IReadOnlyList<Vector2> loop, Vector2 from, Vector2 to ) {
		if ( loop is not { Count: >= 3 } ) {
			yield break;
		}

		var cuts = new List<float> { 0f, 1f };

		cuts.AddRange( Crossings( loop, from, to ) );
		cuts.Sort();

		for ( var index = 0; index + 1 < cuts.Count; index++ ) {
			var start = cuts[index];
			var end = cuts[index + 1];

			if ( end - start > Grain && Contains( loop, Vector2.Lerp( from, to, (start + end) * 0.5f ) ) ) {
				yield return (start, end);
			}
		}
	}

	public static List<List<Vector2>> Trace( IReadOnlyList<ArchBox> boxes ) {
		if ( boxes.Count == 0 ) {
			return new List<List<Vector2>>();
		}

		var xs = Axis( boxes.SelectMany( box => new[] { box.Min.x, box.Max.x } ) );
		var ys = Axis( boxes.SelectMany( box => new[] { box.Min.y, box.Max.y } ) );
		var columns = xs.Count - 1;
		var covered = new bool[columns * (ys.Count - 1)];

		foreach ( var box in boxes ) {
			var fromX = Index( xs, box.Min.x );
			var toX = Index( xs, box.Max.x );
			var fromY = Index( ys, box.Min.y );
			var toY = Index( ys, box.Max.y );

			for ( var ix = fromX; ix < toX; ix++ ) {
				for ( var iy = fromY; iy < toY; iy++ ) {
					covered[iy * columns + ix] = true;
				}
			}
		}

		return Trace( xs, ys, covered );
	}

	public static List<List<Vector2>> Trace( List<float> xs, List<float> ys, bool[] covered ) {
		var columns = xs.Count - 1;
		var rows = ys.Count - 1;
		var segments = new Dictionary<int, List<int>>();

		bool Covered( int ix, int iy ) {
			return ix >= 0 && iy >= 0 && ix < columns && iy < rows && covered[iy * columns + ix];
		}

		int Node( int ix, int iy ) => iy * (columns + 1) + ix;

		void Segment( int fromX, int fromY, int toX, int toY ) {
			var key = Node( fromX, fromY );

			if ( !segments.TryGetValue( key, out var ends ) ) {
				ends = new List<int>();
				segments[key] = ends;
			}

			ends.Add( Node( toX, toY ) );
		}

		for ( var ix = 0; ix < columns; ix++ ) {
			for ( var iy = 0; iy < rows; iy++ ) {
				if ( !Covered( ix, iy ) ) {
					continue;
				}

				if ( !Covered( ix, iy - 1 ) ) Segment( ix, iy, ix + 1, iy );
				if ( !Covered( ix + 1, iy ) ) Segment( ix + 1, iy, ix + 1, iy + 1 );
				if ( !Covered( ix, iy + 1 ) ) Segment( ix + 1, iy + 1, ix, iy + 1 );
				if ( !Covered( ix - 1, iy ) ) Segment( ix, iy + 1, ix, iy );
			}
		}

		var loops = new List<List<Vector2>>();

		while ( segments.Count > 0 ) {
			var start = segments.Keys.First();
			var nodes = new List<int>();
			var at = start;

			while ( segments.TryGetValue( at, out var ends ) && ends.Count > 0 ) {
				var next = ends[0];
				ends.RemoveAt( 0 );

				if ( ends.Count == 0 ) {
					segments.Remove( at );
				}

				nodes.Add( at );
				at = next;

				if ( at == start ) {
					break;
				}
			}

			var loop = Simplify( nodes.Select( node => new Vector2( xs[node % (columns + 1)], ys[node / (columns + 1)] ) ).ToList() );

			if ( loop.Count >= 4 ) {
				loops.Add( loop );
			}
		}

		return loops;
	}

	static List<Vector2> Simplify( List<Vector2> loop ) {
		var kept = new List<Vector2>();

		for ( var index = 0; index < loop.Count; index++ ) {
			var previous = loop[(index - 1 + loop.Count) % loop.Count];
			var current = loop[index];
			var next = loop[(index + 1) % loop.Count];

			var into = current - previous;
			var outOf = next - current;

			if ( MathF.Abs( into.x * outOf.y - into.y * outOf.x ) > Grain ) {
				kept.Add( current );
			}
		}

		return kept;
	}

	static List<float> Axis( IEnumerable<float> values ) {
		return values.Select( ArchGridService.Fine ).Distinct().OrderBy( value => value ).ToList();
	}

	static int Index( List<float> axis, float value ) {
		var found = axis.BinarySearch( ArchGridService.Fine( value ) );

		return found < 0 ? ~found : found;
	}
}