diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs new file mode 100644 index 000000000..66cb3e396 --- /dev/null +++ b/godot-core/src/builtin/basis.rs @@ -0,0 +1,799 @@ +use std::cmp::Ordering; +/* + * 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::{f32::consts::FRAC_PI_2, fmt::Display, ops::*}; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use super::glam_helpers::{GlamConv, GlamType}; +use super::{math::*, Quaternion, Vector3}; + +use glam; + +/// A 3x3 matrix, typically used as an orthogonal basis for [`Transform3D`](crate::builtin::Transform3D). +/// +/// Indexing into a `Basis` is done in row-major order. So `mat[1]` would return the first *row* and not +/// the first *column*/basis vector. This means that indexing into the matrix happens in the same order +/// it usually does in math, except that we index starting at 0. +/// +/// The basis vectors are the columns of the matrix, whereas the [`rows`](Self::rows) field represents +/// the row vectors. +#[derive(Copy, Clone, PartialEq, Debug)] +#[repr(C)] +pub struct Basis { + /// The rows of the matrix. These are *not* the basis vectors. + /// + /// To access the basis vectors see [`col_a()`](Self::col_a), [`set_col_a()`](Self::set_col_a), + /// [`col_b()`](Self::col_b), [`set_col_b()`](Self::set_col_b), [`col_c`](Self::col_c()), + /// [`set_col_c()`](Self::set_col_c). + pub rows: [Vector3; 3], +} + +impl Basis { + /// The identity basis, with no rotation or scaling applied. + /// + /// _Godot equivalent: `Basis.IDENTITY`_ + pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0, 1.0); + + /// The basis that will flip something along the X axis when used in a transformation. + /// + /// _Godot equivalent: `Basis.FLIP_X`_ + pub const FLIP_X: Self = Self::from_diagonal(-1.0, 1.0, 1.0); + + /// The basis that will flip something along the Y axis when used in a transformation. + /// + /// _Godot equivalent: `Basis.FLIP_Y`_ + pub const FLIP_Y: Self = Self::from_diagonal(1.0, -1.0, 1.0); + + /// The basis that will flip something along the Z axis when used in a transformation. + /// + /// _Godot equivalent: `Basis.FLIP_Z`_ + pub const FLIP_Z: Self = Self::from_diagonal(1.0, 1.0, -1.0); + + /// Create a new basis from 3 row vectors. These are *not* basis vectors. + pub const fn from_rows(x: Vector3, y: Vector3, z: Vector3) -> Self { + Self { rows: [x, y, z] } + } + + /// Create a new basis from 3 column vectors. + pub const fn from_cols(a: Vector3, b: Vector3, c: Vector3) -> Self { + Self::from_rows_array(&[a.x, b.x, c.x, a.y, b.y, c.y, a.z, b.z, c.z]) + } + + /// Create a `Basis` from an axis and angle. + /// + /// _Godot equivalent: `Basis(Vector3 axis, float angle)`_ + pub fn from_axis_angle(axis: Vector3, angle: f32) -> Self { + glam::Mat3::from_axis_angle(axis.to_glam(), angle).to_front() + } + + /// Create a diagonal matrix from the given values. + pub const fn from_diagonal(x: f32, y: f32, z: f32) -> Self { + Self { + rows: [ + Vector3::new(x, 0.0, 0.0), + Vector3::new(0.0, y, 0.0), + Vector3::new(0.0, 0.0, z), + ], + } + } + + /// Create a diagonal matrix from the given values. + /// + /// _Godot equivalent: `Basis.from_scale(Vector3 scale)` + pub const fn from_scale(scale: Vector3) -> Self { + Self::from_diagonal(scale.x, scale.y, scale.z) + } + + const fn from_rows_array(rows: &[f32; 9]) -> Self { + let [ax, bx, cx, ay, by, cy, az, bz, cz] = rows; + Self::from_rows( + Vector3::new(*ax, *bx, *cx), + Vector3::new(*ay, *by, *cy), + Vector3::new(*az, *bz, *cz), + ) + } + + /// Create a `Basis` from a `Quaternion`. + /// + /// _Godot equivalent: `Basis(Quaternion from)`_ + pub fn from_quat(quat: Quaternion) -> Self { + glam::Mat3::from_quat(quat.to_glam()).to_front() + } + + /// Create a `Basis` from three angles `a`, `b`, and `c` interpreted + /// as Euler angles according to the given `EulerOrder`. + /// + /// _Godot equivalent: `Basis.from_euler(Vector3 euler, int order)`_ + pub fn from_euler(order: EulerOrder, angles: Vector3) -> Self { + // Translated from "Basis::from_euler" in + // https://github.com/godotengine/godot/blob/master/core/math/basis.cpp + + // We can't use glam to do these conversions since glam uses intrinsic rotations + // whereas godot uses extrinsic rotations. + // see https://github.com/bitshifter/glam-rs/issues/337 + let Vector3 { x: a, y: b, z: c } = angles; + let xmat = + Basis::from_rows_array(&[1.0, 0.0, 0.0, 0.0, a.cos(), -a.sin(), 0.0, a.sin(), a.cos()]); + let ymat = + Basis::from_rows_array(&[b.cos(), 0.0, b.sin(), 0.0, 1.0, 0.0, -b.sin(), 0.0, b.cos()]); + let zmat = + Basis::from_rows_array(&[c.cos(), -c.sin(), 0.0, c.sin(), c.cos(), 0.0, 0.0, 0.0, 1.0]); + + match order { + EulerOrder::XYZ => xmat * ymat * zmat, + EulerOrder::XZY => xmat * zmat * ymat, + EulerOrder::YXZ => ymat * xmat * zmat, + EulerOrder::YZX => ymat * zmat * xmat, + EulerOrder::ZXY => zmat * xmat * ymat, + EulerOrder::ZYX => zmat * ymat * xmat, + } + } + + /// Creates a `Basis` with a rotation such that the forward axis (-Z) points + /// towards the `target` position. + /// + /// The up axis (+Y) points as close to the `up` vector as possible while + /// staying perpendicular to the forward axis. The resulting Basis is + /// orthonormalized. The `target` and `up` vectors cannot be zero, and + /// cannot be parallel to each other. + /// + /// _Godot equivalent: `Basis.looking_at()`_ + pub fn new_looking_at(target: Vector3, up: Vector3) -> Self { + super::inner::InnerBasis::looking_at(target, up) + } + + /// Creates a `[Vector3; 3]` with the columns of the `Basis`. + pub fn to_cols(self) -> [Vector3; 3] { + self.transposed().rows + } + + /// Creates a [`Quaternion`] representing the same rotation as this basis. + /// + /// _Godot equivalent: `Basis()`_ + pub fn to_quat(self) -> Quaternion { + glam::Quat::from_mat3(&self.orthonormalized().to_glam()).to_front() + } + + const fn to_rows_array(self) -> [f32; 9] { + let [Vector3 { + x: ax, + y: bx, + z: cx, + }, Vector3 { + x: ay, + y: by, + z: cy, + }, Vector3 { + x: az, + y: bz, + z: cz, + }] = self.rows; + [ax, bx, cx, ay, by, cy, az, bz, cz] + } + + /// Returns the scale of the matrix. + /// + /// _Godot equivalent: `Basis.get_scale()`_ + #[must_use] + pub fn scale(&self) -> Vector3 { + let det = self.determinant(); + let det_sign = if det < 0.0 { -1.0 } else { 1.0 }; + + Vector3::new( + self.col_a().length(), + self.col_b().length(), + self.col_c().length(), + ) * det_sign + } + + /// Returns the rotation of the matrix in euler angles. + /// + /// The order of the angles are given by `order`. + /// + /// _Godot equivalent: `Basis.get_euler()`_ + pub fn to_euler(self, order: EulerOrder) -> Vector3 { + use glam::swizzles::Vec3Swizzles as _; + use glam::Vec3; + + let col_a = self.col_a().to_glam(); + let col_b = self.col_b().to_glam(); + let col_c = self.col_c().to_glam(); + + let row_a = self.rows[0].to_glam(); + let row_b = self.rows[1].to_glam(); + let row_c = self.rows[2].to_glam(); + + match order { + EulerOrder::XYZ => { + let major = self.rows[1].z; + match Self::is_between_neg1_1(major) { + // Is it a pure Y rotation? + Ordering::Equal if row_b == Vec3::Y && col_b == Vec3::Y => { + // return the simplest form (human friendlier in editor and scripts) + Vec3::new(0.0, f32::atan2(self.rows[0].z, self.rows[0].x), 0.0) + } + _ => -Self::to_euler_inner(self.rows[0].z, col_c.yz(), row_a.yx(), col_b.zy()) + .yxz(), + } + } + EulerOrder::XZY => { + Self::to_euler_inner(self.rows[0].y, col_b.zy(), row_a.zx(), col_c.yz()).yzx() + } + EulerOrder::YXZ => { + let major = self.rows[1].z; + let fst = col_c.xz(); + let snd = row_b.xy(); + let rest = row_a.yx(); + match Self::is_between_neg1_1(major) { + // Is it a pure X rotation? + Ordering::Equal if row_a == Vec3::X && col_a == Vec3::X => { + // return the simplest form (human friendlier in editor and scripts) + Vec3::new(f32::atan2(-major, self.rows[1].y), 0.0, 0.0) + } + Ordering::Less => { + Self::to_euler_inner(major, fst, snd, rest) * Vec3::new(1.0, -1.0, 1.0) + } + _ => Self::to_euler_inner(major, fst, snd, rest), + } + } + EulerOrder::YZX => { + -Self::to_euler_inner(self.rows[1].x, row_b.zy(), col_a.zx(), row_c.yz()).yzx() + } + EulerOrder::ZXY => { + -Self::to_euler_inner(self.rows[2].y, row_c.xz(), col_b.xy(), row_a.zx()) + } + EulerOrder::ZYX => { + Self::to_euler_inner(self.rows[2].x, col_a.yx(), row_c.yz(), col_b.xy()).zxy() + } + } + .to_front() + } + + fn is_between_neg1_1(f: f32) -> Ordering { + if f >= (1.0 - CMP_EPSILON) { + Ordering::Greater + } else if f <= -(1.0 - CMP_EPSILON) { + Ordering::Less + } else { + Ordering::Equal + } + } + + fn to_euler_inner( + major: f32, + fst: glam::Vec2, + snd: glam::Vec2, + rest: glam::Vec2, + ) -> glam::Vec3 { + match Self::is_between_neg1_1(major) { + // It's -1 + Ordering::Less => glam::Vec3::new(FRAC_PI_2, -f32::atan2(rest.x, rest.y), 0.0), + Ordering::Equal => glam::Vec3::new( + f32::asin(-major), + f32::atan2(fst.x, fst.y), + f32::atan2(snd.x, snd.y), + ), + // It's 1 + Ordering::Greater => glam::Vec3::new(-FRAC_PI_2, -f32::atan2(rest.x, rest.y), 0.0), + } + } + + /// Returns the determinant of the matrix. + /// + /// _Godot equivalent: `Basis.determinant()`_ + pub fn determinant(&self) -> f32 { + self.to_glam().determinant() + } + + /// Introduce an additional scaling specified by the given 3D scaling factor. + /// + /// _Godot equivalent: `Basis.scaled()`_ + #[must_use] + pub fn scaled(self, scale: Vector3) -> Self { + Self::from_diagonal(scale.x, scale.y, scale.z) * self + } + + /// Returns the inverse of the matrix. + /// + /// _Godot equivalent: `Basis.inverse()`_ + #[must_use] + pub fn inverse(self) -> Basis { + self.glam(|mat| mat.inverse()) + } + + /// Returns the transposed version of the matrix. + /// + /// _Godot equivalent: `Basis.transposed()`_ + #[must_use] + pub fn transposed(self) -> Self { + Self::from_cols(self.rows[0], self.rows[1], self.rows[2]) + } + + /// Returns the orthonormalized version of the matrix (useful to call from + /// time to time to avoid rounding error for orthogonal matrices). This + /// performs a Gram-Schmidt orthonormalization on the basis of the matrix. + /// + /// _Godot equivalent: `Basis.orthonormalized()`_ + #[must_use] + pub fn orthonormalized(self) -> Self { + assert!( + !is_equal_approx(self.determinant(), 0.0), + "Determinant should not be zero." + ); + + // Gram-Schmidt Process + let mut x = self.col_a(); + let mut y = self.col_b(); + let mut z = self.col_c(); + + x = x.normalized(); + y = y - x * x.dot(y); + y = y.normalized(); + z = z - x * x.dot(z) - y * y.dot(z); + z = z.normalized(); + + Self::from_cols(x, y, z) + } + + /// Introduce an additional rotation around the given `axis` by `angle` + /// (in radians). The axis must be a normalized vector. + /// + /// _Godot equivalent: `Basis.rotated()`_ + #[must_use] + pub fn rotated(self, axis: Vector3, angle: f32) -> Self { + Self::from_axis_angle(axis, angle) * self + } + + /// Assuming that the matrix is a proper rotation matrix, slerp performs + /// a spherical-linear interpolation with another rotation matrix. + /// + /// _Godot equivalent: `Basis.slerp()`_ + #[must_use] + pub fn slerp(self, other: Self, weight: f32) -> Self { + let from = self.to_quat(); + let to = other.to_quat(); + + let mut result = Self::from_quat(from.slerp(to, weight)); + + for i in 0..3 { + result.rows[i] *= lerp(self.rows[i].length(), other.rows[i].length(), weight); + } + + result + } + + /// Transposed dot product with the X axis (column) of the matrix. + /// + /// _Godot equivalent: `Basis.tdotx()`_ + #[must_use] + pub fn tdotx(&self, with: Vector3) -> f32 { + self.col_a().dot(with) + } + + /// Transposed dot product with the Y axis (column) of the matrix. + /// + /// _Godot equivalent: `Basis.tdoty()`_ + #[must_use] + pub fn tdoty(&self, with: Vector3) -> f32 { + self.col_b().dot(with) + } + + /// Transposed dot product with the Z axis (column) of the matrix. + /// + /// _Godot equivalent: `Basis.tdotz()`_ + #[must_use] + pub fn tdotz(&self, with: Vector3) -> f32 { + self.col_c().dot(with) + } + + /// Returns `true` if this basis is finite. Meaning each element of the + /// matrix is not `NaN`, positive infinity, or negative infinity. + /// + /// _Godot equivalent: `Basis.is_finite()`_ + pub fn is_finite(&self) -> bool { + self.rows[0].is_finite() && self.rows[1].is_finite() && self.rows[2].is_finite() + } + + /// Returns `true` if this basis and `other` are approximately equal, + /// by calling `is_equal_approx` on each row. + /// + /// _Godot equivalent: `Basis.is_equal_approx(Basis b)`_ + pub fn is_equal_approx(&self, other: &Self) -> bool { + self.rows[0].is_equal_approx(other.rows[0]) + && self.rows[1].is_equal_approx(other.rows[1]) + && self.rows[2].is_equal_approx(other.rows[2]) + } + + /// Returns the first column of the matrix, + /// + /// _Godot equivalent: `Basis.x`_ + #[must_use] + pub fn col_a(&self) -> Vector3 { + Vector3::new(self.rows[0].x, self.rows[1].x, self.rows[2].x) + } + + /// Set the values of the first column of the matrix. + pub fn set_col_a(&mut self, col: Vector3) { + self.rows[0].x = col.x; + self.rows[1].x = col.y; + self.rows[2].x = col.z; + } + + /// Returns the second column of the matrix, + /// + /// _Godot equivalent: `Basis.y`_ + #[must_use] + pub fn col_b(&self) -> Vector3 { + Vector3::new(self.rows[0].y, self.rows[1].y, self.rows[2].y) + } + + /// Set the values of the second column of the matrix. + pub fn set_col_b(&mut self, col: Vector3) { + self.rows[0].y = col.x; + self.rows[1].y = col.y; + self.rows[2].y = col.z; + } + + /// Returns the third column of the matrix, + /// + /// _Godot equivalent: `Basis.z`_ + #[must_use] + pub fn col_c(&self) -> Vector3 { + Vector3::new(self.rows[0].z, self.rows[1].z, self.rows[2].z) + } + + /// Set the values of the third column of the matrix. + pub fn set_col_c(&mut self, col: Vector3) { + self.rows[0].z = col.x; + self.rows[1].z = col.y; + self.rows[2].z = col.z; + } +} + +impl Display for Basis { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Godot output: + // [X: (1, 0, 0), Y: (0, 1, 0), Z: (0, 0, 1)] + // Where X,Y,Z are the columns + let [a, b, c] = self.to_cols(); + + write!(f, "[a: {a}, b: {b}, c: {c}]") + } +} + +impl GlamConv for Basis { + type Glam = glam::Mat3; +} + +impl GlamType for glam::Mat3 { + type Mapped = Basis; + + fn to_front(&self) -> Self::Mapped { + Basis::from_rows_array(&self.to_cols_array()).transposed() + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self::from_cols_array(&mapped.to_rows_array()).transpose() + } +} + +impl GlamType for glam::Mat3A { + type Mapped = Basis; + + fn to_front(&self) -> Self::Mapped { + Basis::from_rows_array(&self.to_cols_array()).transposed() + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self::from_cols_array(&mapped.to_rows_array()).transpose() + } +} + +impl Default for Basis { + fn default() -> Self { + Self::IDENTITY + } +} + +impl Mul for Basis { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl Mul for Basis { + type Output = Self; + + fn mul(mut self, rhs: f32) -> Self::Output { + self *= rhs; + self + } +} + +impl MulAssign for Basis { + fn mul_assign(&mut self, rhs: f32) { + self.rows[0] *= rhs; + self.rows[1] *= rhs; + self.rows[2] *= rhs; + } +} + +impl Mul for Basis { + type Output = Vector3; + + fn mul(self, rhs: Vector3) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl GodotFfi for Basis { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +/// The ordering used to interpret a set of euler angles as extrinsic +/// rotations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub enum EulerOrder { + XYZ = 0, + XZY = 1, + YXZ = 2, + YZX = 3, + ZXY = 4, + ZYX = 5, +} + +#[cfg(test)] +mod test { + use std::f32::consts::{FRAC_PI_2, PI}; + + use crate::assert_eq_approx; + + use super::*; + + fn deg_to_rad(rotation: Vector3) -> Vector3 { + Vector3::new( + rotation.x.to_radians(), + rotation.y.to_radians(), + rotation.z.to_radians(), + ) + } + + // Translated from Godot + fn test_rotation(deg_original_euler: Vector3, rot_order: EulerOrder) { + // This test: + // 1. Converts the rotation vector from deg to rad. + // 2. Converts euler to basis. + // 3. Converts the above basis back into euler. + // 4. Converts the above euler into basis again. + // 5. Compares the basis obtained in step 2 with the basis of step 4 + // + // The conversion "basis to euler", done in the step 3, may be different from + // the original euler, even if the final rotation are the same. + // This happens because there are more ways to represents the same rotation, + // both valid, using eulers. + // For this reason is necessary to convert that euler back to basis and finally + // compares it. + // + // In this way we can assert that both functions: basis to euler / euler to basis + // are correct. + + // Euler to rotation + let original_euler: Vector3 = deg_to_rad(deg_original_euler); + let to_rotation: Basis = Basis::from_euler(rot_order, original_euler); + + // Euler from rotation + let euler_from_rotation: Vector3 = to_rotation.to_euler(rot_order); + let rotation_from_computed_euler: Basis = Basis::from_euler(rot_order, euler_from_rotation); + + let res: Basis = to_rotation.inverse() * rotation_from_computed_euler; + assert!( + (res.col_a() - Vector3::RIGHT).length() <= 0.1, + "Fail due to X {} with {deg_original_euler} using {rot_order:?}", + res.col_a() + ); + assert!( + (res.col_b() - Vector3::UP).length() <= 0.1, + "Fail due to Y {} with {deg_original_euler} using {rot_order:?}", + res.col_b() + ); + assert!( + (res.col_c() - Vector3::BACK).length() <= 0.1, + "Fail due to Z {} with {deg_original_euler} using {rot_order:?}", + res.col_c() + ); + + // Double check `to_rotation` decomposing with XYZ rotation order. + let euler_xyz_from_rotation: Vector3 = to_rotation.to_euler(EulerOrder::XYZ); + let rotation_from_xyz_computed_euler: Basis = + Basis::from_euler(EulerOrder::XYZ, euler_xyz_from_rotation); + + let res = to_rotation.inverse() * rotation_from_xyz_computed_euler; + + assert!( + (res.col_a() - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to X {} with {deg_original_euler} using {rot_order:?}", + res.col_a(), + ); + assert!( + (res.col_b() - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to Y {} with {deg_original_euler} using {rot_order:?}", + res.col_b(), + ); + assert!( + (res.col_c() - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to Z {} with {deg_original_euler} using {rot_order:?}", + res.col_c(), + ); + } + + #[test] + fn consts_behavior_correct() { + let v = Vector3::new(1.0, 2.0, 3.0); + + assert_eq_approx!(Basis::IDENTITY * v, v, Vector3::is_equal_approx); + assert_eq_approx!( + Basis::FLIP_X * v, + Vector3::new(-v.x, v.y, v.z), + Vector3::is_equal_approx + ); + assert_eq_approx!( + Basis::FLIP_Y * v, + Vector3::new(v.x, -v.y, v.z), + Vector3::is_equal_approx + ); + assert_eq_approx!( + Basis::FLIP_Z * v, + Vector3::new(v.x, v.y, -v.z), + Vector3::is_equal_approx + ); + } + + #[test] + fn basic_rotation_correct() { + assert_eq_approx!( + Basis::from_axis_angle(Vector3::FORWARD, 0.0) * Vector3::RIGHT, + Vector3::RIGHT, + Vector3::is_equal_approx, + ); + assert_eq_approx!( + Basis::from_axis_angle(Vector3::FORWARD, FRAC_PI_2) * Vector3::RIGHT, + Vector3::DOWN, + Vector3::is_equal_approx, + ); + assert_eq_approx!( + Basis::from_axis_angle(Vector3::FORWARD, PI) * Vector3::RIGHT, + Vector3::LEFT, + Vector3::is_equal_approx, + ); + assert_eq_approx!( + Basis::from_axis_angle(Vector3::FORWARD, PI + FRAC_PI_2) * Vector3::RIGHT, + Vector3::UP, + Vector3::is_equal_approx, + ); + } + + // Translated from Godot + #[test] + fn basis_euler_conversions() { + let euler_order_to_test: Vec = vec![ + EulerOrder::XYZ, + EulerOrder::XZY, + EulerOrder::YZX, + EulerOrder::YXZ, + EulerOrder::ZXY, + EulerOrder::ZYX, + ]; + + let vectors_to_test: Vec = vec![ + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(0.5, 0.5, 0.5), + Vector3::new(-0.5, -0.5, -0.5), + Vector3::new(40.0, 40.0, 40.0), + Vector3::new(-40.0, -40.0, -40.0), + Vector3::new(0.0, 0.0, -90.0), + Vector3::new(0.0, -90.0, 0.0), + Vector3::new(-90.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, 90.0), + Vector3::new(0.0, 90.0, 0.0), + Vector3::new(90.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, -30.0), + Vector3::new(0.0, -30.0, 0.0), + Vector3::new(-30.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, 30.0), + Vector3::new(0.0, 30.0, 0.0), + Vector3::new(30.0, 0.0, 0.0), + Vector3::new(0.5, 50.0, 20.0), + Vector3::new(-0.5, -50.0, -20.0), + Vector3::new(0.5, 0.0, 90.0), + Vector3::new(0.5, 0.0, -90.0), + Vector3::new(360.0, 360.0, 360.0), + Vector3::new(-360.0, -360.0, -360.0), + Vector3::new(-90.0, 60.0, -90.0), + Vector3::new(90.0, 60.0, -90.0), + Vector3::new(90.0, -60.0, -90.0), + Vector3::new(-90.0, -60.0, -90.0), + Vector3::new(-90.0, 60.0, 90.0), + Vector3::new(90.0, 60.0, 90.0), + Vector3::new(90.0, -60.0, 90.0), + Vector3::new(-90.0, -60.0, 90.0), + Vector3::new(60.0, 90.0, -40.0), + Vector3::new(60.0, -90.0, -40.0), + Vector3::new(-60.0, -90.0, -40.0), + Vector3::new(-60.0, 90.0, 40.0), + Vector3::new(60.0, 90.0, 40.0), + Vector3::new(60.0, -90.0, 40.0), + Vector3::new(-60.0, -90.0, 40.0), + Vector3::new(-90.0, 90.0, -90.0), + Vector3::new(90.0, 90.0, -90.0), + Vector3::new(90.0, -90.0, -90.0), + Vector3::new(-90.0, -90.0, -90.0), + Vector3::new(-90.0, 90.0, 90.0), + Vector3::new(90.0, 90.0, 90.0), + Vector3::new(90.0, -90.0, 90.0), + Vector3::new(20.0, 150.0, 30.0), + Vector3::new(20.0, -150.0, 30.0), + Vector3::new(-120.0, -150.0, 30.0), + Vector3::new(-120.0, -150.0, -130.0), + Vector3::new(120.0, -150.0, -130.0), + Vector3::new(120.0, 150.0, -130.0), + Vector3::new(120.0, 150.0, 130.0), + ]; + + for order in euler_order_to_test.iter() { + for vector in vectors_to_test.iter() { + test_rotation(*vector, *order); + } + } + } + + // Translated from Godot + #[test] + fn basis_finite_number_test() { + let x: Vector3 = Vector3::new(0.0, 1.0, 2.0); + let infinite: Vector3 = Vector3::new(f32::NAN, f32::NAN, f32::NAN); + + assert!( + Basis::from_cols(x, x, x).is_finite(), + "Basis with all components finite should be finite" + ); + + assert!( + !Basis::from_cols(infinite, x, x).is_finite(), + "Basis with one component infinite should not be finite." + ); + assert!( + !Basis::from_cols(x, infinite, x).is_finite(), + "Basis with one component infinite should not be finite." + ); + assert!( + !Basis::from_cols(x, x, infinite).is_finite(), + "Basis with one component infinite should not be finite." + ); + + assert!( + !Basis::from_cols(infinite, infinite, x).is_finite(), + "Basis with two components infinite should not be finite." + ); + assert!( + !Basis::from_cols(infinite, x, infinite).is_finite(), + "Basis with two components infinite should not be finite." + ); + assert!( + !Basis::from_cols(x, infinite, infinite).is_finite(), + "Basis with two components infinite should not be finite." + ); + + assert!( + !Basis::from_cols(infinite, infinite, infinite).is_finite(), + "Basis with three components infinite should not be finite." + ); + } +} diff --git a/godot-core/src/builtin/math.rs b/godot-core/src/builtin/math.rs index 880c0cb78..8e52185ab 100644 --- a/godot-core/src/builtin/math.rs +++ b/godot-core/src/builtin/math.rs @@ -6,6 +6,8 @@ use std::f32::consts::TAU; +use super::Vector2; + pub const CMP_EPSILON: f32 = 0.00001; pub fn lerp(a: f32, b: f32, t: f32) -> f32 { @@ -23,6 +25,18 @@ pub fn is_equal_approx(a: f32, b: f32) -> bool { (a - b).abs() < tolerance } +/// Check if two angles are approximately equal, by comparing the distance +/// between the points on the unit circle with 0 using [`is_equal_approx`]. +pub fn is_angle_equal_approx(a: f32, b: f32) -> bool { + let (x1, y1) = a.sin_cos(); + let (x2, y2) = b.sin_cos(); + + is_equal_approx( + Vector2::distance_to(Vector2::new(x1, y1), Vector2::new(x2, y2)), + 0.0, + ) +} + pub fn is_zero_approx(s: f32) -> bool { s.abs() < CMP_EPSILON } @@ -190,6 +204,15 @@ mod test { assert_ne_approx!(1.0, 2.0, is_equal_approx, "Message {}", "formatted"); } + #[test] + fn angle_equal_approx() { + assert_eq_approx!(1.0, 1.000001, is_angle_equal_approx); + assert_eq_approx!(0.0, TAU, is_angle_equal_approx); + assert_eq_approx!(PI, -PI, is_angle_equal_approx); + assert_eq_approx!(4.45783, -(TAU - 4.45783), is_angle_equal_approx); + assert_eq_approx!(31.0 * PI, -13.0 * PI, is_angle_equal_approx); + } + #[test] #[should_panic(expected = "I am inside format")] fn eq_approx_fail_with_message() { @@ -198,22 +221,17 @@ mod test { #[test] fn lerp_angle_test() { - assert_eq_approx!(lerp_angle(0.0, PI, 0.5), -FRAC_PI_2, is_equal_approx); + assert_eq_approx!(lerp_angle(0.0, PI, 0.5), -FRAC_PI_2, is_angle_equal_approx); assert_eq_approx!( lerp_angle(0.0, PI + 3.0 * TAU, 0.5), FRAC_PI_2, - is_equal_approx + is_angle_equal_approx ); let angle = PI * 2.0 / 3.0; assert_eq_approx!( - lerp_angle(-5.0 * TAU, angle + 3.0 * TAU, 0.5).sin(), - (angle / 2.0).sin(), - is_equal_approx - ); - assert_eq_approx!( - lerp_angle(-5.0 * TAU, angle + 3.0 * TAU, 0.5).cos(), - (angle / 2.0).cos(), - is_equal_approx + lerp_angle(-5.0 * TAU, angle + 3.0 * TAU, 0.5), + (angle / 2.0), + is_angle_equal_approx ); } } diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 95cb8514b..656f38bc8 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -36,15 +36,19 @@ pub use crate::{array, dict, varray}; pub use array_inner::{Array, TypedArray}; +pub use basis::*; pub use color::*; pub use dictionary_inner::Dictionary; pub use math::*; pub use node_path::*; pub use others::*; pub use packed_array::*; +pub use projection::*; pub use quaternion::*; pub use string::*; pub use string_name::*; +pub use transform2d::*; +pub use transform3d::*; pub use variant::*; pub use vector2::*; pub use vector2i::*; @@ -55,6 +59,7 @@ pub use vector4i::*; /// Meta-information about variant types, properties and class names. pub mod meta; +mod projection; /// Specialized types related to arrays. pub mod array { @@ -79,6 +84,7 @@ mod array_inner; #[path = "dictionary.rs"] mod dictionary_inner; +mod basis; mod color; mod glam_helpers; mod math; @@ -88,6 +94,8 @@ mod packed_array; mod quaternion; mod string; mod string_name; +mod transform2d; +mod transform3d; mod variant; mod vector2; mod vector2i; diff --git a/godot-core/src/builtin/others.rs b/godot-core/src/builtin/others.rs index 82c956526..8deae9c11 100644 --- a/godot-core/src/builtin/others.rs +++ b/godot-core/src/builtin/others.rs @@ -17,10 +17,6 @@ impl_builtin_stub!(Rect2, OpaqueRect2); impl_builtin_stub!(Rect2i, OpaqueRect2i); impl_builtin_stub!(Plane, OpaquePlane); impl_builtin_stub!(Aabb, OpaqueAabb); -impl_builtin_stub!(Basis, OpaqueBasis); -impl_builtin_stub!(Transform2D, OpaqueTransform2D); -impl_builtin_stub!(Transform3D, OpaqueTransform3D); -impl_builtin_stub!(Projection, OpaqueProjection); impl_builtin_stub!(Rid, OpaqueRid); impl_builtin_stub!(Callable, OpaqueCallable); impl_builtin_stub!(Signal, OpaqueSignal); diff --git a/godot-core/src/builtin/projection.rs b/godot-core/src/builtin/projection.rs new file mode 100644 index 000000000..92ee2d5f4 --- /dev/null +++ b/godot-core/src/builtin/projection.rs @@ -0,0 +1,428 @@ +/* + * 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::ops::*; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use super::glam_helpers::{GlamConv, GlamType}; +use super::{inner::InnerProjection, Plane, Transform3D, Vector2, Vector4}; + +use glam; +/// A 4x4 matrix used for 3D projective transformations. It can represent +/// transformations such as translation, rotation, scaling, shearing, and +/// perspective division. It consists of four Vector4 columns. +/// +/// For purely linear transformations (translation, rotation, and scale), it is +/// recommended to use Transform3D, as it is more performant and has a lower +/// memory footprint. +/// +/// Used internally as Camera3D's projection matrix. +/// +/// Note: The current implementation largely makes calls to godot for its +/// methods and as such are not as performant as other types. +#[derive(Copy, Clone, PartialEq, Debug)] +#[repr(C)] +pub struct Projection { + /// The columns of the projection matrix. + pub cols: [Vector4; 4], +} + +impl Projection { + /// A Projection with no transformation defined. When applied to other data + /// structures, no transformation is performed. + /// + /// _Godot equivalent: Projection.IDENTITY_ + pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0, 1.0, 1.0); + + /// A Projection with all values initialized to 0. When applied to other + /// data structures, they will be zeroed. + /// + /// _Godot equivalent: Projection.ZERO_ + pub const ZERO: Self = Self::from_diagonal(0.0, 0.0, 0.0, 0.0); + + /// Create a new projection from a list of column vectors. + pub const fn new(cols: [Vector4; 4]) -> Self { + Self { cols } + } + + /// Create a diagonal matrix from the given values. + pub const fn from_diagonal(x: f32, y: f32, z: f32, w: f32) -> Self { + Self::from_cols( + Vector4::new(x, 0.0, 0.0, 0.0), + Vector4::new(0.0, y, 0.0, 0.0), + Vector4::new(0.0, 0.0, z, 0.0), + Vector4::new(0.0, 0.0, 0.0, w), + ) + } + + /// Create a matrix from four column vectors. + /// + /// _Godot equivalent: Projection(Vector4 x_axis, Vector4 y_axis, Vector4 z_axis, Vector4 w_axis)_ + pub const fn from_cols(x: Vector4, y: Vector4, z: Vector4, w: Vector4) -> Self { + Self { cols: [x, y, z, w] } + } + + /// Creates a new Projection that projects positions from a depth range of + /// -1 to 1 to one that ranges from 0 to 1, and flips the projected + /// positions vertically, according to flip_y. + /// + /// _Godot equivalent: Projection.create_depth_correction()_ + pub fn create_depth_correction(flip_y: bool) -> Self { + InnerProjection::create_depth_correction(flip_y) + } + + /// Creates a new Projection for projecting positions onto a head-mounted + /// display with the given X:Y aspect ratio, distance between eyes, display + /// width, distance to lens, oversampling factor, and depth clipping planes. + /// + /// _Godot equivalent: Projection.create_for_hmd()_ + #[allow(clippy::too_many_arguments)] + pub fn create_for_hmd( + eye: ProjectionEye, + aspect: f64, + intraocular_dist: f64, + display_width: f64, + display_to_lens: f64, + oversample: f64, + near: f64, + far: f64, + ) -> Self { + InnerProjection::create_for_hmd( + eye as i64, + aspect, + intraocular_dist, + display_width, + display_to_lens, + oversample, + near, + far, + ) + } + + /// Creates a new Projection that projects positions in a frustum with the + /// given clipping planes. + /// + /// _Godot equivalent: Projection.create_frustum()_ + pub fn create_frustum( + left: f64, + right: f64, + bottom: f64, + top: f64, + near: f64, + far: f64, + ) -> Self { + InnerProjection::create_frustum(left, right, bottom, top, near, far) + } + + /// Creates a new Projection that projects positions in a frustum with the + /// given size, X:Y aspect ratio, offset, and clipping planes. + /// + /// `flip_fov` determines whether the projection's field of view is flipped + /// over its diagonal. + /// + /// _Godot equivalent: Projection.create_frustum_aspect()_ + pub fn create_frustum_aspect( + size: f64, + aspect: f64, + offset: Vector2, + near: f64, + far: f64, + flip_fov: bool, + ) -> Self { + InnerProjection::create_frustum_aspect(size, aspect, offset, near, far, flip_fov) + } + + /// Creates a new Projection that projects positions using an orthogonal + /// projection with the given clipping planes. + /// + /// _Godot equivalent: Projection.create_orthogonal()_ + pub fn create_orthogonal( + left: f64, + right: f64, + bottom: f64, + top: f64, + near: f64, + far: f64, + ) -> Self { + InnerProjection::create_orthogonal(left, right, bottom, top, near, far) + } + + /// Creates a new Projection that projects positions using an orthogonal + /// projection with the given size, X:Y aspect ratio, and clipping planes. + /// + /// `flip_fov` determines whether the projection's field of view is flipped + /// over its diagonal. + /// + /// _Godot equivalent: Projection.create_orthogonal_aspect()_ + pub fn create_orthogonal_aspect( + size: f64, + aspect: f64, + near: f64, + far: f64, + flip_fov: bool, + ) -> Self { + InnerProjection::create_orthogonal_aspect(size, aspect, near, far, flip_fov) + } + + /// Creates a new Projection that projects positions using a perspective + /// projection with the given Y-axis field of view (in degrees), X:Y aspect + /// ratio, and clipping planes + /// + /// `flip_fov` determines whether the projection's field of view is flipped + /// over its diagonal. + /// + /// _Godot equivalent: Projection.create_perspective()_ + pub fn create_perspective( + fov_y: f64, + aspect: f64, + near: f64, + far: f64, + flip_fov: bool, + ) -> Self { + InnerProjection::create_perspective(fov_y, aspect, near, far, flip_fov) + } + + /// Creates a new Projection that projects positions using a perspective + /// projection with the given Y-axis field of view (in degrees), X:Y aspect + /// ratio, and clipping distances. The projection is adjusted for a + /// head-mounted display with the given distance between eyes and distance + /// to a point that can be focused on. + /// + /// `flip_fov` determines whether the projection's field of view is flipped + /// over its diagonal. + /// + /// _Godot equivalent: Projection.create_perspective_hmd()_ + #[allow(clippy::too_many_arguments)] + pub fn create_perspective_hmd( + fov_y: f64, + aspect: f64, + near: f64, + far: f64, + flip_fov: bool, + eye: ProjectionEye, + intraocular_dist: f64, + convergence_dist: f64, + ) -> Self { + InnerProjection::create_perspective_hmd( + fov_y, + aspect, + near, + far, + flip_fov, + eye as i64, + intraocular_dist, + convergence_dist, + ) + } + + /// Return the determinant of the matrix. + /// + /// _Godot equivalent: Projection.determinant()_ + pub fn determinant(&self) -> f64 { + self.glam(|mat| mat.determinant()) as f64 + } + + /// Returns a copy of this Projection with the signs of the values of the Y + /// column flipped. + /// + /// _Godot equivalent: Projection.flipped_y()_ + pub fn flipped_y(self) -> Self { + let [x, y, z, w] = self.cols; + Self::from_cols(x, -y, z, w) + } + + /// Returns the X:Y aspect ratio of this Projection's viewport. + /// + /// _Godot equivalent: Projection.get_aspect()_ + pub fn aspect(&self) -> f64 { + self.as_inner().get_aspect() + } + + /// Returns the dimensions of the far clipping plane of the projection, + /// divided by two. + /// + /// _Godot equivalent: Projection.get_far_plane_half_extents()_ + pub fn far_plane_half_extents(&self) -> Vector2 { + self.as_inner().get_far_plane_half_extents() + } + + /// Returns the horizontal field of view of the projection (in degrees). + /// + /// _Godot equivalent: Projection.get_fov()_ + pub fn fov(&self) -> f64 { + self.as_inner().get_fov() + } + + /// Returns the vertical field of view of a projection (in degrees) which + /// has the given horizontal field of view (in degrees) and aspect ratio. + /// + /// _Godot equivalent: Projection.get_fovy()_ + pub fn fovy_of(fov_x: f64, aspect: f64) -> f64 { + InnerProjection::get_fovy(fov_x, aspect) + } + + /// Returns the factor by which the visible level of detail is scaled by + /// this Projection. + /// + /// _Godot equivalent: Projection.get_lod_multiplier()_ + pub fn lod_multiplier(&self) -> f64 { + self.as_inner().get_lod_multiplier() + } + + /// Returns the number of pixels with the given pixel width displayed per + /// meter, after this Projection is applied. + /// + /// _Godot equivalent: Projection.get_pixels_per_meter()_ + pub fn pixels_per_meter(&self, pixel_width: i64) -> i64 { + self.as_inner().get_pixels_per_meter(pixel_width) + } + + /// Returns the clipping plane of this Projection whose index is given by + /// plane. + /// + /// _Godot equivalent: Projection.get_projection_plane()_ + pub fn projection_plane(&self, plane: ProjectionPlane) -> Plane { + self.as_inner().get_projection_plane(plane as i64) + } + + /// Returns the dimensions of the viewport plane that this Projection + /// projects positions onto, divided by two. + /// + /// _Godot equivalent: Projection.get_viewport_half_extents()_ + pub fn viewport_half_extents(&self) -> Vector2 { + self.as_inner().get_viewport_half_extents() + } + + /// Returns the distance for this Projection beyond which positions are + /// clipped. + /// + /// _Godot equivalent: Projection.get_z_far()_ + pub fn z_far(&self) -> f64 { + self.as_inner().get_z_far() + } + + /// Returns the distance for this Projection before which positions are + /// clipped. + /// + /// _Godot equivalent: Projection.get_z_near()_ + pub fn z_near(&self) -> f64 { + self.as_inner().get_z_near() + } + + /// Returns a Projection that performs the inverse of this Projection's + /// projective transformation. + /// + /// _Godot equivalent: Projection.inverse()_ + pub fn inverse(self) -> Self { + self.glam(|mat| mat.inverse()) + } + + /// Returns `true` if this Projection performs an orthogonal projection. + /// + /// _Godot equivalent: Projection.is_orthogonal()_ + pub fn is_orthogonal(&self) -> bool { + self.as_inner().is_orthogonal() + } + + /// Returns a Projection with the X and Y values from the given [`Vector2`] + /// added to the first and second values of the final column respectively. + /// + /// _Godot equivalent: Projection.jitter_offseted()_ + #[must_use] + pub fn jitter_offset(&self, offset: Vector2) -> Self { + self.as_inner().jitter_offseted(offset) + } + + /// Returns a Projection with the near clipping distance adjusted to be + /// `new_znear`. + /// + /// Note: The original Projection must be a perspective projection. + /// + /// _Godot equivalent: Projection.perspective_znear_adjusted()_ + pub fn perspective_znear_adjusted(&self, new_znear: f64) -> Self { + self.as_inner().perspective_znear_adjusted(new_znear) + } + + #[doc(hidden)] + pub(crate) fn as_inner(&self) -> InnerProjection { + InnerProjection::from_outer(self) + } +} + +impl From for Projection { + fn from(trans: Transform3D) -> Self { + trans.glam(glam::Mat4::from) + } +} + +impl Default for Projection { + fn default() -> Self { + Self::IDENTITY + } +} + +impl Mul for Projection { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl Mul for Projection { + type Output = Vector4; + + fn mul(self, rhs: Vector4) -> Self::Output { + self.glam2(&rhs, |m, v| m * v) + } +} + +impl GlamType for glam::Mat4 { + type Mapped = Projection; + + fn to_front(&self) -> Self::Mapped { + Projection::from_cols( + self.x_axis.to_front(), + self.y_axis.to_front(), + self.z_axis.to_front(), + self.w_axis.to_front(), + ) + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self::from_cols_array_2d(&mapped.cols.map(|v| v.to_glam().to_array())) + } +} + +impl GlamConv for Projection { + type Glam = glam::Mat4; +} + +impl GodotFfi for Projection { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +/// A projections clipping plane. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(C)] +pub enum ProjectionPlane { + Near = 0, + Far = 1, + Left = 2, + Top = 3, + Right = 4, + Bottom = 5, +} + +/// The eye to create a projection for, when creating a projection adjusted +/// for head-mounted displays. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(C)] +pub enum ProjectionEye { + Left = 1, + Right = 2, +} diff --git a/godot-core/src/builtin/quaternion.rs b/godot-core/src/builtin/quaternion.rs index 0d33d1a16..598a875e2 100644 --- a/godot-core/src/builtin/quaternion.rs +++ b/godot-core/src/builtin/quaternion.rs @@ -11,7 +11,9 @@ use sys::{ffi_methods, GodotFfi}; use crate::builtin::glam_helpers::{GlamConv, GlamType}; use crate::builtin::{inner, math::*, vector3::*}; -#[derive(Copy, Clone, Debug, PartialEq)] +use super::{Basis, EulerOrder}; + +#[derive(Copy, Clone, PartialEq, Debug)] #[repr(C)] pub struct Quaternion { pub x: f32, @@ -96,12 +98,8 @@ impl Quaternion { } } - // TODO: Figure out how godot actually treats "order", then make a match/if chain - pub fn get_euler(self, order: Option) -> Vector3 { - let _o = order.unwrap_or(2); - let vt = self.glam(|quat| quat.to_euler(glam::EulerRot::XYZ)); - - Vector3::new(vt.0, vt.1, vt.2) + pub fn to_euler(self, order: EulerOrder) -> Vector3 { + Basis::from_quat(self).to_euler(order) } pub fn inverse(self) -> Self { diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs new file mode 100644 index 000000000..9161ffea4 --- /dev/null +++ b/godot-core/src/builtin/transform2d.rs @@ -0,0 +1,720 @@ +/* + * 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::{f32::consts::PI, fmt::Display, ops::*}; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use super::glam_helpers::{GlamConv, GlamType}; +use super::{math::*, Vector2}; + +use glam; + +/// Affine 2D transform (2x3 matrix). +/// +/// Represents transformations such as translation, rotation, or scaling. +/// +/// Expressed as a 2x3 matrix, this transform consists of a two column vectors +/// `a` and `b` representing the basis of the transform, as well as the origin: +/// ```text +/// [ a.x b.x origin.x ] +/// [ a.y b.y origin.y ] +/// ``` +/// +/// For methods that don't take translation into account, see [`Basis2D`]. +#[derive(Default, Copy, Clone, PartialEq, Debug)] +#[repr(C)] +pub struct Transform2D { + /// The first basis vector. + /// + /// This is equivalent to the `x` field from godot. + pub a: Vector2, + + /// The second basis vector. + /// + /// This is equivalent to the `y` field from godot. + pub b: Vector2, + + /// The origin of the transform. The coordinate space defined by this transform + /// starts at this point. + /// + /// _Godot equivalent: `Transform2D.origin`_ + pub origin: Vector2, +} + +impl Transform2D { + /// The identity transform, with no translation, rotation or scaling + /// applied. When applied to other data structures, `IDENTITY` performs no + /// transformation. + /// + /// _Godot equivalent: `Transform2D.IDENTITY`_ + pub const IDENTITY: Self = Self::from_basis_origin(Basis2D::IDENTITY, Vector2::ZERO); + + /// The `Transform2D` that will flip something along its x axis. + /// + /// _Godot equivalent: `Transform2D.FLIP_X`_ + pub const FLIP_X: Self = Self::from_basis_origin(Basis2D::FLIP_X, Vector2::ZERO); + + /// The `Transform2D` that will flip something along its y axis. + /// + /// _Godot equivalent: `Transform2D.FLIP_Y`_ + pub const FLIP_Y: Self = Self::from_basis_origin(Basis2D::FLIP_Y, Vector2::ZERO); + + const fn from_basis_origin(basis: Basis2D, origin: Vector2) -> Self { + let [a, b] = basis.cols; + Self { a, b, origin } + } + + /// Create a new `Transform2D` with the given column vectors. + /// + /// _Godot equivalent: `Transform2D(Vector2 x_axis, Vector2 y_axis, Vector2 origin)_ + pub const fn from_cols(a: Vector2, b: Vector2, origin: Vector2) -> Self { + Self { a, b, origin } + } + + /// Create a new `Transform2D` which will rotate by the given angle. + pub fn from_angle(angle: f32) -> Self { + Self::from_angle_origin(angle, Vector2::ZERO) + } + + /// Create a new `Transform2D` which will rotate by `angle` and translate + /// by `origin`. + /// + /// _Godot equivalent: `Transform2D(float rotation, Vector2 position)`_ + pub fn from_angle_origin(angle: f32, origin: Vector2) -> Self { + Self::from_basis_origin(Basis2D::from_angle(angle), origin) + } + + /// Create a new `Transform2D` which will rotate by `angle`, scale by + /// `scale`, skew by `skew` and translate by `origin`. + /// + /// _Godot equivalent: `Transform2D(float rotation, Vector2 scale, float skew, Vector2 position)`_ + pub fn from_angle_scale_skew_origin( + angle: f32, + scale: Vector2, + skew: f32, + origin: Vector2, + ) -> Self { + // Translated from Godot's implementation + + Self::from_basis_origin( + Basis2D::from_cols( + Vector2::new(angle.cos(), angle.sin()), + Vector2::new(-(angle + skew).sin(), (angle + skew).cos()), + ) + .scaled(scale), + origin, + ) + } + + /// Create a reference to the first two columns of the transform + /// interpreted as a [`Basis2D`]. + fn basis<'a>(&'a self) -> &'a Basis2D { + // SAFETY: Both `Basis2D` and `Transform2D` are `repr(C)`, and the + // layout of `Basis2D` is a prefix of `Transform2D` + + unsafe { std::mem::transmute::<&'a Transform2D, &'a Basis2D>(self) } + } + + /// Create a [`Basis2D`] from the first two columns of the transform. + fn to_basis(self) -> Basis2D { + Basis2D::from_cols(self.a, self.b) + } + + /// Returns the inverse of the transform, under the assumption that the + /// transformation is composed of rotation, scaling and translation. + /// + /// _Godot equivalent: `Transform2D.affine_inverse()`_ + #[must_use] + pub fn affine_inverse(self) -> Self { + self.glam(|aff| aff.inverse()) + } + + /// Returns the transform's rotation (in radians). + /// + /// _Godot equivalent: `Transform2D.get_rotation()`_ + pub fn rotation(&self) -> f32 { + self.basis().rotation() + } + + /// Returns the transform's scale. + /// + /// _Godot equivalent: `Transform2D.get_scale()`_ + #[must_use] + pub fn scale(&self) -> Vector2 { + self.basis().scale() + } + + /// Returns the transform's skew (in radians). + /// + /// _Godot equivalent: `Transform2D.get_skew()`_ + #[must_use] + pub fn skew(&self) -> f32 { + self.basis().skew() + } + + /// Returns a transform interpolated between this transform and another by + /// a given `weight` (on the range of 0.0 to 1.0). + /// + /// _Godot equivalent: `Transform2D.interpolate_with()`_ + #[must_use] + pub fn interpolate_with(self, other: Self, weight: f32) -> Self { + Self::from_angle_scale_skew_origin( + lerp_angle(self.rotation(), other.rotation(), weight), + self.scale().lerp(other.scale(), weight), + lerp_angle(self.skew(), other.skew(), weight), + self.origin.lerp(other.origin, weight), + ) + } + + /// Returns `true` if this transform and transform are approximately equal, + /// by calling `is_equal_approx` on each component. + /// + /// _Godot equivalent: `Transform2D.is_equal_approx()`_ + pub fn is_equal_approx(&self, other: &Self) -> bool { + self.a.is_equal_approx(other.a) + && self.b.is_equal_approx(other.b) + && self.origin.is_equal_approx(other.origin) + } + + /// Returns `true` if this transform is finite, by calling + /// [`Vector2::is_finite()`] on each component. + /// + /// _Godot equivalent: `Transform2D.is_finite()`_ + pub fn is_finite(&self) -> bool { + self.a.is_finite() && self.b.is_finite() && self.origin.is_finite() + } + + /// Returns the transform with the basis orthogonal (90 degrees), and + /// normalized axis vectors (scale of 1 or -1). + /// + /// _Godot equivalent: `Transform2D.orthonormalized()`_ + #[must_use] + pub fn orthonormalized(self) -> Self { + Self::from_basis_origin(self.basis().orthonormalized(), self.origin) + } + + /// Returns a copy of the transform rotated by the given `angle` (in radians). + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding rotation transform `R` from the left, i.e., `R * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.rotated()`_ + #[must_use] + pub fn rotated(self, angle: f32) -> Self { + Self::from_angle(angle) * self + } + + /// Returns a copy of the transform rotated by the given `angle` (in radians). + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding rotation transform `R` from the right, i.e., `X * R`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.rotated_local()`_ + #[must_use] + pub fn rotated_local(self, angle: f32) -> Self { + self * Self::from_angle(angle) + } + + /// Returns a copy of the transform scaled by the given scale factor. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding scaling transform `S` from the left, i.e., `S * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.scaled()`_ + #[must_use] + pub fn scaled(self, scale: Vector2) -> Self { + let mut basis = self.to_basis(); + basis.set_row_a(basis.row_a() * scale.x); + basis.set_row_b(basis.row_b() * scale.y); + Self::from_basis_origin(basis, self.origin * scale) + } + + /// Returns a copy of the transform scaled by the given scale factor. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding scaling transform `S` from the right, i.e., `X * S`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.scaled_local()`_ + #[must_use] + pub fn scaled_local(self, scale: Vector2) -> Self { + Self::from_basis_origin(self.basis().scaled(scale), self.origin) + } + + /// Returns a copy of the transform translated by the given offset. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding translation transform `T` from the left, i.e., `T * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.translated()`_ + #[must_use] + pub fn translated(self, offset: Vector2) -> Self { + Self::from_cols(self.a, self.b, self.origin + offset) + } + + /// Returns a copy of the transform translated by the given offset. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding translation transform `T` from the right, i.e., `X * T`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.translated()`_ + #[must_use] + pub fn translated_local(self, offset: Vector2) -> Self { + Self::from_cols(self.a, self.b, self.origin + (self.to_basis() * offset)) + } + + /// Returns a vector transformed (multiplied) by the basis matrix. + /// This method does not account for translation (the origin vector). + /// + /// _Godot equivalent: `Transform2D.basis_xform()`_ + pub fn basis_xform(&self, v: Vector2) -> Vector2 { + self.to_basis() * v + } + + /// Returns a vector transformed (multiplied) by the inverse basis matrix. + /// This method does not account for translation (the origin vector). + /// + /// _Godot equivalent: `Transform2D.basis_xform_inv()`_ + pub fn basis_xform_inv(&self, v: Vector2) -> Vector2 { + self.basis().inverse() * v + } +} + +impl Display for Transform2D { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Godot output: + // [X: (1, 2), Y: (3, 4), O: (5, 6)] + // Where X,Y,O are the columns + + let Transform2D { a, b, origin } = self; + + write!(f, "[a: {a}, b: {b}, o: {origin}]") + } +} + +impl Mul for Transform2D { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl Mul for Transform2D { + type Output = Vector2; + + fn mul(self, rhs: Vector2) -> Self::Output { + self.glam2(&rhs, |t, v| t.transform_point2(v)) + } +} + +impl Mul for Transform2D { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self::from_cols(self.a * rhs, self.b * rhs, self.origin * rhs) + } +} + +impl GlamType for glam::Affine2 { + type Mapped = Transform2D; + + fn to_front(&self) -> Self::Mapped { + Transform2D::from_basis_origin(self.matrix2.to_front(), self.translation.to_front()) + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self { + matrix2: mapped.basis().to_glam(), + translation: mapped.origin.to_glam(), + } + } +} + +impl GlamConv for Transform2D { + type Glam = glam::Affine2; +} + +impl GodotFfi for Transform2D { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +/// A 2x2 matrix, typically used as an orthogonal basis for [`Transform2D`]. +/// +/// Indexing into a `Basis2D` is done in a column-major order, meaning that +/// `basis[0]` is the first basis-vector. +/// +/// This has no direct equivalent in Godot, but is the same as the `x` and `y` +/// vectors from a `Transform2D`. +#[derive(Copy, Clone, PartialEq, Debug)] +#[repr(C)] +pub(crate) struct Basis2D { + /// The columns of the matrix. + cols: [Vector2; 2], +} + +impl Basis2D { + /// The identity basis, with no rotation or scaling applied. + pub(crate) const IDENTITY: Self = Self::from_diagonal(1.0, 1.0); + + /// The basis that will flip something along the X axis when used in a + /// transformation. + pub(crate) const FLIP_X: Self = Self::from_diagonal(-1.0, 1.0); + + /// The basis that will flip something along the X axis when used in a + /// transformation. + pub(crate) const FLIP_Y: Self = Self::from_diagonal(1.0, -1.0); + + /// Create a diagonal matrix from the given values. + pub(crate) const fn from_diagonal(x: f32, y: f32) -> Self { + Self::from_cols(Vector2::new(x, 0.0), Vector2::new(0.0, y)) + } + + /// Create a new basis from 2 basis vectors. + pub(crate) const fn from_cols(x: Vector2, y: Vector2) -> Self { + Self { cols: [x, y] } + } + + /// Create a `Basis2D` from an angle. + pub(crate) fn from_angle(angle: f32) -> Self { + glam::Mat2::from_angle(angle).to_front() + } + + /// Returns the scale of the matrix. + #[must_use] + pub(crate) fn scale(&self) -> Vector2 { + let det_sign = self.determinant().signum(); + Vector2::new(self.cols[0].length(), det_sign * self.cols[1].length()) + } + + /// Introduces an additional scaling. + #[must_use] + pub(crate) fn scaled(self, scale: Vector2) -> Self { + Self { + cols: [self.cols[0] * scale.x, self.cols[1] * scale.y], + } + } + + /// Returns the determinant of the matrix. + pub(crate) fn determinant(&self) -> f32 { + self.glam(|mat| mat.determinant()) + } + + /// Returns the inverse of the matrix. + #[must_use] + pub fn inverse(self) -> Self { + self.glam(|mat| mat.inverse()) + } + + /// Returns the orthonormalized version of the basis. + #[must_use] + pub(crate) fn orthonormalized(self) -> Self { + assert!( + !is_equal_approx(self.determinant(), 0.0), + "Determinant should not be zero." + ); + + // Gram-Schmidt Process + let mut x = self.cols[0]; + let mut y = self.cols[1]; + + x = x.normalized(); + y = y - x * (x.dot(y)); + y = y.normalized(); + + Self::from_cols(x, y) + } + + /// Returns the rotation of the matrix + pub(crate) fn rotation(&self) -> f32 { + // Translated from Godot + f32::atan2(self.cols[0].y, self.cols[0].x) + } + + /// Returns the skew of the matrix + #[must_use] + pub(crate) fn skew(&self) -> f32 { + // Translated from Godot + let det_sign = self.determinant().signum(); + self.cols[0] + .normalized() + .dot(det_sign * self.cols[1].normalized()) + .acos() + - PI * 0.5 + } + + pub(crate) fn set_row_a(&mut self, v: Vector2) { + self.cols[0].x = v.x; + self.cols[1].x = v.y; + } + + pub(crate) fn row_a(&self) -> Vector2 { + Vector2::new(self.cols[0].x, self.cols[1].x) + } + + pub(crate) fn set_row_b(&mut self, v: Vector2) { + self.cols[0].y = v.x; + self.cols[1].y = v.y; + } + + pub(crate) fn row_b(&self) -> Vector2 { + Vector2::new(self.cols[0].y, self.cols[1].y) + } +} + +impl Default for Basis2D { + fn default() -> Self { + Self::IDENTITY + } +} + +impl Display for Basis2D { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let [a, b] = self.cols; + + write!(f, "[a: {a}, b: {b})]") + } +} + +impl Mul for Basis2D { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl Mul for Basis2D { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + (self.to_glam() * rhs).to_front() + } +} + +impl MulAssign for Basis2D { + fn mul_assign(&mut self, rhs: f32) { + self.cols[0] *= rhs; + self.cols[1] *= rhs; + } +} + +impl Mul for Basis2D { + type Output = Vector2; + + fn mul(self, rhs: Vector2) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl GlamType for glam::Mat2 { + type Mapped = Basis2D; + + fn to_front(&self) -> Self::Mapped { + Basis2D { + cols: [self.col(0).to_front(), self.col(1).to_front()], + } + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self::from_cols(mapped.cols[0].to_glam(), mapped.cols[1].to_glam()) + } +} + +impl GlamConv for Basis2D { + type Glam = glam::Mat2; +} + +#[cfg(test)] +mod test { + use crate::assert_eq_approx; + + use super::*; + + #[test] + fn transform2d_constructors_correct() { + let trans = Transform2D::from_angle(115.0f32.to_radians()); + assert_eq_approx!(trans.rotation(), 115.0f32.to_radians(), is_equal_approx); + + let trans = Transform2D::from_angle_origin((-80.0f32).to_radians(), Vector2::new(1.4, 9.8)); + assert_eq_approx!(trans.rotation(), (-80.0f32).to_radians(), is_equal_approx); + assert_eq_approx!( + trans.origin, + Vector2::new(1.4, 9.8), + Vector2::is_equal_approx + ); + + let trans = Transform2D::from_angle_scale_skew_origin( + 170.0f32.to_radians(), + Vector2::new(3.6, 8.0), + 20.0f32.to_radians(), + Vector2::new(2.4, 6.8), + ); + assert_eq_approx!(trans.rotation(), 170.0f32.to_radians(), is_equal_approx); + assert_eq_approx!( + trans.scale(), + Vector2::new(3.6, 8.0), + Vector2::is_equal_approx + ); + assert_eq_approx!(trans.skew(), 20.0f32.to_radians(), is_equal_approx); + assert_eq_approx!( + trans.origin, + Vector2::new(2.4, 6.8), + Vector2::is_equal_approx + ); + } + + // Tests translated from Godot. + + const DUMMY_TRANSFORM: Transform2D = Transform2D::from_basis_origin( + Basis2D::from_cols(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0)), + Vector2::new(5.0, 6.0), + ); + + #[test] + fn translation() { + let offset = Vector2::new(1.0, 2.0); + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform2D::IDENTITY.translated(offset), + Transform2D::IDENTITY.translated_local(offset) + ); + + // Check both versions against left and right multiplications. + let t = Transform2D::IDENTITY.translated(offset); + assert_eq!(DUMMY_TRANSFORM.translated(offset), t * DUMMY_TRANSFORM); + assert_eq!( + DUMMY_TRANSFORM.translated_local(offset), + DUMMY_TRANSFORM * t + ); + } + + #[test] + fn scaling() { + let scaling = Vector2::new(1.0, 2.0); + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform2D::IDENTITY.scaled(scaling), + Transform2D::IDENTITY.scaled_local(scaling) + ); + + // Check both versions against left and right multiplications. + let s: Transform2D = Transform2D::IDENTITY.scaled(scaling); + assert_eq!(DUMMY_TRANSFORM.scaled(scaling), s * DUMMY_TRANSFORM); + assert_eq!(DUMMY_TRANSFORM.scaled_local(scaling), DUMMY_TRANSFORM * s); + } + + #[test] + fn rotation() { + let phi = 1.0; + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform2D::IDENTITY.rotated(phi), + Transform2D::IDENTITY.rotated_local(phi) + ); + + // Check both versions against left and right multiplications. + let r: Transform2D = Transform2D::IDENTITY.rotated(phi); + assert_eq!(DUMMY_TRANSFORM.rotated(phi), r * DUMMY_TRANSFORM); + assert_eq!(DUMMY_TRANSFORM.rotated_local(phi), DUMMY_TRANSFORM * r); + } + + #[test] + fn interpolation() { + let rotate_scale_skew_pos: Transform2D = Transform2D::from_angle_scale_skew_origin( + 170.0f32.to_radians(), + Vector2::new(3.6, 8.0), + 20.0f32.to_radians(), + Vector2::new(2.4, 6.8), + ); + + let rotate_scale_skew_pos_halfway: Transform2D = Transform2D::from_angle_scale_skew_origin( + 85.0f32.to_radians(), + Vector2::new(2.3, 4.5), + 10.0f32.to_radians(), + Vector2::new(1.2, 3.4), + ); + let interpolated: Transform2D = + Transform2D::IDENTITY.interpolate_with(rotate_scale_skew_pos, 0.5); + assert_eq_approx!( + interpolated.origin, + rotate_scale_skew_pos_halfway.origin, + Vector2::is_equal_approx + ); + assert_eq_approx!( + interpolated.rotation(), + rotate_scale_skew_pos_halfway.rotation(), + is_equal_approx + ); + assert_eq_approx!( + interpolated.scale(), + rotate_scale_skew_pos_halfway.scale(), + Vector2::is_equal_approx + ); + assert_eq_approx!( + interpolated.skew(), + rotate_scale_skew_pos_halfway.skew(), + is_equal_approx + ); + assert_eq_approx!( + &interpolated, + &rotate_scale_skew_pos_halfway, + Transform2D::is_equal_approx + ); + let interpolated = rotate_scale_skew_pos.interpolate_with(Transform2D::IDENTITY, 0.5); + assert_eq_approx!( + &interpolated, + &rotate_scale_skew_pos_halfway, + Transform2D::is_equal_approx + ); + } + + #[test] + fn finite_number_checks() { + let x: Vector2 = Vector2::new(0.0, 1.0); + let infinite: Vector2 = Vector2::new(f32::NAN, f32::NAN); + + assert!( + Transform2D::from_basis_origin(Basis2D::from_cols(x, x), x).is_finite(), + "let with: Transform2D all components finite should be finite", + ); + + assert!( + !Transform2D::from_basis_origin(Basis2D::from_cols(infinite, x), x).is_finite(), + "let with: Transform2D one component infinite should not be finite.", + ); + assert!( + !Transform2D::from_basis_origin(Basis2D::from_cols(x, infinite), x).is_finite(), + "let with: Transform2D one component infinite should not be finite.", + ); + assert!( + !Transform2D::from_basis_origin(Basis2D::from_cols(x, x), infinite).is_finite(), + "let with: Transform2D one component infinite should not be finite.", + ); + + assert!( + !Transform2D::from_basis_origin(Basis2D::from_cols(infinite, infinite), x).is_finite(), + "let with: Transform2D two components infinite should not be finite.", + ); + assert!( + !Transform2D::from_basis_origin(Basis2D::from_cols(infinite, x), infinite).is_finite(), + "let with: Transform2D two components infinite should not be finite.", + ); + assert!( + !Transform2D::from_basis_origin(Basis2D::from_cols(x, infinite), infinite).is_finite(), + "let with: Transform2D two components infinite should not be finite.", + ); + + assert!( + !Transform2D::from_basis_origin(Basis2D::from_cols(infinite, infinite), infinite) + .is_finite(), + "let with: Transform2D three components infinite should not be finite.", + ); + } +} diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs new file mode 100644 index 000000000..660813e31 --- /dev/null +++ b/godot-core/src/builtin/transform3d.rs @@ -0,0 +1,418 @@ +/* + * 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::Display, ops::*}; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use super::glam_helpers::{GlamConv, GlamType}; +use super::{Basis, Projection, Vector3}; + +use glam; + +/// Affine 3D transform (3x4 matrix). +/// +/// Used for 3D linear transformations. Uses a basis + origin representation. +/// +/// Expressed as a 3x4 matrix, this transform consists of 3 basis (column) +/// vectors `a`, `b`, `c` as well as an origin `o`: +/// ```text +/// [ a.x b.x c.x o.x ] +/// [ a.y b.y c.y o.y ] +/// [ a.z b.z c.z o.z ] +/// ``` +#[derive(Default, Copy, Clone, PartialEq, Debug)] +#[repr(C)] +pub struct Transform3D { + /// The basis is a matrix containing 3 vectors as its columns. They can be + /// interpreted as the basis vectors of the transformed coordinate system. + pub basis: Basis, + + /// The new origin of the transformed coordinate system. + pub origin: Vector3, +} + +impl Transform3D { + /// The identity transform, with no translation, rotation or scaling + /// applied. When applied to other data structures, `IDENTITY` performs no + /// transformation. + /// + /// _Godot equivalent: `Transform3D.IDENTITY`_ + pub const IDENTITY: Self = Self::new(Basis::IDENTITY, Vector3::ZERO); + + /// `Transform3D` with mirroring applied perpendicular to the YZ plane. + /// + /// _Godot equivalent: `Transform3D.FLIP_X`_ + pub const FLIP_X: Self = Self::new(Basis::FLIP_X, Vector3::ZERO); + + /// `Transform3D` with mirroring applied perpendicular to the XZ plane. + /// + /// _Godot equivalent: `Transform3D.FLIP_Y`_ + pub const FLIP_Y: Self = Self::new(Basis::FLIP_Y, Vector3::ZERO); + + /// `Transform3D` with mirroring applied perpendicular to the XY plane. + /// + /// _Godot equivalent: `Transform3D.FLIP_Z`_ + pub const FLIP_Z: Self = Self::new(Basis::FLIP_Z, Vector3::ZERO); + + /// Create a new transform from a [`Basis`] and a [`Vector3`]. + /// + /// _Godot equivalent: Transform3D(Basis basis, Vector3 origin)_ + pub const fn new(basis: Basis, origin: Vector3) -> Self { + Self { basis, origin } + } + + /// Create a new transform from 4 matrix-columns. + /// + /// _Godot equivalent: Transform3D(Vector3 x_axis, Vector3 y_axis, Vector3 z_axis, Vector3 origin)_ + pub const fn from_cols(a: Vector3, b: Vector3, c: Vector3, origin: Vector3) -> Self { + Self { + basis: Basis::from_cols(a, b, c), + origin, + } + } + + /// Constructs a Transform3d from a Projection by trimming the last row of + /// the projection matrix. + /// + /// _Godot equivalent: Transform3D(Projection from)_ + pub fn from_projection(proj: Projection) -> Self { + let a = Vector3::new(proj.cols[0].x, proj.cols[0].y, proj.cols[0].z); + let b = Vector3::new(proj.cols[1].x, proj.cols[1].y, proj.cols[1].z); + let c = Vector3::new(proj.cols[2].x, proj.cols[2].y, proj.cols[2].z); + let o = Vector3::new(proj.cols[3].x, proj.cols[3].y, proj.cols[3].z); + + Self { + basis: Basis::from_cols(a, b, c), + origin: o, + } + } + + /// Returns the inverse of the transform, under the assumption that the + /// transformation is composed of rotation, scaling and translation. + /// + /// _Godot equivalent: Transform3D.affine_inverse()_ + #[must_use] + pub fn affine_inverse(self) -> Self { + self.glam(|aff| aff.inverse()) + } + + /// Returns a transform interpolated between this transform and another by + /// a given weight (on the range of 0.0 to 1.0). + /// + /// _Godot equivalent: Transform3D.interpolate_with()_ + #[must_use] + pub fn interpolate_with(self, other: Self, weight: f32) -> Self { + let src_scale = self.basis.scale(); + let src_rot = self.basis.to_quat().normalized(); + let src_loc = self.origin; + + let dst_scale = other.basis.scale(); + let dst_rot = other.basis.to_quat().normalized(); + let dst_loc = other.origin; + + let mut basis = Basis::from_scale(src_scale.lerp(dst_scale, weight)); + basis = Basis::from_quat(src_rot.slerp(dst_rot, weight)) * basis; + + Self { + basis, + origin: src_loc.lerp(dst_loc, weight), + } + } + + /// Returns `true if this transform and transform are approximately equal, by + /// calling is_equal_approx each basis and origin. + /// + /// _Godot equivalent: Transform3D.is_equal_approx()_ + pub fn is_equal_approx(&self, other: &Self) -> bool { + self.basis.is_equal_approx(&other.basis) && self.origin.is_equal_approx(other.origin) + } + + /// Returns `true if this transform is finite by calling `is_finite` on the + /// basis and origin. + /// + /// _Godot equivalent: Transform3D.is_finite()_ + pub fn is_finite(&self) -> bool { + self.basis.is_finite() && self.origin.is_finite() + } + + /// Returns a copy of the transform rotated such that the forward axis (-Z) + /// points towards the `target` position. + /// + /// See [`Basis::new_looking_at()`] for more information. + /// + /// _Godot equivalent: Transform3D.looking_at()_ + #[must_use] + pub fn looking_at(self, target: Vector3, up: Vector3) -> Self { + Self { + basis: Basis::new_looking_at(target - self.origin, up), + origin: self.origin, + } + } + + /// Returns the transform with the basis orthogonal (90 degrees), and + /// normalized axis vectors (scale of 1 or -1). + /// + /// _Godot equivalent: Transform3D.orthonormalized()_ + #[must_use] + pub fn orthonormalized(self) -> Self { + Self { + basis: self.basis.orthonormalized(), + origin: self.origin, + } + } + + /// Returns a copy of the transform rotated by the given `angle` (in radians). + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding rotation transform `R` from the left, i.e., `R * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.rotated()`_ + #[must_use] + pub fn rotated(self, axis: Vector3, angle: f32) -> Self { + let rotation = Basis::from_axis_angle(axis, angle); + Self { + basis: rotation * self.basis, + origin: rotation * self.origin, + } + } + /// Returns a copy of the transform rotated by the given `angle` (in radians). + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding rotation transform `R` from the right, i.e., `X * R`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.rotated_local()`_ + #[must_use] + pub fn rotated_local(self, axis: Vector3, angle: f32) -> Self { + Self { + basis: self.basis * Basis::from_axis_angle(axis, angle), + origin: self.origin, + } + } + + /// Returns a copy of the transform scaled by the given scale factor. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding scaling transform `S` from the left, i.e., `S * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.scaled()`_ + #[must_use] + pub fn scaled(self, scale: Vector3) -> Self { + Self { + basis: Basis::from_scale(scale) * self.basis, + origin: self.origin * scale, + } + } + + /// Returns a copy of the transform scaled by the given scale factor. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding scaling transform `S` from the right, i.e., `X * S`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.scaled_local()`_ + #[must_use] + pub fn scaled_local(self, scale: Vector3) -> Self { + Self { + basis: self.basis * Basis::from_scale(scale), + origin: self.origin, + } + } + + /// Returns a copy of the transform translated by the given offset. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding translation transform `T` from the left, i.e., `T * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.translated()`_ + #[must_use] + pub fn translated(self, offset: Vector3) -> Self { + Self { + basis: self.basis, + origin: self.origin + offset, + } + } + + /// Returns a copy of the transform translated by the given offset. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding translation transform `T` from the right, i.e., `X * T`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.translated()`_ + #[must_use] + pub fn translated_local(self, offset: Vector3) -> Self { + Self { + basis: self.basis, + origin: self.origin + (self.basis * offset), + } + } +} + +impl Display for Transform3D { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Godot output: + // [X: (1, 2, 3), Y: (4, 5, 6), Z: (7, 8, 9), O: (10, 11, 12)] + // Where X,Y,O are the columns + let [a, b, c] = self.basis.to_cols(); + let o = self.origin; + + write!(f, "[a: {a}, b: {b}, c: {c}, o: {o}]") + } +} + +impl From for Transform3D { + /// Create a new transform with origin `(0,0,0)` from this basis. + fn from(basis: Basis) -> Self { + Self::new(basis, Vector3::ZERO) + } +} + +impl Mul for Transform3D { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl Mul for Transform3D { + type Output = Vector3; + + fn mul(self, rhs: Vector3) -> Self::Output { + self.glam2(&rhs, |t, v| t.transform_point3(v)) + } +} + +impl Mul for Transform3D { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + basis: self.basis * rhs, + origin: self.origin * rhs, + } + } +} + +impl GlamType for glam::Affine3A { + type Mapped = Transform3D; + + fn to_front(&self) -> Self::Mapped { + Transform3D::new(self.matrix3.to_front(), self.translation.to_front()) + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self { + matrix3: mapped.basis.to_glam().into(), + translation: mapped.origin.to_glam().into(), + } + } +} + +impl GlamConv for Transform3D { + type Glam = glam::Affine3A; +} + +impl GodotFfi for Transform3D { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +#[cfg(test)] +mod test { + use super::*; + + // Tests translated from Godot. + + const DUMMY_TRANSFORM: Transform3D = Transform3D::new( + Basis::from_cols( + Vector3::new(1.0, 2.0, 3.0), + Vector3::new(4.0, 5.0, 6.0), + Vector3::new(7.0, 8.0, 9.0), + ), + Vector3::new(10.0, 11.0, 12.0), + ); + + #[test] + fn translation() { + let offset = Vector3::new(1.0, 2.0, 3.0); + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform3D::IDENTITY.translated(offset), + Transform3D::IDENTITY.translated_local(offset) + ); + + // Check both versions against left and right multiplications. + let t = Transform3D::IDENTITY.translated(offset); + assert_eq!(DUMMY_TRANSFORM.translated(offset), t * DUMMY_TRANSFORM); + assert_eq!( + DUMMY_TRANSFORM.translated_local(offset), + DUMMY_TRANSFORM * t + ); + } + + #[test] + fn scaling() { + let scaling = Vector3::new(1.0, 2.0, 3.0); + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform3D::IDENTITY.scaled(scaling), + Transform3D::IDENTITY.scaled_local(scaling) + ); + + // Check both versions against left and right multiplications. + let s = Transform3D::IDENTITY.scaled(scaling); + assert_eq!(DUMMY_TRANSFORM.scaled(scaling), s * DUMMY_TRANSFORM); + assert_eq!(DUMMY_TRANSFORM.scaled_local(scaling), DUMMY_TRANSFORM * s); + } + + #[test] + fn rotation() { + let axis = Vector3::new(1.0, 2.0, 3.0).normalized(); + let phi: f32 = 1.0; + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform3D::IDENTITY.rotated(axis, phi), + Transform3D::IDENTITY.rotated_local(axis, phi) + ); + + // Check both versions against left and right multiplications. + let r = Transform3D::IDENTITY.rotated(axis, phi); + assert_eq!(DUMMY_TRANSFORM.rotated(axis, phi), r * DUMMY_TRANSFORM); + assert_eq!( + DUMMY_TRANSFORM.rotated_local(axis, phi), + DUMMY_TRANSFORM * r + ); + } + + #[test] + fn finite_number_checks() { + let y = Vector3::new(0.0, 1.0, 2.0); + let infinite_vec = Vector3::new(f32::NAN, f32::NAN, f32::NAN); + let x = Basis::from_rows(y, y, y); + let infinite_basis = Basis::from_rows(infinite_vec, infinite_vec, infinite_vec); + + assert!( + Transform3D::new(x, y).is_finite(), + "Transform3D with all components finite should be finite", + ); + + assert!( + !Transform3D::new(x, infinite_vec).is_finite(), + "Transform3D with one component infinite should not be finite.", + ); + assert!( + !Transform3D::new(infinite_basis, y).is_finite(), + "Transform3D with one component infinite should not be finite.", + ); + + assert!( + !Transform3D::new(infinite_basis, infinite_vec).is_finite(), + "Transform3D with two components infinite should not be finite.", + ); + } +} diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index 53006ce83..7521c9a7c 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -143,6 +143,7 @@ mod impls { use super::*; impl_variant_traits!(bool, bool_to_variant, bool_from_variant, Bool); + impl_variant_traits!(Basis, basis_to_variant, basis_from_variant, Basis); impl_variant_traits!(Vector2, vector2_to_variant, vector2_from_variant, Vector2); impl_variant_traits!(Vector3, vector3_to_variant, vector3_from_variant, Vector3); impl_variant_traits!(Vector4, vector4_to_variant, vector4_from_variant, Vector4); @@ -158,10 +159,6 @@ mod impls { impl_variant_metadata!(Plane, /* plane_to_variant, plane_from_variant, */ Plane); impl_variant_metadata!(Quaternion, /* quaternion_to_variant, quaternion_from_variant, */ Quaternion); impl_variant_metadata!(Aabb, /* aabb_to_variant, aabb_from_variant, */ Aabb); - impl_variant_metadata!(Basis, /* basis_to_variant, basis_from_variant, */ Basis); - impl_variant_metadata!(Transform2D, /* transform_2d_to_variant, transform_2d_from_variant, */ Transform2D); - impl_variant_metadata!(Transform3D, /* transform_3d_to_variant, transform_3d_from_variant, */ Transform3D); - impl_variant_metadata!(Projection, /* projection_to_variant, projection_from_variant, */ Projection); impl_variant_metadata!(Rid, /* rid_to_variant, rid_from_variant, */ Rid); impl_variant_metadata!(Callable, /* callable_to_variant, callable_from_variant, */ Callable); impl_variant_metadata!(Signal, /* signal_to_variant, signal_from_variant, */ Signal); @@ -174,6 +171,9 @@ mod impls { impl_variant_traits!(PackedVector2Array, packed_vector2_array_to_variant, packed_vector2_array_from_variant, PackedVector2Array); impl_variant_traits!(PackedVector3Array, packed_vector3_array_to_variant, packed_vector3_array_from_variant, PackedVector3Array); impl_variant_traits!(PackedColorArray, packed_color_array_to_variant, packed_color_array_from_variant, PackedColorArray); + impl_variant_traits!(Projection, projection_to_variant, projection_from_variant, Projection); + impl_variant_traits!(Transform2D, transform_2d_to_variant, transform_2d_from_variant, Transform2D); + impl_variant_traits!(Transform3D, transform_3d_to_variant, transform_3d_from_variant, Transform3D); impl_variant_traits!(Dictionary, dictionary_to_variant, dictionary_from_variant, Dictionary); impl_variant_traits!(i64, int_to_variant, int_from_variant, Int, GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT64); diff --git a/itest/rust/src/basis_test.rs b/itest/rust/src/basis_test.rs new file mode 100644 index 000000000..7de930843 --- /dev/null +++ b/itest/rust/src/basis_test.rs @@ -0,0 +1,185 @@ +/* + * 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 crate::itest; +use godot::prelude::{inner::InnerBasis, *}; +use godot::private::class_macros::assert_eq_approx; + +const TEST_BASIS: Basis = Basis::from_rows( + Vector3::new(0.942155, -0.270682, 0.197677), + Vector3::new(0.294044, 0.950564, -0.099833), + Vector3::new(-0.160881, 0.152184, 0.97517), +); + +pub(crate) fn run() -> bool { + let mut ok = true; + ok &= basis_multiply_same(); + ok &= basis_euler_angles_same(); + + ok +} + +#[itest] +fn basis_multiply_same() { + let rust_res = TEST_BASIS * Basis::IDENTITY; + let godot_res = TEST_BASIS + .to_variant() + .evaluate(&Basis::IDENTITY.to_variant(), VariantOperator::Multiply) + .unwrap() + .to::(); + assert_eq_approx!(rust_res, godot_res, |a, b| Basis::is_equal_approx(&a, &b)); + + let rhs = Basis::from_axis_angle(Vector3::new(1.0, 2.0, 3.0).normalized(), 0.5); + let rust_res = TEST_BASIS * rhs; + let godot_res = TEST_BASIS + .to_variant() + .evaluate(&rhs.to_variant(), VariantOperator::Multiply) + .unwrap() + .to::(); + assert_eq_approx!(rust_res, godot_res, |a, b| Basis::is_equal_approx(&a, &b)); +} + +#[itest] +fn basis_euler_angles_same() { + let euler_order_to_test: Vec = vec![ + EulerOrder::XYZ, + EulerOrder::XZY, + EulerOrder::YZX, + EulerOrder::YXZ, + EulerOrder::ZXY, + EulerOrder::ZYX, + ]; + + let vectors_to_test: Vec = vec![ + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(0.5, 0.5, 0.5), + Vector3::new(-0.5, -0.5, -0.5), + Vector3::new(40.0, 40.0, 40.0), + Vector3::new(-40.0, -40.0, -40.0), + Vector3::new(0.0, 0.0, -90.0), + Vector3::new(0.0, -90.0, 0.0), + Vector3::new(-90.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, 90.0), + Vector3::new(0.0, 90.0, 0.0), + Vector3::new(90.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, -30.0), + Vector3::new(0.0, -30.0, 0.0), + Vector3::new(-30.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, 30.0), + Vector3::new(0.0, 30.0, 0.0), + Vector3::new(30.0, 0.0, 0.0), + Vector3::new(0.5, 50.0, 20.0), + Vector3::new(-0.5, -50.0, -20.0), + Vector3::new(0.5, 0.0, 90.0), + Vector3::new(0.5, 0.0, -90.0), + Vector3::new(360.0, 360.0, 360.0), + Vector3::new(-360.0, -360.0, -360.0), + Vector3::new(-90.0, 60.0, -90.0), + Vector3::new(90.0, 60.0, -90.0), + Vector3::new(90.0, -60.0, -90.0), + Vector3::new(-90.0, -60.0, -90.0), + Vector3::new(-90.0, 60.0, 90.0), + Vector3::new(90.0, 60.0, 90.0), + Vector3::new(90.0, -60.0, 90.0), + Vector3::new(-90.0, -60.0, 90.0), + Vector3::new(60.0, 90.0, -40.0), + Vector3::new(60.0, -90.0, -40.0), + Vector3::new(-60.0, -90.0, -40.0), + Vector3::new(-60.0, 90.0, 40.0), + Vector3::new(60.0, 90.0, 40.0), + Vector3::new(60.0, -90.0, 40.0), + Vector3::new(-60.0, -90.0, 40.0), + Vector3::new(-90.0, 90.0, -90.0), + Vector3::new(90.0, 90.0, -90.0), + Vector3::new(90.0, -90.0, -90.0), + Vector3::new(-90.0, -90.0, -90.0), + Vector3::new(-90.0, 90.0, 90.0), + Vector3::new(90.0, 90.0, 90.0), + Vector3::new(90.0, -90.0, 90.0), + Vector3::new(20.0, 150.0, 30.0), + Vector3::new(20.0, -150.0, 30.0), + Vector3::new(-120.0, -150.0, 30.0), + Vector3::new(-120.0, -150.0, -130.0), + Vector3::new(120.0, -150.0, -130.0), + Vector3::new(120.0, 150.0, -130.0), + Vector3::new(120.0, 150.0, 130.0), + ]; + + for order in euler_order_to_test.into_iter() { + for vector in vectors_to_test.iter() { + let vector = deg_to_rad(*vector); + let rust_basis = Basis::from_euler(order, vector); + let godot_basis = InnerBasis::from_euler(vector, order as i64); + assert!( + (rust_basis).is_equal_approx(&godot_basis), + "got = {rust_basis}, expected = {godot_basis}" + ); + } + } +} + +#[itest] +fn basis_equiv() { + let inner = InnerBasis::from_outer(&TEST_BASIS); + let outer = TEST_BASIS; + let vec = Vector3::new(1.0, 2.0, 3.0); + + #[rustfmt::skip] + let mappings_basis = [ + ("inverse", inner.inverse(), outer.inverse() ), + ("transposed", inner.transposed(), outer.transposed() ), + ("orthonormalized", inner.orthonormalized(), outer.orthonormalized() ), + ("rotated", inner.rotated(vec.normalized(), 0.1), outer.rotated(vec.normalized(), 0.1)), + ("scaled", inner.scaled(vec), outer.scaled(vec) ), + ("slerp", inner.slerp(Basis::IDENTITY, 0.5), outer.slerp(Basis::IDENTITY, 0.5) ), + ]; + for (name, inner, outer) in mappings_basis { + assert_eq_approx!(&inner, &outer, Basis::is_equal_approx, "function: {name}\n"); + } + + #[rustfmt::skip] + let mappings_float = [ + ("determinant", inner.determinant(), outer.determinant()), + ("tdotx", inner.tdotx(vec), outer.tdotx(vec) ), + ("tdoty", inner.tdoty(vec), outer.tdoty(vec) ), + ("tdotz", inner.tdotz(vec), outer.tdotz(vec) ), + ]; + for (name, inner, outer) in mappings_float { + assert_eq_approx!( + inner, + outer, + |a, b| is_equal_approx(a as f32, b), + "function: {name}\n" + ); + } + + assert_eq_approx!( + inner.get_scale(), + outer.scale(), + Vector3::is_equal_approx, + "function: get_scale\n" + ); + assert_eq_approx!( + inner.get_euler(EulerOrder::XYZ as i64), + outer.to_euler(EulerOrder::XYZ), + Vector3::is_equal_approx, + "function: get_euler\n" + ); + assert_eq_approx!( + inner.get_rotation_quaternion(), + outer.to_quat(), + Quaternion::is_equal_approx, + "function: get_rotation_quaternion\n" + ) +} + +fn deg_to_rad(rotation: Vector3) -> Vector3 { + Vector3::new( + rotation.x.to_radians(), + rotation.y.to_radians(), + rotation.z.to_radians(), + ) +} diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index eb225218e..f481e1cfb 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -11,6 +11,7 @@ use std::panic::UnwindSafe; mod array_test; mod base_test; +mod basis_test; mod builtin_test; mod codegen_test; mod color_test; @@ -24,6 +25,8 @@ mod packed_array_test; mod quaternion_test; mod singleton_test; mod string_test; +mod transform2d_test; +mod transform3d_test; mod utilities_test; mod variant_test; mod virtual_methods_test; @@ -32,6 +35,7 @@ fn run_tests() -> bool { let mut ok = true; ok &= array_test::run(); ok &= base_test::run(); + ok &= basis_test::run(); ok &= builtin_test::run(); ok &= codegen_test::run(); ok &= color_test::run(); @@ -45,6 +49,8 @@ fn run_tests() -> bool { ok &= quaternion_test::run(); ok &= singleton_test::run(); ok &= string_test::run(); + ok &= transform2d_test::run(); + ok &= transform3d_test::run(); ok &= utilities_test::run(); ok &= variant_test::run(); ok &= virtual_methods_test::run(); diff --git a/itest/rust/src/transform2d_test.rs b/itest/rust/src/transform2d_test.rs new file mode 100644 index 000000000..3334e73ea --- /dev/null +++ b/itest/rust/src/transform2d_test.rs @@ -0,0 +1,68 @@ +/* + * 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 crate::itest; + +use godot::prelude::{inner::InnerTransform2D, *}; +use godot::private::class_macros::assert_eq_approx; + +const TEST_TRANSFORM: Transform2D = Transform2D::from_cols( + Vector2::new(1.0, 2.0), + Vector2::new(3.0, 4.0), + Vector2::new(5.0, 6.0), +); + +pub(crate) fn run() -> bool { + let mut ok = true; + ok &= transform2d_equiv(); + ok +} + +#[itest] +fn transform2d_equiv() { + let inner = InnerTransform2D::from_outer(&TEST_TRANSFORM); + let outer = TEST_TRANSFORM; + let vec = Vector2::new(1.0, 2.0); + + #[rustfmt::skip] + let mappings_transform = [ + ("affine_inverse", inner.affine_inverse(), outer.affine_inverse() ), + ("orthonormalized", inner.orthonormalized(), outer.orthonormalized() ), + ("rotated", inner.rotated(1.0), outer.rotated(1.0) ), + ("rotated_local", inner.rotated_local(1.0), outer.rotated_local(1.0) ), + ("scaled", inner.scaled(vec), outer.scaled(vec) ), + ("scaled_local", inner.scaled_local(vec), outer.scaled_local(vec) ), + ("translated", inner.translated(vec), outer.translated(vec) ), + ("translated_local", inner.translated_local(vec), outer.translated_local(vec) ), + ("interpolate_with", inner.interpolate_with(Transform2D::IDENTITY, 0.5), outer.interpolate_with(Transform2D::IDENTITY, 0.5)) + ]; + for (name, inner, outer) in mappings_transform { + assert_eq_approx!( + &inner, + &outer, + Transform2D::is_equal_approx, + "function: {name}\n" + ); + } + + assert_eq_approx!( + inner.get_rotation(), + outer.rotation(), + |a, b| is_equal_approx(a as f32, b), + "function: get_rotation\n" + ); + assert_eq_approx!( + inner.get_rotation(), + outer.rotation(), + |a, b| is_equal_approx(a as f32, b), + "function: get_rotation\n" + ); + assert_eq_approx!( + inner.get_skew(), + outer.skew(), + |a, b| is_equal_approx(a as f32, b), + "function: get_scale\n" + ); +} diff --git a/itest/rust/src/transform3d_test.rs b/itest/rust/src/transform3d_test.rs new file mode 100644 index 000000000..b7a54b0b4 --- /dev/null +++ b/itest/rust/src/transform3d_test.rs @@ -0,0 +1,52 @@ +/* + * 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 crate::itest; + +use godot::prelude::{inner::InnerTransform3D, *}; +use godot::private::class_macros::assert_eq_approx; + +const TEST_TRANSFORM: Transform3D = Transform3D::new( + Basis::from_cols( + Vector3::new(1.0, 2.0, 3.0), + Vector3::new(4.0, 5.0, 6.0), + Vector3::new(7.0, 8.0, -9.0), + ), + Vector3::new(10.0, 11.0, 12.0), +); + +pub(crate) fn run() -> bool { + let mut ok = true; + ok &= transform3d_equiv(); + ok +} + +#[itest] +fn transform3d_equiv() { + let inner = InnerTransform3D::from_outer(&TEST_TRANSFORM); + let outer = TEST_TRANSFORM; + let vec = Vector3::new(1.0, 2.0, 3.0); + + #[rustfmt::skip] + let mappings_transform = [ + ("affine_inverse", inner.affine_inverse(), outer.affine_inverse() ), + ("orthonormalized", inner.orthonormalized(), outer.orthonormalized() ), + ("rotated", inner.rotated(vec.normalized(), 1.0), outer.rotated(vec.normalized(), 1.0) ), + ("rotated_local", inner.rotated_local(vec.normalized(), 1.0), outer.rotated_local(vec.normalized(), 1.0) ), + ("scaled", inner.scaled(vec), outer.scaled(vec) ), + ("scaled_local", inner.scaled_local(vec), outer.scaled_local(vec) ), + ("translated", inner.translated(vec), outer.translated(vec) ), + ("translated_local", inner.translated_local(vec), outer.translated_local(vec) ), + ("interpolate_with", inner.interpolate_with(Transform3D::IDENTITY, 0.5), outer.interpolate_with(Transform3D::IDENTITY, 0.5)) + ]; + for (name, inner, outer) in mappings_transform { + assert_eq_approx!( + &inner, + &outer, + Transform3D::is_equal_approx, + "function: {name}\n" + ); + } +} diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index ff1c497b0..1211baeb7 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -10,11 +10,17 @@ use godot::builtin::{ }; use godot::engine::Node3D; use godot::obj::InstanceId; -use godot::prelude::{Array, Dictionary, VariantConversionError}; +use godot::prelude::{Array, Basis, Dictionary, VariantConversionError}; use godot::sys::{GodotFfi, VariantOperator, VariantType}; use std::cmp::Ordering; use std::fmt::{Debug, Display}; +const TEST_BASIS: Basis = Basis::from_rows( + Vector3::new(1.0, 2.0, 3.0), + Vector3::new(4.0, 5.0, 6.0), + Vector3::new(7.0, 8.0, 9.0), +); + pub fn run() -> bool { let mut ok = true; ok &= variant_nil(); @@ -73,6 +79,9 @@ fn variant_conversions() { let str_val = "abcdefghijklmnop"; let back = String::from_variant(&str_val.to_variant()); assert_eq!(str_val, back.as_str()); + + // basis + roundtrip(TEST_BASIS); } #[itest] @@ -93,6 +102,9 @@ fn variant_get_type() { let variant = gstr("hello").to_variant(); assert_eq!(variant.get_type(), VariantType::String); + + let variant = TEST_BASIS.to_variant(); + assert_eq!(variant.get_type(), VariantType::Basis) } #[itest]