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鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement some of the Transform2D API in rust #791
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot!
Style-wise, you could use Self
to refer to the current type, and use #[inline]
for methods. I'm not a huge fan of the latter, but we have currently configured Clippy to check it. Might need to do a benchmark whether it has an impact on speed/code size.
One thing I'm not very sure about, is the getters/setters for translation (=origin), rotation and scale. Their presence kind of implies that the transform is affine, but with field public, this invariant cannot be upheld. What do we return if someone creates a non-affine transform? Even something like shear (which is affine) cannot be represented.
Also, I would like to go with the existing convention for getters that have no get_()
prefix.
I haven't checked the algorithms themselves yet, this is where tests might come in handy.
pub fn from_rotation_position(rotation: f32, position: Vector2) -> Transform2D { | ||
todo!() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit a strange method, even the way it exists in Godot. Several things are unclear:
- Is position actually the translation?
- Is there a scale component (length of position)
- How does the resulting transform look, if I had to describe it in terms of the identity +
rotated()
,translated()
andscaled()
?
I'm also OK to remove this method for these reasons; we already use sometimes more idiomatic and expressive APIs rather than providing a 1:1 GDScript port.
For example, in interpolate_with()
below you use this method, but still have to provide a scale a posteriori; it would have been clearer to use translated()
& Co.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe there is some value in keeping a method like this, as it seems to be the most widely used constructor used in GDScript. It is a bit unnatural to think of rotations in terms of their axis vectors when, in 2d, rotations are typically represented with a single angle
. So having a more user-friendly constructor feels like a good idea.
What we can do, is extending the API by also accepting a scale vector, and have the implementation be expressed in terms of .translated(), .rotated() and .scaled() to avoid code duplication. I'm including a proposal for this on my next commit 馃憤
/// Returns the transform's origin (translation). | ||
pub fn get_origin(&self) -> Vector2 { | ||
self.origin | ||
} | ||
|
||
/// Sets the transform's origin (translation). | ||
pub fn set_origin(&mut self, origin: Vector2) { | ||
self.origin = origin; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these 2 methods are not useful, since origin
is already public.
What could be an option is to name them set/get_translation()
instead, that would be symmetric with the methods concerning rotation and scale. Would need to be made obvious in the doc that for affine transforms, it's the same as accessing origin
directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree this method is not very useful, I just added it to be symmetrical with the rotation and scale properties. But I think it makes sense to remove it. This way users won't get confused with two different things that are essentially the same.
Would need to be made obvious in the doc that for affine transforms, it's the same as accessing origin directly.
Actually, if we keept this implementation, it's not just for affine transforms, the method is always accessing the origin field directly 馃槄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the PR! It looks good to me and I agree with all points on @Bromeon 's feedback.
One thing that I did notice is that the clippy lints requesting #[inline] fail, so please make sure to check with clippy that all of the necessary inlining has been done :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the good work! I think it's looking good. There were a couple minor things that I noticed related to the comment blocks.
@@ -1,10 +1,216 @@ | |||
use super::Vector2; | |||
|
|||
/// A 2脳3 matrix (2 rows, 3 columns) used for 2D linear transformations. It can represent | |||
/// transformations such as translation, rotation, or scaling. It consists of a three |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slight nitpick. "It consists of three Vector2 values" without the 'a'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops! In my defense I'll say the typo was copied straight from the Godot docs 馃榿
@@ -1,10 +1,216 @@ | |||
use super::Vector2; | |||
|
|||
/// A 2脳3 matrix (2 rows, 3 columns) used for 2D linear transformations. It can represent | |||
/// transformations such as translation, rotation, or scaling. It consists of a three | |||
/// Vector2 values: x, y, and the origin. | |||
#[derive(Copy, Clone, Debug, PartialEq)] | |||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | |||
#[repr(C)] | |||
pub struct Transform2D { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe my understanding is a little off here, but I thought that the x
and y
components were there to represent the scale and rotation along each whereas the origin
represents the translation.
If I'm mistaken please let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the x and y components are the basis vectors of the transformation. So they can represent both a rotation and a translation of the coordinate space defined by the transform.
I don't think this docstring is stating otherwise, but perhaps the wording could be a bit better. I'll change it so it calls the vectors "x axis" and "y axis" instead.
We had to keep the same x
and y
names to be consistent with Godot's naming conveniton, but I prefer writing x_axis
and y_axis
to make the distinction clear where possible (such as in function arguments).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, it's not the doc string that I was commenting on, it was the x
, y
and origin
that were following the doc string. Apologies for the confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In particular the following
/// The x basis vector of the transform. Objects will move along this vector when
/// moving on the X axis in the coordinate space of this transform
pub x: Vector2,
/// The y basis vector of the transform. Objects will move along this vector when
/// moving on the Y axis in the coordinate space of this transform
pub y: Vector2,
/// The origin of the transform. The coordinate space defined by this transform
/// starts at this point.
pub origin: Vector2,
It feels a smidge like x and y translate the object when they should be morphing it, unless my understanding is wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that x
and y
are terrible names, and to be honest, I don't think Godot having them is reason enough to keep them. It's not like there's automated tooling to convert GDScript -> Rust. It's only important that the manual mapping is more or less clear.
This might be a candidate for #773. Maybe basis_x
and basis_y
would be better, but I'm open to suggestions. The 3D class Transform
has the fields basis
and origin
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about this? Maybe field renaming should be part of a separate PR, e.g. one for #773?
Just uploaded a first draft for the tests. Since I'm new at this and there's quite a bit more work to test the remaining functions, I'd like to wait for some early feedback before continuing. If you thing the current strategy's fine, I can go ahead and add the remaining methods 馃憤 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! Added some comments.
|
||
let new_transform_godot = unsafe { | ||
let mut tr = Transform2D::IDENTITY; | ||
let tr_p = &mut tr as *mut _ as *mut sys::godot_transform2d; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be encapsulated in a sys()
or to_sys()
method on the transform. Same for from_sys()
below. You could have a look at how Color
does it.
Also, could you only wrap the unsafe statements in unsafe {}
, even if it means having 2? If things explode, it's easier to locate the potential places :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed a slight inconsistency with the meaning of sys(), from_sys() and to_sys(). For instance, in Vector2, sys()
performs a cast from &Vector2
to *const godot_vector2
. This is the version I implemented. But in Color, it transmutes to a reference instead: from &Color
to &godot_color
. Which convention should we keep for Transform2d?
Moreover, In this case calling the constructor requires passing in a *mut godot_transform2d
. Using sys()
is not useful here since we need a mutable pointer. Should I add a sys_mut()
to encapsulate this logic instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the docs and the dark corners of the Nomicon:
transmute
is incredibly unsafe. There are a vast number of ways to cause undefined behavior with this function.transmute
should be the absolute last resort.Get out of our way type system! We're going to reinterpret these bits or die trying! Even though this book is all about doing things that are unsafe, I really can't emphasize that you should deeply think about finding Another Way than the operations covered in this section. This is really, truly, the most horribly unsafe thing you can do in Rust. The guardrails here are dental floss. [...] The ways to cause Undefined Behavior with this are mind boggling.
We might want to go with pointer casts, while dangerous they don't entirely suspend the type system.
Yes, sys_mut()
is OK.
Let's run some intermediate tests... |
tryBuild failed: |
The current MSRV (minimum supported Rust version) 1.48 does not have error[E0658]: use of unstable library feature 'clamp'
--> gdnative-core/src/core_types/transform2d.rs:190:19
|
190 | let dot = f32::clamp(v1.dot(v2), -1.0, 1.0);
| ^^^^^^^^^^
|
= note: see issue #44095 <https://github.com/rust-lang/rust/issues/44095> for more information If we want to keep that compatibility, we'd probably have to write our own clamp for now... Let's see if there are other issues: |
tryBuild failed: |
Oops, sometimes I forget I'm on nightly. Thanks for catching that! f32::clamp is a very handy method! 馃檪 I will add an implementation for the time being, with a comment that says it can be removed once |
I pushed a new version with one more test case, which uncovered several bugs in the implementation. I will keep adding more test for the remaining functions and let you know once it's ready. @Bromeon I left a few replies on your review's comments. Please have a look whenever you have some time! |
#[inline] | ||
pub fn from_rotation_translation_scale( | ||
rotation: f32, | ||
translation: Vector2, | ||
scale: Vector2, | ||
) -> Transform2D { | ||
Self::IDENTITY | ||
.translated(translation) | ||
.rotated(rotation) | ||
.scaled(scale) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be clearer if the method name and parameter order matched the order of transform application, no?
Also, doc is no longer matching the parameters.
#[doc(hidden)] | ||
#[inline] | ||
pub fn to_sys(self) -> sys::godot_transform2d { | ||
unsafe { std::mem::transmute(self) } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look like this method it used; I would remove it. We can always add it if necessary.
bors try |
tryTimed out. |
bors try |
tryBuild succeeded: |
I've just added a few more tests and I believe this is ready for a final review 馃槃 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks mostly good, just small remarks/questions.
Maybe @jacobsky could have another look?
After that, once you combine the commits to one, we can merge it in my opinion. Let's get it out there, addendums can still be done later (like the mentioned field names).
@@ -1,10 +1,216 @@ | |||
use super::Vector2; | |||
|
|||
/// A 2脳3 matrix (2 rows, 3 columns) used for 2D linear transformations. It can represent | |||
/// transformations such as translation, rotation, or scaling. It consists of a three | |||
/// Vector2 values: x, y, and the origin. | |||
#[derive(Copy, Clone, Debug, PartialEq)] | |||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | |||
#[repr(C)] | |||
pub struct Transform2D { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about this? Maybe field renaming should be part of a separate PR, e.g. one for #773?
let mut inverted = *self; | ||
|
||
let det = self.basis_determinant(); | ||
// assert!(det != 0.0, "The determinant cannot be zero"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we still have this assertion, or maybe a debug_assert!
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about this? Maybe field renaming should be part of a separate PR, e.g. one for #773?
If you want, I could rename this to axis_x
, axis_y
and origin
. I didn't change it to avoid introducing a breaking change, but I agree it's better naming. Just let me know 馃憤
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we still have this assertion, or maybe a debug_assert!?
I was meaning to ask, then forgot 馃槄. The assert was "ported" from Godot's cpp codebase, but then I commented it because I wasn't sure if a panic was acceptable in this kind of function.
A debug_assert!
looks like a good tradeoff to me, I'm pushing it with my next commit 馃憤
/// Returns the transform's scale. | ||
#[inline] | ||
pub fn scale(&self) -> Vector2 { | ||
let det_sign = f32::signum(self.basis_determinant()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a particular reason why you use the notation f32::signum(val)
instead of val.signum()
?
signum()
is not an associated method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No particular reason, I'm used to treating these as plain functions (like sin and cos) as it looks more natural and closer to how it's written in math.
But it's best to keep whatever the current convention is 馃憤 I will change ti to val.signum()
tryBuild succeeded: |
@Bromeon I'm unsure how to do this. There's usually a way to squash the commits when merging on your end and that's typically how I do this. Do you need me to do anything on my fork? 馃槄 |
Added some of the Transform2D methods from the Godot API to its Rust counterpart. The implementation uses rust, but closely follows the C++ codebase. Tests have been added to validate that calling the native transform2d API is equivalent to using the Rust methods.
0038317
to
fca5011
Compare
Alright, I took care of the squashing on my end. Let me know if that works for you 馃槃 |
@setzer22 Thank you for the hard work! I think everything looks good to go! bors r+ |
馃敀 Permission denied Existing reviewers: click here to make jacobsky a reviewer |
bors r+ |
馃敀 Permission denied Existing reviewers: click here to make jacobsky a reviewer |
馃敀 Permission denied Existing reviewers: click here to make jacobsky a reviewer |
bors r+ |
馃敀 Permission denied Existing reviewers: click here to make jacobsky a reviewer |
bors r+ |
Build succeeded: |
Added some of the Transform2D methods from the Godot API to its Rust counterpart. The implementation uses rust, but closely follows the C++ codebase. Tests have been added to validate that calling the native transform2d API is equivalent to using the Rust methods.
821: Implement some methods of the Transform API r=Bromeon a=setzer22 Here are some more Transform methods. I followed the same strategy as for #791, where the implementation was done by translating the C++ code to Rust. I can also add some tests similar to the ones from the previous PR if you'd like 馃憤 Although the code for Transform is far simpler, because it delegates most of the implementation to `Vector3` and `Basis`. Co-authored-by: Setzer22 <jsanchezfsms@gmail.com>
As we discussed, here's an initial implementation of the Transform2D. The implementation is currently untested, so I'm marking it as a work-in-progress. I'm just posting this early so we can discuss if you thing this is the right approach 馃憤
The code closely follows the exposed GDScript API and even copies its documentation strings. The Implementation is adapted from Godot's C++ source code, delegating to glam (via Vector2) when necessary.
I decided against adding a conversion between glam::Affine2 and Transform2D because it was not immediately clear to me if the mapping would be 1:1 (godot's Transform2D does not necessarily need to be an affine transformation, although it's assumed in certain places).
Instead, I decided to just follow the C++ implementation, which was simple enough, to ensure results would be predictable when porting code from GDScript. We are still using glam underneath when delegating all of
Vector3
's operations, which happens in many places.