Editor service that computes valid rectangular placements and plots for architecture building placement. It aligns and trims a requested rectangle against occupied cells, checks usability, finds abutting host buildings, and grows/adjusts plots to meet minimum spans.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public sealed class ArchBoundaryPlacementService
{
readonly ArchPlan plan;
readonly ArchKit kit;
readonly ArchGridService grid;
public ArchBoundaryPlacementService( ArchPlan plan, ArchKit kit )
{
this.plan = plan ?? throw new ArgumentNullException( nameof( plan ) );
this.kit = kit ?? throw new ArgumentNullException( nameof( kit ) );
grid = new ArchGridService();
}
public ArchRectanglePlacement Outside( int level, Vector2 from, Vector2 to, ArchBuilding staged = null )
{
from = grid.Base( from );
to = grid.Base( to );
var requested = grid.Rectangle( from, to );
var blockers = OccupiedCells( level, staged );
var aligned = Align( requested, blockers );
var resolved = Trim( aligned, from, to, blockers );
var usable = IsUsable( resolved );
var host = usable ? Abutted( level, resolved, staged ) : null;
return new ArchRectanglePlacement
{
Min = resolved.Min,
Max = resolved.Max,
Adjusted = resolved.Min != requested.Min || resolved.Max != requested.Max,
Touches = usable && blockers.Any( blocker => Touches( resolved, blocker ) ),
TouchesHost = host is not null,
IsUsable = usable,
Host = host
};
}
// Staged leads only when the drag reaches it - pointing at a building is how you say which you meant.
ArchBuilding Abutted( int level, ArchBox resolved, ArchBuilding staged )
{
if ( staged is not null && Abuts( level, resolved, staged ) )
{
return staged;
}
return plan.Buildings.FirstOrDefault( building => !ReferenceEquals( building, staged ) && Abuts( level, resolved, building ) );
}
bool Abuts( int level, ArchBox resolved, ArchBuilding building )
{
return OccupiedCells( level, new[] { building } ).Any( blocker => Touches( resolved, blocker ) );
}
public ArchPlot PlotOutside(
int level,
Vector2 from,
Vector2 to,
Vector2 minimum,
bool flipX,
bool flipY,
out bool usable )
{
from = grid.Base( from );
to = grid.Base( to );
var required = new Vector2( MinimumSpan( minimum.x ), MinimumSpan( minimum.y ) );
var min = Vector2.Min( from, to );
var max = Vector2.Max( from, to );
var x = Grow( min.x, max.x, required.x, from.x, to.x );
var y = Grow( min.y, max.y, required.y, from.y, to.y );
min = new Vector2( x.Min, y.Min );
max = new Vector2( x.Max, y.Max );
var requested = new ArchPlot { Min = min, Max = max, FlipX = flipX, FlipY = flipY };
var placement = Outside( level, min, max );
var size = placement.Max - placement.Min;
usable = placement.IsUsable && size.x >= required.x && size.y >= required.y;
return usable
? new ArchPlot { Min = placement.Min, Max = placement.Max, FlipX = flipX, FlipY = flipY }
: requested;
}
float MinimumSpan( float value )
{
return MathF.Ceiling( MathF.Max( grid.BaseSize, value ) / grid.BaseSize ) * grid.BaseSize;
}
static (float Min, float Max) Grow( float min, float max, float required, float from, float to )
{
if ( max - min >= required )
{
return (min, max);
}
if ( to < from )
{
return (max - required, max);
}
return (min, min + required);
}
List<ArchBox> OccupiedCells( int level, ArchBuilding staged )
{
var buildings = staged is null
? plan.Buildings
: plan.Buildings.Concat( new[] { staged } ).Distinct();
return OccupiedCells( level, buildings );
}
static List<ArchBox> OccupiedCells( int level, IEnumerable<ArchBuilding> buildings )
{
var footprints = buildings
.SelectMany( building => building.Rooms )
.Where( room => room.Floor == level && room.HasFootprint )
.Select( ArchFloorGen.Footprint )
.Where( footprint => footprint.Count >= 3 )
.ToList();
if ( footprints.Count == 0 )
{
return new List<ArchBox>();
}
return ArchFootprint.Cells( ArchFootprint.Union( footprints ), null );
}
ArchBox Align( (Vector2 Min, Vector2 Max) requested, IReadOnlyList<ArchBox> blockers )
{
var candidate = new ArchBox { Min = requested.Min, Max = requested.Max };
var reach = MathF.Max( grid.BaseSize, kit.WallThickness );
foreach ( var blocker in blockers )
{
var min = candidate.Min;
var max = candidate.Max;
if ( Overlaps( min.y, max.y, blocker.Min.y, blocker.Max.y ) )
{
min.x = Nearby( min.x, blocker.Min.x, blocker.Max.x, reach );
max.x = Nearby( max.x, blocker.Min.x, blocker.Max.x, reach );
}
if ( Overlaps( min.x, max.x, blocker.Min.x, blocker.Max.x ) )
{
min.y = Nearby( min.y, blocker.Min.y, blocker.Max.y, reach );
max.y = Nearby( max.y, blocker.Min.y, blocker.Max.y, reach );
}
if ( max.x - min.x >= grid.BaseSize && max.y - min.y >= grid.BaseSize )
{
candidate = new ArchBox { Min = min, Max = max };
}
}
return candidate;
}
ArchBox Trim( ArchBox requested, Vector2 from, Vector2 to, IReadOnlyList<ArchBox> blockers )
{
var candidate = requested;
var attempts = Math.Max( 1, blockers.Count * 4 );
for ( var attempt = 0; attempt < attempts; attempt++ )
{
var blocker = blockers
.Where( entry => OverlapArea( candidate, entry ) > 0f )
.OrderByDescending( entry => OverlapArea( candidate, entry ) )
.FirstOrDefault();
if ( OverlapArea( candidate, blocker ) <= 0f )
{
return candidate;
}
var choices = Cuts( candidate, blocker )
.Where( IsUsable )
.OrderByDescending( choice => Contains( choice, to ) ? 2 : Contains( choice, from ) ? 1 : 0 )
.ThenBy( choice => blockers.Sum( entry => OverlapArea( choice, entry ) ) )
.ThenByDescending( Area )
.ToList();
if ( choices.Count == 0 )
{
return new ArchBox();
}
candidate = choices[0];
}
return blockers.Any( blocker => OverlapArea( candidate, blocker ) > 0f )
? new ArchBox()
: candidate;
}
IEnumerable<ArchBox> Cuts( ArchBox candidate, ArchBox blocker )
{
if ( candidate.Min.x < blocker.Min.x )
{
yield return new ArchBox { Min = candidate.Min, Max = new Vector2( blocker.Min.x, candidate.Max.y ) };
}
if ( candidate.Max.x > blocker.Max.x )
{
yield return new ArchBox { Min = new Vector2( blocker.Max.x, candidate.Min.y ), Max = candidate.Max };
}
if ( candidate.Min.y < blocker.Min.y )
{
yield return new ArchBox { Min = candidate.Min, Max = new Vector2( candidate.Max.x, blocker.Min.y ) };
}
if ( candidate.Max.y > blocker.Max.y )
{
yield return new ArchBox { Min = new Vector2( candidate.Min.x, blocker.Max.y ), Max = candidate.Max };
}
}
bool IsUsable( ArchBox candidate )
{
return candidate.Max.x - candidate.Min.x >= grid.BaseSize
&& candidate.Max.y - candidate.Min.y >= grid.BaseSize;
}
static float Nearby( float value, float first, float second, float reach )
{
var firstGap = MathF.Abs( value - first );
var secondGap = MathF.Abs( value - second );
if ( firstGap <= secondGap && firstGap <= reach )
{
return first;
}
return secondGap <= reach ? second : value;
}
static bool Overlaps( float firstMin, float firstMax, float secondMin, float secondMax )
{
return MathF.Min( firstMax, secondMax ) - MathF.Max( firstMin, secondMin ) > 0f;
}
static float OverlapArea( ArchBox first, ArchBox second )
{
var width = MathF.Min( first.Max.x, second.Max.x ) - MathF.Max( first.Min.x, second.Min.x );
var height = MathF.Min( first.Max.y, second.Max.y ) - MathF.Max( first.Min.y, second.Min.y );
return MathF.Max( 0f, width ) * MathF.Max( 0f, height );
}
static float Area( ArchBox box )
{
return MathF.Max( 0f, box.Max.x - box.Min.x ) * MathF.Max( 0f, box.Max.y - box.Min.y );
}
static bool Contains( ArchBox box, Vector2 point )
{
return point.x >= box.Min.x && point.x <= box.Max.x
&& point.y >= box.Min.y && point.y <= box.Max.y;
}
static bool Touches( ArchBox first, ArchBox second )
{
var vertical = (first.Max.x == second.Min.x || first.Min.x == second.Max.x)
&& Overlaps( first.Min.y, first.Max.y, second.Min.y, second.Max.y );
var horizontal = (first.Max.y == second.Min.y || first.Min.y == second.Max.y)
&& Overlaps( first.Min.x, first.Max.x, second.Min.x, second.Max.x );
return vertical || horizontal;
}
}