308 lines
13 KiB
C#
308 lines
13 KiB
C#
using ImGuiNET;
|
|
using T3.Core.DataTypes.Vector;
|
|
using T3.Core.Utils;
|
|
using T3.Editor.Gui.Styling;
|
|
|
|
namespace T3.Editor.Gui.Interaction;
|
|
|
|
/// <summary>
|
|
/// Draws a circular dial to manipulate values with various speeds.
|
|
///
|
|
/// The manipulation area centers left and right of the current mouse position
|
|
/// on the ring -- with a value jump on the opposite side.
|
|
///
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Terminology
|
|
/// range - normalized angle from -0.5 ... 0.5 with 0 at current value
|
|
/// valueRange - delta value for complete revolution of current dial
|
|
/// tickInterval = Log10 delta vale between ticks.
|
|
/// </remarks>
|
|
public static class RadialSliderOverlay
|
|
{
|
|
public static void Draw(ref double roundedValue, bool restarted, Vector2 center, double min = double.NegativeInfinity,
|
|
double max = double.PositiveInfinity,
|
|
float scale = 0.1f,
|
|
bool clampMin = false, // Todo Implement
|
|
bool clampMax= false)
|
|
{
|
|
var drawList = ImGui.GetForegroundDrawList();
|
|
_io = ImGui.GetIO();
|
|
|
|
if (restarted)
|
|
{
|
|
_value = roundedValue;
|
|
//_mousePositions.Clear();
|
|
_center = _io.MousePos;
|
|
_dampedRadius = 50;
|
|
_dampedAngleVelocity = 0;
|
|
_dampedModifierScaleFactor = 1;
|
|
_lastValueAngle = 0;
|
|
_framesSinceLastMove = 120;
|
|
_originalValue = roundedValue;
|
|
_framesSinceStart = 0;
|
|
}
|
|
|
|
|
|
//_mousePositions.Add(_io.MousePos);
|
|
// if (_mousePositions.Count > 100)
|
|
// {
|
|
// _mousePositions.RemoveAt(0);
|
|
// }
|
|
_framesSinceStart++;
|
|
//
|
|
// if (_framesSinceStart <= 1)
|
|
// return;
|
|
|
|
var p1 = _io.MousePos;
|
|
var mousePosRadius = Vector2.Distance(_center, p1);
|
|
|
|
// Update angle...
|
|
var dir = p1 - _center;
|
|
var mouseAngle = MathF.Atan2(dir.X, dir.Y);
|
|
var deltaAngle = DeltaAngle(mouseAngle, _lastValueAngle);
|
|
_lastValueAngle = mouseAngle;
|
|
|
|
var hasMoved = Math.Abs(deltaAngle) > 0.015f;
|
|
if (hasMoved)
|
|
{
|
|
|
|
_framesSinceLastMove = 0;
|
|
}
|
|
else
|
|
{
|
|
//if(Math.Abs(mousePosRadius - _dampedRadius) > 40)
|
|
_framesSinceLastMove++;
|
|
}
|
|
|
|
_dampedAngleVelocity = MathUtils.Lerp(_dampedAngleVelocity, (float)deltaAngle, 0.06f);
|
|
|
|
// Update radius and value range
|
|
const float maxRadius = 2500;
|
|
var angleDampingFromDelay = (_framesSinceStart > 30 ? MathF.Pow(MathUtils.SmootherStep(2, 15, _framesSinceLastMove),4) : 1) * 0.07f;
|
|
var angleDampingFromRadiusDistance = MathF.Pow(MathUtils.SmootherStep(40, 70, Math.Abs(mousePosRadius - _dampedRadius)), 4);
|
|
_dampedRadius = MathUtils.Lerp(_dampedRadius, mousePosRadius.Clamp(40f,maxRadius), angleDampingFromDelay * angleDampingFromRadiusDistance);
|
|
var normalizedClampedRadius = ( _dampedRadius/1000).Clamp(0.0f, 1);
|
|
|
|
// Value range and tick interval
|
|
_dampedModifierScaleFactor = MathUtils.Lerp(_dampedModifierScaleFactor, GetKeyboardScaleFactor(), 0.1f);
|
|
var valueRange = (Math.Pow(3 * (normalizedClampedRadius ), 3)) * 50 * scale * _dampedModifierScaleFactor;
|
|
var tickInterval = Math.Pow(10, (int)Math.Log10(valueRange * 250 / _dampedRadius) - 2) ;
|
|
|
|
|
|
// Update value...
|
|
_value += deltaAngle / (Math.PI * 2) * valueRange;
|
|
roundedValue = _io.KeyCtrl ? _value : Math.Round(_value / (tickInterval)) * tickInterval;
|
|
|
|
var numberOfTicks = valueRange / tickInterval;
|
|
var anglePerTick = 2*Math.PI / numberOfTicks;
|
|
|
|
var valueTickOffsetFactor = MathUtils.Fmod(_value, tickInterval) / tickInterval;
|
|
var tickRatioAlignmentAngle = anglePerTick * valueTickOffsetFactor ;
|
|
|
|
drawList.AddCircle(_center, _dampedRadius+25, UiColors.BackgroundFull.Fade(0.1f), 128, 50);
|
|
|
|
// Draw ticks with labels
|
|
for (int tickIndex = -(int)numberOfTicks/2; tickIndex < numberOfTicks/2; tickIndex++)
|
|
{
|
|
var f = MathF.Pow(MathF.Abs(tickIndex / ((float)numberOfTicks/2)), 0.5f);
|
|
var negF = 1 - f;
|
|
var tickAngle = tickIndex * anglePerTick - mouseAngle - tickRatioAlignmentAngle ;
|
|
var direction = new Vector2(MathF.Sin(-(float)tickAngle), MathF.Cos(-(float)tickAngle));
|
|
var valueAtTick = _value + (tickIndex * anglePerTick - tickRatioAlignmentAngle) / (2 * Math.PI) * valueRange;
|
|
var isPrimary = Math.Abs(MathUtils.Fmod(valueAtTick + tickInterval * 5, tickInterval * 10) - tickInterval * 5) < tickInterval / 10;
|
|
var isPrimary2 = Math.Abs(MathUtils.Fmod(valueAtTick + tickInterval * 50, tickInterval * 100) - tickInterval * 50) < tickInterval / 100;
|
|
|
|
drawList.AddLine(direction * _dampedRadius + _center,
|
|
direction * (_dampedRadius + (isPrimary ? 10 : 5f)) + _center,
|
|
UiColors.ForegroundFull.Fade(negF * (isPrimary ? 1 : 0.5f)),
|
|
1
|
|
);
|
|
|
|
if (!isPrimary)
|
|
continue;
|
|
|
|
var font = isPrimary2 ? Fonts.FontBold : Fonts.FontSmall;
|
|
var v = Math.Abs(valueAtTick) < 0.0001 ? 0 : valueAtTick;
|
|
var label = $"{v:G5}";
|
|
|
|
ImGui.PushFont(font);
|
|
var size = ImGui.CalcTextSize(label);
|
|
ImGui.PopFont();
|
|
|
|
drawList.AddText(font,
|
|
font.FontSize,
|
|
direction * (_dampedRadius + 30) + _center - size/2,
|
|
UiColors.ForegroundFull.Fade(negF * (isPrimary2 ? 1 : 0.3f)),
|
|
label);
|
|
}
|
|
|
|
// Draw previous value
|
|
{
|
|
var visible = IsValueVisible(_originalValue, valueRange);
|
|
if (visible)
|
|
{
|
|
var originalValueAngle= ComputeAngleForValue(_originalValue, valueRange, mouseAngle);
|
|
var direction = new Vector2(MathF.Sin(originalValueAngle), MathF.Cos(originalValueAngle));
|
|
drawList.AddLine(direction * _dampedRadius + _center,
|
|
direction * (_dampedRadius - 10) + _center,
|
|
UiColors.StatusActivated.Fade(0.8f),
|
|
2
|
|
);
|
|
}
|
|
}
|
|
|
|
// Draw Value range
|
|
{
|
|
var rangeMin = _value - valueRange * 0.45f;
|
|
var rangeMax = _value + valueRange * 0.45f;
|
|
if (!(min < rangeMin && max < rangeMin || (min > rangeMax && max > rangeMax)))
|
|
{
|
|
const float innerOffset = 4;
|
|
var minAngle= ComputeAngleForValue(Math.Max(min, rangeMin), valueRange, mouseAngle);
|
|
var maxAngle= ComputeAngleForValue(Math.Min(max, rangeMax), valueRange, mouseAngle);
|
|
|
|
drawList.PathClear();
|
|
var minVisible = GetDirectionForValueIfVisible(min, valueRange, mouseAngle, out var dirForMinIndicator);
|
|
if (minVisible)
|
|
{
|
|
drawList.PathLineTo(_center + dirForMinIndicator * (_dampedRadius - 10));
|
|
}
|
|
|
|
drawList.PathArcTo(_center, _dampedRadius-innerOffset, -minAngle + MathF.PI/2 , -maxAngle + MathF.PI/2, 90);
|
|
|
|
var maxVisible = GetDirectionForValueIfVisible(max, valueRange, mouseAngle, out var dirForMaxIndicator);
|
|
if (maxVisible)
|
|
{
|
|
drawList.PathLineTo(_center + dirForMaxIndicator * (_dampedRadius - 10));
|
|
}
|
|
drawList.PathStroke(UiColors.ForegroundFull.Fade(0.4f), ImDrawFlags.None, 2);
|
|
|
|
if (!minVisible)
|
|
{
|
|
drawList.PathClear();
|
|
drawList.PathArcTo(_center, _dampedRadius -innerOffset , -minAngle + MathF.PI/2 - 5 * MathUtils.ToRad , -minAngle + MathF.PI/2, 90);
|
|
drawList.PathStroke(UiColors.ForegroundFull.Fade(0.3f), ImDrawFlags.None, 2);
|
|
}
|
|
if (!maxVisible)
|
|
{
|
|
drawList.PathClear();
|
|
drawList.PathArcTo(_center, _dampedRadius - innerOffset, -maxAngle + MathF.PI/2 + 5 * MathUtils.ToRad , -maxAngle + MathF.PI/2, 90);
|
|
drawList.PathStroke(UiColors.ForegroundFull.Fade(0.2f), ImDrawFlags.None, 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Current value at mouse
|
|
{
|
|
var dialFade = MathUtils.SmootherStep(60, 160, _dampedRadius);
|
|
var dialAngle= (float)( (_value - roundedValue) * (2 * Math.PI) / valueRange + mouseAngle);
|
|
_dampedDialValueAngle = MathUtils.LerpRadianAngle(_dampedDialValueAngle, dialAngle, 0.4f);
|
|
var direction = new Vector2(MathF.Sin(_dampedDialValueAngle), MathF.Cos(_dampedDialValueAngle));
|
|
drawList.AddLine(direction * _dampedRadius + _center,
|
|
direction * (_dampedRadius + 30) + _center,
|
|
UiColors.ForegroundFull.Fade(0.7f * dialFade),
|
|
2
|
|
);
|
|
|
|
var labelFade = MathUtils.SmootherStep(200, 300, _dampedRadius);
|
|
|
|
drawList.AddText(Fonts.FontBold,
|
|
Fonts.FontBold.FontSize,
|
|
direction * (_dampedRadius - 40) + _center + new Vector2(-15,-Fonts.FontSmall.FontSize/2),
|
|
Color.White.Fade(labelFade * dialFade),
|
|
$"{roundedValue:0.00}\n"
|
|
);
|
|
}
|
|
}
|
|
|
|
private static bool IsValueVisible(double value, double valueRange)
|
|
{
|
|
return Math.Abs(_value - value) < valueRange * 0.45f;
|
|
}
|
|
|
|
private static float ComputeAngleForValue(double value, double valueRange, float mouseAngle)
|
|
{
|
|
return (float)( (_value - value) * (2 * Math.PI) / valueRange + mouseAngle);
|
|
}
|
|
|
|
private static bool GetDirectionForValueIfVisible(double value, double valueRange, float mouseAngle, out Vector2 direction)
|
|
{
|
|
direction = Vector2.Zero;
|
|
|
|
if(!IsValueVisible(value, valueRange))
|
|
return false;
|
|
|
|
var angle = ComputeAngleForValue(value, valueRange, mouseAngle);
|
|
direction = new Vector2(MathF.Sin(angle), MathF.Cos(angle));
|
|
return true;
|
|
}
|
|
|
|
|
|
private static double DeltaAngle(double angle1, double angle2)
|
|
{
|
|
angle1 = (angle1 + Math.PI) % (2 * Math.PI) - Math.PI;
|
|
angle2 = (angle2 + Math.PI) % (2 * Math.PI) - Math.PI;
|
|
|
|
var delta = angle2 - angle1;
|
|
return delta switch
|
|
{
|
|
> Math.PI => delta - 2 * Math.PI,
|
|
< -Math.PI => delta + 2 * Math.PI,
|
|
_ => delta
|
|
};
|
|
}
|
|
|
|
|
|
private static bool CalculateIntersection(Vector2 p1A, Vector2 p1B, Vector2 p2A, Vector2 p2B, out Vector2 intersection)
|
|
{
|
|
double a1 = p1B.Y - p1A.Y;
|
|
double b1 = p1A.X - p1B.X;
|
|
double c1 = a1 * p1A.X + b1 * p1A.Y;
|
|
|
|
double a2 = p2B.Y - p2A.Y;
|
|
double b2 = p2A.X - p2B.X;
|
|
double c2 = a2 * p2A.X + b2 * p2A.Y;
|
|
|
|
double determinant = a1 * b2 - a2 * b1;
|
|
|
|
if (Math.Abs(determinant) < 1e-6) // Lines are parallel
|
|
{
|
|
intersection =Vector2.Zero;
|
|
return false;
|
|
}
|
|
|
|
var x = (b2 * c1 - b1 * c2) / determinant;
|
|
var y = (a1 * c2 - a2 * c1) / determinant;
|
|
intersection = new Vector2((float)x, (float)y);
|
|
return true;
|
|
}
|
|
|
|
|
|
private static double GetKeyboardScaleFactor()
|
|
{
|
|
if (_io.KeyAlt)
|
|
return 10;
|
|
|
|
if (_io.KeyShift)
|
|
return 0.1;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/** The precise value before rounding. This used for all internal calculations. */
|
|
private static double _value;
|
|
|
|
private static float _dampedRadius;
|
|
private static Vector2 _center = Vector2.Zero;
|
|
private static float _dampedAngleVelocity;
|
|
private static double _lastValueAngle;
|
|
private static double _dampedModifierScaleFactor;
|
|
private static float _dampedDialValueAngle;
|
|
private static int _framesSinceLastMove;
|
|
private static double _originalValue;
|
|
//private static readonly List<Vector2> _mousePositions = new(100);
|
|
//private static ImDrawListPtr _drawList;
|
|
private static ImGuiIOPtr _io;
|
|
private static int _framesSinceStart;
|
|
} |