Code/Core/Frame.cs
using System;
using System.Collections.Immutable;
using Sandbox;
using Sandbox.Rendering;

namespace Goo;

// Geometry payload for Sector / Arc Blobs.
// Field meaning by BlobKind:
//   Sector: A=StartAngle (deg, 0=up, cw+), B=EndAngle, C=InnerRadius (0..1), D=OuterRadius (0..1), E=CornerRadius (0..1, fraction of outer radius)
//   Arc:    A=StartAngle,                  B=EndAngle,  C=Radius (0..1, centerline), D=StrokeWidth (0..1), E=unused
public struct ShapeParams : System.IEquatable<ShapeParams>
{
    public float A;
    public float B;
    public float C;
    public float D;
    public float E;   // Sector: CornerRadius (0..1 fraction of outer radius). Arc: unused (0).

    public bool Equals(ShapeParams other) => A == other.A && B == other.B && C == other.C && D == other.D && E == other.E;
    public override bool Equals(object? obj) => obj is ShapeParams p && Equals(p);
    public override int GetHashCode() => System.HashCode.Combine(A, B, C, D, E);
    public static bool operator ==(ShapeParams a, ShapeParams b) => a.Equals(b);
    public static bool operator !=(ShapeParams a, ShapeParams b) => !a.Equals(b);

    // Stable 64-bit hash of (kind, A, B, C, D) -- excludes E; used as a debug-name suffix only.
    public long PackKey(BlobKind kind)
    {
        long qAngle(float deg)
        {
            float wrapped = deg - 360f * System.MathF.Floor(deg / 360f);
            int n = (int)System.MathF.Round(wrapped * (16384f / 360f));
            if (n == 16384) n = 0;
            return (long)n;
        }
        long qUnit(float u)
        {
            int n = (int)System.MathF.Round(u * 16384f);
            if (n < 0) n = 0;
            if (n > 16383) n = 16383;
            return (long)n;
        }
        return ((long)(byte)kind)
             | (qAngle(A) << 8)
             | (qAngle(B) << 22)
             | (qUnit(C) << 36)
             | (qUnit(D) << 50);
    }
}

internal struct Frame
{
    public BlobKind Kind;
    public string? Key;
    public string Content;     // Text-only; empty for non-Text
    public StyleList Style;    // Container-only; null until StyleList added
    public Children? Children; // Container-only; null otherwise
    public BlobEvents Events;  // Container/Text/Image/ScenePanel; default = all null
    public Texture? Texture;   // Image-only; null for non-Image
    public string? Path;       // Image (texture path), ScenePanel (.scene path), SvgPanel (svg path), or WebPanel (URL); null otherwise
    public Scene? Scene;       // ScenePanel-only; null for non-ScenePanel
    public bool RenderOnce;    // ScenePanel-only; false for non-ScenePanel
    public bool Paused;        // WebPanel-only; false for non-WebPanel
    public string? Color;     // SvgPanel-only; null for non-SvgPanel
    // TextEntry-only carry-fields. Path slot is reused for the text value via
    // the Op.Text alias; these are the additional control-surface fields.
    public string? Placeholder;
    public int?    MaxLength;
    public bool    Disabled;
    public bool    Numeric;
    public float?  MinValue;
    public float?  MaxValue;
    public string? NumberFormat;
    public bool    Multiline;
    public Action<string>? OnChange;
    public Action<string>? OnSubmit;
    public Action?         OnFocus;
    public Action<string>? OnBlur;
    public Action?         OnCancel;
    public bool    IsControlled;
    public bool    ValueAndInitialTextBothSet;
    public ShapeParams Shape;       // Sector / Arc only; default for non-shape kinds
    public Vector2[]? Points;       // Polygon-only; null for non-Polygon kinds
    // Custom-draw state (Container only in Plan 1; future blobs may carry this too).
    public Material?                       Material;
    public ImmutableArray<UniformValue>    Uniforms;
    public Action<CommandList, Rect>?      Draw;
    // Cell-only carry-fields; null/default for non-Cell kinds.
    public Func<Cell>?    CellFactory;   // first-mount path (new TCell())
    public Action<Cell>?  Configure;     // refresh props on the persistent instance each rebuild
    public Type?          CellType;      // typeof(TCell): reuse-vs-fresh match check
}