1774 lines
69 KiB
C#
1774 lines
69 KiB
C#
#nullable enable
|
|
|
|
using System.Diagnostics;
|
|
using ImGuiNET;
|
|
using T3.Core.DataTypes.Vector;
|
|
using T3.Core.Utils;
|
|
using T3.Core.Operator;
|
|
using T3.Editor.Gui.Interaction;
|
|
using T3.Editor.Gui.Interaction.Snapping;
|
|
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.UiModel;
|
|
using T3.Editor.UiModel.Commands.Graph;
|
|
using T3.Editor.UiModel.InputsAndTypes;
|
|
using T3.Editor.UiModel.Modification;
|
|
using T3.Editor.UiModel.ProjectHandling;
|
|
using T3.Editor.UiModel.Selection;
|
|
using MagGraphView = T3.Editor.Gui.MagGraph.Ui.MagGraphView;
|
|
using Vector2 = System.Numerics.Vector2;
|
|
|
|
// ReSharper disable ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
|
// ReSharper disable UseWithExpressionToCopyStruct
|
|
|
|
namespace T3.Editor.Gui.MagGraph.Interaction;
|
|
|
|
/// <summary>
|
|
/// Provides functions for moving, snapping, connecting, insertion etc. of operators. It controlled by the <see cref="StateMachine{T}"/>
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Things would be slightly more efficient if this would would use SnapGraphItems. However this would
|
|
/// prevent us from reusing fence selection. Enforcing this to be used for dragging inputs and outputs
|
|
/// makes this class unnecessarily complex.
|
|
/// </remarks>
|
|
internal sealed partial class MagItemMovement
|
|
{
|
|
internal MagItemMovement(GraphUiContext graphUiContext, MagGraphView magGraphView, MagGraphLayout layout, NodeSelection nodeSelection)
|
|
{
|
|
_view = magGraphView;
|
|
_layout = layout;
|
|
_nodeSelection = nodeSelection;
|
|
_context = graphUiContext;
|
|
}
|
|
|
|
private readonly GraphUiContext _context;
|
|
|
|
internal void PrepareFrame(GraphUiContext context)
|
|
{
|
|
//PrepareDragInteraction();
|
|
_snapHandlerX.DrawSnapIndicator(context.View, UiColors.StatusActivated.Fade(0.3f));
|
|
}
|
|
|
|
internal void PrepareDragInteraction()
|
|
{
|
|
UpdateConnectionsToDraggedItems(DraggedItems); // Sadly structure might change during drag...
|
|
UpdateSnappedConnectionsToDraggedItems();
|
|
}
|
|
|
|
internal void CompleteDragOperation(GraphUiContext context)
|
|
{
|
|
Debug.Assert(context.MacroCommand != null);
|
|
if (context.MacroCommand != null)
|
|
{
|
|
context.MoveElementsCommand?.StoreCurrentValues();
|
|
CompleteSlowGrow(context);
|
|
GrowSectionsToFitDisplacedMembers(context);
|
|
context.CompleteMacroCommand();
|
|
|
|
// Section ownership is re-derived from geometry on the layout refresh
|
|
_layout.FlagStructureAsChanged();
|
|
}
|
|
|
|
if (!InputPicking.TryInitializeInputSelectionPickerForDraggedItem(context))
|
|
Reset();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dragging ops slowly against their section's bottom/right border makes the border
|
|
/// yield. The speed test only decides the initial engagement — once the border
|
|
/// yielded, it follows the dragged ops continuously, so the growth is smooth instead
|
|
/// of stuttering with the speed hovering around the threshold. Pulling the ops fully
|
|
/// back inside (or holding Shift) releases the latch, and the next border contact
|
|
/// decides afresh. Disabled while the frame is selected, or when
|
|
/// <see cref="UserSettings.ConfigData.SectionSlowResizeSpeed"/> is 0.
|
|
/// </summary>
|
|
private void TrySlowGrowSection(GraphUiContext context)
|
|
{
|
|
var unlockSpeed = UserSettings.Config.SectionSlowResizeSpeed;
|
|
if (unlockSpeed <= 0 || _slowGrowSectionId == Guid.Empty || DraggedItems.Count == 0)
|
|
return;
|
|
|
|
var io = ImGui.GetIO();
|
|
var speed = io.MouseDelta.Length() / MathF.Max(io.DeltaTime, 0.0001f) / T3Ui.UiScaleFactor;
|
|
_dampedDragSpeed = MathUtils.Lerp(_dampedDragSpeed, speed, 0.3f);
|
|
|
|
var symbolUi = context.CompositionInstance.GetSymbolUi();
|
|
if (!symbolUi.Sections.TryGetValue(_slowGrowSectionId, out var section)
|
|
|| section.Collapsed || section.IsHiddenInCollapsedSection
|
|
|| context.Selector.IsNodeSelected(section))
|
|
return;
|
|
|
|
var draggedBounds = MagGraphItem.GetItemsBounds(DraggedItems);
|
|
var boundsMin = section.PosOnCanvas;
|
|
var boundsMax = section.PosOnCanvas + section.Size;
|
|
|
|
// Border contact requires overlapping the frame on the other axis too - an op
|
|
// dragged past the frame's corner is outside, not pressing against the border
|
|
var overlapsVertically = draggedBounds.Min.Y < boundsMax.Y && draggedBounds.Max.Y > boundsMin.Y;
|
|
var overlapsHorizontally = draggedBounds.Min.X < boundsMax.X && draggedBounds.Max.X > boundsMin.X;
|
|
|
|
var touchesRight = overlapsVertically && draggedBounds.Max.X > boundsMax.X && draggedBounds.Min.X < boundsMax.X;
|
|
var touchesBottom = overlapsHorizontally && draggedBounds.Max.Y > boundsMax.Y && draggedBounds.Min.Y < boundsMax.Y;
|
|
|
|
if (io.KeyShift)
|
|
{
|
|
_slowGrowEngaged = false;
|
|
return;
|
|
}
|
|
|
|
if (!_slowGrowEngaged)
|
|
{
|
|
// Fully back inside? The next contact decides afresh.
|
|
if (!touchesRight && !touchesBottom)
|
|
return;
|
|
|
|
if (_dampedDragSpeed > unlockSpeed)
|
|
return;
|
|
|
|
_slowGrowEngaged = true;
|
|
}
|
|
|
|
// Follow the dragged ops continuously while engaged
|
|
var newMax = boundsMax;
|
|
if (touchesRight)
|
|
newMax.X = draggedBounds.Max.X + SectionTree.ContentPadding.X;
|
|
|
|
if (touchesBottom)
|
|
newMax.Y = draggedBounds.Max.Y + SectionTree.ContentPadding.Y;
|
|
|
|
if (draggedBounds.Max.X <= boundsMax.X && draggedBounds.Max.Y <= boundsMax.Y)
|
|
{
|
|
// Ops returned fully inside - release the latch (the frame keeps its size)
|
|
_slowGrowEngaged = false;
|
|
return;
|
|
}
|
|
|
|
if (newMax == boundsMax)
|
|
return;
|
|
|
|
if (_slowGrowCommand == null)
|
|
{
|
|
_slowGrowCommand = new ModifyCanvasElementsCommand(symbolUi, [section], context.Selector);
|
|
_growPushPreview.Engage(symbolUi, section, context.Selector);
|
|
}
|
|
|
|
_slowGrewX |= newMax.X > boundsMax.X;
|
|
_slowGrewY |= newMax.Y > boundsMax.Y;
|
|
section.Size = newMax - section.PosOnCanvas;
|
|
|
|
// Push neighbors ahead of the growing border right away - leaving overlaps
|
|
// unresolved until release would let ownership derivation flip memberships mid-drag
|
|
_growPushPreview.Update(section, pushDown: _slowGrewY, pushRight: _slowGrewX);
|
|
_layout.FlagStructureAsChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Folds the accumulated slow-grow resize into the drag's MacroCommand and pushes
|
|
/// neighbors out of the grown bounds.
|
|
/// </summary>
|
|
private void CompleteSlowGrow(GraphUiContext context)
|
|
{
|
|
if (_slowGrowCommand == null)
|
|
{
|
|
_slowGrowSectionId = Guid.Empty;
|
|
return;
|
|
}
|
|
|
|
_slowGrowCommand.StoreCurrentValues();
|
|
context.MacroCommand!.AddExecutedCommandForUndo(_slowGrowCommand);
|
|
_growPushPreview.Complete(context.MacroCommand);
|
|
|
|
// Sibling overlaps are already resolved by the live preview; this settles
|
|
// ancestor growth for nested frames
|
|
var symbolUi = context.CompositionInstance.GetSymbolUi();
|
|
if (symbolUi.Sections.TryGetValue(_slowGrowSectionId, out var grownSection))
|
|
{
|
|
// Only resolve along axes that actually grew - resolving the other axis would
|
|
// "fix" remaining overlaps by flinging neighbors sideways
|
|
var bounds = ImRect.RectWithSize(grownSection.PosOnCanvas, grownSection.Size);
|
|
if (_slowGrewY)
|
|
SectionTree.ResolveBoundsExpansion(symbolUi, grownSection, bounds, Vector2.UnitY, context.MacroCommand, context.Selector);
|
|
|
|
if (_slowGrewX)
|
|
SectionTree.ResolveBoundsExpansion(symbolUi, grownSection, bounds, Vector2.UnitX, context.MacroCommand, context.Selector);
|
|
}
|
|
|
|
_slowGrowCommand = null;
|
|
_slowGrowSectionId = Guid.Empty;
|
|
}
|
|
|
|
private Guid _slowGrowSectionId;
|
|
private ModifyCanvasElementsCommand? _slowGrowCommand;
|
|
private readonly SectionPushPreview _growPushPreview = new();
|
|
private bool _slowGrowEngaged;
|
|
private bool _slowGrewX;
|
|
private bool _slowGrewY;
|
|
private float _dampedDragSpeed;
|
|
|
|
/// <summary>
|
|
/// Splice-inserting into a stack pushes the ops below/right of the insertion — when such
|
|
/// displaced members cross their section's bottom or right border, the section grows to
|
|
/// keep them and neighboring frames get pushed out of the claimed area, all folded into
|
|
/// the drag's MacroCommand. Dragged items themselves are ignored, so deliberately
|
|
/// dragging an op out of a frame never grows it.
|
|
/// </summary>
|
|
private void GrowSectionsToFitDisplacedMembers(GraphUiContext context)
|
|
{
|
|
Debug.Assert(context.MacroCommand != null);
|
|
var symbolUi = context.CompositionInstance.GetSymbolUi();
|
|
if (symbolUi.Sections.Count == 0)
|
|
return;
|
|
|
|
_draggedChildIds.Clear();
|
|
foreach (var item in DraggedItems)
|
|
{
|
|
if (item.ChildUi != null)
|
|
_draggedChildIds.Add(item.ChildUi.Id);
|
|
}
|
|
|
|
foreach (var section in symbolUi.Sections.Values)
|
|
{
|
|
if (section.Collapsed || section.IsHiddenInCollapsedSection)
|
|
continue;
|
|
|
|
GrowSectionToFitContent(context, symbolUi, section, _draggedChildIds);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Grows a frame (bottom/right) so its member ops stay inside, pushing siblings and
|
|
/// growing ancestors via the bounds-expansion cascade. Folds into the interaction's
|
|
/// MacroCommand. Ops in <paramref name="excludedChildIds"/> are ignored, so a
|
|
/// deliberate drag out of the frame never grows it.
|
|
/// </summary>
|
|
private static void GrowSectionToFitContent(GraphUiContext context, SymbolUi symbolUi, Section section, HashSet<Guid>? excludedChildIds)
|
|
{
|
|
Debug.Assert(context.MacroCommand != null);
|
|
|
|
var contentMax = new Vector2(float.NegativeInfinity, float.NegativeInfinity);
|
|
var hasContent = false;
|
|
foreach (var childUi in symbolUi.ChildUis.Values)
|
|
{
|
|
if (childUi.SectionId != section.Id)
|
|
continue;
|
|
|
|
if (excludedChildIds != null && excludedChildIds.Contains(childUi.Id))
|
|
continue;
|
|
|
|
contentMax = Vector2.Max(contentMax, childUi.PosOnCanvas + childUi.Size);
|
|
hasContent = true;
|
|
}
|
|
|
|
if (!hasContent)
|
|
return;
|
|
|
|
var boundsMax = section.PosOnCanvas + section.Size;
|
|
var requiredMax = Vector2.Max(boundsMax, contentMax + SectionTree.ContentPadding);
|
|
var grewX = requiredMax.X > boundsMax.X + 0.5f;
|
|
var grewY = requiredMax.Y > boundsMax.Y + 0.5f;
|
|
if (!grewX && !grewY)
|
|
return;
|
|
|
|
var resizeCommand = new ModifyCanvasElementsCommand(symbolUi, [section], context.Selector);
|
|
section.Size = requiredMax - section.PosOnCanvas;
|
|
resizeCommand.StoreCurrentValues();
|
|
context.MacroCommand!.AddExecutedCommandForUndo(resizeCommand);
|
|
|
|
var newBounds = ImRect.RectWithSize(section.PosOnCanvas, section.Size);
|
|
if (grewY)
|
|
SectionTree.ResolveBoundsExpansion(symbolUi, section, newBounds, Vector2.UnitY, context.MacroCommand, context.Selector);
|
|
if (grewX)
|
|
SectionTree.ResolveBoundsExpansion(symbolUi, section, newBounds, Vector2.UnitX, context.MacroCommand, context.Selector);
|
|
}
|
|
|
|
private static readonly HashSet<Guid> _draggedChildIds = [];
|
|
|
|
/// <summary>
|
|
/// Reset to avoid accidental dragging of previous elements
|
|
/// </summary>
|
|
internal void StopDragOperation()
|
|
{
|
|
_lastAppliedOffset = Vector2.Zero;
|
|
SpliceSets.Clear();
|
|
DraggedItems.Clear();
|
|
|
|
// If the drag ended without CompleteDragOperation, take the uncommitted
|
|
// slow-grow preview back
|
|
if (_slowGrowCommand != null)
|
|
{
|
|
_slowGrowCommand.Undo();
|
|
_slowGrowCommand = null;
|
|
}
|
|
|
|
_growPushPreview.CancelAndReset();
|
|
_slowGrowSectionId = Guid.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will reset the state including all highlights and indicators
|
|
/// </summary>
|
|
internal void Reset()
|
|
{
|
|
// TODO: should be done by states...
|
|
_context.ActiveSourceItem = null;
|
|
_context.DraggedPrimaryOutputType = null;
|
|
|
|
DraggedItems.Clear();
|
|
_shakeDetector.ResetShaking();
|
|
}
|
|
|
|
internal static void SelectActiveItem(GraphUiContext context)
|
|
{
|
|
var item = context.ActiveItem;
|
|
if (item == null)
|
|
return;
|
|
|
|
var append = ImGui.GetIO().KeyShift;
|
|
|
|
if (context.Selector.IsNodeSelected(item))
|
|
{
|
|
if (append)
|
|
{
|
|
context.Selector.DeselectNode(item, item.Instance);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (append)
|
|
{
|
|
item.AddToSelection(context.Selector);
|
|
}
|
|
else
|
|
{
|
|
item.Select(context.Selector);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void UpdateDragging(GraphUiContext context)
|
|
{
|
|
if (!T3Ui.IsCurrentlySaving && _shakeDetector.TestDragForShake(ImGui.GetMousePos()))
|
|
{
|
|
_shakeDetector.ResetShaking();
|
|
if (HandleShakeDisconnect(context))
|
|
{
|
|
_layout.FlagStructureAsChanged();
|
|
return;
|
|
}
|
|
}
|
|
|
|
var snappingChanged = HandleSnappedDragging(context);
|
|
|
|
TrySlowGrowSection(context);
|
|
|
|
if (!snappingChanged)
|
|
return;
|
|
|
|
_layout.FlagStructureAsChanged();
|
|
|
|
HandleUnsnapAndCollapse(context);
|
|
|
|
if (!_snapping.IsSnapped)
|
|
return;
|
|
|
|
if (_snapping.IsInsertion)
|
|
{
|
|
TrySplitInsert(context);
|
|
}
|
|
else
|
|
{
|
|
TryCreateNewConnectionFromSnap(context);
|
|
}
|
|
}
|
|
|
|
private bool HandleShakeDisconnect(GraphUiContext context)
|
|
{
|
|
//Log.Debug("Shake it!");
|
|
Debug.Assert(context.MacroCommand != null);
|
|
if (_connectionsToDraggedItems.Count == 0)
|
|
return false;
|
|
|
|
NodeActions.DisconnectDraggedNodes(context.CompositionInstance, DraggedItems.Select(i => i.Selectable).ToList());
|
|
|
|
// foreach (var c in _borderConnections)
|
|
// {
|
|
// context.MacroCommand.AddAndExecCommand(new DeleteConnectionCommand(context.CompositionOp.Symbol, c.AsSymbolConnection(), 0));
|
|
// }
|
|
|
|
_layout.FlagStructureAsChanged();
|
|
return true;
|
|
}
|
|
|
|
internal static void UpdateLongPressIndicator(float longTapProgress)
|
|
{
|
|
var dl = ImGui.GetWindowDrawList();
|
|
dl.AddCircle(ImGui.GetMousePos(), 100 * (1 - longTapProgress), Color.White.Fade(MathF.Pow(longTapProgress, 3)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update dragged items and use anchor definitions to identify and use potential snap targets
|
|
/// </summary>
|
|
private bool HandleSnappedDragging(GraphUiContext context)
|
|
{
|
|
var dl = ImGui.GetWindowDrawList();
|
|
|
|
if (!_hasDragged)
|
|
{
|
|
_dragStartPosInOpOnCanvas = context.View.InverseTransformPositionFloat(ImGui.GetMousePos());
|
|
_hasDragged = true;
|
|
|
|
// Remember the frame the drag started in for the slow-grow interaction
|
|
_slowGrowSectionId = Guid.Empty;
|
|
_slowGrowCommand = null;
|
|
_growPushPreview.Reset();
|
|
_slowGrowEngaged = false;
|
|
_slowGrewX = false;
|
|
_slowGrewY = false;
|
|
_dampedDragSpeed = UserSettings.Config.SectionSlowResizeSpeed * 3; // start above unlock so a fresh grab doesn't engage instantly
|
|
|
|
// Rearranging a selection that includes frames must never auto-grow or push -
|
|
// that's deliberate layout work, not ops pressing against a border
|
|
var selectionIncludesSection = false;
|
|
foreach (var node in _nodeSelection.Selection)
|
|
{
|
|
if (node is not Section)
|
|
continue;
|
|
|
|
selectionIncludesSection = true;
|
|
break;
|
|
}
|
|
|
|
if (!selectionIncludesSection)
|
|
{
|
|
foreach (var item in DraggedItems)
|
|
{
|
|
if (item.ChildUi == null || item.ChildUi.SectionId == Guid.Empty)
|
|
continue;
|
|
|
|
_slowGrowSectionId = item.ChildUi.SectionId;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var mousePosOnCanvas = context.View.InverseTransformPositionFloat(ImGui.GetMousePos());
|
|
var requestedDeltaOnCanvas = mousePosOnCanvas - _dragStartPosInOpOnCanvas;
|
|
|
|
var dragExtend = MagGraphItem.GetItemsBounds(DraggedItems);
|
|
dragExtend.Expand(SnapThreshold * context.View.Scale.X);
|
|
|
|
if (_view.ShowDebug)
|
|
{
|
|
dl.AddCircle(_view.TransformPosition(_dragStartPosInOpOnCanvas), 10, Color.Blue);
|
|
|
|
dl.AddLine(_view.TransformPosition(_dragStartPosInOpOnCanvas),
|
|
_view.TransformPosition(_dragStartPosInOpOnCanvas + requestedDeltaOnCanvas), Color.Blue);
|
|
|
|
dl.AddRect(_view.TransformPosition(dragExtend.Min),
|
|
_view.TransformPosition(dragExtend.Max),
|
|
Color.Green.Fade(0.1f));
|
|
}
|
|
|
|
var overlappingItems = new List<MagGraphItem>();
|
|
|
|
foreach (var otherItem in _layout.Items.Values)
|
|
{
|
|
if (DraggedItems.Contains(otherItem) || !dragExtend.Overlaps(otherItem.Area))
|
|
continue;
|
|
|
|
overlappingItems.Add(otherItem);
|
|
}
|
|
|
|
// Move back to non-snapped position
|
|
foreach (var n in DraggedItems)
|
|
{
|
|
n.PosOnCanvas -= _lastAppliedOffset; // Move to position
|
|
n.PosOnCanvas += requestedDeltaOnCanvas; // Move to request position
|
|
}
|
|
|
|
// Only drag selected sections along when the drag started from the selection itself,
|
|
// not when dragging an unselected snapped group.
|
|
if (_draggedItemsFromSelection && DraggedItems.Count != _context.Selector.Selection.Count)
|
|
{
|
|
foreach (var a in _context.Selector.Selection)
|
|
{
|
|
if (a is not Section)
|
|
continue;
|
|
|
|
a.PosOnCanvas -= _lastAppliedOffset; // Move to position
|
|
a.PosOnCanvas += requestedDeltaOnCanvas; // Move to request position
|
|
}
|
|
}
|
|
|
|
|
|
_lastAppliedOffset = requestedDeltaOnCanvas;
|
|
_snapping.Reset();
|
|
|
|
foreach (var ip in SpliceSets)
|
|
{
|
|
var insertionAnchorItem = DraggedItems.FirstOrDefault(i => i.Id == ip.InputItemId);
|
|
if (insertionAnchorItem == null)
|
|
continue;
|
|
|
|
foreach (var otherItem in overlappingItems)
|
|
{
|
|
_snapping.TestItemsForInsertion(otherItem, insertionAnchorItem, ip, _view);
|
|
}
|
|
}
|
|
|
|
foreach (var otherItem in overlappingItems)
|
|
{
|
|
foreach (var draggedItem in DraggedItems)
|
|
{
|
|
_snapping.TestItemsForSnap(otherItem, draggedItem, false, _view);
|
|
_snapping.TestItemsForSnap(draggedItem, otherItem, true, _view);
|
|
}
|
|
}
|
|
|
|
// Highlight best distance
|
|
if (_view.ShowDebug && _snapping.BestDistance < 500)
|
|
{
|
|
var p1 = _view.TransformPosition(_snapping.OutAnchorPos);
|
|
var p2 = _view.TransformPosition(_snapping.InputAnchorPos);
|
|
dl.AddLine(p1, p2, UiColors.ForegroundFull.Fade(0.1f), 6);
|
|
}
|
|
|
|
// Snapped
|
|
var snapPositionChanged = false;
|
|
if (_snapping.IsSnapped)
|
|
{
|
|
var bestSnapDelta = _snapping.Reverse
|
|
? _snapping.InputAnchorPos - _snapping.OutAnchorPos
|
|
: _snapping.OutAnchorPos - _snapping.InputAnchorPos;
|
|
|
|
// var snapTargetPos = _snapping.Reverse
|
|
// ? _snapping.InputAnchorPos
|
|
// : _snapping.OutAnchorPos;
|
|
|
|
var snapPos = mousePosOnCanvas + bestSnapDelta;
|
|
|
|
// dl.AddLine(_canvas.TransformPosition(mousePosOnCanvas),
|
|
// context.Canvas.TransformPosition(mousePosOnCanvas) + _canvas.TransformDirection(bestSnapDelta),
|
|
// Color.White);
|
|
|
|
if (Vector2.Distance(snapPos, _lastSnapDragPositionOnCanvas) > 2) // ugh. Magic number
|
|
{
|
|
snapPositionChanged = true;
|
|
LastSnapTime = ImGui.GetTime();
|
|
_lastSnapDragPositionOnCanvas = snapPos;
|
|
LastSnapTargetPositionOnCanvas = _snapping.Reverse
|
|
? _snapping.InputAnchorPos
|
|
: _snapping.OutAnchorPos;
|
|
}
|
|
|
|
foreach (var n in DraggedItems)
|
|
{
|
|
n.PosOnCanvas += bestSnapDelta;
|
|
}
|
|
|
|
_lastAppliedOffset += bestSnapDelta;
|
|
}
|
|
// Unsnapped...
|
|
else
|
|
{
|
|
_lastSnapDragPositionOnCanvas = Vector2.Zero;
|
|
LastSnapTime = double.NegativeInfinity;
|
|
|
|
HandleHorizontalAlignmentSnapping(context);
|
|
}
|
|
|
|
var snappingChanged = _snapping.IsSnapped != _wasSnapped || _snapping.IsSnapped && snapPositionChanged;
|
|
_wasSnapped = _snapping.IsSnapped;
|
|
return snappingChanged;
|
|
}
|
|
|
|
private void HandleHorizontalAlignmentSnapping(GraphUiContext context)
|
|
{
|
|
if (!UserSettings.Config.EnableHorizontalSnapping)
|
|
return;
|
|
|
|
var snapFactor = 1f;
|
|
var newDragPosInCanvas = _draggedSelectables[0].PosOnCanvas;
|
|
|
|
var isFirst = true;
|
|
ImRect bounds = default;
|
|
foreach (var item in DraggedItems)
|
|
{
|
|
var itemBounds = item.Bounds;
|
|
if (isFirst)
|
|
{
|
|
bounds = itemBounds;
|
|
isFirst = false;
|
|
continue;
|
|
}
|
|
|
|
bounds.Add(itemBounds);
|
|
}
|
|
|
|
bounds.Expand(200);
|
|
|
|
|
|
_visibleItemsForSnapping.Clear();
|
|
foreach (var snapTo in context.Layout.Items.Values)
|
|
{
|
|
if (bounds.Overlaps(snapTo.Bounds))
|
|
_visibleItemsForSnapping.Add(snapTo);
|
|
}
|
|
|
|
if (!_snapHandlerX.TryCheckForSnapping(newDragPosInCanvas.X, out var snappedXValue,
|
|
context.View.Scale.X * snapFactor,
|
|
DraggedItems,
|
|
_visibleItemsForSnapping
|
|
))
|
|
return;
|
|
|
|
var snapDelta = snappedXValue - newDragPosInCanvas.X;
|
|
var offset = new Vector2((float)snapDelta, 0);
|
|
foreach (var n in DraggedItems)
|
|
{
|
|
n.PosOnCanvas += offset;
|
|
}
|
|
|
|
_lastAppliedOffset += offset;
|
|
}
|
|
|
|
private static readonly ValueSnapHandler _snapHandlerX = new(SnapResult.Orientations.Horizontal);
|
|
private static readonly List<MagGraphItem> _visibleItemsForSnapping = [];
|
|
|
|
public void StartDragOperation(GraphUiContext context)
|
|
{
|
|
_draggedSelectables = DraggedItems.Select(i => i as ISelectableCanvasObject).ToList();
|
|
if (_draggedSelectables.Count == 0)
|
|
return;
|
|
|
|
var macroCommand = context.StartMacroCommand("Move nodes");
|
|
|
|
context.MoveElementsCommand = new ModifyCanvasElementsCommand(context.CompositionInstance.Symbol.Id, _draggedSelectables, _nodeSelection);
|
|
macroCommand.AddExecutedCommandForUndo(context.MoveElementsCommand);
|
|
|
|
_lastAppliedOffset = Vector2.Zero;
|
|
_hasDragged = false;
|
|
|
|
UpdateConnectionsToDraggedItems(DraggedItems);
|
|
UpdateSnappedConnectionsToDraggedItems();
|
|
|
|
var mousePosInCanvas = context.View.InverseTransformPositionFloat(ImGui.GetMousePos());
|
|
InitSpliceLinks(DraggedItems, mousePosInCanvas);
|
|
InitPrimaryDraggedOutput();
|
|
|
|
_unsnappedBorderConnectionsBeforeDrag.Clear();
|
|
foreach (var c in _connectionsToDraggedItems)
|
|
{
|
|
if (!c.IsSnapped)
|
|
{
|
|
_unsnappedBorderConnectionsBeforeDrag.Add(c.ConnectionHash);
|
|
}
|
|
}
|
|
|
|
_context.CompositionInstance.Symbol.GetSymbolUi().FlagAsModified();
|
|
}
|
|
|
|
private readonly HashSet<int> _unsnappedBorderConnectionsBeforeDrag = [];
|
|
|
|
/// <summary>
|
|
/// Handles op disconnection and collapsed
|
|
/// </summary>
|
|
private void HandleUnsnapAndCollapse(GraphUiContext context)
|
|
{
|
|
Debug.Assert(context.MacroCommand != null);
|
|
|
|
var unsnappedConnections = new List<MagGraphConnection>();
|
|
|
|
//var enableDisconnected = UserSettings.Config.DisconnectOnUnsnap ^ ImGui.GetIO().KeyShift;
|
|
// if (!enableDisconnected)
|
|
// return;
|
|
|
|
// Delete unsnapped connections
|
|
foreach (var mc in _layout.MagConnections)
|
|
{
|
|
if (!IsBorderConnection(mc, DraggedItems))
|
|
continue;
|
|
|
|
if (_unsnappedBorderConnectionsBeforeDrag.Contains(mc.ConnectionHash))
|
|
continue;
|
|
|
|
var existedBeforeDragStart = _connectionToDragItemHashes.Contains(mc.ConnectionHash);
|
|
if (existedBeforeDragStart)
|
|
{
|
|
var isVerticalStackDisconnect = FindVerticalCollapsableConnectionPairs(_connectionsToDraggedItems);
|
|
var isHorizontalStackDisconnect = FindHorizontalCollapsableConnectionPairs(_connectionsToDraggedItems);
|
|
if(isVerticalStackDisconnect.Count != 1 && isHorizontalStackDisconnect.Count != 1)
|
|
continue;
|
|
}
|
|
|
|
unsnappedConnections.Add(mc);
|
|
|
|
var targetItemInputLine = mc.TargetItem.InputLines[mc.InputLineIndex];
|
|
var connection = mc.AsSymbolConnection();
|
|
|
|
context.MacroCommand.AddAndExecCommand(new DeleteConnectionCommand(context.CompositionInstance.Symbol,
|
|
connection,
|
|
targetItemInputLine.MultiInputIndex));
|
|
mc.IsTemporary = true;
|
|
}
|
|
|
|
if (TryCollapseDragFromVerticalStack(context, unsnappedConnections))
|
|
return;
|
|
|
|
if (TryCollapseDragFromHorizontalStack(context, unsnappedConnections))
|
|
return;
|
|
|
|
TryCollapseDisconnectedInputs(context, unsnappedConnections);
|
|
}
|
|
|
|
private bool TryCollapseDragFromVerticalStack(GraphUiContext context, List<MagGraphConnection> unsnappedConnections)
|
|
{
|
|
Debug.Assert(context.MacroCommand != null);
|
|
|
|
if (unsnappedConnections.Count == 0)
|
|
return false;
|
|
|
|
var list = FindVerticalCollapsableConnectionPairs(unsnappedConnections);
|
|
|
|
if (list.Count > 1)
|
|
{
|
|
Log.Debug("Collapsing has too many possibilities");
|
|
return false;
|
|
}
|
|
|
|
if (list.Count == 0)
|
|
return false;
|
|
|
|
var pair = list[0];
|
|
var bridgeConnection = new Symbol.Connection(pair.Ca.SourceParentOrChildId,
|
|
pair.Ca.SourceOutput.Id,
|
|
pair.Cb.TargetParentOrChildId,
|
|
pair.Cb.TargetInput.Id);
|
|
if (Structure.CheckForCycle(context.CompositionInstance.Symbol, bridgeConnection))
|
|
{
|
|
Log.Debug("Sorry, this connection would create a cycle. (1)");
|
|
return false;
|
|
}
|
|
|
|
var potentialMovers = CollectSnappedItems(pair.Cb.TargetItem);
|
|
|
|
// Clarify if the subset of items snapped to lower target op is sufficient to fill most gaps
|
|
|
|
// First find movable items and then create command with movements
|
|
var movableItems = MoveToCollapseVerticalGaps(pair.Ca, pair.Cb, potentialMovers, true);
|
|
if (movableItems.Count == 0)
|
|
return false;
|
|
|
|
var affectedItemsAsNodes = movableItems.Select(i => i as ISelectableCanvasObject).ToList();
|
|
var newMoveCommand = new ModifyCanvasElementsCommand(context.CompositionInstance.Symbol.Id, affectedItemsAsNodes, _nodeSelection);
|
|
context.MacroCommand.AddExecutedCommandForUndo(newMoveCommand);
|
|
|
|
MoveToCollapseVerticalGaps(pair.Ca, pair.Cb, movableItems, false);
|
|
newMoveCommand.StoreCurrentValues();
|
|
|
|
context.MacroCommand.AddAndExecCommand(new AddConnectionCommand(context.CompositionInstance.Symbol,
|
|
bridgeConnection,
|
|
pair.Cb.MultiInputIndex));
|
|
return true;
|
|
}
|
|
|
|
private bool TryCollapseDragFromHorizontalStack(GraphUiContext context, List<MagGraphConnection> unsnappedConnections)
|
|
{
|
|
Debug.Assert(context.MacroCommand != null);
|
|
|
|
if (unsnappedConnections.Count == 0)
|
|
return false;
|
|
|
|
var list = FindHorizontalCollapsableConnectionPairs(unsnappedConnections);
|
|
|
|
if (list.Count > 1)
|
|
{
|
|
Log.Debug("Collapsing has too many possibilities");
|
|
return false;
|
|
}
|
|
|
|
if (list.Count == 0)
|
|
return false;
|
|
|
|
var pair = list[0];
|
|
|
|
var bridgeConnection = new Symbol.Connection(pair.Ca.SourceParentOrChildId,
|
|
pair.Ca.SourceOutput.Id,
|
|
pair.Cb.TargetParentOrChildId,
|
|
pair.Cb.TargetInput.Id);
|
|
if (Structure.CheckForCycle(context.CompositionInstance.Symbol, bridgeConnection))
|
|
{
|
|
Log.Debug("Sorry, this connection would create a cycle. (1)");
|
|
return false;
|
|
}
|
|
|
|
var potentialMovers = CollectSnappedItems(pair.Cb.TargetItem);
|
|
|
|
// First find movable items and then create command with movements
|
|
var movableItems = MoveToCollapseHorizontalGaps(pair.Ca, pair.Cb, potentialMovers, true);
|
|
if (movableItems.Count == 0)
|
|
return false;
|
|
|
|
var affectedItemsAsNodes = movableItems.Select(i => i as ISelectableCanvasObject).ToList();
|
|
var newMoveCommand = new ModifyCanvasElementsCommand(context.CompositionInstance.Symbol.Id, affectedItemsAsNodes, _nodeSelection);
|
|
context.MacroCommand.AddExecutedCommandForUndo(newMoveCommand);
|
|
|
|
MoveToCollapseHorizontalGaps(pair.Ca, pair.Cb, movableItems, false);
|
|
newMoveCommand.StoreCurrentValues();
|
|
|
|
context.MacroCommand.AddAndExecCommand(new AddConnectionCommand(context.CompositionInstance.Symbol,
|
|
bridgeConnection,
|
|
pair.Cb.MultiInputIndex));
|
|
return true;
|
|
}
|
|
|
|
internal static List<SnapCollapseConnectionPair> FindLinkedVerticalCollapsableConnectionPairs(List<MagGraphConnection> unsnappedConnections)
|
|
{
|
|
// Find collapses
|
|
var pairs = new List<SnapCollapseConnectionPair>();
|
|
|
|
var ordered = unsnappedConnections.OrderBy(x => x.SourcePos.Y);
|
|
|
|
var capturedConnections = new HashSet<MagGraphConnection>();
|
|
// Flood fill vertical snap pairs
|
|
foreach (var topC in ordered)
|
|
{
|
|
if (topC.Style != MagGraphConnection.ConnectionStyles.MainOutToMainInSnappedVertical)
|
|
continue;
|
|
|
|
if (capturedConnections.Contains(topC))
|
|
continue;
|
|
|
|
var linkedOutConnection = topC;
|
|
|
|
// Find matching linked connections
|
|
while (true)
|
|
{
|
|
if (!linkedOutConnection.IsSnapped)
|
|
break;
|
|
|
|
var targetItem = linkedOutConnection.TargetItem;
|
|
if (targetItem == null)
|
|
break;
|
|
|
|
if (!targetItem.TryGetPrimaryOutConnections(out var outConnections))
|
|
break;
|
|
|
|
var cOut = outConnections[0];
|
|
|
|
if (!unsnappedConnections.Contains(cOut))
|
|
break;
|
|
|
|
linkedOutConnection = cOut;
|
|
|
|
capturedConnections.Add(linkedOutConnection);
|
|
}
|
|
|
|
if (linkedOutConnection != topC && topC.Type == linkedOutConnection.Type)
|
|
{
|
|
pairs.Add(new SnapCollapseConnectionPair(topC, linkedOutConnection));
|
|
}
|
|
}
|
|
|
|
return pairs;
|
|
}
|
|
|
|
private static List<SnapCollapseConnectionPair> FindVerticalCollapsableConnectionPairs(List<MagGraphConnection> unsnappedConnections)
|
|
{
|
|
var list = new List<SnapCollapseConnectionPair>();
|
|
|
|
// Collapse ops dragged from vertical stack...
|
|
var potentialLinks = new List<MagGraphConnection>();
|
|
|
|
foreach (var mc in unsnappedConnections)
|
|
{
|
|
if (mc.Style == MagGraphConnection.ConnectionStyles.MainOutToMainInSnappedVertical)
|
|
{
|
|
potentialLinks.Clear();
|
|
foreach (var cb in unsnappedConnections)
|
|
{
|
|
if (mc != cb
|
|
&& cb.Style == MagGraphConnection.ConnectionStyles.MainOutToMainInSnappedVertical
|
|
&& cb.SourcePos.Y > mc.SourcePos.Y
|
|
&& Math.Abs(cb.SourcePos.X - mc.SourcePos.X) < SnapTolerance
|
|
&& cb.Type == mc.Type)
|
|
{
|
|
potentialLinks.Add(cb);
|
|
}
|
|
}
|
|
|
|
if (potentialLinks.Count == 1)
|
|
{
|
|
list.Add(new SnapCollapseConnectionPair(mc, potentialLinks[0]));
|
|
}
|
|
else if (potentialLinks.Count > 1)
|
|
{
|
|
Log.Debug("Collapsing with gaps not supported yet");
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
private static List<SnapCollapseConnectionPair> FindHorizontalCollapsableConnectionPairs(List<MagGraphConnection> unsnappedConnections)
|
|
{
|
|
var list = new List<SnapCollapseConnectionPair>();
|
|
|
|
// Collapse ops dragged from vertical stack...
|
|
var potentialLinks = new List<MagGraphConnection>();
|
|
|
|
foreach (var mc in unsnappedConnections)
|
|
{
|
|
if (mc.Style == MagGraphConnection.ConnectionStyles.MainOutToMainInSnappedHorizontal)
|
|
{
|
|
potentialLinks.Clear();
|
|
foreach (var cb in unsnappedConnections)
|
|
{
|
|
if (mc != cb
|
|
&& (cb.Style == MagGraphConnection.ConnectionStyles.MainOutToMainInSnappedHorizontal ||
|
|
cb.Style ==MagGraphConnection.ConnectionStyles.MainOutToInputSnappedHorizontal)
|
|
&& cb.SourcePos.X > mc.SourcePos.X
|
|
&& Math.Abs(cb.SourcePos.Y - mc.SourcePos.Y) < SnapTolerance
|
|
&& cb.Type == mc.Type)
|
|
{
|
|
potentialLinks.Add(cb);
|
|
}
|
|
}
|
|
|
|
if (potentialLinks.Count == 1)
|
|
{
|
|
list.Add(new SnapCollapseConnectionPair(mc, potentialLinks[0]));
|
|
}
|
|
else if (potentialLinks.Count > 1)
|
|
{
|
|
Log.Debug("Collapsing with gaps not supported yet");
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
private void TryCollapseDisconnectedInputs(GraphUiContext context, List<MagGraphConnection> unsnappedConnections)
|
|
{
|
|
if (unsnappedConnections.Count == 0)
|
|
return;
|
|
|
|
var snappedItems = new HashSet<MagGraphItem>();
|
|
foreach (var mc in unsnappedConnections)
|
|
{
|
|
CollectSnappedItems(mc.TargetItem, snappedItems);
|
|
}
|
|
|
|
// Collapse lines of no longer visible inputs stack...
|
|
var collapseLines = new HashSet<float>();
|
|
foreach (var mc in unsnappedConnections)
|
|
{
|
|
var disconnectedInputWouldCollapseLine = DisconnectedInputWouldCollapseLine(mc);
|
|
if (disconnectedInputWouldCollapseLine)
|
|
{
|
|
collapseLines.Add(mc.SourcePos.Y);
|
|
}
|
|
}
|
|
|
|
if (collapseLines.Count == 0)
|
|
return;
|
|
|
|
foreach (var y in collapseLines.OrderDescending())
|
|
{
|
|
MoveSnappedItemsVertically(context,
|
|
snappedItems,
|
|
y,
|
|
-MagGraphItem.GridSize.Y
|
|
);
|
|
}
|
|
}
|
|
|
|
public static bool DisconnectedInputWouldCollapseLine(MagGraphConnection connection)
|
|
{
|
|
var inputWasNotPrimary = connection.InputLineIndex > 0;
|
|
if (connection.TargetItem.Variant != MagGraphItem.Variants.Operator)
|
|
return false;
|
|
|
|
var inputWasOptional = connection.TargetItem.InputLines[connection.InputLineIndex].InputUi.Relevancy == Relevancy.Optional;
|
|
|
|
var multiInputConnectionCount = 0;
|
|
|
|
foreach (var line in connection.TargetItem.InputLines)
|
|
{
|
|
if (line.InputUi.Id == connection.TargetItem.InputLines[connection.InputLineIndex].Id)
|
|
multiInputConnectionCount++;
|
|
}
|
|
|
|
var multiInputHadOtherConnectedMultiInput = multiInputConnectionCount > 1;
|
|
var connectedToLeftInput =
|
|
connection.Style == MagGraphConnection.ConnectionStyles.BottomToLeft
|
|
|| connection.Style == MagGraphConnection.ConnectionStyles.RightToLeft
|
|
|| connection.Style == MagGraphConnection.ConnectionStyles.MainOutToMainInSnappedHorizontal
|
|
|| connection.Style == MagGraphConnection.ConnectionStyles.MainOutToInputSnappedHorizontal;
|
|
|
|
if (connectedToLeftInput
|
|
&& (inputWasNotPrimary && inputWasOptional)
|
|
|| multiInputHadOtherConnectedMultiInput)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
///<summary>
|
|
/// Iterate through gap lines and move items below upwards
|
|
/// </summary>
|
|
public static HashSet<MagGraphItem> MoveToCollapseVerticalGaps(MagGraphConnection ca, MagGraphConnection cb, HashSet<MagGraphItem> movableItems,
|
|
bool dryRun)
|
|
{
|
|
var minY = ca.SourcePos.Y;
|
|
var maxY = cb.TargetPos.Y;
|
|
var lineWidth = MagGraphItem.GridSize.Y;
|
|
|
|
var snapCount = (maxY - minY) / lineWidth;
|
|
var roundedSnapCount = (int)(snapCount + 0.5f);
|
|
var affectedItems = new HashSet<MagGraphItem>();
|
|
for (var lineIndex = roundedSnapCount - 1; lineIndex >= 0; lineIndex--)
|
|
{
|
|
var isLineBlocked = false;
|
|
var middleSnapLineY = minY + (0.5f + lineIndex) * lineWidth;
|
|
|
|
foreach (var item in movableItems)
|
|
{
|
|
if (item.Area.Min.Y >= middleSnapLineY || item.Area.Max.Y <= middleSnapLineY)
|
|
continue;
|
|
|
|
isLineBlocked = true;
|
|
break;
|
|
}
|
|
|
|
if (isLineBlocked)
|
|
continue;
|
|
|
|
// move lines below up one step
|
|
foreach (var item in movableItems)
|
|
{
|
|
if (item.PosOnCanvas.Y < middleSnapLineY)
|
|
continue;
|
|
|
|
affectedItems.Add(item);
|
|
|
|
if (!dryRun)
|
|
item.PosOnCanvas -= new Vector2(0, lineWidth);
|
|
}
|
|
}
|
|
|
|
return affectedItems;
|
|
}
|
|
|
|
|
|
///<summary>
|
|
/// Try to close gap be looking for disjoint snapped set on the left and right
|
|
/// </summary>
|
|
private HashSet<MagGraphItem> MoveToCollapseHorizontalGaps(MagGraphConnection ca, MagGraphConnection cb, HashSet<MagGraphItem> movableItems,
|
|
bool dryRun)
|
|
{
|
|
var counterOffset = Vector2.Zero;
|
|
foreach (var ss in SpliceSets)
|
|
{
|
|
if (ss.InputItemId == ca.TargetItem.Id && ss.Direction == MagGraphItem.Directions.Horizontal)
|
|
{
|
|
counterOffset = ss.DragPositionWithinBlock;
|
|
}
|
|
}
|
|
|
|
var leftBatch = CollectSnappedItems(ca.SourceItem, null, true, ca.ConnectionHash);
|
|
var rightBatch = CollectSnappedItems(cb.TargetItem, null, true, ca.ConnectionHash);
|
|
|
|
var onlyLeft = new HashSet<MagGraphItem>(leftBatch);
|
|
onlyLeft.ExceptWith(rightBatch);
|
|
|
|
var onlyRight = new HashSet<MagGraphItem>(rightBatch);
|
|
onlyRight.ExceptWith(leftBatch);
|
|
|
|
var intersection = new HashSet<MagGraphItem>(rightBatch);
|
|
intersection.IntersectWith(leftBatch);
|
|
|
|
if (intersection.Count != 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var affectedItems = new HashSet<MagGraphItem>(onlyLeft);
|
|
affectedItems.UnionWith(onlyRight);
|
|
if (dryRun)
|
|
return affectedItems;
|
|
|
|
foreach (var item in onlyRight)
|
|
{
|
|
item.PosOnCanvas += new Vector2( counterOffset.X - MagGraphItem.GridSize.X, 0);
|
|
}
|
|
|
|
foreach (var item in onlyLeft)
|
|
{
|
|
item.PosOnCanvas += new Vector2(counterOffset.X, 0);
|
|
}
|
|
|
|
return affectedItems;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A pair of snapped connections that can be fused after unsnapping some items from a stack
|
|
/// </summary>
|
|
internal sealed record SnapCollapseConnectionPair(MagGraphConnection Ca, MagGraphConnection Cb);
|
|
|
|
///<summary>
|
|
/// Search for potential new connections through snapping
|
|
/// </summary>
|
|
private void TryCreateNewConnectionFromSnap(GraphUiContext context)
|
|
{
|
|
if (!_snapping.IsSnapped)
|
|
return;
|
|
|
|
Debug.Assert(context.MacroCommand != null);
|
|
|
|
var newConnections = new List<PotentialConnection>();
|
|
foreach (var draggedItem in DraggedItems)
|
|
{
|
|
foreach (var otherItem in _layout.Items.Values)
|
|
{
|
|
if (DraggedItems.Contains(otherItem))
|
|
continue;
|
|
|
|
GetPotentialConnectionsAfterSnap(ref newConnections, draggedItem, otherItem);
|
|
GetPotentialConnectionsAfterSnap(ref newConnections, otherItem, draggedItem);
|
|
}
|
|
}
|
|
|
|
foreach (var potentialConnection in newConnections)
|
|
{
|
|
// Avoid accidental vertical double connections to item above when snapping a horizontal connection
|
|
if (potentialConnection.TargetItem.OutputLines.Length > 0
|
|
&& potentialConnection.TargetItem.OutputLines[0].ConnectionsOut.Count > 0)
|
|
{
|
|
var wouldConnectionOutSnapAfterMove = false;
|
|
foreach (var targetOutConnection in potentialConnection.TargetItem.OutputLines[0].ConnectionsOut)
|
|
{
|
|
if (targetOutConnection.IsSnapped)
|
|
continue;
|
|
|
|
var newOutputPos = targetOutConnection.SourceItem.PosOnCanvas
|
|
+ new Vector2(MagGraphItem.GridSize.X, MagGraphItem.GridSize.Y * (0.5f + targetOutConnection.VisibleOutputIndex));
|
|
if (Vector2.Distance(newOutputPos, targetOutConnection.TargetPos) < 0.01f)
|
|
{
|
|
wouldConnectionOutSnapAfterMove = true;
|
|
}
|
|
}
|
|
|
|
if (wouldConnectionOutSnapAfterMove)
|
|
{
|
|
Log.Debug("Avoiding double vertical connection...");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Avoid accidental vertical double connections to item below when snapping a horizontal connection
|
|
if (potentialConnection.SourceItem.OutputLines.Length > 0
|
|
&& potentialConnection.SourceItem.OutputLines[0].ConnectionsOut.Count > 0)
|
|
{
|
|
var wouldConnectionOutSnapAfterMove = false;
|
|
foreach (var sourceOutConnection in potentialConnection.SourceItem.OutputLines[0].ConnectionsOut)
|
|
{
|
|
// if (sourceOutConnection.IsSnapped && !sourceOutConnection.WasDisconnected.)
|
|
// continue;
|
|
|
|
var newOutputPos = sourceOutConnection.SourceItem.PosOnCanvas
|
|
+ new Vector2(MagGraphItem.GridSize.X, MagGraphItem.GridSize.Y * (0.5f + sourceOutConnection.VisibleOutputIndex));
|
|
if (Vector2.Distance(newOutputPos, sourceOutConnection.TargetPos) < 4f)
|
|
{
|
|
wouldConnectionOutSnapAfterMove = true;
|
|
}
|
|
}
|
|
|
|
if (wouldConnectionOutSnapAfterMove)
|
|
{
|
|
Log.Debug("Avoiding double connection...");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var sourceParentOfSymbolChildId =
|
|
potentialConnection.SourceItem.Variant == MagGraphItem.Variants.Input ? Guid.Empty : potentialConnection.SourceItem.Id;
|
|
|
|
var targetParentOfSymbolChildId =
|
|
potentialConnection.TargetItem.Variant == MagGraphItem.Variants.Output ? Guid.Empty : potentialConnection.TargetItem.Id;
|
|
|
|
var newConnection = new Symbol.Connection(sourceParentOfSymbolChildId,
|
|
potentialConnection.OutputLine.Id,
|
|
targetParentOfSymbolChildId,
|
|
potentialConnection.InputLine.Id);
|
|
|
|
if (Structure.CheckForCycle(context.CompositionInstance.Symbol, newConnection))
|
|
{
|
|
Log.Debug("Sorry, this connection would create a cycle. (4)");
|
|
continue;
|
|
}
|
|
|
|
context.MacroCommand.AddAndExecCommand(new AddConnectionCommand(context.CompositionInstance.Symbol,
|
|
newConnection,
|
|
potentialConnection.InputLine.MultiInputIndex));
|
|
}
|
|
}
|
|
|
|
private static void GetPotentialConnectionsAfterSnap(ref List<PotentialConnection> result, MagGraphItem a, MagGraphItem b)
|
|
{
|
|
MagGraphConnection? inConnection;
|
|
|
|
for (var bInputLineIndex = 0; bInputLineIndex < b.InputLines.Length; bInputLineIndex++)
|
|
{
|
|
ref var bInputLine = ref b.InputLines[bInputLineIndex];
|
|
inConnection = bInputLine.ConnectionIn;
|
|
|
|
int aOutLineIndex;
|
|
for (aOutLineIndex = 0; aOutLineIndex < a.OutputLines.Length; aOutLineIndex++)
|
|
{
|
|
ref var outputLine = ref a.OutputLines[aOutLineIndex]; // Avoid copying data from array
|
|
if (bInputLine.Type != outputLine.Output.ValueType)
|
|
continue;
|
|
|
|
// vertical
|
|
if (aOutLineIndex == 0 && bInputLineIndex == 0)
|
|
{
|
|
AddPossibleNewConnections(ref result,
|
|
ref outputLine,
|
|
ref bInputLine,
|
|
new Vector2(a.Area.Min.X + MagGraphItem.WidthHalf, a.Area.Max.Y),
|
|
new Vector2(b.Area.Min.X + MagGraphItem.WidthHalf, b.Area.Min.Y));
|
|
}
|
|
|
|
// horizontal
|
|
if (outputLine.Output.ValueType == bInputLine.Type)
|
|
{
|
|
AddPossibleNewConnections(ref result,
|
|
ref outputLine,
|
|
ref bInputLine,
|
|
new Vector2(a.Area.Max.X, a.Area.Min.Y + (0.5f + outputLine.VisibleIndex) * MagGraphItem.LineHeight),
|
|
new Vector2(b.Area.Min.X, b.Area.Min.Y + (0.5f + bInputLine.VisibleIndex) * MagGraphItem.LineHeight));
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
|
|
void AddPossibleNewConnections(ref List<PotentialConnection> newConnections,
|
|
ref MagGraphItem.OutputLine outputLine,
|
|
ref MagGraphItem.InputLine inputLine,
|
|
Vector2 outPos,
|
|
Vector2 inPos)
|
|
{
|
|
if (Vector2.Distance(outPos, inPos) > SnapTolerance)
|
|
return;
|
|
|
|
if (inConnection != null)
|
|
return;
|
|
|
|
// Clarify if outConnection should also be empty...
|
|
if (outputLine.ConnectionsOut.Count > 0)
|
|
{
|
|
if (outputLine.ConnectionsOut[0].IsSnapped
|
|
&& (outputLine.ConnectionsOut[0].SourcePos - inPos).Length() < SnapTolerance)
|
|
return;
|
|
}
|
|
|
|
newConnections.Add(new PotentialConnection(a, outputLine,
|
|
b, inputLine));
|
|
}
|
|
}
|
|
|
|
private sealed record PotentialConnection(
|
|
MagGraphItem SourceItem,
|
|
MagGraphItem.OutputLine OutputLine,
|
|
MagGraphItem TargetItem,
|
|
MagGraphItem.InputLine InputLine);
|
|
|
|
private void UpdateConnectionsToDraggedItems(HashSet<MagGraphItem> draggedItems)
|
|
{
|
|
_connectionsToDraggedItems.Clear();
|
|
_connectionToDragItemHashes.Clear();
|
|
|
|
// This could be optimized by only looking for dragged item connections
|
|
foreach (var c in _layout.MagConnections)
|
|
{
|
|
if (!IsBorderConnection(c, draggedItems))
|
|
continue;
|
|
|
|
_connectionsToDraggedItems.Add(c);
|
|
_connectionToDragItemHashes.Add(c.ConnectionHash);
|
|
}
|
|
}
|
|
|
|
private static bool IsBorderConnection(MagGraphConnection c, HashSet<MagGraphItem> draggedItems)
|
|
{
|
|
var isTargetDragged = draggedItems.Contains(c.TargetItem);
|
|
var isSourceDragged = draggedItems.Contains(c.SourceItem);
|
|
|
|
return isTargetDragged != isSourceDragged;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// This is updated every frame...
|
|
/// </summary>
|
|
private void UpdateSnappedConnectionsToDraggedItems()
|
|
{
|
|
_snappedConnectionToDragItemHashes.Clear();
|
|
|
|
foreach (var c in _connectionsToDraggedItems)
|
|
{
|
|
if (c.IsSnapped)
|
|
_snappedConnectionToDragItemHashes.Add(c.ConnectionHash);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When starting a new drag operation, we try to identify border input anchors of the dragged items,
|
|
/// that can be used to insert them between other snapped items.
|
|
/// </summary>
|
|
private void InitSpliceLinks(HashSet<MagGraphItem> draggedItems, Vector2 mousePosInCanvas)
|
|
{
|
|
SpliceSets.Clear();
|
|
|
|
foreach (var inputItemA in draggedItems)
|
|
{
|
|
var itemAInputCount = inputItemA.GetInputAnchorCount();
|
|
MagGraphItem.InputAnchorPoint inputAnchor = default;
|
|
|
|
for (var inputAnchorIndex = 0; inputAnchorIndex < itemAInputCount; inputAnchorIndex++)
|
|
{
|
|
inputItemA.GetInputAnchorAtIndex(inputAnchorIndex, ref inputAnchor);
|
|
// make sure it's a snapped border connection
|
|
if (inputAnchor.SnappedConnectionHash != MagGraphItem.FreeAnchor
|
|
&& !_snappedConnectionToDragItemHashes.Contains(inputAnchor.SnappedConnectionHash))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var spliceSets = new List<SpliceLink>();
|
|
foreach (var outputItemB in draggedItems)
|
|
{
|
|
var xy = inputAnchor.Direction == MagGraphItem.Directions.Horizontal ? 0 : 1;
|
|
|
|
if (Math.Abs(inputItemA.PosOnCanvas[1 - xy] - outputItemB.PosOnCanvas[1 - xy]) > SnapTolerance)
|
|
continue;
|
|
|
|
var outputAnchorCount = outputItemB.GetOutputAnchorCount();
|
|
MagGraphItem.OutputAnchorPoint outputAnchor = default;
|
|
|
|
for (var outputIndex = 0; outputIndex < outputAnchorCount; outputIndex++)
|
|
{
|
|
outputItemB.GetOutputAnchorAtIndex(outputIndex, ref outputAnchor);
|
|
if (outputAnchor.SnappedConnectionHash != MagGraphItem.FreeAnchor
|
|
&& !_snappedConnectionToDragItemHashes.Contains(outputAnchor.SnappedConnectionHash))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
outputAnchor.Direction != inputAnchor.Direction
|
|
|| inputAnchor.ConnectionType != outputAnchor.ConnectionType)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var inputAnchorPositionOnCanvas = inputAnchor.PositionOnCanvas - inputItemA.PosOnCanvas;
|
|
|
|
spliceSets.Add(new SpliceLink(inputItemA.Id,
|
|
inputAnchor.SlotId,
|
|
outputItemB.Id,
|
|
outputAnchor.SlotId,
|
|
inputAnchor.Direction,
|
|
inputAnchor.ConnectionType,
|
|
outputAnchor.PositionOnCanvas[xy] - inputAnchor.PositionOnCanvas[xy],
|
|
mousePosInCanvas - inputItemA.PosOnCanvas,
|
|
inputAnchorPositionOnCanvas
|
|
));
|
|
}
|
|
}
|
|
|
|
// Skip insertion lines with gaps
|
|
if (spliceSets.Count == 1)
|
|
{
|
|
SpliceSets.Add(spliceSets[0]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
///<summary>
|
|
/// Search for potential new connections through snapping
|
|
/// </summary>
|
|
// ReSharper disable once UnusedMethodReturnValue.Local
|
|
private static bool TrySplitInsert(GraphUiContext context)
|
|
{
|
|
if (!_snapping.IsSnapped || _snapping.TargetItem == null || _snapping.SourceItem == null)
|
|
return false;
|
|
|
|
Debug.Assert(context.MacroCommand != null);
|
|
|
|
var spliceLink = _snapping.InsertionPoint;
|
|
|
|
// Split connection
|
|
var connection = _snapping.TargetItem.InputLines[_snapping.InputLineIndex].ConnectionIn;
|
|
if (connection == null)
|
|
{
|
|
Log.Warning("Missing connection?");
|
|
return true;
|
|
}
|
|
|
|
if (spliceLink == null)
|
|
{
|
|
Log.Warning("Insertion point is undefined?");
|
|
return false;
|
|
}
|
|
|
|
var connectionIntoSplice = new Symbol.Connection(connection.SourceParentOrChildId,
|
|
connection.SourceOutput.Id,
|
|
spliceLink.InputItemId,
|
|
spliceLink.InputId);
|
|
|
|
var connectionOutOfSplice = new Symbol.Connection(spliceLink.OutputItemId,
|
|
spliceLink.OutputId,
|
|
connection.TargetParentOrChildId,
|
|
connection.TargetInput.Id);
|
|
|
|
if (Structure.CheckForCycle(context.CompositionInstance.Symbol, connectionIntoSplice))
|
|
{
|
|
Log.Debug("Sorry, this connection would create a cycle. (2)");
|
|
return false;
|
|
}
|
|
|
|
if (Structure.CheckForCycle(context.CompositionInstance.Symbol, connectionOutOfSplice))
|
|
{
|
|
Log.Debug("Sorry, this connection would create a cycle. (3)");
|
|
return false;
|
|
}
|
|
|
|
context.MacroCommand.AddAndExecCommand(new DeleteConnectionCommand(context.CompositionInstance.Symbol,
|
|
connection.AsSymbolConnection(),
|
|
connection.MultiInputIndex));
|
|
context.MacroCommand.AddAndExecCommand(new AddConnectionCommand(context.CompositionInstance.Symbol,
|
|
connectionIntoSplice, 0));
|
|
|
|
context.MacroCommand.AddAndExecCommand(new AddConnectionCommand(context.CompositionInstance.Symbol,
|
|
connectionOutOfSplice, connection.MultiInputIndex));
|
|
|
|
// Move items
|
|
if (spliceLink.Direction == MagGraphItem.Directions.Vertical)
|
|
{
|
|
MoveSnappedItemsVertically(context,
|
|
CollectSnappedItems(_snapping.TargetItem),
|
|
_snapping.OutAnchorPos.Y - MagGraphItem.GridSize.Y / 2,
|
|
spliceLink.Distance);
|
|
}
|
|
else
|
|
{
|
|
var leftBatch = CollectSnappedItems(_snapping.SourceItem, null, true, connection.ConnectionHash);
|
|
var rightBatch = CollectSnappedItems(_snapping.TargetItem, null, true, connection.ConnectionHash);
|
|
|
|
var onlyLeft = new HashSet<MagGraphItem>(leftBatch);
|
|
onlyLeft.ExceptWith(rightBatch);
|
|
|
|
var onlyRight = new HashSet<MagGraphItem>(rightBatch);
|
|
onlyRight.ExceptWith(leftBatch);
|
|
|
|
var intersection = new HashSet<MagGraphItem>(rightBatch);
|
|
intersection.IntersectWith(leftBatch);
|
|
|
|
MoveItems(context, onlyLeft, -new Vector2(spliceLink.DragPositionWithinBlock.X ,0));
|
|
MoveItems(context, onlyRight, new Vector2(spliceLink.Distance - spliceLink.DragPositionWithinBlock.X,0));
|
|
MoveSnappedItemsHorizontally(context,
|
|
intersection,
|
|
_snapping.OutAnchorPos.X - MagGraphItem.GridSize.X / 2,
|
|
spliceLink.Distance);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and applies a command to move items vertically
|
|
/// </summary>
|
|
/// <returns>
|
|
/// True if some items where moved
|
|
/// </returns>
|
|
public static void MoveSnappedItemsVertically(GraphUiContext context, HashSet<MagGraphItem> snappedItems, float yThreshold, float yDistance)
|
|
{
|
|
Debug.Assert(context.MacroCommand != null);
|
|
var movableItems = new List<MagGraphItem>();
|
|
foreach (var otherItem in snappedItems)
|
|
{
|
|
if (otherItem.PosOnCanvas.Y > yThreshold)
|
|
{
|
|
movableItems.Add(otherItem);
|
|
}
|
|
}
|
|
|
|
if (movableItems.Count == 0)
|
|
return;
|
|
|
|
MoveItems(context, movableItems, new Vector2(0, yDistance));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and applies a command to move items vertically
|
|
/// </summary>
|
|
/// <returns>
|
|
/// True if some items where moved
|
|
/// </returns>
|
|
public static void MoveSnappedItemsHorizontally(GraphUiContext context, HashSet<MagGraphItem> snappedItems, float xThreshold, float xDistance)
|
|
{
|
|
Debug.Assert(context.MacroCommand != null);
|
|
|
|
var movableItems = new List<MagGraphItem>();
|
|
foreach (var otherItem in snappedItems)
|
|
{
|
|
if (otherItem.PosOnCanvas.X > xThreshold)
|
|
{
|
|
movableItems.Add(otherItem);
|
|
}
|
|
}
|
|
|
|
if (movableItems.Count == 0)
|
|
return;
|
|
|
|
MoveItems(context, movableItems, new Vector2(xDistance,0));
|
|
}
|
|
|
|
private static void MoveItems(GraphUiContext context, IEnumerable<MagGraphItem> movableItems, Vector2 offset)
|
|
{
|
|
Debug.Assert(context.MacroCommand != null);
|
|
|
|
// Move items left...
|
|
var affectedItemsAsNodes = movableItems.Select(i => i as ISelectableCanvasObject).ToList();
|
|
var newMoveComment = new ModifyCanvasElementsCommand(context.CompositionInstance.Symbol.Id, affectedItemsAsNodes, context.Selector);
|
|
context.MacroCommand.AddExecutedCommandForUndo(newMoveComment);
|
|
|
|
Log.Debug($"MoveItems: {affectedItemsAsNodes.Count} items by {offset}");
|
|
foreach (var item in affectedItemsAsNodes)
|
|
{
|
|
item.PosOnCanvas += offset;
|
|
}
|
|
|
|
newMoveComment.StoreCurrentValues();
|
|
|
|
// Programmatic pushes (splice inserts, added input rows, placeholder insertion) can
|
|
// shove ops across their section's bottom/right border - grow those frames to keep them
|
|
_growSectionIds.Clear();
|
|
var symbolUi = context.CompositionInstance.GetSymbolUi();
|
|
foreach (var item in movableItems)
|
|
{
|
|
if (item.ChildUi == null || item.ChildUi.SectionId == Guid.Empty)
|
|
continue;
|
|
|
|
if (!_growSectionIds.Add(item.ChildUi.SectionId))
|
|
continue;
|
|
|
|
if (!symbolUi.Sections.TryGetValue(item.ChildUi.SectionId, out var section)
|
|
|| section.Collapsed || section.IsHiddenInCollapsedSection)
|
|
continue;
|
|
|
|
GrowSectionToFitContent(context, symbolUi, section, excludedChildIds: null);
|
|
}
|
|
}
|
|
|
|
private static readonly HashSet<Guid> _growSectionIds = [];
|
|
|
|
private void InitPrimaryDraggedOutput()
|
|
{
|
|
_context.DraggedPrimaryOutputType = null;
|
|
_context.ItemForInputSelection = null;
|
|
|
|
_context.ActiveSourceItem = FindPrimaryOutputItem();
|
|
if (_context.ActiveSourceItem == null)
|
|
return;
|
|
|
|
_context.DraggedPrimaryOutputType = _context.ActiveSourceItem.PrimaryType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Snapping an item onto a hidden parameter is a tricky ui problem.
|
|
/// To allow this interaction we add "peek" anchor indicator to the dragged item set.
|
|
/// If this is dropped without snapping onto an operator with hidden parameters of the matching type,
|
|
/// we present cables.gl inspired picker interface.
|
|
///
|
|
/// Set primary types to indicate targets for dragging
|
|
/// This part is a little fishy. To have "some" solution we try to identify dragged
|
|
/// constellations that has a single free horizontal output anchor on the left column.
|
|
/// </summary>
|
|
private MagGraphItem? FindPrimaryOutputItem()
|
|
{
|
|
if (DraggedItems.Count == 0)
|
|
return null;
|
|
|
|
if (DraggedItems.Count == 1)
|
|
return DraggedItems.First();
|
|
|
|
var itemsOrderedByX = DraggedItems.OrderByDescending(c => c.PosOnCanvas.X);
|
|
var rightItem = itemsOrderedByX.First();
|
|
|
|
// Check if there are multiple items on right edge column...
|
|
var rightColumnItemCount = 0;
|
|
foreach (var i in DraggedItems)
|
|
{
|
|
if (Math.Abs(i.PosOnCanvas.X - rightItem.PosOnCanvas.X) < SnapTolerance)
|
|
rightColumnItemCount++;
|
|
}
|
|
|
|
if (rightColumnItemCount > 1)
|
|
return null;
|
|
|
|
if (DraggedItems.Count != CollectSnappedItems(rightItem).Count)
|
|
return null;
|
|
|
|
return rightItem;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add snapped items to the given set or create new set
|
|
/// </summary>
|
|
public static HashSet<MagGraphItem> CollectSnappedItems(MagGraphItem rootItem, HashSet<MagGraphItem>? set = null, bool includeRoot= true, int ignoreConnectionHash= 0)
|
|
{
|
|
set ??= [];
|
|
|
|
Collect(rootItem);
|
|
if (!includeRoot)
|
|
set.Remove(rootItem);
|
|
|
|
return set;
|
|
|
|
void Collect(MagGraphItem item)
|
|
{
|
|
if (!set.Add(item))
|
|
return;
|
|
|
|
for (var index = 0; index < item.InputLines.Length; index++)
|
|
{
|
|
var c = item.InputLines[index].ConnectionIn;
|
|
if (c == null || c.ConnectionHash == ignoreConnectionHash)
|
|
continue;
|
|
|
|
if (c.IsSnapped && !c.IsTemporary)
|
|
Collect(c.SourceItem);
|
|
}
|
|
|
|
for (var index = 0; index < item.OutputLines.Length; index++)
|
|
{
|
|
var connections = item.OutputLines[index].ConnectionsOut;
|
|
foreach (var c in connections)
|
|
{
|
|
if (c.ConnectionHash != ignoreConnectionHash && c.IsSnapped)
|
|
Collect(c.TargetItem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static HashSet<MagGraphItem> CollectSnappedItems(IEnumerable<MagGraphItem> rootItems)
|
|
{
|
|
var set = new HashSet<MagGraphItem>();
|
|
|
|
foreach (var i in rootItems)
|
|
{
|
|
CollectSnappedItems(i, set);
|
|
}
|
|
|
|
return set;
|
|
}
|
|
|
|
internal void SetDraggedItemIds(List<Guid> selectedIds)
|
|
{
|
|
DraggedItems.Clear();
|
|
_draggedItemsFromSelection = false;
|
|
foreach (var id in selectedIds)
|
|
{
|
|
if (_layout.Items.TryGetValue(id, out var i))
|
|
{
|
|
DraggedItems.Add(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void SetDraggedItems(List<ISelectableCanvasObject> selection)
|
|
{
|
|
DraggedItems.Clear();
|
|
_draggedItemsFromSelection = true;
|
|
foreach (var s in selection)
|
|
{
|
|
if (_layout.Items.TryGetValue(s.Id, out var i))
|
|
{
|
|
DraggedItems.Add(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void SetDraggedItemIdsToSnappedForItem(MagGraphItem item)
|
|
{
|
|
DraggedItems.Clear();
|
|
_draggedItemsFromSelection = false;
|
|
CollectSnappedItems(item, DraggedItems);
|
|
}
|
|
|
|
internal bool IsItemDragged(MagGraphItem item) => DraggedItems.Contains(item);
|
|
|
|
internal double LastSnapTime = double.NegativeInfinity;
|
|
private Vector2 _lastSnapDragPositionOnCanvas;
|
|
|
|
/** for visual indication only */
|
|
internal Vector2 LastSnapTargetPositionOnCanvas;
|
|
|
|
internal readonly List<SpliceLink> SpliceSets = [];
|
|
|
|
private Vector2 _lastAppliedOffset;
|
|
private const float SnapThreshold = 30;
|
|
|
|
/// <summary>
|
|
/// Connections between dragged and non-dragged items
|
|
/// </summary>
|
|
private readonly List<MagGraphConnection> _connectionsToDraggedItems = [];
|
|
private readonly HashSet<int> _connectionToDragItemHashes = [];
|
|
private readonly HashSet<int> _snappedConnectionToDragItemHashes = [];
|
|
|
|
private bool _wasSnapped;
|
|
private static readonly MagItemMovement.Snapping _snapping = new();
|
|
|
|
private static bool _hasDragged;
|
|
private Vector2 _dragStartPosInOpOnCanvas;
|
|
|
|
private readonly ShakeDetector _shakeDetector = new();
|
|
|
|
internal readonly HashSet<MagGraphItem> DraggedItems = [];
|
|
private bool _draggedItemsFromSelection;
|
|
private List<ISelectableCanvasObject> _draggedSelectables = [];
|
|
|
|
/// <summary>
|
|
/// Defines a method how the currently dragged nodes could split some other snapped graph nodes
|
|
/// either horizontally or vertically. It provides information on how to snap, what type of
|
|
/// connection would be split and which inputs and outputs to splice in.
|
|
/// </summary>
|
|
/// <param name="DragPositionWithinBlock">The distance from top left position of InputItem (A) to where the mouse drag started.</param>
|
|
internal sealed record SpliceLink(
|
|
Guid InputItemId,
|
|
Guid InputId,
|
|
Guid OutputItemId,
|
|
Guid OutputId,
|
|
MagGraphItem.Directions Direction,
|
|
Type Type,
|
|
float Distance,
|
|
Vector2 DragPositionWithinBlock,
|
|
Vector2 AnchorOffset);
|
|
|
|
|
|
|
|
private readonly MagGraphView _view;
|
|
private readonly MagGraphLayout _layout;
|
|
private readonly NodeSelection _nodeSelection;
|
|
private const float SnapTolerance = 0.01f;
|
|
} |