Skip to content
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

Merged
merged 27 commits into from
Oct 6, 2024
Merged

add triangle shape #350

merged 27 commits into from
Oct 6, 2024

Conversation

juliapaci
Copy link
Contributor

@juliapaci juliapaci commented May 29, 2024

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 of Self 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 and Point 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)

@juliapaci
Copy link
Contributor Author

Should we add an Insets for triangle?

src/triangle.rs Outdated
Comment on lines 107 to 110
Point::new(
(self.a.x + self.b.x + self.c.x) / 3.0,
(self.a.y + self.b.y + self.c.y) / 3.0,
)
Copy link
Contributor Author

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()

Copy link
Contributor

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.

Copy link
Contributor Author

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

@juliapaci
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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/point.rs Outdated Show resolved Hide resolved
src/triangle.rs Outdated
Comment on lines 203 to 207
pub fn organise(self) -> Self {
Self::new(self.topmost(), self.leftmost(), self.rightmost())
let t = Self::new(self.topmost(), self.leftmost(), self.rightmost());

t
}
Copy link
Contributor Author

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
Comment on lines 169 to 174
/// 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)
}

Copy link
Contributor Author

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.

Copy link
Contributor

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
Comment on lines 77 to 100
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),
)
}
Copy link
Contributor Author

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?

Copy link
Contributor Author

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
Comment on lines 214 to 222
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)
}
Copy link
Contributor Author

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.

@juliapaci
Copy link
Contributor Author

and with 8b03193 this pr should be ready for a review!

@juliapaci juliapaci marked this pull request as ready for review June 1, 2024 22:49
@juliapaci juliapaci changed the title add triangle shape (WIP) add triangle shape Jun 1, 2024
@juliapaci
Copy link
Contributor Author

on a side note i also think it would be useful to have from/to_triangle/circle/rect(), cause we do it for Ellipse already we may aswell do it for all other shapes which would also be super easy to implement. prob should be another pr tho

@waywardmonkeys waywardmonkeys added this to the August, 2024 Release milestone Aug 6, 2024
Copy link
Contributor

@raphlinus raphlinus left a 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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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`
Copy link
Contributor

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`]
Copy link
Contributor

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.

Copy link
Contributor Author

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`].
Copy link
Contributor

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.

src/triangle.rs Show resolved Hide resolved
src/point.rs Outdated Show resolved Hide resolved
Copy link
Contributor Author

@juliapaci juliapaci left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4d8f906

ill update the tests once you check it out

src/point.rs Outdated Show resolved Hide resolved
src/size.rs Outdated
@@ -336,6 +336,16 @@ impl SubAssign<Size> for Size {
}
}

impl From<f64> for Size {
Copy link
Contributor Author

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)
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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`]
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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

@juliapaci
Copy link
Contributor Author

Ive double checked and it looks like my incircle and circumcircle implementations are correct yet when i check them in desmos.com the incircle radius is too small and the circumcircle radius is too large. Have i done something wrong?

also can the circumcenter function be more efficient?

Copy link
Contributor

@raphlinus raphlinus left a 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 Show resolved Hide resolved
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()
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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

src/triangle.rs Outdated Show resolved Hide resolved
src/triangle.rs Show resolved Hide resolved
src/triangle.rs Outdated Show resolved Hide resolved
src/triangle.rs Outdated Show resolved Hide resolved
src/triangle.rs Outdated Show resolved Hide resolved
src/triangle.rs Show resolved Hide resolved
@juliapaci
Copy link
Contributor Author

do you think there should be an as_triangle in "shape.rs"?

@raphlinus
Copy link
Contributor

do you think there should be an as_triangle in "shape.rs"?

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.

src/triangle.rs Outdated Show resolved Hide resolved
src/triangle.rs Outdated Show resolved Hide resolved
src/triangle.rs Outdated Show resolved Hide resolved
@waywardmonkeys waywardmonkeys mentioned this pull request Aug 30, 2024
@xorgy xorgy modified the milestones: 0.11.1, 0.11.2 Sep 14, 2024
Copy link
Collaborator

@richard-uk1 richard-uk1 left a 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.

src/point.rs Outdated Show resolved Hide resolved
src/point.rs Outdated Show resolved Hide resolved
src/triangle.rs Outdated Show resolved Hide resolved
src/triangle.rs Outdated Show resolved Hide resolved
src/triangle.rs Show resolved Hide resolved
src/triangle.rs Outdated Show resolved Hide resolved
src/triangle.rs Show resolved Hide resolved
use crate::{Point, Triangle, Vec2};

fn assert_approx_eq(x: f64, y: f64) {
assert!((x - y).abs() < 1e-7);
Copy link
Collaborator

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).

Copy link
Contributor Author

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?

Copy link
Collaborator

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.

src/triangle.rs Outdated Show resolved Hide resolved
src/triangle.rs Outdated Show resolved Hide resolved
Copy link
Contributor Author

@juliapaci juliapaci left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review and sorry for the late response.

All suggestions have been implemented (4f17103, 3da32b7, 4d0afb9) except for those which i further commented on, please see those!

src/point.rs Outdated Show resolved Hide resolved
src/triangle.rs Show resolved Hide resolved
src/triangle.rs Outdated

/// The circumcenter of the [`Triangle`]
#[inline]
pub fn circumcenter(&self) -> Point {
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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

src/triangle.rs Show resolved Hide resolved
src/triangle.rs Outdated Show resolved Hide resolved
use crate::{Point, Triangle, Vec2};

fn assert_approx_eq(x: f64, y: f64) {
assert!((x - y).abs() < 1e-7);
Copy link
Contributor Author

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?

@juliapaci
Copy link
Contributor Author

tests are failing from #350 (comment) and #350 (comment)

juliapaci and others added 18 commits September 29, 2024 09:20
fix old tests + add new circle tests
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: Daniel McNab <[email protected]>
private `from_coords` and  instead of `*radius` use `*circle`
name and doc updates + public `from_coords`
remove `as_array` which hasnt been used since `organise`, add doc alias to `inflate`
@DJMcNab
Copy link
Member

DJMcNab commented Sep 29, 2024

Fwiw, you should be able to dismiss the review, if we think everything has been addressed.

@waywardmonkeys
Copy link
Contributor

@juliapaci If you could fix the tests (maybe look at assert_approx_eq), then I think we can land this. Hopefully @derekdreery can approve from his review comments being addressed.

@waywardmonkeys
Copy link
Contributor

I will merge this in about 26-28 hours.

oops. for when we change the bound
@waywardmonkeys waywardmonkeys dismissed richard-uk1’s stale review October 6, 2024 13:27

I think the changes requested have been addressed.

@waywardmonkeys
Copy link
Contributor

I think there are probably things that we could follow up on ... but let's get this landed and deal with those separately.

@waywardmonkeys waywardmonkeys added this pull request to the merge queue Oct 6, 2024
Merged via the queue into linebender:main with commit 5a4b2d9 Oct 6, 2024
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants