Editor/Tool/Subtools/ArchFenceSubtool.cs

Editor subtool for placing fence architecture. Lets the user click points to create an ArchFencePart, samples terrain to drape nodes, previews ghost geometry and creates a fence part on finish.

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

namespace Sunless.Architecture;

[Title( "Fence" ), Icon( "fence" ), Group( "14" )]
public sealed class ArchFenceSubtool( ArchTool owner ) : ArchSubtool( owner )
{
	protected override ArchKind? DraftKind => ArchKind.Fence;

	public override ArchSurface[] Surfaces => new[]
	{
		ArchSurface.Railing, ArchSurface.Panel, ArchSurface.Post, ArchSurface.Wire, ArchSurface.Foundation
	};

	readonly List<ArchCurveNode> nodes = new();
	readonly ArchBarrierSpec spec = new();

	bool conform = true;

	protected override bool UsesDrag => false;

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

	protected override string Advice() => "Click to lay the line. Click the first node again to close the run.";

	protected override ArchRunState Run() => new( nodes.Count > 0, $"{nodes.Count} point{(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( ArchCurveNode.At( Grounded( point ) ) );
	}

	Vector3 Grounded( Vector2 point )
	{
		if ( conform && ArchTerrain.Sample( Owner.Scene, point, out var sampled ) )
		{
			return Owner.Grid.Draped( new Vector3( point.x, point.y, sampled + Owner.Kit.GroundClearance ) );
		}

		return Owner.Grid.Draped( new Vector3( point.x, point.y, Owner.LevelHeight ) );
	}

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

		if ( nodes.Count == 0 )
		{
			ArchGhost.Note( new Vector3( point.x, point.y, Owner.LevelHeight + spec.Height ), "click to start the fence line" );
			return;
		}

		var line = new List<ArchCurveNode>( nodes ) { ArchCurveNode.At( Grounded( point ) ) };
		var curve = ArchCurve.Of( line );
		var barrier = ArchBarrierShape.Resolve( curve, spec, 0f, 0f, Owner.Kit );

		ArchGhost.Curve( curve );

		if ( spec.Style == BarrierStyle.Retaining )
		{
			ArchGhost.Retaining( ArchRetainingShape.Resolve( barrier, Owner.Kit, new ArchGround( Owner.Scene ) ) );
		}
		else
		{
			ArchGhost.Barrier( barrier, spec );
		}

		Gizmo.Draw.Color = ArchGhost.Accent;
		Gizmo.Draw.LineSphere( nodes[0].Position, Owner.Kit.GridSize * 0.5f );
	}

	void Finish( bool closed )
	{
		var building = Owner.ActiveBuilding();

		if ( building is not null && nodes.Count >= 2 )
		{
			var fence = new ArchFencePart
			{
				Id = Owner.Plan.AllocateId(),
				Name = $"Fence{building.Fences.Count + 1}",
				Nodes = nodes.Select( node => node.Copy() ).ToList(),
				Closed = closed,
				Barrier = spec.Copy()
			};

			building.Fences.Add( fence );
			FinishDraft( fence.Id );
			Owner.Commit( "Create Fence" );
		}

		nodes.Clear();
	}

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

	protected override void BuildOptions( ToolSidebarWidget panel )
	{
		ArchSidebarSection.Show( panel, Scope( "placement" ), "Placement", group =>
			group.Add( ArchPartUi.Check( "Follow terrain", conform, value => conform = value ) ) );

		ArchBarrierUi.Build( panel, spec, Refresh );
	}
}