A UI Panel that draws a triangular ear-shape selector and lets the user pick a point inside that triangle. It clamps arbitrary input to the triangle, draws three colored regions and a marker, and exposes Value and OnValueChanged for consumers.
using System;
using Sandbox.UI;
namespace CharacterEditor;
/// <summary>Visible-pointer triangle input, using the same local coordinates and drag state as HairColorSquare.</summary>
public sealed class EarShapeSurface : Panel
{
public Vector2 Value { get; set; } = new(.5f, 2f / 3f);
public Action<Vector2> OnValueChanged { get; set; }
static readonly Vector2 Elf = new(.5f, 0);
static readonly Vector2 Goblin = new(0, 1);
static readonly Vector2 Animal = new(1, 1);
static readonly Vector2 Human = new(.5f, 2f / 3f);
/// <summary>Project an outside drag onto the closest triangle edge in the displayed dimensions.</summary>
public static Vector2 ClampToTriangle(Vector2 point, Vector2 size)
{
if (!float.IsFinite(point.x) || !float.IsFinite(point.y)) return Human;
if (point.y >= 0 && point.y <= 1 && point.x >= .5f * (1 - point.y) && point.x <= .5f * (1 + point.y)) return point;
size = new Vector2(MathF.Max(1, size.x), MathF.Max(1, size.y));
Vector2 Scale(Vector2 p) => new(p.x * size.x, p.y * size.y);
Vector2 Project(Vector2 a, Vector2 b)
{
var edge = Scale(b - a);
var offset = Scale(point - a);
float dot = offset.x * edge.x + offset.y * edge.y;
float lengthSquared = edge.x * edge.x + edge.y * edge.y;
return a + (b - a) * Math.Clamp(dot / lengthSquared, 0, 1);
}
float Distance(Vector2 p)
{
var d = Scale(point - p);
return d.x * d.x + d.y * d.y;
}
var left = Project(Elf, Goblin);
var bottom = Project(Goblin, Animal);
var right = Project(Animal, Elf);
var nearest = Distance(left) <= Distance(bottom) ? left : bottom;
return Distance(nearest) <= Distance(right) ? nearest : right;
}
public override void OnDraw(Painter painter)
{
base.OnDraw(painter);
using var scope = painter.Scope();
float width = Box.Rect.Width, height = Box.Rect.Height;
if (width <= 0 || height <= 0) return;
Vector2 Screen(Vector2 p) => new(p.x * width, p.y * height);
var elf = Screen(Elf);
var goblin = Screen(Goblin);
var animal = Screen(Animal);
var human = Screen(Human);
var left = (elf + goblin) * .5f;
var right = (elf + animal) * .5f;
var bottom = (goblin + animal) * .5f;
painter.Stroke = Stroke.None;
painter.Fill = new Color(.22f, .29f, .28f);
painter.Quad(elf, right, human, left);
painter.Fill = new Color(.29f, .27f, .18f);
painter.Quad(left, human, bottom, goblin);
painter.Fill = new Color(.31f, .23f, .20f);
painter.Quad(human, right, animal, bottom);
painter.Fill = Color.Transparent;
painter.Stroke = Stroke.Solid(new Color(.68f, .58f, .36f, .75f), ScaleToScreen);
painter.Triangle(elf, goblin, animal);
painter.Stroke = Stroke.Solid(new Color(.78f, .70f, .52f, .22f), ScaleToScreen);
painter.Line(human, left);
painter.Line(human, right);
painter.Line(human, bottom);
painter.Stroke = Stroke.Solid(new Color(.81f, .73f, .57f, .6f), ScaleToScreen);
painter.Fill = new Color(.09f, .10f, .07f);
painter.Circle(human, 3 * ScaleToScreen);
var marker = Screen(ClampToTriangle(Value, new Vector2(width, height)));
painter.Stroke = Stroke.Solid(new Color(.08f, .09f, .06f), 4 * ScaleToScreen);
painter.Fill = Color.Transparent;
painter.Circle(marker, 6 * ScaleToScreen);
painter.Stroke = Stroke.Solid(new Color(.96f, .86f, .65f), 2 * ScaleToScreen);
painter.Circle(marker, 6 * ScaleToScreen);
}
void Pick()
{
var rect = Box.Rect;
if (rect.Width <= 0 || rect.Height <= 0) return;
var mouse = MousePosition;
var position = ClampToTriangle(new Vector2(mouse.x / rect.Width, mouse.y / rect.Height), new Vector2(rect.Width, rect.Height));
Value = position;
OnValueChanged?.Invoke(position);
}
protected override void OnMouseDown(MousePanelEvent e)
{
base.OnMouseDown(e);
if (e.MouseButton != MouseButtons.Left) return;
Pick();
e.StopPropagation();
}
protected override void OnMouseMove(MousePanelEvent e)
{
base.OnMouseMove(e);
if (!HasActive || e.MouseButton == MouseButtons.Right) return;
Pick();
e.StopPropagation();
}
protected override void OnMouseUp(MousePanelEvent e)
{
base.OnMouseUp(e);
if (e.MouseButton == MouseButtons.Left) e.StopPropagation();
}
}