diff --git a/src/CultMath/CultMath.csproj b/src/CultMath/CultMath.csproj
index 73b3fd5..dfa5475 100644
--- a/src/CultMath/CultMath.csproj
+++ b/src/CultMath/CultMath.csproj
@@ -8,7 +8,7 @@
CultMath
CultMath
CultMath
- 0.1.0
+ 0.1.1
Clean-room C# math shaped for HLSL-style renderer code.
GameCult
https://github.com/GameCult/CultMath
diff --git a/src/CultMath/rect.cs b/src/CultMath/rect.cs
new file mode 100644
index 0000000..51e05c1
--- /dev/null
+++ b/src/CultMath/rect.cs
@@ -0,0 +1,59 @@
+namespace CultMath;
+
+///
+/// Axis-aligned 2D bounds stored canonically as normalized minimum and maximum
+/// corners. Point containment and intersection include the boundary.
+///
+public readonly record struct rect
+{
+ public rect(float minX, float minY, float maxX, float maxY)
+ : this(new float2(minX, minY), new float2(maxX, maxY))
+ {
+ }
+
+ public rect(float2 min, float2 max)
+ {
+ this.min = new float2(MathF.Min(min.x, max.x), MathF.Min(min.y, max.y));
+ this.max = new float2(MathF.Max(min.x, max.x), MathF.Max(min.y, max.y));
+ }
+
+ public float2 min { get; }
+ public float2 max { get; }
+
+ public float width => max.x - min.x;
+ public float height => max.y - min.y;
+ public float2 size => max - min;
+ public float2 center => (min + max) * 0.5f;
+ public float area => width * height;
+ public bool isEmpty => width == 0.0f || height == 0.0f;
+
+ public bool Contains(float2 point) =>
+ point.x >= min.x && point.x <= max.x &&
+ point.y >= min.y && point.y <= max.y;
+
+ public bool Contains(rect other) =>
+ other.min.x >= min.x && other.max.x <= max.x &&
+ other.min.y >= min.y && other.max.y <= max.y;
+
+ public bool Intersects(rect other) =>
+ min.x <= other.max.x && max.x >= other.min.x &&
+ min.y <= other.max.y && max.y >= other.min.y;
+
+ public rect Intersection(rect other)
+ {
+ if (!Intersects(other))
+ return new rect(0.0f, 0.0f, 0.0f, 0.0f);
+
+ return new rect(
+ MathF.Max(min.x, other.min.x),
+ MathF.Max(min.y, other.min.y),
+ MathF.Min(max.x, other.max.x),
+ MathF.Min(max.y, other.max.y));
+ }
+
+ public rect Encapsulate(float2 point) => new(
+ MathF.Min(min.x, point.x),
+ MathF.Min(min.y, point.y),
+ MathF.Max(max.x, point.x),
+ MathF.Max(max.y, point.y));
+}
diff --git a/src/CultMath/rect.cs.meta b/src/CultMath/rect.cs.meta
new file mode 100644
index 0000000..0b04a5c
--- /dev/null
+++ b/src/CultMath/rect.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5aab0d8e1ca94d42b85bba780a7ee449
diff --git a/tests/CultMath.Tests/MathTests.cs b/tests/CultMath.Tests/MathTests.cs
index ad26821..8d42a0c 100644
--- a/tests/CultMath.Tests/MathTests.cs
+++ b/tests/CultMath.Tests/MathTests.cs
@@ -199,6 +199,41 @@ public sealed class MathTests
Assert.Equal(new float2(2.0f, 0.0f), closest);
}
+ [Fact]
+ public void RectNormalizesAndDerivesFromMinMax()
+ {
+ var bounds = new rect(8.0f, 6.0f, 2.0f, -2.0f);
+
+ Assert.Equal(new float2(2.0f, -2.0f), bounds.min);
+ Assert.Equal(new float2(8.0f, 6.0f), bounds.max);
+ Assert.Equal(new float2(6.0f, 8.0f), bounds.size);
+ Assert.Equal(new float2(5.0f, 2.0f), bounds.center);
+ Assert.Equal(48.0f, bounds.area);
+ }
+
+ [Fact]
+ public void RectContainmentIncludesBoundary()
+ {
+ var bounds = new rect(-2.0f, -1.0f, 4.0f, 3.0f);
+
+ Assert.True(bounds.Contains(new float2(-2.0f, -1.0f)));
+ Assert.True(bounds.Contains(new float2(4.0f, 3.0f)));
+ Assert.True(bounds.Contains(new rect(-1.0f, 0.0f, 1.0f, 2.0f)));
+ Assert.False(bounds.Contains(new float2(4.001f, 3.0f)));
+ }
+
+ [Fact]
+ public void RectIntersectionTreatsTouchingEdgesAsContact()
+ {
+ var left = new rect(0.0f, 0.0f, 4.0f, 4.0f);
+ var touching = new rect(4.0f, 1.0f, 8.0f, 3.0f);
+ var separate = new rect(4.001f, 1.0f, 8.0f, 3.0f);
+
+ Assert.True(left.Intersects(touching));
+ Assert.Equal(0.0f, left.Intersection(touching).area);
+ Assert.False(left.Intersects(separate));
+ }
+
[Fact]
public void FirstOrderInterceptSolvesSimpleClosingTarget()
{
diff --git a/unity/org.gamecult.cultmath/package.json b/unity/org.gamecult.cultmath/package.json
index e45d0a3..d7039df 100644
--- a/unity/org.gamecult.cultmath/package.json
+++ b/unity/org.gamecult.cultmath/package.json
@@ -1,7 +1,7 @@
{
"name": "org.gamecult.cultmath",
"displayName": "CultMath",
- "version": "0.1.0",
+ "version": "0.1.1",
"unity": "2021.3",
"description": "Precompiled CultMath numeric primitives and matching HLSL semantics for GameCult Unity runtimes.",
"license": "MPL-2.0",