// 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; } } }