diff --git a/.gitignore b/.gitignore index 566550e..051ce2b 100644 --- a/.gitignore +++ b/.gitignore @@ -279,3 +279,5 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk +NodeEditor/NodeEditor.nuspec +NodeEditor/nuget.exe diff --git a/NodeEditor/DrawInfo.cs b/NodeEditor/DrawInfo.cs new file mode 100644 index 0000000..2ad2991 --- /dev/null +++ b/NodeEditor/DrawInfo.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; + +namespace NodeEditor +{ + /// + /// Class used to provide custom drawing elements to node editor, such connection styles, node graphics etc. + /// + public class DrawInfo + { + private static Pen boldPen; + + /// + /// Gets pen used to draw connections between nodes. + /// + /// Type indicating data type passing through connection + /// If user is currently dragging connection between nodes + /// Pen to draw line + public virtual Pen GetConnectionStyle(Type dataType, bool isConnecting) + { + if(isConnecting) + { + return boldPen ?? (boldPen = new Pen(Brushes.Black, 3)); + } + return Pens.Black; + } + } +} diff --git a/NodeEditor/IZoomable.cs b/NodeEditor/IZoomable.cs new file mode 100644 index 0000000..d802795 --- /dev/null +++ b/NodeEditor/IZoomable.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NodeEditor +{ + public interface IZoomable + { + /// + /// Indicates current zoom (scale), higher value means bigger elements + /// + float Zoom { get; set; } + } +} diff --git a/NodeEditor/NodeEditor.csproj b/NodeEditor/NodeEditor.csproj index b979688..b0daf36 100644 --- a/NodeEditor/NodeEditor.csproj +++ b/NodeEditor/NodeEditor.csproj @@ -2,7 +2,7 @@ - Debug + Release AnyCPU {2A51D621-3E0D-4293-A2AD-964721BFFF7B} Library @@ -20,6 +20,7 @@ DEBUG;TRACE prompt 4 + bin\Debug\NodeEditor.xml pdbonly @@ -28,6 +29,7 @@ TRACE prompt 4 + bin\Release\NodeEditor.xml @@ -41,11 +43,13 @@ + + diff --git a/NodeEditor/NodeVisual.cs b/NodeEditor/NodeVisual.cs index e6e9825..e6b95af 100644 --- a/NodeEditor/NodeVisual.cs +++ b/NodeEditor/NodeVisual.cs @@ -242,8 +242,11 @@ public SizeF GetNodeBounds() var csize = new SizeF(); if (CustomEditor != null) { - csize = new SizeF(CustomEditor.ClientSize.Width + 2 + 80 +SocketVisual.SocketHeight*2, - CustomEditor.ClientSize.Height + HeaderHeight + 8); + var zoomable = CustomEditor as IZoomable; + float zoom = zoomable == null ? 1f : (float)Math.Sqrt(zoomable.Zoom); + + csize = new SizeF(CustomEditor.ClientSize.Width/zoom + 2 + 80 +SocketVisual.SocketHeight*2, + CustomEditor.ClientSize.Height/zoom + HeaderHeight + 8); } var inputs = GetInputs().Length; @@ -358,11 +361,11 @@ internal void Execute(INodesContext context) } } - internal void LayoutEditor() + internal void LayoutEditor(float zoom) { if (CustomEditor != null) { - CustomEditor.Location = new Point((int)( X + 1 + 40 + SocketVisual.SocketHeight), (int) (Y + HeaderHeight + 4)); + CustomEditor.Location = new Point((int)(zoom * (X + 1 + 40 + SocketVisual.SocketHeight)), (int)(zoom * (Y + HeaderHeight + 4))); } } } diff --git a/NodeEditor/NodesControl.cs b/NodeEditor/NodesControl.cs index 1676075..4ac94d8 100644 --- a/NodeEditor/NodesControl.cs +++ b/NodeEditor/NodesControl.cs @@ -34,7 +34,7 @@ namespace NodeEditor /// Main control of Node Editor Winforms /// [ToolboxBitmap(typeof(NodesControl), "nodeed")] - public partial class NodesControl : UserControl + public partial class NodesControl : UserControl, IZoomable { internal class NodeToken { @@ -76,6 +76,37 @@ public INodesContext Context } } + + private float zoom = 1f; + + /// + /// Indicates scale factor of visual appearance of node graph + /// + public float Zoom + { + get { return zoom; } + set + { + zoom = value; + PassZoomToNodes(); + Invalidate(); + } + } + + private DrawInfo customDrawInfo = new DrawInfo(); + + public DrawInfo CustomDrawInfo + { + get { return customDrawInfo; } + set { customDrawInfo = value; } + } + + + /// + /// If true, drawing events will use fast painting modes instead of high quality ones + /// + public bool PreferFastRendering { get; set; } + /// /// Occurs when user selects a node. In the object will be passed node settings for unplugged inputs/outputs. /// @@ -92,6 +123,11 @@ public INodesContext Context /// public event Action OnShowLocation = delegate { }; + /// + /// Use this event to paint custom background - scaling is already applied into Graphics context. + /// + public event EventHandler OnPaintNodesBackground = delegate { }; + private readonly Dictionary allContextItems = new Dictionary(); private Point lastMouseLocation; @@ -157,15 +193,18 @@ private void TimerOnTick(object sender, EventArgs eventArgs) private void NodesControl_Paint(object sender, PaintEventArgs e) { - e.Graphics.SmoothingMode = SmoothingMode.HighQuality; - e.Graphics.InterpolationMode = InterpolationMode.HighQualityBilinear; + e.Graphics.SmoothingMode = PreferFastRendering ? SmoothingMode.HighSpeed : SmoothingMode.HighQuality; + e.Graphics.InterpolationMode = PreferFastRendering ? InterpolationMode.Low : InterpolationMode.HighQualityBilinear; + e.Graphics.ScaleTransform(zoom, zoom); + + OnPaintNodesBackground(sender, e); - graph.Draw(e.Graphics, PointToClient(MousePosition), MouseButtons); + graph.Draw(e.Graphics, GetLocationWithZoom(PointToClient(MousePosition)), MouseButtons, PreferFastRendering, customDrawInfo); if (dragSocket != null) { - var pen = new Pen(Color.Black, 2); - NodesGraph.DrawConnection(e.Graphics, pen, dragConnectionBegin, dragConnectionEnd); + var pen = customDrawInfo.GetConnectionStyle(dragSocket.Type, true); + NodesGraph.DrawConnection(e.Graphics, pen, dragConnectionBegin, dragConnectionEnd, PreferFastRendering); } if (selectionStart != PointF.Empty) @@ -189,10 +228,12 @@ private static RectangleF MakeRect(PointF a, PointF b) private void NodesControl_MouseMove(object sender, MouseEventArgs e) { - var em = PointToScreen(e.Location); + var loc = GetLocationWithZoom(e.Location); + var em = PointToScreen(loc); + if (selectionStart != PointF.Empty) { - selectionEnd = e.Location; + selectionEnd = loc; } if (mdown) { @@ -201,7 +242,7 @@ private void NodesControl_MouseMove(object sender, MouseEventArgs e) node.X += em.X - lastmpos.X; node.Y += em.Y - lastmpos.Y; node.DiscardCache(); - node.LayoutEditor(); + node.LayoutEditor(zoom); } if (graph.Nodes.Exists(x => x.IsSelected)) { @@ -241,7 +282,9 @@ private void NodesControl_MouseMove(object sender, MouseEventArgs e) } private void NodesControl_MouseDown(object sender, MouseEventArgs e) - { + { + var loc = GetLocationWithZoom(e.Location); + if (e.Button == MouseButtons.Left) { selectionStart = PointF.Empty; @@ -255,7 +298,7 @@ private void NodesControl_MouseDown(object sender, MouseEventArgs e) var node = graph.Nodes.OrderBy(x => x.Order).FirstOrDefault( - x => new RectangleF(new PointF(x.X, x.Y), x.GetHeaderSize()).Contains(e.Location)); + x => new RectangleF(new PointF(x.X, x.Y), x.GetHeaderSize()).Contains(loc)); if (node != null && !mdown) { @@ -266,9 +309,10 @@ private void NodesControl_MouseDown(object sender, MouseEventArgs e) if (node.CustomEditor != null) { node.CustomEditor.BringToFront(); + PassZoomToNodeCustomEditor(node.CustomEditor); } mdown = true; - lastmpos = PointToScreen(e.Location); + lastmpos = PointToScreen(loc); Refresh(); } @@ -276,11 +320,11 @@ private void NodesControl_MouseDown(object sender, MouseEventArgs e) { var nodeWhole = graph.Nodes.OrderBy(x => x.Order).FirstOrDefault( - x => new RectangleF(new PointF(x.X, x.Y), x.GetNodeBounds()).Contains(e.Location)); + x => new RectangleF(new PointF(x.X, x.Y), x.GetNodeBounds()).Contains(loc)); if (nodeWhole != null) { node = nodeWhole; - var socket = nodeWhole.GetSockets().FirstOrDefault(x => x.GetBounds().Contains(e.Location)); + var socket = nodeWhole.GetSockets().FirstOrDefault(x => x.GetBounds().Contains(loc)); if (socket != null) { if ((ModifierKeys & Keys.Control) == Keys.Control) @@ -319,15 +363,15 @@ private void NodesControl_MouseDown(object sender, MouseEventArgs e) dragSocket = socket; dragSocketNode = nodeWhole; } - dragConnectionBegin = e.Location; - dragConnectionEnd = e.Location; + dragConnectionBegin = loc; + dragConnectionEnd = loc; mdown = true; - lastmpos = PointToScreen(e.Location); + lastmpos = PointToScreen(loc); } } else { - selectionStart = selectionEnd = e.Location; + selectionStart = selectionEnd = loc; } } if (node != null) @@ -339,6 +383,15 @@ private void NodesControl_MouseDown(object sender, MouseEventArgs e) needRepaint = true; } + private void PassZoomToNodeCustomEditor(Control control) + { + var zoomable = control as IZoomable; + if (zoomable != null) + { + zoomable.Zoom = zoom * zoom; + } + } + private bool IsConnectable(SocketVisual a, SocketVisual b) { var input = a.Input ? a : b; @@ -370,6 +423,8 @@ private Assembly AssemblyResolver(AssemblyName assemblyName) private void NodesControl_MouseUp(object sender, MouseEventArgs e) { + var loc = GetLocationWithZoom(e.Location); + if (selectionStart != PointF.Empty) { var rect = MakeRect(selectionStart, selectionEnd); @@ -382,10 +437,10 @@ private void NodesControl_MouseUp(object sender, MouseEventArgs e) { var nodeWhole = graph.Nodes.OrderBy(x => x.Order).FirstOrDefault( - x => new RectangleF(new PointF(x.X, x.Y), x.GetNodeBounds()).Contains(e.Location)); + x => new RectangleF(new PointF(x.X, x.Y), x.GetNodeBounds()).Contains(loc)); if (nodeWhole != null) { - var socket = nodeWhole.GetSockets().FirstOrDefault(x => x.GetBounds().Contains(e.Location)); + var socket = nodeWhole.GetSockets().FirstOrDefault(x => x.GetBounds().Contains(loc)); if (socket != null) { if (IsConnectable(dragSocket,socket) && dragSocket.Input != socket.Input) @@ -461,7 +516,8 @@ private void AddToMenu(ToolStripItemCollection items, NodeToken token, string pa private void NodesControl_MouseClick(object sender, MouseEventArgs e) { - lastMouseLocation = e.Location; + var loc = GetLocationWithZoom(e.Location); + lastMouseLocation = loc; if (Context == null) return; @@ -547,9 +603,10 @@ private void NodesControl_MouseClick(object sender, MouseEventArgs e) if (ctrl != null) { ctrl.Tag = nv; - Controls.Add(ctrl); + Controls.Add(ctrl); + PassZoomToNodeCustomEditor(ctrl); } - nv.LayoutEditor(); + nv.LayoutEditor(zoom); } graph.Nodes.Add(nv); @@ -561,6 +618,27 @@ private void NodesControl_MouseClick(object sender, MouseEventArgs e) } } + private Point GetLocationWithZoom(Point location) + { + var zx = location.X / zoom; + var zy = location.Y / zoom; + var zl = new Point((int)zx, (int)zy); + return zl; + } + + private void PassZoomToNodes() + { + foreach(var node in graph.Nodes) + { + if(node.CustomEditor != null) + { + PassZoomToNodeCustomEditor(node.CustomEditor); + node.DiscardCache(); + node.LayoutEditor(zoom); + } + } + } + private void ChangeSelectedNodesColor() { ColorDialog cd = new ColorDialog(); @@ -598,7 +676,11 @@ private void DuplicateSelectedNodes() } graph.Nodes.ForEach(x => x.IsSelected = false); cloned.ForEach(x => x.IsSelected = true); - cloned.Where(x => x.CustomEditor != null).ToList().ForEach(x => x.CustomEditor.BringToFront()); + cloned.Where(x => x.CustomEditor != null).ToList().ForEach(x => + { + x.CustomEditor.BringToFront(); + PassZoomToNodeCustomEditor(x.CustomEditor); + }); graph.Nodes.AddRange(cloned); Invalidate(); } @@ -966,8 +1048,9 @@ private NodeVisual DeserializeNode(BinaryReader br) { ctrl.Tag = nv; Controls.Add(ctrl); + PassZoomToNodeCustomEditor(ctrl); } - nv.LayoutEditor(); + nv.LayoutEditor(zoom); } return nv; } diff --git a/NodeEditor/NodesGraph.cs b/NodeEditor/NodesGraph.cs index 72663e8..d3edb04 100644 --- a/NodeEditor/NodesGraph.cs +++ b/NodeEditor/NodesGraph.cs @@ -29,22 +29,20 @@ internal class NodesGraph { internal List Nodes = new List(); internal List Connections = new List(); + static Pen executionPen; + static Pen executionPen2; - public void Draw(Graphics g, Point mouseLocation, MouseButtons mouseButtons) - { - g.InterpolationMode = InterpolationMode.Low; - g.SmoothingMode = SmoothingMode.HighSpeed; - + public void Draw(Graphics g, Point mouseLocation, MouseButtons mouseButtons, bool preferFastRendering, DrawInfo info) + { foreach (var node in Nodes) { g.FillRectangle(Brushes.Black, new RectangleF(new PointF(node.X+6, node.Y+6), node.GetNodeBounds())); } g.FillRectangle(new SolidBrush(Color.FromArgb(200, Color.White)), g.ClipBounds); - - var cpen = Pens.Black; - var epen = new Pen(Color.Gold, 3); - var epen2 = new Pen(Color.Black, 5); + + executionPen = (executionPen ?? new Pen(Color.Gold, 3)); + executionPen2 = (executionPen2 ?? new Pen(Color.Black, 5)); foreach (var connection in Connections.Where(x=>x.IsExecution)) { var osoc = connection.OutputNode.GetSockets().FirstOrDefault(x => x.Name == connection.OutputSocketName); @@ -54,8 +52,8 @@ public void Draw(Graphics g, Point mouseLocation, MouseButtons mouseButtons) var begin = beginSocket.Location + new SizeF(beginSocket.Width / 2f, beginSocket.Height / 2f); var end = endSocket.Location += new SizeF(endSocket.Width / 2f, endSocket.Height / 2f); - DrawConnection(g, epen2, begin, end); - DrawConnection(g, epen, begin, end); + DrawConnection(g, executionPen2, begin, end, preferFastRendering); + DrawConnection(g, executionPen, begin, end, preferFastRendering); } foreach (var connection in Connections.Where(x => !x.IsExecution)) { @@ -65,8 +63,9 @@ public void Draw(Graphics g, Point mouseLocation, MouseButtons mouseButtons) var endSocket = isoc.GetBounds(); var begin = beginSocket.Location + new SizeF(beginSocket.Width / 2f, beginSocket.Height / 2f); var end = endSocket.Location += new SizeF(endSocket.Width / 2f, endSocket.Height / 2f); - - DrawConnection(g, cpen, begin, end); + + var cpen = info.GetConnectionStyle(connection.InputSocket.Type, false); + DrawConnection(g, cpen, begin, end, preferFastRendering); } @@ -77,13 +76,10 @@ public void Draw(Graphics g, Point mouseLocation, MouseButtons mouseButtons) } } - public static void DrawConnection(Graphics g, Pen pen, PointF output, PointF input) - { - g.InterpolationMode = InterpolationMode.HighQualityBilinear; - g.SmoothingMode = SmoothingMode.HighQuality; - + public static void DrawConnection(Graphics g, Pen pen, PointF output, PointF input, bool preferFastRendering = false) + { if (input == output) return; - const int interpolation = 48; + int interpolation = preferFastRendering ? 16 : 48; PointF[] points = new PointF[interpolation]; for (int i = 0; i < interpolation; i++)