diff --git a/src/bin/topola-egui/painter.rs b/src/bin/topola-egui/painter.rs index e520f08..10d6da7 100644 --- a/src/bin/topola-egui/painter.rs +++ b/src/bin/topola-egui/painter.rs @@ -28,14 +28,9 @@ impl<'a> Painter<'a> { ), PrimitiveShape::Bend(bend) => { let circle = bend.circle(); - let delta_from = bend.from - circle.pos; - let delta_to = bend.to - circle.pos; - let angle_from = delta_from.y().atan2(delta_from.x()); - - let cross = math::cross_product(delta_from, delta_to); - let dot = math::dot_product(delta_from, delta_to); - let angle_step = cross.atan2(dot) / 100.0; + let angle_from = bend.start_angle(); + let angle_step = bend.spanned_angle() / 100.0; let mut points: Vec = vec![]; diff --git a/src/geometry/primitive.rs b/src/geometry/primitive.rs index 35479d7..a79d966 100644 --- a/src/geometry/primitive.rs +++ b/src/geometry/primitive.rs @@ -1,3 +1,5 @@ +use std::f64::consts::TAU; + use enum_dispatch::enum_dispatch; use geo::{point, polygon, Contains, EuclideanDistance, Intersects, Point, Polygon, Rotate}; use rstar::{RTreeObject, AABB}; @@ -243,6 +245,10 @@ pub struct BendShape { } impl BendShape { + pub fn radius(&self) -> f64 { + self.inner_circle.r + self.width / 2.0 + } + pub fn inner_circle(&self) -> Circle { self.inner_circle } @@ -250,7 +256,7 @@ impl BendShape { pub fn circle(&self) -> Circle { Circle { pos: self.inner_circle.pos, - r: self.inner_circle.r + self.width / 2.0, + r: self.radius(), } } @@ -268,21 +274,33 @@ impl BendShape { self.to - self.inner_circle.pos, ) } + + pub fn start_angle(&self) -> f64 { + let r = self.from - self.inner_circle.pos; + math::vector_angle(r) + } + + pub fn spanned_angle(&self) -> f64 { + let r1 = self.from - self.inner_circle.pos; + let r2 = self.to - self.inner_circle.pos; + + // bends always go counterclockwise from `from` to `to` + // (this is the usual convention, no adjustment needed) + let angle = math::angle_between(r1, r2); + + // atan2 returns values normalized into the range (-pi, pi] + // so for angles below 0 we add 1 winding to get a nonnegative angle + if angle < 0.0 { + angle + TAU + } else { + angle + } + } } impl MeasureLength for BendShape { fn length(&self) -> f64 { - // TODO: Not valid for inflated bends, as currently `from` and `to` of these don't lie on - // teir circles. - - // We obtain the angle from the law of cosines and multiply with radius to get the length. - let d = self.to.euclidean_distance(&self.from); - - if d > 0.0 { - (1.0 - d * d / (2.0 * d * d)).acos() - } else { - 0.0 - } + self.spanned_angle() * self.radius() } } diff --git a/src/math.rs b/src/math.rs index 8ef28d9..cd25e16 100644 --- a/src/math.rs +++ b/src/math.rs @@ -230,6 +230,20 @@ pub fn between_vectors(p: Point, from: Point, to: Point) -> bool { } } +/// Computes the (directed) angle between the positive X axis and the vector. +/// +/// The result is measured counterclockwise and normalized into range (-pi, pi] (like atan2). +pub fn vector_angle(vector: Point) -> f64 { + vector.y().atan2(vector.x()) +} + +/// Computes the (directed) angle between two vectors. +/// +/// The result is measured counterclockwise and normalized into range (-pi, pi] (like atan2). +pub fn angle_between(v1: Point, v2: Point) -> f64 { + cross_product(v1, v2).atan2(dot_product(v1, v2)) +} + pub fn seq_cross_product(start: Point, stop: Point, reference: Point) -> f64 { let dx1 = stop.x() - start.x(); let dy1 = stop.y() - start.y();