487 lines
21 KiB
C#
487 lines
21 KiB
C#
using ImGuiNET;
|
|
using T3.Core.DataTypes.Vector;
|
|
using T3.Core.Operator.Interfaces;
|
|
using T3.Core.Utils;
|
|
using T3.Editor.Gui.MagGraph.Interaction;
|
|
using T3.Editor.Gui.MagGraph.Model;
|
|
using T3.Editor.Gui.MagGraph.States;
|
|
using T3.Editor.Gui.Styling;
|
|
using T3.Editor.Gui.UiHelpers;
|
|
using T3.Editor.Gui.Windows.Layouts;
|
|
using T3.Editor.UiModel.InputsAndTypes;
|
|
using T3.Editor.UiModel.Modification;
|
|
using T3.Editor.UiModel.Selection;
|
|
|
|
namespace T3.Editor.Gui.MagGraph.Ui;
|
|
|
|
internal sealed partial class MagGraphView
|
|
{
|
|
private readonly Dictionary<int, (Vector2 source, Vector2 target)> _previousConnectionPositions = new();
|
|
|
|
public void DrawGraph(ImDrawListPtr drawList, float graphOpacity)
|
|
{
|
|
_context.GraphOpacity = graphOpacity;
|
|
|
|
IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
|
|
IsHovered = ImGui.IsWindowHovered();
|
|
|
|
var result = _context.DrawDialogs(_projectView);
|
|
|
|
if (result == ChangeSymbol.SymbolModificationResults.Nothing)
|
|
{
|
|
KeyboardActions.HandleKeyboardActions(_context);
|
|
}
|
|
|
|
if (_context.ProjectView.InstView is not { IsValid: true })
|
|
{
|
|
Log.Warning("Failed to draw graph view without valid composition instance");
|
|
}
|
|
else
|
|
{
|
|
DropHandling.HandleDropOnWindow(_context);
|
|
|
|
// Update view scope if required
|
|
if (FitViewToSelectionHandling.FitViewToSelectionRequested)
|
|
{
|
|
_context.ProjectView.FocusViewToSelection();
|
|
}
|
|
|
|
// Keep visible canvas area to cull non-visible objects later
|
|
_visibleCanvasArea = GetVisibleCanvasArea();
|
|
|
|
var editingFlags = T3Ui.EditingFlags.None;
|
|
if (T3Ui.IsAnyPopupOpen)
|
|
{
|
|
if (!ImGui.IsWindowHovered())
|
|
editingFlags |= T3Ui.EditingFlags.PreventMouseInteractions;
|
|
}
|
|
|
|
if (!_context.PreventInteraction && !FrameStats.Last.OpenedPopupCapturedMouse)
|
|
UpdateCanvas(out _, editingFlags);
|
|
|
|
// Store previous connection lines damped positions before layout recomputes
|
|
_previousConnectionPositions.Clear();
|
|
foreach (var c in _context.Layout.MagConnections)
|
|
{
|
|
if (c.DampedSourcePos != Vector2.Zero || c.DampedTargetPos != Vector2.Zero)
|
|
{
|
|
_previousConnectionPositions[c.ConnectionHash] = (c.DampedSourcePos, c.DampedTargetPos);
|
|
}
|
|
}
|
|
|
|
// Prepare UiModel for frame
|
|
_context.Layout.ComputeLayout(_context);
|
|
|
|
// Restore damped positions after layout
|
|
foreach (var c in _context.Layout.MagConnections)
|
|
{
|
|
if (_previousConnectionPositions.TryGetValue(c.ConnectionHash, out var prevPos))
|
|
{
|
|
if (c.DampedSourcePos == Vector2.Zero)
|
|
c.DampedSourcePos = prevPos.source;
|
|
if (c.DampedTargetPos == Vector2.Zero)
|
|
c.DampedTargetPos = prevPos.target;
|
|
}
|
|
else
|
|
{
|
|
// New connection - initialize to actual position
|
|
if (c.DampedSourcePos == Vector2.Zero)
|
|
c.DampedSourcePos = c.SourcePos;
|
|
if (c.DampedTargetPos == Vector2.Zero)
|
|
c.DampedTargetPos = c.TargetPos;
|
|
}
|
|
}
|
|
|
|
_context.ItemMovement.PrepareFrame(_context);
|
|
|
|
if (_context.StateMachine.CurrentState == GraphStates.Default)
|
|
{
|
|
_context.ActiveItem = null;
|
|
_context.ActiveSourceItem = null;
|
|
_context.ActiveTargetItem = null;
|
|
|
|
_context.ItemWithActiveCustomUi = null;
|
|
_context.ActiveSourceOutputId = Guid.Empty;
|
|
_context.ActiveTargetInputId = Guid.Empty;
|
|
}
|
|
|
|
if (!LayoutHandling.FocusMode)
|
|
{
|
|
DrawBackgroundGrids(drawList);
|
|
}
|
|
|
|
// Selection fence...
|
|
if (!_context.PreventInteraction)
|
|
{
|
|
HandleFenceSelection(_context, _selectionFence);
|
|
}
|
|
|
|
// Draw sections outermost-first so nested sections paint on top of their containers
|
|
foreach (var a in _context.Layout.SectionsInDrawOrder)
|
|
{
|
|
if (a.Section.IsHiddenInCollapsedSection)
|
|
continue;
|
|
|
|
DrawSection(a, drawList, _context);
|
|
}
|
|
|
|
// Draw items
|
|
foreach (var item in _context.Layout.Items.Values)
|
|
{
|
|
DrawNode(item, drawList, _context);
|
|
|
|
InvalidateSelectedGizmoProviders(item);
|
|
}
|
|
|
|
Fonts.FontSmall.Scale = 1; // WTF. Some of the drawNode seems to spill out fontSize
|
|
|
|
// Update active or hovered item
|
|
// Doing this after rendering will add slight frame delay but will
|
|
// keep drag operations more consistent.
|
|
if (_context.ActiveItem != null)
|
|
{
|
|
if (_context.ActiveItem.Id != _lastHoverId)
|
|
{
|
|
_hoverStartTime = ImGui.GetTime();
|
|
_lastHoverId = _context.ActiveItem.Id;
|
|
}
|
|
|
|
FrameStats.AddHoveredId(_context.ActiveItem.Id);
|
|
}
|
|
else
|
|
{
|
|
_hoverStartTime = ImGui.GetTime();
|
|
_lastHoverId = Guid.Empty;
|
|
}
|
|
|
|
if (!_context.PreventInteraction)
|
|
HighlightSplitInsertionPoints(drawList, _context);
|
|
|
|
// Draw connections
|
|
foreach (var connection in _context.Layout.MagConnections)
|
|
{
|
|
DrawConnection(connection, drawList, _context);
|
|
}
|
|
|
|
DrawOffscreenIndicators(drawList);
|
|
|
|
// Draw special overlays
|
|
if (_context.StateMachine.CurrentState == GraphStates.RenameChild)
|
|
{
|
|
RenamingOperator.Draw(_projectView);
|
|
if (!RenamingOperator.IsOpen)
|
|
{
|
|
_context.StateMachine.SetState(GraphStates.Default, _context);
|
|
}
|
|
}
|
|
else if (_context.StateMachine.CurrentState == GraphStates.RenameSection)
|
|
{
|
|
SectionRenaming.Draw(_context);
|
|
}
|
|
else if (_context.StateMachine.CurrentState == GraphStates.DragSection)
|
|
{
|
|
SectionDragging.Draw(_context);
|
|
}
|
|
|
|
// Draw temp connections
|
|
foreach (var tc in _context.TempConnections)
|
|
{
|
|
// Placeholder snapped above its target: draw the vertical snap anchor triangle instead of a spline
|
|
if (tc.Style == MagGraphConnection.ConnectionStyles.MainOutToMainInSnappedVertical
|
|
&& tc.SourceItem != null
|
|
&& tc.TargetItem != null)
|
|
{
|
|
var anchorPosOnScreen = TransformPosition(new Vector2(tc.SourceItem.Area.Min.X + MagGraphItem.GridSize.X / 2,
|
|
tc.TargetItem.Area.Min.Y));
|
|
var anchorColor = TypeUiRegistry.GetPropertiesForType(tc.Type).Color.Fade(0.6f);
|
|
drawList.AddTriangleFilled(anchorPosOnScreen + new Vector2(-3, -2) * CanvasScale * 2,
|
|
anchorPosOnScreen + new Vector2(3, -2) * CanvasScale * 2,
|
|
anchorPosOnScreen + new Vector2(0, 2) * CanvasScale * 2,
|
|
anchorColor);
|
|
continue;
|
|
}
|
|
|
|
var mousePos = ImGui.GetMousePos();
|
|
|
|
var sourcePosOnScreen = mousePos;
|
|
var targetPosOnScreen = mousePos;
|
|
|
|
// Dragging end to new target input...
|
|
if (tc.SourceItem != null)
|
|
{
|
|
var sourcePos = new Vector2(tc.SourceItem.Area.Max.X,
|
|
tc.SourceItem.Area.Min.Y + MagGraphItem.GridSize.Y * (0.5f + tc.OutputLineIndex));
|
|
if (tc.Style is MagGraphConnection.ConnectionStyles.BottomToTop or MagGraphConnection.ConnectionStyles.BottomToLeft)
|
|
{
|
|
sourcePos = new Vector2(sourcePos.X - (tc.SourceItem.Area.GetWidth() / 2.0f), tc.SourceItem.Area.Max.Y);
|
|
}
|
|
|
|
sourcePosOnScreen = TransformPosition(sourcePos);
|
|
|
|
if (_context.StateMachine.CurrentState == GraphStates.DragConnectionEnd
|
|
&& InputSnapper.BestInputMatch.Item != null)
|
|
{
|
|
targetPosOnScreen = InputSnapper.BestInputMatch.PosOnScreen;
|
|
}
|
|
}
|
|
|
|
// Dragging beginning to new source output...
|
|
if (tc.TargetItem != null)
|
|
{
|
|
var targetPos = new Vector2(tc.TargetItem.Area.Min.X,
|
|
tc.TargetItem.Area.Min.Y + MagGraphItem.GridSize.Y * (0.5f + tc.InputLineIndex));
|
|
targetPosOnScreen = TransformPosition(targetPos);
|
|
|
|
if (_context.StateMachine.CurrentState == GraphStates.DragConnectionBeginning
|
|
&& OutputSnapper.BestOutputMatch.Item != null)
|
|
{
|
|
sourcePosOnScreen = TransformPosition(OutputSnapper.BestOutputMatch.Anchor.PositionOnCanvas);
|
|
}
|
|
else if (tc.SourceItem == null
|
|
&& _context.StateMachine.CurrentState == GraphStates.Placeholder
|
|
&& _context.Placeholder.PlaceholderItem != null)
|
|
{
|
|
sourcePosOnScreen = TransformPosition(_context.Placeholder.PlaceholderItem.PosOnCanvas
|
|
+ new Vector2(MagGraphItem.GridSize.X, MagGraphItem.GridSize.Y * 0.5f));
|
|
}
|
|
|
|
var isDisconnectedMultiInput = tc.InputLineIndex >= tc.TargetItem.InputLines.Length;
|
|
if (isDisconnectedMultiInput)
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
if (_context.StateMachine.CurrentState == GraphStates.Placeholder)
|
|
{
|
|
if (_context.Placeholder.PlaceholderItem != null)
|
|
{
|
|
targetPosOnScreen = TransformPosition(_context.Placeholder.PlaceholderItem.PosOnCanvas);
|
|
}
|
|
}
|
|
}
|
|
|
|
var typeColor = TypeUiRegistry.GetPropertiesForType(tc.Type).Color;
|
|
var d = Vector2.Distance(sourcePosOnScreen, targetPosOnScreen) / 2;
|
|
|
|
if (tc.Style is MagGraphConnection.ConnectionStyles.BottomToTop or MagGraphConnection.ConnectionStyles.BottomToLeft)
|
|
{
|
|
drawList.AddBezierCubic(sourcePosOnScreen,
|
|
sourcePosOnScreen,
|
|
targetPosOnScreen - new Vector2(d, 0),
|
|
targetPosOnScreen,
|
|
typeColor.Fade(0.6f),
|
|
2);
|
|
}
|
|
else
|
|
{
|
|
drawList.AddBezierCubic(sourcePosOnScreen,
|
|
sourcePosOnScreen + new Vector2(d, 0),
|
|
targetPosOnScreen - new Vector2(d, 0),
|
|
targetPosOnScreen,
|
|
typeColor.Fade(0.6f),
|
|
2);
|
|
}
|
|
}
|
|
|
|
OutputSnapper.Update(_context);
|
|
InputSnapper.Update(_context);
|
|
|
|
_context.ConnectionHovering.PrepareNewFrame(_context);
|
|
_context.Placeholder.Update(_context);
|
|
|
|
// Draw animated Snap indicator
|
|
{
|
|
var timeSinceSnap = ImGui.GetTime() - _context.ItemMovement.LastSnapTime;
|
|
var progress = ((float)timeSinceSnap).RemapAndClamp(0, 0.4f, 1, 0);
|
|
if (progress < 1)
|
|
{
|
|
drawList.AddCircle(TransformPosition(_context.ItemMovement.LastSnapTargetPositionOnCanvas),
|
|
progress * 50,
|
|
UiColors.ForegroundFull.Fade(progress * 0.2f));
|
|
}
|
|
}
|
|
|
|
if (FrameStats.Current.OpenedPopUpName == string.Empty)
|
|
CustomComponents.DrawContextMenuForScrollCanvas(() => GraphContextMenu.DrawContextMenuContent(_context, _projectView), ref _contextMenuIsOpen);
|
|
|
|
SmoothItemPositions();
|
|
|
|
_context.StateMachine.UpdateAfterDraw(_context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Transform gizmos of cached operators like [Point] might not be visible in the output window.
|
|
/// This method force-invalidates them, if selected.
|
|
/// </summary>
|
|
private void InvalidateSelectedGizmoProviders(MagGraphItem item)
|
|
{
|
|
if (item.Variant == MagGraphItem.Variants.Operator
|
|
&& item.Instance is ITransformable && _context.Selector.IsSelected(item)
|
|
&& item.Instance.Inputs.Count > 0)
|
|
{
|
|
item.Instance.Inputs[0].DirtyFlag.ForceInvalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This a very simple proof-of-concept implementation to test its fidelity.
|
|
/// A simple optimization could be to only do this for some time after a drag manipulation and then apply
|
|
/// the correct position.
|
|
///
|
|
/// It still helps to understand what's going on and feels satisfying. So we're keeping it for now.
|
|
/// </summary>
|
|
|
|
private void SmoothItemPositions()
|
|
{
|
|
const float dampAmount = 0.33f;
|
|
|
|
foreach (var i in _context.Layout.Items.Values)
|
|
{
|
|
i.DampedPosOnCanvas = Vector2.Lerp(i.PosOnCanvas, i.DampedPosOnCanvas, dampAmount);
|
|
}
|
|
|
|
foreach (var a in _context.Layout.Sections.Values)
|
|
{
|
|
a.DampedPosOnCanvas = Vector2.Lerp(a.Section.PosOnCanvas, a.DampedPosOnCanvas, dampAmount);
|
|
a.DampedSize = Vector2.Lerp(a.Section.Size, a.DampedSize, dampAmount);
|
|
}
|
|
|
|
foreach (var c in _context.Layout.MagConnections)
|
|
{
|
|
// Initialize damped positions if they're at zero (new connection or reset)
|
|
if (c.DampedSourcePos == Vector2.Zero)
|
|
c.DampedSourcePos = c.SourcePos;
|
|
|
|
if (c.DampedTargetPos == Vector2.Zero)
|
|
c.DampedTargetPos = c.TargetPos;
|
|
|
|
c.DampedSourcePos = Vector2.Lerp(c.SourcePos, c.DampedSourcePos, dampAmount);
|
|
c.DampedTargetPos = Vector2.Lerp(c.TargetPos, c.DampedTargetPos, dampAmount);
|
|
|
|
}
|
|
}
|
|
|
|
private bool _contextMenuIsOpen;
|
|
|
|
private void HighlightSplitInsertionPoints(ImDrawListPtr drawList, GraphUiContext context)
|
|
{
|
|
foreach (var sp in context.ItemMovement.SpliceSets)
|
|
{
|
|
var inputItem = context.ItemMovement.DraggedItems.FirstOrDefault(i => i.Id == sp.InputItemId);
|
|
if (inputItem == null)
|
|
continue;
|
|
|
|
var center = TransformPosition(inputItem.PosOnCanvas + sp.AnchorOffset);
|
|
|
|
var type = inputItem.InputLines.Length > 0 ? inputItem.InputLines[0].Type : null;
|
|
var typeColor = TypeUiRegistry.GetPropertiesForType(type).Color;
|
|
|
|
if (sp.Direction == MagGraphItem.Directions.Vertical)
|
|
{
|
|
var offset = MagGraphItem.GridSize.X / 16 * CanvasScale;
|
|
// var offset = sp.Direction == MagGraphItem.Directions.Vertical
|
|
// ? new Vector2(MagGraphItem.GridSize.X / 16 * CanvasScale, 0)
|
|
// : new Vector2(0, MagGraphItem.GridSize.Y / 8 * CanvasScale);
|
|
|
|
drawList.AddRectFilled(center + new Vector2(-offset, -1),
|
|
center + new Vector2(offset, 1),
|
|
ColorVariations.ConnectionLines.Apply(typeColor).Fade(Blink)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
// center.X += sp.DragPositionWithinBlock.X * CanvasScale;
|
|
// var offset = MagGraphItem.GridSize.Y * 0.25f * CanvasScale;
|
|
// drawList.AddRectFilled(center + new Vector2(0, -offset),
|
|
// center + new Vector2(2, offset),
|
|
// ColorVariations.ConnectionLines.Apply(typeColor).Fade(Blink)
|
|
// );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawBackgroundGrids(ImDrawListPtr drawList)
|
|
{
|
|
var minSize = MathF.Min(MagGraphItem.GridSize.X, MagGraphItem.GridSize.Y);
|
|
var gridSize = Vector2.One * minSize;
|
|
var maxOpacity = 0.25f;
|
|
|
|
var fineGrid = Scale.X.RemapAndClamp(0.5f, 2f, 0.0f, maxOpacity);
|
|
if (fineGrid > 0.01f)
|
|
{
|
|
var color = UiColors.CanvasGrid.Fade(fineGrid);
|
|
DrawBackgroundGrid(drawList, gridSize, color);
|
|
}
|
|
|
|
var roughGrid = Scale.X.RemapAndClamp(0.1f, 2f, 0.0f, maxOpacity);
|
|
if (roughGrid > 0.01f)
|
|
{
|
|
var color = UiColors.CanvasGrid.Fade(roughGrid);
|
|
DrawBackgroundGrid(drawList, gridSize * 5, color);
|
|
}
|
|
}
|
|
|
|
private void DrawBackgroundGrid(ImDrawListPtr drawList, Vector2 gridSize, Color color)
|
|
{
|
|
var window = new ImRect(WindowPos, WindowPos + WindowSize);
|
|
|
|
var topLeftOnCanvas = InverseTransformPositionFloat(WindowPos);
|
|
var alignedTopLeftCanvas = new Vector2((int)(topLeftOnCanvas.X / gridSize.X) * gridSize.X,
|
|
(int)(topLeftOnCanvas.Y / gridSize.Y) * gridSize.Y);
|
|
|
|
var topLeftOnScreen = TransformPosition(alignedTopLeftCanvas);
|
|
var screenGridSize = TransformDirection(gridSize);
|
|
|
|
var count = new Vector2(window.GetWidth() / screenGridSize.X, window.GetHeight() / screenGridSize.Y);
|
|
|
|
for (int ix = 0; ix < 200 && ix <= count.X + 1; ix++)
|
|
{
|
|
var x = (int)(topLeftOnScreen.X + ix * screenGridSize.X);
|
|
drawList.AddRectFilled(new Vector2(x, window.Min.Y),
|
|
new Vector2(x + 1, window.Max.Y),
|
|
color);
|
|
}
|
|
|
|
for (int iy = 0; iy < 200 && iy <= count.Y + 1; iy++)
|
|
{
|
|
var y = (int)(topLeftOnScreen.Y + iy * screenGridSize.Y);
|
|
drawList.AddRectFilled(new Vector2(window.Min.X, y),
|
|
new Vector2(window.Max.X, y + 1),
|
|
color);
|
|
}
|
|
}
|
|
|
|
[Flags]
|
|
private enum Borders
|
|
{
|
|
None = 0,
|
|
Up = 1,
|
|
Right = 2,
|
|
Down = 4,
|
|
Left = 8,
|
|
}
|
|
|
|
private static readonly ImDrawFlags[] _borderRoundings =
|
|
{
|
|
ImDrawFlags.RoundCornersAll, // 0000
|
|
ImDrawFlags.RoundCornersBottom, // 0001 up
|
|
ImDrawFlags.RoundCornersLeft, // 0010 right
|
|
ImDrawFlags.RoundCornersBottomLeft, // 0011 right up
|
|
ImDrawFlags.RoundCornersTop, // 0100 down
|
|
ImDrawFlags.RoundCornersNone, // 0101 down up
|
|
ImDrawFlags.RoundCornersTopLeft, // 0110 down right
|
|
ImDrawFlags.RoundCornersNone, // 0111 down right up
|
|
|
|
ImDrawFlags.RoundCornersRight, // 1000 left
|
|
ImDrawFlags.RoundCornersBottomRight, //1001 left up
|
|
ImDrawFlags.RoundCornersNone, // 1010 left right
|
|
ImDrawFlags.RoundCornersNone, // 1011 left right up
|
|
ImDrawFlags.RoundCornersTopRight, // 1100 left down
|
|
ImDrawFlags.RoundCornersNone, // 1101 left down up
|
|
ImDrawFlags.RoundCornersNone, // 1110 left down right
|
|
ImDrawFlags.RoundCornersNone, // 1111 left down right up
|
|
};
|
|
|
|
internal static float Blink => MathF.Sin((float)ImGui.GetTime() * 10) * 0.5f + 0.5f;
|
|
} |