Editor/Effigy/MeshRaycast.cs
using System;
using System.Collections.Generic;

namespace Effigy;

/// <summary>Where a ray hit a mesh: the point, the face it hit, and that face's normal.</summary>
public readonly struct MeshHit
{
	public readonly Vec3 Point;
	public readonly int FaceIndex;
	public readonly Vec3 Normal;
	public readonly float Distance;

	public MeshHit( Vec3 point, int faceIndex, Vec3 normal, float distance )
	{
		Point = point;
		FaceIndex = faceIndex;
		Normal = normal;
		Distance = distance;
	}
}

/// <summary>
/// Ray-mesh intersection, for clicking a face of a solid in the viewport.
///
/// PURE GEOMETRY, NO ENGINE SURFACE — which is why it lives here rather than in the editor. The
/// only thing the viewport contributes is the ray itself (Gizmo.CurrentRay, converted to Vec3);
/// everything about deciding which triangle it hit is ordinary math that can be built and proven
/// without s&box anywhere near it.
///
/// Faces are triangulated the same way EffigyPreview builds the render mesh — by Triangulate.Face
/// — so a click hits exactly the triangle that would actually be drawn there. A different
/// triangulation would occasionally pick a face whose diagonal put the real geometry on the other
/// side of the click, and a fan over a concave cap would let clicks land in the notch it wrongly
/// filled in.
/// </summary>
public static class MeshRaycast
{
	/// <summary>
	/// The nearest face of <paramref name="mesh"/> that <paramref name="origin"/> + t *
	/// <paramref name="direction"/> hits, for t > 0. Null if nothing is hit.
	/// </summary>
	public static MeshHit? Raycast( PolyMesh mesh, Vec3 origin, Vec3 direction )
	{
		if ( mesh is null )
			return null;

		var dir = direction.Normal;

		MeshHit? best = null;

		for ( var fi = 0; fi < mesh.Faces.Count; fi++ )
		{
			if ( !HitFace( mesh, fi, origin, dir, out var t, out var point ) )
				continue;

			if ( best is { } current && t >= current.Distance )
				continue;

			best = new MeshHit( point, fi, mesh.FaceNormal( mesh.Faces[fi] ), t );
		}

		return best;
	}

	/// <summary>
	/// Nearest triangle of one face. Shared by the linear scan and the BVH so a click cannot
	/// disagree with a stroke sample about which triangle was there.
	/// </summary>
	public static bool HitFace( PolyMesh mesh, int faceIndex, Vec3 origin, Vec3 dir, out float t, out Vec3 point )
	{
		t = 0f;
		point = default;

		if ( mesh is null || faceIndex < 0 || faceIndex >= mesh.Faces.Count )
			return false;

		var face = mesh.Faces[faceIndex];

		if ( face.Count < 3 )
			return false;

		var corners = new List<Vec3>( face.Count );

		for ( var c = 0; c < face.Count; c++ )
			corners.Add( mesh.Positions[face.Indices[c]] );

		var hit = false;
		var bestT = float.MaxValue;
		var bestP = default( Vec3 );

		foreach ( var (ia, ib, ic) in Triangulate.Face( corners ) )
		{
			if ( !TriangleHit( origin, dir, corners[ia], corners[ib], corners[ic], out var cand, out var p ) )
				continue;

			if ( cand >= bestT )
				continue;

			bestT = cand;
			bestP = p;
			hit = true;
		}

		if ( !hit )
			return false;

		t = bestT;
		point = bestP;
		return true;
	}

	/// <summary>
	/// Nearest hit across several bodies at once, with the winning body reported alongside it —
	/// what a click in a multi-body studio actually needs.
	/// </summary>
	/// <summary>
	/// The nearest face of any of <paramref name="bodies"/> that YOU CAN ACTUALLY SEE.
	///
	/// Effigy does not union bodies - two overlapping extrudes are two separate closed solids, and
	/// the faces of one that fall inside the other are still there, still hit by a ray, and quite
	/// invisible. Picking one is how you end up sketching on a plane buried inside your part,
	/// which is exactly as confusing as it sounds: the highlight paints a rectangle straight
	/// through the model and the sketch lands somewhere you never pointed at.
	///
	/// So a hit is discarded when the surface it landed on is inside another solid. Sorting the
	/// candidates first means the common case - nothing overlapping - costs one containment test.
	/// </summary>
	public static (Body Body, MeshHit Hit)? Raycast( IEnumerable<Body> bodies, Vec3 origin, Vec3 direction )
	{
		if ( bodies is null )
			return null;

		var list = new List<Body>();

		foreach ( var body in bodies )
		{
			if ( body?.Mesh is not null )
				list.Add( body );
		}

		var candidates = new List<(Body Body, MeshHit Hit)>( list.Count );

		foreach ( var body in list )
		{
			if ( Raycast( body.Mesh, origin, direction ) is { } hit )
				candidates.Add( (body, hit) );
		}

		candidates.Sort( ( a, b ) => a.Hit.Distance.CompareTo( b.Hit.Distance ) );

		var dir = direction.Normal;

		foreach ( var candidate in candidates )
		{
			// Step back off the surface along the ray, so the test point is in the space the ray
			// travelled through rather than exactly on the boundary, where inside/outside is a
			// coin flip. Scaled by the distance travelled, since a sketch can be a unit across or
			// a thousand.
			var epsilon = 1e-4f * (1f + candidate.Hit.Distance);
			var probe = candidate.Hit.Point - dir * epsilon;
			var buried = false;

			foreach ( var other in list )
			{
				if ( ReferenceEquals( other, candidate.Body ) )
					continue;

				if ( PointInsideSolid( other.Mesh, probe ) )
				{
					buried = true;
					break;
				}
			}

			if ( !buried )
				return candidate;
		}

		return null;
	}

	/// <summary>
	/// Is a point inside a closed mesh? Crossing count along an arbitrary ray: odd is inside.
	///
	/// The direction is a fixed lopsided one rather than an axis, because an axis-aligned ray from
	/// a point on a box lands exactly along edges and coplanar faces, and every such ray is a
	/// coin-flip on whether a crossing gets counted once, twice or not at all.
	/// </summary>
	public static bool PointInsideSolid( PolyMesh mesh, Vec3 point )
	{
		if ( mesh is null || mesh.Faces.Count == 0 )
			return false;

		var direction = new Vec3( 0.5773f, 0.5771f, 0.5775f ).Normal;
		var crossings = 0;

		foreach ( var face in mesh.Faces )
		{
			if ( face.Count < 3 )
				continue;

			var corners = new List<Vec3>( face.Count );

			for ( var c = 0; c < face.Count; c++ )
				corners.Add( mesh.Positions[face.Indices[c]] );

			foreach ( var (ia, ib, ic) in Triangulate.Face( corners ) )
			{
				if ( TriangleHit( point, direction, corners[ia], corners[ib], corners[ic], out _, out _ ) )
					crossings++;
			}
		}

		return (crossings & 1) == 1;
	}

	/// <summary>
	/// Möller–Trumbore. Returns the ray parameter and world point on a hit with t > 0; a
	/// back-facing triangle counts too, since a click through a thin wall should still register
	/// something rather than nothing.
	/// </summary>
	public static bool TriangleHit( Vec3 origin, Vec3 dir, Vec3 a, Vec3 b, Vec3 c, out float t, out Vec3 point )
	{
		t = 0f;
		point = default;

		const float eps = 1e-7f;

		var edge1 = b - a;
		var edge2 = c - a;
		var h = Vec3.Cross( dir, edge2 );
		var det = Vec3.Dot( edge1, h );

		if ( MathF.Abs( det ) < eps )
			return false;

		var invDet = 1f / det;
		var s = origin - a;
		var u = invDet * Vec3.Dot( s, h );

		if ( u < -eps || u > 1f + eps )
			return false;

		var q = Vec3.Cross( s, edge1 );
		var v = invDet * Vec3.Dot( dir, q );

		if ( v < -eps || u + v > 1f + eps )
			return false;

		var candidate = invDet * Vec3.Dot( edge2, q );

		if ( candidate <= eps )
			return false;

		t = candidate;
		point = origin + dir * t;
		return true;
	}
}