项目文件夹

文件
2026-07-13 13:13:17 +08:00

310 行
11 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
/// </summary>
internal static class JogDialOverlay
{
internal static bool Draw(ref double value, bool restarted, Vector2 center, double min = double.NegativeInfinity,
double max = double.PositiveInfinity,
float scale = 0.1f,
bool clampMin = false,
bool clampMax= false)
{
var modified = false;
_drawList = ImGui.GetForegroundDrawList();
_io = ImGui.GetIO();
_center = center;
if (restarted)
{
_baseLog10Speed = (int)(Math.Log10(scale)+3.5f);
_originalValue = value;
_unclampedValue = value;
_min = min;
_max = max;
_clampMin = clampMin;
_clampMax = clampMax;
_state = States.WaitingInNeutral;
}
for (int ringIndex = 0; ringIndex < RingCount; ringIndex++)
{
modified |= DialRing.Draw(ringIndex);
}
if (modified)
{
value = ClampedValue;
}
return modified;
}
private const int RingCount = 2;
private static double _unclampedValue;
private static double ClampedValue => MathUtils.OptionalClamp(_unclampedValue,_min, _clampMin, _max, _clampMax);
private static float _baseLog10Speed = 1;
private static double _originalValue;
private static float AdjustedBaseLog10Scale
{
get
{
if (_io.KeyAlt)
{
return _baseLog10Speed+1;
}
if (_io.KeyShift)
{
return _baseLog10Speed-1;
}
return _baseLog10Speed;
}
}
private static double _min;
private static double _max;
private static bool _clampMin;
private static bool _clampMax;
private const float NeutralRadius = 15;
private static ImDrawListPtr _drawList;
private static ImGuiIOPtr _io;
private static Vector2 _center;
private static float DistanceToCenter => Vector2.Distance(_io.MousePos, _center);
private static readonly Color _segmentColor = new(0.2f, 0.2f, 0.2f, 0.35f);
private static readonly Color _activeSegmentColor = new(1f, 1f, 1f, 0.5f);
private enum States
{
Hidden,
WaitingInNeutral,
Manipulating,
}
private static States _state = States.Hidden;
private static class DialRing
{
internal static bool Draw(int ringIndex)
{
_ringIndex = ringIndex;
_drawList.AddCircle(_center,
radius: Radius + RingWidth / 2,
_segmentColor,
num_segments: 64,
thickness: RingWidth - Padding);
DrawTicks();
DrawValueLabels();
var valueTickColor = new Color(1, 1, 1, IsActive ? 1 : 0.4f);
DrawTick(ClampedValue / Pow, valueTickColor, lineThickness: IsActive ? 2 : 1);
DrawValueDifference();
return HandleInteraction();
}
private static void DrawTicks()
{
for (int lineIndex = 0; lineIndex < 100; lineIndex++)
{
var f = lineIndex * 0.01f;
var alpha = (lineIndex % 10) == 0
? 0.3f
: 0.1f;
alpha *= IsActive ? 1 : 0.4f;
var color = new Color(0, 0, 0, alpha);
var lineThickness = lineIndex == 0 ? 4 : 1;
var dialValue = ComputeDialValue(_unclampedValue, f);
if ( (!_clampMin || dialValue >= _min) && (!_clampMax || dialValue <= _max) )
{
DrawTick(f, color, lineThickness);
}
}
}
private static float GetDialRatio(Vector2 mousePos)
{
var p = mousePos - _center;
return (float)Math.Round(MathUtils.Fmod(-Math.Atan2(p.X, p.Y) / (Math.PI * 2), 1) * 100) / 100;
}
private static bool HandleInteraction()
{
if (!IsActive)
return false;
var pNow = _io.MousePos;
var dialRatio = GetDialRatio(pNow);
if (_state == States.WaitingInNeutral)
{
_state = States.Manipulating;
if (dialRatio > 0.75f && _originalValue >= 0)
{
var ratioForOriginalValue = _originalValue / Pow;
if (ratioForOriginalValue < 0.5f)
{
_unclampedValue = ComputeDialValue(_originalValue, dialRatio - 1);
return true;
}
}
}
var oldValue = _unclampedValue;
var dialedValue = ComputeDialValue(_unclampedValue, dialRatio);
var oldValueRatio = oldValue / Pow;
var newValueRatio = dialedValue / Pow;
var valueRatioDelta = oldValueRatio - newValueRatio;
if (valueRatioDelta > 0.75f)
{
dialedValue += Pow;
}
else if (valueRatioDelta < -0.75f)
{
dialedValue -= Pow;
}
_unclampedValue = dialedValue;
//Log.Debug($"F {lastDialRatio:0.00} -> {dialRatio:0.00} v:{oldValue:G5}->{_unclampedValue:G5} ratioD: {valueRatioDelta:G4} dialDelta:{delta:G4} offset:{offset:G4} log:{Log10Scale}");
return true;
}
private static void DrawTick(double f, Color color, int lineThickness)
{
var rads = -3.141592 * 2 * (f);
var d = new Vector2((float)Math.Sin(rads), (float)Math.Cos(rads));
_drawList.AddLine(_center + (Radius) * d, _center + (Radius + RingWidth) * d, color, lineThickness);
}
private static void DrawValueDifference()
{
var rads1 = -3.141592 * 2 * (_originalValue + 0.5f - 0.25f);
var rads2 = -3.141592 * 2 * (_unclampedValue + 0.5f - 0.25f);
var clamped = (rads1 - rads2).Clamp(-3.1415f * 2, 3.1415f * 2);
rads2 = rads1 - clamped;
_drawList.PathArcTo(
_center,
radius: Radius + RingWidth * 0.01f,
a_min: -(float)rads1,
a_max: -(float)rads2,
num_segments: 64);
_drawList.PathStroke(_activeSegmentColor, ImDrawFlags.None, 1);
}
private static void DrawValueLabels()
{
var shouldDraw = IsActive || _state == States.WaitingInNeutral && _ringIndex == 0;
if (!shouldDraw)
return;
var d = Vector2.UnitY;
var dialRatio = _state == States.WaitingInNeutral
?0.5f
:GetDialRatio(_io.MousePos);
var lowerPart = MathUtils.Fmod(_unclampedValue, Pow);
var roundedUpperPart = _unclampedValue - lowerPart;
ImGui.PushFont(Fonts.FontLarge);
const float padding = 10;
var isOnLeft = dialRatio <= 0.5f;
const float transitionRange = 0.06f;
var transition = isOnLeft
? MathUtils.RemapAndClamp(dialRatio, 0, transitionRange, 1, 0).Clamp(0, 1)
: MathUtils.RemapAndClamp(dialRatio, 1 - transitionRange, 1, 0, 1).Clamp(0, 1);
{
var smallerLabel = $"{roundedUpperPart:G5}";
var size = ImGui.CalcTextSize(smallerLabel);
var color = isOnLeft
? new Color(1, 1, 1, 1f)
: new Color(1, 1, 1, 1-transition);
var x = isOnLeft
? MathUtils.Lerp(-padding - size.X, -size.X / 2, transition)
: MathUtils.Lerp(-padding - size.X, -padding - size.X -padding, transition);
_drawList.AddText(_center + (Radius + RingWidth / 2) * d + new Vector2(x, 0),
color,
smallerLabel);
}
// Right / Upper
{
var upperLabel = $"{(roundedUpperPart + Pow):G5}";
var size = ImGui.CalcTextSize(upperLabel);
var color = isOnLeft
? new Color(1, 1, 1, 1-transition)
: new Color(1, 1, 1, 1f);
var x = isOnLeft
? MathUtils.Lerp(padding , padding + size.X, transition)
: MathUtils.Lerp(padding, -size.X / 2, transition);
_drawList.AddText(_center + (Radius + RingWidth / 2) * d + new Vector2(x, 0),
color,
upperLabel);
}
ImGui.PopFont();
}
private static double ComputeDialValue(double value, double dialRatio)
{
var pow = Math.Pow(10, Log10Scale - 1);
var lowerPart = MathUtils.Fmod(value, pow);
var roundedUpperPart = value - lowerPart;
var newLowerPart = pow * dialRatio;
var dialedValue = roundedUpperPart + newLowerPart;
return dialedValue;
}
private static bool IsOuterSegment => _ringIndex == RingCount - 1;
private static float Pow => (float)Math.Pow(10, Log10Scale - 1);
private static int _ringIndex;
private static float Log10Scale => AdjustedBaseLog10Scale + _ringIndex;
private static float Radius => NeutralRadius + _ringIndex * RingWidth;
private static bool IsActive =>
(DistanceToCenter > Radius && DistanceToCenter < Radius + RingWidth) ||
(IsOuterSegment && DistanceToCenter > Radius + RingWidth);
private const float RingWidth = 100;
private const float Padding = 3;
}
// private static float AnimateTowards(ref float value, float target, float speed = 1)
// {
// var direction = target - value;
// if (!(Math.Abs(direction) > 0.00001))
// return value;
//
// var offset = speed * ImGui.GetIO().DeltaTime;
// value = direction > 0
// ? Math.Min(target, value + offset)
// : Math.Max(target, value - offset);
// return value;
// }
}