Editor/Prism/Nodes/VectorNodes.cs

Editor-side collection of Prism shader graph nodes for vector and matrix math. Defines enums for coordinate spaces, transform kinds and matrix shapes, shared HLSL helper functions (Rodrigues rotation, TRS apply, matrix inverses), and many PrismNode implementations providing dot/cross/length/normalize/reflect/refract/transform, spherical/polar conversions, matrix construct/split/transpose/determinant/inverse/multiply/outer product, and legacy-compatible TRS and normal transform nodes. Nodes emit IR calls and helpers to generate HLSL/Slang code.

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 — vector geometry, coordinate systems and matrices.
//
// Everything that can be expressed as an intrinsic is one; the four operations that genuinely need a
// function body (Rodrigues rotation and the three matrix inverses) are HelperFunctions with the
// mandatory Prism_ prefix, declared once and shared, so a graph using two of them still emits one copy.
// ---------------------------------------------------------------------------------------------------

/// <summary>A coordinate system a vector can be expressed in.</summary>
public enum PrismCoordinateSpace
{
	/// <summary>The mesh's own space, before the object transform.</summary>
	Object,
	/// <summary>Scene space.</summary>
	World,
	/// <summary>Camera space: the camera sits at the origin.</summary>
	View,
	/// <summary>The surface's tangent frame, as normal maps are authored in.</summary>
	Tangent
}

/// <summary>What a value being transformed represents, which decides how the transform is applied.</summary>
public enum PrismTransformKind
{
	/// <summary>A point. Translation applies.</summary>
	Position,
	/// <summary>A direction or offset. Translation does not apply.</summary>
	Direction,
	/// <summary>A direction that is renormalised afterwards.</summary>
	Normal
}

/// <summary>The square matrix sizes Prism's matrix nodes work with.</summary>
public enum PrismMatrixSize
{
	/// <summary>A 2x2 matrix.</summary>
	Matrix2x2 = 2,
	/// <summary>A 3x3 matrix.</summary>
	Matrix3x3 = 3,
	/// <summary>A 4x4 matrix.</summary>
	Matrix4x4 = 4
}

/// <summary>Whether a matrix is assembled from, or taken apart into, its rows or its columns.</summary>
public enum PrismMatrixOrder
{
	/// <summary>Rows, matching HLSL's own <c>floatNxM( row, row, … )</c> constructor.</summary>
	Rows,
	/// <summary>Columns.</summary>
	Columns
}

/// <summary>What the right-hand operand of a matrix multiply is.</summary>
public enum PrismMatrixOperand
{
	/// <summary>A vector: the result is a vector.</summary>
	Vector,
	/// <summary>Another matrix of the same size: the result is a matrix.</summary>
	Matrix
}

/// <summary>
/// The function bodies the vector and matrix nodes share. Every one is emitted at most once per
/// module, deduplicated by name, and carries only an HLSL body because Slang parses all of it verbatim.
/// </summary>
internal static class PrismMatrixHelpers
{
	/// <summary>Rodrigues' rotation formula, matching the reference implementation exactly.</summary>
	public static readonly HelperFunction RotateAboutAxis = new( "Prism_RotateAboutAxis", ShaderType.Float3,
		[new HelperParam( "v", ShaderType.Float3 ), new HelperParam( "axis", ShaderType.Float3 ),
			new HelperParam( "angle", ShaderType.Float )] )
	{
		Hlsl = @"float3 Prism_RotateAboutAxis( float3 v, float3 axis, float angle )
{
	float s = sin( angle );
	float c = cos( angle );
	float t = 1.0 - c;

	float3 a = normalize( axis );

	float3x3 rot = float3x3(
		t * a.x * a.x + c,       t * a.x * a.y - a.z * s, t * a.z * a.x + a.y * s,
		t * a.x * a.y + a.z * s, t * a.y * a.y + c,       t * a.y * a.z - a.x * s,
		t * a.z * a.x - a.y * s, t * a.y * a.z + a.x * s, t * a.z * a.z + c );

	return mul( rot, v );
}"
	};

	/// <summary>
	/// Scale, then rotate by XYZ Euler angles in radians, then translate. Written out longhand rather
	/// than as three matrix products so both backends fold it to the same handful of instructions.
	/// </summary>
	public static readonly HelperFunction ApplyTrs = new( "Prism_ApplyTrs", ShaderType.Float3,
		[new HelperParam( "v", ShaderType.Float3 ), new HelperParam( "translation", ShaderType.Float3 ),
			new HelperParam( "rotation", ShaderType.Float3 ), new HelperParam( "scale", ShaderType.Float3 )] )
	{
		Hlsl = @"float3 Prism_ApplyTrs( float3 v, float3 translation, float3 rotation, float3 scale )
{
	float3 s = sin( rotation );
	float3 c = cos( rotation );

	float3 p = v * scale;

	float3 rx = float3( p.x, p.y * c.x - p.z * s.x, p.y * s.x + p.z * c.x );
	float3 ry = float3( rx.x * c.y + rx.z * s.y, rx.y, rx.z * c.y - rx.x * s.y );
	float3 rz = float3( ry.x * c.z - ry.y * s.z, ry.x * s.z + ry.y * c.z, ry.z );

	return rz + translation;
}"
	};

	/// <summary>The inverse of a 2x2 matrix, or a zero matrix when it is singular.</summary>
	public static readonly HelperFunction Inverse2x2 = new( "Prism_MatrixInverse2x2", ShaderType.Float2x2,
		[new HelperParam( "m", ShaderType.Float2x2 )] )
	{
		Hlsl = @"float2x2 Prism_MatrixInverse2x2( float2x2 m )
{
	float d = m[0][0] * m[1][1] - m[0][1] * m[1][0];
	float s = ( abs( d ) < 1e-8 ) ? 0.0 : 1.0 / d;

	float2x2 adjugate = float2x2(
		 m[1][1], -m[0][1],
		-m[1][0],  m[0][0] );

	return adjugate * s;
}"
	};

	/// <summary>The inverse of a 3x3 matrix, or a zero matrix when it is singular.</summary>
	public static readonly HelperFunction Inverse3x3 = new( "Prism_MatrixInverse3x3", ShaderType.Float3x3,
		[new HelperParam( "m", ShaderType.Float3x3 )] )
	{
		Hlsl = @"float3x3 Prism_MatrixInverse3x3( float3x3 m )
{
	float3 a = m[0];
	float3 b = m[1];
	float3 c = m[2];

	float3 r0 = cross( b, c );
	float3 r1 = cross( c, a );
	float3 r2 = cross( a, b );

	float d = dot( a, r0 );
	float s = ( abs( d ) < 1e-8 ) ? 0.0 : 1.0 / d;

	return transpose( float3x3( r0, r1, r2 ) ) * s;
}"
	};

	/// <summary>The inverse of a 4x4 matrix, or a zero matrix when it is singular.</summary>
	public static readonly HelperFunction Inverse4x4 = new( "Prism_MatrixInverse4x4", ShaderType.Float4x4,
		[new HelperParam( "m", ShaderType.Float4x4 )] )
	{
		Hlsl = @"float4x4 Prism_MatrixInverse4x4( float4x4 m )
{
	float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3];
	float a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3];
	float a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3];
	float a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3];

	float b00 = a00 * a11 - a01 * a10;
	float b01 = a00 * a12 - a02 * a10;
	float b02 = a00 * a13 - a03 * a10;
	float b03 = a01 * a12 - a02 * a11;
	float b04 = a01 * a13 - a03 * a11;
	float b05 = a02 * a13 - a03 * a12;
	float b06 = a20 * a31 - a21 * a30;
	float b07 = a20 * a32 - a22 * a30;
	float b08 = a20 * a33 - a23 * a30;
	float b09 = a21 * a32 - a22 * a31;
	float b10 = a21 * a33 - a23 * a31;
	float b11 = a22 * a33 - a23 * a32;

	float d = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
	float s = ( abs( d ) < 1e-12 ) ? 0.0 : 1.0 / d;

	return float4x4(
		( a11 * b11 - a12 * b10 + a13 * b09 ) * s,
		( a02 * b10 - a01 * b11 - a03 * b09 ) * s,
		( a31 * b05 - a32 * b04 + a33 * b03 ) * s,
		( a22 * b04 - a21 * b05 - a23 * b03 ) * s,

		( a12 * b08 - a10 * b11 - a13 * b07 ) * s,
		( a00 * b11 - a02 * b08 + a03 * b07 ) * s,
		( a32 * b02 - a30 * b05 - a33 * b01 ) * s,
		( a20 * b05 - a22 * b02 + a23 * b01 ) * s,

		( a10 * b10 - a11 * b08 + a13 * b06 ) * s,
		( a01 * b08 - a00 * b10 - a03 * b06 ) * s,
		( a30 * b04 - a31 * b02 + a33 * b00 ) * s,
		( a21 * b02 - a20 * b04 - a23 * b00 ) * s,

		( a11 * b07 - a10 * b09 - a12 * b06 ) * s,
		( a00 * b09 - a01 * b07 + a02 * b06 ) * s,
		( a31 * b01 - a30 * b03 - a32 * b00 ) * s,
		( a20 * b03 - a21 * b01 + a22 * b00 ) * s );
}"
	};

	/// <summary>The inverse helper for a given square size, or null when the size is not supported.</summary>
	public static HelperFunction InverseFor( int dimension ) => dimension switch
	{
		2 => Inverse2x2,
		3 => Inverse3x3,
		4 => Inverse4x4,
		_ => null
	};

	/// <summary>
	/// One row of a matrix. Uses a real index expression where the context exposes the IR builder, and
	/// falls back to <c>mul( unitRow, m )</c> — which selects exactly one row — where it does not.
	/// </summary>
	public static IrValue Row( EmitContext ctx, IrValue matrix, int index, int dimension )
	{
		if ( !matrix.IsValid ) return IrValue.Invalid;

		var rowType = ShaderType.Vec( matrix.Type.Scalar, dimension );

		if ( ctx is NodeEmitContext concrete && concrete.Builder is { } builder )
		{
			return builder.Index( rowType, matrix, builder.Const( ShaderType.Int, ConstValue.From( index ) ) );
		}

		var unit = new IrValue[dimension];

		for ( int i = 0; i < dimension; i++ ) unit[i] = ctx.Const( i == index ? 1f : 0f );

		return ctx.Call( Intrinsic.Mul, ctx.Construct( rowType, unit ), matrix );
	}
}

// ---- products and magnitudes ----------------------------------------------------------------------

/// <summary>The dot product of two vectors: how much they point the same way, scaled by their lengths.</summary>
[NodeInfo( Id = "prism.vector.dot", Title = "Dot Product", Category = "Vector", Icon = "join_inner",
	Keywords = ["dot", "product", "projection", "cosine"] )]
[NodeVersion( 1 )]
public sealed class DotProductNode : PrismNode
{
	/// <summary>First vector.</summary>
	[In( "T", Name = "A" )] public PortRef A { get; set; }

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

	/// <summary>The scalar product.</summary>
	[Out( "T.scalar", 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; }

	/// <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 ), ctx.Call( Intrinsic.Dot, a, b ) );
	}
}

/// <summary>The cross product of two three-component vectors: a vector perpendicular to both.</summary>
[NodeInfo( Id = "prism.vector.cross", Title = "Cross Product", Category = "Vector", Icon = "close",
	Keywords = ["cross", "perpendicular", "normal", "product"] )]
[NodeVersion( 1 )]
public sealed class CrossProductNode : PrismNode
{
	/// <summary>First vector.</summary>
	[In( "float3", Name = "A" )] public PortRef A { get; set; }

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

	/// <summary>A vector perpendicular to both inputs.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

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

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

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

		var a = ctx.InAs( nameof( A ), ShaderType.Float3 );
		var b = ctx.InAs( nameof( B ), ShaderType.Float3 );

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

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

/// <summary>The length of a vector.</summary>
[NodeInfo( Id = "prism.vector.length", Title = "Length", Category = "Vector", Icon = "straighten",
	Keywords = ["length", "magnitude", "norm", "size"] )]
[NodeVersion( 1 )]
public sealed class LengthNode : PrismNode
{
	/// <summary>The vector to measure.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>Its length.</summary>
	[Out( "T.scalar", 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; }

	/// <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 ), ctx.Call( Intrinsic.Length, value ) );
	}
}

/// <summary>How far apart two points are.</summary>
[NodeInfo( Id = "prism.vector.distance", Title = "Distance", Category = "Vector", Icon = "straighten",
	Keywords = ["distance", "length", "apart", "metric"] )]
[NodeVersion( 1 )]
public sealed class DistanceNode : PrismNode
{
	/// <summary>First point.</summary>
	[In( "T", Name = "A" )] public PortRef A { get; set; }

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

	/// <summary>The distance between them.</summary>
	[Out( "T.scalar", 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; }

	/// <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 ), ctx.Call( Intrinsic.Distance, a, b ) );
	}
}

/// <summary>Scale a vector to unit length.</summary>
[NodeInfo( Id = "prism.vector.normalize", Title = "Normalize", Category = "Vector", Icon = "arrow_forward",
	Keywords = ["normalize", "unit", "direction", "length one"] )]
[NodeVersion( 1 )]
public sealed class NormalizeVectorNode : PrismNode
{
	/// <summary>The vector to scale.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The same direction, with length 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; } = 1f;

	/// <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 ), ctx.Call( Intrinsic.Normalize, value ) );
	}
}

/// <summary>Bounce a direction off a surface.</summary>
[NodeInfo( Id = "prism.vector.reflect", Title = "Reflect", Category = "Vector", Icon = "network_ping",
	Keywords = ["reflect", "bounce", "mirror", "specular"] )]
[NodeVersion( 1 )]
public sealed class ReflectNode : PrismNode
{
	/// <summary>The incoming direction.</summary>
	[In( "T", Name = "In" )] public PortRef A { get; set; }

	/// <summary>The surface normal. Should be unit length.</summary>
	[In( "T", Name = "Normal" )] public PortRef B { get; set; }

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

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

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

	/// <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 ), ctx.Call( Intrinsic.Reflect, a, b ) );
	}
}

/// <summary>Bend a direction as it crosses between two media.</summary>
[NodeInfo( Id = "prism.vector.refract", Title = "Refract", Category = "Vector", Icon = "water_drop",
	Keywords = ["refract", "snell", "ior", "bend", "glass"] )]
[NodeVersion( 1 )]
public sealed class RefractNode : PrismNode
{
	/// <summary>The incoming direction. Should be unit length.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The surface normal. Should be unit length.</summary>
	[In( "float3", Name = "Normal" )] public PortRef Normal { get; set; }

	/// <summary>The ratio of the two indices of refraction.</summary>
	[In( "float", Name = "IOR" )] public PortRef Ior { get; set; }

	/// <summary>The refracted direction, or zero under total internal reflection.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

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

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

	/// <summary>The literal used when <c>IOR</c> is unconnected. 1.0 / 1.5 is air into glass.</summary>
	[InlineValue( nameof( Ior ) ), Title( "IOR" ), Range( 0f, 3f )] public float DefaultIor { get; set; } = 0.6666667f;

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

		var value = ctx.InAs( nameof( In ), ShaderType.Float3 );
		var normal = ctx.InAs( nameof( Normal ), ShaderType.Float3 );
		var ior = ctx.InAs( nameof( Ior ), ShaderType.Float );

		if ( !value.IsValid || !normal.IsValid || !ior.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Refract, value, normal, ior ) );
	}
}

/// <summary>Flip a normal so it faces the same way as a reference direction.</summary>
[NodeInfo( Id = "prism.vector.faceForward", Title = "Face Forward", Category = "Vector",
	Icon = "flip_camera_android", Tier = NodeTier.Advanced,
	Keywords = ["faceforward", "two sided", "backface", "flip"] )]
[NodeVersion( 1 )]
public sealed class FaceForwardNode : PrismNode
{
	/// <summary>The vector to orient.</summary>
	[In( "float3", Name = "N" )] public PortRef N { get; set; }

	/// <summary>The incident direction.</summary>
	[In( "float3", Name = "I" )] public PortRef I { get; set; }

	/// <summary>The geometric normal the sign is taken from.</summary>
	[In( "float3", Name = "Ng" )] public PortRef Ng { get; set; }

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

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

	/// <summary>The literal used when <c>I</c> is unconnected.</summary>
	[InlineValue( nameof( I ) ), Title( "I" )] public Vector3 DefaultI { get; set; } = new( 0f, 0f, -1f );

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

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

		var n = ctx.InAs( nameof( N ), ShaderType.Float3 );
		var i = ctx.InAs( nameof( I ), ShaderType.Float3 );
		var ng = ctx.InAs( nameof( Ng ), ShaderType.Float3 );

		if ( !n.IsValid || !i.IsValid || !ng.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.FaceForward, n, i, ng ) );
	}
}

/// <summary>The part of one vector that lies along another.</summary>
[NodeInfo( Id = "prism.vector.project", Title = "Project", Category = "Vector", Icon = "vertical_align_bottom",
	Keywords = ["project", "projection", "component", "along"] )]
[NodeVersion( 1 )]
public sealed class ProjectNode : PrismNode
{
	/// <summary>The vector being projected.</summary>
	[In( "T", Name = "A" )] public PortRef A { get; set; }

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

	/// <summary>The part of <c>A</c> parallel to <c>B</c>.</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 projection of <paramref name="a"/> onto <paramref name="b"/>, guarded against a zero-length <c>B</c>.</summary>
	internal static IrValue Projection( EmitContext ctx, IrValue a, IrValue b )
	{
		var numerator = ctx.Call( Intrinsic.Dot, a, b );
		var denominator = ctx.Call( Intrinsic.Dot, b, b );
		var scale = PrismMathHelpers.SafeDivide( ctx, numerator, denominator, ctx.Const( 0f ) );

		return ctx.Bin( BinaryOp.Mul, b, scale );
	}

	/// <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 ), Projection( ctx, a, b ) );
	}
}

/// <summary>The part of one vector that is perpendicular to another.</summary>
[NodeInfo( Id = "prism.vector.reject", Title = "Reject", Category = "Vector", Icon = "vertical_align_top",
	Keywords = ["reject", "rejection", "perpendicular", "orthogonal"] )]
[NodeVersion( 1 )]
public sealed class RejectNode : PrismNode
{
	/// <summary>The vector being split.</summary>
	[In( "T", Name = "A" )] public PortRef A { get; set; }

	/// <summary>The vector the parallel part is removed along.</summary>
	[In( "T", Name = "B" )] public PortRef B { get; set; }

	/// <summary>The part of <c>A</c> perpendicular to <c>B</c>.</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;

	/// <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 ), ctx.Bin( BinaryOp.Sub, a, ProjectNode.Projection( ctx, a, b ) ) );
	}
}

/// <summary>The angle between two directions.</summary>
[NodeInfo( Id = "prism.vector.angleBetween", Title = "Angle Between", Category = "Vector",
	Icon = "architecture", Keywords = ["angle", "between", "acos", "dot", "arc"] )]
[NodeVersion( 1 )]
public sealed class AngleBetweenNode : PrismNode
{
	/// <summary>First direction.</summary>
	[In( "T", Name = "A" )] public PortRef A { get; set; }

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

	/// <summary>The angle between them, always positive.</summary>
	[Out( "T.scalar", 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>Report the angle in degrees rather than radians.</summary>
	public bool Degrees { get; set; }

	/// <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;

		var cosine = ctx.Call( Intrinsic.Dot,
			ctx.Call( Intrinsic.Normalize, a ),
			ctx.Call( Intrinsic.Normalize, b ) );

		// Rounding can push a normalised dot product a hair outside -1..1, and acos of that is NaN.
		var clamped = ctx.Call( Intrinsic.Clamp, cosine, ctx.Const( -1f ), ctx.Const( 1f ) );
		var angle = ctx.Call( Intrinsic.Acos, clamped );

		if ( Degrees ) angle = ctx.Call( Intrinsic.Degrees, angle );

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

/// <summary>A soft spherical falloff around a point.</summary>
[NodeInfo( Id = "prism.vector.sphereMask", Title = "Sphere Mask", Category = "Vector",
	Icon = "radio_button_checked", Keywords = ["sphere", "mask", "falloff", "radius", "distance"] )]
[NodeVersion( 1 )]
public sealed class SphereMaskNode : PrismNode
{
	/// <summary>The position being tested.</summary>
	[In( "T", Name = "Coords" )] public PortRef Coords { get; set; }

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

	/// <summary>The radius inside which the mask is fully on.</summary>
	[In( "T.scalar", Name = "Radius" )] public PortRef Radius { get; set; }

	/// <summary>How sharply the mask falls off. One is a hard edge.</summary>
	[In( "T.scalar", Name = "Hardness" )] public PortRef Hardness { get; set; }

	/// <summary>One inside the sphere, zero outside, with a soft edge between.</summary>
	[Out( "T.scalar", Name = "Out" )] public PortRef Out { get; set; }

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

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

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

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

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

		var (coords, center) = ctx.InPair( nameof( Coords ), nameof( Center ) );
		var radius = ctx.In( nameof( Radius ) );
		var hardness = ctx.In( nameof( Hardness ) );

		if ( !coords.IsValid || !center.IsValid || !radius.IsValid || !hardness.IsValid ) return;

		// 1 - saturate( ( distance( coords, center ) - radius ) / max( 1 - hardness, 1e-5 ) )
		var distance = ctx.Call( Intrinsic.Distance, coords, center );
		var beyond = ctx.Bin( BinaryOp.Sub, distance, radius );
		var falloff = ctx.Call( Intrinsic.Max,
			ctx.Bin( BinaryOp.Sub, ctx.Const( 1f ), hardness ), ctx.Const( 1e-5f ) );

		var ramp = PrismMathHelpers.Saturate( ctx, ctx.Bin( BinaryOp.Div, beyond, falloff ) );

		ctx.Out( nameof( Out ), ctx.Bin( BinaryOp.Sub, ctx.Const( 1f ), ramp ) );
	}
}

/// <summary>Spin a vector around an arbitrary axis.</summary>
[NodeInfo( Id = "prism.vector.rotateAboutAxis", Title = "Rotate About Axis", Category = "Vector",
	Icon = "rotate_right", Keywords = ["rotate", "axis", "rodrigues", "spin", "turn"] )]
[NodeVersion( 1 )]
public sealed class RotateAboutAxisNode : PrismNode
{
	/// <summary>The vector to spin.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The axis to spin around. Normalised internally.</summary>
	[In( "float3", Name = "Axis" )] public PortRef Axis { get; set; }

	/// <summary>How far to spin.</summary>
	[In( "float", Name = "Rotation" )] public PortRef Rotation { get; set; }

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

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

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

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

	/// <summary>Read the rotation in degrees rather than radians.</summary>
	public bool Degrees { get; set; } = true;

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

		var value = ctx.InAs( nameof( In ), ShaderType.Float3 );
		var axis = ctx.InAs( nameof( Axis ), ShaderType.Float3 );
		var angle = ctx.InAs( nameof( Rotation ), ShaderType.Float );

		if ( !value.IsValid || !axis.IsValid || !angle.IsValid ) return;

		if ( Degrees ) angle = ctx.Call( Intrinsic.Radians, angle );

		ctx.Out( nameof( Out ), ctx.Helper( PrismMatrixHelpers.RotateAboutAxis, value, axis, angle ) );
	}
}

// ---- coordinate systems ---------------------------------------------------------------------------

/// <summary>Move a position, direction or normal from one coordinate space into another.</summary>
[NodeInfo( Id = "prism.vector.transform", Title = "Transform", Category = "Vector", Icon = "transform",
	Keywords = ["transform", "space", "object", "world", "view", "tangent", "convert"] )]
[NodeVersion( 1 )]
public sealed class SpaceTransformNode : PrismNode
{
	/// <summary>The value to convert.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

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

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

	/// <summary>The space the input is already in.</summary>
	public PrismCoordinateSpace From { get; set; } = PrismCoordinateSpace.World;

	/// <summary>The space to convert into.</summary>
	public PrismCoordinateSpace To { get; set; } = PrismCoordinateSpace.Tangent;

	/// <summary>Whether the value is a point, a direction, or a direction that should stay unit length.</summary>
	public PrismTransformKind Kind { get; set; } = PrismTransformKind.Direction;

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

		if ( From == To )
		{
			ctx.Info( "The source and destination spaces are the same, so this node does nothing" );
			return;
		}

		if ( From == PrismCoordinateSpace.Object || To == PrismCoordinateSpace.Object )
		{
			// The engine exposes no object matrix global, so the backend generates one per stage. In the
			// vertex program it comes straight from the instancing table and is exact; in the pixel
			// program it is rebuilt from the two tangent frames the interpolators already carry, which
			// recovers rotation and translation exactly but cannot recover scale, because the vertex
			// program normalised the world-space frame on the way through.
			ctx.Info( "In the pixel program the object transform is reconstructed from the interpolated " +
				"tangent frame: rotation and translation are exact, scale is not. Convert in the vertex " +
				"program if the object is non-uniformly scaled.", null, DiagnosticCode.SampleLowered );
		}

		if ( Kind != PrismTransformKind.Position ) return;
		if ( From != PrismCoordinateSpace.Tangent && To != PrismCoordinateSpace.Tangent ) return;

		ctx.Warn( "Tangent space has no origin, so a position converted through it is treated as a direction" );
	}

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

		var value = ctx.InAs( nameof( In ), ShaderType.Float3 );

		if ( !value.IsValid ) return;

		if ( From == To )
		{
			ctx.Out( nameof( Out ), Finish( ctx, value ) );
			return;
		}

		var world = ToWorld( ctx, value );

		if ( !world.IsValid ) return;

		ctx.Out( nameof( Out ), Finish( ctx, FromWorld( ctx, world ) ) );
	}

	IrValue Finish( EmitContext ctx, IrValue value ) =>
		Kind == PrismTransformKind.Normal && value.IsValid ? ctx.Call( Intrinsic.Normalize, value ) : value;

	/// <summary>True when translation participates in the conversion.</summary>
	bool IsPoint => Kind == PrismTransformKind.Position;

	IrValue ToWorld( EmitContext ctx, IrValue value )
	{
		switch ( From )
		{
			case PrismCoordinateSpace.World:
				return value;

			case PrismCoordinateSpace.Object:
				return ApplyMatrix( ctx, ctx.Builtin( Builtin.ObjectToWorld ), value, IsPoint );

			case PrismCoordinateSpace.View:
			{
				// Only the forward view matrix is provided, so the reverse trip inverts it. The optimiser
				// hoists the call, and a graph that only ever goes world-to-view never pays for it.
				var inverse = ctx.Helper( PrismMatrixHelpers.Inverse4x4, ctx.Builtin( Builtin.ViewMatrix ) );

				return ApplyMatrix( ctx, inverse, value, IsPoint );
			}

			default:
			{
				// Tangent to world: rebuild the vector from the surface frame.
				var tangent = ctx.Builtin( Builtin.WorldTangentU );
				var bitangent = ctx.Builtin( Builtin.WorldTangentV );
				var normal = ctx.Builtin( Builtin.WorldNormal );

				var x = ctx.Bin( BinaryOp.Mul, tangent, ctx.Swizzle( value, "x" ) );
				var y = ctx.Bin( BinaryOp.Mul, bitangent, ctx.Swizzle( value, "y" ) );
				var z = ctx.Bin( BinaryOp.Mul, normal, ctx.Swizzle( value, "z" ) );

				return ctx.Bin( BinaryOp.Add, ctx.Bin( BinaryOp.Add, x, y ), z );
			}
		}
	}

	IrValue FromWorld( EmitContext ctx, IrValue value )
	{
		switch ( To )
		{
			case PrismCoordinateSpace.World:
				return value;

			case PrismCoordinateSpace.Object:
				return ApplyMatrix( ctx, ctx.Builtin( Builtin.WorldToObject ), value, IsPoint );

			case PrismCoordinateSpace.View:
				return ApplyMatrix( ctx, ctx.Builtin( Builtin.ViewMatrix ), value, IsPoint );

			default:
			{
				// World to tangent: project onto the surface frame.
				var tangent = ctx.Builtin( Builtin.WorldTangentU );
				var bitangent = ctx.Builtin( Builtin.WorldTangentV );
				var normal = ctx.Builtin( Builtin.WorldNormal );

				return ctx.Construct( ShaderType.Float3,
					ctx.Call( Intrinsic.Dot, value, tangent ),
					ctx.Call( Intrinsic.Dot, value, bitangent ),
					ctx.Call( Intrinsic.Dot, value, normal ) );
			}
		}
	}

	/// <summary>Multiply a float3 through a 4x4 matrix, with or without its translation column.</summary>
	static IrValue ApplyMatrix( EmitContext ctx, IrValue matrix, IrValue value, bool isPoint )
	{
		if ( !matrix.IsValid || !value.IsValid ) return IrValue.Invalid;

		var homogeneous = ctx.Construct( ShaderType.Float4, value, ctx.Const( isPoint ? 1f : 0f ) );
		var product = ctx.Call( Intrinsic.Mul, matrix, homogeneous );

		return ctx.Swizzle( product, "xyz" );
	}
}

/// <summary>Build a direction from a radius and two angles.</summary>
[NodeInfo( Id = "prism.vector.sphericalToCartesian", Title = "Spherical To Cartesian", Category = "Vector",
	Icon = "public", Tier = NodeTier.Advanced,
	Keywords = ["spherical", "polar", "cartesian", "sphere", "latitude", "longitude"] )]
[NodeVersion( 1 )]
public sealed class SphericalToCartesianNode : PrismNode
{
	/// <summary>Distance from the origin.</summary>
	[In( "float", Name = "Radius" )] public PortRef Radius { get; set; }

	/// <summary>Angle away from the +Z pole.</summary>
	[In( "float", Name = "Inclination" )] public PortRef Inclination { get; set; }

	/// <summary>Angle around the Z axis, measured from +X.</summary>
	[In( "float", Name = "Azimuth" )] public PortRef Azimuth { get; set; }

	/// <summary>The position those coordinates describe.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

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

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

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

	/// <summary>Read the angles in degrees rather than radians.</summary>
	public bool Degrees { get; set; }

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

		var radius = ctx.InAs( nameof( Radius ), ShaderType.Float );
		var inclination = ctx.InAs( nameof( Inclination ), ShaderType.Float );
		var azimuth = ctx.InAs( nameof( Azimuth ), ShaderType.Float );

		if ( !radius.IsValid || !inclination.IsValid || !azimuth.IsValid ) return;

		if ( Degrees )
		{
			inclination = ctx.Call( Intrinsic.Radians, inclination );
			azimuth = ctx.Call( Intrinsic.Radians, azimuth );
		}

		var sinInclination = ctx.Let( "sinTheta", ctx.Call( Intrinsic.Sin, inclination ) );
		var planar = ctx.Bin( BinaryOp.Mul, radius, sinInclination );

		var x = ctx.Bin( BinaryOp.Mul, planar, ctx.Call( Intrinsic.Cos, azimuth ) );
		var y = ctx.Bin( BinaryOp.Mul, planar, ctx.Call( Intrinsic.Sin, azimuth ) );
		var z = ctx.Bin( BinaryOp.Mul, radius, ctx.Call( Intrinsic.Cos, inclination ) );

		ctx.Out( nameof( Out ), ctx.Construct( ShaderType.Float3, x, y, z ) );
	}
}

/// <summary>Take a direction apart into a radius and two angles.</summary>
[NodeInfo( Id = "prism.vector.cartesianToSpherical", Title = "Cartesian To Spherical", Category = "Vector",
	Icon = "public", Tier = NodeTier.Advanced,
	Keywords = ["spherical", "polar", "cartesian", "sphere", "latitude", "longitude"] )]
[NodeVersion( 1 )]
public sealed class CartesianToSphericalNode : PrismNode
{
	/// <summary>The position to describe.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>Distance from the origin.</summary>
	[Out( "float", Name = "Radius" )] public PortRef Radius { get; set; }

	/// <summary>Angle away from the +Z pole.</summary>
	[Out( "float", Name = "Inclination" )] public PortRef Inclination { get; set; }

	/// <summary>Angle around the Z axis, measured from +X.</summary>
	[Out( "float", Name = "Azimuth" )] public PortRef Azimuth { get; set; }

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

	/// <summary>Report the angles in degrees rather than radians.</summary>
	public bool Degrees { get; set; }

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

		var value = ctx.InAs( nameof( In ), ShaderType.Float3 );

		if ( !value.IsValid ) return;

		var radius = ctx.Let( "radius", ctx.Call( Intrinsic.Length, value ) );
		var safeRadius = ctx.Call( Intrinsic.Max, radius, ctx.Const( PrismMathHelpers.Epsilon ) );

		var inclination = ctx.Call( Intrinsic.Acos, ctx.Call( Intrinsic.Clamp,
			PrismMathHelpers.SafeDivide( ctx, ctx.Swizzle( value, "z" ), safeRadius, ctx.Const( 0f ) ),
			ctx.Const( -1f ), ctx.Const( 1f ) ) );

		var azimuth = ctx.Call( Intrinsic.Atan2, ctx.Swizzle( value, "y" ), ctx.Swizzle( value, "x" ) );

		if ( Degrees )
		{
			inclination = ctx.Call( Intrinsic.Degrees, inclination );
			azimuth = ctx.Call( Intrinsic.Degrees, azimuth );
		}

		ctx.Out( nameof( Radius ), radius );
		ctx.Out( nameof( Inclination ), inclination );
		ctx.Out( nameof( Azimuth ), azimuth );
	}
}

/// <summary>Build a two-dimensional offset from a radius and an angle.</summary>
[NodeInfo( Id = "prism.vector.polarToCartesian", Title = "Polar To Cartesian", Category = "Vector",
	Icon = "explore", Keywords = ["polar", "cartesian", "radius", "angle", "circle"] )]
[NodeVersion( 1 )]
public sealed class PolarToCartesianNode : PrismNode
{
	/// <summary>Distance from the origin.</summary>
	[In( "float", Name = "Radius" )] public PortRef Radius { get; set; }

	/// <summary>Angle measured from +X.</summary>
	[In( "float", Name = "Angle" )] public PortRef Angle { get; set; }

	/// <summary>The offset those coordinates describe.</summary>
	[Out( "float2", Name = "Out" )] public PortRef Out { get; set; }

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

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

	/// <summary>Read the angle in degrees rather than radians.</summary>
	public bool Degrees { get; set; }

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

		var radius = ctx.InAs( nameof( Radius ), ShaderType.Float );
		var angle = ctx.InAs( nameof( Angle ), ShaderType.Float );

		if ( !radius.IsValid || !angle.IsValid ) return;

		if ( Degrees ) angle = ctx.Call( Intrinsic.Radians, angle );

		var x = ctx.Bin( BinaryOp.Mul, radius, ctx.Call( Intrinsic.Cos, angle ) );
		var y = ctx.Bin( BinaryOp.Mul, radius, ctx.Call( Intrinsic.Sin, angle ) );

		ctx.Out( nameof( Out ), ctx.Construct( ShaderType.Float2, x, y ) );
	}
}

/// <summary>Take a two-dimensional offset apart into a radius and an angle.</summary>
[NodeInfo( Id = "prism.vector.cartesianToPolar", Title = "Cartesian To Polar", Category = "Vector",
	Icon = "explore", Keywords = ["polar", "cartesian", "radius", "angle", "atan2"] )]
[NodeVersion( 1 )]
public sealed class CartesianToPolarNode : PrismNode
{
	/// <summary>The offset to describe.</summary>
	[In( "float2", Name = "In" )] public PortRef In { get; set; }

	/// <summary>Distance from the origin.</summary>
	[Out( "float", Name = "Radius" )] public PortRef Radius { get; set; }

	/// <summary>Angle measured from +X.</summary>
	[Out( "float", Name = "Angle" )] public PortRef Angle { get; set; }

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

	/// <summary>Report the angle in degrees rather than radians.</summary>
	public bool Degrees { get; set; }

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

		var value = ctx.InAs( nameof( In ), ShaderType.Float2 );

		if ( !value.IsValid ) return;

		var angle = ctx.Call( Intrinsic.Atan2, ctx.Swizzle( value, "y" ), ctx.Swizzle( value, "x" ) );

		if ( Degrees ) angle = ctx.Call( Intrinsic.Degrees, angle );

		ctx.Out( nameof( Radius ), ctx.Call( Intrinsic.Length, value ) );
		ctx.Out( nameof( Angle ), angle );
	}
}

// ---- matrices -------------------------------------------------------------------------------------

/// <summary>
/// The shared shape of a matrix node: a square size that reshapes the port set, and the arithmetic to
/// keep the ports in step with it whether the size is changed by the user or restored by the loader.
/// </summary>
public abstract class MatrixNodeBase : PrismNode
{
	PrismMatrixSize _size = PrismMatrixSize.Matrix4x4;

	/// <summary>How big the matrix is. Changing this reshapes the node's ports.</summary>
	public PrismMatrixSize Size
	{
		get => _size;
		set
		{
			if ( _size == value ) return;

			_size = value;
			RebuildPorts();
		}
	}

	/// <summary>
	/// The matrix's side length, always 2, 3 or 4. Reads through a clamp because
	/// <see cref="PrismNode.OnDefinePorts"/> runs once from the constructor, before
	/// <see cref="_size"/>'s initialiser has had a chance to run.
	/// </summary>
	protected int Dimension => (int)_size is >= 2 and <= 4 ? (int)_size : 4;

	/// <summary>The declared type of this node's matrix ports, e.g. <c>float4x4</c>.</summary>
	protected string MatrixType => $"float{Dimension}x{Dimension}";

	/// <summary>The declared type of this node's vector ports, e.g. <c>float4</c>.</summary>
	protected string VectorType => $"float{Dimension}";

	/// <summary>The resolved matrix type.</summary>
	protected ShaderType MatrixShaderType => ShaderType.Mat( ScalarKind.Float, Dimension, Dimension );

	/// <summary>The resolved row/column type.</summary>
	protected ShaderType VectorShaderType => ShaderType.Vec( ScalarKind.Float, Dimension );
}

/// <summary>Assemble a square matrix from its rows or its columns.</summary>
[NodeInfo( Id = "prism.matrix.construct", Title = "Matrix Construct", Category = "Matrix",
	Icon = "grid_on", Tier = NodeTier.Advanced,
	Keywords = ["matrix", "construct", "build", "rows", "columns"] )]
[NodeVersion( 1 )]
public sealed class MatrixConstructNode : MatrixNodeBase
{
	PrismMatrixOrder _order = PrismMatrixOrder.Rows;

	/// <summary>Whether the inputs are the matrix's rows or its columns.</summary>
	public PrismMatrixOrder Order
	{
		get => _order;
		set
		{
			if ( _order == value ) return;

			_order = value;
			RebuildPorts();
		}
	}

	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		if ( b is null ) return;

		var label = _order == PrismMatrixOrder.Columns ? "Column" : "Row";

		for ( int i = 0; i < Dimension; i++ )
		{
			b.Input( $"M{i}", VectorType, $"{label} {i}", order: i );
		}

		b.Output( "Out", MatrixType, "Out" );
	}

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

		var dimension = Dimension;
		var parts = new IrValue[dimension];

		for ( int i = 0; i < dimension; i++ )
		{
			parts[i] = ctx.InAs( $"M{i}", VectorShaderType );

			if ( !parts[i].IsValid ) return;
		}

		var matrix = ctx.Construct( MatrixShaderType, parts );

		// HLSL's matrix constructor takes rows, so a column-major authoring mode is one transpose away.
		if ( _order == PrismMatrixOrder.Columns ) matrix = ctx.Call( Intrinsic.Transpose, matrix );

		ctx.Out( "Out", matrix );
	}
}

/// <summary>Take a square matrix apart into its rows or its columns.</summary>
[NodeInfo( Id = "prism.matrix.split", Title = "Matrix Split", Category = "Matrix", Icon = "grid_view",
	Tier = NodeTier.Advanced, Keywords = ["matrix", "split", "rows", "columns", "decompose"] )]
[NodeVersion( 1 )]
public sealed class MatrixSplitNode : MatrixNodeBase
{
	PrismMatrixOrder _order = PrismMatrixOrder.Rows;

	/// <summary>Whether the outputs are the matrix's rows or its columns.</summary>
	public PrismMatrixOrder Order
	{
		get => _order;
		set
		{
			if ( _order == value ) return;

			_order = value;
			RebuildPorts();
		}
	}

	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		if ( b is null ) return;

		var label = _order == PrismMatrixOrder.Columns ? "Column" : "Row";

		b.Input( "In", MatrixType, "In" );

		for ( int i = 0; i < Dimension; i++ )
		{
			b.Output( $"M{i}", VectorType, $"{label} {i}", order: i );
		}
	}

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

		var matrix = ctx.In( "In" );

		if ( !matrix.IsValid ) return;

		// Indexing a matrix yields a row, so pulling columns out is one transpose away.
		if ( _order == PrismMatrixOrder.Columns ) matrix = ctx.Call( Intrinsic.Transpose, matrix );

		matrix = ctx.Let( "m", matrix );

		if ( !matrix.IsValid ) return;

		for ( int i = 0; i < Dimension; i++ )
		{
			ctx.Out( $"M{i}", PrismMatrixHelpers.Row( ctx, matrix, i, Dimension ) );
		}
	}
}

/// <summary>Mirror a matrix across its diagonal.</summary>
[NodeInfo( Id = "prism.matrix.transpose", Title = "Matrix Transpose", Category = "Matrix",
	Icon = "flip", Tier = NodeTier.Advanced, Keywords = ["matrix", "transpose", "flip", "mirror"] )]
[NodeVersion( 1 )]
public sealed class MatrixTransposeNode : MatrixNodeBase
{
	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		if ( b is null ) return;

		b.Input( "In", MatrixType, "In" );
		b.Output( "Out", MatrixType, "Out" );
	}

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

		var matrix = ctx.In( "In" );

		if ( !matrix.IsValid ) return;

		ctx.Out( "Out", ctx.Call( Intrinsic.Transpose, matrix ) );
	}
}

/// <summary>The determinant of a matrix — how much it scales volume, and zero when it is singular.</summary>
[NodeInfo( Id = "prism.matrix.determinant", Title = "Matrix Determinant", Category = "Matrix",
	Icon = "calculate", Tier = NodeTier.Advanced,
	Keywords = ["matrix", "determinant", "det", "volume", "singular"] )]
[NodeVersion( 1 )]
public sealed class MatrixDeterminantNode : MatrixNodeBase
{
	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		if ( b is null ) return;

		b.Input( "In", MatrixType, "In" );
		b.Output( "Out", "float", "Out" );
	}

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

		var matrix = ctx.In( "In" );

		if ( !matrix.IsValid ) return;

		ctx.Out( "Out", ctx.Call( Intrinsic.Determinant, matrix ) );
	}
}

/// <summary>The inverse of a matrix, or an all-zero matrix when it has none.</summary>
[NodeInfo( Id = "prism.matrix.inverse", Title = "Matrix Inverse", Category = "Matrix", Icon = "undo",
	Tier = NodeTier.Advanced, Keywords = ["matrix", "inverse", "invert", "undo transform"] )]
[NodeVersion( 1 )]
public sealed class MatrixInverseNode : MatrixNodeBase
{
	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		if ( b is null ) return;

		b.Input( "In", MatrixType, "In" );
		b.Output( "Out", MatrixType, "Out" );
	}

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx ) =>
		ctx?.Info( "Inverting a matrix per pixel is expensive; prefer an inverse supplied as a parameter where you can" );

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

		var matrix = ctx.In( "In" );

		if ( !matrix.IsValid ) return;

		var helper = PrismMatrixHelpers.InverseFor( Dimension );

		if ( helper is null )
		{
			ctx.Error( $"There is no inverse for a {MatrixType}" );
			return;
		}

		ctx.Out( "Out", ctx.Helper( helper, matrix ) );
	}
}

/// <summary>Multiply a matrix by a vector or by another matrix.</summary>
[NodeInfo( Id = "prism.matrix.multiply", Title = "Matrix Multiply", Category = "Matrix", Icon = "close",
	Tier = NodeTier.Advanced, Keywords = ["matrix", "multiply", "mul", "transform", "apply"] )]
[NodeVersion( 1 )]
public sealed class MatrixMultiplyNode : MatrixNodeBase
{
	PrismMatrixOperand _operand = PrismMatrixOperand.Vector;

	/// <summary>Whether the right-hand operand is a vector or another matrix.</summary>
	public PrismMatrixOperand Operand
	{
		get => _operand;
		set
		{
			if ( _operand == value ) return;

			_operand = value;
			RebuildPorts();
		}
	}

	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		if ( b is null ) return;

		var right = _operand == PrismMatrixOperand.Matrix ? MatrixType : VectorType;

		b.Input( "A", MatrixType, "A" );
		b.Input( "B", right, "B" );
		b.Output( "Out", right, "Out" );
	}

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

		var a = ctx.InAs( "A", MatrixShaderType );
		var b = ctx.InAs( "B", _operand == PrismMatrixOperand.Matrix ? MatrixShaderType : VectorShaderType );

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

		ctx.Out( "Out", ctx.Call( Intrinsic.Mul, a, b ) );
	}
}

/// <summary>The outer product of two vectors: the matrix whose entry <c>(i, j)</c> is <c>A[i] * B[j]</c>.</summary>
[NodeInfo( Id = "prism.matrix.outerProduct", Title = "Outer Product", Category = "Matrix",
	Icon = "grid_on", Tier = NodeTier.Advanced,
	Keywords = ["outer", "product", "tensor", "matrix", "dyadic"] )]
[NodeVersion( 1 )]
public sealed class OuterProductNode : MatrixNodeBase
{
	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		if ( b is null ) return;

		b.Input( "A", VectorType, "A" );
		b.Input( "B", VectorType, "B" );
		b.Output( "Out", MatrixType, "Out" );
	}

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

		var a = ctx.InAs( "A", VectorShaderType );
		var b = ctx.InAs( "B", VectorShaderType );

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

		var dimension = Dimension;
		var rows = new IrValue[dimension];

		a = ctx.Let( "a", a );
		b = ctx.Let( "b", b );

		for ( int i = 0; i < dimension; i++ )
		{
			rows[i] = ctx.Bin( BinaryOp.Mul, ctx.Swizzle( a, "xyzw"[i].ToString() ), b );

			if ( !rows[i].IsValid ) return;
		}

		ctx.Out( "Out", ctx.Construct( MatrixShaderType, rows ) );
	}
}

// ---- legacy-compatible transforms -----------------------------------------------------------------
//
// These two ids are named verbatim by LegacyShaderGraphImporter (ApplyTrs -> prism.transform.trs,
// TransformNormal -> prism.transform.normal), so they are frozen: an imported .shdrgrph resolves
// against them or loses the node.

/// <summary>
/// Apply a translation, an Euler rotation and a scale to a vector, in the order scale, rotate, translate.
/// </summary>
[NodeInfo( Id = "prism.transform.trs", Title = "Apply TRS", Category = "Vector", Icon = "open_with",
	Keywords = ["trs", "transform", "translate", "rotate", "scale", "matrix", "apply"],
	Description = "Scales, rotates and translates a vector. The rotation is XYZ Euler angles in degrees, " +
		"applied X then Y then Z, which is the order the legacy Apply TRS node used." )]
[NodeVersion( 1 )]
public sealed class ApplyTrsNode : PrismNode
{
	/// <summary>The vector to transform.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>Added after the rotation.</summary>
	[In( "float3", Name = "Translation" )] public PortRef Translation { get; set; }

	/// <summary>Euler angles, in degrees unless <see cref="Degrees"/> is off.</summary>
	[In( "float3", Name = "Rotation" )] public PortRef Rotation { get; set; }

	/// <summary>Applied before the rotation.</summary>
	[In( "float3", Name = "Scale" )] public PortRef Scale { get; set; }

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

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

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

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

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

	/// <summary>True when the rotation is authored in degrees rather than radians.</summary>
	public bool Degrees { get; set; } = true;

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

		var value = ctx.InAs( nameof( In ), ShaderType.Float3 );
		var translation = ctx.InAs( nameof( Translation ), ShaderType.Float3 );
		var rotation = ctx.InAs( nameof( Rotation ), ShaderType.Float3 );
		var scale = ctx.InAs( nameof( Scale ), ShaderType.Float3 );

		if ( !value.IsValid || !translation.IsValid || !rotation.IsValid || !scale.IsValid ) return;

		if ( Degrees ) rotation = ctx.Call( Intrinsic.Radians, rotation );

		ctx.Out( nameof( Out ),
			ctx.Helper( PrismMatrixHelpers.ApplyTrs, value, translation, rotation, scale ) );
	}
}

/// <summary>
/// Rotate a tangent-space normal into world space using the surface's own tangent frame — the
/// conversion the engine's <c>TransformNormal</c> performs before shading.
/// </summary>
[NodeInfo( Id = "prism.transform.normal", Title = "Transform Normal", Category = "Vector",
	Icon = "north_east", Keywords = ["normal", "transform", "tangent", "world", "space"],
	Description = "Rotates a tangent-space normal into world space. Feed it the decoded normal, not " +
		"the raw texel." )]
[NodeVersion( 1 )]
public sealed class TransformNormalNode : PrismNode
{
	/// <summary>The tangent-space normal.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The world-space normal.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <summary>The literal used when <c>In</c> is unconnected: straight out of the surface.</summary>
	[InlineValue( nameof( In ) ), Title( "In" )] public Vector3 DefaultIn { get; set; } = new( 0f, 0f, 1f );

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

		var value = ctx.InAs( nameof( In ), ShaderType.Float3 );

		if ( !value.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Helper( PrismCommon.TangentToWorld, value,
			ctx.Builtin( Builtin.WorldNormal ),
			ctx.Builtin( Builtin.WorldTangentU ),
			ctx.Builtin( Builtin.WorldTangentV ) ) );
	}
}