Editor/Tool/Subtools/ArchSpanSubtool.cs

Editor subtool for placing and editing architectural spans (bridges) in the level editor. It handles hover visuals, taking one end, bridging between two ends, drawing pier and span ghosts, and building the sidebar UI for span options.

File Access
using System;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

[Title( "Spans" ), Icon( "bridge" ), Group( "09" )]
public sealed class ArchSpanSubtool( ArchTool owner ) : ArchSubtool( owner )
{
	protected override ArchKind? DraftKind => ArchKind.Span;

	public override ArchSurface[] Surfaces => new[] { ArchSurface.PillarCap };

	ArchSpanPart draft = ArchSpanMemo.Recall();

	ArchSpanEnd taken;

	protected override bool UsesDrag => false;

	// Only over a span. The selection outlives the tool that made it, so an unconditional yes put the LAST
	// pillar's box dragger on screen the moment this tool was picked - and the gizmo takes the press first, so
	// every click meant for a pier went into a widget belonging to something else.
	protected override bool Adjusts => Owner.Picked?.Item is ArchSpanPart;

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

	protected override string Advice() => taken is null
		? "Click a column or a wall to take one end. The nearest column within reach wins, so an eyeballed click still lands on the pier."
		: "Click the far column or wall and it bridges them. An end that lands on nothing stays where it was dropped.";

	protected override ArchRunState Run() => new( taken is not null, "one end taken" );

	protected override void CancelRun() => taken = null;

	protected override void DrawHover( Vector2 point )
	{
		var room = Owner.RoomOnLevel( point );

		if ( room is null )
		{
			base.DrawHover( point );

			return;
		}

		var landing = ArchSpanShape.Anchored( Owner.Plan, room, point );

		Pier( room, landing, false );

		if ( taken is null )
		{
			return;
		}

		Pier( room, taken, true );
		Reaching( room, taken, landing );
	}

	protected override void OnClick( Vector2 point )
	{
		var room = Owner.RoomOnLevel( point );

		if ( room is null )
		{
			return;
		}

		var landing = ArchSpanShape.Anchored( Owner.Plan, room, point );

		if ( taken is null )
		{
			taken = landing;

			return;
		}

		Bridge( room, taken, landing );
	}

	void Bridge( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to )
	{
		if ( Standing( room, from, to ) is not { } span || !ArchSpanShape.Resolve( Owner.Plan, Owner.Kit, room, span, out _ ) )
		{
			Log.Info( "Architecture: nothing to bridge there — a span needs two ends apart, each under a pier tall enough to spring off." );

			return;
		}

		span.Id = Owner.Plan.AllocateId();
		span.Name = $"Span{room.PierSpans.Count + 1}";

		room.PierSpans.Add( span );
		FinishDraft( span.Id );
		Owner.Picked = new ArchSelection { Item = span, Room = room };
		Drew( span );
		Owner.Commit();

		taken = null;
	}

	// A ring on the pier's HEAD and a stem down to the floor: where the end will spring from and which column it
	// grabbed, drawn as a mark on something rather than as a post, which is a pillar ghost by another name.
	void Pier( ArchRoom room, ArchSpanEnd end, bool held )
	{
		var pier = ArchSpanShape.Pier( Owner.Plan, Owner.Kit, room, end );
		var lift = Lift( room );
		var head = new Vector3( pier.At.x, pier.At.y, pier.Head + lift );

		Gizmo.Draw.LineThickness = held ? 4f : 3f;
		Gizmo.Draw.Color = held ? ArchGhost.Accent : ArchGhost.Line;
		Gizmo.Draw.LineCircle( head, Vector3.Up, held ? 14f : 10f );
		Gizmo.Draw.Line( head, head.WithZ( room.BaseHeight + lift ) );
		Gizmo.Draw.LineThickness = 2f;

		if ( !pier.Standing )
		{
			ArchGhost.Cursor( pier.At, room.BaseHeight + lift, ArchSpanShape.Grab );
		}

		Gizmo.Draw.Color = ArchGhost.Line;
	}

	// The plan is authored flat and the building is poured onto its grade, so a ghost drawn at plan height stands
	// a foundation below the span it is previewing.
	float Lift( ArchRoom room )
	{
		var host = Owner.Plan.OwnerOf( room ) ?? Owner.ActiveBuilding();

		return host is null ? 0f : ArchAsks.Lift( Owner.Plan, host, Owner.Kit );
	}

	void Reaching( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to )
	{
		var lift = Lift( room );

		if ( Standing( room, from, to ) is not { } span
			|| !ArchSpanShape.Resolve( Owner.Plan, Owner.Kit, room, span, out var run ) )
		{
			Gizmo.Draw.Line(
				new Vector3( from.At.x, from.At.y, room.BaseHeight + lift ),
				new Vector3( to.At.x, to.At.y, room.BaseHeight + lift ) );

			return;
		}

		var start = new Vector3( run.From.x, run.From.y, run.Springing + lift );
		var end = new Vector3( run.To.x, run.To.y, run.Springing + lift );

		var top = run.Top + lift;

		Gizmo.Draw.LineThickness = 4f;
		Gizmo.Draw.Color = ArchGhost.Accent;

		Gizmo.Draw.Line( start, end );
		Gizmo.Draw.Line( start.WithZ( top ), end.WithZ( top ) );
		Gizmo.Draw.Line( start, start.WithZ( top ) );
		Gizmo.Draw.Line( end, end.WithZ( top ) );

		Gizmo.Draw.LineThickness = 2f;
		Gizmo.Draw.Color = ArchGhost.Line;

		ArchGhost.Note( Vector3.Lerp( start.WithZ( top ), end.WithZ( top ), 0.5f ),
			$"{span.Form} — {(run.To - run.From).Length:0} across, {run.Top - run.Springing:0} deep, springs at {run.Springing - room.BaseHeight:0}" );
	}

	// ONE resolution the ghost and the placed part both read, so what the gesture showed is what the mesh comes out as.
	ArchSpanPart Standing( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to )
	{
		if ( room is null )
		{
			return null;
		}

		var span = draft.Dressing();

		span.Level = room.Floor;
		span.From = from;
		span.To = to;

		return span;
	}

	protected override void BuildOptions( ToolSidebarWidget panel )
	{
		if ( Editing() is { } picked )
		{
			ArchSidebarSection.Show( panel, Scope( "selected" ), $"Selected — {picked.Name}", group =>
				group.Add( new Button( "Deselect", "close" ) { Clicked = () => { Owner.Select( null ); Refresh(); } } ) );

			ArchSpanUi.Build( panel, picked, Refresh, () => { Seed( picked ); Owner.Touch( "Edit Span" ); } );

			return;
		}

		ArchSpanUi.Build( panel, draft, Refresh, () => ArchSpanMemo.Remember( draft ) );
	}

	// Tuning the span that was just placed IS the author saying how the next one should look - the panel points at
	// the selection, so without this every adjustment was spent on one part and the seed never moved.
	void Seed( ArchSpanPart span )
	{
		draft = span.Dressing();

		ArchSpanMemo.Remember( draft );
	}

	ArchSpanPart Editing() => Owner.Picked?.Item as ArchSpanPart;
}