Editor/Effigy/Vec.cs
using System;

namespace Effigy;

/// <summary>
/// The kernel's own vector types.
///
/// THIS IS DELIBERATE, NOT AN OVERSIGHT. The kernel exists to be portable — the same source
/// compiles under s&box, Godot's C#, or a plain console runner, and the engine question stays
/// reversible. The moment it references Sandbox.Vector3 or Godot.Vector3 that stops being true and
/// a port becomes a rewrite.
///
/// Engine glue converts at the boundary. That is a handful of lines per engine, paid once.
/// </summary>
public readonly struct Vec3 : IEquatable<Vec3>
{
	public readonly float x, y, z;

	public Vec3( float x, float y, float z )
	{
		this.x = x;
		this.y = y;
		this.z = z;
	}

	public static readonly Vec3 Zero = new( 0, 0, 0 );
	public static readonly Vec3 One = new( 1, 1, 1 );

	public static Vec3 operator +( Vec3 a, Vec3 b ) => new( a.x + b.x, a.y + b.y, a.z + b.z );
	public static Vec3 operator -( Vec3 a, Vec3 b ) => new( a.x - b.x, a.y - b.y, a.z - b.z );
	public static Vec3 operator -( Vec3 a ) => new( -a.x, -a.y, -a.z );
	public static Vec3 operator *( Vec3 a, float s ) => new( a.x * s, a.y * s, a.z * s );
	public static Vec3 operator *( float s, Vec3 a ) => a * s;
	public static Vec3 operator /( Vec3 a, float s ) => new( a.x / s, a.y / s, a.z / s );

	public float LengthSquared => x * x + y * y + z * z;
	public float Length => MathF.Sqrt( LengthSquared );

	/// <summary>Unit-length copy. Returns Zero for a degenerate vector rather than NaN, because a
	/// zero-area face should produce a harmless normal instead of poisoning everything downstream
	/// of it.</summary>
	public Vec3 Normal
	{
		get
		{
			var len = Length;
			return len > 1e-12f ? this / len : Zero;
		}
	}

	public static float Dot( Vec3 a, Vec3 b ) => a.x * b.x + a.y * b.y + a.z * b.z;

	public static Vec3 Cross( Vec3 a, Vec3 b ) => new(
		a.y * b.z - a.z * b.y,
		a.z * b.x - a.x * b.z,
		a.x * b.y - a.y * b.x );

	public static Vec3 Lerp( Vec3 a, Vec3 b, float t ) => a + (b - a) * t;

	public bool Equals( Vec3 o ) => x == o.x && y == o.y && z == o.z;
	public override bool Equals( object obj ) => obj is Vec3 v && Equals( v );
	public override int GetHashCode() => HashCode.Combine( x, y, z );
	public override string ToString() => $"({x:0.####}, {y:0.####}, {z:0.####})";

	/// <summary>Component-wise comparison with a tolerance, for tests and for weld thresholds.</summary>
	public bool AlmostEquals( Vec3 o, float eps = 1e-4f ) =>
		MathF.Abs( x - o.x ) <= eps && MathF.Abs( y - o.y ) <= eps && MathF.Abs( z - o.z ) <= eps;
}

/// <summary>Texture coordinate. Same portability rule as Vec3.</summary>
public readonly struct Vec2
{
	public readonly float x, y;

	public Vec2( float x, float y )
	{
		this.x = x;
		this.y = y;
	}

	public static readonly Vec2 Zero = new( 0, 0 );

	public static Vec2 operator +( Vec2 a, Vec2 b ) => new( a.x + b.x, a.y + b.y );
	public static Vec2 operator -( Vec2 a, Vec2 b ) => new( a.x - b.x, a.y - b.y );
	public static Vec2 operator *( Vec2 a, float s ) => new( a.x * s, a.y * s );
	public static Vec2 operator /( Vec2 a, float s ) => new( a.x / s, a.y / s );

	public float LengthSquared => x * x + y * y;
	public float Length => MathF.Sqrt( LengthSquared );

	/// <summary>Unit-length copy, or Zero for a degenerate vector — same reasoning as Vec3.Normal:
	/// a zero-length edge should produce something harmless rather than NaN.</summary>
	public Vec2 Normal
	{
		get
		{
			var len = Length;
			return len > 1e-12f ? this / len : Zero;
		}
	}

	public static float Dot( Vec2 a, Vec2 b ) => a.x * b.x + a.y * b.y;

	/// <summary>The 2D cross product — a scalar. Its sign says which side of `a` the vector `b`
	/// falls on, which is how winding and "is this point left of that line" get decided.</summary>
	public static float Cross( Vec2 a, Vec2 b ) => a.x * b.y - a.y * b.x;

	public override string ToString() => $"({x:0.####}, {y:0.####})";
}