Editor/Tool/Subtools/ArchPorchSubtool.cs

Editor subtool for placing porches in the architecture tool. It provides preview drawing, sketch resolution, drag placement, and UI options for porch parts and settings.

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

namespace Sunless.Architecture;

[Title( "Porch" ), Icon( "deck" ), Group( "13" )]
public sealed class ArchPorchSubtool( ArchTool owner ) : ArchSubtool( owner )
{
	protected override ArchKind? DraftKind => ArchKind.Porch;

	public override ArchSurface[] Surfaces => new[] { ArchSurface.Deck, ArchSurface.Railing, ArchSurface.Baseboard, ArchSurface.Roof, ArchSurface.Soffit };

	readonly ArchPorchPart draft = new() { GradeHeight = -24f };

	protected override string Title() => "Porch";

	protected override string Advice() => "Drag the deck over the wall it hangs off. Cover a corner and it wraps it; touch a porch already there and it joins that one. Inside a room it stands free.";

	// Resolved from the shape, not the drag, so the ghost matches the generator.
	protected override void DrawPreview()
	{
		var min = Min( DragStart, DragCurrent );
		var max = Max( DragStart, DragCurrent );
		var room = Owner.RoomAt( (min + max) * 0.5f, out var building );
		var deck = Owner.LevelHeight;
		var legs = ArchPorch.Preview( building, room, Owner.Kit, min, max, draft.Standing, out var standing, out var joining );

		if ( legs.Count == 0 )
		{
			ArchGhost.Plate( min, max, deck, 8 );
			ArchGhost.Note( new Vector3( max.x, max.y, deck + 60f ), "porch — nowhere to stand a deck there" );
			return;
		}

		var sketch = Sketch( legs, standing );
		var shape = ArchPorchShape.Resolve( sketch, room, building, Owner.Kit );
		var head = shape.Head;
		var rail = deck + Owner.Kit.PorchRailHeight;

		foreach ( var leg in legs )
		{
			if ( draft.Deck )
			{
				ArchGhost.Volume( leg.Min, leg.Max, deck - draft.DeckDrop, deck );
			}

			ArchGhost.Plate( leg.Min, leg.Max, deck, 8 );
		}

		foreach ( var bay in ArchPorchGen.Bays( shape, sketch, Owner.Kit ) )
		{
			if ( draft.Posts )
			{
				ArchGhost.Post( bay.From.Point, Owner.Kit.PorchPostSize, deck, head );

				if ( bay.Last )
				{
					ArchGhost.Post( bay.To.Point, Owner.Kit.PorchPostSize, deck, head );
				}
			}

			if ( draft.Railings && !bay.Gated )
			{
				Gizmo.Draw.Line( bay.From.Raised.WithZ( rail ), bay.To.Raised.WithZ( rail ) );
			}
		}

		if ( draft.Roofed )
		{
			ArchGhost.Ring( shape.Footprint, head + Owner.Kit.PorchBeamDepth );
		}

		ArchGhost.Note( new Vector3( max.x, max.y, head ), Note( legs, standing, joining ) );
	}

	static string Note( IReadOnlyList<ArchPorchLeg> legs, PorchStanding standing, ArchPorchPart joining )
	{
		if ( joining is not null )
		{
			return $"porch — joining {joining.Name}";
		}

		if ( standing == PorchStanding.Free )
		{
			return "porch — free-standing, no wall to lean on";
		}

		return legs.Count > 1 ? $"porch — {legs.Count} legs, wrapping the corner" : "porch";
	}

	// Unattached to the plan, so the ghost resolves exactly what Attach will - the gates in its railing
	// included, which is why it carries the flight the placement is about to seed.
	ArchPorchPart Sketch( List<ArchPorchLeg> legs, PorchStanding standing )
	{
		var deck = Owner.LevelHeight;

		var sketch = new ArchPorchPart
		{
			Legs = legs,
			Standing = standing,
			BaseHeight = deck,
			GradeHeight = deck - draft.DeckDrop,
			HeadHeight = MathF.Max( 60f, draft.HeadHeight ),
			PostSpacing = MathF.Max( 32f, Owner.Kit.PorchPostSize * 8f ),
			Deck = draft.Deck,
			Posts = draft.Posts,
			Rafters = draft.Rafters,
			Braces = draft.Braces,
			Pitch = draft.Pitch,
			Railings = draft.Railings,
			Steps = draft.Steps,
			Roofed = draft.Roofed
		};

		return sketch;
	}

	protected override void OnDrag( Vector2 from, Vector2 to )
	{
		var min = Min( from, to );
		var max = Max( from, to );

		if ( (max - min).Length < 24f )
		{
			return;
		}

		var room = Owner.RoomAt( (min + max) * 0.5f, out var building );
		var placement = ArchPorch.Attach( Owner.Plan, building, room, Owner.Kit, min, max, draft );

		if ( placement is null )
		{
			Log.Info( "Architecture: no room under that drag for a porch to stand in." );
			return;
		}

		// A porch is a cluster of legs, not one part - the draft's work is done either way.
		FinishDraft();
		Owner.Commit( "Create Porch" );
	}

	protected override void BuildOptions( ToolSidebarWidget panel )
	{
		ArchPorchUi.Build( panel, draft, Refresh, null );
		ArchPorchUi.Sizes( panel, draft, null );
	}
}