-
Notifications
You must be signed in to change notification settings - Fork 212
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
Implement methods on Rect2 and Aabb #867
Conversation
48e1d60
to
eeb694f
Compare
🎉 I added tests for the new I think this leaves |
a0ecc83
to
bfd1b3e
Compare
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 for this great contribution! 🙂
Also thanks a lot for the added tests, this really helps.
Added comments in code.
Some general thoughts:
-
I would probably make the methods operate on
self
, not&self
. We do it also forQuat
(however,Plane
is a bit inconsistent). I don't know where exactly we should have the "boundary", maybe theTransform
,Basis
etc. should keep using by-ref. -
Methods which have an ambiguous name could use the
#[must_use]
. This makes sure the user doesn't think they are changingself
. Examples:clip
expand
merge
grow*
-
You noted that some operations on negative sizes are unreliable. I think is invites bugs and we should rather write
assert!
to check preconditions, wherever they are needed. This might need a# Panics
section in the method documentation. -
The
get_longest_axis[_{index|size}]
methods are duplicating a lot of code and force the user to recompute the same thing multiple times, if he is interested in more than one. They are also redundant:get_longest_axis_index()
==size.max_axis()
get_longest_axis_size()
==size.as_ref().iter().max()
get_longest_axis()
==size.max_axis().to_unit_vector()
(not yet existing, but something like this could be added)
Furthermore, it's not clear how these methods behave if two edges have the same length. User lower-level primitives (arrays/slices) makes this more explicit.
So either we remove them entirely, or find a good way to represent their API. But at the very least, we should not have 6 times the almost identical code.
bors try
let position = self.position | ||
+ Vector3::new( | ||
self.size.x.min(0.0), | ||
self.size.y.min(0.0), | ||
self.size.z.min(0.0), | ||
); |
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.
Could also be:
let position = self.position + self.size.glam().min(glam::Vec3A::ZERO)
(they have pub(super)
visibility, if necessary can be extended to pub(crate)
).
pub size: Vector3, | ||
} | ||
|
||
impl Aabb { | ||
/// Creates an `Aabb` by position and size. |
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 implied by the constructor signature. Maybe instead mention that it's possible to have negative sizes.
/// Creates an `Aabb` by x, y, z, width, height, and depth. | ||
#[inline] | ||
pub fn from_components(x: f32, y: f32, z: f32, width: f32, height: f32, depth: f32) -> Self { | ||
let position = Vector3::new(x, y, z); | ||
let size = Vector3::new(width, height, depth); | ||
|
||
Self { position, size } | ||
} |
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.
Do we need this constructor? I'd generally tend to encourage the user to think in vectors rather than separate coordinates. It's typically also easier to understand than 6 numbers in an argument list.
Similar how we do it in Transform
, Transform2
and Basis
(but unlike Plane
).
/// Note: This method is not reliable for bounding boxes with a negative size. Use | ||
/// [`abs`][Self::abs] to get a positive sized equivalent box to check for contained points. | ||
#[inline] | ||
pub fn has_point(&self, point: Vector3) -> bool { |
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.
If we rename some methods already, this should be contains_point()
.
Also, if it's not reliable for negative sizes, we should panic in that case -- no point in letting the user run into bugs.
/// Returns the support point in a given direction. This is useful for collision detection | ||
/// algorithms. |
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 quickly explain what the support point is?
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 one confused me. The only information I have about it is the not too useful GDScript documentation and the implementation itself. I was able to infer its behavior from code (enough to write a test) but I don't really "get" its purpose for existing. I'm happy to remove it!
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.
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.
Interesting! Good find. The unfortunate part of this implementation on Aabb
is that it is a support point on a bounding box, and not the actual shape itself. Perhaps it is relevant here due to the simplicity of a cube, but I imagine it's more useful on a convex hull.
let points = [ | ||
self.position, | ||
self.position + Vector3::new(0.0, 0.0, self.size.z), | ||
self.position + Vector3::new(0.0, self.size.y, 0.0), | ||
self.position + Vector3::new(0.0, self.size.y, self.size.z), | ||
self.position + Vector3::new(self.size.x, 0.0, 0.0), | ||
self.position + Vector3::new(self.size.x, 0.0, self.size.z), | ||
self.position + Vector3::new(self.size.x, self.size.y, 0.0), | ||
self.position + self.size, | ||
]; |
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.
Why not reuse the code?
let corners = [Vector::ZERO; 8];
for i in 0..8 {
corner[i] = self.get_endpoint(i);
}
If you find a more compact functional approach, that's also fine!
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.
Funny enough, I was initially using get_endpoint
here, but reneged and switched to duplication like the C++ does.
let from = from.as_ref().iter(); | ||
let to = to.as_ref().iter(); | ||
let begin = self.position.as_ref().iter(); | ||
let end = self.get_end(); | ||
let end = end.as_ref().iter(); | ||
|
||
for (((from, to), begin), end) in from.zip(to).zip(begin).zip(end) { |
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'm not sure if we do ourselves a favor by using functional programming and iterators over components, instead of a simple 0..3
indexing.
I assume you had a reason to deviate from the Godot AABB::intersects_segment()
implementation?
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.
Possibly, a function
fn Vector3::get_coord(self, axis: Axis) -> f32
(or similarly named) could help here.
We could then index a vector component using a simple Vector3::get_coord(i.into())
.
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 code specifically went through a few iterations. This is how it started: 48e1d60#diff-77f6ed9f07c676ba6ec235e93f078192c45ed106f1b4619410f94ddcab499fd1R321-R392
if !self.intersects(b) { | ||
return Self::default(); | ||
} |
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 method should return Option<Self>
. We can then implement intersects()
in terms of this.
/// Note: This method is not reliable for `Rect2` with a negative size. Use [`abs`][Self::abs] | ||
/// to get a positive sized equivalent rectangle to check for contained points. | ||
#[inline] | ||
pub fn has_point(&self, point: Vector2) -> bool { |
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.
Same here: contains_point()
+ assert!(/* size is non-negative */)
&& b.position.x + b.size.x <= self.position.x + self.size.x | ||
&& b.position.y + b.size.y <= self.position.y + self.size.y |
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.
could use get_end()
here
also in a few of the following methods
tryBuild failed: |
It seems I'm going to need a better understanding of how much you are willing to diverge from the Godot interface and implementation. I am aware of some preexisting inconsistencies between For instance, all of the warnings about inverted rectangles are emergent from the implementation, which is more or less a straight port of the C++. If you don't mind a compatibility hazard, we could always make improvements to either support inverted rectangles, or as you suggest not support them at all with an assertion and panic. I think I need to understand what exactly that balance between compatibility and "doing things better" is to have a meaningful conversation. My default is to point at the C++ to justify this code. That includes the documentation, code structure (De-Morgan was brought up), all of the duplication, etc. So yeah, if you can let me know in general terms how much you are willing to break convention or compatibility, I'm all for making improvements above and beyond what Godot offers. |
Keeping compatibility with Godot is a good general approach, however under these constraints:
I guess you meant this specifically for the |
All of the items you provided answer many implied questions, so thank you for that. The Some specifics and my opinion on what to do about them:
pub struct AxisQueryResult {
pub axis: Axis,
pub size: Vector3,
}
Anyway, I think I have a better understanding what you're looking for. Even if my confusion is as broad as my questions. |
Thanks for your elaborate answer!
I think since this is only used really used in one place (with 6 variants), an extra type may not pay off. let (vector, axis) = aabb.get_longest_axis();
let (vector, axis) = aabb.get_shortest_axis(); It's also easily possible to use
Good catch, and I agree with your conclusion. Godot 4 seems to have renamed it to
Fully agree -- and guess what, in Godot 4 it's also called
This invariant is impossible to enforce with public fields. I think negative/inverted rectangles/AABBs are probably OK for certain use cases. The usage of certain methods on negative shapes however indicates usually a bug in my opinion -- if someone shows a valid use case in the future, we can allow it again. But I doubt that supporting negative shapes in this way was a conscious design choice on Godot's part.
Yes, it's a bit annoying, I already "fixed" this issue before. I might actually increase the MSRV (and maybe the Rust edition), otherwise we're stuck with this workaround for entire 0.10 -- incrementing MSRV is a breaking change. So don't change that part yet. |
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 updated the MSRV, let's see about the warnings now.
There are still a few suggestions open 🙂
bors try
pub fn get_support(&self, dir: Vector3) -> Vector3 { | ||
let center = self.size * 0.5; | ||
let offset = self.position + center; | ||
|
||
Vector3::new( | ||
if dir.x > 0.0 { -center.x } else { center.x }, | ||
if dir.y > 0.0 { -center.y } else { center.y }, | ||
if dir.z > 0.0 { -center.z } else { center.z }, | ||
) + offset | ||
} |
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.
The whole Godot implementation is strange. First, variables are badly named (center is not a position, offset is not a direction), second there's unnecessary back-and-forth.
With glam, can we not simply write this as the following?
pub fn get_support(&self, dir: Vector3) -> Vector3 {
// for each component c:
// c >= +0: self.position
// c <= -0: self.position + self.size
self.position + 0.5 * self.size * (1 - dir.glam().signum())
}
Might need to test it or at least double-check it, but the original impl seems quite confusing.
I'm not 100% sure about the border cases with signum()
(positive/negative zero), but even with an explicit if
, we can do better:
pub fn get_support(&self, dir: Vector3) -> Vector3 {
self.position + Vector3::new(
if dir.x > 0.0 { 0.0 } else { self.size.x },
if dir.y > 0.0 { 0.0 } else { self.size.y },
if dir.z > 0.0 { 0.0 } else { self.size.z },
)
}
/// Returns the support point in a given direction. This is useful for collision detection | ||
/// algorithms. |
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.
tryBuild succeeded: |
Yeah! I'm looking forward to getting back to this, soon. I just picked up a consulting gig last week and trying to wrap it up in the next few days. I agree with most (maybe all?) of the feedback you've provided so far, and I appreciate it. |
@parasyte Have you had a chance to look into this again, or is there anything I can help with? 🙂 |
In full disclosure, I'm just being lazy now. 😅 I'll try to find the motivation to pick it up again soon. |
@parasyte Are you still interested in this? If you think you won't continue this PR, please let me know, I can gladly take over 🙂 but I'd like to clean up old PRs to avoid unnecessary code rot. |
@Bromeon You can take over on this. I have been focused on things that are very different. |
bfd1b3e
to
ef1bee8
Compare
ef1bee8
to
b02b6ce
Compare
… Quat, Plane etc.)
7df8463
to
3e82357
Compare
867: Implement methods on Rect2 and Aabb r=Bromeon a=parasyte The docs are based on the Godot documentation for these types, and the code is based on the C++ that implements them. Only documented methods are implemented here. This is especially notable for the `Aabb` type, which has many methods in C++ which are not documented for GDScript. I also took some liberties with a few method names and types. For instance, `Rect2::grow_margin` takes an enum argument, and `Aabb::get_endpoint` returns `Option<Vector3>`. These are different from the GDScript counterparts. `Aabb::get_area` was renamed to `Aabb::get_volume`, etc. Finally, I haven't added any tests for `Aabb` because I did that file last and got bored/lazy before writing any tests (this is what happens when you are not adamant about TDD). The `Rect2` tests did help find plenty of minor bugs, so it is worthwhile to test `Aabb`. Closes #864 Co-authored-by: Jay Oster <jay@kodewerx.org> Co-authored-by: Jan Haller <bromeon@gmail.com>
…o_unit_vector(); rename get_* methods
3e82357
to
3344326
Compare
bors r+ |
867: Implement methods on Rect2 and Aabb r=Bromeon a=parasyte The docs are based on the Godot documentation for these types, and the code is based on the C++ that implements them. Only documented methods are implemented here. This is especially notable for the `Aabb` type, which has many methods in C++ which are not documented for GDScript. I also took some liberties with a few method names and types. For instance, `Rect2::grow_margin` takes an enum argument, and `Aabb::get_endpoint` returns `Option<Vector3>`. These are different from the GDScript counterparts. `Aabb::get_area` was renamed to `Aabb::get_volume`, etc. Finally, I haven't added any tests for `Aabb` because I did that file last and got bored/lazy before writing any tests (this is what happens when you are not adamant about TDD). The `Rect2` tests did help find plenty of minor bugs, so it is worthwhile to test `Aabb`. Closes #864 Co-authored-by: Jay Oster <jay@kodewerx.org> Co-authored-by: Jan Haller <bromeon@gmail.com>
Build failed: |
bors r+ |
Build succeeded: |
Thanks @parasyte for all the work! 👍 |
And thank you @Bromeon for taking it across the finish line! |
The docs are based on the Godot documentation for these types, and the code is based on the C++ that implements them.
Only documented methods are implemented here. This is especially notable for the
Aabb
type, which has many methods in C++ which are not documented for GDScript.I also took some liberties with a few method names and types. For instance,
Rect2::grow_margin
takes an enum argument, andAabb::get_endpoint
returnsOption<Vector3>
. These are different from the GDScript counterparts.Aabb::get_area
was renamed toAabb::get_volume
, etc.Finally, I haven't added any tests for
Aabb
because I did that file last and got bored/lazy before writing any tests (this is what happens when you are not adamant about TDD). TheRect2
tests did help find plenty of minor bugs, so it is worthwhile to testAabb
.Closes #864