Editor/Services/ArchWingGenerationService.cs

Editor service that generates an architectural wing section for a building plan. It resolves placement, creates a room, opens connections to the host building, optionally merges footprints or creates a roof, and returns an ArchSection describing the new wing.

File AccessNative Interop
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchWingGenerationService
{
	readonly ArchPlan plan;
	readonly ArchBuilding building;
	readonly ArchKit kit;
	int level;
	float baseHeight;
	Vector2 min;
	Vector2 max;
	SectionRoof choice;
	float drop;
	bool floor;
	bool gutters;
	RidgeRun ridge;

	public ArchWingGenerationService( ArchPlan plan, ArchBuilding building, ArchKit kit )
	{
		this.plan = plan;
		this.building = building;
		this.kit = kit;
	}

	public ArchWingGenerationService On( int level, float baseHeight, Vector2 min, Vector2 max )
	{
		this.level = level;
		this.baseHeight = baseHeight;
		this.min = min;
		this.max = max;
		return this;
	}

	public ArchWingGenerationService WithRoof( SectionRoof choice, float drop, bool gutters, RidgeRun ridge )
	{
		this.choice = choice;
		this.drop = drop;
		this.gutters = gutters;
		this.ridge = ridge;
		return this;
	}

	public ArchWingGenerationService WithFloor( bool floor )
	{
		this.floor = floor;
		return this;
	}

	public ArchSection Create()
	{
		var resolved = new ArchBoundaryPlacementService( plan, kit ).Outside( level, min, max, building );

		if ( !resolved.IsUsable || !resolved.TouchesHost )
		{
			return null;
		}

		min = resolved.Min;
		max = resolved.Max;
		var placement = new ArchWingPlacementService( building, kit, level );

		var continuing = choice == SectionRoof.Continue;
		var host = continuing ? placement.Host( min, max ) : null;
		var merge = host is not null;
		var wallHeight = merge
			? MathF.Max( 32f, host.BaseHeight - baseHeight )
			: MathF.Max( 32f, kit.WallHeight - (continuing ? 0f : MathF.Max( 0f, drop )) );
		var party = new List<(Vector2 From, Vector2 To)>();
		var wing = ArchBuild.CreateRoom( plan, building, $"Wing{building.Rooms.Count}", level, baseHeight, wallHeight, min, max, floor, party, kit.WallThickness );
		var connections = new ArchConnectionService( plan, kit );

		foreach ( var edge in party )
		{
			connections.OpenRooms( wing, building, edge.From, edge.To, wallHeight, wing.Floor );
		}

		// The breach took the host's wall out of the span, so the wing's own must not stand in its place.
		foreach ( var (from, to) in party )
		{
			var partyWall = wing.Walls.FirstOrDefault( wall => SameRun( wall, from, to ) );

			if ( partyWall is not null )
			{
				plan.Unfile( partyWall );
			}
		}

		connections.NormalizeBoundaries();

		if ( merge )
		{
			var merged = ArchFootprint.Merge( host.Outline(), ArchFootprint.Rect( min, max ) );

			if ( merged is not null )
			{
				host.Reshape( merged );
				host.Style = host.Style == RoofStyle.Flat ? RoofStyle.Flat : RoofStyle.Hip;

				return new ArchSection { Room = wing, Roof = host, SharedWalls = party.Count, Merged = true };
			}
		}

		if ( choice == SectionRoof.None )
		{
			return new ArchSection { Room = wing, SharedWalls = party.Count };
		}

		var roof = ArchBuild.Roof( plan, kit, level, ArchBuild.Winged( choice ), min, max, baseHeight + wallHeight, gutters, choice == SectionRoof.Capped, ridge );
		plan.File( ArchKind.Roof, building, roof );

		return new ArchSection { Room = wing, Roof = roof, SharedWalls = party.Count };
	}

	static bool SameRun( ArchWall wall, Vector2 from, Vector2 to )
	{
		return ( wall.Start - from ).Length <= 0.01f && ( wall.End - to ).Length <= 0.01f
			|| ( wall.Start - to ).Length <= 0.01f && ( wall.End - from ).Length <= 0.01f;
	}
}