diff --git a/turtle-lib/src/export_svg.rs b/turtle-lib/src/export_svg.rs index 0c6a84b..2103d9b 100644 --- a/turtle-lib/src/export_svg.rs +++ b/turtle-lib/src/export_svg.rs @@ -83,6 +83,7 @@ pub mod svg_export { *direction, ); let center = geom.center; + let radius_val = radius.abs(); // Include the bounding box of the full circle so partial arcs // are never clipped. update_bounds( @@ -90,24 +91,24 @@ pub mod svg_export { &mut max_x, &mut min_y, &mut max_y, - center.x - radius, - center.y - radius, + center.x - radius_val, + center.y - radius_val, ); update_bounds( &mut min_x, &mut max_x, &mut min_y, &mut max_y, - center.x + radius, - center.y + radius, + center.x + radius_val, + center.y + radius_val, ); - if (angle.value() - 360.0).abs() < 1e-3 { + if (angle.value().abs() - 360.0).abs() < 1e-3 { // Full circle — emit as let circle = Circle::new() .set("cx", center.x) .set("cy", center.y) - .set("r", *radius) + .set("r", radius_val) .set("stroke", color_to_svg(*color)) .set("stroke-width", *pen_width) .set("fill", "none"); @@ -115,17 +116,19 @@ pub mod svg_export { } else { // Partial arc — emit as let end = geom.position_at_angle(angle.as_radians().value()); - let large_arc = i32::from(angle.value() > 180.0); - let sweep = match direction { - crate::circle_geometry::CircleDirection::Left => 0, - crate::circle_geometry::CircleDirection::Right => 1, + let large_arc = i32::from(angle.value().abs() > 180.0); + let sweep = match (direction, angle.value() >= 0.0) { + (crate::circle_geometry::CircleDirection::Right, true) + | (crate::circle_geometry::CircleDirection::Left, false) => 1, + (crate::circle_geometry::CircleDirection::Left, true) + | (crate::circle_geometry::CircleDirection::Right, false) => 0, }; let d = format!( "M {} {} A {} {} 0 {} {} {} {}", start_position.x, start_position.y, - radius, - radius, + radius_val, + radius_val, large_arc, sweep, end.x, @@ -295,5 +298,45 @@ pub mod svg_export { "SVG export of partial arcs should have round line caps: {svg_string}" ); } + + #[test] + fn test_svg_export_arc_negative_angle_sweep() { + let mut world = TurtleWorld::new(); + let mut turtle = Turtle::default(); + // Circle right with negative angle should sweep counter-clockwise (sweep = 0) + turtle.svg_log.push(SvgRecord::Arc { + start_position: Coordinate::new(0.0, 0.0), + start_heading: 0.0, + radius: 50.0, + angle: Degrees::new(-90.0), + direction: CircleDirection::Right, + color: Color::new(0.0, 0.0, 0.0, 1.0), + pen_width: 2.0, + }); + // Circle left with negative angle should sweep clockwise (sweep = 1) + turtle.svg_log.push(SvgRecord::Arc { + start_position: Coordinate::new(100.0, 100.0), + start_heading: 0.0, + radius: 50.0, + angle: Degrees::new(-90.0), + direction: CircleDirection::Left, + color: Color::new(0.0, 0.0, 0.0, 1.0), + pen_width: 2.0, + }); + world.turtles.push(turtle); + + let doc = SvgExporter::to_svg_document(&world); + let svg_string = doc.to_string(); + // Right with negative angle: large_arc=0, sweep=0 -> "0 0 0" + assert!( + svg_string.contains("A 50 50 0 0 0"), + "Circle right with negative angle should have sweep=0: {svg_string}" + ); + // Left with negative angle: large_arc=0, sweep=1 -> "0 0 1" + assert!( + svg_string.contains("A 50 50 0 0 1"), + "Circle left with negative angle should have sweep=1: {svg_string}" + ); + } } }