-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
Reduce code duplication between Transform and GlobalTransform #2026
Comments
The code duplication was an explicit decision to make GlobalTransform more ergonomic to work with. GlobalTransforms are a core enough component that it was worth it to me to duplicate the code: #374 (comment). I honestly hate the "rust newtype" pattern as a means of reusing implementations in public apis. The following api is actively user-hostile imo: global_transform.0.translation.x += 1.0; Rust should give us better tools for this case. Something like the "type alias" syntax, but for defining actual new types instead of new names for the same type. Until that happens, I won't make compromises for high-usage apis when we can eat the complexity internally. That being said, I am willing to consider alternatives: TraitsRemoves code duplication. Allows users to write code on both transform types. Transform method docs are less clear + discoverable. struct Transform { translation: Vec3, rotation: Quat, scale: Vec3 }
struct GlobalTransform { translation: Vec3, rotation: Quat, scale: Vec3 }
// Similarity is the name for a [translation, rotation, scale] transform representation
trait Similarity {
fn rotation(&self) -> &Quat;
fn rotation_mut(&mut self) -> &mut Vec3;
/* other accessors here */
}
trait TransformOperations {
fn rotate(&mut self, rotation: Quat);
/* other methods here */
}
impl<T: Similarity> TransformOperations for T {
fn rotate(&mut self, rotation: Quat) {
*self.rotation_mut() *= rotation;
}
/* other impls here */
} MacrosCuts down on code duplication at the cost of "macro complexity". Doesn't solve the "using transform types interchangeably" problem. struct Transform { translation: Vec3, rotation: Quat, scale: Vec3 }
struct GlobalTransform { translation: Vec3, rotation: Quat, scale: Vec3 }
impl_transform_methods!(Transform);
impl_transform_methods!(GlobalTransform); |
I'm much more concerned with the user code duplication through not being able to use transform type interchangeably than I am with internal code duplication, so we should focus on finding an acceptable way to handle that.
(Ab)using With that in mind, I think there are only two remaining options: traits and |
Yeah I wouldn't want to use Deref in this context either (however I would do that over It seems like traits are the way to go here. |
There are a few open questions like "how do we handle transform multiplication properly" and other things in that vein. We will probably want manual impls for each type to ensure the context is preserved. Ex: GlobalTransform * Transform = GlobalTransform |
It's not actually clear to me what the best behavior for multiplication would be. I think a reasonable choice would be for the output type to match the LHS, for symmetry with |
With enum TransformType {
Global,
Local,
}
struct Transform<const TYPE: TransformType> {
translation: Vec3,
rotation: Quat,
scale: Vec3,
}
impl<const TYPE: TransformType> Transform<TYPE> {
/* implement methods for both types of transforms */
} Which I think is quite nice but might be a long way off. |
Would this be closed to it with current Rust? trait TransformType {}
struct Global;
impl TransformType for Global {}
struct Local;
impl TransformType for Local {}
struct Transform<T: TransformType> {
translation: Vec3,
rotation: Quat,
scale: Vec3,
ty: std::marker::PhantomData<T>,
} |
Sure but this way is open ended, i.e consumers could make more 'types of transforms'. |
Users could still define their own types that impl type GlobalTransform = Transform<Global>;
struct Transform<T: TransformType = Local> {
...
} |
Generic defaults only work in limited cases: Rust-GCC/gccrs#307 (comment) struct Box<T, A: Default = ()>(T, A);
impl<T, A: Default> Box<T, A> {
fn new(x: T) -> Self {
Box(x, Default::default())
}
}
fn main() {
Box::new(1);
}
|
Oh, that's unfortunate. An alternative would be to rename |
Not sure if this is actually a good idea but this also popped into my head from the coordinate system discussion. Struct Local;
Trait TransformType {}
Trait RightHanded {}
Trait YUp {}
Impl TransformType for Local {}
Impl RightHanded for Local {}
Impl YUp for Local {}
Impl<T> Transform<T>
where
T: TransformType + RightHanded + YUp
{
fn left(&self) -> f32 {
-self.translation.x
}
/* other right handed, y up functions */
} This is probably just complicating the traits approach though 🤣 |
I'm gonna start working on an implementation using the type parameter approach for now. I think having fine-grained traits like this is not necessary at this point. |
Cool cool. I'm curious to see if in practice we hit the "default weirdness" @bjorn3 called out. Thats my only major concern. |
I was planning to just make the generic one |
Yeah I'd prefer to leave Transform naming as it is. Type aliases seem like a reasonable fix, although they might complicate the doc situation a bit. Lets roll with the alias and see what the docs look like. Its probably fine. |
…4379) # Objective - Add capability to use `Affine3A`s for some `GlobalTransform`s. This allows affine transformations that are not possible using a single `Transform` such as shear and non-uniform scaling along an arbitrary axis. - Related to #1755 and #2026 ## Solution - `GlobalTransform` becomes an enum wrapping either a `Transform` or an `Affine3A`. - The API of `GlobalTransform` is minimized to avoid inefficiency, and to make it clear that operations should be performed using the underlying data types. - using `GlobalTransform::Affine3A` disables transform propagation, because the main use is for cases that `Transform`s cannot support. --- ## Changelog - `GlobalTransform`s can optionally support any affine transformation using an `Affine3A`. Co-authored-by: Carter Anderson <mcanders1@gmail.com>
…evyengine#4379) # Objective - Add capability to use `Affine3A`s for some `GlobalTransform`s. This allows affine transformations that are not possible using a single `Transform` such as shear and non-uniform scaling along an arbitrary axis. - Related to bevyengine#1755 and bevyengine#2026 ## Solution - `GlobalTransform` becomes an enum wrapping either a `Transform` or an `Affine3A`. - The API of `GlobalTransform` is minimized to avoid inefficiency, and to make it clear that operations should be performed using the underlying data types. - using `GlobalTransform::Affine3A` disables transform propagation, because the main use is for cases that `Transform`s cannot support. --- ## Changelog - `GlobalTransform`s can optionally support any affine transformation using an `Affine3A`. Co-authored-by: Carter Anderson <mcanders1@gmail.com>
…evyengine#4379) # Objective - Add capability to use `Affine3A`s for some `GlobalTransform`s. This allows affine transformations that are not possible using a single `Transform` such as shear and non-uniform scaling along an arbitrary axis. - Related to bevyengine#1755 and bevyengine#2026 ## Solution - `GlobalTransform` becomes an enum wrapping either a `Transform` or an `Affine3A`. - The API of `GlobalTransform` is minimized to avoid inefficiency, and to make it clear that operations should be performed using the underlying data types. - using `GlobalTransform::Affine3A` disables transform propagation, because the main use is for cases that `Transform`s cannot support. --- ## Changelog - `GlobalTransform`s can optionally support any affine transformation using an `Affine3A`. Co-authored-by: Carter Anderson <mcanders1@gmail.com>
With #4379, the internal representation for both types is now very different. Is this issue still relevant? |
…evyengine#4379) # Objective - Add capability to use `Affine3A`s for some `GlobalTransform`s. This allows affine transformations that are not possible using a single `Transform` such as shear and non-uniform scaling along an arbitrary axis. - Related to bevyengine#1755 and bevyengine#2026 ## Solution - `GlobalTransform` becomes an enum wrapping either a `Transform` or an `Affine3A`. - The API of `GlobalTransform` is minimized to avoid inefficiency, and to make it clear that operations should be performed using the underlying data types. - using `GlobalTransform::Affine3A` disables transform propagation, because the main use is for cases that `Transform`s cannot support. --- ## Changelog - `GlobalTransform`s can optionally support any affine transformation using an `Affine3A`. Co-authored-by: Carter Anderson <mcanders1@gmail.com>
What problem does this solve or what need does it fill?
Currently,
Transform
andGlobalTransform
have effectively the same interface, just as different components. They are implemented as completely different types. Code is duplicated between the two and there is no simple way to write functions that are generic over both.What solution would you like?
Make the definition of
GlobalTransform
:What alternative(s) have you considered?
Creating a trait implemented by both transform types. I don't think there's any real reason for this because the actual implementation details are identical, as far as I know. A trait would make sense if the implementation differed, but since they don't I believe composition makes more sense.
Another thing we could add is a
Deref<Target = Transform>
impl forGlobalTransform
. This would get rid of someglobal_transform.0
calls, but I think it's not worth it. Also, even though this is a common pattern in the rust ecosystem, it goes directly against the standard library's documentation for the intended usage of theDeref
trait:Additional context
This would be a breaking change, and would require a small modification to any code that uses global transforms.
The
#[repr(transparent)]
isn't strictly necessary, but there's no real reason not to add it.I'm happy to write a PR for this if people agree that it's a good idea.
The text was updated successfully, but these errors were encountered: