Marker3d.cs
namespace Marker3d;

public class Marker3d : Component, Component.ExecuteInEditor {
    [Property] public Color Tint { get; set; } = Color.White;

    [Group("Center"), Title("Texture")]
    [Property] public Texture Center { get; set; } = null;
    [Group("Center"), Title("Tint")]
    [Property] public Color CenterTint { get; set; } = Color.White;
    [Group("Center"), Title("Scale")]
    [Property] public float CenterScale { get; set; } = 1f;

    [Group("Arrow"), Title("Texture"), Order(100)]
    [Property] public Texture Arrow { get; set; } = null;
    [Group("Arrow"), Title("Tint"), Order(100)]
    [Property] public Color ArrowTint { get; set; } = Color.White;
    [Group("Arrow"), Title("Rotation"), Order(100)]
    [Property] public float ArrowRotation { get; set; } = 0f;
    [Group("Arrow"), Title("Scale"), Order(100)]
    [Property] public Vector2 ArrowScale { get; set; } = 1f;
    [Group("Arrow"), Title("Distance"), Order(100)]
    [Property] public float ArrowDistance { get; set; } = 32f;

    [Property] public bool UseMarginPercent { get; set; } = false;

    [ShowIf(nameof(UseMarginPercent), false)]
    [Property] public Vector2 MarginPixels { get; set; } = 128f;

    [ShowIf(nameof(UseMarginPercent), true)]
    [Property, Range(0f, 0.5f)] public float MarginPercent { get; set; } = 0.05f;
    protected Vector2 Margin => UseMarginPercent ? MarginPercent : (MarginPixels / Screen.Size);

    [Property, Hide] public bool CenterOverridenOnce { get; set; } = false;
    [Property, Hide] public bool ArrowOverridenOnce { get; set; } = false;

    protected override void OnAwake() {
        FixDefaults();
    }

    protected override void OnUpdate() {
        FixDefaults();
    }

    protected void FixDefaults() {
        if (!Scene.IsEditor) { return; }
        if (!CenterOverridenOnce && Center == null) {
            CenterOverridenOnce = true;
            Center ??= Texture.Load("materials/gizmo/charactercontroller.png");
        }

        if (!ArrowOverridenOnce && Arrow == null) {
            ArrowOverridenOnce = true;
            Arrow ??= Texture.Load("textures/shapes/arrow2.vtex_c");
        }
    }

    public Data? GetData() {
        if (!Scene.Camera.IsValid()) { return null; }

        var percentPosition = Scene.Camera.PointToScreenNormal(WorldPosition, out bool behind);
        var center = Vector2.One * 0.5f;
        var factor = Vector2.One - Margin;
        percentPosition = (percentPosition - center) / factor + center;
        if (behind) {
            percentPosition = (percentPosition - center) * 1000f + center;
        }

        var showArrow = false;
        var arrowDirection = (center - percentPosition).Normal;

        if (percentPosition.x < 0f || percentPosition.x > 1f || percentPosition.y < 0f || percentPosition.y > 1f) {
            var planes = new List<Plane>() {
                new(Vector2.Zero, Vector2.Right),
                new(Vector2.Zero, Vector2.Down),
                new(Vector2.One, Vector2.Left),
                new(Vector2.One, Vector2.Up),
            };
            var intersections = planes
                .Select(x => (x, x.Trace(new Ray(center, (percentPosition - center).Normal))))
                .SelectWhere<(Plane, Vector3?), (Plane, Vector3)>(x => x.Item2.HasValue ? (x.Item1, x.Item2.Value) : null)
                .Select(x => (x.Item2, x.Item1.Normal, x.Item2.DistanceSquared(center)))
                .OrderBy(x => x.Item3)
                .Select(x => (x.Item1, x.Normal));
            if (!intersections.Any()) {
                return null;
            }

            var intersection = intersections.First();
            percentPosition = intersection.Item1;
            showArrow = true;
            // arrowDirection = intersection.Normal;
        }

        percentPosition = (percentPosition - center) * factor + center;

        var position = percentPosition;
        var arrowRotation = float.Atan2(-arrowDirection.Normal.y, -arrowDirection.Normal.x) / float.Pi * 180f + 90f;

        return new() {
            Center = Center,
            CenterTint = Tint * CenterTint,
            CenterPosition = percentPosition,
            CenterScale = CenterScale,

            ShowArrow = showArrow,
            Arrow = Arrow,
            ArrowTint = Tint * ArrowTint,
            ArrowRotation = arrowRotation,
            ArrowLocalRotation = ArrowRotation,
            ArrowScale = ArrowScale,
            ArrowDistance = ArrowDistance,
        };
    }

    public struct Data {
        public Data() {
        }

        public Texture Center { get; set; }
        public Color CenterTint { get; set; } = Color.White;
        public Vector2 CenterPosition { get; set; }
        public float CenterScale { get; set; }

        public bool ShowArrow { get; set; }
        public Texture Arrow { get; set; }
        public Color ArrowTint { get; set; } = Color.White;
        public float ArrowRotation { get; set; }
        public float ArrowLocalRotation { get; set; }
        public Vector2 ArrowScale { get; set; }
        public float ArrowDistance { get; set; }

        public readonly Vector2 ArrowOffset => Vector2.FromDegrees(ArrowRotation) * ArrowDistance;


        public override readonly int GetHashCode() => HashCode.Combine(
            Center,
            CenterTint,
            CenterPosition,

            ShowArrow,
            Arrow,
            ArrowTint,
            ArrowRotation,
            ArrowDistance
        );
    }
}