Editor/Tool/Subtools/ArchRoadSubtool.cs

Editor subtool for creating roads in the architecture editor. It collects centreline nodes, previews the curve and road geometry, and creates an ArchRoadPart added to the plan when finished.

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

namespace Sunless.Architecture;

[Title( "Road" ), Icon( "add_road" ), Group( "02" )]
public sealed class ArchRoadSubtool( ArchTool owner ) : ArchSubtool( owner )
{
	protected override ArchKind? DraftKind => ArchKind.Road;

	public override ArchSurface[] Surfaces => new[]
	{
		ArchSurface.Road, ArchSurface.Pavement, ArchSurface.Kerb, ArchSurface.RoadLine, ArchSurface.Deck, ArchSurface.Railing
	};

	readonly List<ArchCurveNode> nodes = new();
	readonly ArchRoadPart draft = new();

	bool straight;
	// Nothing else could put a centreline in the air - a deck is the road's own surface.
	float lift;

	protected override bool UsesDrag => false;

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

	protected override string Advice() => "Click to lay the centreline. Click the first node again to close a loop.";

	protected override ArchRunState Run() => new( nodes.Count > 0, $"{nodes.Count} node{(nodes.Count == 1 ? "" : "s")}" );

	protected override void FinishRun() => Finish( false );

	protected override void CancelRun() => nodes.Clear();

	protected override void OnClick( Vector2 point )
	{
		if ( nodes.Count >= 3 && (Flat( nodes[0] ) - point).Length < Owner.Kit.GridSize )
		{
			Finish( true );
			return;
		}

		nodes.Add( Node( Grounded( point ) ) );
	}

	// Tangent mode is per node, so a road can turn a corner and then curve.
	ArchCurveNode Node( Vector3 position )
	{
		return new ArchCurveNode
		{
			Position = position,
			Mode = straight ? ArchTangentMode.Linear : ArchTangentMode.Auto
		};
	}

	// Draped on terrain or laid on the level, standing its own section clear of whichever. Neither height lands on
	// the ladder: a rung is coarser than the whole ride and rounds it away, which is a road laid in the ground.
	Vector3 Grounded( Vector2 point )
	{
		var lifted = MathF.Max( 0f, lift );

		if ( draft.Conform && ArchTerrain.Sample( Owner.Scene, point, out var sampled ) )
		{
			return Owner.Grid.Draped( new Vector3( point.x, point.y, sampled + draft.Seat( Owner.Kit ) + lifted ) );
		}

		// A level's height already carries the ground clearance, so only the camber is still owed.
		return Owner.Grid.Draped( new Vector3( point.x, point.y, Owner.LevelHeight + MathF.Max( 0f, draft.Camber ) + lifted ) );
	}

	protected override void DrawHover( Vector2 point )
	{
		base.DrawHover( point );

		if ( nodes.Count == 0 )
		{
			ArchGhost.Note( new Vector3( point.x, point.y, Owner.LevelHeight ), "click to start the centreline" );

			return;
		}

		var line = new List<ArchCurveNode>( nodes ) { Node( Grounded( point ) ) };
		var curve = ArchCurve.Of( line );

		if ( !curve.IsUsable )
		{
			return;
		}

		var frames = ArchRoadGen.Frames( Sketch( line, false ), curve );

		ArchGhost.Curve( curve );
		ArchGhost.Verges( curve, frames, draft.HalfWidth );

		if ( draft.Pavements != RoadSide.None )
		{
			ArchGhost.Verges( curve, frames, draft.HalfWidth + draft.PavementWidth );
		}

		Gizmo.Draw.Color = ArchGhost.Accent;
		Gizmo.Draw.LineSphere( nodes[0].Position, Owner.Kit.GridSize * 0.5f );
		ArchGhost.Note( frames[^1].Position, $"{curve.Length / 12f:0.#} ft" );
	}

	ArchRoadPart Sketch( IReadOnlyList<ArchCurveNode> line, bool closed )
	{
		var road = draft.Copy();

		road.Nodes = line.Select( node => node.Copy() ).ToList();
		road.Closed = closed;

		return road;
	}

	void Finish( bool closed )
	{
		if ( nodes.Count >= 2 )
		{
			var road = Sketch( nodes, closed );

			road.Id = Owner.Plan.AllocateId();
			road.Name = $"Road{Owner.Plan.Roads().Count + 1}";

			Owner.Plan.Units.Add( road );
			FinishDraft( road.Id );
			Owner.Commit( "Create Road" );
		}

		nodes.Clear();
	}

	static Vector2 Flat( ArchCurveNode node ) => new( node.Position.x, node.Position.y );

	protected override void BuildOptions( ToolSidebarWidget panel )
	{
		ArchSidebarSection.Show( panel, Scope( "centreline" ), "Centreline", centreline =>
		{
			using ( var shape = ArchIconGrid.In( centreline ) )
			{
				shape.Pick( "Spline: eased through every node", "road_spline", "gesture", !straight, () => { straight = false; Refresh(); } );
				shape.Pick( "Straight: a corner at every node", "road_straight", "timeline", straight, () => { straight = true; Refresh(); } );
			}

			var deck = ArchPartUi.Number( "Deck height", lift, 0f, value => lift = value );

			deck.ToolTip = "Raise it before the middle nodes to carry a run over something, then span it with the Bridge tool.";
			centreline.Add( deck );
		} );

		ArchRoadUi.Build( panel, draft, Owner.Kit, Refresh, null );
	}
}