Editor/Span/ArchSpanShape.cs

Utility that resolves span geometry for architectural editor tools. It determines which pier or wall an authored span end anchors to, computes seat/pier heights and insets, decides springing/top/thickness for span runs, and returns resolved ArchSpanRun and ArchSpanPier structures used by generators and mesh code.

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

namespace Sunless.Architecture;

// What one end of a span stands on, measured with the run's own direction known: where it is, how high its pier
// reaches, how far into that pier the solid runs, and how wide the pier is across the run.
public readonly record struct ArchSpanSeat( Vector2 At, float Head, float Inset, float Across );

// Where one end actually stands and how high the thing under it reaches, so a gesture can mark the pier at the
// height the span will meet it rather than at the storey floor.
public readonly record struct ArchSpanPier( Vector2 At, float Head, bool Standing );

// The ONE resolve. The ghost, the generator, the extents and the report all read it, or the arch the author drags
// is not the arch the mesh comes out as.
public static class ArchSpanShape
{
	// How near a pier the gesture has to land to name it rather than a bare point.
	public const float Grab = 96f;

	public static ArchSpanPier Pier( ArchPlan plan, ArchKit kit, ArchRoom room, ArchSpanEnd end )
	{
		if ( room is null || end is null )
		{
			return default;
		}

		var standing = Standing( plan, end );
		var at = To( standing, end );

		return new ArchSpanPier( at, Seat( plan, kit, room, standing, end, at, Vector2.Right ).Head, standing is not null );
	}

	public static bool Resolve( ArchPlan plan, ArchKit kit, ArchRoom room, ArchSpanPart span, out ArchSpanRun run )
	{
		run = default;

		if ( room is null || span?.From is null || span.To is null )
		{
			return false;
		}

		var from = Standing( plan, span.From );
		var to = Standing( plan, span.To );
		var start = To( from, span.From );
		var end = To( to, span.To );
		var direction = end - start;

		if ( direction.Length < 1f )
		{
			return false;
		}

		var unit = direction.Normal;
		var seatFrom = Seat( plan, kit, room, from, span.From, start, unit );
		var seatTo = Seat( plan, kit, room, to, span.To, end, unit );

		var top = MathF.Min( seatFrom.Head, seatTo.Head ) - MathF.Max( 0f, span.Drop );
		var springing = Springs( room, span, top );
		var across = MathF.Max( seatFrom.Across, seatTo.Across );

		run = new ArchSpanRun
		{
			From = start,
			To = end,
			Springing = springing,
			Top = top,
			Thickness = Across( span, across ),
			InsetFrom = seatFrom.Inset,
			InsetTo = seatTo.Inset,
			Segments = span.Segments,
			Ring = span.Ring,
			Form = span.Form
		};

		return top - springing > 1f;
	}

	// Taking the piers' own section means taking it SHY, or the span's two sides land exactly on the column's two
	// sides and both pairs flicker. Piers that offer no section at all - a span run wall to wall - fall back to a
	// section of their own rather than to a shy nothing.
	static float Across( ArchSpanPart span, float piers )
	{
		if ( span.Thickness > 0.5f )
		{
			return span.Thickness;
		}

		return piers > 1f ? MathF.Max( 1f, piers - ArchContact.Shy ) : 8f;
	}

	// An authored springing is a height above the floor, so two spans set the same way start level whatever their
	// piers reach; left at zero the form's rise decides and the band hangs off the head instead.
	public static float Springs( ArchRoom room, ArchSpanPart span, float top )
	{
		var wanted = span.Springing > 0.5f ? room.BaseHeight + span.Springing : top - span.Band;

		return MathF.Max( room.BaseHeight + 8f, MathF.Min( wanted, top - 2f ) );
	}

	// A pier's own head, so a span between two columns of different heights springs off the shorter one.
	static ArchSpanSeat Seat( ArchPlan plan, ArchKit kit, ArchRoom room, object standing, ArchSpanEnd end, Vector2 at, Vector2 unit )
	{
		var bite = ArchContact.Bite( kit );
		var ceiling = room.BaseHeight + ArchFloorGen.WallHeight( room, kit );

		if ( standing is ArchPillarPart pillar )
		{
			var half = pillar.Half;
			var local = Rotation.FromYaw( -pillar.Yaw ) * new Vector3( unit.x, unit.y, 0f );
			var along = MathF.Abs( local.x ) * half.x + MathF.Abs( local.y ) * half.y;
			var side = MathF.Abs( local.y ) * half.x + MathF.Abs( local.x ) * half.y;

			return new ArchSpanSeat( at, pillar.BaseHeight + ArchPillarGen.Height( pillar, room, kit, plan ), MathF.Max( 0f, along - bite ), side * 2f );
		}

		if ( standing is ArchWall wall )
		{
			var thickness = wall.Thickness > 0f ? wall.Thickness : kit.WallThickness;

			return new ArchSpanSeat( at, room.BaseHeight + ArchWallSection.Height( wall, room, kit ), MathF.Max( 0f, thickness * 0.5f - bite ), 0f );
		}

		return new ArchSpanSeat( at, ceiling, 0f, 0f );
	}

	// Re-read every build rather than stored, which is the whole reason an end names a part instead of a point.
	static Vector2 To( object standing, ArchSpanEnd end )
	{
		// A grid that lost the column this end named - its counts pulled in - falls back to where the gesture
		// landed rather than to the origin, which is what FirstOrDefault would have handed back.
		if ( standing is ArchPillarPart pillar )
		{
			return ArchPillarGen.Columns( pillar )
				.Where( candidate => candidate.X == end.ColumnX && candidate.Y == end.ColumnY )
				.Select( candidate => new Vector2( candidate.At.x, candidate.At.y ) )
				.DefaultIfEmpty( end.At )
				.First();
		}

		if ( standing is ArchWall wall )
		{
			return Nearest( wall, end.At );
		}

		return end.At;
	}

	public static object Standing( ArchPlan plan, ArchSpanEnd end )
	{
		if ( plan is null || end is null || end.PartId == 0 )
		{
			return null;
		}

		foreach ( var room in plan.Buildings.SelectMany( building => building.Rooms ) )
		{
			if ( room.Pillars.FirstOrDefault( pillar => pillar.Id == end.PartId ) is { } pillar )
			{
				return pillar;
			}

			if ( room.Walls.FirstOrDefault( wall => wall.Id == end.PartId ) is { } wall )
			{
				return wall;
			}
		}

		return null;
	}

	// What the gesture landed on: a column of any grid on the storey first, because a corner an author aimed at is
	// a coordinate they placed where a wall face is only a line to lie along.
	public static ArchSpanEnd Anchored( ArchPlan plan, ArchRoom room, Vector2 point )
	{
		var end = new ArchSpanEnd { At = point };
		var closest = Grab;

		foreach ( var pillar in room.Pillars.Where( pillar => pillar.Placement != PillarPlacement.Pilaster ) )
		{
			foreach ( var column in ArchPillarGen.Columns( pillar ) )
			{
				var at = new Vector2( column.At.x, column.At.y );
				var reach = (at - point).Length;

				if ( reach >= closest )
				{
					continue;
				}

				closest = reach;
				end = new ArchSpanEnd { PartId = pillar.Id, ColumnX = column.X, ColumnY = column.Y, At = at };
			}
		}

		if ( end.PartId != 0 )
		{
			return end;
		}

		foreach ( var wall in room.Walls )
		{
			var at = Nearest( wall, point );
			var reach = (at - point).Length;

			if ( reach >= closest )
			{
				continue;
			}

			closest = reach;
			end = new ArchSpanEnd { PartId = wall.Id, At = at };
		}

		return end;
	}

	static Vector2 Nearest( ArchWall wall, Vector2 point )
	{
		var length = wall.Length;

		if ( length < 0.001f )
		{
			return wall.Start;
		}

		return wall.PointAt( Math.Clamp( Vector2.Dot( point - wall.Start, wall.Direction ), 0f, length ) );
	}
}