Editor/Services/ArchWallSnap.cs

Editor utility for snapping a point to nearby walls in an architectural plan. It finds the nearest corner or face (wall line) within reach among provided walls, preferring corners over faces, and returns a struct describing the hit.

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

namespace Sunless.Architecture;

public enum ArchSnapKind
{
	None,
	Corner,
	Face
}

public readonly struct ArchWallSnapped
{
	public ArchSnapKind Kind { get; init; }
	public Vector2 Point { get; init; }
	public ArchWall Wall { get; init; }
	public float Distance { get; init; }

	public bool Took => Kind != ArchSnapKind.None;
}

// What a plan point comes to rest ON when it comes to rest on something already standing rather than on the
// grid. CORNERS FIRST: a corner is a point the author placed, where a face is only a line to lie along, so a
// corner inside reach wins over a face that happens to be nearer.
public static class ArchWallSnap
{
	// One subgrid step. Half a BASE cell reads as generous and is not: on a 64 grid it grabs everything within
	// 32 inches of any wall, so the cursor leaves the corner it was aimed at rather than arriving at it.
	public static float Reach( float authored )
	{
		return authored > 0.01f ? authored : new ArchGridService().SubgridSize();
	}

	// Only what stands on the surface being worked: a deck's own walls up there, a storey's rooms down here.
	// Offered every wall in the plan, a parapet's end lands on the corner of a room two storeys beneath it.
	public static IEnumerable<ArchWall> On( ArchPlan plan, int level, ArchRoofPart deck )
	{
		if ( plan is null )
		{
			return Enumerable.Empty<ArchWall>();
		}

		return deck is not null ? deck.Walls : plan.WallsOn( level );
	}

	public static ArchWallSnapped Nearest( IEnumerable<ArchWall> walls, Vector2 point, float reach )
	{
		if ( walls is null || reach <= 0.01f )
		{
			return default;
		}

		var corner = default( ArchWallSnapped );
		var face = default( ArchWallSnapped );

		foreach ( var wall in walls )
		{
			var length = wall.Length;

			if ( length < 0.5f )
			{
				continue;
			}

			Closer( ref corner, ArchSnapKind.Corner, wall.Start, wall, reach, point );
			Closer( ref corner, ArchSnapKind.Corner, wall.End, wall, reach, point );

			var along = Math.Clamp( Vector2.Dot( point - wall.Start, wall.Direction ), 0f, length );

			Closer( ref face, ArchSnapKind.Face, wall.PointAt( along ), wall, reach, point );
		}

		return corner.Took ? corner : face;
	}

	static void Closer( ref ArchWallSnapped best, ArchSnapKind kind, Vector2 at, ArchWall wall, float reach, Vector2 point )
	{
		var distance = (point - at).Length;

		if ( distance > reach || (best.Took && distance >= best.Distance) )
		{
			return;
		}

		best = new ArchWallSnapped { Kind = kind, Point = at, Wall = wall, Distance = distance };
	}

	public static string Describe( ArchSnapKind kind ) => kind switch
	{
		ArchSnapKind.Corner => "corner",
		ArchSnapKind.Face => "wall",
		_ => null
	};
}