use crate::core::Point;
use crate::render::RenderContext;
#[inline]
pub fn point_on_circle(center: Point, radius: f32, angle: f32) -> Point {
Point::new(
center.x + (radius * angle.cos()).round() as i32,
center.y + (radius * angle.sin()).round() as i32,
)
}
pub fn draw_arc_segments(
context: &mut RenderContext,
center: Point,
radius: f32,
start_angle: f32,
end_angle: f32,
color: crate::core::Color,
stroke_width: u32,
) {
let total_angle = end_angle - start_angle;
if total_angle.abs() < 0.001 || radius <= 0.0 {
return;
}
let segments = arc_sample_count(radius, total_angle);
let step = total_angle / segments as f32;
let mut prev = point_on_circle(center, radius, start_angle);
for i in 1..=segments {
let angle = start_angle + step * i as f32;
let curr = point_on_circle(center, radius, angle);
if curr != prev {
context.draw_line_stroke(prev, curr, color, stroke_width);
prev = curr;
}
}
}
pub(crate) fn arc_sample_count(radius: f32, total_angle: f32) -> u32 {
const PIXEL_STRIDE: f32 = 1.0;
const MAX_ARC_SEGMENTS: u32 = 40;
let arc_length = radius * total_angle.abs();
(arc_length / PIXEL_STRIDE).ceil().max(1.0).min(MAX_ARC_SEGMENTS as f32) as u32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::Vec;
use crate::core::{Color, Font, Point, Size};
use crate::render::{PaintBackend, RenderCommand, ShapedText, SvgPaintBackend, TextMetrics};
struct LineRecorder {
lines: Vec<(Point, Point)>,
}
impl PaintBackend for LineRecorder {
fn begin_frame(&mut self, _clear: Color) {}
fn end_frame(&mut self) {}
fn execute_command(&mut self, command: &RenderCommand) {
if let RenderCommand::DrawLineStroke { from, to, .. } = command {
self.lines.push((*from, *to));
}
}
fn size(&self) -> Size {
Size::new(240, 120)
}
fn set_size(&mut self, _size: Size) {}
fn dpi_scale(&self) -> f32 {
1.0
}
fn set_dpi_scale(&mut self, _dpi_scale: f32) {}
fn measure_text(&self, text: &str, font: &Font) -> TextMetrics {
TextMetrics {
width: (text.chars().count() as f32 * font.size()).round() as u32,
height: font.size().round() as u32,
ascent: (font.size() * 0.8).round() as u32,
descent: 0,
}
}
fn shape_text(&self, _text: &str, _font: &Font) -> ShapedText {
ShapedText { clusters: Vec::new(), advance: 0.0 }
}
fn frame_rgba(&self) -> &[u8] {
&[]
}
}
fn recorded(radius: f32, sweep: f32) -> Vec<(Point, Point)> {
let mut backend = LineRecorder { lines: Vec::new() };
let mut context = RenderContext::new(&mut backend);
draw_arc_segments(
&mut context,
Point::new(120, 60),
radius,
-core::f32::consts::FRAC_PI_2,
-core::f32::consts::FRAC_PI_2 + sweep,
Color::PRIMARY,
4,
);
backend.lines
}
#[test]
fn an_arc_never_emits_a_zero_length_chord() {
for (radius, sweep) in
[(48.0f32, 2.4f32), (16.0, 2.4), (48.0, 0.5), (120.0, core::f32::consts::TAU)]
{
let lines = recorded(radius, sweep);
assert!(
!lines.is_empty(),
"radius {radius} sweep {sweep}: an arc with a visible length must emit chords"
);
for (from, to) in &lines {
assert_ne!(
from, to,
"radius {radius} sweep {sweep}: a chord with identical endpoints draws no ink"
);
}
}
}
#[test]
fn a_small_arc_still_traces_a_continuous_curve() {
let lines = recorded(48.0, 2.4);
for pair in lines.windows(2) {
assert_eq!(pair[0].1, pair[1].0, "consecutive chords must share an endpoint");
}
let expected_end =
point_on_circle(Point::new(120, 60), 48.0, -core::f32::consts::FRAC_PI_2 + 2.4);
assert_eq!(lines.last().expect("chords were emitted").1, expected_end);
}
#[test]
fn the_sample_count_follows_the_arcs_pixel_length() {
assert_eq!(arc_sample_count(6.0, 0.5), 3, "about 3 px of arc is a handful of samples");
assert_eq!(arc_sample_count(48.0, 2.4), 40, "the spinner's ~115 px arc is clamped");
assert_eq!(
arc_sample_count(200.0, core::f32::consts::TAU),
40,
"a huge arc is clamped to the ceiling"
);
assert_eq!(arc_sample_count(0.0, 2.4), 1);
assert_eq!(arc_sample_count(48.0, 0.0), 1);
}
#[test]
fn an_arc_that_rounds_away_is_skipped_rather_than_drawn_degenerate() {
assert!(recorded(48.0, 0.0).is_empty());
assert!(recorded(0.0, 2.4).is_empty());
}
#[test]
fn the_svg_backend_matches_the_raster_for_an_arc() {
let mut svg = SvgPaintBackend::new(crate::core::Size::new(240, 120));
svg.begin_frame(Color::WHITE);
{
let mut context = RenderContext::new(&mut svg);
draw_arc_segments(
&mut context,
Point::new(120, 60),
48.0,
-core::f32::consts::FRAC_PI_2,
-core::f32::consts::FRAC_PI_2 + 2.4,
Color::PRIMARY,
4,
);
}
svg.end_frame();
let document = svg.finish();
assert!(
document.matches("<line ").count() > 20,
"a 115 px arc must reach the SVG document as a real curve, not three pixels"
);
}
}