[CustomEditor(typeof(VirtualValue))]
public class VirtualValueEditor : Editor
const float TriangleSize = 150f;
const float SnapDistance = 0.1f;
private GUIStyle labelStyle;
public override void OnInspectorGUI()
VirtualValue vv = (VirtualValue)target;
labelStyle = new GUIStyle(EditorStyles.boldLabel)
alignment = TextAnchor.MiddleCenter,
Rect rect = GUILayoutUtility.GetRect(TriangleSize, TriangleSize);
Vector2 center = rect.center;
Vector2 p0 = center + new Vector2(0, -TriangleSize / 2); // Timeline
Vector2 p1 = center + Quaternion.Euler(0, 0, 120) * new Vector2(0, -TriangleSize / 2); // Audio
Vector2 p2 = center + Quaternion.Euler(0, 0, -120) * new Vector2(0, -TriangleSize / 2); // Manual
Handles.DrawSolidRectangleWithOutline(new Vector3[] { p0, p1, p2 }, new Color(1, 1, 1, 0.05f), Color.gray);
Handles.Label(p0 + Vector2.up * 12, "Timeline", labelStyle);
Handles.Label(p1 + Vector2.left * 20, "Audio", labelStyle);
Handles.Label(p2 + Vector2.right * 20, "Manual", labelStyle);
Vector3 weights = GetWeightsFromPerimeterPosition(vv.perimeterPosition);
Vector2 markerPos = weights.x * p0 + weights.y * p1 + weights.z * p2;
// Draw highlight if snapping to a corner
Vector2 mousePos = Event.current.mousePosition;
float snapSize = SnapDistance * TriangleSize;
if ((mousePos - p0).magnitude < snapSize) DrawPulse(p0, Color.cyan);
else if ((mousePos - p1).magnitude < snapSize) DrawPulse(p1, Color.magenta);
else if ((mousePos - p2).magnitude < snapSize) DrawPulse(p2, Color.yellow);
// Marker (final selection point)
Handles.color = Color.yellow;
Handles.DrawSolidDisc(markerPos, Vector3.forward, 5f);
if ((e.type == EventType.MouseDown || e.type == EventType.MouseDrag) && rect.Contains(e.mousePosition))
vv.perimeterPosition = ClosestPointOnPerimeter(e.mousePosition, p0, p1, p2, out t);
EditorUtility.SetDirty(vv);
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("Blending Weights", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"Alpha (Timeline): {vv.Alpha:F2}");
EditorGUILayout.LabelField($"Beta (Audio): {vv.Beta:F2}");
EditorGUILayout.LabelField($"Gamma (Manual): {vv.Gamma:F2}");
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Output Value", EditorStyles.boldLabel);
GUIStyle valueStyle = new GUIStyle(EditorStyles.label)
fontStyle = FontStyle.Bold
EditorGUILayout.LabelField(vv.Value.ToString("F3"), valueStyle);
private void DrawPulse(Vector2 position, Color color)
float pulse = Mathf.Abs(Mathf.Sin((float)EditorApplication.timeSinceStartup * 3f)) * 4f + 6f;
Handles.DrawSolidDisc(position, Vector3.forward, pulse);
private float ClosestPointOnPerimeter(Vector2 point, Vector2 p0, Vector2 p1, Vector2 p2, out float tAlong)
float minDist = float.MaxValue;
float[] edgeOffsets = { 0f, 1f, 2f };
for (int i = 0; i < 3; i++)
Vector2 closest = ClosestPointOnSegment(a, b, point, out float t);
float dist = Vector2.Distance(closest, point);
totalT = edgeOffsets[i] + t;
if (minDist < SnapDistance * TriangleSize)
if (Vector2.Distance(point, p0) < SnapDistance * TriangleSize) return 0f;
if (Vector2.Distance(point, p1) < SnapDistance * TriangleSize) return 1f;
if (Vector2.Distance(point, p2) < SnapDistance * TriangleSize) return 2f;
private Vector2 ClosestPointOnSegment(Vector2 a, Vector2 b, Vector2 point, out float t)
t = Mathf.Clamp01(Vector2.Dot(point - a, ab) / ab.sqrMagnitude);
private Vector3 GetWeightsFromPerimeterPosition(float pos)
float alpha = 0f, beta = 0f, gamma = 0f;
return new Vector3(alpha, beta, gamma);
public Edge(Vector2 a, Vector2 b)