#nullable enable using ImGuiNET; using T3.Core.DataTypes.Vector; using T3.Core.Operator; using T3.Editor.Gui.MagGraph.Model; using T3.Editor.Gui.MagGraph.States; using T3.Editor.UiModel.Commands.Graph; using T3.Editor.UiModel.InputsAndTypes; using T3.Editor.UiModel.ProjectHandling; using Vector2 = System.Numerics.Vector2; namespace T3.Editor.Gui.MagGraph.Interaction; /// /// Handles snapping to input and output connections /// internal static class InputSnapper { public static void Update(GraphUiContext context) { BestInputMatch = _bestInputMatchForCurrentFrame; _bestInputMatchForCurrentFrame = new InputMatch(); if (context.StateMachine.CurrentState != GraphStates.HoldingConnectionEnd) return; if (BestInputMatch.Item != null) { // TODO: Make beautiful ImGui.GetWindowDrawList().AddCircle(context.View.TransformPosition(BestInputMatch.PosOnScreen), 20, Color.Red); } } public static void RegisterAsPotentialTargetInput(MagGraphItem item, Vector2 posOnScreen, Guid slotId, InputSnapTypes inputSnapType = InputSnapTypes.Normal, int multiInputIndex = 0) { var mousePos = ImGui.GetMousePos(); var distance = Vector2.Distance(posOnScreen, mousePos); if ( distance < _bestInputMatchForCurrentFrame.Distance) { _bestInputMatchForCurrentFrame = new InputMatch(item, slotId, posOnScreen, inputSnapType, multiInputIndex, distance); } } public static bool TryToReconnect(GraphUiContext context) { if (BestInputMatch.Item == null) return false; if (context.TempConnections.Count == 0) return false; if (context.MacroCommand == null) { context.StartMacroCommand("Create connection"); } // Only multi-inputs can take more than one dragged connection... var targetIsMultiInput = false; foreach (var line in BestInputMatch.Item.InputLines) { if (line.Id != BestInputMatch.SlotId) continue; targetIsMultiInput = line.InputUi?.InputDefinition.IsMultiInput ?? false; break; } var connectionCount = targetIsMultiInput ? context.TempConnections.Count : 1; var targetParentOrChildId = BestInputMatch.Item.Variant == MagGraphItem.Variants.Output ? Guid.Empty : BestInputMatch.Item.Id; var existingConnection = context.CompositionInstance.Symbol.Connections.FirstOrDefault(c => c.TargetParentOrChildId == targetParentOrChildId && c.TargetSlotId == BestInputMatch.SlotId); if (existingConnection != null && (BestInputMatch.InputSnapType == InputSnapTypes.Normal || BestInputMatch.InputSnapType == InputSnapTypes.ReplaceMultiInput)) { context.MacroCommand?.AddAndExecCommand(new DeleteConnectionCommand(context.CompositionInstance.Symbol, existingConnection, BestInputMatch.MultiInputIndex)); } var multiInputIndex = BestInputMatch.MultiInputIndex; var adjustedMultiInputIndex = multiInputIndex + (BestInputMatch.InputSnapType == InputSnapTypes.InsertAfterMultiInput ? 1 : 0); var addedCount = 0; for (var index = 0; index < connectionCount; index++) { var tempConnection = context.TempConnections[index]; var sourceParentOrChildId = tempConnection.SourceItem.Variant == MagGraphItem.Variants.Input ? Guid.Empty : tempConnection.SourceItem.Id; var connectionToAdd = new Symbol.Connection(sourceParentOrChildId, tempConnection.SourceOutput.Id, targetParentOrChildId, BestInputMatch.SlotId); if (Structure.CheckForCycle(context.CompositionInstance.Symbol, connectionToAdd)) { Log.Debug("This action is not allowed. This connection would create a cycle."); continue; } context.MacroCommand!.AddAndExecCommand(new AddConnectionCommand(context.CompositionInstance.Symbol, connectionToAdd, adjustedMultiInputIndex + addedCount)); addedCount++; } if (addedCount == 0) return false; // Push down other items { var lines = BestInputMatch.Item.InputLines; var inputLineIndex = 0; while ( inputLineIndex < lines.Length && lines[inputLineIndex].Id != BestInputMatch.SlotId) { inputLineIndex++; } var wasInputLineWasConnected = lines[inputLineIndex].ConnectionIn != null; var hasHeightChanged = (BestInputMatch.InputSnapType == InputSnapTypes.InsertAfterMultiInput || BestInputMatch.InputSnapType == InputSnapTypes.InsertBeforeMultiInput) || (BestInputMatch.InputSnapType == InputSnapTypes.Normal && !wasInputLineWasConnected && BestInputMatch.Item.Variant == MagGraphItem.Variants.Operator && lines[inputLineIndex].InputUi.Relevancy == Relevancy.Optional && inputLineIndex > 0); // Every connection beyond the first adds another input line... var rowsToInsert = hasHeightChanged ? addedCount : addedCount - 1; if (rowsToInsert > 0 && inputLineIndex < lines.Length) { var insertionIndex = inputLineIndex + adjustedMultiInputIndex; var collectSnappedItems = MagItemMovement.CollectSnappedItems(BestInputMatch.Item); collectSnappedItems.Remove(BestInputMatch.Item); MagItemMovement.MoveSnappedItemsVertically(context, collectSnappedItems, BestInputMatch.Item.PosOnCanvas.Y + MagGraphItem.GridSize.Y * (insertionIndex - 0.5f), MagGraphItem.GridSize.Y * rowsToInsert); } } return true; } public enum InputSnapTypes { Normal, InsertBeforeMultiInput, ReplaceMultiInput, InsertAfterMultiInput, } public sealed record InputMatch( MagGraphItem? Item = null, Guid SlotId = default, Vector2 PosOnScreen = default, InputSnapTypes InputSnapType = InputSnapTypes.Normal, int MultiInputIndex = 0, float Distance = OutputSnapper.SnapThreshold); private static InputMatch _bestInputMatchForCurrentFrame = new(); public static InputMatch BestInputMatch = new(); }