improve circle construction and rendering

This commit is contained in:
Franz Dietrich 2026-05-21 17:29:27 +02:00
parent 998cffdcbf
commit 96b02f61be
4 changed files with 72 additions and 53 deletions

View File

@ -3,6 +3,41 @@
use crate::general::Radians; use crate::general::Radians;
use macroquad::prelude::*; use macroquad::prelude::*;
/// Generate evenly-spaced points along a circular arc.
///
/// Returns exactly `steps` points, uniformly distributed from (not including)
/// the arc start to (including) the arc end. This is the **single source of
/// truth** for arc sampling used by tessellation, tween stroke drawing, and
/// fill-polygon preview.
///
/// # Arguments
/// * `center` — centre of the circle
/// * `radius` — arc radius
/// * `start_angle` — angle from `center` to the turtle's start position (radians)
/// * `sweep_angle` — total arc sweep in radians (absolute; sign comes from `direction`)
/// * `steps` — number of sample points (clamped to ≥ 1)
/// * `direction` — which way the arc curves
pub(crate) fn arc_points(
center: Vec2,
radius: f32,
start_angle: f32,
sweep_angle: f32,
steps: usize,
direction: CircleDirection,
) -> Vec<Vec2> {
let n = steps.max(1);
let step_size = sweep_angle / n as f32;
(1..=n)
.map(|i| {
let a = match direction {
CircleDirection::Left => start_angle - step_size * i as f32,
CircleDirection::Right => start_angle + step_size * i as f32,
};
Vec2::new(center.x + radius * a.cos(), center.y + radius * a.sin())
})
.collect()
}
/// Direction of circular motion (in screen coordinates with Y-down) /// Direction of circular motion (in screen coordinates with Y-down)
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircleDirection { pub enum CircleDirection {

View File

@ -176,41 +176,32 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
} = &tween.command } = &tween.command
{ {
// Calculate partial arc vertices based on current progress // Calculate partial arc vertices based on current progress
use crate::circle_geometry::CircleGeometry; use crate::circle_geometry::{arc_points, CircleGeometry};
use crate::general::Radians; use crate::general::Radians;
let geom = CircleGeometry::new( let geom = CircleGeometry::new(
tween.start_params.position, tween.start_params.position,
Radians::new(tween.start_params.heading), Radians::new(tween.start_params.heading),
*radius, *radius,
*direction, *direction,
); // Calculate progress );
let elapsed = get_time() - tween.start_time; let elapsed = get_time() - tween.start_time;
let progress = (elapsed / tween.duration).min(1.0); let progress = (elapsed / tween.duration).min(1.0);
let eased_progress = CubicInOut.tween(1.0, progress as f32); let eased_progress = CubicInOut.tween(1.0, progress as f32);
// Generate arc vertices for the partial arc // Delegate to the shared arc_points function — same sampling
let num_samples = *steps.max(&1); // strategy as tessellate_arc, eliminating the divergence.
let samples_to_draw = let samples_to_draw =
((num_samples as f32 * eased_progress) as usize).max(1); (((*steps).max(1) as f32 * eased_progress) as usize).max(1);
let sweep_so_far = angle.as_radians().value() * eased_progress;
for i in 1..=samples_to_draw { for pt in arc_points(
let sample_progress = i as f32 / num_samples as f32; geom.center,
let current_angle = match direction { *radius,
crate::circle_geometry::CircleDirection::Left => { geom.start_angle_from_center,
geom.start_angle_from_center sweep_so_far,
- angle.as_radians().value() * sample_progress samples_to_draw,
} *direction,
crate::circle_geometry::CircleDirection::Right => { ) {
geom.start_angle_from_center current_preview.push(pt);
+ angle.as_radians().value() * sample_progress
}
};
let vertex = Vec2::new(
geom.center.x + radius * current_angle.cos(),
geom.center.y + radius * current_angle.sin(),
);
current_preview.push(vertex);
} }
} else if matches!( } else if matches!(
&tween.command, &tween.command,

View File

@ -69,14 +69,15 @@ pub mod svg_export {
.. ..
} => { } => {
use crate::circle_geometry::CircleGeometry; use crate::circle_geometry::CircleGeometry;
use crate::general::Radians;
let geom = CircleGeometry::new( let geom = CircleGeometry::new(
source.start_position, source.start_position,
source.start_heading, Radians::new(source.start_heading),
*radius, *radius,
*direction, *direction,
); );
let center = geom.center; let center = geom.center;
if (*angle - 360.0).abs() < 1e-3 { if (angle.value() - 360.0).abs() < 1e-3 {
// Voller Kreis // Voller Kreis
update_bounds( update_bounds(
&mut min_x, &mut min_x,
@ -123,7 +124,7 @@ pub mod svg_export {
center.x + radius, center.x + radius,
center.y + radius, center.y + radius,
); );
let large_arc = if *angle > 180.0 { 1 } else { 0 }; let large_arc = if angle.value() > 180.0 { 1 } else { 0 };
let sweep = match direction { let sweep = match direction {
crate::circle_geometry::CircleDirection::Left => 0, crate::circle_geometry::CircleDirection::Left => 0,
crate::circle_geometry::CircleDirection::Right => 1, crate::circle_geometry::CircleDirection::Right => 1,

View File

@ -309,40 +309,32 @@ pub(crate) fn tessellate_arc(
segments: usize, segments: usize,
direction: crate::circle_geometry::CircleDirection, direction: crate::circle_geometry::CircleDirection,
) -> Result<MeshData, Box<dyn std::error::Error>> { ) -> Result<MeshData, Box<dyn std::error::Error>> {
// Build arc path manually from segments use crate::circle_geometry::arc_points;
let mut builder = Path::builder();
let start_angle = start_angle_degrees.to_radians(); let start_angle = start_angle_degrees.to_radians();
let arc_angle = arc_angle_degrees.to_radians(); let sweep_angle = arc_angle_degrees.to_radians();
let step = arc_angle / segments as f32;
// Calculate first point let mut builder = Path::builder();
let first_point = point(
// Start point of the arc (arc_points returns everything *after* this)
builder.begin(point(
center.x + radius * start_angle.cos(), center.x + radius * start_angle.cos(),
center.y + radius * start_angle.sin(), center.y + radius * start_angle.sin(),
); ));
builder.begin(first_point);
// Add remaining points - direction matters! // Remaining points — single source of truth for arc sampling
for i in 1..=segments { for pt in arc_points(
let angle = match direction { center,
crate::circle_geometry::CircleDirection::Left => { radius,
// Counter-clockwise: subtract angle start_angle,
start_angle - step * i as f32 sweep_angle,
} segments,
crate::circle_geometry::CircleDirection::Right => { direction,
// Clockwise: add angle ) {
start_angle + step * i as f32 builder.line_to(point(pt.x, pt.y));
}
};
let pt = point(
center.x + radius * angle.cos(),
center.y + radius * angle.sin(),
);
builder.line_to(pt);
} }
builder.end(false); // Don't close the arc builder.end(false); // open arc, not a closed polygon
let path = builder.build(); let path = builder.build();
// Tessellate stroke // Tessellate stroke