From b608dcd8f809cf0144511e6bcf1779f9095d4391 Mon Sep 17 00:00:00 2001 From: VoidX Date: Mon, 20 May 2024 22:27:02 +0200 Subject: [PATCH] More graph utils --- .../Filters/Utilities/FilterGraphNodeUtils.cs | 40 +++++++++++++ .../FilterStudio/Graphs/ManipulatableGraph.cs | 19 ++++++ .../FilterStudio/MainWindow.Graph.cs | 59 ++++++++++++++++++- CavernSamples/FilterStudio/MainWindow.xaml | 5 +- CavernSamples/FilterStudio/MainWindow.xaml.cs | 19 +----- .../Resources/MainWindowStrings.hu-HU.xaml | 4 ++ .../Resources/MainWindowStrings.xaml | 4 ++ 7 files changed, 130 insertions(+), 20 deletions(-) diff --git a/Cavern/Filters/Utilities/FilterGraphNodeUtils.cs b/Cavern/Filters/Utilities/FilterGraphNodeUtils.cs index bc75ec4d..4449a929 100644 --- a/Cavern/Filters/Utilities/FilterGraphNodeUtils.cs +++ b/Cavern/Filters/Utilities/FilterGraphNodeUtils.cs @@ -5,9 +5,27 @@ namespace Cavern.Filters.Utilities { /// Special functions for handling s. /// public static class FilterGraphNodeUtils { + /// + /// Check if the graph has cycles. + /// + /// All nodes which have no parents + public static bool HasCycles(IEnumerable rootNodes) { + HashSet visited = new HashSet(), + inProgress = new HashSet(); + foreach (FilterGraphNode node in rootNodes) { + if (!visited.Contains(node)) { + if (HasCycles(node, visited, inProgress)) { + return true; + } + } + } + return false; + } + /// /// Get all nodes in a filter graph knowing the root nodes. /// + /// All nodes which have no parents public static HashSet MapGraph(IEnumerable rootNodes) { HashSet visited = new HashSet(); Queue queue = new Queue(rootNodes); @@ -25,5 +43,27 @@ public static HashSet MapGraph(IEnumerable roo return visited; } + + /// + /// Starting from a single node, checks if the graph has cycles. + /// + static bool HasCycles(FilterGraphNode currentNode, HashSet visited, HashSet inProgress) { + if (inProgress.Contains(currentNode)) { + return true; + } + if (visited.Contains(currentNode)) { + return false; + } + + inProgress.Add(currentNode); + foreach (FilterGraphNode child in currentNode.Children) { + if (HasCycles(child, visited, inProgress)) { + return true; + } + } + inProgress.Remove(currentNode); + visited.Add(currentNode); + return false; + } } } \ No newline at end of file diff --git a/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs b/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs index f272972e..20e616ee 100644 --- a/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs +++ b/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs @@ -23,11 +23,21 @@ public class ManipulatableGraph : ScrollViewer { /// public event Action OnRightClick; + /// + /// When the user connects a parent (first parameter) and child (second parameter) nodes, this function is called. + /// + public event Action OnConnect; + /// /// The currently selected node is determined by border line thickness. /// public StyledNode SelectedNode => (StyledNode)viewer.Graph?.Nodes.FirstOrDefault(x => x.Attr.LineWidth > 1); + /// + /// The user started dragging from this node. + /// + StyledNode dragStart; + /// /// Handle to MSAGL. /// @@ -95,6 +105,9 @@ protected override void OnPreviewMouseUp(MouseButtonEventArgs e) { IViewerObject element = viewer.ObjectUnderMouseCursor; object param = null; if (element is IViewerNode vnode) { + if (dragStart != null && vnode.Node != dragStart) { + OnConnect?.Invoke(dragStart, (StyledNode)vnode.Node); + } param = vnode.Node; } else if (element is IViewerEdge edge) { param = edge.Edge; @@ -121,5 +134,11 @@ protected override void OnPreviewMouseMove(MouseEventArgs e) { e.Handled = true; } } + + /// + /// Starts to track dragging a new edge from a node. + /// + protected override void OnPreviewMouseDown(MouseButtonEventArgs e) => + dragStart = (StyledNode)(viewer.ObjectUnderMouseCursor as IViewerNode)?.Node; } } \ No newline at end of file diff --git a/CavernSamples/FilterStudio/MainWindow.Graph.cs b/CavernSamples/FilterStudio/MainWindow.Graph.cs index 8df36d0c..437fb418 100644 --- a/CavernSamples/FilterStudio/MainWindow.Graph.cs +++ b/CavernSamples/FilterStudio/MainWindow.Graph.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Windows; +using Cavern.Filters; +using Cavern.Filters.Utilities; using VoidX.WPF; using FilterStudio.Graphs; @@ -52,6 +54,33 @@ void SetDirection(LayerDirection direction) { /// void Recenter(object _, RoutedEventArgs e) => ReloadGraph(); + /// + /// Delete the currently selected node. + /// + void DeleteNode(object sender, RoutedEventArgs e) { + StyledNode node = graph.GetSelectedNode(sender); + if (node == null || node.Filter == null) { + Error((string)language["NFNod"]); + } else if (node.Filter.Filter is InputChannel) { + Error((string)language["NFInp"]); + } else if (node.Filter.Filter is OutputChannel) { + Error((string)language["NFOut"]); + } else { + node.Filter.DetachFromGraph(); + ReloadGraph(); + } + } + + /// + /// Delete the selected edge. + /// + void DeleteEdge(Edge edge) { + FilterGraphNode parent = ((StyledNode)edge.SourceNode).Filter, + child = ((StyledNode)edge.TargetNode).Filter; + parent.DetachChild(child, false); + ReloadGraph(); + } + /// /// When selecting a node, open it for modification. /// @@ -71,19 +100,47 @@ void GraphLeftClick(object _) { /// Display the context menu when the graph is right clicked. /// void GraphRightClick(object element) { + if (element is not Node && element is not Edge) { + return; + } + List<(string, Action)> menuItems = [ ((string)language["FLabe"], (_, e) => AddLabel(element, e)), ((string)language["FGain"], (_, e) => AddGain(element, e)), ((string)language["FDela"], (_, e) => AddDelay(element, e)), ((string)language["FBiqu"], (_, e) => AddBiquad(element, e)), + (null, null) // Separator for deletion ]; if (element is Node) { - menuItems.Add((null, null)); menuItems.Add(((string)language["CoDel"], (_, e) => DeleteNode(element, e))); + } else { + menuItems.Add(((string)language["CoDel"], (_, e) => DeleteEdge((Edge)element))); } QuickContextMenu.Show(menuItems); } + /// + /// Handle creating a user-selected connection. + /// + void GraphConnect(StyledNode parent, StyledNode child) { + if (child.Filter.Filter is InputChannel) { + Error((string)language["NCInp"]); + return; + } + if (parent.Filter.Filter is OutputChannel) { + Error((string)language["NCOut"]); + return; + } + + parent.Filter.AddChild(child.Filter); + if (FilterGraphNodeUtils.HasCycles(rootNodes)) { + Error((string)language["NLoop"]); + parent.Filter.DetachChild(child.Filter, false); + } else { + ReloadGraph(); + } + } + /// /// Updates the graph based on the . /// diff --git a/CavernSamples/FilterStudio/MainWindow.xaml b/CavernSamples/FilterStudio/MainWindow.xaml index c00ec545..7b2ba38b 100644 --- a/CavernSamples/FilterStudio/MainWindow.xaml +++ b/CavernSamples/FilterStudio/MainWindow.xaml @@ -54,8 +54,9 @@ - - + + + \ No newline at end of file diff --git a/CavernSamples/FilterStudio/MainWindow.xaml.cs b/CavernSamples/FilterStudio/MainWindow.xaml.cs index 3b433715..95159478 100644 --- a/CavernSamples/FilterStudio/MainWindow.xaml.cs +++ b/CavernSamples/FilterStudio/MainWindow.xaml.cs @@ -49,6 +49,7 @@ public MainWindow() { pipeline.language = language; graph.OnLeftClick += GraphLeftClick; graph.OnRightClick += GraphRightClick; + graph.OnConnect += GraphConnect; showInstructions.IsChecked = Settings.Default.showInstructions; SetInstructions(null, null); @@ -120,23 +121,6 @@ void SelectChannels(object _, RoutedEventArgs e) { } } - /// - /// Delete the currently selected node. - /// - void DeleteNode(object sender, RoutedEventArgs e) { - StyledNode node = graph.GetSelectedNode(sender); - if (node == null || node.Filter == null) { - Error((string)language["NFNod"]); - } else if (node.Filter.Filter is InputChannel) { - Error((string)language["NFInp"]); - } else if (node.Filter.Filter is OutputChannel) { - Error((string)language["NFOut"]); - } else { - node.Filter.DetachFromGraph(); - ReloadGraph(); - } - } - /// /// Handle when the instructions are enabled or disabled. /// @@ -144,6 +128,7 @@ void SetInstructions(object _, RoutedEventArgs e) { Visibility instructions = showInstructions.IsChecked ? Visibility.Visible : Visibility.Hidden; help1.Visibility = instructions; help2.Visibility = instructions; + help3.Visibility = instructions; } /// diff --git a/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml b/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml index da64bfa9..ae50e37d 100644 --- a/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml +++ b/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml @@ -29,6 +29,7 @@ Jobb klikkelj egy szűrőre, hogy új szűrőútvonalat indíts belőle. Jobb klikkelj egy nyílra, hogy új szűrőt adj hozzá két szűrő közé. + Húzz egy szűrőt lenyomott egérgombbal a másikba, hogy összekösd őket. Equalizer APO konfigurációk|*.txt Hiba @@ -40,6 +41,9 @@ A csatornakonfiguráció csak új konfigurációs fájl létrehozásakor lesz al Kimenetek után nem adható hozzá szűrő. Egy csatorna bemenete nem törölhető. Egy csatorna kimenete nem törölhető. + Szűrök nem köthetők bemenetbe. + Kimenetek nem köthetők szűrőkbe. + Ez az összeköttetés érvénytelen, mert kört okozna a gráfban. Új Bemenet diff --git a/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml b/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml index c3b8c6ec..e70a9eba 100644 --- a/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml +++ b/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml @@ -29,6 +29,7 @@ Right click a filter to start a new path from it with a new filter. Right click an arrow to add a filter between two filters. + Drag and drop from one filter to another to connect them. Equalizer APO configurations|*.txt Error @@ -40,6 +41,9 @@ The channel configuration will only be applied when you create a new configurati Filters can't be added after an output. The input of a channel can't be deleted. The output of a channel can't be deleted. + Filters cannot be connected to inputs. + Outputs cannot be connected to filters. + This connection is invalid, because it would cause a cycle in the graph. New Input