Editor/ShaderGraphPlus/Nodes/UVNodes.cs

namespace ShaderGraphPlus.Nodes;

/// <summary>
/// Rotate your texture coordinates.
/// </summary>
[Title( "UV Rotation" ), Category( "Coordinates" ), Icon( "texture" )]
public sealed class UVRotationNode : ShaderNodePlus
{
	[JsonIgnore, Hide, Browsable( false )]
	public override Color NodeTitleColor => Color.Orange.Darken( .2f );

	[Hide]
	public string UVRotation => @"
float2 UVRotation( float2 vUv, float2 vRotationCenter, float flRotation )
{
	vUv = vUv.xy - vRotationCenter; // Offset incoming UV's by the specified Rotation Center. For example, the incoming uv's (0.0,0.0) could become (0.5,0.5).

	// Convert degrees to radians
	flRotation = radians( flRotation );

	// U
	float x = ( vUv.x * cos( flRotation ) ) - ( vUv.y * sin( flRotation ) );

	// V
	float y = ( vUv.x * sin( flRotation ) ) + ( vUv.y * cos( flRotation ) );

	return ( float2( x, y ) - vRotationCenter ); // Output the rotation result and then revert UV's 0,0 to its initial position.
}
";

	[Title( "UV" )]
	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput Coords { get; set; }

	[Title( "Rotation Center" )]
	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput RotationCenter { get; set; }

	[Title( "Rotation" )]
	[Input( typeof( float ) )]
	[Hide]
	public NodeInput Rotation { get; set; }

	[InputDefault( nameof( RotationCenter ) )]
	public Vector2 DefaultRotationCenter { get; set; } = new Vector2( 0.5f, 0.5f );

	[InputDefault( nameof( Rotation ) )]
	public float DefaultRotation { get; set; } = 0.0f;

	[Output( typeof( Vector2 ) )]
	[Hide]
	public NodeResult.Func Result => ( GraphCompiler compiler ) =>
	{
		var incoords = compiler.Result( Coords );
		var rotationcenter = compiler.ResultOrDefault( RotationCenter, DefaultRotationCenter );
		var rotation = compiler.ResultOrDefault( Rotation, DefaultRotation );

		var coords = "";

		if ( compiler.Graph.Domain is ShaderDomain.PostProcess )
		{
			coords = incoords.IsValid ? $"{incoords.Cast( 2 )}" : "CalculateViewportUv( i.vPositionSs.xy )";
		}
		else
		{
			coords = incoords.IsValid ? $"{incoords.Cast( 2 )}" : "i.vTextureCoords.xy";
		}

		string func = compiler.RegisterHLSLFunction( UVRotation, "UVRotation" );
		string funcCall = compiler.ResultHLSLFunction( func, $"{coords}, {rotationcenter}, {rotation}" );

		return new NodeResult( ResultType.Vector2, funcCall );
	};

}

/// <summary>
/// Scale your texture coordinates.
/// </summary>
[Title( "UV Scale" ), Category( "Coordinates" ), Icon( "texture" )]
public sealed class UVScaleNode : ShaderNodePlus
{
	[JsonIgnore, Hide, Browsable( false )]
	public override Color NodeTitleColor => ShaderGraphPlusTheme.NodeHeaderColors.FunctionNode;

	[Hide]
	public string UVScale => @"
	float2 UVScale( float2 vUv, float2 vScale )
	{
		return vUv * vScale;
	}
	";

	[Title( "UV" )]
	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput Coords { get; set; }

	[Title( "Scale" )]
	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput Scale { get; set; }

	[InputDefault( nameof( Scale ) )]
	public Vector2 DefaultScale { get; set; } = new Vector2( 1.0f, 1.0f );

	[Output( typeof( Vector2 ) )]
	[Hide]
	public NodeResult.Func Result => ( GraphCompiler compiler ) =>
	{
		var incoords = compiler.Result( Coords );
		var scale = compiler.ResultOrDefault( Scale, DefaultScale );

		var coords = "";

		if ( compiler.Graph.Domain is ShaderDomain.PostProcess )
		{
			coords = incoords.IsValid ? $"{incoords.Cast( 2 )}" : "CalculateViewportUv( i.vPositionSs.xy )";
		}
		else
		{
			coords = incoords.IsValid ? $"{incoords.Cast( 2 )}" : "i.vTextureCoords.xy";
		}

		//string func = compiler.RegisterHLSLFunction( UVScale );
		//string funcCall = compiler.ResultFunction( func, $"{coords}, {scale}" );

		return new NodeResult( ResultType.Vector2, $"({coords} * {scale})" );
	};

}

/// <summary>
/// Scale your texture coordinates by a specified center point.
/// </summary>
[Title( "UV Scale By Point" ), Category( "Coordinates" ), Icon( "texture" )]
public sealed class UVScaleByPointNode : ShaderNodePlus
{
	[JsonIgnore, Hide, Browsable( false )]
	public override Color NodeTitleColor => ShaderGraphPlusTheme.NodeHeaderColors.FunctionNode;

	[Hide]
	public static string UVScaleByPoint => @"
//  vUv - UV coordinates input.
//  flCenter - Center point to scale from. A flCenter 0f 0.5 would let you scale by the center. 
//  flScale - Amount to scale the UVs by in both the X & Y.
float2 UVScaleByPoint( float2 vUv, float flCenter, float2 flScale )
{
    vUv = vUv - flCenter; // Offset the incoming UV so that 0,0 of the UV is now at the defined center.
    float2 vScale = vUv * flScale;
    float2 vResult = vScale + flCenter; // Return Uv's 0,0 to it's initial position.
    return vResult;
}
";

	[Title( "UV" )]
	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput Coords { get; set; }

	[Title( "Center" )]
	[Input( typeof( float ) )]
	[Hide]
	public NodeInput Center { get; set; }

	[Title( "Scale" )]
	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput Scale { get; set; }

	[InputDefault( nameof( Center ) )]
	public float DefaultCenter { get; set; } = 1.0f;

	[InputDefault( nameof( Scale ) )]
	public Vector2 DefaultScale { get; set; } = new Vector2( 1.0f, 1.0f );

	[Output( typeof( Vector2 ) )]
	[Hide]
	public NodeResult.Func Result => ( GraphCompiler compiler ) =>
	{
		var incoords = compiler.Result( Coords );
		var center = compiler.ResultOrDefault( Center, DefaultCenter );
		var scale = compiler.ResultOrDefault( Scale, DefaultScale );

		var coords = "";

		if ( compiler.Graph.Domain is ShaderDomain.PostProcess )
		{
			coords = incoords.IsValid ? $"{incoords.Cast( 2 )}" : "CalculateViewportUv( i.vPositionSs.xy )";
		}
		else
		{
			coords = incoords.IsValid ? $"{incoords.Cast( 2 )}" : "i.vTextureCoords.xy";
		}


		string func = compiler.RegisterHLSLFunction( UVScaleByPoint, "UVScaleByPoint" );
		string funcCall = compiler.ResultHLSLFunction( func, $"{coords}, {center}, {scale}" );

		return new NodeResult( ResultType.Vector2, funcCall );
	};

}

/// <summary>
/// Scroll your texture coordinates in a particular direction.
/// </summary>
[Title( "UV Scroll" ), Category( "Coordinates" ), Icon( "texture" )]
public sealed class UVScrollNode : ShaderNodePlus
{
	[JsonIgnore, Hide, Browsable( false )]
	public override Color NodeTitleColor => ShaderGraphPlusTheme.NodeHeaderColors.FunctionNode;

	[Hide]
	public static string UVScroll => @"
float2 UVScroll( float flTime, float2 vUv, float2 vScrollSpeed )
{
    return vUv + flTime * vScrollSpeed;
}
";

	[Input( typeof( float ) )]
	[Hide]
	public NodeInput Time { get; set; }

	[Title( "UV" )]
	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput Coords { get; set; }

	/// <summary>
	/// Direction & speed of the scrolling.
	/// </summary>
	[Title( "Scroll Speed" )]
	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput ScrollSpeed { get; set; }

	[InputDefault( nameof( ScrollSpeed ) )]
	public Vector2 DefaultScrollSpeed { get; set; } = new Vector2( 1f, 1f );


	[Output( typeof( Vector2 ) )]
	[Hide]
	public NodeResult.Func Result => ( GraphCompiler compiler ) =>
	{
		var incoords = compiler.Result( Coords );
		var result_time = compiler.Result( Time );
		var time = "";
		var scrollspeed = compiler.ResultOrDefault( ScrollSpeed, DefaultScrollSpeed );


		if ( Time.IsValid() )
		{
			time = result_time.Code;
		}
		else
		{
			time = "g_flTime";
		}

		var coords = "";

		if ( compiler.Graph.Domain is ShaderDomain.PostProcess )
		{
			coords = incoords.IsValid ? $"{incoords.Cast( 2 )}" : "CalculateViewportUv( i.vPositionSs.xy )";
		}
		else
		{
			coords = incoords.IsValid ? $"{incoords.Cast( 2 )}" : "i.vTextureCoords.xy";
		}

		//string func = compiler.RegisterHLSLFunction( UVScroll );
		//string funcCall = compiler.ResultFunction( func, $"{time}, {(coords.IsValid ? $"{coords.Cast( 2 )}" : "i.vTextureCoords.xy")}, {scrollspeed}" );

		return new NodeResult( ResultType.Vector2, $"({coords} + {time} * {scrollspeed})" );
	};
}

[Title( "UV Curve Remap" ), Category( "Coordinates" ), Icon( "texture" )]
public class UVCurveRemapNode : ShaderNodePlus
{
	[JsonIgnore, Hide, Browsable( false )]
	public override Color NodeTitleColor => ShaderGraphPlusTheme.NodeHeaderColors.FunctionNode;

	[Input( typeof( Vector2 ) ), Title( "Coords" )]
	[Hide]
	public NodeInput ScreenUVInput { get; set; }

	[Input( typeof( Vector2 ) ), Title( "Curvature" )]
	[Hide]
	public NodeInput CurvatureInput { get; set; }

	[InputDefault( nameof( CurvatureInput ) )]
	public Vector2 DefaultCurvature { get; set; } = new Vector2( 3.0f, 3.0f );

	[Output( typeof( Vector2 ) )]
	[Hide]
	public NodeResult.Func Result => ( GraphCompiler compiler ) =>
	{
		if ( compiler.Graph.Domain != ShaderDomain.PostProcess )
		{
			return NodeResult.Error( $"'{DisplayInfo.Name}' node is only ment to be used in the '{ShaderDomain.PostProcess}' domain." );
		}

		var coords = compiler.Result( ScreenUVInput );
		var curvature = compiler.ResultOrDefault( CurvatureInput, DefaultCurvature );

		var funcCall = compiler.ResultHLSLFunction( "CurveRemapUV", $"{(coords.IsValid ? $"{coords.Cast( 2 )}" : "i.vPositionSs.xy / g_vRenderTargetSize")}, {curvature}" );

		return new NodeResult( ResultType.Vector2, funcCall );

	};
}

/// <summary>
/// Tile or shift your texture coordinates. Tile works by scaling the texture up
/// and down. Offset works by adding or subtracting from the texture coordinates
/// </summary>
[Title( "UV Tile And Offset" ), Category( "Coordinates" ), Icon( "texture" )]
public sealed class TileAndOffset : ShaderNodePlus
{
	[JsonIgnore, Hide, Browsable( false )]
	public override Color NodeTitleColor => ShaderGraphPlusTheme.NodeHeaderColors.FunctionNode;

	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput UV { get; set; }

	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput Tile { get; set; }

	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput Offset { get; set; }

	[InputDefault( nameof( Tile ) )]
	public Vector2 DefaultTile { get; set; } = Vector2.One;

	[InputDefault( nameof( Offset ) )]
	public Vector2 DefaultOffset { get; set; } = Vector2.Zero;

	public bool WrapTo01 { get; set; } = false;

	[Output( typeof( Vector2 ) )]
	[Hide]
	public NodeResult.Func Result => ( GraphCompiler compiler ) =>
	{
		var incoords = compiler.Result( UV );
		var tile = compiler.ResultOrDefault( Tile, DefaultTile );
		var offset = compiler.ResultOrDefault( Offset, DefaultOffset );

		var coords = "";

		if ( compiler.Graph.Domain is ShaderDomain.PostProcess )
		{
			coords = incoords.IsValid ? $"{incoords.Cast( 2 )}" : "CalculateViewportUv( i.vPositionSs.xy )";
		}
		else
		{
			coords = incoords.IsValid ? $"{incoords.Cast( 2 )}" : "i.vTextureCoords.xy";
		}


		var funcCall = $"TileAndOffsetUv( {coords}," +
			$" {(tile.IsValid ? tile.Cast( 2 ) : "1.0f")}," +
			$" {(offset.IsValid ? offset.Cast( 2 ) : "0.0f")} )";

		if ( compiler.IsPreview )
		{
			funcCall = $"{compiler.ResultValue( WrapTo01 )} ? frac( {funcCall} ) : {funcCall}";
		}
		else if ( WrapTo01 )
		{
			funcCall = $"frac( {funcCall} )";
		}

		return new NodeResult( ResultType.Vector2, funcCall );
	};
}

[Title( "UV FlipBook" ), Category( "Coordinates" ), Icon( "texture" )]
public sealed class FlipBookNode : ShaderNodePlus
{
	[JsonIgnore, Hide, Browsable( false )]
	public override Color NodeTitleColor => ShaderGraphPlusTheme.NodeHeaderColors.FunctionNode;

	[Hide]
	public static string FlipBook => @"
float2 FlipBook( float2 vUV, float flWidth, float flHeight, int nTileIndex, bool InvertX, bool InvertY )
{
	float flTile = fmod( (float)nTileIndex, flWidth * flHeight );
	float2 InvertXY = float2( ( InvertX ? 1 : 0 ), ( InvertY ? 1 : 0 ));

	float2 vtileCount = float2( 1.0f, 1.0f ) / float2( flWidth, flHeight );
	float tileY = abs( InvertXY.y * flHeight - ( floor( flTile * vtileCount.x ) + InvertXY.y * 1 ) );
	float tileX = abs( InvertXY.x * flWidth - ( ( flTile - flWidth * floor( flTile * vtileCount.x) ) + InvertXY.x * 1 ) );
	return ( vUV + float2( tileX, tileY ) ) * vtileCount;
}
";

	[Input( typeof( Vector2 ) )]
	[Hide]
	public NodeInput Coords { get; set; }

	[Input( typeof( float ) )]
	[Hide]
	public NodeInput Width { get; set; }

	[Input( typeof( float ) )]
	[Hide]
	public NodeInput Height { get; set; }

	[Input( typeof( int ) )]
	[Hide]
	public NodeInput TileIndex { get; set; }

	[Input( typeof( bool ) )]
	[Hide]
	public NodeInput InvertX { get; set; }

	[Input( typeof( bool ) )]
	[Hide]
	public NodeInput InvertY { get; set; }

	[InputDefault( nameof( Width ) )]
	public float DefaultWidth { get; set; } = 1.0f;

	[InputDefault( nameof( Height ) )]
	public float DefaultHeight { get; set; } = 1.0f;

	[InputDefault( nameof( TileIndex ) )]
	public int DefaultTileIndex { get; set; } = 1;

	[InputDefault( nameof( InvertX ) )]
	public bool DefaultInvertX { get; set; } = false;

	[InputDefault( nameof( InvertY ) )]
	public bool DefaultInvertY { get; set; } = false;

	[Output( typeof( Vector2 ) ), Title( "Result" )]
	[Hide]
	public NodeResult.Func Result => ( GraphCompiler compiler ) =>
	{
		if ( compiler.Graph.Domain is ShaderDomain.PostProcess )
		{
			return NodeResult.Error( $"{DisplayInfo.Name} Is not ment for postprocessing shaders!" );
		}

		var coords = compiler.Result( Coords );
		var width = compiler.ResultOrDefault( Width, DefaultWidth );
		var height = compiler.ResultOrDefault( Height, DefaultHeight );
		var tileindex = compiler.ResultOrDefault( TileIndex, DefaultTileIndex );
		var invertX = compiler.ResultOrDefault( InvertX, DefaultInvertX );
		var invertY = compiler.ResultOrDefault( InvertY, DefaultInvertY );

		string func = compiler.RegisterHLSLFunction( FlipBook, "FlipBook" );
		string funcCall = compiler.ResultHLSLFunction( func, $"{(coords.IsValid ? $"{coords.Cast( 2 )}" : "i.vTextureCoords.xy")}, {width}, {height}, {tileindex}, {invertX}, {invertY}" );

		return new NodeResult( ResultType.Vector2, funcCall );
	};
}