Files
tooll3--t3/Editor/Gui/UiHelpers/VerticalConnectionDrawer.cs
2026-07-13 13:13:17 +08:00

280 lines
9.6 KiB
C#

using ImGuiNET;
using T3.Core.DataTypes.Vector;
using T3.Core.Utils;
using T3.Editor.Gui.Styling;
namespace T3.Editor.Gui.UiHelpers;
/// <summary>
/// Draws vertical-first connections by rotating the proven horizontal algorithm by +90°.
/// Assumes pins start from top/bottom center of their nodes.
/// </summary>
internal static class VerticalConnectionDrawer
{
private const float Pi = (float)System.Math.PI;
internal static bool DrawConnection(float canvasScale,
ImRect sourceNode, Vector2 sourcePos,
ImRect targetNode, Vector2 targetPos,
Color color, float thickness,
out Vector2 hoverPosition, out float normalizedHoverPos)
{
var s = canvasScale.Clamp(0.2f, 2f);
hoverPosition = Vector2.Zero;
normalizedHoverPos = -1;
sourcePos += Vector2.One * 0.5f;
targetPos += Vector2.One * 0.5f;
var debArcOffset = MathF.Sin((float)ImGui.GetTime() * 5) * 0.5f;
var debRadiusOffset = MathF.Sin((float)ImGui.GetTime() * 5) * 3f;
var vis = sourceNode; vis.Add(targetNode); vis.Add(sourcePos); vis.Add(targetPos);
if (!ImGui.IsRectVisible(vis.Min, vis.Max))
return false;
var dl = ImGui.GetWindowDrawList();
dl.PathClear();
var dx = targetPos.X - sourcePos.X;
var dy = targetPos.Y - sourcePos.Y;
// trivial straight vertical
if (dy > 0 && MathF.Abs(dx) < 2)
{
dl.PathLineTo(sourcePos);
dl.PathLineTo(targetPos);
return FinalizeStroke(dl, s, color, thickness,
out hoverPosition, out normalizedHoverPos);
}
var fallbackRectSize = new Vector2(120, 50) * s;
if (sourceNode.GetWidth() < 1)
{
var rectAMin = new Vector2(sourcePos.X - fallbackRectSize.X / 2, sourcePos.Y - fallbackRectSize.Y);
sourceNode = new ImRect(rectAMin, rectAMin + fallbackRectSize);
}
if (targetNode.GetWidth() < 1)
{
var rectBMin = new Vector2(targetPos.X - fallbackRectSize.X / 2, targetPos.Y);
targetNode = new ImRect(rectBMin, rectBMin + fallbackRectSize);
}
var sourceLeftOfTarget = sourcePos.X < targetPos.X;
// radius = horizontal clearance
var scR = sourceLeftOfTarget ? sourceNode.Max.X - sourcePos.X : sourcePos.X - sourceNode.Min.X;
var tcR = sourceLeftOfTarget ? targetPos.X - targetNode.Min.X + 10 : targetNode.Max.X - targetPos.X;
const float verticalCompress = 0.2f;
var minRadius = (dx > 0 ? 5 : 3) * s;
scR = scR * verticalCompress + minRadius;
tcR = tcR * verticalCompress + minRadius;
var possibleSourceRadius = dy - tcR;
var clampedSourceRadius = MathF.Min(possibleSourceRadius, UserSettings.Config.MaxCurveRadius * s);
scR = MathF.Max(scR, clampedSourceRadius);
var d = new Vector2(dx, dy).Length();
scR = MathF.Min(scR, d / 4f);
tcR = MathF.Min(tcR, d / 4f);
var normalRadius = tcR;
var tightRadius = MathF.Min(tcR, MathF.Abs(dx) * 0.1f);
tcR = dy.RemapAndClamp(-400 * s, 20 * s, tightRadius, normalRadius);
var sumR = scR + tcR;
var scY = sourcePos.Y + dy - tcR - scR;
if (dy < sumR)
scY = sourcePos.Y;
var tcY = targetPos.Y;
float scX, tcX;
if (sourceLeftOfTarget)
{
scX = sourcePos.X + scR;
tcX = targetPos.X - tcR;
}
else
{
scX = sourcePos.X - scR;
tcX = targetPos.X + tcR;
}
var sc = new Vector2(scX, scY);
var tc = new Vector2(tcX, tcY);
float startAngleSc, endAngleSc;
float startAngleTc, endAngleTc;
if (scY > sourcePos.Y)
{
dl.PathLineTo(sourcePos);
}
if (dy >= sumR && MathF.Abs(dx) > sumR)
{
if (sourceLeftOfTarget)
{
startAngleSc = 1f * Pi;
endAngleSc = 0.5f * Pi;
startAngleTc = -0.5f * Pi;
endAngleTc = 0 * Pi;
}
else
{
startAngleSc = 0;
endAngleSc = 0.5f * Pi;
startAngleTc = 1.5f * Pi;
endAngleTc = 1f * Pi;
}
}
else
{
var distanceBetweenCenters = Vector2.Distance(sc, tc);
if (distanceBetweenCenters < Math.Abs(scR - tcR))
{
dl.PathLineTo(targetPos);
return FinalizeStroke(dl, s, color, thickness,
out hoverPosition, out normalizedHoverPos);
}
if (MathF.Abs(dx) < sumR)
{
var flipped = scX < tcX;
if (dy < 0) flipped = !flipped;
var angleAdjustment = ComputeInnerTangentAngle(sc, scR - 1, tc, tcR, flipped);
if (sourceLeftOfTarget)
{
startAngleSc = Pi;
endAngleSc = -0f * Pi + angleAdjustment;
startAngleTc = -1f * Pi + angleAdjustment;
endAngleTc = 0f * Pi;
}
else
{
startAngleSc = 0;
endAngleSc = 0f * Pi + angleAdjustment;
startAngleTc = 1f * Pi + angleAdjustment;
endAngleTc = 1f * Pi;
}
}
else
{
var flipped = scY - scR <= tcY + tcR;
if (dx < 0 && scY > tcY + tcR + scR)
flipped = !flipped;
var angleAdjustment = ComputeInnerTangentAngle(sc, scR, tc, tcR, flipped);
if (sourceLeftOfTarget)
{
startAngleSc = 1f * Pi;
endAngleSc = 0.5f * Pi + angleAdjustment;
startAngleTc = 1.5f * Pi + angleAdjustment;
endAngleTc = 2f * Pi;
}
else
{
if (flipped)
{
// scR += debRadiusOffset;
// tcR += debRadiusOffset;
startAngleSc = 0f* Pi;
endAngleSc = 0f * Pi + angleAdjustment;
startAngleTc = 1f * Pi + angleAdjustment;
endAngleTc = 1f * Pi;
}
else
{
startAngleSc = 0f* Pi;
endAngleSc = 1f * Pi + angleAdjustment;
startAngleTc = 2f * Pi + angleAdjustment;
endAngleTc = 1f * Pi;
}
}
}
}
var segS = SegmentCount(MathF.Abs(startAngleSc - endAngleSc), s);
dl.PathArcTo(sc, scR, startAngleSc, endAngleSc, segS);
var segT = SegmentCount(MathF.Abs(startAngleTc - endAngleTc), s);
dl.PathArcTo(tc, tcR, startAngleTc, endAngleTc, segT);
//dl.PathLineTo(targetPos);
return FinalizeStroke(dl, s, color, thickness,
out hoverPosition, out normalizedHoverPos);
}
private static bool FinalizeStroke(ImDrawListPtr dl, float s, Color color, float thickness,
out Vector2 hoverPosition, out float normalizedHoverPos)
{
var hovering = LegacyConnectionDrawer.TestHoverDrawListPath(ref dl, out hoverPosition, out normalizedHoverPos);
if (s > 0.5f)
{
dl.AddPolyline(ref dl._Path[0], dl._Path.Size,
UiColors.WindowBackground.Fade(0.6f * color.A),
ImDrawFlags.None, thickness + 5f);
}
dl.PathStroke(color, ImDrawFlags.None, thickness);
return hovering;
}
private static int SegmentCount(float arcLenRad, float scale)
{
var circleResolution = (int)scale.RemapAndClamp(0.2f, 1.5f, 6, 15);
return (int)(arcLenRad * circleResolution).Clamp(1, UserSettings.Config.MaxSegmentCount);
}
private static float ComputeInnerTangentAngle(Vector2 centerA, float radiusA,
Vector2 centerB, float radiusB,
bool flipped = false)
{
var dx = centerB.X - centerA.X;
var dy = centerB.Y - centerA.Y;
var d = MathF.Sqrt(dx * dx + dy * dy);
if (d <= MathF.Abs(radiusA - radiusB))
{
return 0;
}
var thetaBase = MathF.Atan2(dy, dx);
var thetaOffset = MathF.Acos((radiusA + radiusB) / d);
var a1 = Normalize(thetaBase + thetaOffset);
var a2 = Normalize(thetaBase - thetaOffset);
return flipped ? (a2 < 0 ? a1 : a2)
: (a2 > 0 ? a1 : a2);
}
private static float Normalize(float a)
{
while (a <= -Pi) a += 2 * Pi;
while (a > Pi) a -= 2 * Pi;
return a;
}
}