use crate::{
Point, Rect,
float::{all_finite, at_least, within},
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum StrokeCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum StrokeJoin {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Stroke {
pub width: f32,
pub cap: StrokeCap,
pub join: StrokeJoin,
}
impl Stroke {
pub const fn new(width: f32) -> Self {
Self {
width,
cap: StrokeCap::Butt,
join: StrokeJoin::Miter,
}
}
pub const fn with_width(mut self, width: f32) -> Self {
self.width = width;
self
}
pub const fn with_cap(mut self, cap: StrokeCap) -> Self {
self.cap = cap;
self
}
pub const fn with_join(mut self, join: StrokeJoin) -> Self {
self.join = join;
self
}
pub fn half_width(&self) -> f32 {
if self.width.is_finite() {
at_least(self.width * 0.5, 0.0)
} else {
0.0
}
}
pub fn is_visible(&self) -> bool {
self.width.to_bits().wrapping_sub(1) < f32::MAX.to_bits()
}
pub fn scaled(&self, scale: f32) -> Self {
Self {
width: self.width * scale,
..*self
}
}
}
impl Default for Stroke {
fn default() -> Self {
Self::new(1.0)
}
}
pub const TAU: f32 = std::f32::consts::PI * 2.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ArcGeometry {
pub center: Point,
pub inner_radius: f32,
pub outer_radius: f32,
pub start_angle: f32,
pub sweep_angle: f32,
pub cap: StrokeCap,
}
#[inline]
fn exact_floor(x: f32) -> f32 {
if x == 0.0 {
return x;
}
if x.abs() < 8_388_608.0 {
let truncated = x as i32 as f32;
truncated - ((x < truncated) as i32 as f32)
} else {
x
}
}
#[inline]
fn wrap_angle_tau(x: f32) -> f32 {
if (0.0..TAU).contains(&x) {
return x;
}
let wrapped = x - exact_floor(x * (1.0 / TAU)) * TAU;
if wrapped >= TAU {
wrapped - TAU
} else if wrapped < 0.0 {
0.0
} else {
wrapped
}
}
#[inline]
fn fast_sin_cos(angle: f32) -> (f32, f32) {
use std::f32::consts::{FRAC_PI_2, PI};
#[inline]
fn fold_sin(x: f32) -> f32 {
const B: f32 = 4.0 / PI;
const C: f32 = -4.0 / (PI * PI);
let y = B * x + C * x * x.abs();
0.225 * (y * y.abs() - y) + y
}
let x = wrap_angle_tau(angle);
let x = if x > PI { x - TAU } else { x };
let mut c = x + FRAC_PI_2;
if c > PI {
c -= TAU;
}
(fold_sin(x), fold_sin(c))
}
const FAST_TRIG_ERR: f32 = 1.3e-3;
impl ArcGeometry {
#[inline]
pub fn new(
center: Point,
inner_radius: f32,
outer_radius: f32,
start_angle: f32,
sweep_angle: f32,
cap: StrokeCap,
) -> Self {
if !all_finite([
center.x,
center.y,
inner_radius,
outer_radius,
start_angle,
sweep_angle,
]) {
return Self::DEGENERATE;
}
let outer = at_least(outer_radius, 0.0);
let inner = within(inner_radius, 0.0, outer);
Self::with_angles(center, inner, outer, start_angle, sweep_angle, cap)
}
#[inline]
pub(crate) fn of_band(
center: Point,
(inner, outer, cap): (f32, f32, StrokeCap),
start_angle: f32,
sweep_angle: f32,
) -> Self {
if !all_finite([center.x, center.y, inner, outer, start_angle, sweep_angle]) {
return Self::DEGENERATE;
}
Self::with_angles(center, inner, outer, start_angle, sweep_angle, cap)
}
#[inline(always)]
fn with_angles(
center: Point,
inner: f32,
outer: f32,
start_angle: f32,
sweep_angle: f32,
cap: StrokeCap,
) -> Self {
if sweep_angle.to_bits().wrapping_sub(1) < TAU.to_bits() - 1
&& start_angle.to_bits() < TAU.to_bits()
{
return Self {
center,
inner_radius: inner,
outer_radius: outer,
start_angle,
sweep_angle,
cap,
};
}
Self::normalizing_angles(center, inner, outer, start_angle, sweep_angle, cap)
}
#[inline]
fn normalizing_angles(
center: Point,
inner: f32,
outer: f32,
start_angle: f32,
sweep_angle: f32,
cap: StrokeCap,
) -> Self {
let (mut start, mut sweep) = if sweep_angle < 0.0 {
(start_angle + sweep_angle, -sweep_angle)
} else {
(start_angle, sweep_angle)
};
if sweep >= TAU {
sweep = TAU;
start = 0.0;
}
start = wrap_angle_tau(start);
if !start.is_finite() {
start = 0.0;
}
let cap = if sweep >= TAU { StrokeCap::Round } else { cap };
Self {
center,
inner_radius: inner,
outer_radius: outer,
start_angle: start,
sweep_angle: sweep,
cap,
}
}
const DEGENERATE: Self = Self {
center: Point::ZERO,
inner_radius: 0.0,
outer_radius: 0.0,
start_angle: 0.0,
sweep_angle: 0.0,
cap: StrokeCap::Butt,
};
pub fn mid_radius(&self) -> f32 {
(self.inner_radius + self.outer_radius) * 0.5
}
pub fn half_thickness(&self) -> f32 {
(self.outer_radius - self.inner_radius) * 0.5
}
pub fn is_degenerate(&self) -> bool {
!(self.outer_radius > 0.0
&& self.outer_radius > self.inner_radius
&& self.sweep_angle > 0.0)
}
pub fn contains_angle(&self, angle: f32) -> bool {
if self.sweep_angle >= TAU {
return true;
}
let delta = wrap_angle_tau(angle - self.start_angle);
delta <= self.sweep_angle + 1e-6
}
pub fn scaled_about(&self, center: Point, scale: f32) -> Self {
Self {
center,
inner_radius: self.inner_radius * scale,
outer_radius: self.outer_radius * scale,
..*self
}
}
pub fn bounds(&self) -> Rect {
if self.is_degenerate() {
return Rect {
x: self.center.x,
y: self.center.y,
width: 0.0,
height: 0.0,
};
}
if self.sweep_angle >= TAU && self.cap != StrokeCap::Square {
let r = self.outer_radius;
return Rect {
x: self.center.x - r,
y: self.center.y - r,
width: r + r,
height: r + r,
};
}
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
let mut include = |x: f32, y: f32| {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
};
let rb = self.half_thickness();
let ra = self.mid_radius();
let end_angle = self.start_angle + self.sweep_angle;
for (angle, outward) in [(self.start_angle, -1.0f32), (end_angle, 1.0f32)] {
let (sin, cos) = fast_sin_cos(angle);
match self.cap {
StrokeCap::Butt => {
include(
self.center.x + cos * self.inner_radius,
self.center.y + sin * self.inner_radius,
);
include(
self.center.x + cos * self.outer_radius,
self.center.y + sin * self.outer_radius,
);
}
StrokeCap::Square => {
let tx = -sin * rb * outward;
let ty = cos * rb * outward;
include(
self.center.x + cos * self.inner_radius + tx,
self.center.y + sin * self.inner_radius + ty,
);
include(
self.center.x + cos * self.outer_radius + tx,
self.center.y + sin * self.outer_radius + ty,
);
}
StrokeCap::Round => {
let cx = self.center.x + cos * ra;
let cy = self.center.y + sin * ra;
include(cx - rb, cy - rb);
include(cx + rb, cy + rb);
}
}
}
const AXIS_DIRECTIONS: [(f32, f32); 4] = [(0.0, 1.0), (1.0, 0.0), (0.0, -1.0), (-1.0, 0.0)];
for (quadrant, (sin, cos)) in AXIS_DIRECTIONS.into_iter().enumerate() {
let angle = quadrant as f32 * std::f32::consts::FRAC_PI_2;
if self.contains_angle(angle) {
include(
self.center.x + cos * self.outer_radius,
self.center.y + sin * self.outer_radius,
);
}
}
let pad = (self.outer_radius + rb) * FAST_TRIG_ERR + 0.02;
Rect {
x: min_x - pad,
y: min_y - pad,
width: (max_x - min_x + pad + pad).max(0.0),
height: (max_y - min_y + pad + pad).max(0.0),
}
}
}
#[inline]
pub fn arc_band(radius: f32, inner_radius: f32, stroke: Option<Stroke>) -> (f32, f32, StrokeCap) {
match stroke {
Some(stroke) => {
if !radius.is_finite() || !stroke.is_visible() {
return (0.0, 0.0, stroke.cap);
}
let half = stroke.half_width();
let radius = at_least(radius, 0.0);
(at_least(radius - half, 0.0), radius + half, stroke.cap)
}
None => {
if !radius.is_finite() || !inner_radius.is_finite() {
return (0.0, 0.0, StrokeCap::Butt);
}
let outer = at_least(radius, 0.0);
let inner = within(inner_radius, 0.0, outer);
(inner, outer, StrokeCap::Butt)
}
}
}
pub fn inflate_rect(rect: Rect, amount: f32) -> Rect {
if !amount.is_finite() || amount <= 0.0 {
return rect;
}
Rect {
x: rect.x - amount,
y: rect.y - amount,
width: (rect.width + amount * 2.0).max(0.0),
height: (rect.height + amount * 2.0).max(0.0),
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LineGeometry {
pub start: Point,
pub end: Point,
pub half_width: f32,
pub cap: StrokeCap,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LineFrame {
pub center: Point,
pub direction: Point,
pub half_length: f32,
}
impl LineGeometry {
pub fn new(start: Point, end: Point, stroke: Stroke) -> Self {
Self {
start,
end,
half_width: stroke.half_width(),
cap: stroke.cap,
}
}
pub fn is_degenerate(&self) -> bool {
!all_finite([self.start.x, self.start.y, self.end.x, self.end.y])
|| !(self.half_width > 0.0 && self.half_width.is_finite())
|| (self.cap == StrokeCap::Butt && self.start == self.end)
}
pub fn frame(&self) -> LineFrame {
let (dx, dy) = (self.end.x - self.start.x, self.end.y - self.start.y);
let length = (dx * dx + dy * dy).sqrt();
let direction = if length > 0.0 {
Point::new(dx / length, dy / length)
} else {
Point::new(1.0, 0.0)
};
LineFrame {
center: Point::new(
(self.start.x + self.end.x) * 0.5,
(self.start.y + self.end.y) * 0.5,
),
direction,
half_length: length * 0.5,
}
}
pub fn cap_reach(&self) -> f32 {
if self.cap == StrokeCap::Butt {
0.0
} else {
self.half_width
}
}
pub fn end_bounds(&self) -> Rect {
let min_x = self.start.x.min(self.end.x);
let min_y = self.start.y.min(self.end.y);
Rect {
x: min_x,
y: min_y,
width: self.start.x.max(self.end.x) - min_x,
height: self.start.y.max(self.end.y) - min_y,
}
}
pub fn reach(&self) -> Point {
if self.cap == StrokeCap::Round {
return Point::new(self.half_width, self.half_width);
}
let frame = self.frame();
let (along_x, along_y) = (frame.direction.x.abs(), frame.direction.y.abs());
let cap = self.cap_reach();
Point::new(
along_x * cap + along_y * self.half_width,
along_y * cap + along_x * self.half_width,
)
}
pub fn bounds(&self) -> Rect {
let ends = self.end_bounds();
let reach = self.reach();
Rect {
x: ends.x - reach.x,
y: ends.y - reach.y,
width: ends.width + reach.x * 2.0,
height: ends.height + reach.y * 2.0,
}
}
pub fn placed(&self, start: Point, end: Point, scale: f32) -> Self {
Self {
start,
end,
half_width: self.half_width * scale,
..*self
}
}
pub fn coverage(&self, point: Point) -> f32 {
let frame = self.frame();
let (dx, dy) = (point.x - frame.center.x, point.y - frame.center.y);
let along = (dx * frame.direction.x + dy * frame.direction.y).abs();
let across = (dx * frame.direction.y - dy * frame.direction.x).abs();
let across_coverage = (self.half_width + 0.5 - across).clamp(0.0, 1.0);
if self.cap == StrokeCap::Round {
if along <= frame.half_length {
return across_coverage;
}
let (past, side) = (along - frame.half_length, across);
let distance = (past * past + side * side).sqrt() - self.half_width;
return (0.5 - distance).clamp(0.0, 1.0);
}
let reach = frame.half_length + self.cap_reach();
across_coverage * (reach + 0.5 - along).clamp(0.0, 1.0)
}
}
#[cfg(test)]
#[path = "tests/stroke_tests.rs"]
mod tests;