Editor/Span/ArchSpanHandler.cs

Editor handler for span parts in the architecture editor. It draws and updates a span widget, handles re-aiming span endpoints, adjusts springing, beam depth or arch rise based on user gestures and plan/kit context.

File Access
namespace Sunless.Architecture;

public sealed class ArchSpanHandler : IArchHandler
{
	public ArchKind Kind => ArchKind.Span;

	// An end square per pier and, in an elevation, how far the span reaches down. Dragging an end re-asks what it
	// landed on, so a span is re-aimed at another column by pulling it there rather than by placing a new one.
	public bool Draw( ArchTool tool, ArchSelection picked )
	{
		if ( picked.Item is not ArchSpanPart span )
		{
			return false;
		}

		var room = tool.Plan.RoomStanding( span );

		if ( !ArchSpanShape.Resolve( tool.Plan, tool.Kit, room, span, out var run ) )
		{
			return false;
		}

		var gesture = ArchGesture.On( ArchKind.Span, span.Id );

		// The part is authored on the flat plan and poured onto the building's grade, so a widget drawn at the
		// authored height stands a foundation below the span it is meant to be holding.
		var lift = picked.Lift( tool.Plan, tool.Kit );

		if ( Reaimed( tool, room, span, run, gesture, lift ) )
		{
			return true;
		}

		var middle = Vector2.Lerp( run.From, run.To, 0.5f );

		if ( !ArchShapeHandles.Height( tool, gesture.At( "spring" ), middle, run.Springing + lift, out var pulled, Gizmo.Colors.Roll ) )
		{
			return false;
		}

		var band = MathF.Max( 2f, run.Top + lift - pulled );

		// A pinned springing is the number the shape reads, so the widget writes THAT rather than a rise nothing
		// would look at.
		if ( span.Springing > 0.5f )
		{
			span.Springing = MathF.Max( 2f, pulled - lift - room.BaseHeight );

			return true;
		}

		if ( span.Form == PillarSpan.Beam )
		{
			span.BeamDepth = band;

			return true;
		}

		// An arch spends its band on the ring as well, so pulling the springing down leaves the ring where the
		// author set it and grows the rise.
		span.Rise = span.Form == PillarSpan.Arch ? MathF.Max( 2f, band - MathF.Max( 2f, span.Ring ) ) : band;

		return true;
	}

	static bool Reaimed( ArchTool tool, ArchRoom room, ArchSpanPart span, ArchSpanRun run, ArchGesture gesture, float lift )
	{
		if ( Pulled( tool, gesture.At( "from" ), run.From, run.Springing + lift, out var start ) )
		{
			span.From = ArchSpanShape.Anchored( tool.Plan, room, start );

			return true;
		}

		if ( Pulled( tool, gesture.At( "to" ), run.To, run.Springing + lift, out var end ) )
		{
			span.To = ArchSpanShape.Anchored( tool.Plan, room, end );

			return true;
		}

		return false;
	}

	static bool Pulled( ArchTool tool, ArchGesture gesture, Vector2 at, float height, out Vector2 moved )
	{
		return ArchShapeHandles.Placed( tool, gesture, new Vector3( at.x, at.y, height ), out moved, Gizmo.Colors.Active );
	}
}