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
};
}