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
}