From cd5fc6a100638fb1f1cc46e35c63b1e22908a6bb Mon Sep 17 00:00:00 2001 From: KristofferStrube Date: Sun, 7 Apr 2024 17:32:23 +0200 Subject: [PATCH] Updated so that nodes can also be removed. --- .../Pages/BigRandom.razor | 4 +- .../Pages/Dotnet8.razor | 2 +- .../Pages/Index.razor | 2 +- .../Pages/LiveData.razor | 42 ++++++--- .../Pages/SelectCallback.razor | 2 +- .../Edge.cs | 1 - .../GraphEditor.razor.cs | 85 ++++++++++--------- ...KristofferStrube.Blazor.GraphEditor.csproj | 2 +- 8 files changed, 81 insertions(+), 59 deletions(-) diff --git a/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/BigRandom.razor b/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/BigRandom.razor index f500108..8687ef4 100644 --- a/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/BigRandom.razor +++ b/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/BigRandom.razor @@ -54,7 +54,7 @@ transitions = transitions.DistinctBy(t => t.from + "-" + t.to).ToList(); await GraphEditor.LoadGraph(pages, transitions); - GraphEditor.FitToShapes(delta: 1, 200); + GraphEditor.SVGEditor.FitViewportToAllShapes(delta: 1, 200); GraphEditor.MoveEdgesToBack(); double prevUnixTimeSeconds = DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1000.0; @@ -62,7 +62,7 @@ { await GraphEditor.ForceDirectedLayout(); double unixTimeSeconds = DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1000.0; - GraphEditor.FitToShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 4, 1)); + GraphEditor.SVGEditor.FitViewportToAllShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 4, 1)); prevUnixTimeSeconds = unixTimeSeconds; await Task.Delay(1); } diff --git a/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/Dotnet8.razor b/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/Dotnet8.razor index 5d8a4ff..89ff23c 100644 --- a/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/Dotnet8.razor +++ b/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/Dotnet8.razor @@ -71,7 +71,7 @@ p.Radians = p.BaseOffset + (unixTimeSeconds * 2) % (Math.PI * 2); }); await GraphEditor.ForceDirectedLayout(); - GraphEditor.FitToShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 4, 1)); + GraphEditor.SVGEditor.FitViewportToAllShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 4, 1)); prevUnixTimeSeconds = unixTimeSeconds; await Task.Delay(1); } diff --git a/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/Index.razor b/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/Index.razor index 4abca38..25966b1 100644 --- a/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/Index.razor +++ b/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/Index.razor @@ -55,7 +55,7 @@ double unixTimeSeconds = DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1000.0; if (unixTimeSeconds - startUnixTimeSeconds < 7) { - GraphEditor.FitToShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 2, 1), padding: 100); + GraphEditor.SVGEditor.FitViewportToAllShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 2, 1), padding: 100); prevUnixTimeSeconds = unixTimeSeconds; } await Task.Delay(1); diff --git a/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/LiveData.razor b/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/LiveData.razor index b29d95c..e896fe1 100644 --- a/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/LiveData.razor +++ b/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/LiveData.razor @@ -10,6 +10,11 @@ +@if (pages.Count > 3) +{ + +} +
new Page(i.ToString()) { BaseOffset = i * Math.PI * 2 / nodeCount }).ToList(); - transitions = Enumerable.Range(0, nodeCount).Select(i => new Transition(pages[i % nodeCount], pages[(i + 1) % nodeCount], 1)).ToList(); + pages = Enumerable.Range(0, 8).Select(i => new Page(i.ToString()) { BaseOffset = i * Math.PI * 2 / 8 }).ToList(); + transitions = Enumerable.Range(0, 8).Select(i => new Transition(pages[i % 8], pages[(i + 1) % 8], 1)).ToList(); await GraphEditor.LoadGraph(pages, transitions); @@ -57,8 +61,9 @@ p.Radians = p.BaseOffset + unixTimeSeconds % (Math.PI * 2); }); await GraphEditor.ForceDirectedLayout(); - GraphEditor.FitToShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 2, 1), 50); + GraphEditor.SVGEditor.FitViewportToAllShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 2, 1), 50); prevUnixTimeSeconds = unixTimeSeconds; + StateHasChanged(); await Task.Delay(1); } } @@ -74,19 +79,32 @@ public async Task AddNode() { - pages.Add(new Page(nodeCount++.ToString()) { BaseOffset = DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1000.0 }); - transitions.Add(new Transition(pages[Random.Shared.Next(nodeCount - 2)], pages[^1], 1, 200)); - if (Random.Shared.NextDouble() < 0.07) - { - transitions.Add(new Transition(pages[Random.Shared.Next(nodeCount - 2)], pages[^1], 1, 200)); - } - if (Random.Shared.NextDouble() < 0.07) + pages.Add(new Page(Guid.NewGuid().ToString()) { BaseOffset = DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1000.0 }); + double chance = 1; + for (int i = 0; i < 3; i++) { - transitions.Add(new Transition(pages[Random.Shared.Next(nodeCount - 2)], pages[^1], 1, 200)); + if (chance > 0.93) + { + var fromNode = pages[Random.Shared.Next(Math.Max(0, pages.Count - 2))]; + var toNode = pages[^1]; + if (fromNode != toNode) + { + transitions.Add(new Transition(fromNode, toNode, 1, 200)); + } + } + chance = Random.Shared.NextDouble(); } await GraphEditor.UpdateGraph(pages, transitions); } + public async Task RemoveNode() + { + Page pageToRemove = pages[Random.Shared.Next(pages.Count - 1)]; + pages.Remove(pageToRemove); + transitions = transitions.Where(t => t.from != pageToRemove && t.to != pageToRemove).ToList(); + await GraphEditor.UpdateGraph(pages, transitions); + } + double EdgeLengthMapper(Transition edge) => edge.length * (1.5 + Math.Sin(edgeLengthSinAngle) / 2); public record Page(string id) diff --git a/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/SelectCallback.razor b/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/SelectCallback.razor index e3dc23d..1b8e690 100644 --- a/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/SelectCallback.razor +++ b/samples/KristofferStrube.Blazor.GraphEditor.WasmExample/Pages/SelectCallback.razor @@ -60,7 +60,7 @@ else double unixTimeSeconds = DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1000.0; if (unixTimeSeconds - startUnixTimeSeconds < 3) { - GraphEditor.FitToShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 2, 1), padding: 50); + GraphEditor.SVGEditor.FitViewportToAllShapes(delta: Math.Min((unixTimeSeconds - prevUnixTimeSeconds) * 2, 1), padding: 50); prevUnixTimeSeconds = unixTimeSeconds; } await Task.Delay(1); diff --git a/src/KristofferStrube.Blazor.GraphEditor/Edge.cs b/src/KristofferStrube.Blazor.GraphEditor/Edge.cs index e1e2772..b385a21 100644 --- a/src/KristofferStrube.Blazor.GraphEditor/Edge.cs +++ b/src/KristofferStrube.Blazor.GraphEditor/Edge.cs @@ -1,7 +1,6 @@ using AngleSharp.Dom; using KristofferStrube.Blazor.SVGEditor; using Microsoft.AspNetCore.Components.Web; -using System.Xml.Linq; namespace KristofferStrube.Blazor.GraphEditor; diff --git a/src/KristofferStrube.Blazor.GraphEditor/GraphEditor.razor.cs b/src/KristofferStrube.Blazor.GraphEditor/GraphEditor.razor.cs index 2ff1c7e..1f50bee 100644 --- a/src/KristofferStrube.Blazor.GraphEditor/GraphEditor.razor.cs +++ b/src/KristofferStrube.Blazor.GraphEditor/GraphEditor.razor.cs @@ -1,9 +1,5 @@ -using AngleSharp.Dom; using KristofferStrube.Blazor.SVGEditor; using Microsoft.AspNetCore.Components; -using System.Globalization; -using System.Xml.Linq; -using static System.Formats.Asn1.AsnWriter; namespace KristofferStrube.Blazor.GraphEditor; @@ -48,6 +44,14 @@ private string EdgeId(TEdge e) public bool IsReadyToLoad => SVGEditor.BBox is not null; + protected Dictionary Nodes { get; set; } = []; + + protected Dictionary Edges { get; set; } = []; + + public SVGEditor.SVGEditor SVGEditor { get; set; } = default!; + + protected string Input { get; set; } = ""; + protected override void OnInitialized() { callbackContext = new() @@ -104,27 +108,60 @@ public async Task UpdateGraph(List nodes, List edges) { Dictionary> newNodeElementDictionary = []; + HashSet newSetOfNodes = new(nodes.Count); + HashSet newSetOfEdges = new(edges.Count); + + // Add new nodes foreach (TNode node in nodes) { - if (!Nodes.ContainsKey(NodeIdMapper(node))) + string nodeKey = NodeIdMapper(node); + if (!Nodes.ContainsKey(nodeKey)) { Node element = Node.CreateNew(SVGEditor, this, node); newNodeElementDictionary.Add(node, element); - Nodes.Add(NodeIdMapper(node), node); + Nodes.Add(nodeKey, node); } + newSetOfNodes.Add(nodeKey); } + + // Add new edges foreach (TEdge edge in edges) { - if (!Edges.ContainsKey(EdgeId(edge))) + string edgeKey = EdgeId(edge); + if (!Edges.ContainsKey(edgeKey)) { TNode from = EdgeFromMapper(edge); TNode to = EdgeToMapper(edge); Node fromElement = newNodeElementDictionary.TryGetValue(from, out var eFrom) ? eFrom : nodeElements.First(n => n.Data.Equals(from)); Node toElement = newNodeElementDictionary.TryGetValue(to, out var eTo) ? eTo : nodeElements.First(n => n.Data.Equals(to)); Edge.AddNew(SVGEditor, this, edge, fromElement, toElement); - Edges.Add(EdgeId(edge), edge); + Edges.Add(edgeKey, edge); + } + newSetOfEdges.Add(edgeKey); + } + + // Remove old edges + foreach (string edgeKey in Edges.Keys) + { + if (!newSetOfEdges.Contains(edgeKey)) + { + Edge edgeToRemove = (Edge)SVGEditor.Elements.First(e => e is Edge edge && EdgeId(edge.Data) == edgeKey); + SVGEditor.RemoveElement(edgeToRemove); + Edges.Remove(edgeKey); + } + } + + // Remove old nodes + foreach (string nodeKey in Nodes.Keys) + { + if (!newSetOfNodes.Contains(nodeKey)) + { + Node nodeToRemove = (Node)SVGEditor.Elements.First(e => e is Node node && NodeIdMapper(node.Data) == nodeKey); + SVGEditor.RemoveElement(nodeToRemove); + Nodes.Remove(nodeKey); } } + foreach (var newNodeElement in newNodeElementDictionary.Values) { if (newNodeElement.Edges.Count is 0) @@ -244,30 +281,6 @@ public Task ForceDirectedLayout() return Task.CompletedTask; } - public void FitToShapes(double delta = 1, double padding = 20) - { - if (SVGEditor.BBox is null || SVGEditor.SelectedShapes.Count > 0) return; - double lowerX = double.MaxValue, lowerY = double.MaxValue; - double upperX = double.MinValue, upperY = double.MinValue; - foreach (Shape shape in SVGEditor.Elements.Cast()) - { - foreach ((double x, double y) in shape.SelectionPoints) - { - double strokeWidth = double.TryParse(shape.StrokeWidth, CultureInfo.InvariantCulture, out double result) ? result : 0; - lowerX = Math.Min(x - strokeWidth, lowerX); - upperX = Math.Max(x + strokeWidth, upperX); - lowerY = Math.Min(y - strokeWidth, lowerY); - upperY = Math.Max(y + strokeWidth, upperY); - } - } - double elementsWidth = upperX - lowerX; - double elementsHeight = upperY - lowerY; - var newScale = Math.Min((SVGEditor.BBox.Width - (padding * 2)) / elementsWidth, (SVGEditor.BBox.Height - (padding * 2)) / elementsHeight); - (double x, double y) newTranslate = ((SVGEditor.BBox.Width / 2) - ((lowerX + (elementsWidth / 2)) * newScale), (SVGEditor.BBox.Height / 2) - ((lowerY + (elementsHeight / 2)) * newScale)); - SVGEditor.Scale = (SVGEditor.Scale * (1 - delta) + newScale * delta); - SVGEditor.Translate = (SVGEditor.Translate.x * (1 - delta) + newTranslate.x * delta, SVGEditor.Translate.y * (1 - delta) + newTranslate.y * delta); - } - public void MoveEdgesToBack() { var prevSelectedShapes = SVGEditor.SelectedShapes.ToList(); @@ -280,14 +293,6 @@ public void MoveEdgesToBack() SVGEditor.SelectedShapes = prevSelectedShapes; } - protected Dictionary Nodes { get; set; } = []; - - protected Dictionary Edges { get; set; } = []; - - protected SVGEditor.SVGEditor SVGEditor { get; set; } = default!; - - protected string Input { get; set; } = ""; - protected List SupportedElements { get; set; } = [ new(typeof(Node), element => element.TagName is "CIRCLE" && element.GetAttribute("data-elementtype") == "node"), diff --git a/src/KristofferStrube.Blazor.GraphEditor/KristofferStrube.Blazor.GraphEditor.csproj b/src/KristofferStrube.Blazor.GraphEditor/KristofferStrube.Blazor.GraphEditor.csproj index 272e62a..62edd7a 100644 --- a/src/KristofferStrube.Blazor.GraphEditor/KristofferStrube.Blazor.GraphEditor.csproj +++ b/src/KristofferStrube.Blazor.GraphEditor/KristofferStrube.Blazor.GraphEditor.csproj @@ -35,7 +35,7 @@ - +