Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extension trait for floats #330

Merged
merged 2 commits into from
Jul 11, 2023

Conversation

lilizoey
Copy link
Member

@lilizoey lilizoey commented Jul 3, 2023

Adds an extension trait FloatExt that implements the various utility methods we have directly on f32 and f64.

This is implemented for both f64 and f32 instead of just real to make it easier to use them everywhere. Since godot float values are always f64 and real may be either f32 or f64 depending on configuration.

Also moves a bunch of methods in Vector2 and Vector3 that used these methods into a macro, as well as their ApproxEq impls. And used that macro to implement those methods for Vector2, Vector3 and Vector4. (Vector4 in godot doesn't actually contain the bezier functions but i figured it's fine to add them.)

Also split up the tests for eq_approx for floats into f32 and f64 variants, which also makes it so we dont need to use cfg in the one test.

labelled QOL despite this technically adding a couple of functions to Vector4, since it's mainly just about improving ergonomics of float functions.

@lilizoey lilizoey added quality-of-life No new functionality, but improves ergonomics/internals c: core Core components labels Jul 3, 2023
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-330

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! While this makes sense from a technical perspective, there are some smaller things to discuss about the resulting API.

Some names are now a bit confusing or make it hard to see that they're from Godot: f.sign() vs. f.signum() from the standard library.

Others already have a trait: f.approx_eq(other) -- one should be removed, possibly with #[doc(alias)] in the ApproxEq trait itself.

For many functions, the receiver syntax is a bit less clear as it consumes the from argument, introducing asymmetry: lerp_angle(from, to, weight) vs. from.lerp_angle(to, weight). It's also different from GDScript for floats. At least it's more consistent with vectors, and we could theoretically use the fully qualified syntax... I guess this is just a matter of getting used to?

Also, if we refactor those, are there ergonomic improvements we could do? Some methods take a big number of parameters, but I guess encapsulating just makes matters worse. Other ideas? (This could of course be done separately...)


Apart from that, did you change any of the implementations? It's impossible for me to see because you moved everything to another file and changed indentation, so git cannot detect changes. Just to know what I should review 🙂

@@ -364,15 +364,19 @@ impl Basis {
Self::from_cols(self.rows[0], self.rows[1], self.rows[2])
}

/// Returns the orthonormalized version of the matrix (useful to call from
/// ⚠️ Returns the orthonormalized version of the matrix (useful to call from
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This use of the warning sign is incorrect, according to

gdext/godot/src/lib.rs

Lines 81 to 85 in 565bf10

//! To help you identify panicking methods, we use the symbol "⚠️" at the beginning of the documentation;
//! this should also appear immediately in the auto-completion of your IDE. Note that this warning sign is
//! not used as a general panic indicator, but particularly for methods which have a `Option`/`Result`-based
//! overload. If you want to know whether and how a method can panic, check if its documentation has a
//! _Panics_ section.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh i wasn't aware of that distinction. fair enough.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also discuss it of course. I thought it would be redundant if we already have a "Panics" section that had the exact same meaning. But maybe it helps in IDE suggestions... 🤷

@Bromeon
Copy link
Member

Bromeon commented Jul 3, 2023

Also:

This is implemented for both f64 and f32 instead of just real to make it easier to use them everywhere. Since godot float values are always f64 and real may be either f32 or f64 depending on configuration.

That issue is not really solved by this PR, but made a bit more convenient to deal with.

The default configuration for godot is real == f32, yet all Godot APIs use f64. This means there is constant friction between the two points, already starting at fn process(f64: delta) and passing it to anything that takes real.

In some cases, real is critical for FFI safety (e.g. as a component of vectors), but in others it could maybe be passed as f64, narrowing where needed.

@lilizoey
Copy link
Member Author

lilizoey commented Jul 3, 2023

Thanks! While this makes sense from a technical perspective, there are some smaller things to discuss about the resulting API.

Some names are now a bit confusing or make it hard to see that they're from Godot: f.sign() vs. f.signum() from the standard library.

Yeah i wasn't sure what to do there, since signum and sign have different semantics.

Others already have a trait: f.approx_eq(other) -- one should be removed, possibly with #[doc(alias)] in the ApproxEq trait itself.

fair

For many functions, the receiver syntax is a bit less clear as it consumes the from argument, introducing asymmetry: lerp_angle(from, to, weight) vs. from.lerp_angle(to, weight). It's also different from GDScript for floats. At least it's more consistent with vectors, and we could theoretically use the fully qualified syntax... I guess this is just a matter of getting used to?

You can also just do f32::lerp_angle(from, to, weight). And i can also just implement these as not taking a self.

Also, if we refactor those, are there ergonomic improvements we could do? Some methods take a big number of parameters, but I guess encapsulating just makes matters worse. Other ideas? (This could of course be done separately...)

I'll have a look

Apart from that, did you change any of the implementations? It's impossible for me to see because you moved everything to another file and changed indentation, so git cannot detect changes. Just to know what I should review 🙂

sign:

// old
pub fn sign(value: real) -> real {
    if value == 0.0 {
        0.0
    } else if value < 0.0 {
        -1.0
    } else {
        1.0
    }
}

// new
fn sign(self) -> Self {
    use std::cmp::Ordering;

    match self.partial_cmp(&0.0) {
        Some(Ordering::Equal) => 0.0,
        Some(Ordering::Greater) => 1.0,
        Some(Ordering::Less) => -1.0,
        // NAN, Godot return `1` in this case.
        None => 1.0,
    }
}

fposmod:

// old
pub fn fposmod(x: real, y: real) -> real {
    let mut value = x % y;
    if ((value < 0.0) && (y > 0.0)) || ((value > 0.0) && (y < 0.0)) {
        value += y;
    }
    value += 0.0;
    value
}

// new
fn fposmod(self, pmod: Self) -> Self {
    let mut value = self % pmod;
    if ((value < 0.0) && (pmod > 0.0)) || ((value > 0.0) && (pmod < 0.0)) {
        value += pmod;
    }
    value
}

is_angle_equal_approx:

// old
pub fn is_angle_equal_approx(a: &real, b: &real) -> bool {
    let (x1, y1) = a.sin_cos();
    let (x2, y2) = b.sin_cos();

    println!("({x1}, {y1}) ({x2}, {y2})");

    is_equal_approx(
        Vector2::distance_to(Vector2::new(x1, y1), Vector2::new(x2, y2)),
        0.0,
    )
}

// new
fn is_angle_equal_approx(self, other: Self) -> bool {
    let (x1, y1) = self.sin_cos();
    let (x2, y2) = other.sin_cos();

    let point_1 = Vector2::new(real::$to_real(x1), real::$to_real(y1));
    let point_2 = Vector2::new(real::$to_real(x2), real::$to_real(y2));

    point_1.distance_to(point_2).is_zero_approx()
}

The rest should be identical with just some renaming (mainly real -> Self)

@Bromeon
Copy link
Member

Bromeon commented Jul 3, 2023

Yeah i wasn't sure what to do there, since signum and sign have different semantics.

I think it's OK. Maybe a short doc that appears in IDEs helps:

/// Godot sign; returns 0 for input 0.
///
/// See also [`signum`][Self::signum], which always returns -1 or +1 (or NaN).
fn sign(self) -> Self {

Is Godot's "return 1 for NaN" a feature? It looks like a bug for me, one that our previous implementation also had: the else branch returns 1.

Either way, this is completely unintuitive -- we should return NaN in that case, too.


The fposmod has useless parentheses around the if (already from the old code), could you remove them? Precedence of || and && should be well-known to any developer.


And i can also just implement these as not taking a self.

It's probably fine.


The rest should be identical with just some renaming (mainly real -> Self)

Thanks a lot for the side-by-side! Changes look good, comments about sign and fposmod see above.

For the future, please keep refactoring/API changes and semantics in separate commits (at least when easily possible), that allows to track such changes better 🙂

@lilizoey
Copy link
Member Author

lilizoey commented Jul 6, 2023

Yeah i wasn't sure what to do there, since signum and sign have different semantics.

I think it's OK. Maybe a short doc that appears in IDEs helps:

/// Godot sign; returns 0 for input 0.
///
/// See also [`signum`][Self::signum], which always returns -1 or +1 (or NaN).
fn sign(self) -> Self {

Is Godot's "return 1 for NaN" a feature? It looks like a bug for me, one that our previous implementation also had: the else branch returns 1.

Either way, this is completely unintuitive -- we should return NaN in that case, too.

seems godot somewhat agrees godotengine/godot#79036

The fposmod has useless parentheses around the if (already from the old code), could you remove them? Precedence of || and && should be well-known to any developer.

Honestly i somewhat disagree. yes i know the precedence of && and || but it's such an easy thing to accidentally get wrong, and it's easy to miss when it's wrong. so i usually add some reduntant parentheses there anyway. but i dont think we need parentheses around the < and >.

For the future, please keep refactoring/API changes and semantics in separate commits (at least when easily possible), that allows to track such changes better slightly_smiling_face

oki

@lilizoey
Copy link
Member Author

lilizoey commented Jul 6, 2023

Regarding other refactoring, only thing i can really think of would be to create a like bezier struct, or cubic struct of some kind to store some of the arguments to cubic/bezier interpolation. but im not sure if that'd really be worth it. maybe in the future we could make a better abstraction for doing these more advanced interpolation functions. something that we could reuse for the vectors too since they can also be interpolated.

@lilizoey lilizoey force-pushed the feature/float-extension-trait branch from 7ca49a3 to e2e1b6f Compare July 6, 2023 17:24
@Bromeon
Copy link
Member

Bromeon commented Jul 6, 2023

Yep, we can always think about other abstractions :) but also, in many cases, polymorphism may not always be needed -- it's also OK if the same methods exist for different types without having an underlying trait.

There are some remaining uses of is_equal_approx that render the CI red; is the PR ready apart from that?

@lilizoey
Copy link
Member Author

lilizoey commented Jul 6, 2023

Yep, we can always think about other abstractions :) but also, in many cases, polymorphism may not always be needed -- it's also OK if the same methods exist for different types without having an underlying trait.

There are some remaining uses of is_equal_approx that render the CI red; is the PR ready apart from that?

oh, yeah it should be. i guess i forgot to run check locally.

- Add `impl_float_vector_component_fns` macro for float-vectors
@lilizoey lilizoey force-pushed the feature/float-extension-trait branch from e2e1b6f to 85a8c0e Compare July 6, 2023 19:28
@Bromeon Bromeon enabled auto-merge July 11, 2023 17:50
@Bromeon Bromeon added this pull request to the merge queue Jul 11, 2023
Merged via the queue into godot-rust:master with commit 8a22bd7 Jul 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: core Core components quality-of-life No new functionality, but improves ergonomics/internals
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants