Skip to content

Commit 1b8850e

Browse files
committed
feat: improve connection deletion (fixes #28)
1 parent c5af089 commit 1b8850e

File tree

6 files changed

+277
-11
lines changed

6 files changed

+277
-11
lines changed

CHANGES.md

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.6.0] - 2022-12-07
8+
### Added
9+
- You can now delete connections by right-clicking them. This is especially useful when multiple connections go into the same node, where until now you had to one by one drag them out of the node until you had the correct one ([#28](https://github.com/derkork/openscad-graph-editor/issues/28)).
10+
- You can now quickly insert a reroute node on a connection by moving the mouse over a connection and then `Shift`+`Right-Click`. If you additionally hold `Ctrl`/`Cmd` while doing this a wireless reroute node is inserted. This complements the already existing functionality of deleting a reroute node with `Shift`+`Delete`, which will delete the reroute node but keep the connection.
11+
712
## [0.5.2] - 2022-12-02
813
### Added
914
- It is now possible to modify references to external files (until now you could only delete and add them again).

OpenScadGraphEditor.csproj

+1-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<PackageReference Include="GodotExt" Version="0.2.0" />
1313
<PackageReference Include="GodotTestDriver" Version="1.0.0-pre1" />
1414
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
15+
<PackageReference Include="KdTree" Version="1.4.1" />
1516
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
1617
<PackageReference Include="Serilog" Version="2.11.0" />
1718
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
@@ -48,12 +49,6 @@
4849
<CustomToolNamespace>OpenScadGraphEditor.Library.External</CustomToolNamespace>
4950
</Antlr4>
5051
</ItemGroup>
51-
<ItemGroup>
52-
<Compile Remove="Library\LightWeightGraph.cs" />
53-
<Compile Remove="Library\ICanBeRendered.cs" />
54-
<Compile Remove="Library\IScadGraph.cs" />
55-
<Compile Remove="Nodes\MainEntryPoint.cs" />
56-
</ItemGroup>
5752

5853
<PropertyGroup>
5954
<Antlr4UseCSharpGenerator>True</Antlr4UseCSharpGenerator>

Utils/KeyMap.cs

+14
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,21 @@ private static bool IsCmdOrControlPressed(this InputEvent evt)
4141

4242
return inputEventKey.Control;
4343
}
44+
45+
public static bool IsShiftPressed()
46+
{
47+
return Input.IsKeyPressed((int) KeyList.Shift);
48+
}
4449

50+
public static bool IsCmdOrControlPressed()
51+
{
52+
if (OS.GetName() == "OSX")
53+
{
54+
return Input.IsKeyPressed((int) KeyList.Meta);
55+
}
56+
57+
return Input.IsKeyPressed((int) KeyList.Control);
58+
}
4559

4660
// copy+paste
4761
public static bool IsCopy(this InputEvent inputEvent) => inputEvent.IsCmdOrControlPressed() && inputEvent.IsKeyPressed(KeyList.C);

Widgets/ScadGraphEdit.cs

+247-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Linq;
45
using Godot;
56
using GodotExt;
67
using JetBrains.Annotations;
8+
using KdTree;
9+
using KdTree.Math;
710
using OpenScadGraphEditor.Library;
811
using OpenScadGraphEditor.Nodes;
912
using OpenScadGraphEditor.Nodes.Reroute;
@@ -76,8 +79,18 @@ public class ScadGraphEdit : GraphEdit
7679

7780
private readonly HashSet<string> _selection = new HashSet<string>();
7881
private readonly Dictionary<string, ScadNodeWidget> _widgets = new Dictionary<string, ScadNodeWidget>();
82+
/// <summary>
83+
/// Lookup tree for finding the closest connection to a given point.
84+
/// </summary>
85+
private KdTree<float, (ScadConnection Connection,List<Vector2> BezierPoints)> _connectionTree =
86+
new KdTree<float, (ScadConnection Connection,List<Vector2> BezierPoints)>(2, new FloatMath());
7987
private ScadConnection _pendingDisconnect;
8088
public ScadGraph Graph { get; private set; }
89+
90+
private Control _connectionHighlightLayer;
91+
92+
private List<Vector2> _connectionHighlightPoints;
93+
private ScadConnection _highlightedConnection;
8194

8295

8396
public void SelectNodes(List<ScadNode> nodes)
@@ -123,7 +136,21 @@ public override void _Ready()
123136

124137
this.Connect("_end_node_move")
125138
.To(this, nameof(OnEndNodeMove));
139+
140+
this.Connect("draw")
141+
.To(this, nameof(BuildConnectionLookupTree));
142+
143+
144+
_connectionHighlightLayer = this.WithName<Control>("CLAYER");
145+
_connectionHighlightLayer
146+
.Connect("draw")
147+
.To(this, nameof(OnConnectionHighlightLayerDraw));
148+
149+
this.Connect("scroll_offset_changed")
150+
.To(this, nameof(OnScrollOffsetChanged));
126151
}
152+
153+
127154

128155
public override bool CanDropData(Vector2 position, object data)
129156
{
@@ -220,6 +247,9 @@ public void Render(ScadGraph graph)
220247
ConnectNode(_widgets[connection.From.Id].Name, connection.FromPort, _widgets[connection.To.Id].Name, connection.ToPort);
221248
}
222249

250+
// build the lookup tree
251+
BuildConnectionLookupTree();
252+
223253
// highlight any bound nodes
224254
HighlightBoundNodes();
225255

@@ -239,7 +269,22 @@ public void Render(ScadGraph graph)
239269
_selection.Remove(id);
240270
}
241271
}
242-
272+
273+
private void BuildConnectionLookupTree()
274+
{
275+
_connectionTree =
276+
new KdTree<float, (ScadConnection Connection, List<Vector2> BezierPoints)>(2, new FloatMath());
277+
foreach (var connection in Graph.GetAllConnections())
278+
{
279+
var connectionPoints = GetConnectionPoints(connection);
280+
var bezierPoints = BakeBezierLine(connectionPoints.Start, connectionPoints.End, 1);
281+
foreach (var bezierPoint in bezierPoints)
282+
{
283+
_connectionTree.Add(new[] {bezierPoint.x, bezierPoint.y}, (connection, bezierPoints));
284+
}
285+
}
286+
}
287+
243288
/// <summary>
244289
/// Calculates the graph relative position for any global coordinate taking zoom and scroll offset into account.
245290
/// </summary>
@@ -264,21 +309,64 @@ private Vector2 LocalToGraphRelative(Vector2 localPosition)
264309
private void OnPopupRequest(Vector2 position)
265310
{
266311
var relativePosition = GlobalToGraphRelative(position);
312+
313+
if (_highlightedConnection != null)
314+
{
315+
// a right click on a connection will in every case delete the connection, so first
316+
// make it no longer highlighted but keep the reference.
317+
var toDelete = _highlightedConnection;
318+
_highlightedConnection = null;
319+
_connectionHighlightPoints = null;
320+
321+
// if shift is pressed we want to break the connection and insert a reroute node
322+
// if additionally ctrl is pressed we want to break the connection and insert a wireless reroute node
323+
if (KeyMap.IsShiftPressed())
324+
{
325+
var rerouteNode = NodeFactory.Build<RerouteNode>();
326+
if (KeyMap.IsCmdOrControlPressed())
327+
{
328+
rerouteNode.IsWireless = true;
329+
}
330+
rerouteNode.Offset = relativePosition;
331+
332+
PerformRefactorings("Insert reroute node",
333+
// first, delete the connection
334+
new DeleteConnectionRefactoring( toDelete),
335+
// then insert the reroute node and connect it to the old connection's target
336+
new AddNodeRefactoring(Graph, rerouteNode, toDelete.To, PortId.Input(toDelete.ToPort)),
337+
// and add a new connection from the old connection's source to the reroute node
338+
new AddConnectionRefactoring(new ScadConnection(Graph, toDelete.From, toDelete.FromPort, rerouteNode, 0))
339+
);
340+
return;
341+
}
342+
343+
// otherwise we want to delete the connection
344+
PerformRefactorings("Delete connection", new DeleteConnectionRefactoring(toDelete));
345+
return;
346+
}
267347

268-
var matchingWidgets = _widgets.Values
269-
.FirstOrDefault(it => new Rect2(it.Offset, it.RectSize).HasPoint(relativePosition));
348+
270349

271-
if (matchingWidgets == null)
350+
if (!TryGetNodeAtPosition(relativePosition, out var node))
272351
{
273352
// right-click in empty space yields you the add dialog
274353
AddDialogRequested?.Invoke(RequestContext.ForPosition(Graph, relativePosition));
275354
return;
276355
}
277356

278-
NodePopupRequested?.Invoke(RequestContext.ForNode(Graph, position, matchingWidgets.BoundNode));
357+
NodePopupRequested?.Invoke(RequestContext.ForNode(Graph, position, node));
279358

280359
}
281360

361+
private bool TryGetNodeAtPosition(Vector2 relativePosition, out ScadNode node)
362+
{
363+
var firstMatchingWidget = _widgets.Values
364+
.FirstOrDefault(it => new Rect2(it.Offset, it.RectSize).HasPoint(relativePosition));
365+
366+
node = firstMatchingWidget?.BoundNode;
367+
return node != null;
368+
}
369+
282370

283371
public override void _GuiInput(InputEvent evt)
284372
{
@@ -290,6 +378,48 @@ public override void _GuiInput(InputEvent evt)
290378
return;
291379
}
292380

381+
if (evt is InputEventMouseMotion mouseMotionEvent)
382+
{
383+
// if the mouse is over a node, don't do anything
384+
var mousePositionRelativeToTheGraph = GlobalToGraphRelative(mouseMotionEvent.GlobalPosition);
385+
if (TryGetNodeAtPosition(mousePositionRelativeToTheGraph, out _))
386+
{
387+
ClearHighlightedConnection();
388+
return;
389+
}
390+
391+
// find the closest connection to the mouse
392+
// we use * zoom because GlobalToGraphRelative will give us the coordinates in graph space and
393+
// we need to convert them to screen space but relative to the graph widget.
394+
var mousePosition = mousePositionRelativeToTheGraph * Zoom;
395+
396+
var closest = _connectionTree.GetNearestNeighbours(new []{mousePosition.x, mousePosition.y}, 1);
397+
if (closest.Length <= 0)
398+
{
399+
ClearHighlightedConnection();
400+
return;
401+
}
402+
403+
404+
// are we close enough to the connection?
405+
var distance = new Vector2(closest[0].Point[0], closest[0].Point[1]).DistanceTo(mousePosition);
406+
if (distance > 20)
407+
{
408+
ClearHighlightedConnection();
409+
return;
410+
}
411+
412+
// no need to redraw, if the connection is the same
413+
if (_highlightedConnection == closest[0].Value.Connection)
414+
{
415+
return;
416+
}
417+
418+
_highlightedConnection = closest[0].Value.Connection;
419+
_connectionHighlightPoints = closest[0].Value.BezierPoints;
420+
_connectionHighlightLayer.Update();
421+
}
422+
293423
if (evt.IsCopy())
294424
{
295425
CopyRequested?.Invoke(this, GetSelectedNodes().ToList());
@@ -357,6 +487,17 @@ public override void _GuiInput(InputEvent evt)
357487
}
358488
}
359489

490+
private void ClearHighlightedConnection()
491+
{
492+
if (_highlightedConnection == null)
493+
{
494+
return;
495+
}
496+
_highlightedConnection = null;
497+
_connectionHighlightPoints = null;
498+
_connectionHighlightLayer.Update();
499+
}
500+
360501
private Vector2 GetPastePosition()
361502
{
362503
// get the offset over the mouse position
@@ -676,6 +817,107 @@ private void PerformRefactorings(string description, IEnumerable<Refactoring> re
676817
RefactoringsRequested?.Invoke(description, refactoringsAsArray);
677818
}
678819

820+
private (Vector2 Start, Vector2 End) GetConnectionPoints(ScadConnection connection)
821+
{
822+
var fromNode = _widgets[connection.From.Id];
823+
var toNode = _widgets[connection.To.Id];
824+
var fromPort = fromNode.GetConnectionOutputPosition(connection.FromPort) + fromNode.Offset * Zoom;
825+
var toPort = toNode.GetConnectionInputPosition(connection.ToPort) + toNode.Offset * Zoom;
826+
return (fromPort, toPort);
827+
}
828+
829+
830+
/// <summary>
831+
/// Bezier interpolation function for the connection lines. This is copied straight from the Godot source code.
832+
/// </summary>
833+
private static Vector2 BezierInterpolate(float t, Vector2 start, Vector2 control1, Vector2 control2, Vector2 end)
834+
{
835+
var omt = 1.0f - t;
836+
var omt2 = omt * omt;
837+
var omt3 = omt2 * omt;
838+
var t2 = t * t;
839+
var t3 = t2 * t;
840+
841+
return start * omt3 + control1 * omt2 * t * 3.0f + control2 * omt * t2 * 3.0f + end * t3;
842+
}
843+
844+
/// <summary>
845+
/// Segment baking function for the connection lines. This is copied straight from the Godot source code + some modifications.
846+
/// </summary>
847+
private static void BakeSegment2D(List<Vector2> points, float pBegin, float pEnd,
848+
Vector2 pA, Vector2 pOut, Vector2 pB, Vector2 pIn, int pDepth, int pMinDepth, int pMaxDepth,
849+
float pTol, float pMaxLength, ref int lines)
850+
{
851+
var mp = pBegin + (pEnd - pBegin) * 0.5f;
852+
var beg = BezierInterpolate(pBegin, pA, pA + pOut, pB + pIn, pB);
853+
var mid = BezierInterpolate(mp, pA, pA + pOut, pB + pIn, pB);
854+
var end = BezierInterpolate(pEnd, pA, pA + pOut, pB + pIn, pB);
855+
856+
var na = (mid - beg).Normalized();
857+
var nb = (end - mid).Normalized();
858+
var dp = Mathf.Rad2Deg(Mathf.Acos(na.Dot(nb)));
859+
860+
// the maxLength check is to ensure we don't get longer straight segments of lines because we want
861+
// to keep the points that make up the lines evenly spaced, so our KDTree can find the closest point
862+
// to the mouse cursor and we don't get "holes" in the detection when the mouse is over a straight line
863+
// that technically only needs two points to be drawn. We insert additional points this way so
864+
// if the line is longer the algorithm can still detect it if the mouse cursor is in the middle of it.
865+
if (pDepth >= pMinDepth && (dp < pTol || pDepth >= pMaxDepth) && (beg - end).Length() < pMaxLength)
866+
{
867+
points.Add(end);
868+
lines++;
869+
}
870+
else
871+
{
872+
BakeSegment2D(points, pBegin, mp, pA, pOut, pB, pIn, pDepth + 1, pMinDepth, pMaxDepth, pTol, pMaxLength, ref lines);
873+
BakeSegment2D(points, mp, pEnd, pA, pOut, pB, pIn, pDepth + 1, pMinDepth, pMaxDepth, pTol, pMaxLength, ref lines);
874+
}
875+
}
876+
877+
/// <summary>
878+
/// Builds a bezier curve from the given points and returns a list of points that make up the curve.
879+
/// </summary>
880+
private List<Vector2> BakeBezierLine(Vector2 fromPoint, Vector2 toPoint, float bezierRatio)
881+
{
882+
//cubic bezier code
883+
var diff = toPoint.x - fromPoint.x;
884+
var cpLen = GetConstant("bezier_len_pos") * bezierRatio;
885+
var cpNegLen = GetConstant("bezier_len_neg") * bezierRatio;
886+
887+
var cpOffset = diff > 0
888+
? Mathf.Min(cpLen, diff * 0.5f)
889+
: Mathf.Max(Mathf.Min(cpLen - diff, cpNegLen), -diff * 0.5f);
890+
891+
var c1 = new Vector2(cpOffset * Zoom, 0);
892+
var c2 = new Vector2(-cpOffset * Zoom, 0);
893+
894+
var lines = 0;
895+
896+
var points = new List<Vector2> {fromPoint};
897+
BakeSegment2D(points, 0, 1, fromPoint, c1, toPoint, c2, 0, 3, 9, 3, 20, ref lines);
898+
points.Add(toPoint);
899+
900+
return points;
901+
}
902+
903+
private void OnScrollOffsetChanged([UsedImplicitly] Vector2 _)
904+
{
905+
// when the scroll offset/zoom is changed our previously highlighted connection is no longer valid
906+
// so we need to clear it, otherwise we'll get an artifact where the connection is still highlighted
907+
// in its old position and scale
908+
_highlightedConnection = null;
909+
_connectionHighlightPoints = null;
910+
}
911+
912+
private void OnConnectionHighlightLayerDraw()
913+
{
914+
if (_highlightedConnection == null)
915+
{
916+
return;
917+
}
918+
_connectionHighlightLayer.DrawPolyline(_connectionHighlightPoints.ToArray(), new Color(1,1,1, 0.5f), 5, true);
919+
}
920+
679921
}
680922

681923
}

global.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"sdk": {
3+
"version": "6.0.0",
4+
"rollForward": "latestMajor",
5+
"allowPrerelease": false
6+
}
7+
}

0 commit comments

Comments
 (0)