Editor/Walkway/ArchWalkwayGenerationService.cs

Editor service that generates walkway (bridge) architecture pieces. It computes geometry, creates an ArchRoom and roof deck, files them into the plan, and connects endpoints; supports rising or flat walkways with options for piers, skirting and cornice.

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

namespace Sunless.Architecture;


sealed class ArchWalkwayGenerationService
{
	readonly ArchPlan plan;
	readonly ArchBuilding building;
	readonly ArchKit kit;
	readonly ArchConnectionService connections;
	readonly ArchWalkwayGeometryService geometry;
	readonly ArchKinds kinds;
	int level;
	float baseHeight;
	Vector2 dragMin;
	Vector2 dragMax;
	Vector2 dragFrom;
	float width;
	float height;
	ArchPillarPart pier;
	int rise;
	WalkwayInterior interior = WalkwayInterior.None;
	bool cornice = true;
	bool skirting = true;

	public ArchWalkwayGenerationService( ArchPlan plan, ArchBuilding building, ArchKit kit )
	{
		this.plan = plan;
		this.building = building;
		this.kit = kit;
		connections = new ArchConnectionService( plan, kit );
		geometry = new ArchWalkwayGeometryService( connections );
		kinds = ArchKinds.Load();
	}

	public ArchWalkwayGenerationService Between( int level, float baseHeight, Vector2 dragMin, Vector2 dragMax, Vector2 dragFrom )
	{
		this.level = level;
		this.baseHeight = baseHeight;
		this.dragMin = dragMin;
		this.dragMax = dragMax;
		this.dragFrom = dragFrom;
		return this;
	}

	public ArchWalkwayGenerationService WithWidth( float width )
	{
		this.width = width;
		return this;
	}

	public ArchWalkwayGenerationService WithHeight( float height )
	{
		this.height = height;
		return this;
	}

	public ArchWalkwayGenerationService WithPiers( ArchPillarPart pier )
	{
		this.pier = pier;
		return this;
	}

	public ArchWalkwayGenerationService WithRise( int rise )
	{
		this.rise = rise;
		return this;
	}

	public ArchWalkwayGenerationService WithInterior( WalkwayInterior interior )
	{
		this.interior = interior;
		return this;
	}

	public ArchWalkwayGenerationService WithCornice( bool cornice )
	{
		this.cornice = cornice;
		return this;
	}

	public ArchWalkwayGenerationService WithSkirting( bool skirting )
	{
		this.skirting = skirting;
		return this;
	}

	public ArchWalkway.Link Create()
	{
		if ( building is null )
		{
			return null;
		}

		var span = geometry.Resolve( level, rise, dragFrom, dragMin, dragMax, width );

		if ( span is null )
		{
			return null;
		}

		var clear = MathF.Max( 60f, height );
		var connection = new ArchWalkwayConnectionService( connections, building, span.Mouths, clear );
		var endpoints = connection.ResolveEndpoints( span );
		var lowFloor = endpoints.Min( endpoint => endpoint.Floor );
		var highFloor = endpoints.Max( endpoint => endpoint.Floor );
		var rising = highFloor > lowFloor;

		// A flat link stands at the tallest end it touches; a rising link starts at the low one.
		var lowEnd = endpoints.FirstOrDefault( endpoint => endpoint.Floor == lowFloor );
		var resolvedBaseHeight = rising
			? (lowEnd?.Height ?? baseHeight)
			: MathF.Max( baseHeight, endpoints.Select( endpoint => endpoint.Height ).DefaultIfEmpty( baseHeight ).Max() );
		var standing = ArchContact.Bury( kit, clear, -1f );
		var room = CreateRoom( span, resolvedBaseHeight, standing, rising, lowFloor, highFloor, endpoints );
		var connected = connection.Connect( room, endpoints );

		room.Name = $"Walkway{connected.Host.Rooms.Count}";

		plan.File( ArchKind.Walkway, connected.Host, room, kinds );

		if ( rising )
		{
			ArchWalkway.SetInterior( plan, room, kit, interior );
		}
		else
		{
			new ArchWalkwayTransitionService( plan, room, kit ).AddLowerFlights( span, endpoints );
			new ArchWalkwaySupportService( plan, connected.Host, room, kit ).AddPiers( pier, span.Min, span.Max, span.AlongX );
		}

		var deck = CreateDeck( span, resolvedBaseHeight, room.WalkwayTop, clear, connected.Host, rising, room.Floor, endpoints );

		plan.File( ArchKind.Roof, connected.Host, deck, kinds );

		return new ArchWalkway.Link { Room = room, Roof = deck, Host = connected.Host, Endpoints = connected.Endpoints, Breached = connected.Opened.Count, Rising = rising };
	}

	ArchRoom CreateRoom( ArchWalkwaySpan span, float baseHeight, float standing, bool rising, int lowFloor, int highFloor, IReadOnlyList<ArchWalkwayEndpoint> endpoints )
	{
		var room = new ArchRoom
		{
			Id = plan.AllocateId(),
			Floor = lowFloor,
			BaseHeight = baseHeight,
			WallHeight = standing,
			HasFloor = !rising,
			Spans = true,
			Interior = interior,
			WalkwaySkirting = skirting,
			WalkwayTop = rising ? endpoints.First( endpoint => endpoint.Floor == highFloor ).Height : 0f,
			Footprint = ArchFootprint.Rect( span.Min, span.Max )
		};

		for ( var index = 0; index < room.Footprint.Count; index++ )
		{
			var start = room.Footprint[index];
			var end = room.Footprint[(index + 1) % room.Footprint.Count];
			var runsAlong = span.AlongX
				? MathF.Abs( end.x - start.x ) > MathF.Abs( end.y - start.y )
				: MathF.Abs( end.y - start.y ) > MathF.Abs( end.x - start.x );

			if ( !runsAlong )
			{
				continue;
			}

			plan.File( ArchKind.Wall, room, new ArchWall
			{
				Id = plan.AllocateId(),
				Start = start,
				End = end,
				Exterior = true,
				Cap = false,
				Height = standing,
				// The link draws its own boards so they die square at the mouth corners.
				Baseboard = false
			}, kinds );
		}

		ArchWalkway.SeatSides( room, span );

		return room;
	}

	ArchRoofPart CreateDeck( ArchWalkwaySpan span, float baseHeight, float highBase, float clear, ArchBuilding host, bool rising, int floor, IReadOnlyList<ArchWalkwayEndpoint> endpoints )
	{
		var deck = ArchBuild.Roof( plan, kit, floor, rising ? RoofStyle.Shed : RoofStyle.Flat, span.DeckMin, span.DeckMax, baseHeight + clear, false, false );

		deck.Name = $"WalkwayRoof{plan.CountFiled( ArchKind.Roof, host, kinds ) + 1}";
		deck.Overhang = 0f;
		deck.RidgeCap = false;
		deck.Fascia = false;
		deck.Soffit = false;
		deck.Walkway = true;

		if ( !rising )
		{
			deck.RidgeAlongX = span.AlongX;
			deck.Ceiling = true;
			deck.Cornice = cornice;

			return deck;
		}

		// The shed follows the climb: its low eave is the low end's headroom, its pitch the rise over the run.
		var rise = MathF.Max( 1f, highBase - baseHeight );
		var run = span.AlongX ? span.DeckMax.x - span.DeckMin.x : span.DeckMax.y - span.DeckMin.y;
		var highAtMin = endpoints[0].Floor > endpoints[1].Floor;

		deck.Pitch = MathF.Atan( rise / MathF.Max( 1f, run ) ).RadianToDegree();
		deck.RidgeAlongX = !span.AlongX;
		deck.Reversed = highAtMin;
		deck.EndWalls = false;

		return deck;
	}
}