Internal/ShapeRasterizers.cs
using System;
using Sandbox;

namespace Goo.Internal;

internal static class ShapeRasterizers
{
    public const int BakeResolution = 512;

    // Even-odd scanline fill of unit-coord [0..1] points onto the BakeResolution square; edges use a 16-sub-row supersample. Fewer than 3 points bakes a transparent mask.
    public static byte[] BakePolygon(Vector2[] points)
    {
        int W = BakeResolution;
        byte[] px = new byte[W * W * 4];

        // Pre-fill RGB to white; alpha defaults to 0 (transparent).
        for (int i = 0; i < px.Length; i += 4)
        {
            px[i + 0] = 255;
            px[i + 1] = 255;
            px[i + 2] = 255;
        }

        if (points is null || points.Length < 3) return px;

        const int SubSamples = 16;
        Span<float> xs = stackalloc float[64];
        Span<int> cover = stackalloc int[W];

        for (int y = 0; y < W; y++)
        {
            // Coverage = fraction of sub-rows where this pixel lies inside the polygon.
            // Accumulated per pixel column across sub-rows; final alpha = accum / SubSamples.
            cover.Clear();

            for (int s = 0; s < SubSamples; s++)
            {
                float yLine = y + (s + 0.5f) / SubSamples;
                int count = 0;

                for (int i = 0; i < points.Length; i++)
                {
                    var p1 = points[i];
                    var p2 = points[(i + 1) % points.Length];
                    float y1 = p1.y * W;
                    float y2 = p2.y * W;
                    if ((y1 <= yLine && y2 > yLine) || (y2 <= yLine && y1 > yLine))
                    {
                        float t = (yLine - y1) / (y2 - y1);
                        float xCross = (p1.x + t * (p2.x - p1.x)) * W;
                        if (count < xs.Length) xs[count++] = xCross;
                    }
                }

                if (count < 2) continue;

                // Insertion sort (n is small, typically 2-8).
                for (int i = 1; i < count; i++)
                {
                    float key = xs[i];
                    int j = i - 1;
                    while (j >= 0 && xs[j] > key) { xs[j + 1] = xs[j]; j--; }
                    xs[j + 1] = key;
                }

                for (int i = 0; i + 1 < count; i += 2)
                {
                    int xStart = (int)MathF.Ceiling(xs[i]);
                    int xEnd   = (int)MathF.Floor(xs[i + 1]);
                    if (xStart < 0) xStart = 0;
                    if (xEnd >= W) xEnd = W - 1;
                    for (int x = xStart; x <= xEnd; x++) cover[x]++;
                }
            }

            for (int x = 0; x < W; x++)
            {
                if (cover[x] == 0) continue;
                int idx = (y * W + x) * 4 + 3;
                int a = cover[x] * 255 / SubSamples;
                if (a > 255) a = 255;
                px[idx] = (byte)a;
            }
        }

        return px;
    }

    public static byte[] BakeSector(in ShapeParams p)
    {
        int W = BakeResolution;
        byte[] px = new byte[W * W * 4];
        float center = W * 0.5f;
        float halfDim = center;
        float outerR = halfDim * p.D;       // OuterRadius fraction
        float innerR = halfDim * p.C;       // InnerRadius fraction
        float cornerPx = halfDim * p.E;     // CornerRadius fraction of outer radius (in bake-resolution pixels)
        const float AaBand = 1.0f;

        // Angle convention: 0 deg = up (12 o'clock), clockwise positive.
        // In screen coords (y-down), MathF.Atan2(dx, -dy) puts 0 at up, cw+.
        float startRad = p.A * MathF.PI / 180f;
        float endRad   = p.B * MathF.PI / 180f;
        float arcRad   = endRad - startRad;
        if (arcRad < 0) arcRad += MathF.Tau;
        if (arcRad > MathF.Tau) arcRad = MathF.Tau;
        bool fullSweep = arcRad >= MathF.Tau - 1e-4f;
        bool roundCorners = cornerPx > 0f && !fullSweep;

        for (int y = 0; y < W; y++)
        for (int x = 0; x < W; x++)
        {
            float dx = x - center, dy = y - center;
            float dist = MathF.Sqrt(dx * dx + dy * dy);
            int idx = (y * W + x) * 4;
            px[idx + 0] = 255;
            px[idx + 1] = 255;
            px[idx + 2] = 255;

            // Radial band with 1-px SmoothStep AA
            float outerA = 1f - SmoothStep(outerR - AaBand, outerR + AaBand, dist);
            float innerA = SmoothStep(innerR - AaBand, innerR + AaBand, dist);
            float a = outerA * innerA;

            if (!fullSweep && a > 0f)
            {
                // 0=up, cw+: atan2(dx, -dy) maps right (dx>0, dy=0) to +pi/2
                float angle = MathF.Atan2(dx, -dy);                  // -pi..pi
                if (angle < 0) angle += MathF.Tau;                   // 0..2pi
                float rel = angle - startRad;
                while (rel < 0)          rel += MathF.Tau;
                while (rel >= MathF.Tau) rel -= MathF.Tau;
                float angAaRad = AaBand / MathF.Max(dist, 1f);
                float angA = SmoothStep(-angAaRad, angAaRad, rel);
                float angB = 1f - SmoothStep(arcRad - angAaRad, arcRad + angAaRad, rel);
                a *= angA * angB;

                if (roundCorners && a > 0f)
                {
                    // Signed distances from the pixel to each of the 4 wedge edges.
                    // Positive = inside the wedge from that edge.
                    float dInner   = dist - innerR;                  // distance past inner arc
                    float dOuter   = outerR - dist;                  // distance before outer arc
                    float dStart   = rel * dist;                     // arc-length from start radial
                    float dEnd     = (arcRad - rel) * dist;          // arc-length to end radial

                    // Corner regions are disjoint for small E; MathF.Min guards against overlap.
                    float cornerAlpha = 1f;
                    if (dInner < cornerPx && dStart < cornerPx)
                    {
                        float cx = cornerPx - dInner;
                        float cy = cornerPx - dStart;
                        float d  = MathF.Sqrt(cx * cx + cy * cy);
                        cornerAlpha = MathF.Min(cornerAlpha, 1f - SmoothStep(cornerPx - AaBand, cornerPx + AaBand, d));
                    }
                    if (dInner < cornerPx && dEnd < cornerPx)
                    {
                        float cx = cornerPx - dInner;
                        float cy = cornerPx - dEnd;
                        float d  = MathF.Sqrt(cx * cx + cy * cy);
                        cornerAlpha = MathF.Min(cornerAlpha, 1f - SmoothStep(cornerPx - AaBand, cornerPx + AaBand, d));
                    }
                    if (dOuter < cornerPx && dStart < cornerPx)
                    {
                        float cx = cornerPx - dOuter;
                        float cy = cornerPx - dStart;
                        float d  = MathF.Sqrt(cx * cx + cy * cy);
                        cornerAlpha = MathF.Min(cornerAlpha, 1f - SmoothStep(cornerPx - AaBand, cornerPx + AaBand, d));
                    }
                    if (dOuter < cornerPx && dEnd < cornerPx)
                    {
                        float cx = cornerPx - dOuter;
                        float cy = cornerPx - dEnd;
                        float d  = MathF.Sqrt(cx * cx + cy * cy);
                        cornerAlpha = MathF.Min(cornerAlpha, 1f - SmoothStep(cornerPx - AaBand, cornerPx + AaBand, d));
                    }
                    a *= cornerAlpha;
                }
            }

            a = MathF.Max(0f, MathF.Min(1f, a));
            px[idx + 3] = (byte)(a * 255f);
        }
        return px;
    }

    public static byte[] BakeArc(in ShapeParams p)
    {
        // Arc = sector with inner = mid - stroke/2, outer = mid + stroke/2.
        float halfStroke = p.D * 0.5f;
        var sectorParams = new ShapeParams
        {
            A = p.A,
            B = p.B,
            C = MathF.Max(0f, p.C - halfStroke),
            D = MathF.Min(1f, p.C + halfStroke),
        };
        return BakeSector(in sectorParams);
    }

    static float SmoothStep(float edge0, float edge1, float x)
    {
        if (edge0 == edge1) return x < edge0 ? 0f : 1f;
        float t = MathF.Max(0f, MathF.Min(1f, (x - edge0) / (edge1 - edge0)));
        return t * t * (3f - 2f * t);
    }
}