Connecting two WPF canvas elements by a line, without using anchors? - wpf

I have a canvas for diagramming, and want to join nodes in the diagram by directed lines (arrow ends).
I tried the anchor approach, where lines only attach at specific points on the nodes but that did not work for me, it looked like crap.
I simply want a line from the centre of each object to the other, and stop the line at the nodes' edge in order for the arrow end to show properly. But finding the edge of a canvas element to test intersections against has proven difficult.
Any ideas?

I got a method working using the bounding box of the element. It is not perfect, since my elements are not perfectly rectangular, but it looks OK.
Basically I find the bounding box of the element in Canvas coordinates by:
private static Rect GetBounds(FrameworkElement element, UIElement visual)
{
return new Rect(
element.TranslatePoint(new Point(0, 0), visual),
element.TranslatePoint(new Point(element.ActualWidth, element.ActualHeight), visual));
}
Then I find the intersection of the centre-to-centre line against each of the four sides of the bounding box, and use that intersection point to connect the two elements by a Line shape.
I found the intersection code at Third Party Ninjas:
http://thirdpartyninjas.com/blog/2008/10/07/line-segment-intersection/
private void ProcessIntersection()
{
float ua = (point4.X - point3.X) * (point1.Y - point3.Y) - (point4.Y - point3.Y) * (point1.X - point3.X);
float ub = (point2.X - point1.X) * (point1.Y - point3.Y) - (point2.Y - point1.Y) * (point1.X - point3.X);
float denominator = (point4.Y - point3.Y) * (point2.X - point1.X) - (point4.X - point3.X) * (point2.Y - point1.Y);
intersection = coincident = false;
if (Math.Abs(denominator) <= 0.00001f)
{
if (Math.Abs(ua) <= 0.00001f && Math.Abs(ub) <= 0.00001f)
{
intersection = coincident = true;
intersectionPoint = (point1 + point2) / 2;
}
}
else
{
ua /= denominator;
ub /= denominator;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1)
{
intersection = true;
intersectionPoint.X = point1.X + ua * (point2.X - point1.X);
intersectionPoint.Y = point1.Y + ua * (point2.Y - point1.Y);
}
}
}
And voilá! The lines are now drawn as if they go from the centre of each node to the other, but stops approximately at the node's edge so the arrow end is visible.
An improvement of this method would be to test against the actual edge of the node itself, such as for elliptical nodes, but I have yet to find a WPF method that provides me with a Geometry or Path that I can test against.

Related

Implementing stroke drawing similar to InkCanvas

My problem effectively boils down to accurate mouse movement detection.
I need to create my own implementation of an InkCanvas and have succeeded for the most part, except for drawing strokes accurately.
void OnMouseMove(object sneder, MouseEventArgs e)
{
var position = e.GetPosition(this);
if (!Rect.Contains(position))
return;
var ratio = new Point(Width / PixelDisplay.Size.X, Height / PixelDisplay.Size.Y);
var intPosition = new IntVector(Math2.FloorToInt(position.X / ratio.X), Math2.FloorToInt(position.Y / ratio.Y));
DrawBrush.Draw(intPosition, PixelDisplay);
UpdateStroke(intPosition); // calls CaptureMouse
}
This works. The Bitmap (PixelDisplay) is updated and all is well. However, any kind of quick mouse movement causes large skips in the drawing. I've narrowed down the problem to e.GetPosition(this), which blocks the event long enough to be inaccurate.
There's this question which is long beyond revival, and its answers are unclear or simply don't have a noticeable difference.
After some more testing, the stated solution and similar ideas fail specifically because of e.GetPosition.
I know InkCanvas uses similar methods after looking through the source; detect the device, if it's a mouse, get its position and capture. I see no reason for the same process to not work identically here.
I ended up being able to partially solve this.
var position = e.GetPosition(this);
if (!Rect.Contains(position))
return;
if (DrawBrush == null)
return;
var ratio = new Point(Width / PixelDisplay.Size.X, Height / PixelDisplay.Size.Y);
var intPosition = new IntVector(Math2.FloorToInt(position.X / ratio.X), Math2.FloorToInt(position.Y / ratio.Y));
// Calculate pixel coordinates based on the control height
var lastPoint = CurrentStroke?.Points.LastOrDefault(new IntVector(-1, -1));
// Uses System.Linq to grab the last stroke, if it exists
PixelDisplay.Lock();
// My special locking mechanism, effectively wraps Bitmap.Lock
if (lastPoint != new IntVector(-1, -1)) // Determine if we're in the middle of a stroke
{
var alphaAdd = 1d / new IntVector(intPosition.X - lastPoint.Value.X, intPosition.Y - lastPoint.Value.Y).Magnitude;
// For some interpolation, calculate 1 / distance (magnitude) of the two points.
// Magnitude formula: Math.Sqrt(Math.Pow(X, 2) + Math.Pow(Y, 2));
var alpha = 0d;
var xDiff = intPosition.X - lastPoint.Value.X;
var yDiff = intPosition.Y - lastPoint.Value.Y;
while (alpha < 1d)
{
alpha += alphaAdd;
var adjusted = new IntVector(
Math2.FloorToInt((position.X + (xDiff * alpha)) / ratio.X),
Math2.FloorToInt((position.Y + (yDiff * alpha)) / ratio.Y));
// Inch our way towards the current intPosition
DrawBrush.Draw(adjusted, PixelDisplay); // Draw to the bitmap
UpdateStroke(intPosition);
}
}
DrawBrush.Draw(intPosition, PixelDisplay); // Draw the original point
UpdateStroke(intPosition);
PixelDisplay.Unlock();
This implementation interpolates between the last point and the current one to fill in any gaps. It's not perfect when using a very small brush size for example, but is a solution nonetheless.
Some remarks
IntVector is a lazily implemented Vector2 by me, just using integers instead.
Math2 is a helper class. FloorToInt is short for (int)MathF.Round(...))

Mandelbrot Set Zoom Distortion in C

I'm writing a C program to render a Mandelbrot set and currently, I'm stuck with trying out to figure out how to zoom in properly.
I want for the zoom to be able to follow the mouse pointer on the screen - so that the fractal zooms in into the cursor position.
I have a window defined by:
# define WIDTH 800
# define HEIGHT 600
My Re_max, Re_min, Im_Max, Im_Min are defined and initialized as follows:
man->re_max = 2.0;
man->re_min = -2.0;
man->im_max = 2.0;
man->im_min = -2.0;
The interpolation value (more on in later) is defined and initialized as follows:
pos->interp = 1.0;
To map the pixel coordinates to the center of the screen, I'm using the position function:
void position(int x, int y, t_mandel *man)
{
double *s_x;
double *s_y;
s_x = &man->pos->shift_x;
s_y = &man->pos->shift_y;
man->c_re = (x / (WIDTH / (man->re_max - man->re_min)) + man->re_min) + *s_x;
man->c_im =(y / (HEIGHT / (man->im_max - man->re_min)) + man->im_min) + *s_y;
man->c_im *= 0.8;
}
To zoom in, I first get the coordinates of the mouse pointer and map them to the visible area given by the rectangle defined by the (Re_Max, Re_Min, Im_Max, Im_Min) using this function, where x and y are coordinates of the pointer on a screen:
int mouse_move(int x, int y, void *p)
{
t_fract *fract;
t_mandel *man;
fract = (t_fract *)p;
man = fract->mandel;
fract->mouse->Re = x / (WIDTH / (man->re_max - man->re_min)) + man->re_min;
fract->mouse->Im = y / (HEIGHT / (man->im_max - man->re_min)) + man->im_min;
return (0);
}
This function is called when a mouse wheel scroll is registered. The actual zooming is achieved by this function:
void zoom_control(int key, t_fract *fract)
{
double *interp;
interp = &fract->mandel->pos->interp;
if (key == 5) // zoom in
{
*interp = 1.0 / 1.03;
apply_zoom(fract->mandel, fract->mouse->Re, fract->mouse->Im, *interp);
}
else if (key == 4) // zoom out
{
*interp = 1.0 * 1.03;
apply_zoom(fract->mandel, fract->mouse->Re, fract->mouse->Im, *interp);
}
}
Which calls this:
void apply_zoom(t_mandel *man, double m_re, double m_im, double interp)
{
man->re_min = interpolate(m_re, man->re_min, interp);
man->im_min = interpolate(m_im, man->im_min, interp);
man->re_max = interpolate(m_re, man->re_max, interp);
man->im_max = interpolate(m_im, man->im_max, interp);
}
I have a simple interpolate function to redefine the area bounding rectangle:
double interpolate(double start, double end, double interp)
{
return (start + ((end - start) * interp));
}
So the problem is:
My code renders the fractal like this -
Mandelbrot set
But when I try to zoom in as described with the mouse, instead of going nicely "in", it just distorts like this, the image just sort of collapses onto itself instead of actually diving into the fractal.
I would really appreciate help with this one as I've been stuck on it for a while now.
If you please could also explain the actual math behind your solutions, I would be overjoyed!
Thank you!
After quite a bit of headache and a lot of paper wasted on recalculation interpolation methods, I've realized that the way I've mapped my complex numbers on-screen was incorrect, to begin with. Reworking my mapping method solved my problem, so I'll share what have I done.
-------------------------------OLD WAY--------------------------------------
I've initialized my Re_max, Re_min, Im_Max, Im_Min values, which define the visible area in the following way:
re_max = 2.0;
re_min = -2.0;
im_max = 2.0;
im_min = -2.0;
Then, I used this method to convert my on-screen coordinates to the complex numbers used to calculate the fractal (note that the coordinates used for mapping the mouse position for zoom interpolation and coordinates used to calculate the fractal itself use the same method):
Re = x / (WIDTH / (re_max - re_min)) + re_min;
Im = y / (HEIGHT / (im_max - re_min)) + im_min;
However, this way I didn't take the screen ratio into account and I've neglected the fact (due to a lack of knowledge) that the y coordinate on-screen is inverse (at least in my program) - negative direction is up, positive is down.
This way, when I tried to zoom in with my interpolation, naturally, the image distorted.
------------------------------CORRECT WAY-----------------------------------
When defining the bounding rectangle of the set, maximum imaginary im_max) part should be calculated, based on the screen ratio, to avoid image distortion when the display window isn't a square:
re_max = 2.0;
re_min = -2.0;
im_min = -2.0;
im_max = im_min + (re_max - re_min) * HEIGHT / WIDTH;
To map the on-screen coordinates to the complex numbers, I first found the "coordinate-to-number* ratio, which is equal to *rectangle length / screen width*:
re_factor = (re_max - re_min) / (WIDTH - 1);
im_factor = (im_max - im_min) / (HEIGHT - 1);
Then, I've mapped my pixel coordinates to the real and imaginary part of a complex number used in calculations like so:
c_re = re_min + x * re_factor;
c_im = im_max - y * im_factor;
After implementing those changes, I was finally able to smoothly zoom into the mouse position without any distortion or image "jumps".
If I understand you correctly, you want to make the point where the mouse is located a new center of the image, and change the scale of the image by a factor of 1.03. I would try something like that:
Your position() and mouse_move() functions remain the same.
in zoom_control() just change the way how you set the new value of interpolation, it should not be a fixed constant, but should be based on its current value. Also, pass the new scaling factor to the apply_zoom():
void zoom_control(int key, t_fract *fract)
{
double *interp;
interp = &fract->mandel->pos->interp;
double zoom_factor = 1.03;
if (key == 5) // zoom in
{
*interp /= zoom_factor;
apply_zoom(fract->mandel, fract->mouse->Re, fract->mouse->Im, 1.0 / zoom_factor);
}
else if (key == 4) // zoom out
{
*interp *= zoom_factor;
apply_zoom(fract->mandel, fract->mouse->Re, fract->mouse->Im, zoom_factor);
}
}
modify the apply zoom function:
void apply_zoom(t_mandel *man, double m_re, double m_im, double zoom_factor)
{
// Calculate the new ranges along the real and imaginary axes.
// They are equal to the current ranges multiplied by the zoom_factor.
double re_range = (man->re_max - man->re_min) * zoom_factor;
double im_range = (man->im_max - man->im_min) * zoom_factor;
// Set the new min/max values for real and imaginary axes with the center at
// mouse coordinates m_re and m_im.
man->re_min = m_re - re_range / 2;
man->re_max = m_re + re_range / 2;
man->im_min = m_im - im_range / 2;
man->im_max = m_im + im_range / 2;
}

Problems with Instantiating Next and Previous Prefab in Array (Unity 3D)

I am working on a simple virtual tour app where I place a camera inside a sphere, map the sphere with a 360 photo, and click on arrow objects (sprites) to navigate forward and backward. My script below instantiates the next sphere in the array (mapped with the next 360 photo) while destroying the current sphere, simulating forward movement in the application. It seems to work OK.
public void Update()
{
if (Input.GetMouseButtonDown(0))
{
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
if (hit.collider != null)
{
Destroy(currentObject);
currentIndex++;
currentIndex = currentIndex >= Spheres.Length ? 0 : currentIndex;
currentObject = Instantiate(Spheres[currentIndex]);
}
}
}
}
The question I have is how do I reverse the order if the array so I can click the "back" arrow to instantiate the previous prefab in the array.
I thought this was a simple as currentIndex--; but I cannot get it to work. Any help is great appreciated.
pic of virtual tour in editor
Just invert the wrap-around using
currentIndex--;
if(currentIndex < 0) currentIndex = Spheres.Length - 1;
You also want to do the same for the ++ way btw:
currentIndex++;
if(currentIndex > Spheres.Length - 1) currentIndex = 0;
note the - 1! Your index should never reach Spehres.Length since indices are 0-based. So the last element in an array always has index = array.Length - 1.
A slightly easier way for having a wrap-around in positive way would btw be simply
currentIndex = (currentIndex + 1) % Spheres.Length;

How to detect a 'pinch out' in a list of containers?

I want to be able to pinch two containers in a list of containers away from each other to insert a new empty container between them. Similar to how the iPhone app “Clear” inserts new tasks (see for example the very first picture on this page https://www.raywenderlich.com/22174/how-to-make-a-gesture-driven-to-do-list-app-part-33 - the small red container is inserted when the two sorounding containers are pinched away from each other). Any hints on how I can achieve this in Codename One?
Normally you would override the pinch method to implement pinch to zoom or similar calls. However, this won't work in this case as the pinch will exceed component boundaries and it wouldn't work.
The only way I can think of doing this is to override the pointerDragged(int[],int[]) method in Form and detect the pinch motion as growing to implement this. You can check out the code for pinch in Component.java as it should be a good base for this:
public void pointerDragged(int[] x, int[] y) {
if (x.length > 1) {
double currentDis = distance(x, y);
// prevent division by 0
if (pinchDistance <= 0) {
pinchDistance = currentDis;
}
double scale = currentDis / pinchDistance;
if (pinch((float)scale)) {
return;
}
}
pointerDragged(x[0], y[0]);
}
private double distance(int[] x, int[] y) {
int disx = x[0] - x[1];
int disy = y[0] - y[1];
return Math.sqrt(disx * disx + disy * disy);
}
Adding the entry is simple, just place a blank component in the place and grow its preferred size until it reaches the desired size.

How can I check whether a location is within a MapPolygon in the Silverlight Bing Maps control?

I have a MapPolygon which covers a certain area on the Silverlight Bing Maps control,
and I would like to know if a particular Location is located within this MapPolygon.
I have tried the following code which doesen't return the result I want because it only checks if the tested location is one of the vertices of the MapPolygon, and doesn't check if this Location is contained within this MapPolygon.
polygon.Locations.Contains(new Location(this.Site.Latitude, this.Site.Longitude, this.Site.Altitude));
Is it also possible to determine if two MapPolygons intersect one another?
The polygon.Locations is a list of points defining the polygon.
You have to make a method to find if your point is inside the polygon.
Use something like this (not tested if compiles):
static bool PointInPolygon(LocationCollection polyPoints, Location point)
{
if (polyPoints.Length < 3)
{
return false;
}
bool inside = false;
Location p1, p2;
//iterate each side of the polygon
Location oldPoint = polyPoints[polyPoints.Count - 1];
foreach(Location newPoint in polyPoints)
{
//order points so p1.lat <= p2.lat;
if (newPoint.Latitude > oldPoint.Latitude)
{
p1 = oldPoint;
p2 = newPoint;
}
else
{
p1 = newPoint;
p2 = oldPoint;
}
//test if the line is crossed and if so invert the inside flag.
if ((newPoint.Latitude < point.Latitude) == (point.Latitude <= oldPoint.Latitude)
&& (point.Longitude - p1.Longitude) * (p2.Latitude - p1.Latitude)
< (p2.Longitude - p1.Longitude) * (point.Latitude - p1.Latitude))
{
inside = !inside;
}
oldPoint = newPoint;
}
return inside;
}
And call it like this:
if (PointInPolygon(polygon.Locations, new Location(this.Site.Latitude, this.Site.Longitude, this.Site.Altitude)))
{
//do something
}
Sure both of these things are fairly trivial, take a look at the following article. http://msdn.microsoft.com/en-us/library/cc451895.aspx It gives good methods for Bounding Box, Radius, and Polygon Search. Particularity take note of the pointInPolygon method.

Resources