From f51acf5d70cbbb830a841be0779e414b6a9a2037 Mon Sep 17 00:00:00 2001 From: beauchama Date: Sun, 5 Jul 2026 10:44:59 -0400 Subject: [PATCH] Added Shapes --- .../Graphics/Drawing/CircleShape.cs | 67 ++++ .../Graphics/Drawing/ConvexShape.cs | 62 ++++ .../Graphics/Drawing/RectangleShape.cs | 66 ++++ .../Graphics/Drawing/RegularPolygon.cs | 78 +++++ src/KappaDuck.Quack/Graphics/Drawing/Shape.cs | 304 ++++++++++++++++++ 5 files changed, 577 insertions(+) create mode 100644 src/KappaDuck.Quack/Graphics/Drawing/CircleShape.cs create mode 100644 src/KappaDuck.Quack/Graphics/Drawing/ConvexShape.cs create mode 100644 src/KappaDuck.Quack/Graphics/Drawing/RectangleShape.cs create mode 100644 src/KappaDuck.Quack/Graphics/Drawing/RegularPolygon.cs create mode 100644 src/KappaDuck.Quack/Graphics/Drawing/Shape.cs diff --git a/src/KappaDuck.Quack/Graphics/Drawing/CircleShape.cs b/src/KappaDuck.Quack/Graphics/Drawing/CircleShape.cs new file mode 100644 index 0000000..498fb07 --- /dev/null +++ b/src/KappaDuck.Quack/Graphics/Drawing/CircleShape.cs @@ -0,0 +1,67 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +using KappaDuck.Quack.Geometry; + +namespace KappaDuck.Quack.Graphics.Drawing; + +/// +/// A circle shape, approximated by a configurable number of points. +/// +public sealed class CircleShape : Shape +{ + private float _radius; + private int _pointCount; + + /// + /// Creates a circle of the given radius. + /// + /// The radius of the circle, in local units. + /// The number of points used to approximate the circle; more points give a smoother circle. + public CircleShape(float radius, int pointCount = 30) + { + _radius = radius; + _pointCount = pointCount; + Update(); + } + + /// + /// Gets or sets the radius of the circle, in local units. + /// + public float Radius + { + get => _radius; + set + { + _radius = value; + Update(); + } + } + + /// + public override int PointCount => _pointCount; + + /// + /// Sets the number of points used to approximate the circle. + /// + /// The number of points; more points give a smoother circle. + public void SetPointCount(int count) + { + _pointCount = count; + Update(); + } + + /// + public override PointF GetPoint(int index) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _pointCount); + + Angle angle = Angle.FromDegrees((index * 360f / _pointCount) - 90f); + + float x = _radius + (_radius * angle.Cos); + float y = _radius + (_radius * angle.Sin); + + return new PointF(x, y); + } +} diff --git a/src/KappaDuck.Quack/Graphics/Drawing/ConvexShape.cs b/src/KappaDuck.Quack/Graphics/Drawing/ConvexShape.cs new file mode 100644 index 0000000..7036d12 --- /dev/null +++ b/src/KappaDuck.Quack/Graphics/Drawing/ConvexShape.cs @@ -0,0 +1,62 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +using KappaDuck.Quack.Geometry; + +namespace KappaDuck.Quack.Graphics.Drawing; + +/// +/// A convex polygon shape defined by an arbitrary set of points. +/// +/// The points must form a convex polygon; concave shapes will not fill correctly. +public sealed class ConvexShape : Shape +{ + private PointF[] _points; + + /// + /// Creates a convex shape with the given number of points, all at the origin. + /// + /// The number of points. Set each one with . + public ConvexShape(int pointCount) + { + _points = new PointF[pointCount]; + Update(); + } + + /// + /// Creates a convex shape from the given points. + /// + /// The points of the convex polygon, in local coordinates. + public ConvexShape(ReadOnlySpan points) + { + _points = points.ToArray(); + Update(); + } + + /// + public override int PointCount => _points.Length; + + /// + public override PointF GetPoint(int index) => _points[index]; + + /// + /// Sets the position of a single point of the shape. + /// + /// The index of the point, from 0 to minus one. + /// The new position of the point, in local coordinates. + public void SetPoint(int index, PointF point) + { + _points[index] = point; + Update(); + } + + /// + /// Replaces all points of the shape. + /// + /// The new points of the convex polygon, in local coordinates. + public void SetPoints(ReadOnlySpan points) + { + _points = points.ToArray(); + Update(); + } +} diff --git a/src/KappaDuck.Quack/Graphics/Drawing/RectangleShape.cs b/src/KappaDuck.Quack/Graphics/Drawing/RectangleShape.cs new file mode 100644 index 0000000..692dc48 --- /dev/null +++ b/src/KappaDuck.Quack/Graphics/Drawing/RectangleShape.cs @@ -0,0 +1,66 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +using KappaDuck.Quack.Geometry; + +namespace KappaDuck.Quack.Graphics.Drawing; + +/// +/// A rectangle shape defined by its width and height. +/// +public sealed class RectangleShape : Shape +{ + private SizeF _size; + + /// + /// Creates a rectangle of the given size. + /// + /// The width and height of the rectangle, in local units. + public RectangleShape(SizeF size) + { + _size = size; + Update(); + } + + /// + /// Creates a rectangle of the given position and size. + /// + /// The position of the rectangle, in local units. + /// The width and height of the rectangle, in local units. + public RectangleShape(PointF position, SizeF size) : this(size) + => Position = position; + + /// + /// Creates a rectangle of the given rect. + /// + /// The rectangle, in local units. + public RectangleShape(Rect rect) : this(rect.Position, rect.Size) + { + } + + /// + /// Gets or sets the width and height of the rectangle, in local units. + /// + public SizeF Size + { + get => _size; + set + { + _size = value; + Update(); + } + } + + /// + public override int PointCount { get; } = 4; + + /// + public override PointF GetPoint(int index) => index switch + { + 0 => new PointF(0f, 0f), + 1 => new PointF(_size.Width, 0f), + 2 => new PointF(_size.Width, _size.Height), + 3 => new PointF(0f, _size.Height), + _ => throw new ArgumentOutOfRangeException(nameof(index)) + }; +} diff --git a/src/KappaDuck.Quack/Graphics/Drawing/RegularPolygon.cs b/src/KappaDuck.Quack/Graphics/Drawing/RegularPolygon.cs new file mode 100644 index 0000000..f95bee8 --- /dev/null +++ b/src/KappaDuck.Quack/Graphics/Drawing/RegularPolygon.cs @@ -0,0 +1,78 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +using KappaDuck.Quack.Geometry; + +namespace KappaDuck.Quack.Graphics.Drawing; + +/// +/// A regular polygon shape whose points are evenly spaced around a circle. +/// +/// Use it for triangles, pentagons, hexagons and so on; a high side count approximates a circle. +public sealed class RegularPolygon : Shape +{ + private float _radius; + private int _sideCount; + + /// + /// Creates a regular polygon. + /// + /// The distance from the center to each point, in local units. + /// The number of sides. Must be at least 3. + /// is less than 3. + public RegularPolygon(float radius, int sideCount) + { + ArgumentOutOfRangeException.ThrowIfLessThan(sideCount, 3); + + _radius = radius; + _sideCount = sideCount; + + Update(); + } + + /// + /// Gets or sets the distance from the center to each point, in local units. + /// + public float Radius + { + get => _radius; + set + { + _radius = value; + Update(); + } + } + + /// + /// Gets or sets the number of sides. Must be at least 3. + /// + /// The value is less than 3. + public int SideCount + { + get => _sideCount; + set + { + ArgumentOutOfRangeException.ThrowIfLessThan(value, 3); + + _sideCount = value; + Update(); + } + } + + /// + public override int PointCount => _sideCount; + + /// + public override PointF GetPoint(int index) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _sideCount); + + Angle angle = Angle.FromDegrees((index * 360f / _sideCount) - 90f); + + float x = _radius + (_radius * angle.Cos); + float y = _radius + (_radius * angle.Sin); + + return new PointF(x, y); + } +} diff --git a/src/KappaDuck.Quack/Graphics/Drawing/Shape.cs b/src/KappaDuck.Quack/Graphics/Drawing/Shape.cs new file mode 100644 index 0000000..8634a25 --- /dev/null +++ b/src/KappaDuck.Quack/Graphics/Drawing/Shape.cs @@ -0,0 +1,304 @@ +// Copyright (c) KappaDuck. +// Licensed under the MIT license. + +using KappaDuck.Quack.Geometry; +using KappaDuck.Quack.Graphics.Primitives; +using KappaDuck.Quack.Graphics.Rendering; +using KappaDuck.Quack.Video.Pixels; + +namespace KappaDuck.Quack.Graphics.Drawing; + +/// +/// The base class for drawable shapes defined by a set of points such as rectangles, circles and polygons. +/// +/// +/// A shape has a solid and an optional outline ( and +/// ), and may be textured. Derive from it by implementing and +/// , and call whenever those would return different values. The points +/// must describe a convex polygon. +/// +public abstract class Shape : Transformable, IDrawable +{ + private Vertex[] _shape = []; + private int[] _indices = []; + + private Vertex[] _outline = []; + private int[] _outlineIndices = []; + + private Color _color = Colors.White; + private Color _outlineColor = Colors.White; + private RectI _region; + + private Rect _insideBounds; + private Rect _bounds; + + /// + /// Gets or sets the color that fills the interior of the shape. Defaults to . + /// + public Color FillColor + { + get => _color; + set + { + _color = value; + UpdateFillColors(); + } + } + + /// + /// Gets or sets the color of the shape's outline. Defaults to . + /// + /// The outline is only visible when is non-zero. + public Color OutlineColor + { + get => _outlineColor; + set + { + _outlineColor = value; + UpdateOutlineColors(); + } + } + + /// + /// Gets or sets the thickness of the shape's outline, in local units. Defaults to 0 (no outline). + /// + /// A positive thickness grows the outline outwards, a negative thickness grows it inwards. + public float OutlineThickness + { + get; + set + { + field = value; + Update(); + } + } + + /// + /// Gets or sets the texture drawn inside the shape, or for a solid fill. + /// + /// The is mapped across the shape's local bounds. The outline is never textured. + public Texture? Texture + { + get; + set + { + field = value; + UpdateTexCoords(); + } + } + + /// + /// Gets or sets the region of the mapped across the shape, in texture pixels. + /// + public RectI Region + { + get => _region; + set + { + _region = value; + UpdateTexCoords(); + } + } + + /// + /// Gets the number of points that define the shape. + /// + public abstract int PointCount { get; } + + /// + /// Gets the shape's bounding rectangle in its own local space, before its transform is applied. + /// + /// The bounds include the outline. + public Rect LocalBounds => _bounds; + + /// + /// Gets the shape's axis-aligned bounding rectangle in its parent's space, after its transform is applied. + /// + public Rect GlobalBounds => Transform.TransformRect(_bounds); + + /// + /// Gets the position of a point of the shape, in local coordinates. + /// + /// The index of the point, from 0 to minus one. + /// The point at . + public abstract PointF GetPoint(int index); + + /// + public void Draw(IRenderTarget target, RenderState state) + { + state = state with { Transform = state.Transform * Transform }; + + if (_shape.Length > 0) + target.Draw(_shape, _indices, state with { Texture = Texture }); + + if (_outline.Length > 0) + target.Draw(_outline, _outlineIndices, state with { Texture = null }); + } + + /// + /// Rebuilds the shape's geometry from its points. + /// + /// Call this from a derived class whenever or would return different values. + protected void Update() + { + if (PointCount < 3) + { + _shape = []; + _indices = []; + _outline = []; + _outlineIndices = []; + _insideBounds = default; + _bounds = default; + + return; + } + + _shape = new Vertex[PointCount + 1]; + + for (int i = 0; i < PointCount; i++) + _shape[i + 1].Position = GetPoint(i); + + _insideBounds = ComputeBounds(_shape, start: 1, PointCount); + _shape[0].Position = new PointF(_insideBounds.X + (_insideBounds.Width / 2f), _insideBounds.Y + (_insideBounds.Height / 2f)); + + _indices = new int[PointCount * 3]; + + for (int i = 0; i < PointCount; i++) + { + _indices[(i * 3) + 0] = 0; + _indices[(i * 3) + 1] = i + 1; + _indices[(i * 3) + 2] = ((i + 1) % PointCount) + 1; + } + + UpdateFillColors(); + UpdateTexCoords(); + UpdateOutline(); + + _bounds = _outline.Length > 0 ? ComputeBounds(_outline, start: 0, _outline.Length) : _insideBounds; + } + + private void UpdateFillColors() + { + ColorF color = _color.ToColorF(); + + for (int i = 0; i < _shape.Length; i++) + _shape[i].Color = color; + } + + private void UpdateTexCoords() + { + if (Texture is null || _shape.Length == 0) + return; + + float textureWidth = Texture.Width; + float textureHeight = Texture.Height; + + for (int i = 0; i < _shape.Length; i++) + { + float ratioX = _insideBounds.Width > 0f ? (_shape[i].Position.X - _insideBounds.X) / _insideBounds.Width : 0f; + float ratioY = _insideBounds.Height > 0f ? (_shape[i].Position.Y - _insideBounds.Y) / _insideBounds.Height : 0f; + + float u = (_region.X + (_region.Width * ratioX)) / textureWidth; + float v = (_region.Y + (_region.Height * ratioY)) / textureHeight; + + _shape[i].TextureCoordinate = new PointF(u, v); + } + } + + private void UpdateOutline() + { + if (MathF.ApproximatelyZero(OutlineThickness)) + { + _outline = []; + _outlineIndices = []; + + return; + } + + int count = PointCount; + PointF center = _shape[0].Position; + + _outline = new Vertex[(count + 1) * 2]; + + for (int i = 0; i < count; i++) + { + PointF p1 = _shape[i + 1].Position; + PointF p0 = _shape[((i - 1 + count) % count) + 1].Position; + PointF p2 = _shape[((i + 1) % count) + 1].Position; + + Vector2 n1 = Normal(p0, p1); + Vector2 n2 = Normal(p1, p2); + + if (Vector2.Dot(n1, center - p1) > 0f) + n1 = -n1; + + if (Vector2.Dot(n2, center - p1) > 0f) + n2 = -n2; + + float factor = 1f + Vector2.Dot(n1, n2); + Vector2 normal = !MathF.ApproximatelyZero(factor) ? (n1 + n2) / factor : n1; + + _outline[(i * 2) + 0].Position = p1; + _outline[(i * 2) + 1].Position = p1 + (normal * OutlineThickness); + } + + _outline[(count * 2) + 0].Position = _outline[0].Position; + _outline[(count * 2) + 1].Position = _outline[1].Position; + + _outlineIndices = new int[(_outline.Length - 2) * 3]; + + for (int i = 0; i < _outline.Length - 2; i++) + { + _outlineIndices[(i * 3) + 0] = i; + _outlineIndices[(i * 3) + 1] = i + 1; + _outlineIndices[(i * 3) + 2] = i + 2; + } + + UpdateOutlineColors(); + } + + private void UpdateOutlineColors() + { + ColorF color = _outlineColor.ToColorF(); + + for (int i = 0; i < _outline.Length; i++) + _outline[i].Color = color; + } + + private static Vector2 Normal(PointF from, PointF to) + { + Vector2 edge = to - from; + Vector2 normal = new(-edge.Y, edge.X); + + float magnitude = normal.Magnitude; + return !MathF.ApproximatelyZero(magnitude) ? normal / magnitude : normal; + } + + private static Rect ComputeBounds(Vertex[] vertices, int start, int count) + { + if (count == 0) + return default; + + PointF first = vertices[start].Position; + float minX = first.X, minY = first.Y, maxX = first.X, maxY = first.Y; + + for (int i = start + 1; i < start + count; i++) + { + PointF point = vertices[i].Position; + + if (point.X < minX) + minX = point.X; + + if (point.X > maxX) + maxX = point.X; + + if (point.Y < minY) + minY = point.Y; + + if (point.Y > maxY) + maxY = point.Y; + } + + return new Rect(minX, minY, maxX - minX, maxY - minY); + } +}