-
Notifications
You must be signed in to change notification settings - Fork 24
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 non uniform scale #6
Implement non uniform scale #6
Conversation
|
||
/// Something which can be scaled **non-uniform** in x and y directions in *Drawing Space* | ||
pub trait ScaleNonUniform { | ||
fn scale_nu(&mut self, factor_x: f64, factor_y: f64, base: Vector); |
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.
Instead of _nu
, what about using the full _non_uniform
to improve readability?
Otherwise here are some synonyms for uniform:
- constant
- consistent
- steady
- invariable
- unvarying
- unfluctuating
- unvaried
- unchanging
- unwavering
- undeviating
- stable
- static
- sustained
- regular
- fixed
- even
- equal
- equable
- monotonous
- identical
- matching
- similar
- same
- alike
- like
- selfsame
- homogeneous (
scale_non_homogeneous()
sounds fairly accurate, but it's pretty long...) - corresponding
- equivalent
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.
reading through this list i lean towards non-constant, although non-uniform comes from another CAD package i frequently use.
Definitely agree on the readability.
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 the word "constant" be confused with Rust's concept of mutability?
I'm leaning towards scale_non_uniform()
. The thinking being that trait scales something, but the scaling doesn't happen uniformly in all directions.
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.
One could argue that it would be scale_mutable
then 😉
I'm totally fine with scale_non_uniform
, lets name it that.
|
||
assert_eq!(actual, expected); | ||
|
||
let base = Vector::new(2.0, 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.
What about moving this into its own test (e.g. scale_vector_around_base
).
|
||
let actual = original.scaled_nu(factor_x, factor_y, Vector::zero()); | ||
// known value | ||
let expected = Vector::new(-2.0, 12.5); |
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.
How about Vector::new(-1.0 * factor_x, 5.0 * factor_y)
? That way you can drop the comment and it's obvious where the numbers came from.
|
||
#[test] | ||
fn line() { | ||
let start = Vector::new(2.0, 4.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.
(Same comments as the vector
test)
} | ||
} | ||
|
||
impl ScaleNonUniform for Vector { |
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.
Are there any other types that could implement ScaleNonUniform
and Scale
? BoundingBox
would be a good candidate.
It almost feels like we could use a more general AffineTransformable
trait which accepts a kurbo::Affine
to transform itself with. That way we could automatically implement Scale
, ScaleNonUniform
, Translate
, and Rotate
for any T: AffineTransformable
... Thoughts?
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.
BoundingBox
could also implement Translate
then.
Using a AffineTransformable
trait we could avoid writing all those seperate files.
So should we still use a layer of abstraction, e.g. a Transformation
struct wich has methods for returning Affine
instances?
We still would need to think about how to handle Transformations which are possibly destructive e.g. shear
and non-uniform
.
Maybe implementing a Primitive
enum and a return an instance of it from the transform function when using destuctive transforms.
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.
So should we still use a layer of abstraction, e.g. a Transformation struct wich has methods for returning Affine instances?
Isn't that just the various constructor functions on kurbo::Affine
?
What is your definition of destructive? My thoughts are that if you can apply an affine transform and the end product is still the same type of shape, it's okay.
So Line
could implement AffineTransformation
because stretching and scaling a Line
still gives you a Line
, but if you shear a BoundingBox
it turns into a rhombus and is no longer an axis-aligned bounding box, so it wouldn't make sense for it to implement AffineTransformable
and you'd need to implement Scale
, ScaleNonUniform
, and Translate
manually. Same with Arc
.
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.
Ok that makes a lot of sense, so we define one base trait AffineTransformable
with all the transforms auto-implemented and if primitives need other transforms they get it implemented manually.
First draft for AffineTransformable
:
pub trait AffineTransformable {
fn transform(&mut self, affine: Affine, base: Vector);
fn transformed(&self, affine: Affine, base: Vector) -> Self
where
Self: Sized + Clone,
{
let mut clone = self.clone();
clone.transform(affine, base);
clone
}
fn translate(&mut self, displacement: Vector)
where
Self: Sized,
{
let affine = Affine::translate(displacement);
self.transform(affine, Vector::zero());
}
fn translated(&mut self, displacement: Vector) -> Self
where
Self: Sized + Clone,
{
let mut clone = self.clone();
clone.translate(displacement);
clone
}
// TODO: implement other transforms, e.g. scale, scale-non-uniform, etc...
}
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 was thinking more along the lines of this.
My thoughts are as follows:
- Make each transformation its own trait, so each type only implements the things that make sense for them (i.e.
translate()
doesn't belong in theAffineTransformable
trait) - Keep each trait as small as possible, preferably with just one core method
- Where some transformation can be implemented exactly using a more general transformation (
Scale
is just aScaleNonUniform
where both factors are the same,Translate
is just aAffineTransformable
with anAffine::translate()
) add a generic impl so you don't need to type out code twice (i.e.impl <S: ScaleNonUniform> Scale for S { ... }
) - Methods like
translated()
andscaled()
which take an immutable reference and return a mutated copy are purely for convenience, so it's okay for them to have awhere Self: ...
clause and default implementation
... Thoughts?
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 will have time to really look at this starting tomorrow.
My first impressions:
- Thank you for the detailed write-up
- Keeping traits (interfaces) small sounds logical and good
I didn't know this pattern of writing traits and it looks like a real good foundation for the transformation system here
This should be all good now, if you still see things let me know. After this gets merged i would like to change the implementation of |
(*self).scale_nu(factor_x, factor_y, base); | ||
impl<A: AffineTransformable> ScaleNonUniform for A { | ||
fn scale_non_uniform(&mut self, factor_x: f64, factor_y: f64, base: Vector){ | ||
// TODO: Change to `Affine::scale_non_uniform()` after crates.io update |
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.
After a kurbo
version update we can change this to Affine::scale_non_uniform(factor_x, factor_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.
Other than my question about base
I'm happy to merge this.
use kurbo::Affine; | ||
|
||
pub trait AffineTransformable { | ||
fn transform(&mut self, affine: Affine, base: Vector); |
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 actually need the base
parameter here? I would have thought if you need an operation to be relative to a particular point you'd incorporate that into your transform matrix.
Something else to keep in mind is this function will be the simplest building block for most geometric operations so it'll be really, really hot. If we push the translate_to_base * affine * translate_back
dance up to the caller it means they can reuse the same Affine
for multiple calls to transform()
and there's less code to get in the way of inlining or auto-vectorisation.
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.
We don't need the base
parameter, it's just convenient.
Removing it for better performance is something I can get behind, although I would -at some point- implement some convenience wrappers, as in my experience, composing low-level transforms can be quite error-prone for users.
But de-coupling this from the most basic implementation seems right to me.
I ran all builds again with stable and nightly to double check but it all just works on my machine ©. |
I implemented non-uniform-scale mostly by copying over the structure of
scale
.To implement the trait for
primitives::Arc
we would need either anElipticalArc
orNursbArc
primitive. Similar to how a circle would turn into an ellipse with two distinct focii when scaled non-uniform.Also, the transformation traits are all returning
Self
right now, which might have to change for transforms likenon-uniform
orshear
for example.