Editor/Services/ArchGridService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Sizes always answer; only coordinates check whether snapping is on.
public sealed class ArchGridService {
	public const float FinestSize = 0.25f;

	// A step this small is no ladder at all, so Snap passes the value through rather than dividing by it.
	public const float FreeStep = 0.01f;

	// Below this a wall's direction is noise, so no nearest-wall query offers it as a host.
	public const float LeastWallLength = 0.5f;

	// The angle ladder every authored turn ratchets onto, authored like the size ladder the scene view owns.
	// ZERO is the free swing, which is why every reader asks for it rather than testing a flag of its own.
	public static float AngleStep {
		get => Math.Clamp( EditorCookie.Get( "arch.grid.anglestep", 15f ), 0f, 90f );
		set => EditorCookie.Set( "arch.grid.anglestep", Math.Clamp( value, 0f, 90f ) );
	}

	public static bool Snapping => EditorScene.GizmoSettings.SnapToGrid;

	public float BaseSize => Math.Clamp( EditorScene.GizmoSettings.GridSpacing, FinestSize, 128f );

	public float Base( float value ) {
		return Snapping ? Snap( value, BaseSize ) : value;
	}

	public Vector2 Base( Vector2 point ) {
		return Snapping ? Snap( point, BaseSize ) : point;
	}

	public Vector3 Base( Vector3 point ) {
		return Snapping ? Snap( point, BaseSize ) : point;
	}

	public float Subgrid( float value, int divisions = 8 ) {
		return Snapping ? Snap( value, SubgridSize( divisions ) ) : value;
	}

	public Vector2 Subgrid( Vector2 point, int divisions = 8 ) {
		return Snapping ? Snap( point, SubgridSize( divisions ) ) : point;
	}

	public Vector3 Subgrid( Vector3 point, int divisions = 8 ) {
		return Snapping ? Snap( point, SubgridSize( divisions ) ) : point;
	}

	public Vector3 CurveControl( Vector3 point ) {
		var flat = Base( new Vector2( point.x, point.y ) );

		return new Vector3( flat.x, flat.y, Height( point.z ) );
	}

	// Flat snaps; height does not — terrain heights are measured, not authored.
	public Vector3 Draped( Vector3 point ) {
		var flat = Base( new Vector2( point.x, point.y ) );

		return new Vector3( flat.x, flat.y, point.z );
	}

	// No second grid for z - the same ladder a plan coordinate lands on.
	public float Height( float value ) {
		return Base( value );
	}

	public List<Vector2> Base( IEnumerable<Vector2> points ) {
		return points.Select( Base ).ToList();
	}

	public (Vector2 Min, Vector2 Max) Rectangle( Vector2 first, Vector2 second ) {
		var min = Vector2.Min( first, second );
		var max = Vector2.Max( first, second );

		return (Base( min ), Base( max ));
	}

	public float SubgridSize( int divisions = 8 ) {
		var count = PowerOfTwo( Math.Max( 1, divisions ) );

		return MathF.Max( FinestSize, BaseSize / count );
	}

	public ArchDivision DivideAtMost( float span, float spacing, int divisions = 8 ) {
		var length = MathF.Abs( Subgrid( span, divisions ) );

		if ( length < FinestSize ) {
			return new ArchDivision { Span = 0f, Count = 0 };
		}

		var unit = SubgridSize( divisions );
		var units = Math.Max( 1, (int)MathF.Round( length / unit ) );
		var minimum = Math.Max( 1, (int)MathF.Ceiling( length / MathF.Max( unit, spacing ) ) );
		var count = minimum;

		while ( count < units && units % count != 0 ) {
			count++;
		}

		return new ArchDivision { Span = length, Count = Math.Min( count, units ) };
	}

	public static float Fine( float value ) {
		return Snap( value, FinestSize );
	}

	public static Vector2 Fine( Vector2 point ) {
		return Snap( point, FinestSize );
	}

	public static Vector3 Fine( Vector3 point ) {
		return Snap( point, FinestSize );
	}

	public static float Least( float value, float least ) {
		return MathF.Max( least, Fine( value ) );
	}

	public static float Angle( float degrees ) {
		return Snap( degrees, AngleStep );
	}

	public static float Snap( float value, float size ) {
		return size > FreeStep ? MathF.Round( value / size ) * size : value;
	}

	// Snap the STEP, not the coordinate — existing shapes keep their authored offset.
	public static float Stepped( float from, float to, float size ) {
		return from + Snap( to - from, size );
	}

	public static Vector2 Stepped( Vector2 from, Vector2 to, float size ) {
		return new Vector2( Stepped( from.x, to.x, size ), Stepped( from.y, to.y, size ) );
	}

	static Vector2 Snap( Vector2 point, float size ) {
		return new Vector2( Snap( point.x, size ), Snap( point.y, size ) );
	}

	static Vector3 Snap( Vector3 point, float size ) {
		return new Vector3( Snap( point.x, size ), Snap( point.y, size ), Snap( point.z, size ) );
	}

	static int PowerOfTwo( int value ) {
		var result = 1;

		while ( result < value ) {
			result *= 2;
		}

		return result;
	}
}