Files
2026-07-13 13:13:17 +08:00

984 lines
43 KiB
C#

#nullable enable
using System.Diagnostics;
using System.Runtime.CompilerServices;
using T3.Core.Operator;
using T3.Core.Operator.Slots;
using T3.Editor.Gui.MagGraph.Interaction;
using T3.Editor.Gui.Interaction;
using T3.Editor.Gui.MagGraph.States;
using T3.Editor.Gui.OutputUi;
using T3.Editor.Gui.UiHelpers;
using T3.Editor.UiModel;
using T3.Editor.UiModel.InputsAndTypes;
// ReSharper disable MergeIntoPattern
// ReSharper disable ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
// ReSharper disable ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
// ReSharper disable SuggestBaseTypeForParameter
namespace T3.Editor.Gui.MagGraph.Model;
/// <summary>
/// Holds an intermediate view model that is updated if required. This view model
/// builds referenceable items, view elements, and connections. This makes it much easier to traverse the
/// graph without dictionary lookups. The layout also precomputes the visibility of input links, which simplifies
/// the layout and rendering of connection lines (one of the most complicated parts of the legacy layout).
/// Generate considerations:
/// - The layout model is a temporary data that is completely generated from the t3-ui data.
/// - Its computation is expensive and should only be done if required.
/// - This might be especially complicated if the state cannot be stored in the layout model, because it
/// relies on a temp. property like expanding a parameter type group.
///
/// It basically separates the following steps:
/// - generating the ui-model data (esp. elements, their inputs and connection slot indices)
/// - Updating the layouts
///
/// </summary>
internal sealed class MagGraphLayout
{
public readonly Dictionary<Guid, MagGraphItem> Items = new(127);
public readonly List<MagGraphConnection> MagConnections = new(127);
public readonly Dictionary<Guid, MagGraphSection> Sections = new(63);
/// <summary>
/// <see cref="Sections"/> ordered outermost-first, so nested sections draw on top of the
/// sections that contain them. Rebuilt on structural changes; iterate this when drawing.
/// </summary>
public readonly List<MagGraphSection> SectionsInDrawOrder = new(63);
public void ComputeLayout(GraphUiContext context, bool forceUpdate = false)
{
var compositionOp = context.CompositionInstance;
if (!SymbolUiRegistry.TryGetSymbolUi(compositionOp.Symbol.Id, out var parentSymbolUi))
return;
if (StructureFlaggedAsChanged)
{
// Only bump the display-cache version here: this also runs when merely checking out a
// composition, which must not mark it as modified (read-only symbols would prompt to
// save as copy). Actual edits flag the symbol through their commands.
parentSymbolUi.BumpVersionCounter();
}
if (forceUpdate || FrameStats.Last.UndoRedoTriggered || StructureFlaggedAsChanged ||
HasCompositionDataChanged(compositionOp.Symbol, ref _compositionModelHash))
RefreshDataStructure(context, parentSymbolUi);
// TODO: This only needs to be done, on structural changes or when items have been moved
UpdateConnectionLayout();
ComputeVerticalStackBoundaries(context.View);
}
public void FlagStructureAsChanged()
{
StructureFlaggedAsChanged = true;
}
private int _structureUpdateCycle;
private void RefreshDataStructure(GraphUiContext context, SymbolUi parentSymbolUi)
{
var composition = context.CompositionInstance;
_structureUpdateCycle++;
CollectItemReferences(composition, parentSymbolUi);
CollectedSections(parentSymbolUi);
SectionTree.UpdateOwnershipFromGeometry(parentSymbolUi);
SectionTree.UpdateCollapsedVisibility(parentSymbolUi);
RebuildSectionDrawOrder(parentSymbolUi);
UpdateConnectionSources(composition);
UpdateVisibleItemLines(context);
CollectConnectionReferences(composition);
StructureFlaggedAsChanged = false;
}
private void CollectedSections(SymbolUi compositionSymbolUi)
{
var addedCount = 0;
var updatedCount = 0;
foreach (var (sectionId, section) in compositionSymbolUi.Sections)
{
if (Sections.TryGetValue(sectionId, out var opItem))
{
updatedCount++;
opItem.Section = section; // instance can change through undo or reload
opItem.LastUpdateCycle = _structureUpdateCycle;
}
else
{
opItem = new MagGraphSection
{
Id = section.Id,
Section = section,
DampedPosOnCanvas = section.PosOnCanvas,
DampedSize = section.Size,
LastUpdateCycle = _structureUpdateCycle,
};
Sections[sectionId] = opItem;
addedCount++;
}
}
var hasObsoleteSections = Sections.Count > updatedCount + addedCount;
if (!hasObsoleteSections) return;
foreach (var a in Sections.Values)
{
if (a.LastUpdateCycle >= _structureUpdateCycle)
continue;
Sections.Remove(a.Id);
a.IsRemoved = true;
}
}
/// <summary>
/// Orders sections outermost-first by nesting depth so inner sections draw on top of their
/// containers. Runs after ownership is resolved, so <see cref="Section.ParentSectionId"/> is current.
/// </summary>
private void RebuildSectionDrawOrder(SymbolUi compositionSymbolUi)
{
SectionsInDrawOrder.Clear();
foreach (var magSection in Sections.Values)
{
magSection.NestingDepth = GetSectionDepth(compositionSymbolUi, magSection.Section);
SectionsInDrawOrder.Add(magSection);
}
SectionsInDrawOrder.Sort(static (a, b) => a.NestingDepth.CompareTo(b.NestingDepth));
}
private static int GetSectionDepth(SymbolUi compositionSymbolUi, Section section)
{
var depth = 0;
var parentId = section.ParentSectionId;
var remainingSteps = compositionSymbolUi.Sections.Count; // guards against parent-reference cycles
while (parentId != Guid.Empty && remainingSteps-- > 0)
{
if (!compositionSymbolUi.Sections.TryGetValue(parentId, out var parent))
break;
depth++;
parentId = parent.ParentSectionId;
}
return depth;
}
/// <remarks>
/// This method is extremely slow for large compositions...
/// </remarks>
private void CollectItemReferences(Instance compositionOp, SymbolUi compositionSymbolUi)
{
//Items.Clear();
var addedItemCount = 0;
var updatedItemCount = 0;
foreach (var (childId, childInstance) in compositionOp.Children)
{
if (!compositionSymbolUi.ChildUis.TryGetValue(childId, out var childUi))
continue;
if (!SymbolUiRegistry.TryGetSymbolUi(childInstance.Symbol.Id, out var symbolUi))
continue;
if (Items.TryGetValue(childId, out var opItem))
{
opItem.ResetConnections(_structureUpdateCycle);
updatedItemCount++;
}
else
{
opItem = new MagGraphItem
{
// Variant = MagGraphItem.Variants.Operator,
Id = childId,
// Instance = childInstance,
Selectable = childUi,
SymbolChild = childInstance.SymbolChild,
ChildUi = childUi,
DampedPosOnCanvas = childUi.PosOnCanvas,
InstancePath = childInstance.InstancePath,
// SymbolUi = symbolUi,
// SymbolChild = childUi.SymbolChild,
// ChildUi = childUi,
// Size = MagGraphItem.GridSize,
// LastUpdateCycle = _structureUpdateCycle,
// DampedPosOnCanvas = childUi.PosOnCanvas,
};
Items[childId] = opItem;
addedItemCount++;
}
opItem.Variant = MagGraphItem.Variants.Operator;
//opItem.Id = childId;
opItem.InstancePath = childInstance.InstancePath;
opItem.Selectable = childUi;
opItem.SymbolUi = symbolUi;
opItem.SymbolChild = childUi.SymbolChild;
opItem.ChildUi = childUi;
opItem.Size = MagGraphItem.GridSize;
opItem.LastUpdateCycle = _structureUpdateCycle;
}
foreach (var input in compositionOp.Inputs)
{
var inputUi = compositionSymbolUi.InputUis[input.Id];
if (Items.TryGetValue(input.Id, out var inputOp))
{
inputOp.ResetConnections(_structureUpdateCycle);
// Refresh references so renames (same id, new definition) are picked up
inputOp.Selectable = inputUi;
inputOp.SymbolChild = compositionOp.SymbolChild;
inputOp.InstancePath = compositionOp.InstancePath;
updatedItemCount++;
}
else
{
Items[input.Id] = new MagGraphItem
{
Variant = MagGraphItem.Variants.Input,
Id = input.Id,
SymbolChild = compositionOp.SymbolChild,
InstancePath = compositionOp.InstancePath,
Selectable = inputUi,
Size = MagGraphItem.GridSize,
DampedPosOnCanvas = inputUi.PosOnCanvas,
LastUpdateCycle = _structureUpdateCycle,
};
addedItemCount++;
}
}
foreach (var output in compositionOp.Outputs)
{
var outputUi = compositionSymbolUi.OutputUis[output.Id];
if (Items.TryGetValue(output.Id, out var outputOp))
{
outputOp.ResetConnections(_structureUpdateCycle);
// Refresh references so renames (same id, new definition) are picked up
outputOp.Selectable = outputUi;
outputOp.SymbolChild = compositionOp.SymbolChild;
outputOp.InstancePath = compositionOp.InstancePath;
updatedItemCount++;
}
else
{
Items[output.Id] = new MagGraphItem
{
Variant = MagGraphItem.Variants.Output,
Id = output.Id,
SymbolChild = compositionOp.SymbolChild,
InstancePath = compositionOp.InstancePath,
Selectable = outputUi,
Size = MagGraphItem.GridSize,
DampedPosOnCanvas = outputUi.PosOnCanvas,
};
addedItemCount++;
}
}
if (Items.TryGetValue(PlaceholderCreation.PlaceHolderId, out var placeholderOp))
{
placeholderOp.ResetConnections(_structureUpdateCycle);
updatedItemCount++;
}
var hasObsoleteItems = Items.Count > updatedItemCount + addedItemCount;
if (hasObsoleteItems)
{
foreach (var item in Items.Values)
{
if (item.LastUpdateCycle >= _structureUpdateCycle)
continue;
Items.Remove(item.Id);
item.Variant = MagGraphItem.Variants.Obsolete;
}
}
}
private readonly HashSet<int> _connectedOutputs = new(100);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetConnectionSourceHash(Symbol.Connection c)
{
var hash = c.SourceSlotId.GetHashCode();
hash = hash * 31 + c.SourceParentOrChildId.GetHashCode();
return hash;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetHashCodeForOutputSlot(ISlot output)
{
var hash = output.Id.GetHashCode();
hash = hash * 31 + output.Parent.SymbolChildId.GetHashCode();
return hash;
}
/// <summary>
/// Sadly there is no obvious easy method to store if an output has a connection.
/// So we collect connected outputs in a hashset.
/// </summary>
private void UpdateConnectionSources(Instance composition)
{
_connectedOutputs.Clear();
foreach (var c in composition.Symbol.Connections)
{
if (c.IsConnectedToSymbolInput)
continue;
_connectedOutputs.Add(GetConnectionSourceHash(c));
}
}
private void UpdateVisibleItemLines(GraphUiContext context)
{
var inputLines = new List<MagGraphItem.InputLine>(8);
var outputLines = new List<MagGraphItem.OutputLine>(4);
foreach (var item in Items.Values)
{
inputLines.Clear();
outputLines.Clear();
// if (item.IsCollapsedAway)
// continue;
var visibleIndex = 0;
switch (item.Variant)
{
// Collect inputs
case MagGraphItem.Variants.Operator:
{
visibleIndex = CollectVisibleLines(context, item, inputLines, outputLines, _connectedOutputs);
break;
}
case MagGraphItem.Variants.Input:
{
Debug.Assert(item.Selectable is IInputUi);
var parentInstance = item.Instance;
Debug.Assert(parentInstance != null);
var parentInput = parentInstance.Inputs.FirstOrDefault(i => i.Id == item.Id);
outputLines.Add(new MagGraphItem.OutputLine
{
Output = parentInput!, // This looks confusing but is correct
Id = parentInput!.Id,
OutputUi = null,
OutputIndex = 0,
VisibleIndex = 0,
ConnectionsOut = [],
});
item.PrimaryType = parentInput.ValueType;
break;
}
case MagGraphItem.Variants.Output:
{
Debug.Assert(item.Selectable is IOutputUi);
var parentInstance = item.Instance;
Debug.Assert(parentInstance != null);
var parentOutput = parentInstance.Outputs.FirstOrDefault(o => o.Id == item.Id);
Debug.Assert(parentOutput != null);
inputLines.Add(new MagGraphItem.InputLine
{
Type = parentOutput.ValueType,
Id = parentOutput.Id,
Input = parentOutput,
MultiInputIndex = 0,
VisibleIndex = 0,
});
item.PrimaryType = parentOutput.ValueType;
break;
}
case MagGraphItem.Variants.Placeholder:
break;
default:
throw new ArgumentOutOfRangeException();
}
item.InputLines = inputLines.ToArray();
item.OutputLines = outputLines.ToArray();
//var count = Math.Max(1, item.InputLines.Count + item.OutputLines.Count -2);
item.Size = new Vector2(MagGraphItem.Width, MagGraphItem.LineHeight * (Math.Max(1, visibleIndex)));
}
}
/// <summary>
/// Aggregate visible inputs and outputs into a consistent stack
/// </summary>
/// <remarks>
/// This is accessible because for some use-cases we need to compute the height of inserted items.
/// </remarks>
internal static int CollectVisibleLines(GraphUiContext context, MagGraphItem item, List<MagGraphItem.InputLine> inputLines,
List<MagGraphItem.OutputLine> outputLines,
HashSet<int>? connectedOutputs = null)
{
var instance = item.Instance;
Debug.Assert(instance != null && item.SymbolUi != null);
int visibleIndex = 0;
item.HasHiddenOutputs = false;
for (var inputLineIndex = 0; inputLineIndex < instance.Inputs.Count; inputLineIndex++)
{
var input = instance.Inputs[inputLineIndex];
if (!item.SymbolUi.InputUis.TryGetValue(input.Id, out var inputUi))
{
Log.Warning("Can't find input? " + input.Id);
continue;
}
var isRelevant = inputUi.Relevancy is Relevancy.Relevant or Relevancy.Required;
//var isMatchingType = false; //input.ValueType == typeof(float);//ConnectionTargetType;
var isPrimaryInput = inputLineIndex == 0;
if (input.IsMultiInput && input is IMultiInputSlot)
{
var shouldInputBeVisible = isRelevant || isPrimaryInput || input.HasInputConnections;
var connectionsToInput = context
.CompositionInstance
.Symbol
.Connections
.FindAll(c => c.TargetParentOrChildId == item.Id
&& c.TargetSlotId == input.Id);
var multiConIndex = 0;
var virtualConnectionCount = 0; // including disconnected
var tempConnectionCount = 0; // count additional to connectionsToInput.count
for (var virtualSubIndex = 0; virtualSubIndex < connectionsToInput.Count + tempConnectionCount + 1; virtualSubIndex++)
{
if (IsDisconnectedVisibleMultiInputLine(context, item.Id, input.Id, virtualConnectionCount))
{
inputLines.Add(new MagGraphItem.InputLine
{
Id = input.Id,
Type = input.ValueType,
Input = input,
InputUi = inputUi,
VisibleIndex = visibleIndex,
MultiInputIndex = multiConIndex,
ConnectionState = MagGraphItem.InputLineStates.TempConnection,
});
visibleIndex++;
virtualConnectionCount++;
tempConnectionCount++;
continue;
}
if (shouldInputBeVisible && multiConIndex < connectionsToInput.Count)
{
inputLines.Add(new MagGraphItem.InputLine
{
Id = input.Id,
Type = input.ValueType,
Input = input,
InputUi = inputUi,
VisibleIndex = visibleIndex,
MultiInputIndex = multiConIndex,
ConnectionState = MagGraphItem.InputLineStates.Connected,
});
virtualConnectionCount++;
visibleIndex++;
multiConIndex++;
}
}
// Show input even it not connected
if (shouldInputBeVisible && virtualConnectionCount == 0)
{
inputLines.Add(new MagGraphItem.InputLine
{
Id = input.Id,
Type = input.ValueType,
Input = input,
InputUi = inputUi,
VisibleIndex = visibleIndex,
MultiInputIndex = multiConIndex,
ConnectionState = MagGraphItem.InputLineStates.NotConnected,
});
visibleIndex++;
}
}
else
{
var hasInputConnections = input.HasInputConnections;
if (isRelevant || isPrimaryInput || hasInputConnections)
{
inputLines.Add(new MagGraphItem.InputLine
{
Id = input.Id,
Type = input.ValueType,
Input = input,
InputUi = inputUi,
VisibleIndex = visibleIndex,
ConnectionState = hasInputConnections
? MagGraphItem.InputLineStates.Connected
: MagGraphItem.InputLineStates.NotConnected,
});
visibleIndex++;
continue;
}
var hasTempConnection = context.DisconnectedInputHashes.Count > 0
&& context.DisconnectedInputHashes.Contains(MagGraphConnection.GetItemInputHash(item.Id, input.Id, 0));
if (hasTempConnection)
{
inputLines.Add(new MagGraphItem.InputLine
{
Id = input.Id,
Type = input.ValueType,
Input = input,
InputUi = inputUi,
VisibleIndex = visibleIndex,
ConnectionState = MagGraphItem.InputLineStates.TempConnection,
});
visibleIndex++;
}
}
}
var hasNoInputs = visibleIndex == 0;
// Collect outputs
for (var outputIndex = 0; outputIndex < instance.Outputs.Count; outputIndex++)
{
var output = instance.Outputs[outputIndex];
if (!item.SymbolUi.OutputUis.TryGetValue(output.Id, out var outputUi))
{
Log.Warning("Can't find outputUi:" + output.Id);
continue;
}
// Should non connected secondary outputs be visible?
int outputHash2 = GetHashCodeForOutputSlot(output);
var isConnected = connectedOutputs != null && connectedOutputs.Contains(outputHash2);
if (outputIndex > 0 && !isConnected)
{
item.HasHiddenOutputs = true;
continue;
}
outputLines.Add(new MagGraphItem.OutputLine
{
Id = outputUi.Id,
Output = output,
OutputUi = outputUi,
OutputIndex = outputIndex,
VisibleIndex = outputIndex == 0 ? 0 : visibleIndex,
ConnectionsOut = [],
});
if (outputIndex == 0)
{
item.PrimaryType = output.ValueType;
if (hasNoInputs)
visibleIndex++;
}
else
{
visibleIndex++;
}
}
// Fix height
return visibleIndex;
}
private static bool IsDisconnectedVisibleMultiInputLine(GraphUiContext context, Guid itemId, Guid inputId,
int multiInputIndex)
{
var itemInputHash = MagGraphConnection.GetItemInputHash(itemId, inputId, multiInputIndex);
var isDisconnectedVisibleMultiInputLine =
context.DisconnectedInputHashes.Count > 0
&& context.DisconnectedInputHashes.Contains(itemInputHash);
return isDisconnectedVisibleMultiInputLine;
}
private void CollectConnectionReferences(Instance composition)
{
MagConnections.Clear();
MagConnections.Capacity = composition.Symbol.Connections.Count;
Symbol.Connection c; // Avoid repetitive closure capture
for (var cIndex = 0; cIndex < composition.Symbol.Connections.Count; cIndex++)
{
c = composition.Symbol.Connections[cIndex];
if (c.IsConnectedToSymbolInput)
{
if (!Items.TryGetValue(c.TargetParentOrChildId, out var targetFromInputItem)
|| !Items.TryGetValue(c.SourceSlotId, out var symbolInputItem))
continue;
Debug.Assert(targetFromInputItem.Instance != null);
var symbolInput = composition.Inputs.FirstOrDefault(i => i.Id == c.SourceSlotId);
var targetInput = targetFromInputItem.Instance.Inputs.FirstOrDefault(i => i.Input.InputDefinition.Id == c.TargetSlotId);
Debug.Assert(targetInput != null);
FindVisibleIndex(targetFromInputItem, targetInput, out var targetInputIndex, out var targetMultiInputIndex);
var connectionFromSymbolInput = new MagGraphConnection
{
Style = MagGraphConnection.ConnectionStyles.Unknown,
SourceItem = symbolInputItem,
SourceOutput = symbolInput,
TargetItem = targetFromInputItem,
//TargetInput = targetInput,
InputLineIndex = targetInputIndex,
OutputLineIndex = 0,
ConnectionHash = c.GetHashCode(),
MultiInputIndex = targetMultiInputIndex,
VisibleOutputIndex = 0,
};
symbolInputItem.OutputLines[0].ConnectionsOut.Add(connectionFromSymbolInput);
targetFromInputItem.InputLines[targetInputIndex].ConnectionIn = connectionFromSymbolInput;
MagConnections.Add(connectionFromSymbolInput);
continue;
}
if (c.IsConnectedToSymbolOutput)
{
if (!Items.TryGetValue(c.SourceParentOrChildId, out var sourceItem2)
|| !Items.TryGetValue(c.TargetSlotId, out var symbolOutputItem))
{
Log.Warning("Inconsistent output connection " + c);
continue;
}
Debug.Assert(sourceItem2.Instance != null);
//var symbolOutput = composition.Outputs.FirstOrDefault((o => o.Id == c.TargetSlotId));
var sourceOutput = sourceItem2.Instance.Outputs.FirstOrDefault(o => o.Id == c.SourceSlotId);
Debug.Assert(sourceOutput != null);
// Find the position within OutputLines (not OutputLine.OutputIndex, which indexes
// instance.Outputs and diverges once hidden outputs are skipped)
int outputLineIndex2;
for (outputLineIndex2 = 0; outputLineIndex2 < sourceItem2.OutputLines.Length; outputLineIndex2++)
{
if (sourceItem2.OutputLines[outputLineIndex2].Output == sourceOutput)
break;
}
if (outputLineIndex2 >= sourceItem2.OutputLines.Length)
{
Log.Warning($"Can't find output line for connection to symbol output in {sourceItem2}");
outputLineIndex2 = sourceItem2.OutputLines.Length - 1;
}
var visibleOutputIndex = sourceItem2.OutputLines[outputLineIndex2].VisibleIndex;
var connectionToSymbolOutput = new MagGraphConnection
{
Style = MagGraphConnection.ConnectionStyles.Unknown,
SourceItem = sourceItem2,
SourceOutput = sourceOutput,
TargetItem = symbolOutputItem,
//TargetInput = targetInput,
InputLineIndex = 0,
OutputLineIndex = outputLineIndex2,
ConnectionHash = c.GetHashCode(),
MultiInputIndex = 0,
VisibleOutputIndex = visibleOutputIndex,
};
sourceItem2.OutputLines[outputLineIndex2].ConnectionsOut.Add(connectionToSymbolOutput);
symbolOutputItem.InputLines[0].ConnectionIn = connectionToSymbolOutput;
MagConnections.Add(connectionToSymbolOutput);
continue;
}
// Skip connection to symbol inputs and outputs for now
if (c.IsConnectedToSymbolOutput
|| !Items.TryGetValue(c.SourceParentOrChildId, out var sourceItem)
|| !Items.TryGetValue(c.TargetParentOrChildId, out var targetItem)
)
{
//Log.Warning("Can't find items for connection line");
continue;
}
if (sourceItem.IsCollapsedAway && targetItem.IsCollapsedAway)
continue;
// Connections between nodes
if (sourceItem.Instance == null || targetItem.Instance == null)
continue;
var output = sourceItem.Variant == MagGraphItem.Variants.Input
? sourceItem.Instance.Inputs.FirstOrDefault(o => o.Id == c.SourceSlotId)
: sourceItem.Instance.Outputs.FirstOrDefault(o => o.Id == c.SourceSlotId);
var input = targetItem.Instance.Inputs.FirstOrDefault(i => i.Input.InputDefinition.Id == c.TargetSlotId);
if (output == null || input == null)
{
Log.Warning("Unable to find connected items?");
continue;
}
FindVisibleIndex(targetItem, input, out var inputLineIndex, out var multiInputIndex2);
int outputLineIndex;
for (outputLineIndex = 0; outputLineIndex < sourceItem.OutputLines.Length; outputLineIndex++)
{
if (sourceItem.OutputLines[outputLineIndex].Output == output)
break;
}
if (outputLineIndex >= sourceItem.OutputLines.Length)
{
Log.Warning($"OutputIndex {outputLineIndex} exceeds number of output lines {sourceItem.OutputLines.Length} in {sourceItem}");
outputLineIndex = sourceItem.OutputLines.Length - 1;
}
var snapGraphConnection = new MagGraphConnection
{
Style = MagGraphConnection.ConnectionStyles.Unknown,
SourceItem = sourceItem,
SourceOutput = output,
TargetItem = targetItem,
InputLineIndex = inputLineIndex,
OutputLineIndex = outputLineIndex,
ConnectionHash = c.GetHashCode(),
MultiInputIndex = multiInputIndex2,
VisibleOutputIndex = sourceItem.OutputLines[outputLineIndex].VisibleIndex,
};
var valid = inputLineIndex < targetItem.InputLines.Length
&& outputLineIndex < sourceItem.OutputLines.Length;
if (!valid)
{
Log.Warning($"Input line index {inputLineIndex} ouf of range?");
continue;
}
targetItem.InputLines[inputLineIndex].ConnectionIn = snapGraphConnection;
sourceItem.OutputLines[outputLineIndex].ConnectionsOut.Add(snapGraphConnection);
MagConnections.Add(snapGraphConnection);
}
}
/// <summary>
/// This method needs to be rethought and cleaned up.
/// MultiInputIndex will always return max count?
/// </summary>
private static void FindVisibleIndex(MagGraphItem targetItem, IInputSlot input, out int visibleInputIndex, out int multiInputIndex)
{
// Find connected index
multiInputIndex = 0;
for (visibleInputIndex = 0; visibleInputIndex < targetItem.InputLines.Length; visibleInputIndex++)
{
// Count other inputs
var targetItemInputLine = targetItem.InputLines[visibleInputIndex];
if (targetItemInputLine.Id != input.Id)
continue;
// Skip temp connections
if (targetItemInputLine.ConnectionState == MagGraphItem.InputLineStates.TempConnection)
continue;
// Skip already connected multi-inputs slots...
// (This assumes ConnectionIn to be nullified before using this)
if (targetItemInputLine.ConnectionIn != null && visibleInputIndex < targetItem.InputLines.Length)
{
multiInputIndex++;
continue;
}
// Is a match
return;
}
Log.Warning("Input line not found?");
}
// Reuse list to avoid allocations
private static readonly List<MagGraphItem> _listStackedItems = new(32);
/// <summary>
/// This improves the layout of arc connections inputs into multiple stacked ops so they
/// avoid overlap.
/// </summary>
private void ComputeVerticalStackBoundaries(ScalableCanvas canvas)
{
MagGraphItem? previousItem = null;
_listStackedItems.Clear();
foreach (var item in Items.Values.OrderBy(i => MathF.Round(i.PosOnCanvas.X)).ThenBy(i => i.PosOnCanvas.Y))
{
item.VerticalStackArea = item.Area;
if (previousItem == null)
{
_listStackedItems.Clear();
_listStackedItems.Add(item);
previousItem = item;
continue;
}
// is stacked?
if (!(Math.Abs(item.PosOnCanvas.X - previousItem.PosOnCanvas.X) < 10f)
|| !(Math.Abs(item.PosOnCanvas.Y - previousItem.Area.Max.Y) < 180f))
{
ApplyStackToItems();
}
_listStackedItems.Add(item);
previousItem = item;
}
ApplyStackToItems();
return;
void ApplyStackToItems()
{
if (_listStackedItems.Count > 1)
{
var stackArea = new ImRect(_listStackedItems[0].PosOnCanvas,
_listStackedItems[^1].Area.Max);
foreach (var x in _listStackedItems)
{
x.VerticalStackArea = stackArea;
}
// Draw Debug
// var aOnScreen = canvas.TransformRect(stackArea);
// ImGui.GetWindowDrawList().AddRect(aOnScreen.Min, aOnScreen.Max, Color.Green);
}
_listStackedItems.Clear();
}
}
/// <summary>
/// Update the layout of the connections (e.g. if they are snapped, of flowing vertically or horizontally)
/// </summary>
private void UpdateConnectionLayout()
{
foreach (var sc in MagConnections)
{
var sourceMin = sc.SourceItem.PosOnCanvas;
var sourceMax = sourceMin + sc.SourceItem.Size;
var targetMin = sc.TargetItem.PosOnCanvas;
// Snapped horizontally
if (
MathF.Abs(sourceMax.X - targetMin.X) < 1
&& MathF.Abs((sourceMin.Y + sc.VisibleOutputIndex * MagGraphItem.GridSize.Y)
- (targetMin.Y + sc.InputLineIndex * MagGraphItem.GridSize.Y)) < 1)
{
sc.Style = sc.InputLineIndex == 0
? MagGraphConnection.ConnectionStyles.MainOutToMainInSnappedHorizontal
: MagGraphConnection.ConnectionStyles.MainOutToInputSnappedHorizontal;
var p = new Vector2(sourceMax.X, sourceMin.Y + (+sc.VisibleOutputIndex + 0.5f) * MagGraphItem.GridSize.Y);
sc.SourcePos = p;
sc.TargetPos = p;
continue;
}
if (sc.InputLineIndex == 0
&& sc.OutputLineIndex == 0
&& MathF.Abs(sourceMin.X - targetMin.X) < 1
&& MathF.Abs(sourceMax.Y - targetMin.Y) < 1)
{
sc.Style = MagGraphConnection.ConnectionStyles.MainOutToMainInSnappedVertical;
var p = new Vector2(sourceMin.X + MagGraphItem.GridSize.X / 2, targetMin.Y);
sc.SourcePos = p;
sc.TargetPos = p;
continue;
}
if (sc.OutputLineIndex == 0
&& sc.InputLineIndex > 0
&& MathF.Abs(sourceMax.X - targetMin.X) < 1
&& MathF.Abs(sourceMin.Y - (targetMin.Y + sc.VisibleOutputIndex * MagGraphItem.GridSize.Y)) < 1
)
{
//sc.Style = MagGraphConnection.ConnectionStyles.MainOutToInputSnappedHorizontal;
sc.Style = MagGraphConnection.ConnectionStyles.BottomToLeft;
var p = new Vector2(sourceMax.X, targetMin.Y + (0.5f + sc.InputLineIndex) * MagGraphItem.GridSize.Y);
sc.SourcePos = new Vector2((sourceMin.X + sourceMax.X) / 2, sourceMax.Y);
sc.TargetPos = p;
continue;
}
if (sc.OutputLineIndex > 0
&& sc.InputLineIndex == 0
&& MathF.Abs(sourceMax.X - targetMin.Y) < 1
&& MathF.Abs(sourceMax.Y + (1 + sc.SourceItem.OutputLines.Length + sc.VisibleOutputIndex) * MagGraphItem.GridSize.Y) < 1)
{
sc.Style = MagGraphConnection.ConnectionStyles.AdditionalOutToMainInputSnappedVertical;
var p = new Vector2(sourceMax.X, targetMin.Y + 0.5f * MagGraphItem.GridSize.Y);
sc.SourcePos = p;
sc.TargetPos = p;
continue;
}
if (sc.OutputLineIndex == 0
&& sc.InputLineIndex == 0
&& sourceMax.Y < targetMin.Y
//&& MathF.Abs(sourceMin.X - targetMin.X) < MagGraphItem.GridSize.X / 2
&& sourceMin.X > targetMin.X - MagGraphItem.GridSize.X / 2
)
{
sc.SourcePos = new Vector2(sourceMin.X + MagGraphItem.GridSize.X / 2, sourceMax.Y);
sc.TargetPos = new Vector2(targetMin.X + MagGraphItem.GridSize.X / 2, targetMin.Y);
sc.Style = MagGraphConnection.ConnectionStyles.BottomToTop;
continue;
}
sc.SourcePos = new Vector2(sourceMax.X, sourceMin.Y + (sc.VisibleOutputIndex + 0.5f) * MagGraphItem.GridSize.Y);
sc.TargetPos = new Vector2(targetMin.X, targetMin.Y + (sc.InputLineIndex + 0.5f) * MagGraphItem.GridSize.Y);
sc.Style = MagGraphConnection.ConnectionStyles.RightToLeft;
//TODO: Snapped from output
//TODO: Snapped to input
//TODO: Snapped vertically
}
}
/// <summary>
/// We rely on manually flagging structure changes, because
/// computing a hash of a composition is not easy because the item order of children can change...
/// </summary>
private static bool HasCompositionDataChanged(Symbol composition, ref int originalHash)
{
var newHash = composition.Id.GetHashCode();
if (newHash == originalHash)
return false;
originalHash = newHash;
return true;
}
private int _compositionModelHash;
private bool StructureFlaggedAsChanged { get; set; }
}