Title: Understand fill modes in C#
C# drawing routines have two fill modes. In Windows Forms they are called Alternate and Winding. In WPF they are called Odd/Even and Non-Zero.
This example lets you draw polygons and see how the two fill modes determine which points are colored when you fill the polygon.
Fill Modes
In both modes, you can determine whether a point should be considered as inside the polygon by drawing a ray starting at the point and extending infinitely in any direction.
With the Alternate fill mode, the point is considered inside the polygon if ray crosses an odd number of the polygon's edges.
For example, consider the red point in picture on the right. The black ray extending to the right crosses three of the polygon's edges so that point is considered part of the polygon and it is shaded. In contrast, the ray extending from the green point crosses two edges so it is not part of the polygon.
With the Winding fill mode, you consider a ray leaving the point as before. This time you add one for each clockwise intersection and you subtract one for each counterclockwise intersection. Or if you prefer, add one each time the edge crosses the ray from left to right and subtract one when the edge crosses the ray from right to left.
For example, consider the rays shown in the picture on the right. From the red point, the ray crosses three edges right-to-left (-1), left-to-right (+1), and left-to-right (+1). The final count is +1, so the red point lies inside the polygon.
Next consider the green point. It ray crosses two edges left-to-right (+1) and left-to-right (+1). The final count is +2, so the green point also lies inside the polygon.
Finally consider the purple point. It ray crosses four edges right-to-left (-1), left-to-right (+1), right-to-left (-1), and left-to-right (+1). The final count is 0, so the purple point lies outside the polygon.
Note that the edges are considered clockwise/counterclockwise or left/right-to-right/left depending on the order in which the edges are drawn. Because the Winding fill mode relies on the total count being zero, it doesn't matter which order you use. If the total is zero using one direction, then it will also be zero using the other direction.
A Surprising Case
It seems like the winding mode should fill "the whole polygon," but it does not always as shown in the picture on the right. You can follow rays from a few points inside the isolated areas within the polygon to verify that this is correct.
The Example Program
The following snippet shows how the example program stores the points that make up the polygon.
private List Points = new List<Point>();
private bool Drawing = false;
The Points list holds the points. The Drawing variable indicates whether the user is currently defining a new polygon.
The following MouseClick event handler lets the user add new points to the list.
private void picCanvas_MouseClick(object sender, MouseEventArgs e)
{
// See if this is a left or right click.
if (e.Button == MouseButtons.Left)
{
// Left click.
if (!Drawing)
{
// Start a new polygon.
Points = new List<Point>();
Drawing = true;
}
// Add the point to the polygon.
Points.Add(e.Location);
}
else
{
// Right click.
Drawing = false;
}
// Redraw.
picCanvas.Refresh();
}
If the user clicked the left mouse button, then the code adds a new point to the list. If the user is not currently drawing a new polygon, then the code creates a new points list and sets Drawing to true to indicate that we are now drawing a new polygon.
The code then adds the new point to the list.
If the user clicked the right mouse button, then the program sets Drawing to false to indicate that we are done creating the new polygon.
The event handler finishes by refreshing the picCanvas PictureBox to make the following Paint event handler execute.
private void picCanvas_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.Clear(picCanvas.BackColor);
// See if we are drawing a new polygon.
if (Drawing)
{
// Draw the polygon so far.
if (Points.Count > 1)
e.Graphics.DrawLines(Pens.Blue, Points.ToArray());
DrawLineArrows(e.Graphics, Pens.Blue, Points,
Points.Count - 1);
}
else
{
// Draw the finished polygon.
if (Points.Count > 2)
{
FillMode fill_mode;
if (radWinding.Checked)
fill_mode = FillMode.Winding;
else
fill_mode = FillMode.Alternate;
e.Graphics.FillPolygon(Brushes.LightBlue,
Points.ToArray(), fill_mode);
e.Graphics.DrawPolygon(Pens.Blue, Points.ToArray());
}
DrawLineArrows(e.Graphics, Pens.Blue, Points, Points.Count);
}
foreach (Point point in Points)
{
DrawDot(e.Graphics, Brushes.White, Pens.Black, point);
}
}
If the user is currently drawing a polygon, the code invokes the Graphics object's DrawLines method to draw the polygon's edges. It does not connect the final point to the first point, and it does not fill the result. The code then calls the DrawLineArrows method described shortly to draw arrow indicators on the edges.
If the user is not drawing a new polygon, the code examines the radio boxes to see which one is checked and sets the variable fill_mode accordingly. The program then calls the Graphics object's FillPolygon method, passing it fill_mode to indicate how the polygon should be filled. The code then uses the DrawPolygon method to draw the polygon's edges, and then uses DrawLineArrows to draw arrow indicators on the edges.
After it has drawn and/or filled the polygon, the code uses the DrawDot method to draw dots on the polygon's vertices.
The following code shows the DrawLineArrows method.
// Draw a sequence of line arrowheads.
private void DrawLineArrows(Graphics gr, Pen pen,
List<Point> points, int num_segments)
{
for (int i = 0; i %lt; num_segments; i++)
{
int j = (i + 1) % points.Count;
DrawArrowhead(gr, pen, points[i], points[j]);
}
}
This method loops through adjacent pairs of points and calls the following DrawArrowhead method to draw a direction indicator in the middle of the segment connecting the points.
// Draw an arrowhead in the middle of the
// line segment showing its direction.
private void DrawArrowhead(Graphics gr, Pen pen, Point p1, Point p2)
{
float dx = p2.X - p1.X;
float dy = p2.Y - p1.Y;
float dist = (float)Math.Sqrt(dx * dx + dy * dy);
dx /= dist;
dy /= dist;
const float scale = 4;
dx *= scale;
dy *= scale;
float p1x = -dy;
float p1y = dx;
float p2x = dy;
float p2y = -dx;
float cx = (p1.X + p2.X) / 2f;
float cy = (p1.Y + p2.Y) / 2f;
PointF[] points =
{
new PointF(cx - dx + p1x, cy - dy + p1y),
new PointF(cx, cy),
new PointF(cx - dx + p2x, cy - dy + p2y),
};
gr.DrawLines(pen, points);
}
This method calculates the vector <dx, dy> between the start and end points. It finds the distance between the points and normalizes dx and dy so the vector has length 1. It the multiples those values by 4 to scale the result to length 4.
Next the code calculates two perpendicular vectors <p1x, p1y> and <p2x, p2y>. One of those vectors points to the line's left and the other to the right.
The program then find the point (cx, cy) that is halfway between the two original points. It then adds the components of the vector along the segment <dx, dy> and the perpendicular vectors to find the points that define the arrowhead. The method finishes by drawing the lines that make up the arrowhead.
The last helper method used in the earlier code is the following DrawDot method.
// Draw a dot at this point.
private void DrawDot(Graphics gr, Brush brush, Pen pen, PointF point)
{
const float radius = 3;
RectangleF rectf = new RectangleF(
point.X - radius,
point.Y - radius,
2 * radius, 2 * radius);
gr.FillEllipse(brush, rectf);
gr.DrawEllipse(pen, rectf);
}
This method simply fills and then outlines a circle at a specified point.
Conclusion
This post explains the difference between the Alternate (Odd/Even) and Winding (Non-Zero) fill modes. You can use the example program to draw polygons and then trace rays from points to see whether those points should be be shaded depending on the fill modes that you select.
It seems like there should be an addition to the fill modes that fills "the whole polygon." When I have time I may try to write a method to do that.
Download the example to experiment with it and to see additional details.
|