Editor/Services/ArchWingPlacementService.cs

Editor service that snaps and assigns roof placement for an ArchWing (building wing) in the editor. It snaps a rectangular placement to nearby outline lines from storey loops, enforces minimum size, and finds the best hosting roof by overlap area.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

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

	public ArchWingPlacementService( 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;
	}
}