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.
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;
}
}