Editor/Data/ArchBuild.cs

Editor-side architecture builder utilities. It creates and places rooms, roofs and wings for building plans, computes placement/snapping, detects overlaps and builds ArchSection/ArchRoofPart/ArchRoom objects.

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

namespace Sunless.Architecture;

// Auto lays the ridge along the longer side, so the flat ends land on the short walls.
public enum RidgeRun
{
	Auto,
	AlongX,
	AlongY
}

public enum SectionRoof
{
	Continue,
	Hip,
	Gable,
	Capped,
	None
}

public sealed class ArchSection
{
	public ArchRoom Room { get; init; }
	public ArchRoofPart Roof { get; init; }
	public int SharedWalls { get; init; }
	public bool Merged { get; init; }
}

// Shared by the Building subtool and the arch_* tools - both grow a plan the same way.
public static class ArchBuild
{
	public static ArchSection Shell(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		bool floor,
		RoofStyle? style,
		bool gutters,
		RidgeRun ridge = RidgeRun.Auto )
	{
		var placement = new ArchBoundaryPlacementService( plan, kit ).Outside( level, min, max );

		if ( !placement.IsUsable )
		{
			return null;
		}

		min = placement.Min;
		max = placement.Max;
		var shell = CreateRoom( plan, building, "Shell", level, baseHeight, kit.WallHeight, min, max, floor, null, kit.WallThickness );

		if ( style is null )
		{
			return new ArchSection { Room = shell };
		}

		var roof = Roof( plan, kit, level, style.Value, min, max, baseHeight + kit.WallHeight, gutters, false, ridge );
		building.Roofs.Add( roof );

		return new ArchSection { Room = shell, Roof = roof };
	}

	public static ArchSection Partition(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		bool floor )
	{
		( min, max ) = new ArchGridService().Rectangle( min, max );
		var party = new List<(Vector2 From, Vector2 To)>();
		var room = CreateRoom( plan, building, $"Room{building.Rooms.Count + 1}", level, baseHeight, kit.WallHeight, min, max, floor, party, kit.WallThickness );

		return new ArchSection { Room = room, SharedWalls = party.Count };
	}

	// Two buildings on one plot each roof their own storey and the pair clip each other into nonsense.
	public static bool Stacks( ArchBuilding building, ArchKit kit, Vector2 min, Vector2 max )
	{
		if ( building is null || !building.HasContent )
		{
			return false;
		}

		var covered = ArchRegion.Shell( ArchRegion.Footprints( building.Rooms ), kit.WallThickness );

		return ArchRegion.Covers( covered, ArchFootprint.Rect( min, max ) );
	}

	public static ArchSection Storey(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		bool floor,
		RoofStyle? style,
		bool gutters,
		RidgeRun ridge = RidgeRun.Auto )
	{
		( min, max ) = new ArchGridService().Rectangle( min, max );
		var snapped = new ArchWingPlacementService( building, kit, level ).Snap( min, max );
		min = snapped.Min;
		max = snapped.Max;

		var room = CreateRoom( plan, building, $"Storey{level}", level, baseHeight, kit.WallHeight, min, max, floor, null, kit.WallThickness );
		var plate = baseHeight + kit.WallHeight;
		var outline = ArchFootprint.Rect( min, max );

		if ( DeckBuiltOverBy( building, outline ) is { } below )
		{
			below.Level = level;
			below.BaseHeight = plate;
			below.Reshape( outline );

			return new ArchSection { Room = room, Roof = below, Merged = true };
		}

		if ( style is null )
		{
			return new ArchSection { Room = room };
		}

		var roof = Roof( plan, kit, level, style.Value, min, max, plate, gutters, false, ridge );
		building.Roofs.Add( roof );

		return new ArchSection { Room = room, Roof = roof };
	}

	// Only a FULLY covered deck is rebuilt - a partial one is a real stepped building.
	static ArchRoofPart DeckBuiltOverBy( ArchBuilding building, List<Vector2> outline )
	{
		return building.Roofs.FirstOrDefault( roof => ArchRegion.Covers( new[] { outline }, roof.Outline() ) );
	}

	// Continuing folds the wing into the section's footprint (valleys inside); otherwise its own roof, stepped by the eave drop.
	public static ArchSection Extend(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		SectionRoof choice,
		float drop,
		bool floor,
		bool gutters,
		RidgeRun ridge = RidgeRun.Auto )
	{
		( min, max ) = new ArchGridService().Rectangle( min, max );

		return new ArchWingGenerationService( plan, building, kit )
			.On( level, baseHeight, min, max )
			.WithRoof( choice, drop, gutters, ridge )
			.WithFloor( floor )
			.Create();
	}

	public static ArchRoofPart Roof(
		ArchPlan plan,
		ArchKit kit,
		int level,
		RoofStyle style,
		Vector2 min,
		Vector2 max,
		float baseHeight,
		bool gutters,
		bool parapet,
		RidgeRun ridge = RidgeRun.Auto )
	{
		return new ArchRoofPart
		{
			Id = plan.AllocateId(),
			Name = "Roof",
			Level = level,
			Style = style,
			Min = min,
			Max = max,
			Footprint = ArchFootprint.Rect( min, max ),
			BaseHeight = baseHeight,
			Pitch = kit.RoofPitch,
			RidgeAlongX = Ridged( ridge, min, max ),
			// A parapet stands on the wall line - an overhang would leave the coping floating.
			Overhang = parapet ? 0f : kit.RoofOverhang,
			Thickness = kit.RoofThickness,
			Gutters = gutters && !parapet,
			Fascia = !parapet,
			Soffit = !parapet,
			Parapet = parapet,
			Ceiling = Ceiling( style )
		};
	}

	// Shared with the ghost, so what is drawn is what gets built.
	public static bool Ridged( RidgeRun ridge, Vector2 min, Vector2 max ) => ridge switch
	{
		RidgeRun.AlongX => true,
		RidgeRun.AlongY => false,
		_ => max.x - min.x >= max.y - min.y
	};

	// Hip/gable leave a void, so they get a ceiling; shed/sawtooth are meant to be seen from underneath.
	static bool Ceiling( RoofStyle style )
	{
		return style is RoofStyle.Hip or RoofStyle.Gable;
	}

	// The one mapping for a wing's roof choice - the ghost, the generator and the re-dress must agree.
	public static RoofStyle Winged( SectionRoof choice )
	{
		return choice switch
		{
			SectionRoof.Gable => RoofStyle.Gable,
			SectionRoof.Capped => RoofStyle.Flat,
			_ => RoofStyle.Hip
		};
	}

	internal static ArchRoom CreateRoom(
		ArchPlan plan,
		ArchBuilding building,
		string name,
		int level,
		float baseHeight,
		float wallHeight,
		Vector2 min,
		Vector2 max,
		bool floor,
		List<(Vector2 From, Vector2 To)> shared,
		float thickness )
	{
		var room = new ArchRoom
		{
			Id = plan.AllocateId(),
			Name = name,
			Floor = level,
			BaseHeight = baseHeight,
			WallHeight = wallHeight,
			HasFloor = floor,
			Footprint = ArchFootprint.Rect( min, max )
		};

		for ( var index = 0; index < room.Footprint.Count; index++ )
		{
			var start = room.Footprint[index];
			var end = room.Footprint[(index + 1) % room.Footprint.Count];

			room.Walls.Add( new ArchWall
			{
				Id = plan.AllocateId(),
				Start = start,
				End = end,
				Exterior = true,
				Cap = true
			} );

			if ( shared is not null && ArchWallJoins.PartyWallExists( building, room.BaseHeight, start, end, thickness ) )
			{
				shared.Add( (start, end) );
			}
		}

		building.Rooms.Add( room );

		return room;
	}

}