Drawing line attached to two shapes - c#

NOTE: I'm not looking for a XAML Solution.
I'm having trouble figuring out how to attach a line to two shapes. The best visible representation of what I'm looking for would be two balls attached to both ends of a straight stick. The problem I'm having is on how to display the line which is dependent on both the positions of ball01's and ball02's center position. As of now, both balls display as I want it, but when ball02 moves away from ball01 (ball02 starts off centered on ball01), the line is not visible.
ball01 = new Ellipse() { Height = BIG_SIZE, Width = BIG_SIZE };
ball01.Fill = baseBrush;
ball01.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
setBall01X(e.GetPosition(canvas).X - (BIG_SIZE / 2));
setBall01Y(e.GetPosition(canvas).Y - (BIG_SIZE / 2));
Canvas.SetLeft(ball01, getBall01X());
Canvas.SetTop(ball01, getBall01Y());
canvas.Children.Add(ball01);
ball02 = new Ellipse() { Height = SMALL_SIZE, Width = SMALL_SIZE };
ball02.Fill = childBrush;
ball02.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
setBall02X(e.GetPosition(canvas).X - (SMALL_SIZE / 2));
setBall02Y(e.GetPosition(canvas).Y - (SMALL_SIZE / 2));
Canvas.SetLeft(ball02, getBall02X());
Canvas.SetTop(ball02, getBall02Y());
canvas.Children.Add(ball02);
// line's X's and Y's are to point to the center of both balls
// Regardless of where the balls move.
line01 = new Line()
{
X1 = getBall01X() + (BIG_SIZE / 2),
Y1 = getBall01Y() + (BIG_SIZE / 2),
X2 = getBall02X() + (SMALL_SIZE / 2),
Y2 = getBall02Y() + (SMALL_SIZE / 2)
};
line01.Fill = baseBrush;
line01.SnapsToDevicePixels = true;
line01.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
line01.StrokeThickness = 2;
// Canvas.Set???
canvas.Children.Add(line01);

Instead of using Ellipse and Line controls and positioning them by Canvas.Left and Canvas.Top you may prefer to use three Path controls with appropriate geometries. Especially the EllipseGeometry provides far easier handling of its center point, compared to an Ellipse control.
private EllipseGeometry ball1Geometry = new EllipseGeometry();
private EllipseGeometry ball2Geometry = new EllipseGeometry();
private LineGeometry lineGeometry = new LineGeometry();
public MainWindow()
{
InitializeComponent();
canvas.Children.Add(new Path
{
Stroke = Brushes.Black,
Data = ball1Geometry
});
canvas.Children.Add(new Path
{
Stroke = Brushes.Black,
Data = ball2Geometry
});
canvas.Children.Add(new Path
{
Stroke = Brushes.Black,
Data = lineGeometry
});
}
...
private void UpdateDrawing(
Point ball1Position, double ball1Radius,
Point ball2Position, double ball2Radius)
{
ball1Geometry.RadiusX = ball1Radius;
ball1Geometry.RadiusY = ball1Radius;
ball1Geometry.Center = ball1Position;
ball2Geometry.RadiusX = ball2Radius;
ball2Geometry.RadiusY = ball2Radius;
ball2Geometry.Center = ball2Position;
lineGeometry.StartPoint = ball1Position;
lineGeometry.EndPoint = ball2Position;
}
Then you may also prefer to do it the WPF way and create the Paths in XAML:
<Canvas>
<Path Stroke="Black">
<Path.Data>
<EllipseGeometry x:Name="ball1Geometry"/>
</Path.Data>
</Path>
<Path Stroke="Black">
<Path.Data>
<EllipseGeometry x:Name="ball2Geometry"/>
</Path.Data>
</Path>
<Path Stroke="Black">
<Path.Data>
<LineGeometry x:Name="lineGeometry"/>
</Path.Data>
</Path>
</Canvas>

I think you'd better draw in two steps :
1) add the 3 figures and store them (when building your window).
2) update the coordinates in an animating loop.
It will be faster / handier than clearing/filling the canvas on each frame.
For your line issue : hook it on circle 1's center, and have it go to circle 2's center :
// new line coordinates :
X1 = Y1 = 0
X2 = Balle02X - Balle01X + ( SMALL_SIZE / 2 )
Y2 = Balle02Y - Balle01Y + ( SMALL_SIZE / 2 )
Canvas.SetTop ( line01, Balle01X + (BIG_SIZE / 2) )
Canvas.SetLeft( line01, Balle01Y + (BIG_SIZE / 2) )

Related

WPF mapping border to Bitmapimage (Not a duplicate)

I post this before and it was remove for being a duplicate. It is not. My problem is different then what that other people is doing. He is not doing zoom nor pan, and does not have a boarder.
I am using Stretch="Fill" to place my entire picture in the borders of an Image box. I am using a Border so that I can do Zoom and Pan. I am using the Canvas to draw rectangles around giving click areas. I want to map the left mouse click coordinates of the Canvas with zoom and pan back to the original image. here is my XAML code :
`
<Border x:Name="VideoPlayerBorder" ClipToBounds="True" Background="Gray" >
<Canvas x:Name="CanvasGridScreen" MouseLeftButtonDown="VideoPlayerSource_OnMouseLeftButtonDown" >
<Image x:Name="VideoPlayerSource" Opacity="1" RenderTransformOrigin="0.5,0.5" MouseLeftButtonUp="VideoPlayerSource_OnMouseLeftButtonUp" MouseWheel="VideoPlayerSource_OnMouseWheel" MouseMove="VideoPlayerSource_OnMouseMove" Width="{Binding Path=ActualWidth, ElementName=CanvasGridScreen}" Height="{Binding Path=ActualHeight, ElementName=CanvasGridScreen}" Stretch="Fill" >
</Image>
</Canvas>
`
here is my C# code:
`private void VideoPlayerSource_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
VideoPlayerSource.CaptureMouse();
var tt = (TranslateTransform)((TransformGroup)VideoPlayerSource.RenderTransform).Children.First(tr => tr is TranslateTransform);
start = e.GetPosition(VideoPlayerBorder);
origin = new Point(tt.X, tt.Y);
_stIR = start;
_stIR2 = start;
addRemoveItems(sender, e);
}
private void addRemoveItems(object sender, MouseButtonEventArgs e)
{
// this is the event that will check if we clicked on a rectangle or if we clicked on the canvas
// if we clicked on a rectangle then it will do the following
if (e.OriginalSource is Rectangle)
{
// if the click source is a rectangle then we will create a new rectangle
// and link it to the rectangle that sent the click event
Rectangle activeRec = (Rectangle)e.OriginalSource; // create the link between the sender rectangle
CanvasGridScreen.Children.Remove(activeRec); // find the rectangle and remove it from the canvas
}
// if we clicked on the canvas then we do the following
else
{
// generate a random colour and save it inside the custom brush variable
Custombrush = new SolidColorBrush(Color.FromRgb((byte)r.Next(1, 255),
(byte)r.Next(1, 255), (byte)r.Next(1, 233)));
// create a re rectangle and give it the following properties
// height and width 50 pixels
// border thickness 3 pixels, fill colour set to the custom brush created above
// border colour set to black
Rectangle newRec = new Rectangle
{
Width = 50,
Height = 50,
StrokeThickness = 3,
Fill = Custombrush,
Stroke = Brushes.Black
};
// once the rectangle is set we need to give a X and Y position for the new object
// we will calculate the mouse click location and add it there
Canvas.SetLeft(newRec, Mouse.GetPosition(CanvasGridScreen).X); // set the left position of rectangle to mouse X
Canvas.SetTop(newRec, Mouse.GetPosition(CanvasGridScreen).Y); // set the top position of rectangle to mouse Y
CanvasGridScreen.Children.Add(newRec); // add the new rectangle to the canvas
}
}
private void VideoPlayerSource_OnMouseWheel(object sender, MouseWheelEventArgs e)
{
TransformGroup transformGroup = (TransformGroup)VideoPlayerSource.RenderTransform;
ScaleTransform transform = (ScaleTransform)transformGroup.Children[0];
double zoom = e.Delta > 0 ? .2 : -.2;
double transformScaleX = Math.Round((transform.ScaleX + zoom), 2);
double transformScaleY = Math.Round((transform.ScaleY + zoom), 2);
if (transformScaleX <= 8.2 && transformScaleX >= 1)
{
transform.ScaleX = Math.Round(transform.ScaleX + zoom, 2);
transform.ScaleY = Math.Round(transform.ScaleY + zoom, 2);
zoomFactor2 = zoomFactor2 + zoom;
zoomFactor = zoomFactor2;
}
}
void PanMethod(MouseEventArgs e)
{
var tt = (TranslateTransform)((TransformGroup)VideoPlayerSource.RenderTransform).Children.First(tr => tr is TranslateTransform);
Vector v = start - e.GetPosition(VideoPlayerBorder);
if (zoomFactor > 1.0)
{
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
}
}
is there a function that would give me this information ? is there a way of using TransformGroup or ScaleTransform to return the actual location in the picture that was clicked? again the Image with possible zoom and/or pan
Check out: https://learn.microsoft.com/en-us/dotnet/api/system.windows.media.visual.transformtovisual
The right way to translate coordinates back to the original pre-transforms control is to use the TransformToVisual helper. It's probably a good idea to do that regardless since transforms could be applied higher up in the stack.
In your case you want to call:
GeneralTransform transform = CanvasGridScreen.TransformToVisual(VideoPlayerSource);
Point normalizedPoint = transform.Transform(new Point(0, 0));

WPF - Touch manipulation problem when translate after a rotation

I have a problem with object transformations via Manipulation events, more precisely matrix.
Problem: After a first successfull rotation/scale/translation of my object(btw: a Rectangle), the second manipulation (a translation in this case) will move the object in the direction of its new angle.
For example, if after a first rotation (new angle of 45°) I later touch the screen from right to left, the object will follow a 45° diagonal path instead of the path that I draw.
Expectation: I want my object to follow exactly the path I make on the screen regardless of its rotation.
Situation: I drop a Rectangle in a Canvas by code, this rectangle has "IsManipulationEnabled = true" and a "RenderTransformOrigin = new Point(.5, .5)" and "mySuperRectangle.ManipulationDelta += Container_ManipulationDelta;"
This is the code I use:
private void Container_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
try
{
if (e.OriginalSource != null)
{
Rectangle image = e.OriginalSource as Rectangle;
Point center = new Point(image.ActualWidth / 2.0, image.ActualHeight / 2.0);
Matrix imageMatrix = ((MatrixTransform)image.RenderTransform).Matrix;
center = imageMatrix.Transform(center);
// Move the Rectangle.
imageMatrix.Translate(e.DeltaManipulation.Translation.X,
e.DeltaManipulation.Translation.Y);
// Rotate the Rectangle.
imageMatrix.RotateAt(e.DeltaManipulation.Rotation,
center.X,
center.Y);
// Resize the Rectangle. Keep it square
// so use only the X value of Scale.
imageMatrix.ScaleAt(e.DeltaManipulation.Scale.X,
e.DeltaManipulation.Scale.X,
center.X,
center.Y);
// Apply changes
image.RenderTransform = new MatrixTransform(imageMatrix);
Rect containingRect =
new Rect(((FrameworkElement)e.ManipulationContainer).RenderSize);
Rect shapeBounds =
image.RenderTransform.TransformBounds(
new Rect(image.RenderSize));
// Check if the rectangle is completely in the window.
// If it is not and intertia is occuring, stop the manipulation.
if (e.IsInertial && !containingRect.Contains(shapeBounds))
{
e.Complete();
}
e.Handled = true;
}
}
catch (Exception)
{
throw;
}
}
Any ideas ?
Thanx a lot.
You should handle the manipulation event on the Canvas, because the origin of the manipulation should be relative to the fixed Canvas, instead of the moving Rectangle.
<Canvas IsManipulationEnabled="True"
ManipulationDelta="CanvasManipulationDelta">
<Rectangle x:Name="rectangle" Width="200" Height="200" Fill="Red">
<Rectangle.RenderTransform>
<MatrixTransform/>
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
The event handler would work like this:
private void CanvasManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
var transform = (MatrixTransform)rectangle.RenderTransform;
var matrix = transform.Matrix;
var center = e.ManipulationOrigin;
var scale = (e.DeltaManipulation.Scale.X + e.DeltaManipulation.Scale.Y) / 2;
matrix.ScaleAt(scale, scale, center.X, center.Y);
matrix.RotateAt(e.DeltaManipulation.Rotation, center.X, center.Y);
matrix.Translate(e.DeltaManipulation.Translation.X, e.DeltaManipulation.Translation.Y);
transform.Matrix = matrix;
}
Final code for me:
private void CanvasManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
var transform = rectangle.RenderTransform as MatrixTransform;
var matrix = transform.Matrix;
Point center = new Point(rectangle.ActualWidth / 2.0, rectangle.ActualHeight / 2.0);
center = matrix.Transform(center);
matrix.ScaleAt(e.DeltaManipulation.Scale.X , e.DeltaManipulation.Scale.X, center.X, center.Y);
matrix.RotateAt(e.DeltaManipulation.Rotation, center.X, center.Y);
matrix.Translate(e.DeltaManipulation.Translation.X, e.DeltaManipulation.Translation.Y);
rectangle.RenderTransform = new MatrixTransform(matrix);
}
Edit: Don't use the as operator without checking the result for null. In this case, if transform is null, create a new MatrixTransform and assign it to the RenderTransform property. Afterwards, reuse the transform and just update its Matrix property.
Improved code:
private void CanvasManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
var transform = rectangle.RenderTransform as MatrixTransform;
if (transform == null) // here
{
transform = new MatrixTransform();
rectangle.RenderTransform = transform;
}
var matrix = transform.Matrix;
var center = new Point(rectangle.ActualWidth / 2.0, rectangle.ActualHeight / 2.0);
center = matrix.Transform(center);
matrix.ScaleAt(e.DeltaManipulation.Scale.X , e.DeltaManipulation.Scale.X, center.X, center.Y);
matrix.RotateAt(e.DeltaManipulation.Rotation, center.X, center.Y);
matrix.Translate(e.DeltaManipulation.Translation.X, e.DeltaManipulation.Translation.Y);
transform.Matrix = matrix; // here
}

Why are my polygons being drawn in the wrong location?

I am practicing C# basic WPF/XAML drawing for an assignment and right off the bat I cannot figure out why my polygons are being drawn in the wrong place.
My window is of 1280x720 fixed, non-resizeable. I am trying to programmatically create my polygons by:
Creating points in the coordinates I want them to be:
`
[0,0]
[max height, 0],
[max height, max width],
[0, max width],
[max height/2, max width/2]
`
Creating polygons that consists of three points each, [0,0] and two edges. My screen is supposed to be split into four triangles.
I tried breaking down the code to something really explicit to see if I could figure out where the issue is, so this is what I have:
private void CreatePolygons()
{
List<Point> PointList = new List<Point>
{
new Point(MainUI.Height / 2, MainUI.Width / 2),
new Point(0, 0),
new Point(0, MainUI.Height),
new Point(MainUI.Width, MainUI.Height),
new Point(MainUI.Width, 0)
};
Polygon p1 = new Polygon();
Polygon p2 = new Polygon();
Polygon p3 = new Polygon();
Polygon p4 = new Polygon();
p1.Points.Add(PointList[0]);
p1.Points.Add(PointList[1]);
p1.Points.Add(PointList[2]);
p2.Points.Add(PointList[0]);
p2.Points.Add(PointList[2]);
p2.Points.Add(PointList[3]);
p3.Points.Add(PointList[0]);
p3.Points.Add(PointList[3]);
p3.Points.Add(PointList[4]);
p4.Points.Add(PointList[0]);
p4.Points.Add(PointList[4]);
p4.Points.Add(PointList[1]);
p1.Stroke = System.Windows.Media.Brushes.LightSkyBlue;
p2.Stroke = System.Windows.Media.Brushes.LightSkyBlue;
p3.Stroke = System.Windows.Media.Brushes.LightSkyBlue;
p4.Stroke = System.Windows.Media.Brushes.LightSkyBlue;
p1.StrokeThickness = 1;
p2.StrokeThickness = 1;
p3.StrokeThickness = 1;
p4.StrokeThickness = 1;
MainGrid.Children.Add(p1);
MainGrid.Children.Add(p2);
MainGrid.Children.Add(p3);
MainGrid.Children.Add(p4);
}
The end result is a completely misplaced grid and I can't understand what the coordinates it ended up creating refer to:
What am I missing?
You have accidentally swapped the Width and Height in the first point:
new Point(MainUI.Height / 2, MainUI.Width / 2),
Should be:
new Point(MainUI.Width / 2, MainUI.Height / 2),
Further, assuming MainUI is the app window itself, the points will still be a bit off, because the Height of the window includes its title bar height. You should better use MainGrid.ActualWidth and MainGrid.ActualHeight:
List<Point> PointList = new List<Point>
{
new Point(MainGrid.ActualWidth / 2, MainGrid.ActualHeight / 2),
new Point(0, 0),
new Point(0, MainGrid.ActualHeight),
new Point(MainGrid.ActualWidth, MainGrid.ActualHeight),
new Point(MainGrid.ActualWidth, 0)
};
As an alternative to all the Polygon point calcuations, you may use this simple Path element, which produces the same output and stretches automatically:
<Grid>
<Path Stretch="Fill" Stroke="LightSkyBlue" StrokeThickness="1"
Data="M0,0 L1,0 1,1 0,1Z M0,0 L1,1 M0,1 L1,0"/>
</Grid>
Besides that you have confused Width and Height of the first point, I'd suggest not to create UI elements like Polygons in code behind. Better use an ItemsControl like this:
<Grid SizeChanged="MainUISizeChanged">
<ItemsControl x:Name="polygons">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Polygon Stroke="LightSkyBlue" StrokeThickness="1"
Points="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
and assign its ItemsSource property to a collection of PointCollections, e.g. whenever the size of your MainUI element changes:
private void MainUISizeChanged(object sender, SizeChangedEventArgs e)
{
var points = new List<Point>
{
new Point(e.NewSize.Width / 2, e.NewSize.Height / 2),
new Point(0, 0),
new Point(0, e.NewSize.Height),
new Point(e.NewSize.Width, e.NewSize.Height),
new Point(e.NewSize.Width, 0)
};
polygons.ItemsSource = new List<PointCollection>
{
new PointCollection(new Point[] { points[0], points[1], points[2] }),
new PointCollection(new Point[] { points[0], points[2], points[3] }),
new PointCollection(new Point[] { points[0], points[3], points[4] }),
new PointCollection(new Point[] { points[0], points[4], points[1] }),
};
}

Usage of a Picture instead of Rectangle Shape in C#

Background: I am currently busy with showing position of a Vehicle on a Zoomable Canvas based on the Position (X,Y) and Orientation (for Rotation). I use Rectangle for visualizing the vehicle. Everything works well but I got a bit greedy and now I want to replace the Rectangle with Top View Picture of the Vehicle, so it looks that the vehicle itself is moving instead a Rectangle.
Code Below:
private void PaintLocationVehicle(VehicleClass vc)
{
IEnumerable<Rectangle> collection = vc.ZoomableCanvas.Children.OfType<Rectangle>().Where(x => x.Name == _vehicleobjectname);
List<Rectangle> listE = collection.ToList<Rectangle>();
for (int e = 0; e < listE.Count; e++)
vc.ZoomableCanvas.Children.Remove(listE[e]);
// Assign X and Y Position from Vehicle
double drawingX = vc.gCurrentX * GlobalVar.DrawingQ;
double drawingY = vc.gCurrentY * GlobalVar.DrawingQ;
// Scale Length and Width of Vehicle
double tractorWidthScaled = vc.tractorWidth * GlobalVar.DrawingQ;
double tractorLengthScaled = vc.tractorLength * GlobalVar.DrawingQ;
// Get Drawing Location
double _locationX = drawingX - (tractorLengthScaled / 2);
double _locationY = drawingY - ((tractorWidthScaled / 2));
RotateTransform rotation = new RotateTransform();
// Angle in 10th of a Degree
rotation.Angle = vc.gCurrentTheeta/10 ;
double i = 0;
//paint the node
Rectangle _rectangle = new Rectangle();
_rectangle.Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString(vc.VehicleColor == "" ? "Black" : vc.VehicleColor));
_rectangle.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString(vc.VehicleColor == "" ? "Black" : vc.VehicleColor));
i += 0;
_rectangle.Width = tractorLengthScaled ;
_rectangle.Height = tractorWidthScaled;
rotation.CenterX = _rectangle.Width / 2;
rotation.CenterY = _rectangle.Height / 2;
_rectangle.RenderTransform = rotation;
Canvas.SetTop(_rectangle, _locationY + i);
Canvas.SetLeft(_rectangle, _locationX + i);
_rectangle.SetValue(ZoomableCanvas.ZIndexProperty, 2);
string _tooltipmsg = "Canvas: " + vc.ZoomableCanvas.Name;
// Assign ToolTip Values for User
_tooltipmsg += "\nX: " + vc.gCurrentX;
_tooltipmsg += "\nY: " + vc.gCurrentY;
_rectangle.ToolTip = _tooltipmsg;
_rectangle.Name = _vehicleobjectname;
//add to the canvas
vc.ZoomableCanvas.Children.Add(_rectangle);
}
Note: VehicleClass holds all the Values for a certain Vehicle. DrawingQ holds the transformation scale from Reality to Zoomable Canvas.
So the issues I forsee:
How to append the Size of a Jpeg file to get the size same as
Rectangle?
What kind of Shape object shall I use? Please
suggest.
If i undrestand you correctly. you wanted to show an image of the vechicle inside the rectangle. in order to do that you can use
ImageBrush and assign to the Rectangle Fill property
something like this
Rectangle rect = new Rectangle();
rect.Width = 100;
rect.Height = 100;
ImageBrush img = new ImageBrush();
BitmapImage bmp = new BitmapImage();
bmp.BeginInit();
bmp.UriSource = new Uri("vehicle image path");
bmp.EndInit();
img.ImageSource = bmp;
rect.Fill = img;
I hope that helps

How to clip non-closed geometry

Intro
I noticed an issue while implementing clipping (see this).
It looks like UIElement.Clip still render invisible parts
Rendering relatively small geometry (lines to only fill 1920x1200 area ~ 2000 vertical lines) take a lot of time. When using Clip and moving that geometry offscreen (so that clipping should remove significant part of it) it is still take same time (around 1 sec).
Ok, I found what using Geometry.Combine will do a clip (render time is reduced proportionally to removed after clipping geometry). Perfect!
Problem
Geometry.Combine doesn't work with non-closed geometry properly. It produce closed geometry. And it looks ugly, connecting first and last point:
Question
How can I perform clipping (reducing amount of geometry to be rendered) for non-closed figures?
Edit
Here is geometry before (small peace of shown on picture)
{M0;50L0;50L1;53,1395259764657L2;56,2666616782152L3;59,3690657292862L4;62,4344943582427L5;65,4508497187474L6;68,4062276342339L7;71,2889645782536L8; ...
and after
{F1M54,9999923706055;34,5491371154785L53,9999885559082;37,5655174255371 53,0000114440918;40,6309471130371 52,0000076293945;43,7333335876465 ...
Notice change at beginning, was M 0;50 L ..., become F 1 M 55;34 L ...
F1 means NonZero filling
Rule that determines whether a point is in the fill region of the path by drawing a ray from that point to infinity in any direction and then examining the places where a segment of the shape crosses the ray. Starting with a count of zero, add one each time a segment crosses the ray from left to right and subtract one each time a path segment crosses the ray from right to left. After counting the crossings, if the result is zero then the point is outside the path. Otherwise, it is inside.
And I have absolutely no clue what that means. But maybe it is important?
Edit
I should have been looking at the end of strings. There is z at the end of Path.Data, which means figure is closed.
Strangely enough, trying to remove z (by using Geometry.ToString()/Geometry.Parse() combo) doesn't works. After some investigation I found what Combine produces physically enclosing figures (commands L x;y, where x;y is the leftmost point). And the worst thing is what it's not always the last point, so simply removing last L x;y before parsing doesn't works either. =(
Edit
Sample to demonstrate problem:
Xaml:
<Path x:Name="path" Stroke="Red"/>
Code:
var geometry1 = new RectangleGeometry(new Rect(100, 100, 100, 100));
var geometry2 = new PathGeometry(new[] { new PathFigure(new Point(0,0), new[] {
new LineSegment(new Point(300, 300), true),
new LineSegment(new Point(300, 0), true),
}, false) });
//path.Data = geometry1;
//path.Data = geometry2;
//path.Data = Geometry.Combine(geometry1, geometry2, GeometryCombineMode.Intersect, null);
Pictures of geometry1 and geometry2:
Resulting Combine:
As you can see 2 lines become 3 after clipping, debugging proves it:
{F1M100;100L200;100 200;200 100;100z}
Notice, it's not only z, but also 100;100 point at the end, connecting starting point.
I attempted to implement a clipping solutions for non closed geometry based on this line intersection algorithm
Code
public static PathGeometry ClipGeometry(PathGeometry geom, Rect clipRect)
{
PathGeometry clipped = new PathGeometry();
foreach (var fig in geom.Figures)
{
PathSegmentCollection segments = new PathSegmentCollection();
Point lastPoint = fig.StartPoint;
foreach (LineSegment seg in fig.Segments)
{
List<Point> points;
if (LineIntersectsRect(lastPoint, seg.Point, clipRect, out points))
{
LineSegment newSeg = new LineSegment(points[1], true);
PathFigure newFig = new PathFigure(points[0], new[] { newSeg }, false);
clipped.Figures.Add(newFig);
}
lastPoint = seg.Point;
}
}
return clipped;
}
static bool LineIntersectsRect(Point lineStart, Point lineEnd, Rect rect, out List<Point> points)
{
points = new List<Point>();
if (rect.Contains(lineStart) && rect.Contains(lineEnd))
{
points.Add(lineStart);
points.Add(lineEnd);
return true;
}
Point outPoint;
if (Intersects(lineStart, lineEnd, rect.TopLeft, rect.TopRight, out outPoint))
{
points.Add(outPoint);
}
if (Intersects(lineStart, lineEnd, rect.BottomLeft, rect.BottomRight, out outPoint))
{
points.Add(outPoint);
}
if (Intersects(lineStart, lineEnd, rect.TopLeft, rect.BottomLeft, out outPoint))
{
points.Add(outPoint);
}
if (Intersects(lineStart, lineEnd, rect.TopRight, rect.BottomRight, out outPoint))
{
points.Add(outPoint);
}
if (points.Count == 1)
{
if (rect.Contains(lineStart))
points.Add(lineStart);
else
points.Add(lineEnd);
}
return points.Count > 0;
}
static bool Intersects(Point a1, Point a2, Point b1, Point b2, out Point intersection)
{
intersection = new Point(0, 0);
Vector b = a2 - a1;
Vector d = b2 - b1;
double bDotDPerp = b.X * d.Y - b.Y * d.X;
if (bDotDPerp == 0)
return false;
Vector c = b1 - a1;
double t = (c.X * d.Y - c.Y * d.X) / bDotDPerp;
if (t < 0 || t > 1)
return false;
double u = (c.X * b.Y - c.Y * b.X) / bDotDPerp;
if (u < 0 || u > 1)
return false;
intersection = a1 + t * b;
return true;
}
currently solution works for line based geometry, other types perhaps need to be included if needed.
test xaml
<UniformGrid Columns="2"
Margin="250,250,0,0">
<Grid>
<Path x:Name="pathClip"
Fill="#22ff0000" />
<Path x:Name="path"
Stroke="Black" />
</Grid>
<Path x:Name="path2"
Margin="100,0,0,0"
Stroke="Black" />
</UniformGrid>
test code 1
void test()
{
var geometry = new PathGeometry(new[] { new PathFigure(new Point(0,0), new[] {
new LineSegment(new Point(300, 300), true),
new LineSegment(new Point(300, 0), true),
}, false) });
Rect clipRect= new Rect(10, 10, 180, 180);
path.Data = ClipGeometry(geometry, clipRect);
path2.Data = geometry;
pathClip.Data = new RectangleGeometry(clipRect);
}
result
test code 2
void test()
{
var radius = 1.0;
var figures = new List<LineSegment>();
for (int i = 0; i < 2000; i++, radius += 0.1)
{
var segment = new LineSegment(new Point(radius * Math.Sin(i), radius * Math.Cos(i)), true);
segment.Freeze();
figures.Add(segment);
}
var geometry = new PathGeometry(new[] { new PathFigure(figures[0].Point, figures, false) });
Rect clipRect= new Rect(10, 10, 180, 180);
path.Data = ClipGeometry(geometry, clipRect);
path2.Data = geometry;
pathClip.Data = new RectangleGeometry(clipRect);
}
result
give it a try and see how close it is.
If i am right your mainquestion is:"How do i improve the performance of drawing many shapes?"
To get this working you have to understand Geometry Math.
Geometric objects can only merged/combined if they connect or overlap. And there is a big difference between path-geometry and shape-geometry.
As example if two circle overlap you can combine them in WPF
to get the overlapping region: Intersect
to get the difference: Xor
to get the combined surface: Union
to get the difference of only one shape: Exclude
For path-geometry it's a little different, because a path has no surface a path cannot Intersect | Xor | Union | Exclude another path or shape.
But WPF thinks you just forgot to close the path and is doing that for you, which result in the given result in your question.
so to achieve a performanceboost you have to filter all the geometry first for shapes and paths.
foreach(Shape geometryObj in ControlsOrWhatEver)
{
if(geometryObj is Line || geometryObj is Path || geometryObj is Polypath)
{
pathList.Add(geometryObj);
}
else
{
shapeList.Add(geometryObj);
}
}
for the shapeList you can use Geometry.Combine, but for the pathList you have to do some other work. You have to check if the connect at some point, doesnt matter if beginningPoint, endPoint or somwhere inbetween.
If you have done that you can Merge not Combine the path by yourself like:
public Polyline mergePaths(Shape line1, Shape line2)
{
if(!checkLineType(line1) || !checkLineType(line2))
{
return null;
}
if(hitTest(line1, line2))
{
//here you have to do some math to determine the overlapping points
//on these points you can do something like this:
foreach(Point p in Overlapping Points)
{
//add the first line until p then add line2 and go on to add lin 1 until another p
}
}
else
{
return null;
}
}

Categories

Resources