Editor/Services/ArchDivide.cs
using System;
using System.Collections.Generic;

namespace Sunless.Architecture;

// Spacing only decides how MANY bays; the step is span/count, so the last lands exactly on the end.
public readonly struct ArchDivision {
	public float Span { get; init; }
	public int Count { get; init; }

	public float Step => Count > 0 ? Span / Count : 0f;

	public float At( int index ) => Step * index;

	public (float From, float To) Bay( int index ) => (At( index ), At( index + 1 ));

	// Includes both ends: a corner lands on a post, never between two.
	public IEnumerable<float> Nodes {
		get {
			for ( var index = 0; index <= Count; index++ ) {
				yield return At( index );
			}
		}
	}

	public IEnumerable<float> Inner {
		get {
			for ( var index = 1; index < Count; index++ ) {
				yield return At( index );
			}
		}
	}

	public IEnumerable<float> Centres {
		get {
			for ( var index = 0; index < Count; index++ ) {
				yield return At( index ) + Step * 0.5f;
			}
		}
	}

	public IEnumerable<(float From, float To)> Bays {
		get {
			for ( var index = 0; index < Count; index++ ) {
				yield return Bay( index );
			}
		}
	}
}

// Even division in one place - generators used to disagree whether spacing was a ceiling or a floor.
public static class ArchDivide {
	// Spacing is a MAXIMUM: no bay comes out longer - the only answer that covers the span.
	public static ArchDivision AtMost( float span, float spacing ) {
		return Cut( span, spacing < 0.05f ? 1f : MathF.Ceiling( span / spacing ) );
	}

	// Spacing is a MINIMUM: no bay comes out shorter, so what stands in them cannot touch.
	public static ArchDivision AtLeast( float span, float spacing ) {
		return Cut( span, spacing < 0.05f ? 1f : MathF.Floor( span / spacing ) );
	}

	public static ArchDivision Into( float span, int count ) {
		return Cut( span, Math.Max( 1, count ) );
	}

	// Fixed panels do not stretch: a too-short remainder snaps the last node onto the end instead of leaving a sliver.
	public static List<float> Fixed( float span, float size ) {
		var nodes = new List<float> { 0f };

		if ( span < 0.05f ) {
			return nodes;
		}

		var pitch = MathF.Max( 1f, size );
		var whole = (int)MathF.Floor( span / pitch );

		for ( var index = 1; index <= whole; index++ ) {
			nodes.Add( index * pitch );
		}

		if ( span - nodes[^1] < pitch * 0.15f && nodes.Count > 1 ) {
			nodes[^1] = span;

			return nodes;
		}

		if ( span - nodes[^1] > 0.05f ) {
			nodes.Add( span );
		}

		return nodes;
	}

	// A zero span has no bays and one node - a single column under a drag with no reach.
	static ArchDivision Cut( float span, float count ) {
		return new ArchDivision {
			Span = span,
			Count = span < 0.05f ? 0 : Math.Max( 1, (int)count )
		};
	}
}