#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using ImGuiNET; using T3.Core.Operator; using T3.Core.Operator.Interfaces; using T3.Editor.Gui.Interaction.TransformGizmos; using T3.Editor.Gui.UiHelpers; using T3.Editor.UiModel.InputsAndTypes; using T3.Editor.UiModel.ProjectHandling; namespace T3.Editor.UiModel.Selection; /// /// Some notes on selection handling: /// /// - We can't store references to instances because this leads to null-references and reference leaks. For this /// reason we save id-paths: Basically the nesting path of instances from the dashboard operator down. /// /// - Frequently we want to select the parent operator when clicking on the background of a composition (e.g. to /// show its parameters. /// internal sealed class NodeSelection : ISelection { public NodeSelection(NavigationHistory navigationHistory, Structure structure) { NavigationHistory = navigationHistory; _structure = structure; } //public readonly HashSet HoveredIds = []; public readonly HashSet PinnedIds = []; public void Clear() { TransformGizmoHandling.ClearSelectedTransformables(); Selection.Clear(); ChangeCounter++; } /// /// This called when clicking on background /// public void SetSelectionToComposition(Instance instance) { NavigationHistory.UpdateSelectedInstance(instance); Clear(); _childUiInstanceIdPaths.Clear(); _selectedCompositionPath = instance.InstancePath; ChangeCounter++; } /// /// Replaces the current selection with the given node /// /// /// required if the object is a public void SetSelection(ISelectableCanvasObject node, Instance? instance = null) { Clear(); AddSelection(node, instance); if (node is SymbolUi.Child) { Debug.Assert(instance != null); NavigationHistory.UpdateSelectedInstance(instance); } ChangeCounter++; } public void AddSelection(ISelectableCanvasObject node, Instance? instance = null) { if (Selection.Contains(node)) return; if (node is SymbolUi.Child childUi) { ArgumentNullException.ThrowIfNull(instance); _childUiInstanceIdPaths[childUi] = instance.InstancePath; if (instance is ITransformable transformable) { TransformGizmoHandling.RegisterSelectedTransformable(childUi, transformable); } } Selection.Add(node); ChangeCounter++; } public bool TrySelectCompositionChild(Instance compositionOp, Guid id, bool add=true) { if (!Structure.TryGetUiAndInstanceInComposition(id, compositionOp, out var childUi, out var instance)) return false; if(!add) Clear(); AddSelection(childUi, instance); return true; } public IEnumerable GetSelectedNodes() where T : ISelectableCanvasObject { foreach (var item in Selection) { if (item is T typedItem) yield return typedItem; } } public bool IsNodeSelected(ISelectableCanvasObject node) => Selection.Contains(node); public bool IsAnythingSelected() => Selection.Count > 0; /// /// Returns null if more than one /// public Instance? GetSelectedInstanceWithoutComposition() { if (Selection.Count != 1) return null; var selection = GetFirstSelectedInstance(); // Clear invalid or obsolete selection //if(selection==null) // Clear(); return selection == _structure.GetInstanceFromIdPath(_selectedCompositionPath) ? null : selection; } public Instance? GetFirstSelectedInstance() { if (Selection.Count == 0) return _structure.GetInstanceFromIdPath(_selectedCompositionPath); if (Selection[0] is SymbolUi.Child firstNode) { if (!_childUiInstanceIdPaths.TryGetValue(firstNode, out var idPath)) { Log.Error("Failed to access id-path of selected childUi " + firstNode.SymbolChild.Name); //Clear(); return null; } return _structure.GetInstanceFromIdPath(idPath); } return null; } public IEnumerable GetSelectedChildUis() => GetSelectedNodes(); public IEnumerable GetSelectedInstances() { return GetSelectedNodes() .Where(x => _childUiInstanceIdPaths.ContainsKey(x)) .Select(symbolChildUi => { var idPath = _childUiInstanceIdPaths[symbolChildUi]; return _structure.GetInstanceFromIdPath(idPath)!; }); } /// /// Returns null if there are other selections /// public Instance? GetSelectedComposition() => Selection.Count > 0 ? null : _structure.GetInstanceFromIdPath(_selectedCompositionPath); public Guid? GetSelectionSymbolChildId() { if (_selectedCompositionPath == null) return null; if (Selection.Count == 0) return _selectedCompositionPath[^1]; if (Selection[0] is not SymbolUi.Child firstNode) return null; return firstNode.SymbolChild.Id; } public void DeselectCompositionChild(Instance compositionOp, Guid symbolChildId) { if (!Structure.TryGetUiAndInstanceInComposition(symbolChildId, compositionOp, out var childUi, out var instance)) return; DeselectNode(childUi, instance); } public void DeselectNode(ISelectableCanvasObject node, Instance? instance = null) { Selection.Remove(node); if (instance is ITransformable transformable) { TransformGizmoHandling.ClearDeselectedTransformableNode(transformable); } ChangeCounter++; } public Instance? GetInstanceForChildUi(SymbolUi.Child symbolChildUi) { var idPath = _childUiInstanceIdPaths[symbolChildUi]; return _structure.GetInstanceFromIdPath(idPath); } public bool IsSelected(ISelectableCanvasObject item) { // Avoid linq foreach (var i in Selection) { if (i.Id == item.Id) return true; } return false; } public static IEnumerable GetSelectableChildren(Instance compositionOp) { List selectableItems = []; selectableItems.Clear(); try { var symbolUi = compositionOp.GetSymbolUi(); var selectableCanvasObjects = compositionOp.Children.Values.Select(x => x.GetChildUi()); selectableItems.AddRange(selectableCanvasObjects); selectableItems.AddRange(symbolUi.InputUis.Values); selectableItems.AddRange(symbolUi.OutputUis.Values); selectableItems.AddRange(symbolUi.Sections.Values); } catch (Exception e) { Log.Error($"Failed to get selected children. This might be caused be recompilation. {e.Message} Please report"); } return selectableItems; } internal static ImRect GetSelectionBounds(NodeSelection nodeSelection, Instance compositionOp, float padding = 50) { var selectedOrAll = nodeSelection.IsAnythingSelected() ? nodeSelection.GetSelectedNodes().ToArray() : NodeSelection.GetSelectableChildren(compositionOp).ToArray(); if (selectedOrAll.Length == 0) return new ImRect(); var firstElement = selectedOrAll[0]; if(firstElement == null) return new ImRect(); var bounds = new ImRect(firstElement.PosOnCanvas, firstElement.PosOnCanvas + firstElement.Size); foreach (var element in selectedOrAll) { if (element == null) continue; if (element is SymbolUi.Child item && item.IsHiddenInCollapsedSection) continue; if (element is Section section && section.Collapsed) { bounds.Add(element.PosOnCanvas); bounds.Add(element.PosOnCanvas + new Vector2(element.Size.X,10)); continue; } if (float.IsInfinity(element.PosOnCanvas.X) || float.IsInfinity(element.PosOnCanvas.Y)) element.PosOnCanvas = Vector2.Zero; bounds.Add(element.PosOnCanvas); bounds.Add(element.PosOnCanvas + element.Size); } bounds.Expand(padding); return bounds; } public static bool TryGetSelectedInstanceOrInput([NotNullWhen(true)] out Instance? instance, out IInputUi? inputUi, out bool selectionChanged) { selectionChanged = false; inputUi = null; instance = null; Guid id; var projectView = ProjectView.Focused; var nodeSelection = projectView?.NodeSelection; if (nodeSelection == null) return false; instance = nodeSelection.GetSelectedInstanceWithoutComposition() ?? nodeSelection.GetSelectedComposition(); if (instance == null) { var selectedInputs = nodeSelection.GetSelectedNodes().ToList(); if (selectedInputs.Count == 0) return false; instance = projectView?.CompositionInstance; if (instance == null) return false; inputUi = selectedInputs[0]; id = inputUi.Id; } else { id = instance.SymbolChildId; } selectionChanged = id != _lastSelectionId; if (ImGui.GetFrameCount() == _lastFrameCount) return true; _lastFrameCount = ImGui.GetFrameCount(); _lastSelectionId = id; return true; } public readonly NavigationHistory NavigationHistory; private readonly Structure _structure; //TODO: This should be a dict because selecting many (> 500) ops will lead to framedrops public readonly List Selection = []; private IReadOnlyList? _selectedCompositionPath; private readonly Dictionary> _childUiInstanceIdPaths = new(); public int ChangeCounter { get; private set; } private static int _lastFrameCount; private static Guid _lastSelectionId = Guid.Empty; public static void InvalidateSelectedOpsForTransformGizmo(NodeSelection nodeSelection) { // Keep invalidating the selected op to enforce rendering of Transform gizmo foreach (var si in nodeSelection.GetSelectedInstances().ToList()) { if (si is not ITransformable) continue; foreach (var i in si.Inputs) { // Skip string inputs to prevent potential interference with resource file paths hooks // I.e. Invalidating these every frame breaks shader recompiling if Shader-op is selected if (i.ValueType != typeof(Vector3)) { continue; } i.DirtyFlag.ForceInvalidate(); } } } }