Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using GodotExt;
using JetBrains.Annotations;
using KdTree;
using KdTree.Math;
using OpenScadGraphEditor.Actions;
using OpenScadGraphEditor.Library;
using OpenScadGraphEditor.Nodes;
using OpenScadGraphEditor.Nodes.Reroute;
using OpenScadGraphEditor.Refactorings;
using OpenScadGraphEditor.Utils;
using Serilog;
namespace OpenScadGraphEditor.Widgets
{
/// <summary>
/// This is our main editing interface for editing graphs of invokable things (functions/modules). It is the
/// heavyweight alternative to <see cref="ScadGraph"/>. In contrast to <see cref="ScadGraph"/>,
/// this graph never really changes the actual graph data but just invokes refactoring events in response to
/// user input that will do the proper refactoring actions.
/// </summary>
public class ScadGraphEdit : GraphEdit
{
private readonly HashSet<string> _selection = new HashSet<string>();
private readonly Dictionary<string, ScadNodeWidget> _widgets = new Dictionary<string, ScadNodeWidget>();
/// <summary>
/// Lookup tree for finding the closest connection to a given point.
/// </summary>
private KdTree<float, (ScadConnection Connection, List<Vector2> BezierPoints)> _connectionTree =
new KdTree<float, (ScadConnection Connection, List<Vector2> BezierPoints)>(2, new FloatMath());
private ScadConnection _pendingDisconnect;
public ScadGraph Graph { get; private set; }
private Control _connectionHighlightLayer;
private List<Vector2> _connectionHighlightPoints;
private ScadConnection _highlightedConnection;
private ScadProject _project;
private IEditorContext _context;
private GraphLayout _graphLayout;
private void SelectNodes(List<ScadNode> nodes)
{
var set = nodes.Select(it => it.Id).ToHashSet();
_widgets.Values.ForAll(it => it.Selected = set.Contains(it.BoundNode.Id));
_selection.Clear();
set.ForAll(it => OnNodeSelected(_widgets[it]));
}
public override void _Ready()
{
RightDisconnects = true;
// we'll be handling all connection request ourselves, so we will per se allow
// everything to be connected to everything else.
var allPortTypes = Enum.GetValues(typeof(PortType)).Cast<PortType>().ToList();
foreach (var from in allPortTypes)
{
foreach (var to in allPortTypes)
{
AddValidConnectionType((int)from, (int)to);
}
}
this.Connect("connection_request")
.To(this, nameof(OnConnectionRequest));
this.Connect("disconnection_request")
.To(this, nameof(OnDisconnectionRequest));
this.Connect("connection_to_empty")
.To(this, nameof(OnConnectionToEmpty));
this.Connect("connection_from_empty")
.To(this, nameof(OnConnectionFromEmpty));
this.Connect("node_selected")
.To(this, nameof(OnNodeSelected));
this.Connect("node_unselected")
.To(this, nameof(OnNodeUnselected));
this.Connect("delete_nodes_request")
.To(this, nameof(DeleteSelection));
this.Connect("popup_request")
.To(this, nameof(OnPopupRequest));
this.Connect("_end_node_move")
.To(this, nameof(OnEndNodeMove));
this.Connect("draw")
.To(this, nameof(BuildConnectionLookupTree));
_connectionHighlightLayer = this.WithName<Control>("CLAYER");
_connectionHighlightLayer
.Connect("draw")
.To(this, nameof(OnConnectionHighlightLayerDraw));
this.Connect("scroll_offset_changed")
.To(this, nameof(OnScrollOffsetChanged));
}
public override bool CanDropData(Vector2 position, object data)
{
return data is DragData;
}
public override void DropData(Vector2 position, object data)
{
if (!(data is DragData dragData))
{
return; // we cannot handle this data.
}
if (dragData.TryGetData(out InvokableDescription invokableDescription))
{
// someone dragged an invokable description here, so we'll create a new node for it.
var node = invokableDescription switch
{
FunctionDescription functionDescription =>
NodeFactory.Build<FunctionInvocation>(functionDescription),
ModuleDescription moduleDescription =>
NodeFactory.Build<ModuleInvocation>(moduleDescription),
_ => null
};
if (node == null)
{
return; // we cannot handle this data.
}
// ensure we don't drag in stuff that isn't usable by the graph in question.
if (!Graph.Description.CanUse(node))
{
return;
}
node.Offset = LocalToGraphRelative(position);
_context.PerformRefactoring("Add node", new AddNodeRefactoring(Graph, node));
return;
}
if (dragData.TryGetData(out VariableDescription variableDescription))
{
// in case of a variable we can either _get_ or _set_ the variable
// so we will need to open a popup menu to let the user choose.
var getNode = NodeFactory.Build<GetVariable>(variableDescription);
getNode.Offset = LocalToGraphRelative(position);
var setNode = NodeFactory.Build<SetVariable>(variableDescription);
setNode.Offset = LocalToGraphRelative(position);
var actions = new List<QuickAction>
{
new QuickAction($"Get {variableDescription.Name}",
() => _context.PerformRefactoring("Add node", new AddNodeRefactoring(Graph, getNode))),
new QuickAction($"Set {variableDescription.Name}",
() => _context.PerformRefactoring("Add node", new AddNodeRefactoring(Graph, setNode)))
};
_context.ShowPopupMenu(GetGlobalMousePosition(), actions);
}
}
private void CreateOrGet(ScadNode node)
{
if (!_widgets.TryGetValue(node.Id, out var widget))
{
widget = node is IHaveCustomWidget iUseCustomWidget
? iUseCustomWidget.InstantiateCustomWidget()
: Prefabs.New<ScadNodeWidget>();
// this is technically not needed but it would seem that the graph edit gets confused if you don't set
// the name of the widget to something unique.
widget.Name = node.Id;
widget.LiteralToggled += (port, enabled) =>
PerformRefactorings("Toggle literal", new ToggleLiteralRefactoring(Graph, node, port, enabled));
widget.LiteralValueChanged += (port, value) =>
PerformRefactorings("Set literal value", new SetLiteralValueRefactoring(Graph, node, port, value));
widget.SizeChanged += size =>
PerformRefactorings("Change size", new ChangeNodeSizeRefactoring(Graph, node, size));
_widgets[node.Id] = widget;
widget.MoveToNewParent(this);
}
widget.BindTo(_project, Graph, node);
}
private void OnEndNodeMove()
{
// all nodes which are currently selected have moved, so we need to send a refactoring request for them.
IEnumerable<Refactoring> refactorings = GetSelectedNodes()
.Select(it => new ChangeNodePositionRefactoring(Graph, it, _widgets[it.Id].Offset));
_context.PerformRefactorings("Move node(s)", refactorings);
}
public void FocusNode(ScadNode node)
{
var widget = _widgets[node.Id];
var middle = RectSize / 2;
// calculate the offset we need to move to get the widget into the middle.
ScrollOffset = widget.Offset - (middle * Zoom);
widget.Flash();
}
public void Render(IEditorContext context, ScadGraph graph)
{
Graph = graph;
_project = context.CurrentProject;
_context = context;
_graphLayout = new GraphLayout(graph, _context, GlobalToGraphRelative);
// rebuild the nodes
foreach (var node in Graph.GetAllNodes())
{
CreateOrGet(node);
}
// redo the connections
ClearConnections();
foreach (var connection in Graph.GetAllConnections())
{
// do not show connections to wireless reroutes by default, unless the reroute node is currently selected
if (connection.To.IsWirelessReroute() && !_selection.Contains(connection.To.Id))
{
continue;
}
// connection contains ScadNode ids but we need to connect widgets, so first we need to find the
// the widget and then connect these widgets.
ConnectNode(_widgets[connection.From.Id].Name, connection.FromPort, _widgets[connection.To.Id].Name,
connection.ToPort);
// if the connection is from or to a deactivated node, make it less visible
if (connection.From.GetModifiers().HasFlag(ScadNodeModifier.Disable) ||
connection.To.GetModifiers().HasFlag(ScadNodeModifier.Disable))
{
SetConnectionActivity(_widgets[connection.From.Id].Name, connection.FromPort, _widgets[connection.To.Id].Name,
connection.ToPort, 0.9f);
}
}
// highlight any bound nodes
HighlightBoundNodes();
// finally destroy all the widgets that are not in the graph anymore
// set of Ids of nodes that are in the graph
var nodeIds = graph.GetAllNodes().Select(n => n.Id).ToHashSet();
// set of Ids of our widgets
var widgetIds = _widgets.Keys.ToHashSet();
// difference between the two sets
var idsToDestroy = widgetIds.Except(nodeIds).ToList();
// destroy all the widgets that are not in the graph anymore
foreach (var id in idsToDestroy)
{
_widgets[id].RemoveAndFree();
_widgets.Remove(id);
// also remove them from the selection if they were in
_selection.Remove(id);
}
BuildConnectionLookupTree();
// prevent phantom connections from being drawn
ClearHighlightedConnection();
}
private void BuildConnectionLookupTree()
{
_connectionTree =
new KdTree<float, (ScadConnection Connection, List<Vector2> BezierPoints)>(2, new FloatMath());
foreach (var connection in Graph.GetAllConnections())
{
var connectionPoints = GetConnectionPoints(connection);
var bezierPoints = BakeBezierLine(connectionPoints.Start, connectionPoints.End, 1);
foreach (var bezierPoint in bezierPoints)
{
_connectionTree.Add(new[] { bezierPoint.x, bezierPoint.y }, (connection, bezierPoints));
}
}
}
/// <summary>
/// Calculates the graph relative position for any global coordinate taking zoom and scroll offset into account.
/// </summary>
private Vector2 GlobalToGraphRelative(Vector2 globalPosition)
{
var localPosition = globalPosition - RectGlobalPosition;
return LocalToGraphRelative(localPosition);
}
/// <summary>
/// Calculates the graph relative position for any local coordinate (relative to this editor) taking zoom and scroll offset into account.
/// </summary>
private Vector2 LocalToGraphRelative(Vector2 localPosition)
{
var relativePosition = localPosition;
relativePosition += ScrollOffset;
relativePosition /= Zoom;
return relativePosition;
}
private void OnPopupRequest(Vector2 position)
{
var relativePosition = GlobalToGraphRelative(position);
if (_highlightedConnection != null)
{
// a right click on a connection will in every case delete the connection, so first
// make it no longer highlighted but keep the reference.
var toDelete = _highlightedConnection;
_highlightedConnection = null;
_connectionHighlightPoints = null;
// if shift is pressed we want to break the connection and insert a reroute node
// if additionally ctrl is pressed we want to break the connection and insert a wireless reroute node
if (KeyMap.IsShiftPressed())
{
var rerouteNode = NodeFactory.Build<RerouteNode>();
if (KeyMap.IsCmdOrControlPressed())
{
rerouteNode.IsWireless = true;
}
rerouteNode.Offset = relativePosition;
PerformRefactorings("Insert reroute node",
// first, delete the connection
new DeleteConnectionRefactoring(toDelete),
// then insert the reroute node and connect it to the old connection's target
new AddNodeRefactoring(Graph, rerouteNode, toDelete.To, PortId.Input(toDelete.ToPort)),
// and add a new connection from the old connection's source to the reroute node
new AddConnectionRefactoring(new ScadConnection(Graph, toDelete.From, toDelete.FromPort,
rerouteNode, 0))
);
return;
}
// TODO: BUG - if you have a textfield selected and then right click on a connection
// two refactorings will run at the same time, crashing the editor
// otherwise we want to delete the connection
PerformRefactorings("Delete connection", new DeleteConnectionRefactoring(toDelete));
return;
}
if (!TryGetNodeAtPosition(relativePosition, out var node))
{
// right-click in empty space yields you the add dialog
AddNodeOrOpenDialog(relativePosition);
return;
}
_context.ShowPopupMenu(RequestContext.ForNode(Graph, node, position));
}
private bool TryGetNodeAtPosition(Vector2 relativePosition, out ScadNode node)
{
var firstMatchingWidget = _widgets.Values
.FirstOrDefault(it => new Rect2(it.Offset, it.RectSize).HasPoint(relativePosition));
node = firstMatchingWidget?.BoundNode;
return node != null;
}
public override void _Input(InputEvent evt)
{
// whenever meta is pressed eat it and inject a copy with control instead
if (OS.GetName() == "OSX" && evt is InputEventKey key && key.Scancode == (uint) KeyList.Meta)
{
// eat it
GetViewport().SetInputAsHandled();
// inject a copy with Control
var copy = new InputEventKey();
copy.Scancode = (uint)KeyList.Control;
copy.Pressed = key.Pressed;
Input.ParseInputEvent(copy);
}
}
public override void _GuiInput(InputEvent evt)
{
if (evt.IsSelectAll())
{
SelectNodes(Graph.GetAllNodes().ToList());
return;
}
if (evt.IsCopy())
{
CopySelection();
return;
}
if (evt.IsDuplicate())
{
DuplicateSelection();
}
if (evt.IsExtract())
{
ExtractSelection();
return;
}
if (evt.IsCut())
{
CutSelection();
return;
}
if (evt.IsPaste())
{
PasteClipboard();
return;
}
if (evt.IsEditComment())
{
CommentSelection();
return;
}
if (evt.IsShowHelp())
{
ShowHelpForSelection();
return;
}
if (evt.IsStraighten())
{
StraightenSelection();
return;
}
if (evt.IsAlignLeft())
{
AlignSelectionLeft();
return;
}
if (evt.IsAlignRight())
{
GD.Print("Align right");
AlignSelectionRight();
return;
}
if (evt.IsAlignTop())
{
AlignSelectionTop();
return;
}
if (evt.IsAlignBottom())
{
AlignSelectionBottom();
return;
}
if (evt.IsSelectUnused())
{
SelectUnusedNodes();
return;
}
if (evt is InputEventMouseButton { Pressed: false } &&
_pendingDisconnect != null)
{
Log.Debug("Resolving pending disconnect");
PerformRefactorings("Remove connection", new DeleteConnectionRefactoring(_pendingDisconnect));
_pendingDisconnect = null;
return;
}
if (evt is InputEventMouseMotion mouseMotionEvent)
{
// if the mouse is over a node, don't do anything
var mousePositionRelativeToTheGraph = GlobalToGraphRelative(mouseMotionEvent.GlobalPosition);
if (TryGetNodeAtPosition(mousePositionRelativeToTheGraph, out _))
{
ClearHighlightedConnection();
return;
}
// find the closest connection to the mouse
// we use * zoom because GlobalToGraphRelative will give us the coordinates in graph space and
// we need to convert them to screen space but relative to the graph widget.
var mousePosition = mousePositionRelativeToTheGraph * Zoom;
var closest = _connectionTree.GetNearestNeighbours(new[] { mousePosition.x, mousePosition.y }, 1);
if (closest.Length <= 0)
{
ClearHighlightedConnection();
return;
}
// are we close enough to the connection?
var distance = new Vector2(closest[0].Point[0], closest[0].Point[1]).DistanceTo(mousePosition);
if (distance > 20)
{
ClearHighlightedConnection();
return;
}
// no need to redraw, if the connection is the same
if (_highlightedConnection == closest[0].Value.Connection)
{
return;
}
_highlightedConnection = closest[0].Value.Connection;
_connectionHighlightPoints = closest[0].Value.BezierPoints;
_connectionHighlightLayer.Update();
}
}
public void CopySelection()
{
_context.CopyNodesToClipboard(Graph, GetSelectedNodes());
_context.ShowInfo("Copied selection to clipboard.");
}
public void DuplicateSelection()
{
var selectedNodes = GetSelectedNodes().ToHashSet();
var duplicatedNodes =
Graph.CloneSelection(_context.CurrentProject, selectedNodes, out var mappedIds);
// make a list of all refactorings that need to be performed
var refactorings = new List<Refactoring>();
var pastePosition = CalculatePastePosition();
// since we already make a clone of the nodes, we set "pasteCopy" to false here. This will also help
// with the ID-remapping for the extra connections.
var pasteNodesRefactoring = new PasteNodesRefactoring(Graph, duplicatedNodes, pastePosition, false);
refactorings.Add(pasteNodesRefactoring);
// when shift is pressed we want to also duplicate all connections that go into or out of the duplicated nodes
// https://github.com/derkork/openscad-graph-editor/issues/62
if (KeyMap.IsShiftPressed())
{
// get a list of all connections that go into or out of the duplicated nodes
// but only to nodes which are not part of the selection.
var connectionsToDuplicate = Graph.GetAllConnections()
// the connection can either be from or to the duplicated nodes but not both
.Where(it => selectedNodes.Contains(it.From) ^ selectedNodes.Contains(it.To))
.ToList();
// now walk over the connections and create a copy of each, replacing either the from or
// to node with the new node.
var duplicatedConnections = connectionsToDuplicate
.Select(it =>
{
var from = selectedNodes.Contains(it.From)
? duplicatedNodes.ById(mappedIds[it.From.Id])
: it.From;
var to = selectedNodes.Contains(it.To) ? duplicatedNodes.ById(mappedIds[it.To.Id]) : it.To;
return new ScadConnection(Graph, from, it.FromPort, to, it.ToPort);
})
.Select(it => new AddConnectionRefactoring(it))
.ToList();
refactorings.AddRange(duplicatedConnections);
}
var refactoringData = _context.PerformRefactorings("Duplicate nodes", refactorings);
// select the pasted nodes
if (refactoringData.TryGetData(PasteNodesRefactoring.PastedNodes, out var pastedNodes))
{
SelectNodes(pastedNodes);
}
}
public void PasteClipboard()
{
var clipboardContents = _context.GetNodesInClipboard();
var pastePosition = CalculatePastePosition();
var refactoringData = _context.PerformRefactoring(
"Paste nodes", new PasteNodesRefactoring(Graph, clipboardContents, pastePosition));
if (refactoringData.TryGetData(PasteNodesRefactoring.PastedNodes, out var pastedNodes))
{
SelectNodes(pastedNodes);
}
}
public void CutSelection()
{
// first copy the nodes to the clipboard
var selectedNodes = GetSelectedNodes();
_context.CopyNodesToClipboard(Graph, selectedNodes);
// then run refactorings to delete them
// this will implicitly also delete the connections.
var deleteRefactorings = selectedNodes
.Select(it => new DeleteNodeRefactoring(Graph, it))
.ToList();
_context.PerformRefactorings("Cut nodes", deleteRefactorings);
}
private Vector2 CalculatePastePosition()
{
return GlobalMousePositionToGraphOffset(GetGlobalMousePosition());
}
public void ExtractSelection()
{
_context.PerformRefactoring("Extract nodes",
new ExtractInvokableRefactoring(Graph, GetSelectedNodes().ToList()));
}
public void StraightenSelection()
{
_graphLayout.StraightenConnections(GetSelectedWidgets());
}
public void CommentSelection()
{
if (!TryGetSingleSelectedNode("Please select exactly one node to edit its comment.", out var selection))
{
return;
}
_context.EditComment(Graph, selection);
}
public void ShowHelpForSelection()
{
if (!TryGetSingleSelectedNode("Please select exactly one node to show its help.", out var selection))
{
return;
}
_context.ShowHelp(Graph, selection);
}
public void AlignSelectionBottom()
{
_graphLayout.AlignNodesBottom(GetSelectedWidgets());
}
public void AlignSelectionTop()
{
_graphLayout.AlignNodesTop(GetSelectedWidgets());
}
public void AlignSelectionRight()
{
_graphLayout.AlignNodesRight(GetSelectedWidgets());
}
public void AlignSelectionLeft()
{
_graphLayout.AlignNodesLeft(GetSelectedWidgets());
}
public void SelectUnusedNodes()
{
var unusedNodes = Graph.FindUnusedNodes();
SelectNodes(unusedNodes);
_context.ShowInfo(unusedNodes.Count > 0
? "Selected all nodes which do not contribute to the output."
: "All nodes in this graph currently contribute to the output.");
}
private void ClearHighlightedConnection()
{
if (_highlightedConnection == null)
{
return;
}
_highlightedConnection = null;
_connectionHighlightPoints = null;
_connectionHighlightLayer.Update();
}
/// <summary>
/// Calculates a graph offset from a given global mouse position. If the mouse position is outside the graph
/// the returned position will be at 100,100 relative to the currently visible graph area.
/// </summary>
private Vector2 GlobalMousePositionToGraphOffset(Vector2 globalMousePosition)
{
// get the offset over the mouse position
var globalRect = GetGlobalRect();
// default position
var result = ScrollOffset + new Vector2(100, 100);
if (globalRect.HasPoint(globalMousePosition))
{
// if the mouse is inside the graph, paste the nodes at the mouse position
result = GlobalToGraphRelative(globalMousePosition);
}
return result;
}
private void AddNodeOrOpenDialog(Vector2 position, [CanBeNull] ScadNode otherNode = null,
PortId otherPort = default)
{
// when shift is pressed this means we want to have a reroute node.
if (KeyMap.IsShiftPressed())
{
var rerouteNode = NodeFactory.Build<RerouteNode>();
// when also CTRL/CMD is down, we make the reroute node a wireless node
if (KeyMap.IsCmdOrControlPressed())
{
rerouteNode.IsWireless = true;
}
rerouteNode.Offset = position;
_context.PerformRefactoring("Add reroute node",
new AddNodeRefactoring(Graph, rerouteNode, otherNode, otherPort));
return;
}
// otherwise let the user choose a node.
var requestContext = new RequestContext().WithNodePort(Graph, otherNode, otherPort)
.WithPosition(position);
_context.OpenAddDialog(requestContext);
}
private List<ScadNode> GetSelectedNodes()
{
return _selection.Select(it => _widgets[it].BoundNode).ToList();
}
private List<ScadNodeWidget> GetSelectedWidgets()
{
return _selection.Select(it => _widgets[it]).ToList();
}
private void OnDisconnectionRequest(string fromWidgetName, int fromSlot, string toWidgetName, int toSlot)
{
Log.Debug("Disconnect request");
var connection = new ScadConnection(Graph, ScadNodeForWidgetName(fromWidgetName), fromSlot,
ScadNodeForWidgetName(toWidgetName), toSlot);
// the disconnect is not done until the user has released the mouse button, so in case this is called
// while the mouse is still down, just visually disconnect, but don't do a refactoring yet.
if (Input.IsMouseButtonPressed((int)ButtonList.Left))
{
DisconnectNode(fromWidgetName, fromSlot, toWidgetName, toSlot);
_pendingDisconnect = connection;
}
}
private void OnConnectionToEmpty(string fromWidgetName, int fromPort, Vector2 releasePosition)
{
AddNodeOrOpenDialog(LocalToGraphRelative(releasePosition), ScadNodeForWidgetName(fromWidgetName),
PortId.Output(fromPort));
}
private void OnConnectionFromEmpty(string toWidgetName, int toPort, Vector2 releasePosition)
{
AddNodeOrOpenDialog(LocalToGraphRelative(releasePosition), ScadNodeForWidgetName(toWidgetName),
PortId.Input(toPort));
}
private void OnNodeSelected(ScadNodeWidget node)
{
_selection.Add(node.BoundNode.Id);
// if the node was a wireless node, then temporarily show its wireless connection
if (node.BoundNode.IsWirelessReroute())
{
var relevantConnections = Graph.GetAllConnections()
.Where(it => it.To == node.BoundNode);
foreach (var connection in relevantConnections)
{
ConnectNode(_widgets[connection.From.Id].Name, connection.FromPort, _widgets[connection.To.Id].Name,
connection.ToPort);
}
}
HighlightBoundNodes();
}
private void HighlightBoundNodes()
{
var boundNodes = Graph.GetAllNodes().OfType<IAmBoundToOtherNode>().ToList();
foreach (var node in boundNodes)
{
var partnerNodeId = node.OtherNodeId;
var partnerNodeWidget = _widgets[partnerNodeId];
// if the node's partner node is not currently selected, but the node is selected then highlight the node's partner node, otherwise clear it.
if (!_selection.Contains(partnerNodeId) && _selection.Contains(((ScadNode)node).Id))
{
partnerNodeWidget.Modulate = Colors.Yellow;
}
else
{
partnerNodeWidget.Modulate = Colors.White;
}
}
}
private void OnNodeUnselected(ScadNodeWidget node)
{
_selection.Remove(node.BoundNode.Id);
// if the node was a wireless node, then hide its wireless connections
if (node.BoundNode.IsWirelessReroute())
{
var relevantConnections = Graph.GetAllConnections()
.Where(it => it.To == node.BoundNode);
foreach (var connection in relevantConnections)
{
DisconnectNode(_widgets[connection.From.Id].Name, connection.FromPort,
_widgets[connection.To.Id].Name, connection.ToPort);
}
}
HighlightBoundNodes();
}
// Godot 3.4
public void DeleteSelection()
{
var refactorings =
GetSelectedNodes()
.Select(it => new DeleteNodeRefactoring(Graph, it, KeyMap.IsKeepConnectionsPressed()))
.ToList();
_selection.Clear();
_context.PerformRefactorings("Delete selection", refactorings);
}
// Godot 3.5
private void DeleteSelection([UsedImplicitly] Node[] _)
{
DeleteSelection();
}
public void AddNode()
{
AddNodeOrOpenDialog(ScrollOffset + GetGlobalRect().Size / 2);
}
public override void _UnhandledKeyInput(InputEventKey evt)
{
if (KeyMap.IsRemoveNodePressed(evt))
{
DeleteSelection();
}
}
private void OnConnectionRequest(string fromWidgetName, int fromPort, string toWidgetName, int toPort)
{
var connection = new ScadConnection(Graph, ScadNodeForWidgetName(fromWidgetName), fromPort,
ScadNodeForWidgetName(toWidgetName), toPort);
if (_pendingDisconnect != null)
{
if (_pendingDisconnect.RepresentsSameAs(connection))
{
Log.Debug("Re-connected pending node");
ConnectNode(fromWidgetName, fromPort, toWidgetName, toPort);
_pendingDisconnect = null;
return;
}
}
var refactorings = new AddConnectionRefactoring(connection);
PerformRefactorings("Create connection", refactorings);
}
private ScadNode ScadNodeForWidgetName(string widgetName)
{
return this.AtPath<ScadNodeWidget>(widgetName).BoundNode;
}
private void PerformRefactorings(string description, params Refactoring[] refactorings)
{
_context.PerformRefactorings(description, refactorings);
}
private (Vector2 Start, Vector2 End) GetConnectionPoints(ScadConnection connection)
{
var fromNode = _widgets[connection.From.Id];
var toNode = _widgets[connection.To.Id];
var fromPort = fromNode.GetConnectionOutputPosition(connection.FromPort) + fromNode.Offset * Zoom;
var toPort = toNode.GetConnectionInputPosition(connection.ToPort) + toNode.Offset * Zoom;
return (fromPort, toPort);
}
/// <summary>
/// Bezier interpolation function for the connection lines. This is copied straight from the Godot source code.
/// </summary>
private static Vector2 BezierInterpolate(float t, Vector2 start, Vector2 control1, Vector2 control2,
Vector2 end)
{
var omt = 1.0f - t;
var omt2 = omt * omt;
var omt3 = omt2 * omt;
var t2 = t * t;
var t3 = t2 * t;
return start * omt3 + control1 * omt2 * t * 3.0f + control2 * omt * t2 * 3.0f + end * t3;
}
/// <summary>
/// Segment baking function for the connection lines. This is copied straight from the Godot source code + some modifications.
/// </summary>
private static void BakeSegment2D(List<Vector2> points, float pBegin, float pEnd,
Vector2 pA, Vector2 pOut, Vector2 pB, Vector2 pIn, int pDepth, int pMinDepth, int pMaxDepth,
float pTol, float pMaxLength, ref int lines)
{
var mp = pBegin + (pEnd - pBegin) * 0.5f;
var beg = BezierInterpolate(pBegin, pA, pA + pOut, pB + pIn, pB);
var mid = BezierInterpolate(mp, pA, pA + pOut, pB + pIn, pB);
var end = BezierInterpolate(pEnd, pA, pA + pOut, pB + pIn, pB);
var na = (mid - beg).Normalized();
var nb = (end - mid).Normalized();
var dp = Mathf.Rad2Deg(Mathf.Acos(na.Dot(nb)));
// the maxLength check is to ensure we don't get longer straight segments of lines because we want
// to keep the points that make up the lines evenly spaced, so our KDTree can find the closest point
// to the mouse cursor and we don't get "holes" in the detection when the mouse is over a straight line
// that technically only needs two points to be drawn. We insert additional points this way so
// if the line is longer the algorithm can still detect it if the mouse cursor is in the middle of it.
if (pDepth >= pMinDepth && (dp < pTol || pDepth >= pMaxDepth) && (beg - end).Length() < pMaxLength)
{
points.Add(end);
lines++;
}
else
{
BakeSegment2D(points, pBegin, mp, pA, pOut, pB, pIn, pDepth + 1, pMinDepth, pMaxDepth, pTol, pMaxLength,
ref lines);
BakeSegment2D(points, mp, pEnd, pA, pOut, pB, pIn, pDepth + 1, pMinDepth, pMaxDepth, pTol, pMaxLength,
ref lines);
}
}
/// <summary>
/// Builds a bezier curve from the given points and returns a list of points that make up the curve.
/// </summary>
private List<Vector2> BakeBezierLine(Vector2 fromPoint, Vector2 toPoint, float bezierRatio)
{
//cubic bezier code
var diff = toPoint.x - fromPoint.x;
var cpLen = GetConstant("bezier_len_pos") * bezierRatio;
var cpNegLen = GetConstant("bezier_len_neg") * bezierRatio;
var cpOffset = diff > 0
? Mathf.Min(cpLen, diff * 0.5f)
: Mathf.Max(Mathf.Min(cpLen - diff, cpNegLen), -diff * 0.5f);
var c1 = new Vector2(cpOffset * Zoom, 0);
var c2 = new Vector2(-cpOffset * Zoom, 0);
var lines = 0;
var points = new List<Vector2> { fromPoint };
BakeSegment2D(points, 0, 1, fromPoint, c1, toPoint, c2, 0, 3, 9, 3, 20, ref lines);
points.Add(toPoint);
return points;
}
private void OnScrollOffsetChanged([UsedImplicitly] Vector2 _)
{
// when the scroll offset/zoom is changed our previously highlighted connection is no longer valid
// so we need to clear it, otherwise we'll get an artifact where the connection is still highlighted
// in its old position and scale
ClearHighlightedConnection();
}
private void OnConnectionHighlightLayerDraw()
{
if (_highlightedConnection == null)
{
return;
}
_connectionHighlightLayer.DrawPolyline(_connectionHighlightPoints.ToArray(), new Color(1, 1, 1, 0.5f), 5,
true);
}
private bool TryGetSingleSelectedNode(string warningMessage, out ScadNode result)
{
var selection = GetSelectedNodes().ToList();
if (selection.Count != 1)
{
result = default;
_context.ShowInfo(warningMessage);
return false;
}
result = selection.First();
return true;
}
}
}7192f5216This file is pinned to the selected release. Download the original to open it in your design tool.