use emath::{Pos2, Rangef, Rect, TSTransform};
use crate::{Color32, Shape, Stroke, StrokeKind};
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct BandPoint {
pub x: f32,
pub y: Rangef,
}
impl BandPoint {
#[inline]
pub fn new(x: f32, y: impl Into<Rangef>) -> Self {
Self { x, y: y.into() }
}
#[inline]
pub fn from_pos_and_width(pos: Pos2, width: f32) -> Self {
let half_width = 0.5 * width;
Self::new(pos.x, (pos.y - half_width)..=(pos.y + half_width))
}
#[inline]
pub fn is_finite(self) -> bool {
self.x.is_finite() && self.y.min.is_finite() && self.y.max.is_finite()
}
pub(crate) fn is_valid(self) -> bool {
self.is_finite() && self.y.min <= self.y.max
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct BandShape {
pub points: Vec<BandPoint>,
pub fill: Color32,
pub stroke: Stroke,
pub stroke_kind: StrokeKind,
pub angle: f32,
}
impl BandShape {
#[inline]
pub fn new(
points: Vec<BandPoint>,
fill: impl Into<Color32>,
stroke: impl Into<Stroke>,
) -> Self {
Self {
points,
fill: fill.into(),
stroke: stroke.into(),
stroke_kind: StrokeKind::Middle,
angle: 0.0,
}
}
#[inline]
pub fn filled(points: Vec<BandPoint>, fill: impl Into<Color32>) -> Self {
Self::new(points, fill, Stroke::NONE)
}
#[inline]
pub fn stroke(points: Vec<BandPoint>, stroke: impl Into<Stroke>) -> Self {
Self::new(points, Color32::TRANSPARENT, stroke)
}
#[inline]
pub fn with_stroke_kind(mut self, stroke_kind: StrokeKind) -> Self {
self.stroke_kind = stroke_kind;
self
}
#[inline]
pub fn with_angle_and_pivot(mut self, angle: f32, pivot: Pos2) -> Self {
let pivot = pivot.to_vec2();
let translation =
emath::Rot2::from_angle(-angle) * pivot - emath::Rot2::from_angle(-self.angle) * pivot;
self.angle = angle;
for point in &mut self.points {
point.x += translation.x;
point.y.min += translation.y;
point.y.max += translation.y;
}
self
}
pub fn transform(&mut self, transform: TSTransform) {
let translation = emath::Rot2::from_angle(-self.angle) * transform.translation;
for point in &mut self.points {
point.x = transform.scaling * point.x + translation.x;
point.y.min = transform.scaling * point.y.min + translation.y;
point.y.max = transform.scaling * point.y.max + translation.y;
}
self.stroke.width *= transform.scaling;
}
pub fn visual_bounding_rect(&self) -> Rect {
if !self.angle.is_finite() || (self.fill == Color32::TRANSPARENT && self.stroke.is_empty())
{
return Rect::NOTHING;
}
let rect = self.local_bounding_rect();
if rect == Rect::NOTHING {
return Rect::NOTHING;
}
let stroke_width = match self.stroke_kind {
StrokeKind::Inside | StrokeKind::Outside => self.stroke.width,
StrokeKind::Middle => self.stroke.width / 2.0,
};
rect.expand(stroke_width)
.rotate_bb(emath::Rot2::from_angle(self.angle))
}
fn local_bounding_rect(&self) -> Rect {
let mut rect = Rect::NOTHING;
for &[left, right] in self.points.array_windows() {
if left.is_valid() && right.is_valid() && left.x < right.x {
rect.extend_with(Pos2::new(left.x, left.y.min));
rect.extend_with(Pos2::new(left.x, left.y.max));
rect.extend_with(Pos2::new(right.x, right.y.min));
rect.extend_with(Pos2::new(right.x, right.y.max));
}
}
rect
}
}
impl From<BandShape> for Shape {
#[inline(always)]
fn from(shape: BandShape) -> Self {
Self::Band(shape)
}
}
#[cfg(test)]
mod tests {
use emath::{TSTransform, pos2, vec2};
use super::*;
#[test]
fn band_point_is_finite() {
assert!(BandPoint::new(0.0, 1.0..=2.0).is_finite());
assert!(!BandPoint::new(f32::NAN, 1.0..=2.0).is_finite());
assert!(!BandPoint::new(0.0, f32::NEG_INFINITY..=2.0).is_finite());
assert!(!BandPoint::new(0.0, 1.0..=f32::INFINITY).is_finite());
}
#[test]
fn band_point_from_pos_and_width() {
assert_eq!(
BandPoint::from_pos_and_width(pos2(2.0, 3.0), 4.0),
BandPoint::new(2.0, 1.0..=5.0),
);
}
#[test]
fn transform_preserves_rotation() {
let mut band = BandShape::filled(
vec![
BandPoint::new(0.0, 1.0..=3.0),
BandPoint::new(2.0, 2.0..=4.0),
],
Color32::WHITE,
)
.with_angle_and_pivot(core::f32::consts::FRAC_PI_2, Pos2::ZERO);
band.transform(TSTransform::from_translation(vec2(2.0, 3.0)));
assert!((band.points[0].x - 3.0).abs() < 1e-6);
assert!((band.points[0].y.min + 1.0).abs() < 1e-6);
assert!((band.points[0].y.max - 1.0).abs() < 1e-6);
}
#[test]
fn with_angle_and_pivot_preserves_pivot() {
let band = BandShape::filled(
vec![
BandPoint::new(1.0, 0.0..=2.0),
BandPoint::new(3.0, 0.0..=2.0),
],
Color32::WHITE,
)
.with_angle_and_pivot(core::f32::consts::FRAC_PI_2, pos2(2.0, 1.0));
assert!((band.points[0].x - 0.0).abs() < 1e-6);
assert!((band.points[0].y.min + 3.0).abs() < 1e-6);
assert!((band.points[0].y.max + 1.0).abs() < 1e-6);
}
#[test]
fn with_angle_and_pivot_is_relative_to_the_previous_angle() {
let points = vec![BandPoint::new(1.0, 0.0..=2.0)];
let pivot = pos2(2.0, 1.0);
let once = BandShape::filled(points.clone(), Color32::WHITE)
.with_angle_and_pivot(core::f32::consts::FRAC_PI_2, pivot);
let twice = BandShape::filled(points, Color32::WHITE)
.with_angle_and_pivot(core::f32::consts::FRAC_PI_4, pivot)
.with_angle_and_pivot(core::f32::consts::FRAC_PI_2, pivot);
assert!((once.points[0].x - twice.points[0].x).abs() < 1e-6);
assert!((once.points[0].y.min - twice.points[0].y.min).abs() < 1e-6);
assert!((once.points[0].y.max - twice.points[0].y.max).abs() < 1e-6);
}
#[test]
fn transform_before_with_angle_and_pivot_rotates_around_translated_origin() {
let mut band = BandShape::filled(vec![BandPoint::new(1.0, 0.0..=2.0)], Color32::WHITE);
let center = pos2(10.0, 20.0);
band.transform(TSTransform::from_translation(center.to_vec2()));
let band = band.with_angle_and_pivot(core::f32::consts::FRAC_PI_2, center);
let rotation = emath::Rot2::from_angle(band.angle);
let min = rotation * vec2(band.points[0].x, band.points[0].y.min);
let max = rotation * vec2(band.points[0].x, band.points[0].y.max);
assert!((min.x - 10.0).abs() < 1e-6);
assert!((min.y - 21.0).abs() < 1e-6);
assert!((max.x - 8.0).abs() < 1e-6);
assert!((max.y - 21.0).abs() < 1e-6);
}
}