From 2c071ae6d1a64f9ff27e0a593e3b3e179b07e771 Mon Sep 17 00:00:00 2001 From: yannick-was-taken Date: Mon, 10 Apr 2023 21:57:26 +0200 Subject: [PATCH 1/3] godot-core: bug: fix size calculation in Rect2i::from_corners --- godot-core/src/builtin/rect2i.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/godot-core/src/builtin/rect2i.rs b/godot-core/src/builtin/rect2i.rs index 4fc611a5a..c33e1e9e3 100644 --- a/godot-core/src/builtin/rect2i.rs +++ b/godot-core/src/builtin/rect2i.rs @@ -58,7 +58,7 @@ impl Rect2i { pub fn from_corners(position: Vector2i, end: Vector2i) -> Self { Self { position, - size: position + end, + size: end - position, } } From 38e8f9b987010b3694f57c6ca2577a5647a975ea Mon Sep 17 00:00:00 2001 From: yannick-was-taken Date: Mon, 10 Apr 2023 22:00:18 +0200 Subject: [PATCH 2/3] godot-core: builtin: vector: provide component-wise min/max under unambiguous name --- godot-core/src/builtin/mod.rs | 6 ++-- godot-core/src/builtin/transform2d.rs | 4 +-- godot-core/src/builtin/transform3d.rs | 11 +++++-- godot-core/src/builtin/vector2.rs | 23 +++++++++++++++ godot-core/src/builtin/vector2i.rs | 36 +++++++++++++++++++++-- godot-core/src/builtin/vector3.rs | 31 ++++++++++++++------ godot-core/src/builtin/vector3i.rs | 38 +++++++++++++++++++++++-- godot-core/src/builtin/vector4.rs | 31 ++++++++++++++++++++ godot-core/src/builtin/vector4i.rs | 38 +++++++++++++++++++++++-- godot-core/src/builtin/vector_macros.rs | 24 ++++++++-------- 10 files changed, 206 insertions(+), 36 deletions(-) diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 7855f0cdf..7211e0b9e 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -212,7 +212,7 @@ mod real_mod { /// A 4-dimensional vector from [`glam`]. Using a floating-point format compatible with [`real`]. pub type RVec4 = glam::Vec4; - /// A 2x2 column-major matrix from [`glam`]. Using a floating-point format compatible with [`real`]. + /// A 2x2 column-major matrix from [`glam`]. Using a floating-point format compatible with [`real`]. pub type RMat2 = glam::Mat2; /// A 3x3 column-major matrix from [`glam`]. Using a floating-point format compatible with [`real`]. pub type RMat3 = glam::Mat3; @@ -279,7 +279,7 @@ mod real_mod { /// A 4-dimensional vector from [`glam`]. Using a floating-point format compatible with [`real`]. pub type RVec4 = glam::DVec4; - /// A 2x2 column-major matrix from [`glam`]. Using a floating-point format compatible with [`real`]. + /// A 2x2 column-major matrix from [`glam`]. Using a floating-point format compatible with [`real`]. pub type RMat2 = glam::DMat2; /// A 3x3 column-major matrix from [`glam`]. Using a floating-point format compatible with [`real`]. pub type RMat3 = glam::DMat3; @@ -302,6 +302,8 @@ pub use crate::real; pub(crate) use real_mod::*; pub use real_mod::{consts as real_consts, real}; +pub(crate) use glam::{IVec2, IVec3, IVec4}; + /// A macro to coerce float-literals into the real type. Mainly used where /// you'd normally use a suffix to specity the type, such as `115.0f32`. /// diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs index 7d6c94e4b..b6d0de9c1 100644 --- a/godot-core/src/builtin/transform2d.rs +++ b/godot-core/src/builtin/transform2d.rs @@ -333,8 +333,8 @@ impl Mul for Transform2D { let ya = self.b * rhs.position.y; let yb = self.b * rhs.end().y; - let position = Vector2::min(xa, xb) + Vector2::min(ya, yb) + self.origin; - let end = Vector2::max(xa, xb) + Vector2::max(ya, yb) + self.origin; + let position = Vector2::coord_min(xa, xb) + Vector2::coord_min(ya, yb) + self.origin; + let end = Vector2::coord_max(xa, xb) + Vector2::coord_max(ya, yb) + self.origin; Rect2::new(position, end - position) } } diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs index 5b4e4b284..4efc860c0 100644 --- a/godot-core/src/builtin/transform3d.rs +++ b/godot-core/src/builtin/transform3d.rs @@ -311,9 +311,14 @@ impl Mul for Transform3D { let za = self.basis.col_c() * rhs.position.z; let zb = self.basis.col_c() * rhs.end().z; - let position = - Vector3::min(xa, xb) + Vector3::min(ya, yb) + Vector3::min(za, zb) + self.origin; - let end = Vector3::max(xa, xb) + Vector3::max(ya, yb) + Vector3::max(za, zb) + self.origin; + let position = Vector3::coord_min(xa, xb) + + Vector3::coord_min(ya, yb) + + Vector3::coord_min(za, zb) + + self.origin; + let end = Vector3::coord_max(xa, xb) + + Vector3::coord_max(ya, yb) + + Vector3::coord_max(za, zb) + + self.origin; Aabb::new(position, end - position) } } diff --git a/godot-core/src/builtin/vector2.rs b/godot-core/src/builtin/vector2.rs index 17af59e37..5bf947596 100644 --- a/godot-core/src/builtin/vector2.rs +++ b/godot-core/src/builtin/vector2.rs @@ -343,3 +343,26 @@ impl GlamType for RVec2 { impl GlamConv for Vector2 { type Glam = RVec2; } + +#[cfg(test)] +mod test { + use crate::assert_eq_approx; + + use super::*; + + #[test] + fn coord_min_max() { + let a = Vector2::new(1.2, 3.4); + let b = Vector2::new(0.1, 5.6); + assert_eq_approx!( + a.coord_min(b), + Vector2::new(0.1, 3.4), + Vector2::is_equal_approx + ); + assert_eq_approx!( + a.coord_max(b), + Vector2::new(1.2, 5.6), + Vector2::is_equal_approx + ); + } +} diff --git a/godot-core/src/builtin/vector2i.rs b/godot-core/src/builtin/vector2i.rs index b20a2dd87..89c61c225 100644 --- a/godot-core/src/builtin/vector2i.rs +++ b/godot-core/src/builtin/vector2i.rs @@ -11,6 +11,9 @@ use sys::{ffi_methods, GodotFfi}; use crate::builtin::Vector2; +use super::glam_helpers::{GlamConv, GlamType}; +use super::IVec2; + /// Vector used for 2D math using integer coordinates. /// /// 2-element structure that can be used to represent positions in 2D space or any other pair of @@ -67,13 +70,13 @@ impl Vector2i { } /// Converts the corresponding `glam` type to `Self`. - fn from_glam(v: glam::IVec2) -> Self { + fn from_glam(v: IVec2) -> Self { Self::new(v.x, v.y) } /// Converts `self` to the corresponding `glam` type. fn to_glam(self) -> glam::IVec2 { - glam::IVec2::new(self.x, self.y) + IVec2::new(self.x, self.y) } } @@ -105,3 +108,32 @@ pub enum Vector2iAxis { impl GodotFfi for Vector2iAxis { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } + +impl GlamType for IVec2 { + type Mapped = Vector2i; + + fn to_front(&self) -> Self::Mapped { + Vector2i::new(self.x, self.y) + } + + fn from_front(mapped: &Self::Mapped) -> Self { + IVec2::new(mapped.x, mapped.y) + } +} + +impl GlamConv for Vector2i { + type Glam = IVec2; +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn coord_min_max() { + let a = Vector2i::new(1, 3); + let b = Vector2i::new(0, 5); + assert_eq!(a.coord_min(b), Vector2i::new(0, 3)); + assert_eq!(a.coord_max(b), Vector2i::new(1, 5)); + } +} diff --git a/godot-core/src/builtin/vector3.rs b/godot-core/src/builtin/vector3.rs index 33d1f8d6d..d229ee00e 100644 --- a/godot-core/src/builtin/vector3.rs +++ b/godot-core/src/builtin/vector3.rs @@ -381,13 +381,10 @@ impl GlamConv for Vector3 { #[cfg(test)] mod test { + use crate::assert_eq_approx; + use super::*; use godot::builtin::real_consts::TAU; - use godot::private::class_macros::assert_eq_approx; - - fn vec3_equal_approx(a: Vector3, b: Vector3) -> bool { - a.is_equal_approx(b) - } // Translated from Godot #[test] @@ -397,22 +394,38 @@ mod test { assert_eq_approx!( vector.rotated(Vector3::new(0.0, 1.0, 0.0), TAU), vector, - vec3_equal_approx + Vector3::is_equal_approx ); assert_eq_approx!( vector.rotated(Vector3::new(0.0, 1.0, 0.0), TAU / 4.0), Vector3::new(5.6, 3.4, -1.2), - vec3_equal_approx + Vector3::is_equal_approx ); assert_eq_approx!( vector.rotated(Vector3::new(1.0, 0.0, 0.0), TAU / 3.0), Vector3::new(1.2, -6.54974226119285642, 0.1444863728670914), - vec3_equal_approx + Vector3::is_equal_approx ); assert_eq_approx!( vector.rotated(Vector3::new(0.0, 0.0, 1.0), TAU / 2.0), vector.rotated(Vector3::new(0.0, 0.0, 1.0), TAU / -2.0), - vec3_equal_approx + Vector3::is_equal_approx + ); + } + + #[test] + fn coord_min_max() { + let a = Vector3::new(1.2, 3.4, 5.6); + let b = Vector3::new(0.1, 5.6, 2.3); + assert_eq_approx!( + a.coord_min(b), + Vector3::new(0.1, 3.4, 2.3), + Vector3::is_equal_approx + ); + assert_eq_approx!( + a.coord_max(b), + Vector3::new(1.2, 5.6, 5.6), + Vector3::is_equal_approx ); } } diff --git a/godot-core/src/builtin/vector3i.rs b/godot-core/src/builtin/vector3i.rs index dd7eaa69b..e2076c3fa 100644 --- a/godot-core/src/builtin/vector3i.rs +++ b/godot-core/src/builtin/vector3i.rs @@ -11,6 +11,9 @@ use sys::{ffi_methods, GodotFfi}; use crate::builtin::Vector3; +use super::glam_helpers::{GlamConv, GlamType}; +use super::IVec3; + /// Vector used for 3D math using integer coordinates. /// /// 3-element structure that can be used to represent positions in 3D space or any other triple of @@ -76,13 +79,13 @@ impl Vector3i { } /// Converts the corresponding `glam` type to `Self`. - fn from_glam(v: glam::IVec3) -> Self { + fn from_glam(v: IVec3) -> Self { Self::new(v.x, v.y, v.z) } /// Converts `self` to the corresponding `glam` type. - fn to_glam(self) -> glam::IVec3 { - glam::IVec3::new(self.x, self.y, self.z) + fn to_glam(self) -> IVec3 { + IVec3::new(self.x, self.y, self.z) } } @@ -116,3 +119,32 @@ pub enum Vector3iAxis { impl GodotFfi for Vector3iAxis { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } + +impl GlamType for IVec3 { + type Mapped = Vector3i; + + fn to_front(&self) -> Self::Mapped { + Vector3i::new(self.x, self.y, self.z) + } + + fn from_front(mapped: &Self::Mapped) -> Self { + IVec3::new(mapped.x, mapped.y, mapped.z) + } +} + +impl GlamConv for Vector3i { + type Glam = IVec3; +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn coord_min_max() { + let a = Vector3i::new(1, 3, 5); + let b = Vector3i::new(0, 5, 2); + assert_eq!(a.coord_min(b), Vector3i::new(0, 3, 2)); + assert_eq!(a.coord_max(b), Vector3i::new(1, 5, 5)); + } +} diff --git a/godot-core/src/builtin/vector4.rs b/godot-core/src/builtin/vector4.rs index f57392099..83d84974a 100644 --- a/godot-core/src/builtin/vector4.rs +++ b/godot-core/src/builtin/vector4.rs @@ -9,6 +9,7 @@ use std::fmt; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; +use crate::builtin::math::*; use crate::builtin::Vector4i; use super::glam_helpers::{GlamConv, GlamType}; @@ -80,6 +81,13 @@ impl Vector4 { fn to_glam(self) -> RVec4 { RVec4::new(self.x, self.y, self.z, self.w) } + + pub fn is_equal_approx(self, to: Self) -> bool { + is_equal_approx(self.x, to.x) + && is_equal_approx(self.y, to.y) + && is_equal_approx(self.z, to.z) + && is_equal_approx(self.w, to.w) + } } /// Formats the vector like Godot: `(x, y, z, w)`. @@ -126,3 +134,26 @@ impl GlamType for RVec4 { impl GlamConv for Vector4 { type Glam = RVec4; } + +#[cfg(test)] +mod test { + use crate::assert_eq_approx; + + use super::*; + + #[test] + fn coord_min_max() { + let a = Vector4::new(1.2, 3.4, 5.6, 0.1); + let b = Vector4::new(0.1, 5.6, 2.3, 1.2); + assert_eq_approx!( + a.coord_min(b), + Vector4::new(0.1, 3.4, 2.3, 0.1), + Vector4::is_equal_approx + ); + assert_eq_approx!( + a.coord_max(b), + Vector4::new(1.2, 5.6, 5.6, 1.2), + Vector4::is_equal_approx + ); + } +} diff --git a/godot-core/src/builtin/vector4i.rs b/godot-core/src/builtin/vector4i.rs index d8c341736..7d6088a80 100644 --- a/godot-core/src/builtin/vector4i.rs +++ b/godot-core/src/builtin/vector4i.rs @@ -11,6 +11,9 @@ use sys::{ffi_methods, GodotFfi}; use crate::builtin::Vector4; +use super::glam_helpers::{GlamConv, GlamType}; +use super::IVec4; + /// Vector used for 4D math using integer coordinates. /// /// 4-element structure that can be used to represent 4D grid coordinates or sets of integers. @@ -65,13 +68,13 @@ impl Vector4i { pub const ONE: Self = Self::splat(1); /// Converts the corresponding `glam` type to `Self`. - fn from_glam(v: glam::IVec4) -> Self { + fn from_glam(v: IVec4) -> Self { Self::new(v.x, v.y, v.z, v.w) } /// Converts `self` to the corresponding `glam` type. - fn to_glam(self) -> glam::IVec4 { - glam::IVec4::new(self.x, self.y, self.z, self.w) + fn to_glam(self) -> IVec4 { + IVec4::new(self.x, self.y, self.z, self.w) } } @@ -103,3 +106,32 @@ pub enum Vector4iAxis { impl GodotFfi for Vector4iAxis { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } + +impl GlamType for IVec4 { + type Mapped = Vector4i; + + fn to_front(&self) -> Self::Mapped { + Vector4i::new(self.x, self.y, self.z, self.w) + } + + fn from_front(mapped: &Self::Mapped) -> Self { + IVec4::new(mapped.x, mapped.y, mapped.z, mapped.w) + } +} + +impl GlamConv for Vector4i { + type Glam = IVec4; +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn coord_min_max() { + let a = Vector4i::new(1, 3, 5, 0); + let b = Vector4i::new(0, 5, 2, 1); + assert_eq!(a.coord_min(b), Vector4i::new(0, 3, 2, 0),); + assert_eq!(a.coord_max(b), Vector4i::new(1, 5, 5, 1)); + } +} diff --git a/godot-core/src/builtin/vector_macros.rs b/godot-core/src/builtin/vector_macros.rs index e66460e5c..3d3d8110c 100644 --- a/godot-core/src/builtin/vector_macros.rs +++ b/godot-core/src/builtin/vector_macros.rs @@ -238,6 +238,18 @@ macro_rules! impl_common_vector_fns { pub fn abs(self) -> Self { Self::from_glam(self.to_glam().abs()) } + + /// Returns a new vector containing the minimum of the two vectors, component-wise. + #[inline] + pub fn coord_min(self, other: Self) -> Self { + self.glam2(&other, |a, b| a.min(b)) + } + + /// Returns a new vector containing the maximum of the two vectors, component-wise. + #[inline] + pub fn coord_max(self, other: Self) -> Self { + self.glam2(&other, |a, b| a.max(b)) + } } }; } @@ -266,18 +278,6 @@ macro_rules! impl_float_vector_fns { pub fn normalized(self) -> Self { Self::from_glam(self.to_glam().normalize_or_zero()) } - - /// Returns a vector containing the minimum values for each element of `self` and `other`. - #[inline] - pub fn min(self, other: Self) -> Self { - self.glam2(&other, |a, b| a.min(b)) - } - - /// Returns a vector containing the maximum values for each element of `self` and `other`. - #[inline] - pub fn max(self, other: Self) -> Self { - self.glam2(&other, |a, b| a.max(b)) - } } }; } From d7e0e815ea346182600b09ecf43a83341677f0a6 Mon Sep 17 00:00:00 2001 From: yannick-was-taken Date: Mon, 10 Apr 2023 22:00:35 +0200 Subject: [PATCH 3/3] godot-core: builtin: reimplement Rect2i to provide functionality on outer type --- godot-core/src/builtin/mod.rs | 1 + godot-core/src/builtin/rect2i.rs | 488 ++++++++++++++++++++++++++++++- itest/rust/src/lib.rs | 1 + itest/rust/src/rect2i_test.rs | 92 ++++++ 4 files changed, 574 insertions(+), 8 deletions(-) create mode 100644 itest/rust/src/rect2i_test.rs diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 7211e0b9e..7a6d40000 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -332,6 +332,7 @@ macro_rules! real { /// The side of a [`Rect2`] or [`Rect2i`]. /// /// _Godot equivalent: `@GlobalScope.Side`_ +#[derive(Copy, Clone)] #[repr(C)] pub enum RectSide { Left = 0, diff --git a/godot-core/src/builtin/rect2i.rs b/godot-core/src/builtin/rect2i.rs index c33e1e9e3..8021b11ca 100644 --- a/godot-core/src/builtin/rect2i.rs +++ b/godot-core/src/builtin/rect2i.rs @@ -3,22 +3,23 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::cmp; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -use super::{Rect2, Vector2i}; +use super::{Rect2, RectSide, Vector2i}; /// 2D axis-aligned integer bounding box. /// /// `Rect2i` consists of a position, a size, and several utility functions. It is typically used for /// fast overlap tests. -/// -/// Currently most methods are only available through [`InnerRect2i`](super::inner::InnerRect2i). #[derive(Default, Copy, Clone, Eq, PartialEq, Debug)] #[repr(C)] pub struct Rect2i { + /// The position of the rectangle. pub position: Vector2i, + /// The size of the rectangle. pub size: Vector2i, } @@ -78,21 +79,492 @@ impl Rect2i { self.size = end - self.position } - /* Add in when `Rect2i::abs()` is implemented. + /// Returns a `Rect2i` with equivalent position and area, modified so that the top-left corner + /// is the origin and `width` and `height` are positive. + #[inline] + pub fn abs(self) -> Self { + let abs_size = self.size.abs(); + let offset = Vector2i::new(cmp::min(self.size.x, 0), cmp::min(self.size.y, 0)); + Self::new(self.position + offset, abs_size) + } + + /// Returns `true` if this `Rect2i` completely encloses another one. + /// + /// Any `Rect2i` encloses itself, i.e. an enclosed `Rect2i` does is not required to be a + /// proper sub-rect. + #[inline] + pub const fn encloses(&self, other: Self) -> bool { + self.assert_nonnegative(); + other.assert_nonnegative(); + + let own_end = self.end(); + let other_end = other.end(); + other.position.x >= self.position.x + && other.position.y >= self.position.y + && other_end.x <= own_end.x + && other_end.y <= own_end.y + } + + /// Returns a copy of this `Rect2i` expanded so that the borders align with the given point. + #[inline] + pub fn expand(self, to: Vector2i) -> Self { + self.assert_nonnegative(); + + let begin = self.position; + let end = self.end(); + Self::from_corners(begin.coord_min(to), end.coord_max(to)) + } + + /// Returns the area of the `Rect2i`. + /// + /// _Godot equivalent: `Rect2i.get_area` function_ + #[doc(alias = "get_area")] + #[inline] + pub const fn area(&self) -> i32 { + self.size.x * self.size.y + } + + /// Returns the center of the `Rect2i`, which is equal to `position + (size / 2)`. + /// + /// If `size` is an odd number, the returned center value will be rounded towards `position`. + /// + /// _Godot equivalent: `Rect2i.get_center` function_ + #[doc(alias = "get_center")] + #[inline] + pub fn center(&self) -> Vector2i { + self.position + (self.size / 2) + } + + /// Returns a copy of the `Rect2i` grown by the specified `amount` on all sides. + /// + /// `amount` may be negative, but care must be taken: If the resulting `size` has + /// negative components the computation may be incorrect. + #[inline] + pub fn grow(self, amount: i32) -> Self { + let amount_2d = Vector2i::new(amount, amount); + Self::from_corners(self.position - amount_2d, self.end() + amount_2d) + } + + /// Returns a copy of the `Rect2i` grown by the specified amount on each side individually. + /// + /// The individual amounts may be negative, but care must be taken: If the resulting `size` has + /// negative components the computation may be incorrect. + #[inline] + pub fn grow_individual(self, left: i32, top: i32, right: i32, bottom: i32) -> Self { + let top_left = Vector2i::new(left, top); + let bottom_right = Vector2i::new(right, bottom); + Self::from_corners(self.position - top_left, self.end() + bottom_right) + } + + /// Returns a copy of the `Rect2i` grown by the specified `amount` on the specified `RectSide`. + /// + /// `amount` may be negative, but care must be taken: If the resulting `size` has + /// negative components the computation may be incorrect. + #[inline] + pub fn grow_side(self, side: RectSide, amount: i32) -> Self { + match side { + RectSide::Left => self.grow_individual(amount, 0, 0, 0), + RectSide::Top => self.grow_individual(0, amount, 0, 0), + RectSide::Right => self.grow_individual(0, 0, amount, 0), + RectSide::Bottom => self.grow_individual(0, 0, 0, amount), + } + } + + /// Returns `true` if the `Rect2i` has area, and `false` if the `Rect2i` is linear, empty, or + /// has a negative `size`. + #[inline] + pub const fn has_area(&self) -> bool { + self.size.x > 0 && self.size.y > 0 + } + + /// Returns `true` if the `Rect2i` contains a point. By convention, the right and bottom edges + /// of the `Rect2i` are considered exclusive, so points on these edges are not included. + /// + /// _Godot equivalent: `Rect2i.has_point` function_ + #[doc(alias = "has_point")] + #[inline] + pub const fn contains_point(&self, point: Vector2i) -> bool { + self.assert_nonnegative(); + + let end = self.end(); + point.x >= self.position.x + && point.y >= self.position.y + && point.x < end.x + && point.y < end.y + } + + /// Returns the intersection of this `Rect2i` and `b`. + /// + /// If the rectangles do not intersect, `None` is returned. + /// + /// Note that rectangles that only share a border do not intersect. + #[inline] + pub fn intersection(self, b: Self) -> Option { + self.assert_nonnegative(); + b.assert_nonnegative(); + + let own_end = self.end(); + let b_end = b.end(); + if self.position.x >= b_end.x + || own_end.x <= b.position.x + || self.position.y >= b_end.y + || own_end.y <= b.position.y + { + return None; + } + + let new_pos = b.position.coord_max(self.position); + let new_end = b_end.coord_min(own_end); + + Some(Self::from_corners(new_pos, new_end)) + } + + /// Returns `true` if the `Rect2i` overlaps with `b` (i.e. they have at least one + /// point in common) + #[inline] + pub fn intersects(&self, b: Self) -> bool { + self.intersection(b).is_some() + } + + /// Returns a larger `Rect2i` that contains this `Rect2i` and `b`. + #[inline] + pub fn merge(self, b: Self) -> Self { + self.assert_nonnegative(); + b.assert_nonnegative(); + + let new_pos = b.position.coord_min(self.position); + let new_end = b.end().coord_max(self.end()); + + Self::from_corners(new_pos, new_end) + } + + /// Returns `true` if either of the coordinates of this `Rect2i`s `size` vector is negative. + #[inline] + pub const fn is_negative(&self) -> bool { + self.size.x < 0 || self.size.y < 0 + } + /// Assert that the size of the `Rect2i` is not negative. /// /// Certain functions will fail to give a correct result if the size is negative. #[inline] pub const fn assert_nonnegative(&self) { assert!( - self.size.x >= 0.0 && self.size.y >= 0.0, - "size {:?} is negative", - self.size + !self.is_negative(), + "Rect2i size is negative" /* Uncomment once formatting in const contexts is allowed. + Currently: + error[E0015]: cannot call non-const formatting macro in constant functions + "size {:?} is negative", + self.size + */ ); } - */ } impl GodotFfi for Rect2i { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn construction_tests() { + let zero = Rect2i::default(); + let new = Rect2i::new(Vector2i::new(0, 100), Vector2i::new(1280, 720)); + let from_components = Rect2i::from_components(0, 100, 1280, 720); + let from_rect2 = Rect2i::from_rect2(Rect2::from_components(0.1, 100.3, 1280.1, 720.42)); + let from_corners = Rect2i::from_corners(Vector2i::new(0, 100), Vector2i::new(1280, 820)); + + assert_eq!(zero.position.x, 0); + assert_eq!(zero.position.y, 0); + assert_eq!(zero.size.x, 0); + assert_eq!(zero.size.y, 0); + + assert_eq!(new, from_components); + assert_eq!(new, from_rect2); + assert_eq!(new, from_corners); + + assert_eq!(from_components, from_rect2); + assert_eq!(from_components, from_corners); + + assert_eq!(from_rect2, from_corners); + } + + #[test] + fn end() { + let rect = Rect2i::from_components(1, 2, 3, 4); + assert_eq!(rect.end(), Vector2i::new(4, 6)); + + let rect = Rect2i::from_components(1, 2, 0, 0); + assert_eq!(rect.end(), rect.position); + } + + #[test] + fn set_end() { + let mut old = Rect2i::from_components(1, 2, 3, 4); + let new = Rect2i::from_components(1, 2, 4, 4); + + old.set_end(Vector2i::new(5, 6)); + assert_eq!(old, new); + + old.set_end(old.position); + assert_eq!(old.end(), old.position); + } + + #[test] + fn abs() { + let rect = Rect2i::from_components(1, 2, -3, -4); + let abs = rect.abs(); + assert_eq!(abs.position.x, -2); + assert_eq!(abs.position.y, -2); + assert_eq!(abs.size.x, 3); + assert_eq!(abs.size.y, 4); + + let new_abs = abs.abs(); + assert_eq!(abs, new_abs); + } + + #[test] + fn encloses() { + let a = Rect2i::from_components(0, 0, 10, 10); + let b = Rect2i::from_components(4, 4, 1, 1); + let c = Rect2i::from_components(8, 8, 2, 2); + let d = Rect2i::from_components(8, 8, 2, 3); + + assert!(a.encloses(a)); + assert!(a.encloses(b)); + assert!(a.encloses(c)); + assert!(!a.encloses(d)); + + assert!(!b.encloses(a)); + assert!(b.encloses(b)); + assert!(!b.encloses(c)); + assert!(!b.encloses(d)); + + assert!(!c.encloses(a)); + assert!(!c.encloses(b)); + assert!(c.encloses(c)); + assert!(!c.encloses(d)); + + assert!(!d.encloses(a)); + assert!(!d.encloses(b)); + assert!(d.encloses(c)); + assert!(d.encloses(d)); + } + + #[test] + #[should_panic] + fn encloses_self_negative_panics() { + let rect = Rect2i::from_components(0, 0, -5, -5); + rect.encloses(Rect2i::default()); + } + + #[test] + #[should_panic] + fn encloses_other_negative_panics() { + let rect = Rect2i::from_components(0, 0, -5, -5); + Rect2i::default().encloses(rect); + } + + #[test] + fn expand_and_contains_point() { + let rect = Rect2i::from_components(0, 0, 0, 0); + let a = Vector2i::new(0, 0); + let b = Vector2i::new(5, 0); + let c = Vector2i::new(0, 5); + let d = Vector2i::new(4, 4); + + assert!(!rect.contains_point(a)); + assert!(!rect.contains_point(b)); + assert!(!rect.contains_point(c)); + assert!(!rect.contains_point(d)); + + let rect = rect.expand(a); + + // Note: expanding to a point does not necessarily include containing that point! + assert!(!rect.contains_point(a)); + assert!(!rect.contains_point(b)); + assert!(!rect.contains_point(c)); + assert!(!rect.contains_point(d)); + + let rect = rect.expand(b); + assert!(!rect.contains_point(a)); + assert!(!rect.contains_point(b)); + assert!(!rect.contains_point(c)); + assert!(!rect.contains_point(d)); + + let rect = rect.expand(c); + assert!(rect.contains_point(a)); + assert!(!rect.contains_point(b)); + assert!(!rect.contains_point(c)); + assert!(rect.contains_point(d)); + + let new_rect = rect.expand(d); + assert_eq!(rect, new_rect); + } + + #[test] + #[should_panic] + fn expand_self_negative_panics() { + let rect = Rect2i::from_components(0, 0, -5, -5); + rect.expand(Vector2i::ZERO); + } + + #[test] + #[should_panic] + fn contains_point_self_negative_panics() { + let rect = Rect2i::from_components(0, 0, -5, -5); + rect.contains_point(Vector2i::ZERO); + } + + #[test] + fn area_and_has_area() { + let a = Rect2i::from_components(0, 0, 10, 10); + let b = Rect2i::from_components(4, 4, 1, 1); + let c = Rect2i::from_components(8, 8, 2, 0); + let d = Rect2i::from_components(8, 8, 0, 3); + + assert!(a.has_area()); + assert_eq!(a.area(), 100); + assert!(b.has_area()); + assert_eq!(b.area(), 1); + assert!(!c.has_area()); + assert_eq!(c.area(), 0); + assert!(!d.has_area()); + assert_eq!(d.area(), 0); + } + + #[test] + fn center() { + let a = Rect2i::from_components(0, 0, 10, 10); + let b = Rect2i::from_components(4, 4, 1, 1); + let c = Rect2i::from_components(8, 8, 2, 0); + let d = Rect2i::from_components(8, 8, 0, 3); + + assert_eq!(a.center(), Vector2i::new(5, 5)); + assert_eq!(b.center(), Vector2i::new(4, 4)); + assert_eq!(c.center(), Vector2i::new(9, 8)); + assert_eq!(d.center(), Vector2i::new(8, 9)); + } + + #[test] + fn grow() { + let a = Rect2i::from_components(3, 3, 4, 4); + let b = Rect2i::from_components(0, 0, 10, 10); + let c = Rect2i::from_components(-3, -3, 16, 16); + + assert_eq!(a.grow(3), b); + assert_eq!(b.grow(3), c); + assert_eq!(a.grow(6), c); + + assert_eq!(a.grow(0), a); + assert_eq!(b.grow(0), b); + assert_eq!(c.grow(0), c); + + assert_eq!(c.grow(-3), b); + assert_eq!(b.grow(-3), a); + assert_eq!(c.grow(-6), a); + } + + #[test] + fn grow_individual_and_side() { + let begin = Rect2i::from_components(3, 3, 4, 4); + let end = Rect2i::from_components(0, 0, 10, 10); + + assert_ne!(begin, end); + assert!(end.encloses(begin)); + + let now = begin.grow_individual(3, 0, 0, 0); + let now_side = begin.grow_side(RectSide::Left, 3); + assert_ne!(now, end); + assert_eq!(now, now_side); + assert!(end.encloses(now)); + + let now = now.grow_individual(0, 3, 0, 0); + let now_side = now_side.grow_side(RectSide::Top, 3); + assert_ne!(now, end); + assert_eq!(now, now_side); + assert!(end.encloses(now)); + + let now = now.grow_individual(0, 0, 3, 0); + let now_side = now_side.grow_side(RectSide::Right, 3); + assert_ne!(now, end); + assert_eq!(now, now_side); + assert!(end.encloses(now)); + + let now = now.grow_individual(0, 0, 0, 3); + let now_side = now_side.grow_side(RectSide::Bottom, 3); + assert_eq!(now, end); + assert_eq!(now, now_side); + } + + #[test] + fn intersects_and_intersection() { + let a = Rect2i::from_components(0, 0, 10, 10); + let b = Rect2i::from_components(4, 4, 1, 1); + let c = Rect2i::from_components(8, 8, 2, 2); + let d = Rect2i::from_components(8, 8, 2, 3); + + assert!(a.intersects(b)); + assert_eq!(a.intersection(b), Some(b)); + assert!(a.intersects(c)); + assert_eq!(a.intersection(c), Some(c)); + assert!(a.intersects(d)); + assert_eq!(a.intersection(d), Some(c)); + + assert!(!b.intersects(c)); + assert_eq!(b.intersection(c), None); + assert!(!b.intersects(d)); + assert_eq!(b.intersection(d), None); + + assert!(c.intersects(d)); + assert_eq!(c.intersection(d), Some(c)); + } + + #[test] + #[should_panic] + fn intersects_self_negative_panics() { + let rect = Rect2i::from_components(0, 0, -5, -5); + rect.intersects(Rect2i::default()); + } + + #[test] + #[should_panic] + fn intersects_other_negative_panics() { + let rect = Rect2i::from_components(0, 0, -5, -5); + Rect2i::default().intersects(rect); + } + + #[test] + fn merge() { + let a = Rect2i::from_components(0, 0, 10, 10); + let b = Rect2i::from_components(4, 4, 1, 1); + let c = Rect2i::from_components(8, 8, 2, 2); + let d = Rect2i::from_components(8, 8, 2, 3); + + assert_eq!(a.merge(b), a); + assert_eq!(a.merge(c), a); + assert_eq!(a.merge(d), Rect2i::from_components(0, 0, 10, 11)); + + assert_eq!(b.merge(c), Rect2i::from_components(4, 4, 6, 6)); + assert_eq!(b.merge(d), Rect2i::from_components(4, 4, 6, 7)); + + assert_eq!(c.merge(d), d); + } + + #[test] + #[should_panic] + fn merge_self_negative_panics() { + let rect = Rect2i::from_components(0, 0, -5, -5); + rect.merge(Rect2i::default()); + } + + #[test] + #[should_panic] + fn merge_other_negative_panics() { + let rect = Rect2i::from_components(0, 0, -5, -5); + Rect2i::default().merge(rect); + } +} diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 9596878f6..b4640d125 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -25,6 +25,7 @@ mod object_test; mod packed_array_test; mod projection_test; mod quaternion_test; +mod rect2i_test; mod rid_test; mod singleton_test; mod string_test; diff --git a/itest/rust/src/rect2i_test.rs b/itest/rust/src/rect2i_test.rs new file mode 100644 index 000000000..14a5db1ff --- /dev/null +++ b/itest/rust/src/rect2i_test.rs @@ -0,0 +1,92 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +use std::fmt::Debug; + +use crate::itest; +use godot::prelude::{inner::InnerRect2i, *}; + +#[itest] +fn rect2i_equiv_unary() { + let test_rects = [ + Rect2i::from_components(0, 0, 1, 0), + Rect2i::from_components(0, 0, 1, 1), + Rect2i::from_components(0, 0, 10, 10), + Rect2i::from_components(4, 4, 1, 1), + Rect2i::from_components(8, 8, 2, 2), + Rect2i::from_components(8, 8, 2, 3), + ]; + let test_vectors = [ + Vector2i::ZERO, + Vector2i::new(0, 10), + Vector2i::new(10, 0), + Vector2i::new(10, 10), + ]; + let test_ints = [0, 1, 10, 32]; + let test_sides = [ + RectSide::Left, + RectSide::Top, + RectSide::Right, + RectSide::Bottom, + ]; + + fn evaluate_mappings(key: &str, a: T, b: T) + where + T: Eq + Debug, + { + assert_eq!(a, b, "{}: outer != inner ({:?} != {:?})", key, a, b); + } + + for a in test_rects { + let inner_a = InnerRect2i::from_outer(&a); + + evaluate_mappings("abs", a.abs(), inner_a.abs()); + evaluate_mappings("area", a.area(), inner_a.get_area() as i32); + evaluate_mappings("center", a.center(), inner_a.get_center()); + evaluate_mappings("has_area", a.has_area(), inner_a.has_area()); + + for b in test_rects { + evaluate_mappings("encloses", a.encloses(b), inner_a.encloses(b)); + evaluate_mappings("intersects", a.intersects(b), inner_a.intersects(b)); + evaluate_mappings( + "intersection", + a.intersection(b).unwrap_or_default(), + inner_a.intersection(b), + ); + evaluate_mappings("merge", a.merge(b), inner_a.merge(b)); + } + + for b in test_vectors { + evaluate_mappings("expand", a.expand(b), inner_a.expand(b)); + evaluate_mappings("contains_point", a.contains_point(b), inner_a.has_point(b)); + } + + for b in test_ints { + evaluate_mappings("grow", a.grow(b), inner_a.grow(b as i64)); + + for c in test_ints { + for d in test_ints { + for e in test_ints { + evaluate_mappings( + "grow_individual", + a.grow_individual(b, c, d, e), + inner_a.grow_individual(b as i64, c as i64, d as i64, e as i64), + ); + } + } + } + } + + for b in test_sides { + for c in test_ints { + evaluate_mappings( + "grow_side", + a.grow_side(b, c), + inner_a.grow_side(b as i64, c as i64), + ); + } + } + } +}