Click here to Skip to main content
15,886,763 members
Articles / Programming Languages / C#

DSGraphEdit: A Reasonable Facsimile of Microsoft's GraphEdit in .NET

Rate me:
Please Sign up or sign in to vote.
4.93/5 (79 votes)
28 Jan 2018MIT7 min read 297.2K   10K   142  
A library for adding DirectShow GraphEdit-like abilities to .NET applications
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Collections.Generic;
using System.Text;

using DaggerLib.Core;
using DaggerLib.Interfaces;

namespace DaggerLib.UI.Windows
{
    /// <summary>
    /// Class to multi-select Noodles and Nodes
    /// </summary>
    public class Selector : ISelector
    {
        private Point _startPoint;
        private Point _previousPoint;
        private Point _currentPoint;
        private Panel _parent;
        private bool _initialPoint = true;

        //all the nodes in the panel we are tracking
        private List<IDaggerUINode> _allNodes;

        //all the noodle in the panel we are tracking
        private List<IDaggerNoodle> _allNoodles;

        //the nodes we have selected
        private List<IDaggerUINode> _selectedNodes;

        //the noodles we have selected
        private List<IDaggerNoodle> _selectedNoodles;

        //are we currently dragging the rectangle with the mouse?
        private bool _tracking = true;

        public Selector(Panel parent, Point startPoint, List<IDaggerNoodle> noodles, List<IDaggerUINode> nodes)
        {
            _parent = parent;
            CurrentPoint = _previousPoint = _startPoint = startPoint;
            _allNodes = nodes;
            _allNoodles = noodles;
            _selectedNodes = new List<IDaggerUINode>();
            _selectedNoodles = new List<IDaggerNoodle>();
        }

        public Selector(DaggerUIGraph graph)
        {
            _tracking = false;
            _selectedNodes = new List<IDaggerUINode>(graph.AllNodes);
            _selectedNoodles = new List<IDaggerNoodle>(graph.AllNoodles);
        }

        public Selector(List<IDaggerUINode> nodes, List<IDaggerNoodle> noodles)
        {
            _tracking = false;
            _selectedNodes = new List<IDaggerUINode>(nodes);
            _selectedNoodles = new List<IDaggerNoodle>(noodles);
        }

        /// <summary>
        /// Get's the rectangle that is selected
        /// </summary>
        public Rectangle SelectionArea
        {
            get
            {
                int left = Math.Min(_startPoint.X, _currentPoint.X);
                int right = Math.Max(_startPoint.X, _currentPoint.X);
                int top = Math.Min(_startPoint.Y, _currentPoint.Y);
                int bottom = Math.Max(_startPoint.Y, _currentPoint.Y);

                return new Rectangle(left, top, right - left, bottom - top);
            }
        }

        //the nodes we have selected
        public List<IDaggerUINode> SelectedNodes
        {
            get
            {
                return _selectedNodes;
            }
            set
            {
                _selectedNodes = value;
            }
        }

        //the noodles we have selected
        public List<IDaggerNoodle> SelectedNoodles
        {
            get
            {
                return _selectedNoodles;
            }
            set
            {
                _selectedNoodles = value;
            }
        }

        /// <summary>
        /// Gets if the selection if currently empty
        /// </summary>
        public bool IsEmpty
        {
            get
            {
                return (SelectedNodes.Count == 0 && SelectedNoodles.Count == 0);
            }
        }

        /// <summary>
        /// Gets or sets the current mouse position of selection
        /// </summary>
        public Point CurrentPoint
        {
            get
            {
                return _currentPoint;
            }
            set
            {
                _previousPoint = _currentPoint;
                _currentPoint = value;

                Rectangle newrect = new Rectangle(SelectionArea.X, SelectionArea.Y, SelectionArea.Width, SelectionArea.Height);
                newrect.Offset(_parent.AutoScrollPosition.X, _parent.AutoScrollPosition.Y);

                //if this is not the initial point, erase previous frame
                if (!_initialPoint)
                {
                    int left = Math.Min(_startPoint.X, _previousPoint.X);
                    int right = Math.Max(_startPoint.X, _previousPoint.X);
                    int top = Math.Min(_startPoint.Y, _previousPoint.Y);
                    int bottom = Math.Max(_startPoint.Y, _previousPoint.Y);
                    Rectangle oldrect = new Rectangle(left, top, right - left, bottom - top);
                    oldrect.Offset(_parent.AutoScrollPosition.X, _parent.AutoScrollPosition.Y);

                    ControlPaint.DrawReversibleFrame(_parent.RectangleToScreen(oldrect), Color.Black, FrameStyle.Dashed);
                }

                ControlPaint.DrawReversibleFrame(_parent.RectangleToScreen(newrect), Color.Black, FrameStyle.Dashed);

                //show that we have moved at least once
                _initialPoint = false;
            }
        }

        /// <summary>
        /// Returns true if we are currently tracking the mouse position.
        /// If setting to false, search and add items to selection lists.
        /// </summary>
        public bool Tracking
        {
            get
            {
                return _tracking;
            }
            set
            {
                _tracking = value;
                if (!_tracking)
                {
                    //we've stopped tracking, so calculate the intersection of all Nodes and Noodles

                    //if it was just a click, expand the selected area to 7x7 and ignore Node testing
                    if ((SelectionArea.Width == 0) && (SelectionArea.Height == 0))
                    {
                        _startPoint.X -= 3;
                        _currentPoint.X += 3;
                        _startPoint.Y -= 3;
                        _currentPoint.Y += 3;

                        UpdateSelected(true);
                    }
                    else
                    {
                        //wasn't just a click, so erase the Dragging Frame and perform hit tests on noodles AND nodes
                        Rectangle newrect = new Rectangle(SelectionArea.X, SelectionArea.Y, SelectionArea.Width, SelectionArea.Height);
                        newrect.Offset(_parent.AutoScrollPosition.X, _parent.AutoScrollPosition.Y);
                        ControlPaint.DrawReversibleFrame(_parent.RectangleToScreen(newrect), Color.Black, FrameStyle.Dashed);

                        UpdateSelected(false);
                    }
                }
            }
        }

        /// <summary>
        /// Calculate the nodes and noodles that fall within the selction area
        /// </summary>
        /// <param name="IsClick">true if the user clicked without dragging</param>
        private void UpdateSelected(bool IsClick)
        {
            SelectedNoodles.Clear();

            //hit test Nodes if it wasn't just a click

            if (_allNodes != null)
            {
                foreach (DaggerUINode node in _allNodes)
                {
                    if (!IsClick)
                    {
                        Rectangle noderect = new Rectangle(node.Location, node.Size);

                        if (_parent is ScrollableControl)
                        {
                            // modify noderect to account for scrollbar positions
                            noderect.Offset((_parent as ScrollableControl).AutoScrollPosition.X * -1, (_parent as ScrollableControl).AutoScrollPosition.Y * -1);
                        }

                        if (noderect.IntersectsWith(SelectionArea))
                        {
                            SelectedNodes.Add((IDaggerUINode)node);
                        }
                    }
                }
            }

            //hit test Noodles
            foreach (DaggerNoodle noodle in _allNoodles)
            {
                //get the points from the noodle path
                PointF[] pathpoints = noodle.path.PathPoints;

                //check each segment to see if it intersects with the selction area
                for (int i = 0; i < pathpoints.Length - 1; i++)
                {
                    if (lineRectangleIntersection(SelectionArea, pathpoints[i], pathpoints[i + 1]))
                    {
                        SelectedNoodles.Add(noodle);
                        break;
                    }
                }
            }
        }

        /// <summary>
        /// Determine if a Rectangle contains or intersects a line segment
        /// </summary>
        /// <param name="rect"></param>
        /// <param name="sega"></param>
        /// <param name="segb"></param>
        /// <returns></returns>
        internal static bool lineRectangleIntersection(Rectangle rect, PointF sega, PointF segb)
        {
            //are either segment points actually inside the rectangle?
            if (rect.Contains((int)sega.X, (int)sega.Y) || rect.Contains((int)segb.X, (int)segb.Y))
            {
                return true;
            }

            PointF ul = new PointF(rect.X, rect.Y);
            PointF ur = new PointF(rect.Right, rect.Y);
            PointF bl = new PointF(rect.X, rect.Bottom);
            PointF br = new PointF(rect.Right, rect.Bottom);

            if (lineSegmentIntersection(sega.X, sega.Y, segb.X, segb.Y, ul.X, ul.Y, ur.X, ur.Y))
            {
                return true;
            }

            if (lineSegmentIntersection(sega.X, sega.Y, segb.X, segb.Y, bl.X, bl.Y, br.X, br.Y))
            {
                return true;
            }

            if (lineSegmentIntersection(sega.X, sega.Y, segb.X, segb.Y, ul.X, ul.Y, br.X, br.Y))
            {
                return true;
            }

            if (lineSegmentIntersection(sega.X, sega.Y, segb.X, segb.Y, ur.X, ur.Y, br.X, br.Y))
            {
                return true;
            }

            return false;
        }

        internal static bool lineRectangleIntersection(Rectangle rect,Point p1, Point p2)
        {
            //top
            if(lineSegmentIntersection(p1,p2,new Point(rect.Left,rect.Top),new Point(rect.Right,rect.Top)))
            {
                return true;
            }

            //bottom
            if(lineSegmentIntersection(p1,p2,new Point(rect.Left,rect.Bottom),new Point(rect.Right,rect.Bottom)))
            {
                return true;
            }

            //left
            if(lineSegmentIntersection(p1,p2,new Point(rect.Left,rect.Top),new Point(rect.Left,rect.Bottom)))
            {
                return true;
            }

            //right
            if (lineSegmentIntersection(p1, p2, new Point(rect.Right, rect.Top), new Point(rect.Right, rect.Bottom)))
            {
                return true;
            }

            // no intersection
            return false;
        }

        /// <summary>
        /// Returns true if segment p1,p2 intersects segment p3,p4
        /// </summary>
        internal static bool lineSegmentIntersection(Point p1, Point p2, Point p3, Point p4)
        {
            return lineSegmentIntersection(p1.X, p1.Y, p2.X, p2.Y, p3.X, p3.Y, p4.X, p4.Y);
        }

        /// <summary>
        /// Returns true if segment AB and CD intersect
        /// </summary>
        internal static bool lineSegmentIntersection(
            float Ax, float Ay,
            float Bx, float By,
            float Cx, float Cy,
            float Dx, float Dy)
        {

            float distAB, theCos, theSin, newX, ABpos;

            //  Fail if either line segment is zero-length.
            if (Ax == Bx && Ay == By || Cx == Dx && Cy == Dy) return false;

            //  (1) Translate the system so that point A is on the origin.
            Bx -= Ax; By -= Ay;
            Cx -= Ax; Cy -= Ay;
            Dx -= Ax; Dy -= Ay;

            //  Discover the length of segment A-B.
            distAB = (float)Math.Sqrt(Bx * Bx + By * By);

            //  (2) Rotate the system so that point B is on the positive X axis.
            theCos = Bx / distAB;
            theSin = By / distAB;
            newX = Cx * theCos + Cy * theSin;
            Cy = Cy * theCos - Cx * theSin; Cx = newX;
            newX = Dx * theCos + Dy * theSin;
            Dy = Dy * theCos - Dx * theSin; Dx = newX;

            //  Fail if segment C-D doesn't cross line A-B.
            if (Cy < 0f && Dy < 0f || Cy >= 0f && Dy >= 0f) return false;

            //  (3) Discover the position of the intersection point along line A-B.
            ABpos = Dx + (Cx - Dx) * Dy / (Dy - Cy);

            //  Fail if segment C-D crosses line A-B outside of segment A-B.
            if (ABpos < 0f || ABpos > distAB) return false;

            //  Success.
            return true;
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer (Senior)
United States United States
AKA Rich Insley.

I have over 25 years experience in programming, and I'm completely self taught. (Except for one year at California State University Fresno where I had to learn the God awful language Miranda (http://miranda.org.uk/). I've spent 10 years as a Paratrooper in the US Army during the Clinton Administration.

Comments and Discussions