Editor/Prism/Nodes/MathNodes.cs

Editor Prism node library for arithmetic and interpolation nodes. It defines helper IR builders and many Prism node types (binary/unary/fused/range/periodic/random) that emit IR calls for math intrinsics used by the Prism shader/node system.

Native Interop
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;

namespace Editor.Prism.Nodes;

// ---------------------------------------------------------------------------------------------------
// WP-6 · node library A — arithmetic.
//
// Every node here is polymorphic: the operands declare the type variable "T", so one Multiply serves
// floats, float2s, float3s and float4s and the solver picks the width. Nothing emits text; everything
// goes through EmitContext, so the same node lowers to both the s&box .shader and the .slang module.
// ---------------------------------------------------------------------------------------------------

/// <summary>Which base an exponential raises.</summary>
public enum PrismExponentBase
{
	/// <summary>e to the power x.</summary>
	Natural,
	/// <summary>2 to the power x.</summary>
	Two
}

/// <summary>Which base a logarithm is taken in.</summary>
public enum PrismLogBase
{
	/// <summary>Natural logarithm.</summary>
	Natural,
	/// <summary>Base-2 logarithm.</summary>
	Two,
	/// <summary>Base-10 logarithm.</summary>
	Ten
}

/// <summary>
/// Small IR-building utilities shared by every node in this package.
/// <para>
/// Nodes never write shader text, so anything that would have been an inline HLSL snippet in the
/// built-in editor is expressed here as a few <see cref="EmitContext"/> calls instead. The handful of
/// operations that genuinely need a function body live as <see cref="HelperFunction"/> fields on the
/// node that owns them, named with the mandatory <c>Prism_</c> prefix.
/// </para>
/// </summary>
internal static class PrismMathHelpers
{
	/// <summary>The smallest magnitude a guarded division will divide by.</summary>
	public const float Epsilon = 1e-6f;

	/// <summary>Clamp to 0..1.</summary>
	public static IrValue Saturate( EmitContext ctx, IrValue value ) =>
		value.IsValid ? ctx.Call( Intrinsic.Saturate, value ) : value;

	/// <summary>Reduce a per-component boolean to a single one, so a scalar socket gets a scalar answer.</summary>
	public static IrValue Reduce( EmitContext ctx, IrValue value, bool all )
	{
		if ( !value.IsValid || value.Type.Components <= 1 ) return value;

		return ctx.Call( all ? Intrinsic.All : Intrinsic.Any, value );
	}

	/// <summary>
	/// <c>a / b</c>, with every component where <c>b</c> is exactly zero replaced by
	/// <paramref name="fallback"/>. Lowered through component-wise <c>select()</c>, never a vector
	/// ternary, so it stays correct when the operands are vectors.
	/// </summary>
	public static IrValue SafeDivide( EmitContext ctx, IrValue a, IrValue b, IrValue fallback )
	{
		if ( !a.IsValid || !b.IsValid ) return IrValue.Invalid;

		var isZero = ctx.Bin( BinaryOp.Equal, b, ctx.Const( 0f ) );
		if ( !isZero.IsValid ) return IrValue.Invalid;

		var denominator = ctx.Select( isZero, ctx.Const( 1f ), b );
		var quotient = ctx.Bin( BinaryOp.Div, a, denominator );

		if ( !quotient.IsValid ) return IrValue.Invalid;
		if ( !fallback.IsValid ) return quotient;

		return ctx.Select( isZero, fallback, quotient );
	}

	/// <summary>The normalised position of <c>value</c> between <c>low</c> and <c>high</c>, guarded against an empty range.</summary>
	public static IrValue InverseLerp( EmitContext ctx, IrValue value, IrValue low, IrValue high )
	{
		var numerator = ctx.Bin( BinaryOp.Sub, value, low );
		var denominator = ctx.Bin( BinaryOp.Sub, high, low );

		return SafeDivide( ctx, numerator, denominator, ctx.Const( 0f ) );
	}
}

/// <summary>
/// The shared shape of a two-operand arithmetic node: two polymorphic operands, one result, and an
/// inline literal per operand so a freshly dropped node already computes something.
/// </summary>
public abstract class BinaryMathNode : PrismNode
{
	/// <summary>Left operand.</summary>
	[In( "T", Name = "A" )] public PortRef A { get; set; }

	/// <summary>Right operand.</summary>
	[In( "T", Name = "B" )] public PortRef B { get; set; }

	/// <summary>The result.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>A</c> is unconnected.</summary>
	[InlineValue( nameof( A ) ), Title( "A" )] public float DefaultA { get; set; }

	/// <summary>The literal used when <c>B</c> is unconnected.</summary>
	[InlineValue( nameof( B ) ), Title( "B" )] public float DefaultB { get; set; } = 1f;

	/// <summary>Combine the two promoted operands into the result.</summary>
	protected abstract IrValue Combine( EmitContext ctx, IrValue a, IrValue b );

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var (a, b) = ctx.InPair( nameof( A ), nameof( B ) );

		if ( !a.IsValid || !b.IsValid ) return;

		ctx.Out( nameof( Out ), Combine( ctx, a, b ) );
	}
}

/// <summary>
/// The shared shape of a one-operand arithmetic node: one polymorphic input, one result of the same
/// type, and an inline literal so the node is useful before anything is wired to it.
/// </summary>
public abstract class UnaryMathNode : PrismNode
{
	/// <summary>The operand.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The result.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>In</c> is unconnected.</summary>
	[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }

	/// <summary>Apply the operation to the operand.</summary>
	protected abstract IrValue Apply( EmitContext ctx, IrValue value );

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.In( nameof( In ) );

		if ( !value.IsValid ) return;

		ctx.Out( nameof( Out ), Apply( ctx, value ) );
	}
}

// ---- basic arithmetic -----------------------------------------------------------------------------

/// <summary>The sum of two values.</summary>
[NodeInfo( Id = "prism.math.add", Title = "Add", Category = "Math/Basic", Icon = "add",
	Keywords = ["+", "plus", "sum", "addition"] )]
[NodeVersion( 1 )]
public sealed class AddNode : BinaryMathNode
{
	/// <summary>Adding nothing should change nothing, so both literals start at zero.</summary>
	public AddNode() => DefaultB = 0f;

	/// <inheritdoc/>
	protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Bin( BinaryOp.Add, a, b );
}

/// <summary>The difference of two values.</summary>
[NodeInfo( Id = "prism.math.subtract", Title = "Subtract", Category = "Math/Basic", Icon = "remove",
	Keywords = ["-", "minus", "difference"] )]
[NodeVersion( 1 )]
public sealed class SubtractNode : BinaryMathNode
{
	/// <summary>Subtracting nothing should change nothing, so both literals start at zero.</summary>
	public SubtractNode() => DefaultB = 0f;

	/// <inheritdoc/>
	protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Bin( BinaryOp.Sub, a, b );
}

/// <summary>The component-wise product of two values.</summary>
[NodeInfo( Id = "prism.math.multiply", Title = "Multiply", Category = "Math/Basic", Icon = "close",
	Keywords = ["*", "times", "product", "scale"] )]
[NodeVersion( 1 )]
public sealed class MultiplyNode : BinaryMathNode
{
	/// <summary>One is the identity for multiplication, so a half-wired node passes its input through.</summary>
	public MultiplyNode() => DefaultA = 1f;

	/// <inheritdoc/>
	protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Bin( BinaryOp.Mul, a, b );
}

/// <summary>The component-wise quotient of two values.</summary>
[NodeInfo( Id = "prism.math.divide", Title = "Divide", Category = "Math/Basic", Icon = "safety_divider",
	Keywords = ["/", "quotient", "ratio", "over"] )]
[NodeVersion( 1 )]
public sealed class DivideNode : BinaryMathNode
{
	/// <summary>Dividing by one is the identity, so a half-wired node passes its input through.</summary>
	public DivideNode() => DefaultA = 1f;

	/// <inheritdoc/>
	protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Bin( BinaryOp.Div, a, b );
}

/// <summary>The remainder of a division, written with the <c>%</c> operator.</summary>
[NodeInfo( Id = "prism.math.modulo", Title = "Modulo", Category = "Math/Basic", Icon = "percent",
	Keywords = ["%", "mod", "remainder"] )]
[NodeVersion( 1 )]
public sealed class ModuloNode : BinaryMathNode
{
	/// <summary>A modulus of one gives the fractional part, which is a useful starting point.</summary>
	public ModuloNode() => DefaultA = 1f;

	/// <inheritdoc/>
	protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Bin( BinaryOp.Mod, a, b );
}

/// <summary>The floating-point remainder, written as an explicit <c>fmod</c> call.</summary>
[NodeInfo( Id = "prism.math.fmod", Title = "FMod", Category = "Math/Basic", Icon = "percent",
	Tier = NodeTier.Advanced, Keywords = ["fmod", "remainder", "modulo"],
	Description = "The same value as Modulo, spelled as an intrinsic call rather than the % operator." )]
[NodeVersion( 1 )]
public sealed class FmodNode : BinaryMathNode
{
	/// <summary>A modulus of one gives the fractional part, which is a useful starting point.</summary>
	public FmodNode() => DefaultA = 1f;

	/// <inheritdoc/>
	protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Call( Intrinsic.Fmod, a, b );
}

/// <summary>One value raised to the power of another.</summary>
[NodeInfo( Id = "prism.math.power", Title = "Power", Category = "Math/Basic", Icon = "superscript",
	Keywords = ["pow", "^", "exponent", "raise"] )]
[NodeVersion( 1 )]
public sealed class PowerNode : BinaryMathNode
{
	/// <summary>A base of one and an exponent of one both leave the other operand alone.</summary>
	public PowerNode() => DefaultA = 1f;

	/// <inheritdoc/>
	protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Call( Intrinsic.Pow, a, b );
}

/// <summary>The smaller of two values, component by component.</summary>
[NodeInfo( Id = "prism.math.min", Title = "Minimum", Category = "Math/Range", Icon = "arrow_downward",
	Keywords = ["min", "smaller", "lower", "floor"] )]
[NodeVersion( 1 )]
public sealed class MinimumNode : BinaryMathNode
{
	/// <summary>Both literals start at zero so the node reads as an ordinary comparison.</summary>
	public MinimumNode() => DefaultB = 0f;

	/// <inheritdoc/>
	protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Call( Intrinsic.Min, a, b );
}

/// <summary>The larger of two values, component by component.</summary>
[NodeInfo( Id = "prism.math.max", Title = "Maximum", Category = "Math/Range", Icon = "arrow_upward",
	Keywords = ["max", "larger", "upper", "ceiling"] )]
[NodeVersion( 1 )]
public sealed class MaximumNode : BinaryMathNode
{
	/// <summary>Both literals start at zero so the node reads as an ordinary comparison.</summary>
	public MaximumNode() => DefaultB = 0f;

	/// <inheritdoc/>
	protected override IrValue Combine( EmitContext ctx, IrValue a, IrValue b ) => ctx.Call( Intrinsic.Max, a, b );
}

// ---- single-operand arithmetic --------------------------------------------------------------------

/// <summary>The magnitude of a value, discarding its sign.</summary>
[NodeInfo( Id = "prism.math.abs", Title = "Absolute", Category = "Math/Basic", Icon = "exposure",
	Keywords = ["abs", "magnitude", "unsigned"] )]
[NodeVersion( 1 )]
public sealed class AbsoluteNode : UnaryMathNode
{
	/// <inheritdoc/>
	protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Abs, value );
}

/// <summary>Minus one, zero or plus one, per component.</summary>
[NodeInfo( Id = "prism.math.sign", Title = "Sign", Category = "Math/Basic", Icon = "trending_flat",
	Keywords = ["sign", "signum", "direction"] )]
[NodeVersion( 1 )]
public sealed class SignNode : UnaryMathNode
{
	/// <inheritdoc/>
	protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Sign, value );
}

/// <summary>Negate a value.</summary>
[NodeInfo( Id = "prism.math.negate", Title = "Negate", Category = "Math/Basic", Icon = "exposure_neg_1",
	Keywords = ["negate", "minus", "invert", "flip sign"] )]
[NodeVersion( 1 )]
public sealed class NegateNode : UnaryMathNode
{
	/// <inheritdoc/>
	protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Un( UnaryOp.Negate, value );
}

/// <summary>The square root of a value.</summary>
[NodeInfo( Id = "prism.math.sqrt", Title = "Square Root", Category = "Math/Advanced", Icon = "square_foot",
	Keywords = ["sqrt", "root", "radical"] )]
[NodeVersion( 1 )]
public sealed class SquareRootNode : UnaryMathNode
{
	/// <summary>A default of one keeps the unconnected result finite and neutral.</summary>
	public SquareRootNode() => DefaultIn = 1f;

	/// <inheritdoc/>
	protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Sqrt, value );
}

/// <summary>One over the square root of a value — the fast normalisation primitive.</summary>
[NodeInfo( Id = "prism.math.rsqrt", Title = "Reciprocal Square Root", Category = "Math/Advanced",
	Icon = "square_foot", Tier = NodeTier.Advanced, Keywords = ["rsqrt", "inverse sqrt", "fast"] )]
[NodeVersion( 1 )]
public sealed class ReciprocalSquareRootNode : UnaryMathNode
{
	/// <summary>A default of one keeps the unconnected result finite and neutral.</summary>
	public ReciprocalSquareRootNode() => DefaultIn = 1f;

	/// <inheritdoc/>
	protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Rsqrt, value );
}

/// <summary>One over a value.</summary>
[NodeInfo( Id = "prism.math.reciprocal", Title = "Reciprocal", Category = "Math/Advanced", Icon = "swap_vert",
	Keywords = ["rcp", "inverse", "one over", "1/x"] )]
[NodeVersion( 1 )]
public sealed class ReciprocalNode : UnaryMathNode
{
	/// <summary>A default of one keeps the unconnected result finite and neutral.</summary>
	public ReciprocalNode() => DefaultIn = 1f;

	/// <inheritdoc/>
	protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Rcp, value );
}

/// <summary>Clamp a value into the zero-to-one range.</summary>
[NodeInfo( Id = "prism.math.saturate", Title = "Saturate", Category = "Math/Range", Icon = "opacity",
	Keywords = ["saturate", "clamp01", "clamp", "0..1"] )]
[NodeVersion( 1 )]
public sealed class SaturateNode : UnaryMathNode
{
	/// <inheritdoc/>
	protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Intrinsic.Saturate, value );
}

/// <summary>Raise a fixed base to a value, in base e or base 2.</summary>
[NodeInfo( Id = "prism.math.exp", Title = "Exponential", Category = "Math/Advanced", Icon = "trending_up",
	Keywords = ["exp", "exp2", "e", "power of two"] )]
[NodeVersion( 1 )]
public sealed class ExponentialNode : UnaryMathNode
{
	/// <summary>Which base to raise.</summary>
	public PrismExponentBase Base { get; set; } = PrismExponentBase.Natural;

	/// <inheritdoc/>
	protected override IrValue Apply( EmitContext ctx, IrValue value ) =>
		ctx.Call( Base == PrismExponentBase.Two ? Intrinsic.Exp2 : Intrinsic.Exp, value );
}

/// <summary>The logarithm of a value, in base e, 2 or 10.</summary>
[NodeInfo( Id = "prism.math.log", Title = "Logarithm", Category = "Math/Advanced", Icon = "show_chart",
	Keywords = ["log", "log2", "log10", "ln"] )]
[NodeVersion( 1 )]
public sealed class LogarithmNode : UnaryMathNode
{
	/// <summary>Which base the logarithm is taken in.</summary>
	public PrismLogBase Base { get; set; } = PrismLogBase.Natural;

	/// <summary>A default of one makes the unconnected result exactly zero in every base.</summary>
	public LogarithmNode() => DefaultIn = 1f;

	/// <inheritdoc/>
	protected override IrValue Apply( EmitContext ctx, IrValue value ) => ctx.Call( Base switch
	{
		PrismLogBase.Two => Intrinsic.Log2,
		PrismLogBase.Ten => Intrinsic.Log10,
		_ => Intrinsic.Log
	}, value );
}

// ---- range and interpolation ----------------------------------------------------------------------

/// <summary>Constrain a value to a range.</summary>
[NodeInfo( Id = "prism.math.clamp", Title = "Clamp", Category = "Math/Range", Icon = "compress",
	Keywords = ["clamp", "limit", "bound", "range"] )]
[NodeVersion( 1 )]
public sealed class ClampNode : PrismNode
{
	/// <summary>The value to constrain.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The lower bound.</summary>
	[In( "T", Name = "Min" )] public PortRef Min { get; set; }

	/// <summary>The upper bound.</summary>
	[In( "T", Name = "Max" )] public PortRef Max { get; set; }

	/// <summary>The constrained value.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>In</c> is unconnected.</summary>
	[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }

	/// <summary>The literal used when <c>Min</c> is unconnected.</summary>
	[InlineValue( nameof( Min ) ), Title( "Min" )] public float DefaultMin { get; set; }

	/// <summary>The literal used when <c>Max</c> is unconnected.</summary>
	[InlineValue( nameof( Max ) ), Title( "Max" )] public float DefaultMax { get; set; } = 1f;

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx is null ) return;
		if ( ctx.IsConnected( nameof( Min ) ) || ctx.IsConnected( nameof( Max ) ) ) return;
		if ( DefaultMin <= DefaultMax ) return;

		ctx.Warn( $"The lower bound ({DefaultMin}) is above the upper bound ({DefaultMax}); every result will be the upper bound",
			nameof( Min ) );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.In( nameof( In ) );
		var low = ctx.In( nameof( Min ) );
		var high = ctx.In( nameof( Max ) );

		if ( !value.IsValid || !low.IsValid || !high.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Clamp, value, low, high ) );
	}
}

/// <summary>Blend linearly between two values.</summary>
[NodeInfo( Id = "prism.math.lerp", Title = "Lerp", Category = "Math/Interpolation", Icon = "linear_scale",
	Keywords = ["mix", "blend", "interpolate", "lerp"] )]
[NodeVersion( 1 )]
public sealed class LerpNode : PrismNode
{
	/// <summary>The value returned when the fraction is zero.</summary>
	[In( "T", Name = "A" )] public PortRef A { get; set; }

	/// <summary>The value returned when the fraction is one.</summary>
	[In( "T", Name = "B" )] public PortRef B { get; set; }

	/// <summary>How far between <c>A</c> and <c>B</c> the result sits.</summary>
	[In( "T", Name = "T" )] public PortRef T { get; set; }

	/// <summary>The blended value.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>A</c> is unconnected.</summary>
	[InlineValue( nameof( A ) ), Title( "A" )] public float DefaultA { get; set; }

	/// <summary>The literal used when <c>B</c> is unconnected.</summary>
	[InlineValue( nameof( B ) ), Title( "B" )] public float DefaultB { get; set; } = 1f;

	/// <summary>The literal used when <c>T</c> is unconnected.</summary>
	[InlineValue( nameof( T ) ), Title( "T" ), Range( 0f, 1f )] public float DefaultT { get; set; } = 0.5f;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var (a, b) = ctx.InPair( nameof( A ), nameof( B ) );
		var t = ctx.In( nameof( T ) );

		if ( !a.IsValid || !b.IsValid || !t.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Lerp, a, b, t ) );
	}
}

/// <summary>Where a value sits between two others — the inverse of a Lerp.</summary>
[NodeInfo( Id = "prism.math.inverseLerp", Title = "Inverse Lerp", Category = "Math/Interpolation",
	Icon = "swap_horiz", Keywords = ["unlerp", "inverse", "normalize", "fraction"] )]
[NodeVersion( 1 )]
public sealed class InverseLerpNode : PrismNode
{
	/// <summary>The value that maps to zero.</summary>
	[In( "T", Name = "A" )] public PortRef A { get; set; }

	/// <summary>The value that maps to one.</summary>
	[In( "T", Name = "B" )] public PortRef B { get; set; }

	/// <summary>The value to locate.</summary>
	[In( "T", Name = "T" )] public PortRef T { get; set; }

	/// <summary>The normalised position.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>A</c> is unconnected.</summary>
	[InlineValue( nameof( A ) ), Title( "A" )] public float DefaultA { get; set; }

	/// <summary>The literal used when <c>B</c> is unconnected.</summary>
	[InlineValue( nameof( B ) ), Title( "B" )] public float DefaultB { get; set; } = 1f;

	/// <summary>The literal used when <c>T</c> is unconnected.</summary>
	[InlineValue( nameof( T ) ), Title( "T" )] public float DefaultT { get; set; } = 0.5f;

	/// <summary>Clamp the result to 0..1 instead of letting it run past the ends of the range.</summary>
	public bool Clamp { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var a = ctx.In( nameof( A ) );
		var b = ctx.In( nameof( B ) );
		var t = ctx.In( nameof( T ) );

		if ( !a.IsValid || !b.IsValid || !t.IsValid ) return;

		var result = PrismMathHelpers.InverseLerp( ctx, t, a, b );

		if ( Clamp ) result = PrismMathHelpers.Saturate( ctx, result );

		ctx.Out( nameof( Out ), result );
	}
}

/// <summary>Move a value from one range into another.</summary>
[NodeInfo( Id = "prism.math.remap", Title = "Remap", Category = "Math/Range", Icon = "tune",
	Keywords = ["remap", "range", "rescale", "fit"] )]
[NodeVersion( 1 )]
public sealed class RemapNode : PrismNode
{
	/// <summary>The value to remap.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The bottom of the input range.</summary>
	[In( "T", Name = "In Min" )] public PortRef InMin { get; set; }

	/// <summary>The top of the input range.</summary>
	[In( "T", Name = "In Max" )] public PortRef InMax { get; set; }

	/// <summary>The bottom of the output range.</summary>
	[In( "T", Name = "Out Min" )] public PortRef OutMin { get; set; }

	/// <summary>The top of the output range.</summary>
	[In( "T", Name = "Out Max" )] public PortRef OutMax { get; set; }

	/// <summary>The remapped value.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>In</c> is unconnected.</summary>
	[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; } = 0.5f;

	/// <summary>The literal used when <c>In Min</c> is unconnected.</summary>
	[InlineValue( nameof( InMin ) ), Title( "In Min" )] public float DefaultInMin { get; set; }

	/// <summary>The literal used when <c>In Max</c> is unconnected.</summary>
	[InlineValue( nameof( InMax ) ), Title( "In Max" )] public float DefaultInMax { get; set; } = 1f;

	/// <summary>The literal used when <c>Out Min</c> is unconnected.</summary>
	[InlineValue( nameof( OutMin ) ), Title( "Out Min" )] public float DefaultOutMin { get; set; }

	/// <summary>The literal used when <c>Out Max</c> is unconnected.</summary>
	[InlineValue( nameof( OutMax ) ), Title( "Out Max" )] public float DefaultOutMax { get; set; } = 1f;

	/// <summary>Clamp to the output range instead of extrapolating past its ends.</summary>
	public bool Clamp { get; set; } = true;

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx is null ) return;
		if ( ctx.IsConnected( nameof( InMin ) ) || ctx.IsConnected( nameof( InMax ) ) ) return;
		if ( DefaultInMin != DefaultInMax ) return;

		ctx.Warn( "The input range is empty, so every result will be the bottom of the output range",
			nameof( InMin ) );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.In( nameof( In ) );
		var inMin = ctx.In( nameof( InMin ) );
		var inMax = ctx.In( nameof( InMax ) );
		var outMin = ctx.In( nameof( OutMin ) );
		var outMax = ctx.In( nameof( OutMax ) );

		if ( !value.IsValid || !inMin.IsValid || !inMax.IsValid || !outMin.IsValid || !outMax.IsValid ) return;

		var normalized = PrismMathHelpers.InverseLerp( ctx, value, inMin, inMax );

		if ( Clamp ) normalized = PrismMathHelpers.Saturate( ctx, normalized );

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Lerp, outMin, outMax, normalized ) );
	}
}

/// <summary>Zero below an edge and one above it — a hard threshold.</summary>
[NodeInfo( Id = "prism.logic.step", Title = "Step", Category = "Math/Interpolation", Icon = "stairs",
	Keywords = ["step", "threshold", "cutoff", "edge"] )]
[NodeVersion( 1 )]
public sealed class StepNode : PrismNode
{
	/// <summary>The value to test.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The threshold the value is compared against.</summary>
	[In( "T", Name = "Edge" )] public PortRef Edge { get; set; }

	/// <summary>Zero or one.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>In</c> is unconnected.</summary>
	[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }

	/// <summary>The literal used when <c>Edge</c> is unconnected.</summary>
	[InlineValue( nameof( Edge ) ), Title( "Edge" )] public float DefaultEdge { get; set; } = 0.5f;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var (value, edge) = ctx.InPair( nameof( In ), nameof( Edge ) );

		if ( !value.IsValid || !edge.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Step, edge, value ) );
	}
}

/// <summary>A smooth Hermite ramp between two edges.</summary>
[NodeInfo( Id = "prism.logic.smoothstep", Title = "Smoothstep", Category = "Math/Interpolation",
	Icon = "gradient", Keywords = ["smoothstep", "ramp", "ease", "hermite"] )]
[NodeVersion( 1 )]
public sealed class SmoothStepNode : PrismNode
{
	/// <summary>The value to ramp.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The value that maps to zero.</summary>
	[In( "T", Name = "Low" )] public PortRef Low { get; set; }

	/// <summary>The value that maps to one.</summary>
	[In( "T", Name = "High" )] public PortRef High { get; set; }

	/// <summary>The eased result.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>In</c> is unconnected.</summary>
	[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; } = 0.5f;

	/// <summary>The literal used when <c>Low</c> is unconnected.</summary>
	[InlineValue( nameof( Low ) ), Title( "Low" )] public float DefaultLow { get; set; }

	/// <summary>The literal used when <c>High</c> is unconnected.</summary>
	[InlineValue( nameof( High ) ), Title( "High" )] public float DefaultHigh { get; set; } = 1f;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.In( nameof( In ) );
		var low = ctx.In( nameof( Low ) );
		var high = ctx.In( nameof( High ) );

		if ( !value.IsValid || !low.IsValid || !high.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.SmoothStep, low, high, value ) );
	}
}

/// <summary>Ken Perlin's second-order smoothstep: zero first <em>and</em> second derivatives at both ends.</summary>
[NodeInfo( Id = "prism.math.smootherStep", Title = "Smoother Step", Category = "Math/Interpolation",
	Icon = "gradient", Tier = NodeTier.Advanced,
	Keywords = ["smootherstep", "quintic", "ease", "perlin"] )]
[NodeVersion( 1 )]
public sealed class SmootherStepNode : PrismNode
{
	/// <summary>The value to ramp.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The value that maps to zero.</summary>
	[In( "T", Name = "Low" )] public PortRef Low { get; set; }

	/// <summary>The value that maps to one.</summary>
	[In( "T", Name = "High" )] public PortRef High { get; set; }

	/// <summary>The eased result.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>In</c> is unconnected.</summary>
	[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; } = 0.5f;

	/// <summary>The literal used when <c>Low</c> is unconnected.</summary>
	[InlineValue( nameof( Low ) ), Title( "Low" )] public float DefaultLow { get; set; }

	/// <summary>The literal used when <c>High</c> is unconnected.</summary>
	[InlineValue( nameof( High ) ), Title( "High" )] public float DefaultHigh { get; set; } = 1f;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.In( nameof( In ) );
		var low = ctx.In( nameof( Low ) );
		var high = ctx.In( nameof( High ) );

		if ( !value.IsValid || !low.IsValid || !high.IsValid ) return;

		var t = ctx.Let( "t", PrismMathHelpers.Saturate( ctx, PrismMathHelpers.InverseLerp( ctx, value, low, high ) ) );

		if ( !t.IsValid ) return;

		// t * t * t * ( t * ( t * 6 - 15 ) + 10 )
		var inner = ctx.Bin( BinaryOp.Sub, ctx.Bin( BinaryOp.Mul, t, ctx.Const( 6f ) ), ctx.Const( 15f ) );
		var poly = ctx.Bin( BinaryOp.Add, ctx.Bin( BinaryOp.Mul, t, inner ), ctx.Const( 10f ) );
		var cube = ctx.Bin( BinaryOp.Mul, ctx.Bin( BinaryOp.Mul, t, t ), t );

		ctx.Out( nameof( Out ), ctx.Bin( BinaryOp.Mul, cube, poly ) );
	}
}

// ---- periodic and guarded arithmetic --------------------------------------------------------------

/// <summary>Fold a value back into a range, repeating it forever in both directions.</summary>
[NodeInfo( Id = "prism.math.wrap", Title = "Wrap", Category = "Math/Range", Icon = "wrap_text",
	Keywords = ["wrap", "repeat", "tile", "cycle", "modulo"] )]
[NodeVersion( 1 )]
public sealed class WrapNode : PrismNode
{
	/// <summary>The value to fold.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The bottom of the range.</summary>
	[In( "T", Name = "Min" )] public PortRef Min { get; set; }

	/// <summary>The top of the range.</summary>
	[In( "T", Name = "Max" )] public PortRef Max { get; set; }

	/// <summary>The folded value, always inside the range.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>In</c> is unconnected.</summary>
	[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }

	/// <summary>The literal used when <c>Min</c> is unconnected.</summary>
	[InlineValue( nameof( Min ) ), Title( "Min" )] public float DefaultMin { get; set; }

	/// <summary>The literal used when <c>Max</c> is unconnected.</summary>
	[InlineValue( nameof( Max ) ), Title( "Max" )] public float DefaultMax { get; set; } = 1f;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.In( nameof( In ) );
		var low = ctx.In( nameof( Min ) );
		var high = ctx.In( nameof( Max ) );

		if ( !value.IsValid || !low.IsValid || !high.IsValid ) return;

		var range = ctx.Let( "range", ctx.Bin( BinaryOp.Sub, high, low ) );

		if ( !range.IsValid ) return;

		var empty = ctx.Bin( BinaryOp.Equal, range, ctx.Const( 0f ) );
		var safeRange = ctx.Select( empty, ctx.Const( 1f ), range );

		// in - range * floor( ( in - min ) / range )
		var offset = ctx.Bin( BinaryOp.Sub, value, low );
		var turns = ctx.Call( Intrinsic.Floor, ctx.Bin( BinaryOp.Div, offset, safeRange ) );
		var wrapped = ctx.Bin( BinaryOp.Sub, value, ctx.Bin( BinaryOp.Mul, safeRange, turns ) );

		ctx.Out( nameof( Out ), ctx.Select( empty, low, wrapped ) );
	}
}

/// <summary>Bounce a value back and forth between zero and a length.</summary>
[NodeInfo( Id = "prism.math.pingPong", Title = "Ping Pong", Category = "Math/Range", Icon = "sync_alt",
	Keywords = ["pingpong", "bounce", "triangle", "yoyo"] )]
[NodeVersion( 1 )]
public sealed class PingPongNode : PrismNode
{
	/// <summary>The value to bounce.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The distance travelled before the value turns around.</summary>
	[In( "T", Name = "Length" )] public PortRef Length { get; set; }

	/// <summary>The bounced value, always between zero and <c>Length</c>.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>In</c> is unconnected.</summary>
	[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }

	/// <summary>The literal used when <c>Length</c> is unconnected.</summary>
	[InlineValue( nameof( Length ) ), Title( "Length" )] public float DefaultLength { get; set; } = 1f;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var (value, length) = ctx.InPair( nameof( In ), nameof( Length ) );

		if ( !value.IsValid || !length.IsValid ) return;

		var span = ctx.Let( "span", length );
		var period = ctx.Bin( BinaryOp.Mul, span, ctx.Const( 2f ) );
		var empty = ctx.Bin( BinaryOp.Equal, period, ctx.Const( 0f ) );
		var safePeriod = ctx.Select( empty, ctx.Const( 1f ), period );

		// length - abs( fmod( in, 2 * length ) - length )
		var folded = ctx.Call( Intrinsic.Fmod, value, safePeriod );
		var distance = ctx.Call( Intrinsic.Abs, ctx.Bin( BinaryOp.Sub, folded, span ) );
		var result = ctx.Bin( BinaryOp.Sub, span, distance );

		ctx.Out( nameof( Out ), ctx.Select( empty, ctx.Const( 0f ), result ) );
	}
}

/// <summary>Divide, substituting a chosen value wherever the divisor is exactly zero.</summary>
[NodeInfo( Id = "prism.math.safeDivide", Title = "Safe Divide", Category = "Math/Basic", Icon = "shield",
	Keywords = ["divide", "guard", "zero", "nan", "safe"] )]
[NodeVersion( 1 )]
public sealed class SafeDivideNode : PrismNode
{
	/// <summary>The numerator.</summary>
	[In( "T", Name = "A" )] public PortRef A { get; set; }

	/// <summary>The denominator.</summary>
	[In( "T", Name = "B" )] public PortRef B { get; set; }

	/// <summary>The value used wherever the denominator is zero.</summary>
	[In( "T", Name = "Fallback" )] public PortRef Fallback { get; set; }

	/// <summary>The quotient.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>A</c> is unconnected.</summary>
	[InlineValue( nameof( A ) ), Title( "A" )] public float DefaultA { get; set; } = 1f;

	/// <summary>The literal used when <c>B</c> is unconnected.</summary>
	[InlineValue( nameof( B ) ), Title( "B" )] public float DefaultB { get; set; } = 1f;

	/// <summary>The literal used when <c>Fallback</c> is unconnected.</summary>
	[InlineValue( nameof( Fallback ) ), Title( "Fallback" )] public float DefaultFallback { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var (a, b) = ctx.InPair( nameof( A ), nameof( B ) );
		var fallback = ctx.In( nameof( Fallback ) );

		if ( !a.IsValid || !b.IsValid ) return;

		ctx.Out( nameof( Out ), PrismMathHelpers.SafeDivide( ctx, a, b, fallback ) );
	}
}

// ---- fused forms ----------------------------------------------------------------------------------

/// <summary>Multiply by a scale and add an offset in one operation.</summary>
[NodeInfo( Id = "prism.math.scaleAndOffset", Title = "Scale And Offset", Category = "Math/Basic",
	Icon = "open_in_full", Keywords = ["scale", "bias", "offset", "mad", "affine"] )]
[NodeVersion( 1 )]
public sealed class ScaleAndOffsetNode : PrismNode
{
	/// <summary>The value to transform.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The multiplier.</summary>
	[In( "T", Name = "Scale" )] public PortRef Scale { get; set; }

	/// <summary>The addend.</summary>
	[In( "T", Name = "Offset" )] public PortRef Offset { get; set; }

	/// <summary>The transformed value.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>In</c> is unconnected.</summary>
	[InlineValue( nameof( In ) ), Title( "In" )] public float DefaultIn { get; set; }

	/// <summary>The literal used when <c>Scale</c> is unconnected.</summary>
	[InlineValue( nameof( Scale ) ), Title( "Scale" )] public float DefaultScale { get; set; } = 1f;

	/// <summary>The literal used when <c>Offset</c> is unconnected.</summary>
	[InlineValue( nameof( Offset ) ), Title( "Offset" )] public float DefaultOffset { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.In( nameof( In ) );
		var scale = ctx.In( nameof( Scale ) );
		var offset = ctx.In( nameof( Offset ) );

		if ( !value.IsValid || !scale.IsValid || !offset.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Mad, value, scale, offset ) );
	}
}

/// <summary><c>A * B + C</c>, emitted as a single multiply-add.</summary>
[NodeInfo( Id = "prism.math.multiplyAdd", Title = "Multiply Add", Category = "Math/Basic",
	Icon = "calculate", Keywords = ["mad", "fma", "multiply add", "a*b+c"] )]
[NodeVersion( 1 )]
public sealed class MultiplyAddNode : PrismNode
{
	/// <summary>First factor.</summary>
	[In( "T", Name = "A" )] public PortRef A { get; set; }

	/// <summary>Second factor.</summary>
	[In( "T", Name = "B" )] public PortRef B { get; set; }

	/// <summary>The addend.</summary>
	[In( "T", Name = "C" )] public PortRef C { get; set; }

	/// <summary>The result.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>A</c> is unconnected.</summary>
	[InlineValue( nameof( A ) ), Title( "A" )] public float DefaultA { get; set; } = 1f;

	/// <summary>The literal used when <c>B</c> is unconnected.</summary>
	[InlineValue( nameof( B ) ), Title( "B" )] public float DefaultB { get; set; } = 1f;

	/// <summary>The literal used when <c>C</c> is unconnected.</summary>
	[InlineValue( nameof( C ) ), Title( "C" )] public float DefaultC { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var a = ctx.In( nameof( A ) );
		var b = ctx.In( nameof( B ) );
		var c = ctx.In( nameof( C ) );

		if ( !a.IsValid || !b.IsValid || !c.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Mad, a, b, c ) );
	}
}

/// <summary>A repeatable pseudo-random value in a range, derived from a two-dimensional seed.</summary>
[NodeInfo( Id = "prism.math.randomRange", Title = "Random Range", Category = "Math/Advanced",
	Icon = "casino", Keywords = ["random", "hash", "noise", "rand", "seed"] )]
[NodeVersion( 1 )]
public sealed class RandomRangeNode : PrismNode
{
	/// <summary>The seed the value is derived from. The same seed always gives the same result.</summary>
	[In( "float2", Name = "Seed" )] public PortRef Seed { get; set; }

	/// <summary>The lowest value that can be produced.</summary>
	[In( "float", Name = "Min" )] public PortRef Min { get; set; }

	/// <summary>The highest value that can be produced.</summary>
	[In( "float", Name = "Max" )] public PortRef Max { get; set; }

	/// <summary>The pseudo-random value.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>Seed</c> is unconnected.</summary>
	[InlineValue( nameof( Seed ) ), Title( "Seed" )] public Vector2 DefaultSeed { get; set; } = new( 0f, 0f );

	/// <summary>The literal used when <c>Min</c> is unconnected.</summary>
	[InlineValue( nameof( Min ) ), Title( "Min" )] public float DefaultMin { get; set; }

	/// <summary>The literal used when <c>Max</c> is unconnected.</summary>
	[InlineValue( nameof( Max ) ), Title( "Max" )] public float DefaultMax { get; set; } = 1f;

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var seed = ctx.InAs( nameof( Seed ), ShaderType.Float2 );
		var low = ctx.In( nameof( Min ) );
		var high = ctx.In( nameof( Max ) );

		if ( !seed.IsValid || !low.IsValid || !high.IsValid ) return;

		// frac( sin( dot( seed, float2( 12.9898, 78.233 ) ) ) * 43758.5453 )
		var dot = ctx.Call( Intrinsic.Dot, seed, ctx.Const( new Vector2( 12.9898f, 78.233f ) ) );
		var wave = ctx.Bin( BinaryOp.Mul, ctx.Call( Intrinsic.Sin, dot ), ctx.Const( 43758.5453f ) );
		var hash = ctx.Call( Intrinsic.Frac, wave );

		if ( !hash.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Lerp, low, high, hash ) );
	}
}