Editor/Roof/ArchRoofRake.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// One edge of the deck, dragged. A fall is a thing you aim at a street, and typing a pitch into a box is the one
// way of saying it nobody can picture - so this is the inverse of ArchRoofPlane.At: told what an edge should stand
// at, it hands back the style, the pitch and the direction of fall that put it there.
public static class ArchRoofRake {
	// Only an edge standing at ONE height along its whole run. A flat deck has four of them; a shed has the two it
	// falls between. A gable eave is level too, but dropping it would have to argue with the ridge over it.
	public static bool Carries( ArchRoofPart roof, Vector2 outward ) {
		if ( !Axial( outward, out var acrossY ) ) {
			return false;
		}

		return roof.Style == RoofStyle.Flat || (roof.Style == RoofStyle.Shed && acrossY == roof.RidgeAlongX);
	}

	// Every edge worth a grip, as the point it is grabbed at and the height it stands at now - read through the
	// same resolve the deck is lofted from, so the widget cannot sit off the surface it is dragging.
	public static IEnumerable<(int Index, Vector2 At, Vector2 Outward, float Height)> Grips( ArchRoofPart roof ) {
		var outline = ArchFootprint.Wind( roof.Outline() );

		for ( var index = 0; index < outline.Count; index++ ) {
			var from = outline[index];
			var to = outline[(index + 1) % outline.Count];

			if ( (to - from).Length < 1f ) {
				continue;
			}

			var outward = ArchRegion.Outward( from, to );

			if ( !Carries( roof, outward ) ) {
				continue;
			}

			var at = (from + to) * 0.5f;

			yield return (index, at, outward, ArchRoofPlane.At( roof, at ));
		}
	}

	// The opposite edge keeps the height it already had, so one drag moves one edge. Level again means flat again -
	// the same gesture undoes itself rather than leaving a shed of no pitch behind.
	public static void Apply( ArchRoofPart roof, Vector2 at, Vector2 outward, float height ) {
		if ( !Axial( outward, out var acrossY ) ) {
			return;
		}

		ArchFootprint.Bounds( ArchFootprint.Wind( roof.Outline() ), out var min, out var max );

		var low = acrossY ? min.y : min.x;
		var high = acrossY ? max.y : max.x;
		var span = MathF.Max( 1f, high - low );
		var atMax = (acrossY ? outward.y : outward.x) > 0f;
		var far = ArchRoofPlane.At( roof, Across( at, acrossY, atMax ? low : high ) );
		var drop = MathF.Abs( height - far );

		roof.RidgeAlongX = acrossY;
		roof.BaseHeight = MathF.Min( height, far );

		if ( drop < 0.5f ) {
			roof.Style = RoofStyle.Flat;

			return;
		}

		roof.Style = RoofStyle.Shed;
		roof.Pitch = MathF.Atan( drop / span ).RadianToDegree();
		roof.Reversed = height < far ? atMax : !atMax;
	}

	static Vector2 Across( Vector2 at, bool acrossY, float value ) {
		return acrossY ? new Vector2( at.x, value ) : new Vector2( value, at.y );
	}

	static bool Axial( Vector2 outward, out bool acrossY ) {
		acrossY = MathF.Abs( outward.y ) > MathF.Abs( outward.x );

		return (acrossY ? MathF.Abs( outward.y ) : MathF.Abs( outward.x )) > 0.99f;
	}
}