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

namespace Sunless.Architecture;

sealed class ArchWingPlacement {
	readonly ArchBuilding building;
	readonly ArchKit kit;
	readonly int level;

	public ArchWingPlacement( ArchBuilding building, ArchKit kit, int level ) {
		this.building = building;
		this.kit = kit;
		this.level = level;
	}

	public (Vector2 Min, Vector2 Max) Snap( Vector2 min, Vector2 max ) {
		var loops = ArchRegion.Storey( building, level );

		if ( loops.Count == 0 ) {
			return (min, max);
		}

		var xs = loops.SelectMany( loop => loop.Select( point => point.x ) ).ToList();
		var ys = loops.SelectMany( loop => loop.Select( point => point.y ) ).ToList();
		var reach = MathF.Max( 1f, kit.WallThickness );
		var snapped = (Min: Nearest( min.x, xs, reach ), MinY: Nearest( min.y, ys, reach ), Max: Nearest( max.x, xs, reach ), MaxY: Nearest( max.y, ys, reach ));

		if ( snapped.Max - snapped.Min < 16f || snapped.MaxY - snapped.MinY < 16f ) {
			return (min, max);
		}

		return (new Vector2( snapped.Min, snapped.MinY ), new Vector2( snapped.Max, snapped.MaxY ));
	}

	public ArchRoofPart Host( Vector2 min, Vector2 max ) {
		return building.Roofs
			.Where( roof => roof.Level == level )
			.Select( roof => (Roof: roof, Area: OverlapArea( roof.Outline(), min, max )) )
			.Where( candidate => candidate.Area > 0f )
			.OrderByDescending( candidate => candidate.Area )
			.Select( candidate => candidate.Roof )
			.FirstOrDefault();
	}

	static float OverlapArea( IReadOnlyList<Vector2> outline, Vector2 min, Vector2 max ) {
		ArchFootprint.Bounds( outline, out var lo, out var hi );
		var across = MathF.Min( hi.x, max.x ) - MathF.Max( lo.x, min.x );
		var along = MathF.Min( hi.y, max.y ) - MathF.Max( lo.y, min.y );

		if ( across < -1f || along < -1f || (across < 1f && along < 1f) ) {
			return 0f;
		}

		return MathF.Max( 1f, MathF.Max( 0f, across ) * MathF.Max( 0f, along ) );
	}

	static float Nearest( float value, IReadOnlyList<float> lines, float reach ) {
		var best = value;
		var gap = reach;

		foreach ( var line in lines ) {
			var distance = MathF.Abs( line - value );

			if ( distance < gap ) {
				gap = distance;
				best = line;
			}
		}

		return best;
	}
}