Title: Draw a triangular grid in C#
This example shows how to use a triangular grid. (In case you need to build a Tholian web or something.) Row numbers are defined in the obvious way with the first row having number 0.
Column numbers are a little stranger. The first complete triangle has index 0 and the indexes for the following triangles increase by 0.5. That way triangles that have whole number columns have the same orientation as the first complete triangle on the row ad the triangles with fractional indexes are upside down.
When you move the mouse over the grid, the program changes its title bar to indicate the row and column below the mouse. When you click on a triangle, the program displays that triangle in blue.
To use a triangular grid, the program needs to be able to do two things:
- Map a row and column to the points that make up the triangle
- Map a point (x, y) to a row and column
Row/Column → Triangle Points
The following TriangleToPoints method maps a row and column to a triangle's points. Given the triangle height and a row and column, it returns an array containing three points that you can use to draw the triangle.
// Return the points that define the indicated triangle.
private PointF[] TriangleToPoints(float height,
float row, float col)
{
float width = TriangleWidth(height);
float y = row * height;
float x = (col + 0.5f) * width;
// See if this triangle should be drawn
// right-side up or upside down.
bool whole_col = (Math.Abs(col - (int)col) < 0.1);
bool rightside_up;
if ((int)row % 2 == 0)
{
// Even row.
rightside_up = whole_col;
}
else
{
// Odd row.
rightside_up = !whole_col;
}
// Draw the triangle.
if (rightside_up)
return new PointF[]
{
new PointF(x, y),
new PointF(x + width / 2, y + height),
new PointF(x - width / 2, y + height),
};
else
return new PointF[]
{
new PointF(x, y + height),
new PointF(x + width / 2, y),
new PointF(x - width / 2, y),
};
}
The code first calls the following TriangleWidth method to get the triangle's width. This method simply uses trigonometry to calculate the width of an equilateral triangle.
// Return the width of a triangle.
private float TriangleWidth(float height)
{
return (float)(2 * height / Math.Sqrt(3));
}
Next the TriangleToPoints method calculates the triangle's top Y coordinate. This is simply the height of the triangles times the row number.
The code then sets x equal to the X coordinate for the point at the top or bottom of the triangle.
Now things get a little weirder. The code needs to figure out if it should draw the triangle right-side up or upside down.
The code first sets variable whole_col to true if the column is a whole number (like 3) and false if it has a fractional part (like 1.5).
It then sets variable rightside_up to indicate whether it should draw the triangle right-side up. If the row number is even, then rightside_up is true if the column number is a whole number. If the row number is odd, then rightside_up is true if the column number is not a whole number.
Now knowing the X and Y coordinates of the triangle's top/bottom vertex and whether the triangle should be right-side up, the code can build and return the array holding the triangle's points.
The following Paint event handler uses the TriangleToPoints method to draw the grid.
// Selected triangles.
private List Triangles = new List();
// Redraw the grid.
private void picGrid_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
// Draw the selected triangles.
foreach (PointF point in Triangles)
{
e.Graphics.FillPolygon(Brushes.LightBlue,
TriangleToPoints(TriangleHeight, point.X, point.Y));
}
// Draw the grid.
DrawTriangularGrid(e.Graphics, Pens.Black,
0, picGrid.ClientSize.Width,
0, picGrid.ClientSize.Height,
TriangleHeight);
}
The Triangles list holds Point objects that represent selected triangles. The points' X and Y coordinates give the selected triangles' rows and columns. I'll explain how triangles are selected later.
The Paint event handler loops through the selected triangles, calls TriangleToPoints to get their points, and fills them. It then calls the following DrawTriangularGrid method to draw the grid.
// Draw a triangular grid for the indicated area.
private void DrawTriangularGrid(Graphics gr, Pen pen,
float xmin, float xmax, float ymin, float ymax,
float height)
{
float width = TriangleWidth(height);
int row = 0;
for (float y = 0; y <= ymax + width / 2; y += height)
{
float x = 0;
if (row % 2 == 0) x = width / 2;
PointF[] points =
{
new PointF(x, y),
new PointF(x + width / 2, y + height),
new PointF(x - width / 2, y + height),
};
for (; x <= xmax; x += width)
{
gr.DrawPolygon(pen, points);
points[0].X += width;
points[1].X += width;
points[2].X += width;
}
row++;
}
}
This method first gets the triangles' width. It then loops over Y coordinate values until the variable y drops off the bottom of the drawing area.
For each y value, the code sets x equal to the X coordinate for the first right-side up triangle in the row. It then creates an array holding points to represent the right-side up triangle.
The code then enters a loop that runs until x exceeds the width of the drawing area. For each trip through the loop, the code draws the triangle and then adds the triangle width to each point's X coordinate to move the triangle one position to the right.
Point → Row/Column
The following PointToTriangle method maps a point to a triangle's row and column.
// Return the row and column of the triangle at this point.
private void PointToTriangle(float x, float y, float height,
out float row, out float col)
{
float width = TriangleWidth(height);
row = (int)(y / height);
col = (int)(x / width);
float dy = (row + 1) * height - y;
float dx = x - col * width;
if (row % 2 == 1) dy = height - dy;
if (dy > 1)
{
if (dx < width / 2)
{
// Left half of triangle.
float ratio = dx / dy;
if (ratio < 1f / Math.Sqrt(3)) col -= 0.5f;
}
else
{
// Right half of triangle.
float ratio = (width - dx) / dy;
if (ratio < 1f / Math.Sqrt(3)) col += 0.5f;
}
}
}
First the method divides the Y and X coordinates by the triangle height and width respectively. That gives a row and column assuming a rectangular grid. The red dashed rectangle shown in Figure 1 indicates the rectangular grid position for the point.
Now the method needs to figure out if the point lies outside of the triangle that occupies most of the rectangular box. To do that it first calculates dx and dy, the distances from the point to the upper left corner of the rectangular box.
Whether the point could lie above or below the edges of the triangle depends on whether the triangle is right-side up or upside down. The code takes that into account by subtracting dy from the triangle height for odd numbered rows.
Depending on whether the point lies in the left or right half of the rectangular box, the code determines whether the point lies above the left or right edge of the triangle (adjusted for upside down triangles) and updates the column if necessary.
The program uses the PointToTriangle method in two places: the MouseMove and MouseClick event handlers.
// Display the row and column under the mouse.
private void picGrid_MouseMove(object sender, MouseEventArgs e)
{
float row, col;
PointToTriangle(e.X, e.Y, TriangleHeight, out row, out col);
this.Text = "(" + row + ", " + col + ")";
}
// Add the clicked triangle to the Triangles list.
private void picGrid_MouseClick(object sender, MouseEventArgs e)
{
float row, col;
PointToTriangle(e.X, e.Y, TriangleHeight, out row, out col);
Triangles.Add(new PointF(row, col));
picGrid.Refresh();
}
The MouseMove event handler calls PointToTriangle and displays the point's row and column in the form's title bar.
The MouseClick event handler also calls PointToTriangle. It adds the triangle's row and column to the Triangles list and then refreshes the PictureBox to redraw the newly selected triangle in blue.
Download the example to experiment with it and to see additional details.
|