diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 0d36599cac..677400e8b9 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,7 +35,7 @@ END TEMPLATE--> ### Breaking changes -*None yet* +* Validate Box2i inputs to ensure no negative-sized boxes. ### New features diff --git a/Robust.Shared.Maths.Tests/Box2i_Test.cs b/Robust.Shared.Maths.Tests/Box2i_Test.cs index c924fc50f6..be1a5cdd5b 100644 --- a/Robust.Shared.Maths.Tests/Box2i_Test.cs +++ b/Robust.Shared.Maths.Tests/Box2i_Test.cs @@ -1,38 +1,129 @@ using NUnit.Framework; -namespace Robust.Shared.Maths.Tests +namespace Robust.Shared.Maths.Tests; + +[TestFixture, Parallelizable, TestOf(typeof(Box2i))] +internal sealed class Box2i_Test { - [TestFixture, Parallelizable, TestOf(typeof(Box2i))] - internal sealed class Box2i_Test + [Test] + public void Box2iUnion() { - [Test] - public void Box2iUnion() + var boxOne = new Box2i(-1, -1, 1, 1); + var boxTwo = new Box2i(0, 0, 2, 2); + + var result = boxOne.Union(boxTwo); + + using (Assert.EnterMultipleScope()) { - var boxOne = new Box2i(-1, -1, 1, 1); - var boxTwo = new Box2i(0, 0, 2, 2); - - var result = boxOne.Union(boxTwo); - Assert.That(result.Left, Is.EqualTo(-1)); Assert.That(result.Bottom, Is.EqualTo(-1)); Assert.That(result.Right, Is.EqualTo(2)); Assert.That(result.Top, Is.EqualTo(2)); } + } - [Test] - public void Box2iVector2iUnion() + [Test] + public void Box2iVector2iUnion() + { + var box = new Box2i(); + Assert.That(box, Is.EqualTo(Box2i.Empty)); + + box = box.UnionTile(Vector2i.Zero); + Assert.That(box.Right, Is.EqualTo(1)); + + box = box.UnionTile(Vector2i.One); + Assert.That(box.Top, Is.EqualTo(2)); + + box = box.Union(new Vector2i(2, 0)); + Assert.That(box.Right, Is.EqualTo(2)); + } + + [Test] + public void Box2iUsesDirectDimensions() + { + var valid = new Box2i(-1, -2, 3, 4); + + using (Assert.EnterMultipleScope()) { - var box = new Box2i(); - Assert.That(box, Is.EqualTo(Box2i.Empty)); + Assert.That(valid.Width, Is.EqualTo(4)); + Assert.That(valid.Height, Is.EqualTo(6)); + Assert.That(valid.Size, Is.EqualTo(new Vector2i(4, 6))); + Assert.That(valid.IsValid(), Is.True); + } + } - box = box.UnionTile(Vector2i.Zero); - Assert.That(box.Right, Is.EqualTo(1)); + [Test] + public void Box2iValidatesConstruction() + { + using (Assert.EnterMultipleScope()) + { + Assert.Throws(() => new Box2i(3, 4, -1, -2)); + Assert.Throws(() => new Box2i(new Vector2i(3, 4), new Vector2i(-1, -2))); + } + } - box = box.UnionTile(Vector2i.One); - Assert.That(box.Top, Is.EqualTo(2)); + [Test] + public void Box2iValidatesProperties() + { + var box = new Box2i(-1, -2, 3, 4); - box = box.Union(new Vector2i(2, 0)); - Assert.That(box.Right, Is.EqualTo(2)); + using (Assert.EnterMultipleScope()) + { + Assert.Throws(() => box.Left = 4); + Assert.Throws(() => box.Bottom = 5); + Assert.Throws(() => box.Right = -2); + Assert.Throws(() => box.Top = -3); + Assert.Throws(() => box.BottomLeft = new Vector2i(4, 0)); + Assert.Throws(() => box.TopRight = new Vector2i(0, -3)); + } + } + + [Test] + public void Box2iFromTwoPointsNormalizes() + { + var box = Box2i.FromTwoPoints(new Vector2i(3, -2), new Vector2i(-1, 4)); + + Assert.That(box, Is.EqualTo(new Box2i(-1, -2, 3, 4))); + Assert.That(box.IsValid(), Is.True); + } + + [Test] + public void Box2iContainsUsesValidBounds() + { + var box = new Box2i(-1, -1, 1, 1); + + using (Assert.EnterMultipleScope()) + { + Assert.That(box.Contains(Vector2i.Zero), Is.True); + Assert.That(box.Contains(new Vector2i(1, 1)), Is.True); + Assert.That(box.Contains(new Vector2i(1, 1), false), Is.False); + Assert.That(box.Contains(new Box2i(0, 0, 1, 1)), Is.True); + Assert.That(box.Encloses(new Box2i(0, 0, 1, 1)), Is.False); + } + } + + [Test] + public void Box2iIntersect() + { + var boxOne = new Box2i(-1, -1, 2, 2); + var boxTwo = new Box2i(0, 1, 3, 4); + + using (Assert.EnterMultipleScope()) + { + Assert.That(boxOne.Intersect(boxTwo), Is.EqualTo(new Box2i(0, 1, 2, 2))); + Assert.That(boxOne.Intersect(new Box2i(3, 3, 4, 4)), Is.EqualTo(Box2i.Empty)); + } + } + + [Test] + public void Box2iClosestPoint() + { + var box = new Box2i(-1, -2, 3, 4); + + using (Assert.EnterMultipleScope()) + { + Assert.That(box.ClosestPoint(new Vector2i(10, -10)), Is.EqualTo(new Vector2i(3, -2))); + Assert.That(box.ClosestPoint(Vector2i.Zero), Is.EqualTo(Vector2i.Zero)); } } } diff --git a/Robust.Shared.Maths/Box2i.cs b/Robust.Shared.Maths/Box2i.cs index f0596ed57e..710af7a11f 100644 --- a/Robust.Shared.Maths/Box2i.cs +++ b/Robust.Shared.Maths/Box2i.cs @@ -5,293 +5,514 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Robust.Shared.Utility; -namespace Robust.Shared.Maths +namespace Robust.Shared.Maths; + +[Serializable] +[StructLayout(LayoutKind.Explicit)] +public struct Box2i : IEquatable, ISpanFormattable { - [Serializable] - [StructLayout(LayoutKind.Explicit)] - public struct Box2i : IEquatable, ISpanFormattable + public static Box2i Empty => new(); + + [FieldOffset(sizeof(int) * 0)] internal int _left; + [FieldOffset(sizeof(int) * 1)] internal int _bottom; + [FieldOffset(sizeof(int) * 2)] internal int _right; + [FieldOffset(sizeof(int) * 3)] internal int _top; + + [FieldOffset(sizeof(int) * 0)] internal Vector2i _bottomLeft; + [FieldOffset(sizeof(int) * 2)] internal Vector2i _topRight; + + public int Left { - public static Box2i Empty => new(); - - [FieldOffset(sizeof(int) * 0)] public int Left; - [FieldOffset(sizeof(int) * 1)] public int Bottom; - [FieldOffset(sizeof(int) * 2)] public int Right; - [FieldOffset(sizeof(int) * 3)] public int Top; - - [FieldOffset(sizeof(int) * 0)] public Vector2i BottomLeft; - [FieldOffset(sizeof(int) * 2)] public Vector2i TopRight; - - public readonly Vector2i BottomRight => new(Right, Bottom); - public readonly Vector2i TopLeft => new(Left, Top); - public readonly int Width => Math.Abs(Right - Left); - public readonly int Height => Math.Abs(Top - Bottom); - public readonly Vector2i Size => new(Width, Height); - - public readonly int Area => Width * Height; - public readonly Vector2 Center => Size / 2f + BottomLeft; - - public Box2i(Vector2i bottomLeft, Vector2i topRight) + readonly get => _left; + set { - Unsafe.SkipInit(out this); + if (value > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "Left cannot be greater than Right."); - BottomLeft = bottomLeft; - TopRight = topRight; + _left = value; } + } - public Box2i(int left, int bottom, int right, int top) + public int Bottom + { + readonly get => _bottom; + set { - Unsafe.SkipInit(out this); + if (value > _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "Bottom cannot be greater than Top."); - Left = left; - Right = right; - Top = top; - Bottom = bottom; + _bottom = value; } + } - public static Box2i FromDimensions(int left, int bottom, int width, int height) + public int Right + { + readonly get => _right; + set { - return new(left, bottom, left + width, bottom + height); - } + if (value < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "Right cannot be less than Left."); - public static Box2i FromDimensions(Vector2i position, Vector2i size) + _right = value; + } + } + + public int Top + { + readonly get => _top; + set { - return FromDimensions(position.X, position.Y, size.X, size.Y); - } + if (value < _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "Top cannot be less than Bottom."); - public readonly bool Contains(int x, int y) + _top = value; + } + } + + public Vector2i BottomLeft + { + readonly get => _bottomLeft; + set { - return Contains(new Vector2i(x, y)); - } + if (value.X > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomLeft.X cannot be greater than Right."); - public readonly bool Contains(Vector2i point, bool closedRegion = true) + if (value.Y > _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomLeft.Y cannot be greater than Top."); + + _bottomLeft = value; + } + } + + public Vector2i TopRight + { + readonly get => _topRight; + set { - var xOk = closedRegion - ? point.X >= Left ^ point.X > Right - : point.X > Left ^ point.X >= Right; - var yOk = closedRegion - ? point.Y >= Bottom ^ point.Y > Top - : point.Y > Bottom ^ point.Y >= Top; - return xOk && yOk; + if (value.X < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopRight.X cannot be less than Left."); + + if (value.Y < _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopRight.Y cannot be less than Bottom."); + + _topRight = value; } + } - /// - /// Unlike Contains this assumes the Vector2i occupies an entire tile so we need the point to the top-right of it for consideration. - /// - public readonly bool ContainsTile(Vector2i tile, bool closedRegion = true) - { - var xOk = closedRegion - ? tile.X >= Left ^ tile.X + 1 > Right - : tile.X > Left ^ tile.X + 1 >= Right; - var yOk = closedRegion - ? tile.Y >= Bottom ^ tile.Y + 1 > Top - : tile.Y > Bottom ^ tile.Y + 1 >= Top; - return xOk && yOk; - } + public readonly Vector2i BottomRight => new(Right, Bottom); - public readonly bool IsEmpty() - { - return Bottom == Top || Left == Right; - } + public readonly Vector2i TopLeft => new(Left, Top); - /// Returns a UIBox2 translated by the given amount. - public readonly Box2i Translated(Vector2i point) - { - return new(Left + point.X, Bottom + point.Y, Right + point.X, Top + point.Y); - } + public readonly int Width => _right - _left; - /// - /// Returns the smallest rectangle that contains both of the rectangles. - /// - [Pure] - public readonly Box2i Union(in Box2i other) - { - var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft); - var topRight = Vector2i.ComponentMax(TopRight, other.TopRight); + public readonly int Height => _top - _bottom; - if (botLeft.X <= topRight.X && botLeft.Y <= topRight.Y) - return new Box2i(botLeft, topRight); + public readonly Vector2i Size => new(Width, Height); - return new Box2i(); - } + public readonly int Area => Width * Height; + public readonly Vector2 Center => new Vector2(_left + _right, _bottom + _top) / 2f; - /// - /// Unions the box2i with the specified Vector2i. - /// - /// - /// Union treating other as a single point and not an entire tile. - /// - public readonly Box2i Union(in Vector2i other) - { - if (Contains(other)) - return this; + private static void Validate(int left, int bottom, int right, int top) + { + if (left > right) + throw new ArgumentException("Left cannot be greater than Right.", nameof(left)); - var botLeft = Vector2i.ComponentMin(BottomLeft, other); - var topRight = Vector2i.ComponentMax(TopRight, other); + if (bottom > top) + throw new ArgumentException("Bottom cannot be greater than Top.", nameof(bottom)); + } - return new Box2i(botLeft, topRight); - } + public Box2i(Vector2i bottomLeft, Vector2i topRight) + { + Unsafe.SkipInit(out this); - /// - /// Unions the box2i with the specified Vector2i. - /// - /// - /// Union treating other as an entire tile and not a single point. - /// - public readonly Box2i UnionTile(in Vector2i other) - { - if (ContainsTile(other)) - return this; + Validate(bottomLeft.X, bottomLeft.Y, topRight.X, topRight.Y); - var botLeft = Vector2i.ComponentMin(BottomLeft, other); - var topRight = Vector2i.ComponentMax(TopRight, other + Vector2i.One); + _bottomLeft = bottomLeft; + _topRight = topRight; + } - return new Box2i(botLeft, topRight); - } + public Box2i(int left, int bottom, int right, int top) + { + Unsafe.SkipInit(out this); - // override object.Equals - public readonly override bool Equals(object? obj) - { - if (obj is Box2i box) - { - return Equals(box); - } + Validate(left, bottom, right, top); - return false; - } - - public readonly bool Equals(Box2i other) - { - return other.Left == Left && other.Right == Right && other.Bottom == Bottom && other.Top == Top; - } - - // override object.GetHashCode - public readonly override int GetHashCode() - { - var code = Left.GetHashCode(); - code = (code * 929) ^ Right.GetHashCode(); - code = (code * 929) ^ Top.GetHashCode(); - code = (code * 929) ^ Bottom.GetHashCode(); - return code; - } - - public static explicit operator Box2i(Box2 box) - { - return new((int) box.Left, (int) box.Bottom, (int) box.Right, (int) box.Top); - } - - public static implicit operator Box2(Box2i box) - { - return new(box.Left, box.Bottom, box.Right, box.Top); - } - - public readonly override string ToString() - { - return $"({Left}, {Bottom}, {Right}, {Top})"; - } - - public readonly string ToString(string? format, IFormatProvider? formatProvider) - { - return ToString(); - } - - public readonly bool TryFormat( - Span destination, - out int charsWritten, - ReadOnlySpan format, - IFormatProvider? provider) - { - return FormatHelpers.TryFormatInto( - destination, - out charsWritten, - $"({Left}, {Bottom}, {Right}, {Top})"); - } - - /// - /// Multiplies each side of the box by the scalar. - /// - [Pure] - public Box2i Scale(int scalar) - { - return new Box2i( - Left * scalar, - Bottom * scalar, - Right * scalar, - Top * scalar); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool Intersects(in Box2i other) - { - return other.Bottom <= this.Top && other.Top >= this.Bottom && other.Right >= this.Left && - other.Left <= this.Right; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public readonly Box2i Enlarged(int size) - { - return new(Left - size, Bottom - size, Right + size, Top + size); - } + _left = left; + _right = right; + _top = top; + _bottom = bottom; } /// - /// Iterates neighbouring tiles to a box2i. + /// Creates a Box2i with no bounds validation applied, use at your own risk. /// - public struct Box2iEdgeEnumerator + internal static Box2i DangerousCreate(int left, int bottom, int right, int top) { - private readonly bool _corners; - private readonly Box2i _box; - private readonly int _offset; - private int _x; - private int _y; + Unsafe.SkipInit(out Box2i box); + box._left = left; + box._right = right; + box._top = top; + box._bottom = bottom; + return box; + } - public Box2iEdgeEnumerator(Box2i box, bool corners, int offset = 1) + [Pure] + public static Box2i FromDimensions(int left, int bottom, int width, int height) + { + return new Box2i(left, bottom, left + width, bottom + height); + } + + [Pure] + public static Box2i FromDimensions(Vector2i position, Vector2i size) + { + return FromDimensions(position.X, position.Y, size.X, size.Y); + } + + [Pure] + public static Box2i FromTwoPoints(Vector2i a, Vector2i b) + { + return new Box2i(Vector2i.ComponentMin(a, b), Vector2i.ComponentMax(a, b)); + } + + [Pure] + public readonly bool Contains(int x, int y) + { + return Contains(new Vector2i(x, y)); + } + + [Pure] + public readonly bool Contains(in Box2i inner) + => Left <= inner.Left + && Bottom <= inner.Bottom + && Right >= inner.Right + && Top >= inner.Top; + + [Pure] + public readonly bool Contains(Vector2i point, bool closedRegion = true) + { + var xOk = closedRegion + ? point.X >= Left ^ point.X > Right + : point.X > Left ^ point.X >= Right; + var yOk = closedRegion + ? point.Y >= Bottom ^ point.Y > Top + : point.Y > Bottom ^ point.Y >= Top; + return xOk && yOk; + } + + /// + /// Unlike Contains this assumes the Vector2i occupies an entire tile so we need the point to the top-right of it for consideration. + /// + [Pure] + public readonly bool ContainsTile(Vector2i tile, bool closedRegion = true) + { + if (closedRegion) { - _box = box; - _corners = corners; - _x = _box.Left - offset; - _y = _box.Bottom - offset; - _offset = offset; + return tile.X >= Left + && tile.X + 1 <= Right + && tile.Y >= Bottom + && tile.Y + 1 <= Top; } - public bool MoveNext(out Vector2i index) + return tile.X > Left + && tile.X + 1 < Right + && tile.Y > Bottom + && tile.Y + 1 < Top; + } + + [Pure] + public readonly bool IsEmpty() + { + return Bottom >= Top || Left >= Right; + } + + /// Returns a UIBox2 translated by the given amount. + [Pure] + public readonly Box2i Translated(Vector2i point) + { + return new Box2i(Left + point.X, Bottom + point.Y, Right + point.X, Top + point.Y); + } + + /// + /// Returns the smallest rectangle that contains both of the rectangles. + /// + [Pure] + public readonly Box2i Union(in Box2i other) + { + var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft); + var topRight = Vector2i.ComponentMax(TopRight, other.TopRight); + + if (botLeft.X <= topRight.X && botLeft.Y <= topRight.Y) + return new Box2i(botLeft, topRight); + + return new Box2i(); + } + + /// + /// Unions the box2i with the specified Vector2i. + /// + /// + /// Union treating other as a single point and not an entire tile. + /// + [Pure] + public readonly Box2i Union(in Vector2i other) + { + if (Contains(other)) + return this; + + var botLeft = Vector2i.ComponentMin(BottomLeft, other); + var topRight = Vector2i.ComponentMax(TopRight, other); + + return new Box2i(botLeft, topRight); + } + + /// + /// Unions the box2i with the specified Vector2i. + /// + /// + /// Union treating other as an entire tile and not a single point. + /// + [Pure] + public readonly Box2i UnionTile(in Vector2i other) + { + if (ContainsTile(other)) + return this; + + var botLeft = Vector2i.ComponentMin(BottomLeft, other); + var topRight = Vector2i.ComponentMax(TopRight, other + Vector2i.One); + + return new Box2i(botLeft, topRight); + } + + // override object.Equals + public readonly override bool Equals(object? obj) + { + if (obj is Box2i box) { - for (var x = _x; x < _box.Right + _offset; x++) - { - for (var y = _y; y < _box.Top + _offset; y++) - { - if (x != _box.Left - _offset && - x != _box.Right + (_offset - 1) && - y != _box.Bottom - _offset && - y != _box.Top + (_offset - 1)) - { - continue; - } - - if (!_corners && - (x == _box.Left - _offset && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)) || - x == _box.Right && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)))) - { - continue; - } - - _x = x; - _y = y + 1; - - if (_y == _box.Top + _offset) - { - _x++; - _y = _box.Bottom - _offset; - } - - index = new Vector2i(x, y); - return true; - } - } - - index = default; - return false; + return Equals(box); } + + return false; + } + + public readonly bool Equals(Box2i other) + { + return other.Left == Left && other.Right == Right && other.Bottom == Bottom && other.Top == Top; + } + + // override object.GetHashCode + public readonly override int GetHashCode() + { + var code = Left.GetHashCode(); + code = (code * 929) ^ Right.GetHashCode(); + code = (code * 929) ^ Top.GetHashCode(); + code = (code * 929) ^ Bottom.GetHashCode(); + return code; + } + + public static explicit operator Box2i(Box2 box) + { + return new Box2i((int) box.Left, (int) box.Bottom, (int) box.Right, (int) box.Top); + } + + public static implicit operator Box2(Box2i box) + { + return new Box2(box.Left, box.Bottom, box.Right, box.Top); + } + + public readonly override string ToString() + { + return $"({Left}, {Bottom}, {Right}, {Top})"; + } + + /// + /// Compares two objects for equality by value. + /// + public static bool operator ==(Box2i a, Box2i b) + { + return a.Equals(b); + } + + public static bool operator !=(Box2i a, Box2i b) + { + return !a.Equals(b); + } + + public readonly string ToString(string? format, IFormatProvider? formatProvider) + { + return ToString(); + } + + public readonly bool TryFormat( + Span destination, + out int charsWritten, + ReadOnlySpan format, + IFormatProvider? provider) + { + return FormatHelpers.TryFormatInto( + destination, + out charsWritten, + $"({Left}, {Bottom}, {Right}, {Top})"); + } + + /// + /// Multiplies each side of the box by the scalar. + /// + [Pure] + public readonly Box2i Scale(int scalar) + { + return new Box2i( + Left * scalar, + Bottom * scalar, + Right * scalar, + Top * scalar); + } + + [Pure] + public readonly bool Intersects(in Box2i other) + { + return other._bottom <= _top + && other._top >= _bottom + && other._right >= _left + && other._left <= _right; + } + + [Pure] + public readonly Box2i Enlarged(int size) + { + return new Box2i(Left - size, Bottom - size, Right + size, Top + size); + } + + /// + /// Returns the intersection box created when two boxes overlap. + /// + [Pure] + public readonly Box2i Intersect(in Box2i other) + { + var bottomLeft = Vector2i.ComponentMax(BottomLeft, other.BottomLeft); + var topRight = Vector2i.ComponentMin(TopRight, other.TopRight); + + if (bottomLeft.X <= topRight.X && bottomLeft.Y <= topRight.Y) + return new Box2i(bottomLeft, topRight); + + return new Box2i(); + } + + [Pure] + public readonly bool IsValid() + { + return Right >= Left && Top >= Bottom; + } + + [Pure] + public readonly bool Encloses(in Box2i inner) + { + return Left < inner.Left && Bottom < inner.Bottom && Right > inner.Right && Top > inner.Top; + } + + /// + /// Returns this box enlarged to also contain the specified position. + /// + [Pure] + public readonly Box2i ExtendToContain(Vector2i vec) + { + return new Box2i(Vector2i.ComponentMin(BottomLeft, vec), Vector2i.ComponentMax(TopRight, vec)); + } + + /// + /// Given a point, returns the closest point to it inside the box. + /// + [Pure] + public readonly Vector2i ClosestPoint(in Vector2i position) + { + return new Vector2i( + MathHelper.Clamp(position.X, Left, Right), + MathHelper.Clamp(position.Y, Bottom, Top)); + } + + public static int Perimeter(in Box2i box) + => (box.Width + box.Height) * 2; + + public static int UnionPerimeter(in Box2i a, in Box2i b) + { + var left = Math.Min(a._left, b._left); + var bottom = Math.Min(a._bottom, b._bottom); + var right = Math.Max(a._right, b._right); + var top = Math.Max(a._top, b._top); + + return 2 * ((right - left) + (top - bottom)); + } + + [Pure] + public static Box2i Union(Box2i a, Box2i b) + { + return new Box2i( + Vector2i.ComponentMin(a.BottomLeft, b.BottomLeft), + Vector2i.ComponentMax(a.TopRight, b.TopRight)); + } + + [Pure] + public static Box2i Union(in Vector2i a, in Vector2i b) + { + return FromTwoPoints(a, b); + } +} + +/// +/// Iterates neighbouring tiles to a box2i. +/// +public struct Box2iEdgeEnumerator +{ + private readonly bool _corners; + private readonly Box2i _box; + private readonly int _offset; + private int _x; + private int _y; + + public Box2iEdgeEnumerator(Box2i box, bool corners, int offset = 1) + { + _box = box; + _corners = corners; + _x = _box.Left - offset; + _y = _box.Bottom - offset; + _offset = offset; + } + + public bool MoveNext(out Vector2i index) + { + for (var x = _x; x < _box.Right + _offset; x++) + { + for (var y = _y; y < _box.Top + _offset; y++) + { + if (x != _box.Left - _offset && + x != _box.Right + (_offset - 1) && + y != _box.Bottom - _offset && + y != _box.Top + (_offset - 1)) + { + continue; + } + + if (!_corners && + (x == _box.Left - _offset && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)) || + x == _box.Right && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)))) + { + continue; + } + + _x = x; + _y = y + 1; + + if (_y == _box.Top + _offset) + { + _x++; + _y = _box.Bottom - _offset; + } + + index = new Vector2i(x, y); + return true; + } + } + + index = default; + return false; } }