Editor/Tool/ArchToolShapes.cs

Editor tool code for architecture editing. It caches results of expensive shape and geometry queries (outlines, road lookup, elevation cuts, barrier/retaining shapes) so repeated queries within an edit/frame return the same resolved object; provides helpers to find road parts under a point and to get building extents and sections.

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

namespace Sunless.Architecture;

// What the plan resolves to, held between frames: resolved once per EDIT, because every answer walks the whole curve.
public partial class ArchTool
{
	static readonly List<Vector2> NoOutline = new();

	readonly Dictionary<(Type Type, int Id), object> resolved = new();

	ArchElevationCut section;

	Vector2 roadUnderAt;
	ArchRoadPart roadUnder;
	bool roadUnderTaken;

	// The ceiling plane the cursor may land on. Held between frames like every other resolve: the cursor is
	// asked several times a frame and this walks the room's cuts to find any recess in it.
	float overhead;
	bool overheadTaken;

	// Every edit funnels here, so nothing resolved before one survives it; a handle drag edits per frame without committing.
	public void Revise()
	{
		resolved.Clear();
		section = null;
		roadUnderTaken = false;
		overheadTaken = false;
		cursorTaken = false;
		projectedLayers = null;

		// Every face of the build has just been remade, so the probed ones are somebody else's geometry.
		ForgetBuiltFaces();
	}

	public T Resolved<T>( int id, Func<T> resolve )
	{
		if ( id == 0 )
		{
			return resolve();
		}

		var key = (typeof( T ), id);

		if ( resolved.TryGetValue( key, out var held ) )
		{
			return (T)held;
		}

		var found = resolve();
		resolved[key] = found;

		return found;
	}

	// The cached loop itself, not a copy - reshape a room via ArchFloorGen.
	public List<Vector2> Outline( ArchRoom room )
	{
		if ( room is null )
		{
			return NoOutline;
		}

		return Resolved( room.Id, () => ArchFloorGen.Footprint( room ) );
	}

	// Nearest scans a whole dense walk, and the overlay asks every frame.
	public ArchRoadPart RoadUnder( Vector2 point )
	{
		if ( roadUnderTaken && roadUnderAt == point )
		{
			return roadUnder;
		}

		roadUnderTaken = true;
		roadUnderAt = point;

		return roadUnder = Plan.Roads().Count == 0 ? null : RoadAt( point, out _ );
	}

	// Only ever ONE, and keyed on depth as well as building: scrubbing the section changes it without the plan changing, so Revise never fires.
	public ArchElevationCut Section( ArchBuilding building )
	{
		var axis = Axis;
		var depth = Depth( axis );

		if ( section is not null && section.Matches( building, axis, depth ) )
		{
			return section;
		}

		return section = ArchElevation.Cut( Plan, Kit, building, axis, depth );
	}

	// Framing and section defaults both ask per frame.
	public BBox Extent( ArchBuilding building )
	{
		var id = building?.Id ?? 0;

		return Resolved( id, () => ArchLocate.Find( this, id != 0 ? id.ToString() : "plan" ).Frame );
	}

	public ArchCurve Curve( ArchFencePart fence ) => Resolved( fence.Id, fence.Curve );

	public ArchBarrierShape Barrier( ArchFencePart fence )
		=> Resolved( fence.Id, () => ArchBarrierShape.Resolve( Curve( fence ), fence.Barrier, 0f, 0f, Kit ) );

	// A whole terrain walk per bay, so the overlay asking every frame has to be held like every other resolve.
	public ArchRetainingShape Retaining( ArchFencePart fence )
		=> Resolved( fence.Id, () => ArchRetainingShape.Resolve( Barrier( fence ), Kit, new ArchGround( Scene ) ) );

	// Four subtools and the overlay used to walk every centreline per frame for this.
	public ArchRoadPart RoadAt( Vector2 point, out ArchFrame frame ) => RoadAt( point, road => road.Reach(), out frame );

	public ArchRoadPart RoadAt( Vector2 point, Func<ArchRoadPart, float> reach, out ArchFrame frame )
	{
		ArchRoadPart best = null;
		var nearest = float.MaxValue;

		frame = default;

		foreach ( var road in Plan.Roads() )
		{
			if ( !Resolved( road.Id, road.Curve ).Nearest( point, out var station, out var gap ) )
			{
				continue;
			}

			if ( gap > reach( road ) || gap >= nearest )
			{
				continue;
			}

			nearest = gap;
			best = road;
			frame = station;
		}

		return best;
	}
}