Code/General/BBoxExtensions.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace ExtendedBox.General;

public static class BBoxExtensions
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBox Grow(this BBox bbox, float value, bool clampAtCenter = false) =>
        bbox.Grow(new Vector3(value), clampAtCenter);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBox Grow(this BBox bbox, in Vector3 value, bool clampAtCenter = false)
    {
        var result = bbox;
        result.Mins -= value;
        result.Maxs += value;

        if(clampAtCenter)
        {
            var center = bbox.Center;
            result.Mins = result.Mins.ComponentMin(center);
            result.Maxs = result.Maxs.ComponentMax(center);
        }

        return result;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBox Shrink(this BBox bbox, float value, bool clampAtCenter = false) =>
        bbox.Shrink(new Vector3(value), clampAtCenter);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBox Shrink(this BBox bbox, in Vector3 value, bool clampAtCenter = false)
    {
        var result = bbox;
        result.Mins += value;
        result.Maxs -= value;

        if(clampAtCenter)
        {
            var center = bbox.Center;
            result.Mins = result.Mins.ComponentMin(center);
            result.Maxs = result.Maxs.ComponentMax(center);
        }

        return result;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBox AddOrCreate(this BBox? @this, Vector3 point)
    {
        if(@this.HasValue)
            return @this.Value.AddPoint(point);
        return new(point, point);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBox AddOrCreate(this BBox? @this, BBox bbox)
    {
        if(@this.HasValue)
            return @this.Value.AddBBox(bbox);
        return bbox;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBox? AddOrCreate(this BBox? @this, IEnumerable<Vector3> points)
    {
        if(!points.Any())
            return @this;

        return @this.AddOrCreate(BBox.FromPoints(points));
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBox? AddOrCreate(this BBox? @this, IEnumerable<BBox> boxes)
    {
        if(!boxes.Any())
            return @this;

        return @this.AddOrCreate(BBox.FromBoxes(boxes));
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBox GetIntersection(this BBox @this, BBox other)
    {
        if([email protected](other))
            return new(Vector3Int.Zero, Vector3Int.Zero);

        BBox result = new()
        {
            Mins = @this.Mins.ComponentMax(other.Mins),
            Maxs = @this.Maxs.ComponentMin(other.Maxs)
        };
        return result;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AlmostEqual(this BBox @this, in BBox other, float delta = 0.0001f) =>
        @this.Mins.AlmostEqual(other.Mins, delta) && @this.Maxs.AlmostEqual(other.Maxs, delta);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBoxInt FloorToInt(this BBox bbox) => new(bbox.Mins.FloorToInt(), bbox.Maxs.FloorToInt());
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBoxInt RoundToInt(this BBox bbox) => new(bbox.Mins.RoundToInt(), bbox.Maxs.RoundToInt());
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBoxInt CeilToInt(this BBox bbox) => new(bbox.Mins.CeilToInt(), bbox.Maxs.CeilToInt());
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static BBoxInt ExpandToInt(this BBox bbox) => new(bbox.Mins.FloorToInt(), bbox.Maxs.CeilToInt());


    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Trace(this BBox bbox, Ray ray, float distance, out float hitDistance, out Vector3Int hitNormal)
    {
        hitDistance = 0f;
        hitNormal = Vector3Int.Zero;

        Vector3 invDir = new Vector3(1.0f / ray.Forward.x, 1.0f / ray.Forward.y, 1.0f / ray.Forward.z);

        Vector3 t0 = (bbox.Mins - ray.Position) * invDir;
        Vector3 t1 = (bbox.Maxs - ray.Position) * invDir;

        Vector3 tNearV = Vector3.Min(t0, t1);
        Vector3 tFarV = Vector3.Max(t0, t1);

        float tEnter = MathF.Max(MathF.Max(tNearV.x, tNearV.y), tNearV.z);
        float tExit = MathF.Min(MathF.Min(tFarV.x, tFarV.y), tFarV.z);

        if(tEnter <= tExit && tExit > 0f && tEnter < distance)
        {
            hitDistance = tEnter < 0f ? 0f : tEnter;

            if(tEnter > 0f)
            {
                if(tEnter == tNearV.x)
                    hitNormal = new Vector3Int(ray.Forward.x > 0 ? -1 : 1, 0, 0);
                else if(tEnter == tNearV.y)
                    hitNormal = new Vector3Int(0, ray.Forward.y > 0 ? -1 : 1, 0);
                else
                    hitNormal = new Vector3Int(0, 0, ray.Forward.z > 0 ? -1 : 1);
            }
            else
            {
                hitNormal = Vector3Int.Zero;
            }

            return true;
        }

        return false;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Trace(this BBox target, Ray ray, float distance, BBox extents, out float hitDistance)
    {
        return target.Trace(ray, distance, extents, out hitDistance, out _);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Trace(this BBox target, Ray ray, float distance, BBox extents, out float hitDistance, out Vector3Int hitNormal)
    {
        Vector3 min = target.Mins - extents.Maxs;
        Vector3 max = target.Maxs - extents.Mins;

        Vector3 invDir = new(1.0f / ray.Forward.x, 1.0f / ray.Forward.y, 1.0f / ray.Forward.z);

        Vector3 t0 = (min - ray.Position) * invDir;
        Vector3 t1 = (max - ray.Position) * invDir;

        Vector3 tNearV = Vector3.Min(t0, t1);
        Vector3 tFarV = Vector3.Max(t0, t1);

        float tEnter = MathF.Max(MathF.Max(tNearV.x, tNearV.y), tNearV.z);
        float tExit = MathF.Min(MathF.Min(tFarV.x, tFarV.y), tFarV.z);

        if(tEnter <= tExit && tExit > 0f && tEnter < distance)
        {
            hitDistance = MathF.Max(0f, tEnter);

            if(tEnter > 0f)
            {
                if(tEnter == tNearV.x)
                    hitNormal = new Vector3Int(ray.Forward.x < 0 ? 1 : -1, 0, 0);
                else if(tEnter == tNearV.y)
                    hitNormal = new Vector3Int(0, ray.Forward.y < 0 ? 1 : -1, 0);
                else
                    hitNormal = new Vector3Int(0, 0, ray.Forward.z < 0 ? 1 : -1);
            }
            else
            {
                hitNormal = Vector3Int.Zero;
            }

            return true;
        }

        hitDistance = 0;
        hitNormal = Vector3Int.Zero;
        return false;
    }
}