Editor/Carve/ArchCarveHosts.cs

Interfaces and helpers for carving tools in the editor. Defines IArchCarveHost (contract for things that carving tools can act on), a small value type ArchCarveDrag that records which host and drag endpoints, and static helper functions to query hosts under points, aim a drag to a host, and test containment and rise.

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

namespace Sunless.Architecture;

// A contract, not a tool mode, so carvers ask "what am I over" - no per-tool switches.
public interface IArchCarveHost
{
	int Id { get; }
	string Name { get; }
	int Level { get; }
	float GradeHeight { get; }
	float TopHeight { get; }
	List<ArchStairPart> Stairs { get; }

	List<Vector2> Outline();
}

// Resolved once, so the placement ghost and the commit can never disagree.
public sealed class ArchCarveDrag
{
	public IArchCarveHost Host { get; init; }
	public Vector2 Foot { get; init; }
	public Vector2 Head { get; init; }

	public bool Lands => Host is not null;
}

public static class ArchCarveHosts
{
	// Every building, never the active one - ActiveBuilding() falls back to an empty placeholder.
	public static IEnumerable<IArchCarveHost> On( ArchPlan plan, int level, ArchKinds table = null )
	{
		if ( plan is null )
		{
			return Enumerable.Empty<IArchCarveHost>();
		}

		return plan.Parts( ArchKind.Platform, table ).OfType<IArchCarveHost>().Where( host => host.Level == level );
	}

	// One lookup, so every tool's drag lands on the same host.
	public static IArchCarveHost Under( ArchPlan plan, int level, Vector2 point, ArchKinds table = null )
	{
		return On( plan, level, table ).FirstOrDefault( host => host.Holds( point ) );
	}

	// Aimed, not literal: drawing up or down from the ground must mean the same flight.
	public static ArchCarveDrag Aim( ArchPlan plan, int level, Vector2 from, Vector2 to )
	{
		var middle = (from + to) * 0.5f;
		var kinds = ArchKinds.Load();
		var host = Under( plan, level, from, kinds ) ?? Under( plan, level, to, kinds ) ?? Under( plan, level, middle, kinds );

		if ( host is null )
		{
			return new ArchCarveDrag { Foot = from, Head = to };
		}

		var onFoot = host.Holds( from );
		var onHead = host.Holds( to );

		// One end off the host = ground, so climb away; otherwise the drag is taken as authored.
		var flip = onFoot && !onHead;

		return new ArchCarveDrag
		{
			Host = host,
			Foot = flip ? to : from,
			Head = flip ? from : to
		};
	}

	public static IArchCarveHost Holding( ArchPlan plan, ArchStairPart stair )
	{
		return plan?.HostOf<IArchCarveHost>( stair );
	}

	public static bool Holds( this IArchCarveHost host, Vector2 point )
	{
		return ArchFootprint.Encloses( new[] { (IReadOnlyList<Vector2>)host.Outline() }, point );
	}

	public static float Rise( this IArchCarveHost host ) => MathF.Max( 1f, host.TopHeight - host.GradeHeight );
}