diff --git a/turtle-lib/src/circle_geometry.rs b/turtle-lib/src/circle_geometry.rs index 29f700b..c9d31a2 100644 --- a/turtle-lib/src/circle_geometry.rs +++ b/turtle-lib/src/circle_geometry.rs @@ -3,6 +3,41 @@ use crate::general::Radians; 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 { + 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) #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CircleDirection { diff --git a/turtle-lib/src/drawing.rs b/turtle-lib/src/drawing.rs index 4dd8c83..fbad8d2 100644 --- a/turtle-lib/src/drawing.rs +++ b/turtle-lib/src/drawing.rs @@ -176,41 +176,32 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { } = &tween.command { // Calculate partial arc vertices based on current progress - use crate::circle_geometry::CircleGeometry; + use crate::circle_geometry::{arc_points, CircleGeometry}; use crate::general::Radians; let geom = CircleGeometry::new( tween.start_params.position, Radians::new(tween.start_params.heading), *radius, *direction, - ); // Calculate progress + ); let elapsed = get_time() - tween.start_time; let progress = (elapsed / tween.duration).min(1.0); let eased_progress = CubicInOut.tween(1.0, progress as f32); - // Generate arc vertices for the partial arc - let num_samples = *steps.max(&1); + // Delegate to the shared arc_points function — same sampling + // strategy as tessellate_arc, eliminating the divergence. let samples_to_draw = - ((num_samples as f32 * eased_progress) as usize).max(1); - - for i in 1..=samples_to_draw { - let sample_progress = i as f32 / num_samples as f32; - let current_angle = match direction { - crate::circle_geometry::CircleDirection::Left => { - geom.start_angle_from_center - - angle.as_radians().value() * sample_progress - } - crate::circle_geometry::CircleDirection::Right => { - geom.start_angle_from_center - + 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); + (((*steps).max(1) as f32 * eased_progress) as usize).max(1); + let sweep_so_far = angle.as_radians().value() * eased_progress; + for pt in arc_points( + geom.center, + *radius, + geom.start_angle_from_center, + sweep_so_far, + samples_to_draw, + *direction, + ) { + current_preview.push(pt); } } else if matches!( &tween.command, diff --git a/turtle-lib/src/export_svg.rs b/turtle-lib/src/export_svg.rs index 2ab162c..e06d99d 100644 --- a/turtle-lib/src/export_svg.rs +++ b/turtle-lib/src/export_svg.rs @@ -69,14 +69,15 @@ pub mod svg_export { .. } => { use crate::circle_geometry::CircleGeometry; + use crate::general::Radians; let geom = CircleGeometry::new( source.start_position, - source.start_heading, + Radians::new(source.start_heading), *radius, *direction, ); let center = geom.center; - if (*angle - 360.0).abs() < 1e-3 { + if (angle.value() - 360.0).abs() < 1e-3 { // Voller Kreis update_bounds( &mut min_x, @@ -123,7 +124,7 @@ pub mod svg_export { center.x + 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 { crate::circle_geometry::CircleDirection::Left => 0, crate::circle_geometry::CircleDirection::Right => 1, diff --git a/turtle-lib/src/tessellation.rs b/turtle-lib/src/tessellation.rs index 3703a05..196acb4 100644 --- a/turtle-lib/src/tessellation.rs +++ b/turtle-lib/src/tessellation.rs @@ -309,40 +309,32 @@ pub(crate) fn tessellate_arc( segments: usize, direction: crate::circle_geometry::CircleDirection, ) -> Result> { - // Build arc path manually from segments - let mut builder = Path::builder(); + use crate::circle_geometry::arc_points; let start_angle = start_angle_degrees.to_radians(); - let arc_angle = arc_angle_degrees.to_radians(); - let step = arc_angle / segments as f32; + let sweep_angle = arc_angle_degrees.to_radians(); - // Calculate first point - let first_point = point( + let mut builder = Path::builder(); + + // Start point of the arc (arc_points returns everything *after* this) + builder.begin(point( center.x + radius * start_angle.cos(), center.y + radius * start_angle.sin(), - ); - builder.begin(first_point); + )); - // Add remaining points - direction matters! - for i in 1..=segments { - let angle = match direction { - crate::circle_geometry::CircleDirection::Left => { - // Counter-clockwise: subtract angle - start_angle - step * i as f32 - } - crate::circle_geometry::CircleDirection::Right => { - // Clockwise: add angle - start_angle + step * i as f32 - } - }; - let pt = point( - center.x + radius * angle.cos(), - center.y + radius * angle.sin(), - ); - builder.line_to(pt); + // Remaining points — single source of truth for arc sampling + for pt in arc_points( + center, + radius, + start_angle, + sweep_angle, + segments, + direction, + ) { + builder.line_to(point(pt.x, pt.y)); } - builder.end(false); // Don't close the arc + builder.end(false); // open arc, not a closed polygon let path = builder.build(); // Tessellate stroke