Editor/Tool/Subtools/ArchWalkwaySubtool.cs

Editor subtool for creating and editing walkway architectural links. It renders previews, handles drag placement, builds UI options for width/height/rise/interior/cornice/skirting/piers, and applies changes to the plan via ArchWalkway and related types.

NetworkingFile Access
using System;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

[Title( "Walkway" ), Icon( "linear_scale" ), Group( "07" )]
public sealed class ArchWalkwaySubtool( ArchTool owner ) : ArchSubtool( owner )
{
	protected override ArchKind? DraftKind => ArchKind.Walkway;

	public override ArchSurface[] Surfaces => new[]
	{
		ArchSurface.WallExterior, ArchSurface.WallInterior, ArchSurface.Floor, ArchSurface.Roof,
		ArchSurface.Ceiling, ArchSurface.Baseboard, ArchSurface.Trim, ArchSurface.Pillar, ArchSurface.PillarCap
	};

	float width = 72f;
	float height = 96f;
	int rise;
	bool piers = true;
	WalkwayInterior interior;
	bool cornice = true;
	bool skirting = true;

	// Held here so section and footing tune BEFORE the drag, not only after.
	ArchPillarPart draft = new();

	// Re-dresses the newest link in the plan; authoring anything else moves the counter.
	int placed;
	int stamp;

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

	protected override string Advice() => "Drag the link between two shells. Both ends open into whatever wall they land on.";

	ArchRoom Placed => placed != 0 && (adopted || Owner.Plan.NextId == stamp) ? Owner.Plan.FindRoom( placed ) : null;

	// A link taken from the stack is edited live like one just dragged - but it stays editable
	// through commits, where the freshly-placed one is only current until the plan moves on.
	bool adopted;

	public override bool Adopts => true;

	protected override void Adopt( ArchSelection picked )
	{
		adopted = picked?.Item is ArchRoom { Spans: true };
		placed = adopted ? ((ArchRoom)picked.Item).Id : 0;
		stamp = Owner.Plan.NextId;

		if ( Placed is { } link )
		{
			height = MathF.Max( 60f, (Deck( link )?.BaseHeight ?? link.BaseHeight + link.WallHeight) - link.BaseHeight );
		}
	}

	ArchRoofPart Deck( ArchRoom link )
	{
		return Owner.Plan.Buildings
			.SelectMany( building => building.Roofs )
			.FirstOrDefault( roof => roof.Level == link.Floor && MathF.Abs( roof.BaseHeight - (link.BaseHeight + height) ) < link.WallHeight );
	}

	bool Rising( ArchRoom link ) => link is not null && link.WalkwayTop > link.BaseHeight + 1f;

	// The drag squared onto its long axis and widened to the Width box.
	protected override void DrawPreview()
	{
		ArchWalkway.Shape( Min( DragStart, DragCurrent ), Max( DragStart, DragCurrent ), width, out var min, out var max, out var alongX );

		var deck = Owner.LevelHeight;
		var head = deck + MathF.Max( 60f, height );

		ArchGhost.Volume( min, max, deck, head );
		ArchGhost.Plate( min, max, deck, 8 );

		var run = alongX ? max.x - min.x : max.y - min.y;
		var across = alongX ? max.y - min.y : max.x - min.x;

		ArchGhost.Note( new Vector3( max.x, max.y, head ), $"walkway — {run:0} x {across:0}" );
	}

	protected override void OnDrag( Vector2 from, Vector2 to )
	{
		var link = ArchWalkway.Add(
			Owner.Plan, Owner.ActiveBuilding(), Owner.Kit, Owner.Level, Owner.LevelHeight,
			Min( from, to ), Max( from, to ), from, width, height, piers ? draft : null,
			rise, interior, cornice, skirting );

		if ( link is null )
		{
			Log.Info( "Architecture: that drag leaves nothing to span - it needs to reach across the gap between two shells." );
			return;
		}

		Owner.ActiveBuildingId = link.Host.Id;
		Owner.ActiveRoomId = link.Room.Id;
		FinishDraft( link.Room.Id );

		// The link is a site connection: it forms the group holding it and both houses it joined,
		// so the tree shows the join and the mouths know which walls they are allowed to stand in.
		Owner.InsertionTarget = null;
		ArchWalkwayConnection.Form( Owner.Plan, link );

		Owner.Commit( "Create Walkway" );

		placed = link.Room.Id;
		stamp = Owner.Plan.NextId;

		Refresh();

		if ( link.Breached == 0 )
		{
			Log.Info( "Architecture: the walkway reached no wall at either end, so it opens into nothing yet." );
		}
	}

	void Seeded()
	{
		if ( draft.Type.Length > 0 || Owner.DefaultPillar() is not { } first )
		{
			return;
		}

		draft = first.Standing( default, 0f, 0f );
	}

	void Adopt( ArchPillarType type )
	{
		draft = type.Standing( default, 0f, 0f );

		Refresh();
	}

	void Reheight( ArchRoom link, float value )
	{
		height = value;

		ArchWalkway.Reheight( link, Deck( link ), Owner.Kit, value );
		Owner.Touch( "Walkway Height" );
	}

	// Refreshed, unlike the height: the reshape clamps a width the passage cannot take and re-seats both
	// ends, so the box has to show back what the link actually settled on.
	void Rewidth( ArchRoom link, float value )
	{
		width = value;

		ArchWalkway.SetWidth( Owner.Plan, link, Owner.Kit, value );
		Owner.Touch( "Walkway Width" );
		Refresh();
	}

	protected override void BuildOptions( ToolSidebarWidget panel )
	{
		Seeded();

		var link = Placed;

		if ( link is not null )
		{
			panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {link.Name} live. Drag anything else and these go back to seeding the next link." ) );
		}

		ArchSidebarSection.Show( panel, Scope( "placement" ), "Placement", span =>
		{
			span.Add( ArchPartUi.Number( "Width", link is null ? width : ArchWalkway.WidthOf( link ), 72f,
				value => { if ( link is null ) { width = value; } else { Rewidth( link, value ); } } ) );

			// A rise decides which STOREY the far end opens at, and that is resolved by the placement rather than
			// held on the link - so it seeds the next drag and an existing link changes storeys by being redrawn.
			if ( link is null )
			{
				span.Add( ArchPartUi.Integer( "Rise (storeys)", rise, 0, value => rise = Math.Clamp( value, 0, 3 ) ) );
			}

			span.Add( ArchPartUi.Number( "Height (floor to ceiling)", height, 96f,
				value => { if ( link is null ) { height = value; } else { Reheight( link, value ); } } ) );
		} );

		if ( link is null ? rise > 0 : Rising( link ) )
		{
			Interior( panel, link );
		}

		Finish( panel, link );
		Piers( panel, link );
	}

	// Stairs, a ramp, or a plain slope carry the climb inside a rising link.
	void Interior( ToolSidebarWidget panel, ArchRoom link )
	{
		var current = link?.Interior ?? interior;

		ArchSidebarSection.Show( panel, Scope( "interior" ), "Inside the climb", group =>
		{
			using var grid = ArchIconGrid.In( group );

			grid.Pick( "A plain slope to walk up", "opt_slope", "landscape", current == WalkwayInterior.None,
				() => ChooseInterior( link, WalkwayInterior.None ) );
			grid.Pick( "A flight of stairs", "opt_stairs", "stairs", current == WalkwayInterior.Stairs,
				() => ChooseInterior( link, WalkwayInterior.Stairs ) );
			grid.Pick( "A ramp with kerbs", "opt_ramp", "trending_up", current == WalkwayInterior.Ramp,
				() => ChooseInterior( link, WalkwayInterior.Ramp ) );
		} );

		Underside( panel, link );
	}

	// A link stands clear of the ground, so what is under its flight is a face the street sees.
	void Underside( ToolSidebarWidget panel, ArchRoom link )
	{
		if ( link?.Stairs.FirstOrDefault( stair => stair.Name == "WalkwayFlight" ) is not { } flight )
		{
			return;
		}

		ArchSidebarSection.Show( panel, Scope( "underside" ), "Under the climb", group =>
		{
			using var grid = ArchIconGrid.In( group );

			foreach ( var value in new[] { StairUnder.Filled, StairUnder.Open } )
			{
				var captured = value;

				grid.Pick( ArchStairLabels.Describe( captured ), $"under_{captured}".ToLowerInvariant(), ArchStairLabels.Fallback( captured ),
					flight.Under == captured, () => { flight.Under = captured; Owner.Touch( "Walkway Underside" ); } );
			}
		} );
	}

	void ChooseInterior( ArchRoom link, WalkwayInterior value )
	{
		if ( link is null )
		{
			interior = value;
			Refresh();

			return;
		}

		ArchWalkway.SetInterior( Owner.Plan, link, Owner.Kit, value );
		Owner.Touch( "Walkway Interior" );
	}

	// Cornice and skirting are dress on the placed link; without one they seed the next drag.
	void Finish( ToolSidebarWidget panel, ArchRoom link )
	{
		var deck = link is null ? null : Deck( link );

		ArchSidebarSection.Show( panel, Scope( "finish" ), "Finish", group =>
		{
			using var grid = ArchIconGrid.In( group );

			grid.Toggle( "Cornice under the deck, along the walls", "opt_cornice", "border_top",
				link is null ? cornice : deck?.Cornice ?? true,
				value =>
				{
					if ( link is null )
					{
						cornice = value;
					}
					else if ( deck is not null )
					{
						deck.Cornice = value;
						Owner.Touch( "Walkway Cornice" );
					}

					Refresh();
				} );

			grid.Toggle( "Skirting boards along the walls", "opt_skirting", "format_align_center",
				link is null ? skirting : link.WalkwaySkirting,
				value =>
				{
					if ( link is null )
					{
						skirting = value;
					}
					else
					{
						link.WalkwaySkirting = value;
						Owner.Touch( "Walkway Skirting" );
					}

					Refresh();
				} );
		} );
	}

	// Always shown - hiding them until a link has some hides them when wanted. On an adopted link the toggle
	// STANDS them or takes them away, because a link is the thing being edited and its piers are part of it.
	void Piers( ToolSidebarWidget panel, ArchRoom link )
	{
		var standing = link?.Pillars.FirstOrDefault();

		// Nothing to stand piers in, so say that rather than offer a toggle the support service will refuse.
		if ( link is not null && !ArchWalkway.Raised( link, Owner.Kit ) )
		{
			ArchSidebarSection.Show( panel, Scope( "piers" ), "Piers underneath", group =>
				group.Add( Wrapped( "This link sits on grade, so there is no gap under it to stand piers in. Raise it and they can go in." ) ) );

			return;
		}

		ArchSidebarSection.Show( panel, Scope( "piers" ), "Piers underneath", group =>
		{
			using var grid = ArchIconGrid.In( group );

			grid.Toggle( "Stand piers under the link where it is raised above grade", "opt_pillars", "view_column",
				link is null ? piers : standing is not null,
				value =>
				{
					if ( link is null )
					{
						piers = value;
					}
					else
					{
						ArchWalkway.SetPiers( Owner.Plan, link, Owner.Kit, value ? draft : null );
						Owner.Touch( "Walkway Piers" );
					}

					Refresh();
				} );
		} );

		// A pier IS a pillar type, so its section, footing and span are the pillar designer's and are folded
		// away here. A link's piers are tuned ON the link; with none standing there is nothing to tune, and
		// the SEED editor shown there instead wrote every click into the next drag while the author watched
		// the selection not move.
		if ( link is null )
		{
			if ( piers )
			{
				ArchSidebarSection.Disclosure( panel, Scope( "pier" ), "Pier type", true, group =>
					ArchSidebarLayout.Form( group, form =>
					{
						ArchAsks.PillarKinds( form, Owner.OfferedPillars(), draft.Type, Adopt );
						ArchAsks.BuildPillar( form, draft, Owner.FindPillarType( draft.Type ), Refresh, null );
					} ) );
			}

			return;
		}

		if ( standing is null )
		{
			return;
		}

		ArchSidebarSection.Disclosure( panel, Scope( "pier" ), "Pier type", true, group =>
			ArchSidebarLayout.Form( group, form =>
			{
				ArchAsks.PillarKinds( form, Owner.OfferedPillars(), standing.Type,
					type => { ArchAsks.RetypePillar( Owner, standing, type ); Refresh(); } );

				ArchAsks.SizePillar( form, standing, Owner.FindPillarType( standing.Type ), Refresh, () => Owner.Touch( "Retune Piers" ) );
			} ) );
	}
}