Strange drawing with DrawingContext - wpf

I'm working on a custom WPF control that should visualize thousands of graphical primitives in a scrollable area. The core part of the control's template is the following:
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ItemVisualizer}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<local:ItemAreaElement Grid.Row="0" Grid.Column="0" x:Name="PART_ItemArea" />
<ScrollBar Grid.Row="0" Grid.Column="1" x:Name="PART_ScrollBarVert" Orientation="Vertical" Maximum="100" />
<ScrollBar Grid.Row="1" Grid.Column="0" x:Name="PART_ScrollBarHorz" Orientation="Horizontal" Maximum="100" />
<Rectangle Grid.Row="1" Grid.Column="1" x:Name="PART_SizeGrip" Focusable="False" Fill="#F0F0F0" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
For better performance, all drawing operations are performed in the OnRender method of the ItemAreaElement. To have crisp drawing, I also use the following setting in the initialization code:
this.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
However, I have some strange issues with my drawing. To demonstrate them, I simplified the definition of my ItemAreaElement to the following:
class ItemAreaElement : FrameworkElement
{
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
const int ITEM_WIDTH = 60;
const int ITEM_HEIGHT = 20;
Pen penRed = new Pen(Brushes.Red, 1);
Pen penGreen = new Pen(Brushes.Green, 1);
int y = 0;
for (int iRow = 0; iRow < 3; iRow++)
{
int x = 0;
for (int iCol = 0; iCol < 2; iCol++)
{
Point cornerTopLeft = new Point(x, y);
dc.DrawLine(penRed, cornerTopLeft, new Point(x + ITEM_WIDTH - 1, y));
dc.DrawLine(penRed, cornerTopLeft, new Point(x, y + ITEM_HEIGHT - 1));
Point cornerBottomRight = new Point(x + ITEM_WIDTH - 1, y + ITEM_HEIGHT - 1);
dc.DrawLine(penGreen, new Point(x + ITEM_WIDTH - 1, y), cornerBottomRight);
dc.DrawLine(penGreen, new Point(x, y + ITEM_HEIGHT - 1), cornerBottomRight);
x += ITEM_WIDTH;
}
y += ITEM_HEIGHT;
}
}
}
When I launch this code on my main dev laptop equipped with an Ultra-HD screen with 282ppi (the system scale factor is 300%), I get this picture:
Or, after zooming in paint.net with gridlines:
As you see, the left, and top edges of my ItemAreaElement are partially covered by the border of the control. Must it be so? Is there a setting I can use to avoid this?
The second problem are lines that do not include the start point (see the top-left corner of my "cells"). Is this the expected behavior? IF so, how to force WPF to draw the start pixel?
And the third problem is the place or device-independent point in which the green lines should meet (the bottom-right corner of my cells). As you can see, this point is jagged. I expected to see just a green square in that place. Can I implement this with the help of the DrawingContext.DrawLine method? Or do I need to use a more complex geometry with special settings for multi-point lines, etc.?
By the way, when I launch this code on a test pc with a "classic" 96 ppi monitor and the scale factor of the OS set to 100%, the situation is a little bit better in the bottom-right corner:
But I even do not see the horizontal red lines in the top row or vertical red lines in the first column. I expected to see them there but not to be covered by the control's border.

I've managed to solve all my problems by setting the corresponding guidelines. Below you'll find the improved version of the OnRender() method presented above:
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
const int ITEM_WIDTH = 60;
const int ITEM_HEIGHT = 20;
const double sizeOfPen = 1;
double halfSizeOfPen = sizeOfPen / 2.0;
Pen penRed = new Pen
{
Brush = Brushes.Red,
Thickness = sizeOfPen,
StartLineCap = PenLineCap.Square,
EndLineCap = PenLineCap.Square
};
Pen penGreen = new Pen
{
Brush = Brushes.Green,
Thickness = sizeOfPen,
StartLineCap = PenLineCap.Square,
EndLineCap = PenLineCap.Square
};
int y = 0;
for (int iRow = 0; iRow < 3; iRow++)
{
int x = 0;
for (int iCol = 0; iCol < 2; iCol++)
{
GuidelineSet guidelines = new GuidelineSet();
guidelines.GuidelinesX.Add(x);
guidelines.GuidelinesX.Add(x + ITEM_WIDTH);
guidelines.GuidelinesY.Add(y);
guidelines.GuidelinesY.Add(y + ITEM_HEIGHT);
dc.PushGuidelineSet(guidelines);
Point cornerTopLeft = new Point(x + halfSizeOfPen, y + halfSizeOfPen);
dc.DrawLine(penRed, cornerTopLeft, new Point(x + ITEM_WIDTH - halfSizeOfPen, y + halfSizeOfPen));
dc.DrawLine(penRed, cornerTopLeft, new Point(x + halfSizeOfPen, y + ITEM_HEIGHT - halfSizeOfPen));
Point cornerBottomRight = new Point(x + ITEM_WIDTH - halfSizeOfPen, y + ITEM_HEIGHT - halfSizeOfPen);
dc.DrawLine(penGreen, new Point(x + ITEM_WIDTH - halfSizeOfPen, y + halfSizeOfPen), cornerBottomRight);
dc.DrawLine(penGreen, new Point(x + halfSizeOfPen, y + ITEM_HEIGHT - halfSizeOfPen), cornerBottomRight);
dc.Pop();
x += ITEM_WIDTH;
}
y += ITEM_HEIGHT;
}
}

Related

How can I zoom in on a certain region of a BitmapSource in a WPF Image control?

Let's say I have a 1280x1024 BitmapSource in a WPF Image control. There is a 100x100 "active" area of this image that I want to be able to zoom in on with a button click. I want to zoom as much as possible while maintaining the aspect ratio and keeping all "active" pixels visible. This is what I have:
XAML:
<DockPanel>
<Button DockPanel.Dock="Bottom" Content="Zoom" Click="Button_Click" />
<Border DockPanel.Dock="Top" ClipToBounds="True">
<Image Name="TheImage" />
</Border>
</DockPanel>
Code:
private const int WIDTH = 1280;
private const int HEIGHT = 1024;
private const int MIN_X = 100;
private const int MAX_X = 200;
private const int MIN_Y = 100;
private const int MAX_Y = 200;
public MainWindow()
{
InitializeComponent();
byte[] image = new byte[WIDTH * HEIGHT];
for (int y = MIN_Y; y <= MAX_Y; y++)
for (int x = MIN_X; x <= MAX_X; x++)
image[y * WIDTH + x] = byte.MaxValue;
TheImage.Source = BitmapSource.Create(WIDTH, HEIGHT, 96.0, 96.0, PixelFormats.Gray8, null, image, WIDTH);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
double factor = 1.0 / Math.Max((MAX_X - MIN_X) / (double)WIDTH, (MAX_Y - MIN_Y) / (double)HEIGHT);
Matrix matrix = Matrix.Identity;
matrix.ScaleAt(factor, factor, (MIN_X + MAX_X) / 2.0 / WIDTH * TheImage.ActualWidth, (MIN_Y + MAX_Y) / 2.0 / HEIGHT * TheImage.ActualHeight);
TheImage.RenderTransform = new MatrixTransform(matrix);
}
Here is what it looks like before zoom:
And here is after zoom:
It looks like the amount of zoom is correct, but I think the issue is that the center of the scaling should shift due to the scaling, but I'm not sure how to account for that upfront.
Figured it out. The simplest approach seems to be to first do a translation such that the center of the active area is in the center of the image, then do the scaling centered on that:
private void Button_Click(object sender, RoutedEventArgs e)
{
double factor = 1.0 / Math.Max((MAX_X - MIN_X) / (double)WIDTH, (MAX_Y - MIN_Y) / (double)HEIGHT);
Matrix matrix = Matrix.Identity;
matrix.Translate(0.5 * TheImage.ActualWidth - (MIN_X + MAX_X) / 2.0 / WIDTH * TheImage.ActualWidth, 0.5 * TheImage.ActualHeight - (MIN_Y + MAX_Y) / 2.0 / HEIGHT * TheImage.ActualHeight);
matrix.ScaleAt(factor, factor, 0.5 * TheImage.ActualWidth, 0.5 * TheImage.ActualHeight);
TheImage.RenderTransform = new MatrixTransform(matrix);
}
Screenshot after zoom:

Create a line of 1 Pixel Images in WPF

I am creating an application in which I have to draw a Quadratic Curve. I did this using Polyline as:
BindPoints = new PointCollection();
for (int i = 0; i < 100; i++)
{
double val = i * i - 5;
var point = new Point(i, val);
Points.Add(new Point(i, i));
BindPoints.Add(point);
}
<Polyline Name="MyLine" Points="{Binding BindPoints,Mode=TwoWay} Stroke="Black" StrokeThickness="4" />
Now the issue is that I want to replace each point with an Image of 1 Pixel height. So it gives me an effect of a line but drawn with Images instead of points.
The Goal is to stack images on top of each other so they look like a line/curve.

Spray effect on a Canvas

I've got this Canvas defined in XAML:
<Canvas x:Name="MyCanvas" Background="White" TouchMove="MyCanvas_TouchMove"/>
And here's my first try to simulate a spray effect during TouchMove events :
for (int i = 0; i < 10; i++)
{
double r = random.NextDouble() * radius;
double theta = random.NextDouble() * (Math.PI * 2);
double x = e.GetTouchPoint(MyCanvas).Position.X + Math.Cos(theta) * r;
double y = e.GetTouchPoint(MyCanvas).Position.Y + Math.Sin(theta) * r;
Ellipse ellipse = new Ellipse();
ellipse.Height = 2;
ellipse.Width = 2;
ellipse.SetValue(Canvas.LeftProperty, x);
ellipse.SetValue(Canvas.TopProperty, y);
ellipse.Fill = (blackBrush);
MyCanvas.Children.Add(ellipse);
}
}
It works great at the beginning, but the more I "spray", the rendering slows dramatically.
I'm aware that the problem is most likely because I add a lot of elements on the canvas, and also because the Children.Add method is called too often.
How can I improve this ?

Avoiding seam in 3D cylinder model

I have this texture representing internal area of a pipe:
I have created a simple cylinder model (GeometryModel3D) by dissecting the above texture to a grid of points and than wrapping this to form a cylinder, then mapping the above texture to it (each square on a grid having two triangles):
The left and right edges of the texture meet at the above black seam line, which is unwanted.
I could not use single vertex point for smooth continuation there, because that point corresponds to both 0 and 360 degrees which is single angle, but two distinct points on edges of the 2D texture.
So I have used pairs of vertices on the same location but each corresponding to different point in the source texture.
Is there anything I can do to hide the seam line?
Model creation code:
private void SceneViewerWindowOnLoaded(object sender, RoutedEventArgs e)
{
var imageSource = new BitmapImage(new Uri("pack://application:,,,/Resources/out1.jpg"));
var meshGeometry3D = new MeshGeometry3D();
double circumference = imageSource.PixelWidth;
double radius = (circumference / (2.0 * Math.PI));
double angleSegment = (2.0 * Math.PI / SegmentCountTrans);
double sizeSegmentLong = ((double)imageSource.PixelHeight / SegmentCountLong);
double sizeSegmentTrans = ((double)imageSource.PixelWidth / SegmentCountTrans);
for (int indexSegmentLong = 0; indexSegmentLong < SegmentCountLong; indexSegmentLong++)
{
double depth = (indexSegmentLong * sizeSegmentLong);
for (int indexSegmentTrans = 0; indexSegmentTrans < SegmentCountTrans; indexSegmentTrans++)
{
meshGeometry3D.Positions.Add(GetVertexPosition(indexSegmentTrans, depth, radius, angleSegment));
meshGeometry3D.TextureCoordinates.Add(new Point(indexSegmentTrans * sizeSegmentTrans, depth));
}
// add one extra vertex representing 360 degrees for complete wrap-around
meshGeometry3D.Positions.Add(GetVertexPosition(SegmentCountTrans, depth, radius, angleSegment));
meshGeometry3D.TextureCoordinates.Add(new Point(SegmentCountTrans * sizeSegmentTrans, depth));
}
meshGeometry3D.TriangleIndices.Clear();
for (int indexSegmentLong = 0; indexSegmentLong < (SegmentCountLong - 1); indexSegmentLong++)
{
for (int indexSegmentTrans = 0; indexSegmentTrans < SegmentCountTrans; indexSegmentTrans++)
{
int indexCurrent = (indexSegmentLong * (SegmentCountTrans + 1) + indexSegmentTrans);
meshGeometry3D.TriangleIndices.Add(indexCurrent);
meshGeometry3D.TriangleIndices.Add(indexCurrent + 1);
meshGeometry3D.TriangleIndices.Add(indexCurrent + SegmentCountTrans + 1);
meshGeometry3D.TriangleIndices.Add(indexCurrent + SegmentCountTrans + 1);
meshGeometry3D.TriangleIndices.Add(indexCurrent + 1);
meshGeometry3D.TriangleIndices.Add(indexCurrent + SegmentCountTrans + 2);
}
}
var geometryModel3D = new GeometryModel3D
{
Geometry = meshGeometry3D,
Material = new DiffuseMaterial
{
Brush = new ImageBrush(imageSource)
}
};
this.SceneViewer.Model = geometryModel3D;
}
private static Point3D GetVertexPosition(int indexSegmentAngle, double depth, double radius, double angleSegment)
{
double angle = (StartAngle + indexSegmentAngle * angleSegment);
return new Point3D(
radius * Math.Cos(angle),
radius * Math.Sin(angle),
-depth);
}
XAML code for the Viewport3D element containing the model:
<Viewport3D x:Name="Viewport3D">
<Viewport3D.Camera>
<PerspectiveCamera x:Name="Camera" Position="0.0,0.0,0.0" LookDirection="0.0,0.0,-1.0" UpDirection="0.0,1.0,0.0" FieldOfView="90"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White"/>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D x:Name="ModelVisualModel"/>
</Viewport3D>
When you create your image brush try setting the TileMode to TileMode.Tile and ViewportUnits to BrushMappingMode.Absolute. If that still doesn't work then post a follow-up comment here and I'll try to reproduce it on my end.
I used DrawingBrush instead of Imagebrush. This fixed the seams and I also get an fps boost when setting the cachinghint.
Brush brush = new DrawingBrush(new ImageDrawing(_texture, new Rect(0,0,1,1)));
RenderOptions.SetCachingHint(brush , CachingHint.Cache);
RenderOptions.SetCacheInvalidationThresholdMinimum(brush , 0.5);
RenderOptions.SetCacheInvalidationThresholdMaximum(brush , 2.0);

How to create a circle in 3D with known center point, radius and it is on a plane which perpendicular to a line in WPF?

How to create a circle in 3D with known center point, radius and it is on a plane which perpendicular to a line (vector) in WPF?
Following is an example based on the comment I posted earlier.
First we define a function to generate the model of the circle:
/// <summary>
/// Generates a model of a Circle given specified parameters
/// </summary>
/// <param name="radius">Radius of circle</param>
/// <param name="normal">Vector normal to circle's plane</param>
/// <param name="center">Center position of the circle</param>
/// <param name="resolution">Number of slices to iterate the circumference of the circle</param>
/// <returns>A GeometryModel3D representation of the circle</returns>
private GeometryModel3D GetCircleModel(double radius, Vector3D normal, Point3D center, int resolution)
{
var mod = new GeometryModel3D();
var geo = new MeshGeometry3D();
// Generate the circle in the XZ-plane
// Add the center first
geo.Positions.Add(new Point3D(0, 0, 0));
// Iterate from angle 0 to 2*PI
double t = 2 * Math.PI / resolution;
for (int i = 0; i < resolution; i++)
{
geo.Positions.Add(new Point3D(radius * Math.Cos(t * i), 0, -radius * Math.Sin(t * i)));
}
// Add points to MeshGeoemtry3D
for (int i = 0; i < resolution; i++)
{
var a = 0;
var b = i + 1;
var c = (i < (resolution - 1)) ? i + 2 : 1;
geo.TriangleIndices.Add(a);
geo.TriangleIndices.Add(b);
geo.TriangleIndices.Add(c);
}
mod.Geometry = geo;
// Create transforms
var trn = new Transform3DGroup();
// Up Vector (normal for XZ-plane)
var up = new Vector3D(0, 1, 0);
// Set normal length to 1
normal.Normalize();
var axis = Vector3D.CrossProduct(up, normal); // Cross product is rotation axis
var angle = Vector3D.AngleBetween(up, normal); // Angle to rotate
trn.Children.Add(new RotateTransform3D(new AxisAngleRotation3D(axis, angle)));
trn.Children.Add(new TranslateTransform3D(new Vector3D(center.X, center.Y, center.Z)));
mod.Transform = trn;
return mod;
}
Setting up our ViewPort3D:
<Grid Background="Black">
<Viewport3D Name="mainViewPort">
<Viewport3D.Camera>
<PerspectiveCamera NearPlaneDistance="0.1" FarPlaneDistance="100" UpDirection="0,1,0" Position="0,0,10"
LookDirection="0,0,-1" FieldOfView="60" />
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<AmbientLight Color="#40FFFFFF" />
<DirectionalLight Color="#FFFFFF" Direction="1,-1,-1" />
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Grid>
Then to test:
private void AddCircleModel()
{
var mod = GetCircleModel(1.5, new Vector3D(0, 1, 1), new Point3D(0, -1, 0), 20);
mod.Material = new DiffuseMaterial(Brushes.Silver);
var vis = new ModelVisual3D() { Content = mod };
mainViewPort.Children.Add(vis);
}
Load up your window, call AddCircleModel(); and enjoy. Adjust view/parameters to your heart's content.
old thread but still, if anyone comes accross.
The "newb" Answer would work but is quitte lazy math and time consuming wize, here's why :
there are 2 for loops where really 1 is needed.
using transform after building a circle on (x,z) plane centered on (0,0,0) is overkill since you know the maths and vectors constraints.
I would suggest you implement it that way (i made it so it works for ellipses too, just set uSize = vSize for circles, u and v really just need to no be parallel for it to work but for mesh quality purposes i force them to be perpendicular):
public static Mesh CreateEllipse3D(Vector3 center, Vector3 normal, Vector3 u,
Vector3 v, float uSize, float vSize, int
subdiv = 30)
{
if (Math.Abs(Vector3.Dot(u, v)) != 0)
{
Debug.LogError("Vectors u and v must be orthogonals");
return new Mesh();
}
var mesh = new Mesh();
var vertices = new List<Vector3>();
var normals = new List<Vector3>();
var triangles = new List<int>();
vertices.Add(center);
float t = 2 * Mathf.PI / subdiv;
int a = 0, b, c;
for (int i = 0; i < subdiv; i++)
{
vertices.Add(center + u.normalized * uSize * Mathf.Cos(t * i) -
v.normalized * vSize * Mathf.Sin(t * i));
b = i + 1;
c = (i < (subdiv - 1)) ? i + 2 : 1;
triangles.Add(a);
triangles.Add(b);
triangles.Add(c);
normals.Add(normal);
normals.Add(normal);
normals.Add(normal);
}
mesh.vertices = vertices.ToArray();
mesh.triangles = triangles.ToArray();
mesh.normals = normals.ToArray();
return mesh;
}

Resources