Fix: Svg export of arcs with negative angle were wrong
In `flower.rs`, several arc commands are drawn with negative angles (or
negative extents), for example:
- Right leaves: [`circle_right(RIGHT_LEAF_TOP_RADIUS,
-RIGHT_LEAF_TOP_EXTENT,
75)`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/flower.rs#L94)
- Petals: [`circle_right(PETALS_SPACE_RADIUS, -PETALS_SPACE_EXTENT,
30)`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/flower.rs#L116)
- Petal finish: [`circle_left(PETALS_SIDE_RADIUS + 1.0, 3.0 -
PETALS_SIDE_EXTENT,
87)`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/flower.rs#L124)
In SVG path arc syntax (`A rx ry x-axis-rotation large-arc-flag
sweep-flag x y`):
- `sweep-flag`: `1` specifies clockwise motion along the circle; `0`
specifies counter-clockwise motion.
- `large-arc-flag`: `1` specifies an arc sweep $> 180^\circ$; `0`
specifies $\le 180^\circ$.
In
[`turtle-lib/src/export_svg.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/export_svg.rs):
1. **Sweep direction**: `sweep` was mapped purely based on
[`CircleDirection`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/circle_geometry.rs#L43)
(`Right => 1`, `Left => 0`), without taking the sign of `angle` into
account. Traveling a negative angle reverses the angular motion along
the circle (e.g. `Right` with a negative angle moves
counter-clockwise, needing `sweep = 0`). Because `sweep` was not
flipped for negative angles, SVG drew the arc on the opposite side of
the circle/chord, resulting in arcs bulging in the wrong direction
across the leaves and flower petals.
2. **Large arc flag**: `large_arc` was computed using `angle.value() >
180.0` rather than checking absolute magnitude `angle.value().abs() >
180.0`.
3. **Full circle check**: Full circle check compared `angle.value() -
360.0` instead of `angle.value().abs() - 360.0`.
In
[`turtle-lib/src/export_svg.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/export_svg.rs#L116-L135):
- Determined `sweep` from both `direction` and `angle.value() >= 0.0`:
```rust
let sweep = match (direction, angle.value() >= 0.0) {
(CircleDirection::Right, true) | (CircleDirection::Left, false) => 1,
(CircleDirection::Left, true) | (CircleDirection::Right, false) => 0,
};
```
- Evaluated `large_arc` and full-circle checks using
`angle.value().abs()`.
- Added a unit test
[`test_svg_export_arc_negative_angle_sweep`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/export_svg.rs#L302-L340)
to verify SVG arc sweep flags for negative angles.
This commit is contained in:
@@ -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 <circle>
|
||||
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 <path A …>
|
||||
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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user