Editor/Services/ArchDivide.cs

Editor utility for dividing a linear span into bays and nodes. Defines ArchDivision, which computes step, node positions, bay ranges, inner nodes and centres, and ArchDivide static helpers to create divisions by maximum spacing, minimum spacing, fixed-size panels, or explicit count.

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