-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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](https://github.com/linebender/kurbo/blob/main/src/rect.rs#L43) 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) --------- Co-authored-by: Richard Dodd <richard.o.dodd@gmail.com> Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
- Loading branch information
1 parent
40acae8
commit 5a4b2d9
Showing
2 changed files
with
344 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,342 @@ | ||
// Copyright 2024 the Kurbo Authors | ||
// SPDX-License-Identifier: Apache-2.0 OR MIT | ||
|
||
//! Triangle shape | ||
use crate::{Circle, PathEl, Point, Rect, Shape, Vec2}; | ||
|
||
use core::cmp::*; | ||
use core::f64::consts::FRAC_PI_4; | ||
use core::ops::{Add, Sub}; | ||
|
||
#[cfg(not(feature = "std"))] | ||
use crate::common::FloatFuncs; | ||
|
||
/// Triangle | ||
// A | ||
// * | ||
// / \ | ||
// / \ | ||
// *-----* | ||
// B C | ||
#[derive(Clone, Copy, PartialEq, Debug)] | ||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] | ||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | ||
pub struct Triangle { | ||
/// vertex a. | ||
pub a: Point, | ||
/// vertex b. | ||
pub b: Point, | ||
/// vertex c. | ||
pub c: Point, | ||
} | ||
|
||
impl Triangle { | ||
/// The empty [`Triangle`] at the origin. | ||
pub const ZERO: Self = Self::from_coords((0., 0.), (0., 0.), (0., 0.)); | ||
|
||
/// Equilateral [`Triangle`] with the x-axis unit vector as its base. | ||
pub const EQUILATERAL: Self = Self::from_coords( | ||
( | ||
1.0 / 2.0, | ||
1.732050807568877293527446341505872367_f64 / 2.0, /* (sqrt 3)/2 */ | ||
), | ||
(0.0, 0.0), | ||
(1.0, 0.0), | ||
); | ||
|
||
/// A new [`Triangle`] from three vertices ([`Point`]s). | ||
#[inline] | ||
pub fn new(a: impl Into<Point>, b: impl Into<Point>, c: impl Into<Point>) -> Self { | ||
Self { | ||
a: a.into(), | ||
b: b.into(), | ||
c: c.into(), | ||
} | ||
} | ||
|
||
/// A new [`Triangle`] from three float vertex coordinates. | ||
/// | ||
/// Works as a constant [`Triangle::new`]. | ||
#[inline] | ||
pub const fn from_coords(a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> Self { | ||
Self { | ||
a: Point::new(a.0, a.1), | ||
b: Point::new(b.0, b.1), | ||
c: Point::new(c.0, c.1), | ||
} | ||
} | ||
|
||
/// The centroid of the [`Triangle`]. | ||
#[inline] | ||
pub fn centroid(&self) -> Point { | ||
(1.0 / 3.0 * (self.a.to_vec2() + self.b.to_vec2() + self.c.to_vec2())).to_point() | ||
} | ||
|
||
/// The circumcenter of the [`Triangle`]. | ||
#[inline] | ||
fn circumcenter(&self) -> Point { | ||
let d = 2.0 | ||
* (self.a.x * (self.b.y - self.c.y) | ||
+ self.b.x * (self.c.y - self.a.y) | ||
+ self.c.x * (self.a.y - self.b.y)); | ||
|
||
let ux = ((self.a.x.powi(2) + self.a.y.powi(2)) * (self.b.y - self.c.y) | ||
+ (self.b.x.powi(2) + self.b.y.powi(2)) * (self.c.y - self.a.y) | ||
+ (self.c.x.powi(2) + self.c.y.powi(2)) * (self.a.y - self.b.y)) | ||
/ d; | ||
|
||
let uy = ((self.a.x.powi(2) + self.a.y.powi(2)) * (self.c.x - self.b.x) | ||
+ (self.b.x.powi(2) + self.b.y.powi(2)) * (self.a.x - self.c.x) | ||
+ (self.c.x.powi(2) + self.c.y.powi(2)) * (self.b.x - self.a.x)) | ||
/ d; | ||
|
||
Point::new(ux, uy) | ||
} | ||
|
||
/// The offset of each vertex from the centroid. | ||
#[inline] | ||
pub fn offsets(&self) -> [Vec2; 3] { | ||
let centroid = self.centroid().to_vec2(); | ||
|
||
[ | ||
(self.a.to_vec2() - centroid), | ||
(self.b.to_vec2() - centroid), | ||
(self.c.to_vec2() - centroid), | ||
] | ||
} | ||
|
||
/// The area of the [`Triangle`]. | ||
#[inline] | ||
pub fn area(&self) -> f64 { | ||
0.5 * (self.b - self.a).cross(self.c - self.a) | ||
} | ||
|
||
/// Whether this [`Triangle`] has zero area. | ||
#[doc(alias = "is_empty")] | ||
#[inline] | ||
pub fn is_zero_area(&self) -> bool { | ||
self.area() == 0.0 | ||
} | ||
|
||
/// The inscribed circle of [`Triangle`]. | ||
/// | ||
/// This is defined as the greatest [`Circle`] that lies within the [`Triangle`]. | ||
#[doc(alias = "incircle")] | ||
#[inline] | ||
pub fn inscribed_circle(&self) -> Circle { | ||
let ab = self.a.distance(self.b); | ||
let bc = self.b.distance(self.c); | ||
let ac = self.a.distance(self.c); | ||
|
||
Circle::new(self.circumcenter(), 2.0 * self.area() / (ab + bc + ac)) | ||
} | ||
|
||
/// The circumscribed circle of [`Triangle`]. | ||
/// | ||
/// This is defined as the smallest [`Circle`] which intercepts each vertex of the [`Triangle`]. | ||
#[doc(alias = "circumcircle")] | ||
#[inline] | ||
pub fn circumscribed_circle(&self) -> Circle { | ||
let ab = self.a.distance(self.b); | ||
let bc = self.b.distance(self.c); | ||
let ac = self.a.distance(self.c); | ||
|
||
Circle::new(self.circumcenter(), (ab * bc * ac) / (4.0 * self.area())) | ||
} | ||
|
||
/// Expand the triangle by a constant amount (`scalar`) in all directions. | ||
#[doc(alias = "offset")] | ||
pub fn inflate(&self, scalar: f64) -> Self { | ||
let centroid = self.centroid(); | ||
|
||
Self::new( | ||
centroid + (0.0, scalar), | ||
centroid + scalar * Vec2::from_angle(5.0 * FRAC_PI_4), | ||
centroid + scalar * Vec2::from_angle(7.0 * FRAC_PI_4), | ||
) | ||
} | ||
|
||
/// Is this [`Triangle`] [finite]? | ||
/// | ||
/// [finite]: f64::is_finite | ||
#[inline] | ||
pub fn is_finite(&self) -> bool { | ||
self.a.is_finite() && self.b.is_finite() && self.c.is_finite() | ||
} | ||
|
||
/// Is this [`Triangle`] [NaN]? | ||
/// | ||
/// [NaN]: f64::is_nan | ||
#[inline] | ||
pub fn is_nan(&self) -> bool { | ||
self.a.is_nan() || self.b.is_nan() || self.c.is_nan() | ||
} | ||
} | ||
|
||
impl From<(Point, Point, Point)> for Triangle { | ||
fn from(points: (Point, Point, Point)) -> Triangle { | ||
Triangle::new(points.0, points.1, points.2) | ||
} | ||
} | ||
|
||
impl Add<Vec2> for Triangle { | ||
type Output = Triangle; | ||
|
||
#[inline] | ||
fn add(self, v: Vec2) -> Triangle { | ||
Triangle::new(self.a + v, self.b + v, self.c + v) | ||
} | ||
} | ||
|
||
impl Sub<Vec2> for Triangle { | ||
type Output = Triangle; | ||
|
||
#[inline] | ||
fn sub(self, v: Vec2) -> Triangle { | ||
Triangle::new(self.a - v, self.b - v, self.c - v) | ||
} | ||
} | ||
|
||
#[doc(hidden)] | ||
pub struct TrianglePathIter { | ||
triangle: Triangle, | ||
ix: usize, | ||
} | ||
|
||
impl Shape for Triangle { | ||
type PathElementsIter<'iter> = TrianglePathIter; | ||
|
||
fn path_elements(&self, _tolerance: f64) -> TrianglePathIter { | ||
TrianglePathIter { | ||
triangle: *self, | ||
ix: 0, | ||
} | ||
} | ||
|
||
#[inline] | ||
fn area(&self) -> f64 { | ||
Triangle::area(self) | ||
} | ||
|
||
#[inline] | ||
fn perimeter(&self, _accuracy: f64) -> f64 { | ||
self.a.distance(self.b) + self.b.distance(self.c) + self.c.distance(self.a) | ||
} | ||
|
||
#[inline] | ||
fn winding(&self, pt: Point) -> i32 { | ||
let s0 = (self.b - self.a).cross(pt - self.a).signum(); | ||
let s1 = (self.c - self.b).cross(pt - self.b).signum(); | ||
let s2 = (self.a - self.c).cross(pt - self.c).signum(); | ||
|
||
if s0 == s1 && s1 == s2 { | ||
s0 as i32 | ||
} else { | ||
0 | ||
} | ||
} | ||
|
||
#[inline] | ||
fn bounding_box(&self) -> Rect { | ||
Rect::new( | ||
self.a.x.min(self.b.x.min(self.c.x)), | ||
self.a.y.min(self.b.y.min(self.c.y)), | ||
self.a.x.max(self.b.x.max(self.c.x)), | ||
self.a.y.max(self.b.y.max(self.c.y)), | ||
) | ||
} | ||
} | ||
|
||
// Note: vertices a, b and c are not guaranteed to be in order as described in the struct comments | ||
// (i.e. as "vertex a is topmost, vertex b is leftmost, and vertex c is rightmost") | ||
impl Iterator for TrianglePathIter { | ||
type Item = PathEl; | ||
|
||
fn next(&mut self) -> Option<PathEl> { | ||
self.ix += 1; | ||
match self.ix { | ||
1 => Some(PathEl::MoveTo(self.triangle.a)), | ||
2 => Some(PathEl::LineTo(self.triangle.b)), | ||
3 => Some(PathEl::LineTo(self.triangle.c)), | ||
4 => Some(PathEl::ClosePath), | ||
_ => None, | ||
} | ||
} | ||
} | ||
|
||
// TODO: better and more tests | ||
#[cfg(test)] | ||
mod tests { | ||
use crate::{Point, Triangle, Vec2}; | ||
|
||
fn assert_approx_eq(x: f64, y: f64) { | ||
assert!((x - y).abs() < 1e-7); | ||
} | ||
|
||
fn assert_approx_eq_point(x: Point, y: Point) { | ||
assert_approx_eq(x.x, y.x); | ||
assert_approx_eq(x.y, x.y); | ||
} | ||
|
||
#[test] | ||
fn centroid() { | ||
let test = Triangle::from_coords((-90.02, 3.5), (7.2, -9.3), (8.0, 9.1)).centroid(); | ||
let expected = Point::new(-24.94, 1.1); | ||
|
||
assert_approx_eq_point(test, expected); | ||
} | ||
|
||
#[test] | ||
fn offsets() { | ||
let test = Triangle::from_coords((-20.0, 180.2), (1.2, 0.0), (290.0, 100.0)).offsets(); | ||
let expected = [ | ||
Vec2::new(-110.4, 86.8), | ||
Vec2::new(-89.2, -93.4), | ||
Vec2::new(199.6, 6.6), | ||
]; | ||
|
||
test.iter() | ||
.zip(expected.iter()) | ||
.for_each(|(t, e)| assert_approx_eq_point(t.to_point(), e.to_point())); | ||
} | ||
|
||
#[test] | ||
fn area() { | ||
let test = Triangle::new( | ||
(12123.423, 2382.7834), | ||
(7892.729, 238.459), | ||
(7820.2, 712.23), | ||
); | ||
let expected = 1079952.91574081; | ||
|
||
// initial | ||
assert_approx_eq(test.area(), -expected); | ||
// permutate vertex | ||
let test = Triangle::new(test.b, test.a, test.c); | ||
assert_approx_eq(test.area(), expected); | ||
} | ||
|
||
#[test] | ||
fn circumcenter() { | ||
let test = Triangle::EQUILATERAL.circumcenter(); | ||
let expected = Point::new(0.5, 0.2886751345948128); | ||
|
||
assert_approx_eq_point(test, expected); | ||
} | ||
|
||
#[test] | ||
fn inradius() { | ||
let test = Triangle::EQUILATERAL.inscribed_circle().radius; | ||
let expected = 0.28867513459481287; | ||
|
||
assert_approx_eq(test, expected); | ||
} | ||
|
||
#[test] | ||
fn circumradius() { | ||
let test = Triangle::EQUILATERAL.circumscribed_circle().radius; | ||
let expected = 0.5773502691896258; | ||
|
||
assert_approx_eq(test, expected); | ||
} | ||
} |