-
Notifications
You must be signed in to change notification settings - Fork 71
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 triangle shape #350
add triangle shape #350
Conversation
Should we add an |
src/triangle.rs
Outdated
Point::new( | ||
(self.a.x + self.b.x + self.c.x) / 3.0, | ||
(self.a.y + self.b.y + self.c.y) / 3.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.
One of those situations where we could do this or:
((self.a.to_vec2() + self.b.to_vec2() + self.c.to_vec2()) / 3.0).to_point()
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 do prefer the latter. Also, it would be more efficient to multiply by (1.0 / 3.0)
but this is a matter of preference. Leaving the division in preserves the case where the resulting coordinate is, say, an integer, while multiplication might not.
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.
Its less likely to be an integer so ill multiply by 1/3
i realise that changes to other source files like vec2.rs should be another pr but right no this pr depends on them so ill keep it here for no. if this doesn't get merged then ill split my fork and make a pr just for those other source files. |
src/triangle.rs
Outdated
/// Updates [`Triangle`]'s vertice positions | ||
/// such that [`Triangle::a`] is topmost, [`Triangle::b`] is leftmost, and [`Triangle::c`] is rightmost | ||
#[inline] | ||
pub fn organise(self) -> 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.
should British spelling be changed? (probably)
src/triangle.rs
Outdated
pub fn organise(self) -> Self { | ||
Self::new(self.topmost(), self.leftmost(), self.rightmost()) | ||
let t = Self::new(self.topmost(), self.leftmost(), self.rightmost()); | ||
|
||
t | ||
} |
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.
(ignoring the bug which will be fixed, im just trying to find a clean way) there are a few issues / design choices that can be made here:
- should organise() mutate
self
? - a lot of other functions assume an organised position so should we either let the user call organised before these functions OR call organise in these functions
src/triangle.rs
Outdated
/// The perimeter of the [`Triangle`] | ||
#[inline] | ||
fn perimeter(&self) -> f64 { | ||
self.a.distance(self.b) + self.b.distance(self.c) + self.c.distance(self.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.
we have general perimeter
and area
functions but we also have specific ones for right angled triangles. should we add more specific ones for equilateral triangles and stuff or is it too much clutter?
also, i dont see the need to implement perimeter and area here when its in the Shape
trait? this is also done for Rect
.
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 should just be the Shape
trait impls. I think the reason it's in Rect is historical, those methods were written before the Shape
trait existed.
src/triangle.rs
Outdated
pub fn from_centroid_sizes(centroid: impl Into<Point>, sizes: [impl Into<Size>; 3]) -> Self { | ||
let centroid = centroid.into(); | ||
|
||
let mut points: [Point; 3] = Default::default(); | ||
for (i, size) in sizes.into_iter().enumerate() { | ||
points[i] = centroid + size.into().to_vec2(); | ||
} | ||
|
||
Self::new(points[0], points[1], points[2]) | ||
} | ||
|
||
/// A new [`Triangle`] with vertex distances of `distances` from the `centroid` | ||
/// | ||
/// NOTE: the new [`Triangle`] does not keep `centroid` | ||
#[inline] | ||
pub fn from_centroid_distances(centroid: impl Into<Point>, distances: [f64; 3]) -> Self { | ||
let centroid = centroid.into(); | ||
|
||
Self::new( | ||
centroid + (0.0, distances[0]), | ||
centroid + distances[1] * Vec2::from_angle(5.0 * FRAC_PI_4), | ||
centroid + distances[2] * Vec2::from_angle(7.0 * FRAC_PI_4), | ||
) | ||
} |
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 a transition from from*
to with*
like in the builder pattern is better. although im hesitant to do this here because this would require a change for the entire codebase so maybe should be another pr?
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 also wouldnt have to include the centroid
param this way since users could chain with with_centroid()
src/triangle.rs
Outdated
pub fn organise(self) -> Self { | ||
let mut points = self.as_array(); | ||
points.sort_by(|x, y| x.rightmost(y)); | ||
|
||
let a = self.topmost(); | ||
let b = *points.iter().find(|&&p| p != a).unwrap(); | ||
let c = *points.iter().rev().find(|&&p| p != a).unwrap(); | ||
Self::new(a, b, c) | ||
} |
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 probably can do a better way, we cant just do this though because of double ups.
and with 8b03193 this pr should be ready for a review! |
on a side note i also think it would be useful to have |
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 PR, and apologies for taking so long to review. Triangle is a beautiful shape with useful special properties, and this PR is a good start. Some general thoughts.
A very large number of items are marked as "not justified." Basically, we want methods that are used by potentially a broad range of applications. If it's only going to be used once, or for a test, it doesn't belong in the public API. It's very easy to add methods, but unfortunately extremely difficult to remove them. In addition, a theme in kurbo is mathematical correctness, which means that some methods which might be convenient (like Point + Point
or, recently Affine * Vec2
) aren't present. So most methods that aren't present for other shapes are out. That said, when there's something really special to triangles, it makes sense. I like centroid, and I'm also open to incircle (and circumcircle). For other methods, I'm open to an argument why it should be included.
There should be an impl Mul<Triangle> for Affine
. Fortunately, it's simple, you can pretty much cut'n'paste the one for line.
Looking forward to the revisions, then I'll do another review.
src/size.rs
Outdated
@@ -336,6 +336,16 @@ impl SubAssign<Size> for Size { | |||
} | |||
} | |||
|
|||
impl From<f64> for 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 not mathematically correct.
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 i only did that for testing in one of the commits but forgot to remove it sorry
src/triangle.rs
Outdated
/// empty [`Triangle`] at (1.0, 1.0) | ||
pub const ONE: Self = Self::from_coords((1.0, 1.0), (1.0, 1.0), (1.0, 1.0)); | ||
|
||
/// equilateral [`Triangle`] identity |
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 not in fact an equilateral triangle. And I don't think it's justified.
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.
haha sorry about that
i think it is useful for the user though (atleast one out of Triangle::Equilateral
and Triangle::ONE
) because the user can just scale them and work with scalars instead of triangle data.
Not sure if that makes sense but i can defiantly see this as a use case however if you still want it removed then thats fine
also why is Triangle::ZERO
alright to keep?
src/triangle.rs
Outdated
/// The area of the [`Triangle`] | ||
#[inline] | ||
pub fn area(&self) -> f64 { | ||
let ab = self.a.distance(self.b); |
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 more efficient to compute this using the shoelace rule, no trig necessary. Also, it's usual in kurbo to compute signed area (with the same sign convention as converting into a bezpath).
This should work: 0.5 * (self.b - self.a).cross(self.c - self.a)
src/triangle.rs
Outdated
(self.b.distance(self.c) * (self.a.y - self.b.y)) / 2.0 | ||
} | ||
|
||
/// Updates [`Triangle`]'s vertice positions |
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 convinced this should be in the public API.
"vertex"
Also, the description doesn't clearly state what happens if, for example, a
is both topmost and rightmost.
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 convinced this should be in the public API.
yeah im not sure why i did that.
now that the functions that relied upon this method are gone should we still keep this?
src/triangle.rs
Outdated
most_ordering!(self, { |x, y| y.x.total_cmp(&x.x) }) | ||
} | ||
|
||
/// Maximum x-coordinate of the [`Triangle`]'s vertices |
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 individual functions should just be combined into the Shape::bounding_box
implementation.
src/triangle.rs
Outdated
Self::new(points[0], points[1], points[2]) | ||
} | ||
|
||
/// A new [`Triangle`] with vertex distances of `distances` from the `centroid` |
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 don't think this is justified.
src/triangle.rs
Outdated
self.as_array().iter().find(|&v| v.y == y).copied() | ||
} | ||
|
||
/// The topmost vertex of `self` as a [`Point`] |
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 don't think these are justified, I think it's fairly specialized and users can write it themselves. In addition, there's a naming problem, as "topmost" implies a y-down coordinate system, and one of the conceits of kurbo is that it's agnostic to whether the coordinates are y-up or y-down.
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.
Oh right, their useless now anyway because the organise
is useless
src/triangle.rs
Outdated
) | ||
} | ||
|
||
/// Returns the [`Ellipse`] that is bounded by this [`Triangle`]. |
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.
As mentioned elsewhere, I think incircle and circumcircle are the methods that should be included.
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.
ill update the tests once you check it out
src/size.rs
Outdated
@@ -336,6 +336,16 @@ impl SubAssign<Size> for Size { | |||
} | |||
} | |||
|
|||
impl From<f64> for 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.
i think i only did that for testing in one of the commits but forgot to remove it sorry
src/vec2.rs
Outdated
@@ -31,6 +31,9 @@ impl Vec2 { | |||
/// The vector (0, 0). | |||
pub const ZERO: Vec2 = Vec2::new(0., 0.); | |||
|
|||
/// The vector (1, 1) |
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 only used this in one of the prior commits so i can just remove it entirely
src/ellipse.rs
Outdated
/// | ||
/// [`with_rotation`]: Ellipse::with_rotation | ||
#[inline] | ||
pub fn from_triangle(triangle: Triangle) -> 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.
Yeah you're right
src/triangle.rs
Outdated
/// empty [`Triangle`] at (1.0, 1.0) | ||
pub const ONE: Self = Self::from_coords((1.0, 1.0), (1.0, 1.0), (1.0, 1.0)); | ||
|
||
/// equilateral [`Triangle`] identity |
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.
haha sorry about that
i think it is useful for the user though (atleast one out of Triangle::Equilateral
and Triangle::ONE
) because the user can just scale them and work with scalars instead of triangle data.
Not sure if that makes sense but i can defiantly see this as a use case however if you still want it removed then thats fine
also why is Triangle::ZERO
alright to keep?
src/triangle.rs
Outdated
) | ||
} | ||
|
||
/// The euclidean distance of each vertex from the centroid |
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.
Like alot of these, im not sure why its needed either haha.
src/triangle.rs
Outdated
(self.b.distance(self.c) * (self.a.y - self.b.y)) / 2.0 | ||
} | ||
|
||
/// Updates [`Triangle`]'s vertice positions |
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 convinced this should be in the public API.
yeah im not sure why i did that.
now that the functions that relied upon this method are gone should we still keep this?
src/triangle.rs
Outdated
self.as_array().iter().find(|&v| v.y == y).copied() | ||
} | ||
|
||
/// The topmost vertex of `self` as a [`Point`] |
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.
Oh right, their useless now anyway because the organise
is useless
src/triangle.rs
Outdated
area / s | ||
} | ||
|
||
/// Expand the triangle by a constant amount (`sizes`) 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.
Oops i forgot to update this. is 4d8f906 inflate
fine?
src/triangle.rs
Outdated
) | ||
} | ||
|
||
/// A new [`Triangle`] with each vertecie's ordinates rounded to the nearest integer |
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.
me neither, i just took alot of these helpers from rect
Ive double checked and it looks like my also can the |
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 looking pretty good, thanks for all the progress. And apologies again for the slow turnaround on review, I'm juggling too many things. I think not much more is needed to land it.
src/triangle.rs
Outdated
/// The area of the [`Triangle`] | ||
#[inline] | ||
pub fn area(&self) -> f64 { | ||
0.5 * (self.b - self.a).cross(self.c - self.a).abs() |
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 disagree that there should be an abs
here. We default to signed area in kurbo, and by taking the absolute value we'll have situations where the return value of this function doesn't match the area of converting into shape first.
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.
not exactly sure the use of this cause i dont know of any benefits for signed area but ill just do it if its the norm.
should the area test be updated to be negative now?
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, or permute the order of vertices. Converting to shape and evaluating area gives a negative result, so that's confirmation that the sign conventions are consistent. And in fact, you can make a good argument that this should be tested explicitly.
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.
Converting to shape and evaluating area gives a negative result, so that's confirmation that the sign conventions are consistent
but doesn't the shape implementation default to triangles pre-existing signed area function?
no problem though, ive changed to test the original and one with a vertex permutation
do you think there should be an |
Probably not needed. The main purpose of those methods is fast paths for renderers, and triangle is not (unlike circle, rect, etc) not a common specialized shape - for example, it's not in SVG or the Canvas API. |
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 like this PR and think it would make a good addition to kurbo
, but I think it needs some changes first. The biggest suggestion in my review is to add functions inscribed_circle
and circumscribed_circle
and remove the functions that calculate radii/centers for these circles.
Feel free to push back if you disagree with any of my suggestions, and we can have a discussion.
use crate::{Point, Triangle, Vec2}; | ||
|
||
fn assert_approx_eq(x: f64, y: f64) { | ||
assert!((x - y).abs() < 1e-7); |
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 quite a loose bound for f64
. I'd use something closer to machine epsilon e.g. 1e-13
. Out of scope for this PR, but we really need to standardize this (I like float_cmp
because it allows big epsilons as long as the expected and actually are <n representable numbers apart in f64).
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 copied the bound from "rect.rs" i understood that it was loose but knew that it was only a temporary solution.
i could change it to be more strict but even with 1e-13
, Triangle::area()
tests are already failing from precision. i still could arbitrarily restrict the bound or could just wait for it to be standardized as you said, which would you prefer?
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.
Just leave it as-is for now - what you have is 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.
src/triangle.rs
Outdated
|
||
/// The circumcenter of the [`Triangle`] | ||
#[inline] | ||
pub fn circumcenter(&self) -> Point { |
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 looked a bit through the compiler explorer for some small optimizations and cleanups but i fear that i am missing a faster algorithm in general. Also any thoughts on this (specifically the first paragraph)?
src/triangle.rs
Outdated
/// Circumradius of [`Triangle`] (the smallest radius of a circle such that it intercepts each vertex) | ||
/// with center [`Triangle::circumcenter`] | ||
#[inline] | ||
pub fn circumradius(&self) -> f64 { |
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.
Yeah i have no problem either way.
The only argument against is that we might be computing extra information...
I think its more likely that the user would want the center and the radius rather than just one of them
use crate::{Point, Triangle, Vec2}; | ||
|
||
fn assert_approx_eq(x: f64, y: f64) { | ||
assert!((x - y).abs() < 1e-7); |
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 copied the bound from "rect.rs" i understood that it was loose but knew that it was only a temporary solution.
i could change it to be more strict but even with 1e-13
, Triangle::area()
tests are already failing from precision. i still could arbitrarily restrict the bound or could just wait for it to be standardized as you said, which would you prefer?
tests are failing from #350 (comment) and #350 (comment) |
one with and without permutated vertices
`is_zero_area`, `is_nan`/`is_finite` docs, typo
wasnt supposed to be committed at all
Co-authored-by: Richard Dodd <[email protected]>
Co-authored-by: Daniel McNab <[email protected]>
private `from_coords` and instead of `*radius` use `*circle`
name and doc updates + public `from_coords`
Fwiw, you should be able to dismiss the review, if we think everything has been addressed. |
@juliapaci If you could fix the tests (maybe look at |
I will merge this in about 26-28 hours. |
oops. for when we change the bound
I think the changes requested have been addressed.
I think there are probably things that we could follow up on ... but let's get this landed and deal with those separately. |
Implements (at least starts to) a triangle shape, im not sure if this is useful enough to merge though.
Also while looking at the source i noticed that that places like here use
Rect
instead ofSelf
unlike other parts of the codebase. And some parts of the documentation uses "[`Type`]" and other parts just use "Type". Wasnt sure to make an issue or not but should there be a cleanup of the codebase?also there is alot of conversion between
Vec2
andPoint
right now. is there a better way to handle these situations where i want to use Vec2 methods on a Point but will end up converting right back to a Point afterwards?triangles have alot of opportunities for more specific methods so its worth noting that this can be heavily expanded upon which could come later. Aswell as this, i have made some decisions like using centroids as the origin of the triangle (https://web.archive.org/web/20131104015950/http://www.jimloy.com/geometry/centers.htm) which might not be the best? let me know.
ill continue working on this, specifically the TODOs (see inlined todos)