WPF Diagramming. Lines pointing at the center of the element. Calculating angles for render transforms.


All my previous posts from "WPF Diagramming" series were presenting simple lines connected to the center of each shape object. For the first steps it is very convenient not to care about complex maths and just bind endpoints of the line to the centers of objects being connected. The rest will be triggered by WPF.

One day you will decide to implement the arrowheads for your connectors because it’s a mandatory requirement for any diagramming project. But how can you draw the arrowhead for the line that is pointing to the center of the shape? You won’t see that arrowhead in any way because your shape will overlap your connector endpoint. There are at least three ways of implementing this requirement to your project.

Changing layers

You simply change the ZIndex of your geometry to bring the connectors one level upper than your shapes. This will make the arrowheads to be seen but this also make the whole diagramming layout so ugly that I no more dwell on this approach.

Sticking to shape border

Guess this can be regarded as the most common approach when developing diagramming tools or controls as I’ve looked though a variety of implementations. You always know the size of the shape at runtime so you always can get all the border sizes and positions of it. When the connection between two shapes is being established the start point of the line is bound to the bottom border of source shape and the end point is simply bound to the upper border of the target shape. Upon performing a drag operations for both shape objects the line coordinates are recalculated to apply the scheme described. Of course this also influence the layers ordering because the connectors are to be placed one layer upper than the shapes are.

As for me I don’t like this approach at all. I’ve spent several days to perform the third approach that is more close to what I wanted for my article and for my home project.

Pointing at the center of the element

This means that whenever you drag your target shape source shape connector will always point to the center of it. It’s up to the layout design where to place the start point of the line (shape center or some border) but the end point with arrowhead will point at the center of the target shape thus not touching the shape visually. This is what I’m going to describe later on.

 

When the line is pointing at the center of some object it may seem visually, that all you need is to bind line endpoint to the center of the shape and then make it shorter a bit. But when you try to turn your "shortened" line into coordinates you encounter one simple question "What will be the endpoint coordinates and how I can calculate them dynamically?". Upon Googling you then find some answers than make you feel rather unhappy and willing to use the "Sticking to shape border" approach I mentioned before 🙂 The answer is that you have to use angles calculation, trigonometry, vector algebra, geometry and a lot of stuff do understand.

I’ve used Google heavily for a last week to get small and easy solution to be implement to my project and not to write this article at all 🙂 There were some interesting pieces of information on DirectX and XNA forums that were nearly close to what I wanted but not exactly the perfect and easy ones. By that time I’ve already understood what I needed to implement.

Some frightening theory…

First you have to get the angle between you source shape and the target shape. Then you imagine and abstract circle that is drawn around your source shape. After that you get the point on the circle by the angle known already by that time. This point will be exactly the required coordinate of your line endpoint.

As I’ve mentioned at the beginning of the article it took me several days to get the smallest code for the angles calculations possible for performing the process. I could simply give you the sample code to use but when I remembered my own feeling when looking on dozens of code implementations without any supplementary info I decided to spend some more time preparing the drawings and description of the whole process. This can be extremely useful when you’ll decide to customize the process or adopt it to your own needs.

For the sample we will be using a simple button element placed somewhere on the form. According to mouse position it will be rotated to face the cursor. Also one line will be drawn from the cursor position with arrowhead pointing to the center of the button.

 

Getting angle between two vectors

From the Vector Algebra books you can get all the required information on how the vectors are used and how the angles between them are calculated. Microsoft did a really huge piece of work towards this. Presentation Framework (WindowsBase assembly to be exact) contains all vector procedures you need to feel happy implementing this stuff. All you need is two vectors to be compared.

angles_1

You have vector a (0,100) and vector b (100, 100). Using the static method Vector.AngleBetween(b, a) you will get the exact angle between them in degrees. In our case vector a is based on our button location and vector be is created using the current mouse position on the canvas.

 

Turn element positions into vectors

Of course to get the angle between two vectors we need to have some vectors to be calculated. In a real life we have only the coordinates of the button element and can freely get the position of mouse cursor according to the canvas at runtime. Mouse can be placed anywhere like the mouse, so we don’t have much to do with zero coordinates and the image above. How this all can be converted to vectors?

Assume you have a button positioned with it’s center to point (100, 150). Just to note somewhere: we can get the center of element on the canvas by the following template

Point p1 = new Point(
                Canvas.GetLeft(button1) + button1.ActualWidth / 2,
                Canvas.GetTop(button1) + button1.ActualHeight / 2);

So let’s imagine again that our button center is at (100, 150) and the mouse cursor position (think of dragging the shape in this case or simply some another static shape position) is actually at point (200, 150). These two points are quite enough for us to get the rectangle covering our working area. We are taking the center of the button as a starting point of our coordinate system.

angles_2

From the picture given you can easily get the idea. You get the line a X(100, 150) – Y(100, 50) by the button element "Left" position and mouse "Top" position. You get the line b X(100, 150) – Y(200, 150) by center of the button and the mouse cursor position. So we have to lines looking like the vectors we exactly need from the first picture and we know the working area bounds as rectangle X1(100, 150) – Y1(100, 50) – X2(200, 50) – Y2(200, 150). Again to keep simplicity we have a 45 degree angle between them and both lines are of 100 length. To get the two vectors needed we simply get the differences between left and right side coordinates of our rectangle by Y axis.

Next issue we come across is that Microsoft uses left top orientation of coordinates while we require left bottom for your calculations. So additionally we have to reverse our rectangle positions upside down to meet the requirement. Finally as we want our button center to be the starting point for our coordinate system we have to align the rectangle to zero point removing the left button canvas offset from all the four points according to X axis.

So what we have after all is

Vector A = (100, 150) – (100, 50) by Y axis = (100, 100)

Vector B = (200, 150) – (200, 50) by Y axis = (200, 100)

and aligned to zero based positioning by 100

Vector A = (0, 100)

Vector B = (100, 100)

Looks like we have got our first picture describing vectors angle calculation 😉

Next issue you will get when you move the mouse to another three quarters of coordinate system drawn in the picture. According to the approach described you will have to deal with negative coordinates and so have something like on the picture

angles_3

Your internal coordinate system seems to be divided into 4 parts each receiving 0 to 90 or 0 to -90 degrees (see the picture). Additionally when you will move the mouse to the X or Y axis you will get the 0 degrees instead of 0, 90, 180 and 360 degrees. What is happening?

Well this is quite expected behavior as upon mouse cursor moving to to negative coordinates our rectangle and so the line a endpoint goes to negative position either line b. To fix the behavior we need functionality similar to common clock. Line a should always be static: being positive and laying on the same place according to Y axis. Math.Abs() method will help you much in doing the calculations 😉

I intended to write the smallest code possible for the calculations needed. The quickest approach I guess is splitting our coordinate system into two parts. Our 360 degrees rotation is simply performed by doing 180 and -180 degrees. There’s no visual difference but code behind checks become extremely small. So placing line a statically according to Y axis gives us the opportunity of getting +/- 180 degrees.

The only issue will arise is 90 degrees positioning. As two Y points of our line b and so vector lay on the same axis their subtraction will give you a zero value and the further angles calculation will give you 0 degrees respectively. We have to provide additional check for this case and restrict subtraction.

 

Calculating angle using C#

Here’s the source code for getting the angle between button on the canvas and mouse pointer. I’ve optimized the rectangle points usage and vector creation additionally. Also I’ve implemented the simple Y axis check.

Point p1 = new Point(
    Canvas.GetLeft(button1) + button1.ActualWidth / 2,
    Canvas.GetTop(button1) + button1.ActualHeight / 2);

Point p2 = e.GetPosition(this);

Vector a = new Vector(0, Math.Abs((p1.Y != p2.Y) ? p1.Y – p2.Y : p2.Y));           
Vector b = new Vector(p2.X – p1.X, p1.Y – p2.Y);
double angle = Vector.AngleBetween(b, a);

And that’s all 🙂 so much talking and so small to do. According to two points you have the angle in degrees between button1 element and mouse pointer position. You can use this code freely anywhere you need getting angles between two elements on the canvas 🙂

To rotate the button element to face the mouse pointer you can use the following sample

button1.RenderTransformOrigin = new Point(0.5, 0.5);
button1.RenderTransform = new RotateTransform(angle);

 

Positioning the line to point at the center of the element

As we have already calculated the angle between the button element and mouse pointer we can get the right position the line with arrowhead should be bound to.

First we must have an abstract circle around which our arrowhead will be moving in case of angle changed. The radius of circle should be large enough not to allow the arrowhead touch the element at any angle. This is up to you what values to provide, I’ve simply used the button’s "radiuses" according to X and Y axis

double radius = button1.ActualWidth/2 + button1.ActualHeight / 2;

according to the school course of geometry we know already how to find a point on a circle knowing the radius and angle

double x = radius * Math.Cos(angle);
double y = radius * Math.Sin(angle);

But here we also come across two issues. First is that .net Math.Cos and Math.Sin get the values in radians though Vector.AngleBetween returns a value in degrees. The second is that this well known formula for point evaluation on circle does calculations in counter-clockwise mode when we require exactly the clockwise result. To fix these two issues we transform the angle to the radians and invert the calculation formula to clockwise mode:

double x = radius * Math.Sin(angle * Math.PI / 180);
double y = radius * Math.Cos(angle * Math.PI / 180);

Note the inverted usage of Sin and Cos according to the traditional usage. Also you can increase the calculation performance by predefining the value of (PI/180) but this is up to you 😉

That’s all. You can use this coordinates to assign the value of the line endpoint with arrowhead.

 

Getting all together

Notes: For the line with arrowhead I used the sample by Charles Petzold. For a detailed information you really should refer to his own blog.

Window1.xaml

<Window x:Class="Rotation.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300" Loaded="Window_Loaded">
    <Canvas Name="myCanvas">
        <Button Canvas.Left="100" Canvas.Top="150" Height="20" Name="button1" Width="40">Button</Button>
        <CheckBox Canvas.Left="10" Canvas.Top="10" Panel.ZIndex="1" Height="15.554" Name="chButtonRotate" Width="119.988">Rotate button</CheckBox>
    </Canvas>
</Window>

Window1.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Petzold.Media2D;

namespace Rotation
{    
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        ArrowLine directionLine;

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            this.MouseMove += new MouseEventHandler(Window1_MouseMove);
            button1.RenderTransformOrigin = new Point(0.5, 0.5);

            directionLine = new ArrowLine();
            directionLine.Stroke = Brushes.Red;
            directionLine.StrokeThickness = 2;            
            myCanvas.Children.Add(directionLine);

            button1.RenderTransformOrigin = new Point(0.5, 0.5);
        }

        void Window1_MouseMove(object sender, MouseEventArgs e)
        {
            Point p1 = new Point(
                Canvas.GetLeft(button1) + button1.ActualWidth / 2,
                Canvas.GetTop(button1) + button1.ActualHeight / 2);

            Point p2 = e.GetPosition(this);

            Vector a = new Vector(0, Math.Abs((p1.Y != p2.Y) ? p1.Y - p2.Y : p2.Y));            
            Vector b = new Vector(p2.X - p1.X, p1.Y - p2.Y);
            double angle = Vector.AngleBetween(b, a); 
           
            this.Title = angle.ToString();

            if (chButtonRotate.IsChecked == true)
                button1.RenderTransformOrigin = new Point(0.5, 0.5);
                button1.RenderTransform = new RotateTransform(angle);

            double radius = button1.ActualWidth/2 + button1.ActualHeight / 2;

            double x = radius * Math.Sin(angle * Math.PI / 180);
            double y = radius * Math.Cos(angle * Math.PI / 180);

            Point touchPoint = new Point(p1.X + x, p1.Y - y);
            
            directionLine.X1 = p2.X;
            directionLine.Y1 = p2.Y;
            directionLine.X2 = touchPoint.X;
            directionLine.Y2 = touchPoint.Y;
        }
    }
}

 

What you get on the screen

_angles_1 _angles_2

Source code for the article

Advertisements

One thought on “WPF Diagramming. Lines pointing at the center of the element. Calculating angles for render transforms.

  1. Awesome diagramming work. Keep it up!
     

    Perhaps down the road you could cover using the model-viewmodel-view pattern. It is what the Microsoft Robotics Studio Visual Programming Language diagram uses (a nice diagramming tool written in WPF)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s