// Copyright (c) Craftwork Games. All rights reserved.
// Licensed under the MIT license.
// See LICENSE file in the project root for full license information.
// Adapted from Velcro Physics (formerly known as Farseer Physics).
// Used with permission: https://github.com/craftworkgames/MonoGame.Extended/issues/574
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.Extended.Triangulation;
namespace MonoGame.Extended.VectorDraw
{
///
/// Provides methods for drawing primitive shapes using a .
///
///
///
/// This class renders filled shapes and outlines directly via the GPU, producing correct alpha-blended
/// results even with semi-transparent colors. For quick outline-only drawing via
/// during prototyping or debugging, see ShapeExtensions.
///
///
/// Call on the associated before issuing any
/// draw calls, and when finished to flush geometry to the GPU.
///
///
public class PrimitiveDrawing
{
///
/// The default number of segments used when approximating circles and ellipses.
///
#if XBOX || WINDOWS_PHONE
public const int CircleSegments = 16;
#else
public const int CircleSegments = 32;
#endif
private readonly PrimitiveBatch _primitiveBatch;
///
/// Initializes a new instance of .
///
/// The used to issue draw calls.
/// is .
public PrimitiveDrawing(PrimitiveBatch primitiveBatch)
{
ArgumentNullException.ThrowIfNull(primitiveBatch);
_primitiveBatch = primitiveBatch;
}
///
/// Draws a single point at the specified position.
///
/// The position of the point.
/// The color of the point.
/// must be called first.
public void DrawPoint(Vector2 center, Color color)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
_primitiveBatch.AddVertex(center, color, PrimitiveType.LineList);
_primitiveBatch.AddVertex(center, color, PrimitiveType.LineList);
}
///
/// Draws a rectangle outline.
///
/// The top-left position of the rectangle in world space.
/// The width of the rectangle.
/// The height of the rectangle.
/// The color of the outline.
/// must be called first.
public void DrawRectangle(Vector2 location, float width, float height, Color color)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
Vector2[] rectVerts = new Vector2[4]
{
new Vector2(0, 0),
new Vector2(width, 0),
new Vector2(width, height),
new Vector2(0, height)
};
DrawPolygon(location, rectVerts, color);
}
///
/// Draws a solid (filled) rectangle with an optional outline.
///
/// The top-left position of the rectangle in world space.
/// The width of the rectangle.
/// The height of the rectangle.
///
/// The fill color. When is , also used for the outline.
///
///
/// When , an outline is drawn over the filled rectangle. Defaults to .
///
/// must be called first.
public void DrawSolidRectangle(Vector2 location, float width, float height, Color color, bool outline = true)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
Vector2[] rectVerts = new Vector2[4]
{
new Vector2(0, 0),
new Vector2(width, 0),
new Vector2(width, height),
new Vector2(0, height)
};
DrawSolidPolygon(location, rectVerts, color, outline);
}
///
/// Draws a circle outline using segments.
///
/// The center of the circle.
/// The radius of the circle.
/// The color of the outline.
/// must be called first.
public void DrawCircle(Vector2 center, float radius, Color color)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
double increment = Math.PI * 2.0 / CircleSegments;
double theta = 0.0;
for (int i = 0; i < CircleSegments; i++)
{
Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment));
_primitiveBatch.AddVertex(v1, color, PrimitiveType.LineList);
_primitiveBatch.AddVertex(v2, color, PrimitiveType.LineList);
theta += increment;
}
}
///
/// Draws a solid (filled) circle with an optional outline using segments.
/// The fill and outline use the same color.
///
/// The center of the circle.
/// The radius of the circle.
/// The color used for both the fill and the outline.
///
/// When , an outline is drawn over the filled circle. Defaults to .
///
/// must be called first.
public void DrawSolidCircle(Vector2 center, float radius, Color color, bool outline = true)
{
DrawSolidCircle(center, radius, color, color, outline);
}
///
/// Draws a solid (filled) circle with an optional outline using segments.
///
/// The center of the circle.
/// The radius of the circle.
/// The color of the outline.
/// The color of the fill.
///
/// When , an outline is drawn over the filled circle. Defaults to .
///
/// must be called first.
public void DrawSolidCircle(Vector2 center, float radius, Color color, Color fillColor, bool outline = true)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
double increment = Math.PI * 2.0 / CircleSegments;
double theta = 0.0;
Vector2 v0 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
theta += increment;
for (int i = 1; i < CircleSegments - 1; i++)
{
Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment));
_primitiveBatch.AddVertex(v0, fillColor, PrimitiveType.TriangleList);
_primitiveBatch.AddVertex(v1, fillColor, PrimitiveType.TriangleList);
_primitiveBatch.AddVertex(v2, fillColor, PrimitiveType.TriangleList);
theta += increment;
}
if (outline)
{
DrawCircle(center, radius, color);
}
}
///
/// Draws an arc outline.
///
/// The center point of the arc.
/// The radius of the arc.
/// The starting angle in radians.
///
/// The sweep angle in radians. Positive values sweep counter-clockwise.
/// Use MathHelper.TwoPi to draw a full circle.
///
/// The number of line segments used to approximate the arc.
/// The color of the arc.
/// must be called first.
public void DrawArc(Vector2 center, float radius, float startAngle, float sweepAngle, int sides, Color color)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
float step = sweepAngle / sides;
float theta = startAngle;
for (int i = 0; i < sides; i++)
{
Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + step), (float)Math.Sin(theta + step));
_primitiveBatch.AddVertex(v1, color, PrimitiveType.LineList);
_primitiveBatch.AddVertex(v2, color, PrimitiveType.LineList);
theta += step;
}
}
///
/// Draws a solid (filled) arc (pie slice) with an outline. The fill and outline use the same color.
///
/// The center point of the arc.
/// The radius of the arc.
/// The starting angle in radians.
///
/// The sweep angle in radians. Positive values sweep counter-clockwise.
/// Use MathHelper.TwoPi for a full circle.
///
/// The number of triangle segments used to fill the arc.
/// The color used for both the fill and the outline.
/// must be called first.
public void DrawSolidArc(Vector2 center, float radius, float startAngle, float sweepAngle, int sides, Color color)
{
DrawSolidArc(center, radius, startAngle, sweepAngle, sides, color, color);
}
///
/// Draws a solid (filled) arc (pie slice) with an outline.
///
/// The center point of the arc.
/// The radius of the arc.
/// The starting angle in radians.
///
/// The sweep angle in radians. Positive values sweep counter-clockwise.
/// Use MathHelper.TwoPi for a full circle.
///
/// The number of triangle segments used to fill the arc.
/// The color of the outline.
/// The color of the fill.
/// must be called first.
public void DrawSolidArc(Vector2 center, float radius, float startAngle, float sweepAngle, int sides, Color color, Color fillColor)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
float step = sweepAngle / sides;
float theta = startAngle;
for (int i = 0; i < sides; i++)
{
Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + step), (float)Math.Sin(theta + step));
_primitiveBatch.AddVertex(center, fillColor, PrimitiveType.TriangleList);
_primitiveBatch.AddVertex(v1, fillColor, PrimitiveType.TriangleList);
_primitiveBatch.AddVertex(v2, fillColor, PrimitiveType.TriangleList);
theta += step;
}
DrawArc(center, radius, startAngle, sweepAngle, sides, color);
}
///
/// Draws a line segment between two points.
///
/// The start point.
/// The end point.
/// The color of the line.
/// must be called first.
public void DrawSegment(Vector2 start, Vector2 end, Color color)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
_primitiveBatch.AddVertex(start, color, PrimitiveType.LineList);
_primitiveBatch.AddVertex(end, color, PrimitiveType.LineList);
}
///
/// Draws a polygon outline.
///
/// The world offset applied to all vertices.
/// The polygon vertices in local space.
/// The color of the outline.
///
/// When , an edge is drawn between the last and first vertex to close the polygon.
/// Defaults to .
///
/// must be called first.
public void DrawPolygon(Vector2 position, Vector2[] vertices, Color color, bool closed = true)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
int count = vertices.Length;
for (int i = 0; i < count - 1; i++)
{
_primitiveBatch.AddVertex(new Vector2(vertices[i].X + position.X, vertices[i].Y + position.Y), color, PrimitiveType.LineList);
_primitiveBatch.AddVertex(new Vector2(vertices[i + 1].X + position.X, vertices[i + 1].Y + position.Y), color, PrimitiveType.LineList);
}
if (closed)
{
_primitiveBatch.AddVertex(new Vector2(vertices[count - 1].X + position.X, vertices[count - 1].Y + position.Y), color, PrimitiveType.LineList);
_primitiveBatch.AddVertex(new Vector2(vertices[0].X + position.X, vertices[0].Y + position.Y), color, PrimitiveType.LineList);
}
}
///
/// Draws a solid (filled) polygon with an optional outline.
///
/// The world offset applied to all vertices.
/// The polygon vertices in local space.
///
/// The fill color. When is , also used for the outline.
///
///
/// When , an outline is drawn over the filled polygon. Defaults to .
///
/// must be called first.
public void DrawSolidPolygon(Vector2 position, Vector2[] vertices, Color color, bool outline = true)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
int count = vertices.Length;
if (count == 2)
{
DrawPolygon(position, vertices, color);
return;
}
Vector2[] outVertices;
int[] outIndices;
Triangulator.Triangulate(vertices, WindingOrder.CounterClockwise, out outVertices, out outIndices);
for (int i = 0; i < outIndices.Length - 2; i += 3)
{
_primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i]].X + position.X, outVertices[outIndices[i]].Y + position.Y), color, PrimitiveType.TriangleList);
_primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 1]].X + position.X, outVertices[outIndices[i + 1]].Y + position.Y), color, PrimitiveType.TriangleList);
_primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 2]].X + position.X, outVertices[outIndices[i + 2]].Y + position.Y), color, PrimitiveType.TriangleList);
}
if (outline)
{
DrawPolygon(position, vertices, color);
}
}
///
/// Draws an ellipse outline.
///
/// The center of the ellipse.
/// The horizontal (X) and vertical (Y) radii of the ellipse.
/// The number of line segments used to approximate the ellipse.
/// The color of the outline.
/// must be called first.
public void DrawEllipse(Vector2 center, Vector2 radius, int sides, Color color)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
DrawPolygon(center, CreateEllipse(radius.X, radius.Y, sides), color);
}
///
/// Draws a solid (filled) ellipse with an optional outline.
///
/// The center of the ellipse.
/// The horizontal (X) and vertical (Y) radii of the ellipse.
/// The number of segments used to approximate the ellipse.
///
/// The fill color. When is , also used for the outline.
///
///
/// When , an outline is drawn over the filled ellipse. Defaults to .
///
/// must be called first.
public void DrawSolidEllipse(Vector2 center, Vector2 radius, int sides, Color color, bool outline = true)
{
if (!_primitiveBatch.IsReady())
{
throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
}
Vector2[] vertices = CreateEllipse(radius.X, radius.Y, sides);
Vector2[] outVertices;
int[] outIndices;
Triangulator.Triangulate(vertices, WindingOrder.CounterClockwise, out outVertices, out outIndices);
for (int i = 0; i < outIndices.Length - 2; i += 3)
{
_primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i]].X + center.X, outVertices[outIndices[i]].Y + center.Y), color, PrimitiveType.TriangleList);
_primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 1]].X + center.X, outVertices[outIndices[i + 1]].Y + center.Y), color, PrimitiveType.TriangleList);
_primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 2]].X + center.X, outVertices[outIndices[i + 2]].Y + center.Y), color, PrimitiveType.TriangleList);
}
if (outline)
{
DrawPolygon(center, vertices, color);
}
}
private static Vector2[] CreateEllipse(float rx, float ry, int sides)
{
Vector2[] vertices = new Vector2[sides];
double t = 0.0;
double dt = 2.0 * Math.PI / sides;
for (int i = 0; i < sides; i++, t += dt)
{
vertices[i] = new Vector2((float)(rx * Math.Cos(t)), (float)(ry * Math.Sin(t)));
}
return vertices;
}
}
}