Code/Mask/MaskField.cs
using System;
using System.Linq;

namespace Sandbox.Mask;


public class MaskField
{
	#region Fields

	public int    Resolution { get; }
	public float  WorldSize  { get; }
	public float[] Values    { get; }
	
	public Vector2 WorldOffset { get; set; } = Vector2.Zero; 

	#endregion

	#region Constructor

	public MaskField( int resolution, float worldSize )
	{
		Resolution = resolution;
		WorldSize  = worldSize;
		Values     = new float[resolution * resolution];
	}

	private MaskField( int resolution, float worldSize, float[] values )
	{
		Resolution = resolution;
		WorldSize  = worldSize;
		Values     = values;
	}

	#endregion

	#region Access

	public float Get( int x, int y )
	{
		x = Math.Clamp( x, 0, Resolution - 1 );
		y = Math.Clamp( y, 0, Resolution - 1 );
		return Values[y * Resolution + x];
	}

	public void Set( int x, int y, float value )
	{
		x = Math.Clamp( x, 0, Resolution - 1 );
		y = Math.Clamp( y, 0, Resolution - 1 );
		Values[y * Resolution + x] = Math.Clamp( value, 0f, 1f );
	}

	public void SetRaw( int x, int y, float value )
	{
		Values[y * Resolution + x] = value;
	}

	#endregion

	#region Sampling

	/// <summary>
	/// Samples the field at a world position using bilinear interpolation.
	/// WorldPos origin (0,0) maps to texel (0,0). 
	/// </summary>
	public float Sample( Vector2 worldPos )
	{
		float u = worldPos.x / WorldSize * (Resolution - 1);
		float v = worldPos.y / WorldSize * (Resolution - 1);

		int x0 = Math.Clamp( (int)MathF.Floor( u ), 0, Resolution - 1 );
		int y0 = Math.Clamp( (int)MathF.Floor( v ), 0, Resolution - 1 );
		int x1 = Math.Clamp( x0 + 1,                0, Resolution - 1 );
		int y1 = Math.Clamp( y0 + 1,                0, Resolution - 1 );

		float tx = u - x0;
		float ty = v - y0;

		float v00 = Get( x0, y0 );
		float v10 = Get( x1, y0 );
		float v01 = Get( x0, y1 );
		float v11 = Get( x1, y1 );

		float top    = MathX.Lerp( v00, v10, tx );
		float bottom = MathX.Lerp( v01, v11, tx );
		return MathX.Lerp( top, bottom, ty );
	}

	public float Sample( Vector3 worldPos ) => Sample( new Vector2( worldPos.x, worldPos.y ) );

	#endregion

	#region Combine Operations

	public MaskField Add( MaskField other )
	{
		var result = Clone();
		var resampled = other.ResampleTo( Resolution, WorldSize );
		for ( int i = 0; i < Values.Length; i++ )
			result.Values[i] = Math.Clamp( Values[i] + resampled.Values[i], 0f, 1f );
		return result;
	}

	public MaskField Subtract( MaskField other )
	{
		var result = Clone();
		var resampled = other.ResampleTo( Resolution, WorldSize );
		for ( int i = 0; i < Values.Length; i++ )
			result.Values[i] = Math.Clamp( Values[i] - resampled.Values[i], 0f, 1f );
		return result;
	}

	public MaskField Multiply( MaskField other )
	{
		var result = Clone(); // Clone needs to copy WorldOffset too
		var resampled = other.ResampleTo( Resolution, WorldSize );
		for ( int i = 0; i < Values.Length; i++ )
			result.Values[i] = Values[i] * resampled.Values[i];
		return result;
	}

	public MaskField Max( MaskField other )
	{
		var result = Clone();
		var resampled = other.ResampleTo( Resolution, WorldSize );
		for ( int i = 0; i < Values.Length; i++ )
			result.Values[i] = MathF.Max( Values[i], resampled.Values[i] );
		return result;
	}

	public MaskField Min( MaskField other )
	{
		var result = Clone();
		var resampled = other.ResampleTo( Resolution, WorldSize );
		for ( int i = 0; i < Values.Length; i++ )
			result.Values[i] = MathF.Min( Values[i], resampled.Values[i] );
		return result;
	}

	public MaskField Lerp( MaskField other, float t )
	{
		var result = Clone();
		var resampled = other.ResampleTo( Resolution, WorldSize );
		for ( int i = 0; i < Values.Length; i++ )
			result.Values[i] = MathX.Lerp( Values[i], resampled.Values[i], t );
		return result;
	}

	public MaskField Invert()
	{
		var result = Clone();
		for ( int i = 0; i < Values.Length; i++ )
			result.Values[i] = 1f - Values[i];
		return result;
	}

	public MaskField Clamp( float min = 0f, float max = 1f )
	{
		var result = Clone();
		for ( int i = 0; i < Values.Length; i++ )
			result.Values[i] = Math.Clamp( Values[i], min, max );
		return result;
	}

	public MaskField Power( float exponent )
	{
		var result = Clone();
		for ( int i = 0; i < Values.Length; i++ )
			result.Values[i] = MathF.Pow( Values[i], exponent );
		return result;
	}

	#endregion

	#region Resampling

	/// <summary>
	/// Resamples this field to a different resolution or world size using bilinear interpolation.
	/// </summary>
	public MaskField ResampleTo( int targetResolution, float targetWorldSize )
	{
		if ( targetResolution == Resolution && MathF.Abs( targetWorldSize - WorldSize ) < 0.001f )
			return this;

		var result = new MaskField( targetResolution, targetWorldSize ) { WorldOffset = WorldOffset }; // preserve offset
		float step = targetWorldSize / (targetResolution - 1);

		for ( int y = 0; y < targetResolution; y++ )
		for ( int x = 0; x < targetResolution; x++ )
		{
			var worldPos = new Vector2( x * step, y * step );
			result.Values[y * targetResolution + x] = Sample( worldPos );
		}

		return result;
	}

	#endregion

	#region Utilities

	public MaskField Clone()
	{
		var copy = new float[Values.Length];
		Array.Copy( Values, copy, Values.Length );
		return new MaskField( Resolution, WorldSize, copy ) { WorldOffset = WorldOffset }; // ADD WorldOffset
	}

	public void Fill( float value )
	{
		value = Math.Clamp( value, 0f, 1f );
		for ( int i = 0; i < Values.Length; i++ )
			Values[i] = value;
	}

	public float GetMin() => Values.Min();
	public float GetMax() => Values.Max();

	public void Normalize()
	{
		float min = GetMin();
		float max = GetMax();
		float range = max - min;
		if ( range < 0.0001f ) return;
		for ( int i = 0; i < Values.Length; i++ )
			Values[i] = (Values[i] - min) / range;
	}

	public Vector2 TexelToWorld( int x, int y )
	{
		float step = WorldSize / (Resolution - 1);
		return WorldOffset + new Vector2( x * step, y * step );
	}

	public (int x, int y) WorldToTexel( Vector2 worldPos )
	{
		int x = Math.Clamp( (int)(worldPos.x / WorldSize * (Resolution - 1)), 0, Resolution - 1 );
		int y = Math.Clamp( (int)(worldPos.y / WorldSize * (Resolution - 1)), 0, Resolution - 1 );
		return (x, y);
	}

	public override string ToString() => $"MaskField [{Resolution}x{Resolution}] world={WorldSize}";

	#endregion
}