[CustomEditor(typeof(VirtualValue))]
public class VirtualValuePolarEditor : Editor
// Temporary parameters for controlling the virtual value in polar space.
// "polarCorner" is now expressed as a continuous value from 1 to 3.
// A value of 1 means corner 1, 2 means corner 2, 3 means corner 3,
// with interpolation in between.
private float polarCorner = 1f;
// Radius remains [0,1]: 0 at center, 1 at the edge.
private float polarRadius = 0f;
public override void OnInspectorGUI()
// 1) Draw default inspector (exposes timelineValue, oscValue1, oscValue2, etc.)
// 2) Draw polar controls.
EditorGUILayout.LabelField("Polar Controls (Expressed as 1,2,3)", EditorStyles.boldLabel);
// Slider for the "corner" parameter. It ranges from 1 to 3.
polarCorner = EditorGUILayout.Slider("Corner", polarCorner, 1f, 3f);
// Slider for the polar radius.
polarRadius = EditorGUILayout.Slider("Polar Radius", polarRadius, 0f, 1f);
// 3) Provide three buttons for quickly jumping to a triangle's corner.
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Corner 1"))
polarRadius = 1f; // exactly on the vertex
if (GUILayout.Button("Corner 2"))
if (GUILayout.Button("Corner 3"))
EditorGUILayout.EndHorizontal();
// 4) Convert the discrete "corner" to an angle in degrees.
float angleDegrees = DiscreteCornerToAngle(polarCorner);
// 5) Convert polar (angle in degrees, radius) to barycentrics.
Vector3 bary = PolarToBarycentrics(angleDegrees, polarRadius);
// 6) Display the computed barycentrics.
EditorGUILayout.LabelField("Computed Barycentrics:",
string.Format("u: {0:F2}, v: {1:F2}, w: {2:F2}", bary.x, bary.y, bary.z));
// 7) Update the VirtualValue's barycentrics.
VirtualValue vv = (VirtualValue)target;
EditorGUILayout.LabelField("Visualizations", EditorStyles.boldLabel);
// 8) Reserve a rectangular area for drawing both the polar circle and the triangle.
Rect overallRect = EditorGUILayout.GetControlRect(false, 200f);
float halfWidth = overallRect.width * 0.5f;
Rect circleRect = new Rect(overallRect.x, overallRect.y, halfWidth, overallRect.height);
Rect triangleRect = new Rect(overallRect.x + halfWidth, overallRect.y, halfWidth, overallRect.height);
// 9) Draw the polar circle in its designated area.
GUI.BeginGroup(circleRect);
// Define the local center and radius of the polar domain.
Vector2 circleCenter = new Vector2(circleRect.width * 0.5f, circleRect.height * 0.5f);
float domainRadius = Mathf.Min(circleRect.width, circleRect.height) * 0.5f - 5f;
// Draw the polar domain as a gray wire disc.
Handles.color = Color.gray;
Handles.DrawWireDisc(circleCenter, Vector3.forward, domainRadius);
// Compute the polar point using our mapped angle.
float angleRad = angleDegrees * Mathf.Deg2Rad;
float xOffset = polarRadius * Mathf.Cos(angleRad) * domainRadius;
float yOffset = polarRadius * Mathf.Sin(angleRad) * domainRadius;
Vector2 polarPoint = circleCenter + new Vector2(xOffset, yOffset);
// Compute a blended color based on barycentrics.
Color blended = bary.x * Color.red + bary.y * Color.green + bary.z * Color.blue;
Handles.DrawSolidDisc(polarPoint, Vector3.forward, 5f);
Handles.color = Color.yellow;
Handles.DrawWireDisc(polarPoint, Vector3.forward, 5f);
// 10) Draw the triangle visualization.
GUI.BeginGroup(triangleRect);
// Define an equilateral triangle.
Vector2 triCenter = new Vector2(triangleRect.width * 0.5f, triangleRect.height * 0.5f);
float triScale = Mathf.Min(triangleRect.width, triangleRect.height) * 0.4f;
// Use the same vertices as our polar-to-barycentrics mapping.
Vector2 v1 = triCenter + new Vector2(0, triScale); // Corner 1 (red)
Vector2 v2 = triCenter + new Vector2(-triScale * Mathf.Sqrt(3)/2f, -triScale * 0.5f); // Corner 2 (blue)
Vector2 v3 = triCenter + new Vector2(triScale * Mathf.Sqrt(3)/2f, -triScale * 0.5f); // Corner 3 (green)
Handles.color = Color.red;
Handles.DrawLine(v1, v2);
Handles.color = Color.blue;
Handles.DrawLine(v2, v3);
Handles.color = Color.green;
Handles.DrawLine(v3, v1);
// Compute the point inside the triangle using barycentrics.
Vector2 triPoint = bary.x * v1 + bary.y * v2 + bary.z * v3;
Color blendedTri = bary.x * Color.red + bary.y * Color.blue + bary.z * Color.green;
Handles.color = blendedTri;
Handles.DrawSolidDisc(triPoint, Vector3.forward, 5f);
Handles.color = Color.yellow;
Handles.DrawWireDisc(triPoint, Vector3.forward, 5f);
EditorUtility.SetDirty(target);
/// Maps a value in [1,3] to an angle in degrees corresponding to the triangle's corners.
/// Corner 1 = 90°, Corner 2 = 210°, Corner 3 = 330°.
/// Values in between are linearly interpolated along the shortest arc.
private float DiscreteCornerToAngle(float t)
return Mathf.Lerp(90f, 210f, t - 1f);
return Mathf.Lerp(210f, 330f, t - 2f);
// Convert polar coordinates (angle in degrees, radius in [0,1]) to barycentrics.
private Vector3 PolarToBarycentrics(float angleDegrees, float radius)
// Define an equilateral triangle in 2D.
// Vertices correspond to:
// P1: timelineValue (red) at (0,1) → 90°
// P2: oscValue1 (green) at (√3/2,-0.5) → 330°
// P3: oscValue2 (blue) at (-√3/2,-0.5) → 210°
Vector2 P1 = new Vector2(0, 1);
Vector2 P2 = new Vector2(Mathf.Sqrt(3) / 2, -0.5f);
Vector2 P3 = new Vector2(-Mathf.Sqrt(3) / 2, -0.5f);
// Convert the angle to a direction vector.
float angleRad = angleDegrees * Mathf.Deg2Rad;
Vector2 d = new Vector2(Mathf.Cos(angleRad), Mathf.Sin(angleRad));
// Find intersection of ray (origin,d) with each triangle edge.
float tMin = float.PositiveInfinity;
if (RaySegmentIntersection(Vector2.zero, d, P1, P2, out float t12))
tMin = Mathf.Min(tMin, t12);
if (RaySegmentIntersection(Vector2.zero, d, P2, P3, out float t23))
tMin = Mathf.Min(tMin, t23);
if (RaySegmentIntersection(Vector2.zero, d, P3, P1, out float t31))
tMin = Mathf.Min(tMin, t31);
// Intersection point on the triangle's edge.
Vector2 edgePoint = d * tMin;
// Lerp from center (0,0) to the edge based on radius.
Vector2 finalPoint = Vector2.Lerp(Vector2.zero, edgePoint, radius);
return ComputeBarycentrics2D(P1, P2, P3, finalPoint);
// Computes the intersection of a ray and a segment in 2D.
private static bool RaySegmentIntersection(Vector2 rayOrigin, Vector2 rayDir, Vector2 segA, Vector2 segB, out float t)
Vector2 v1 = rayOrigin - segA;
Vector2 v2 = segB - segA;
Vector2 v3 = new Vector2(-rayDir.y, rayDir.x);
float dot = Vector2.Dot(v2, v3);
if (Mathf.Abs(dot) < 0.0001f)
float t1 = Cross(v2, v1) / dot;
float t2 = Vector2.Dot(v1, v3) / dot;
if (t1 >= 0 && t2 >= 0 && t2 <= 1)
private static float Cross(Vector2 a, Vector2 b)
return a.x * b.y - a.y * b.x;
// Compute barycentrics for point P with respect to triangle ABC (2D).
private static Vector3 ComputeBarycentrics2D(Vector2 A, Vector2 B, Vector2 C, Vector2 P)
float det = (B.y - C.y) * (A.x - C.x) + (C.x - B.x) * (A.y - C.y);
float u = ((B.y - C.y) * (P.x - C.x) + (C.x - B.x) * (P.y - C.y)) / det;
float v = ((C.y - A.y) * (P.x - C.x) + (A.x - C.x) * (P.y - C.y)) / det;
return new Vector3(u, v, w);