diff --git a/moli-svg/src/geometry.rs b/moli-svg/src/geometry.rs index 8aa1c949ea..83fe8c23d4 100644 --- a/moli-svg/src/geometry.rs +++ b/moli-svg/src/geometry.rs @@ -98,7 +98,7 @@ pub enum SvgGeometryElement { } pub fn segments_for_element(element: SvgGeometryElement) -> Vec { - let path = geometry_path(&element).unwrap_or_default(); + let path = distance_query_path(&element).unwrap_or_default(); let segments = path .segments() .map(SvgGeometrySegment::new) @@ -229,6 +229,36 @@ pub fn point_at_length(segments: &[SvgGeometrySegment], distance: f64) -> SvgGeo .unwrap_or(SvgGeometryPoint::new(0.0, 0.0)) } +fn distance_query_path(element: &SvgGeometryElement) -> Option { + // A zero axis disables rendering, but distance queries can still compute + // the equivalent path along the other axis. + match element { + SvgGeometryElement::Rect { + x, + y, + width, + height, + .. + } if (*width == 0.0 && *height > 0.0) || (*height == 0.0 && *width > 0.0) => { + Some(Rect::new(*x, *y, *x + *width, *y + *height).to_path(SHAPE_PATH_TOLERANCE)) + } + SvgGeometryElement::Ellipse { cx, cy, rx, ry } + if (*rx == 0.0 && *ry > 0.0) || (*ry == 0.0 && *rx > 0.0) => + { + // Collapsed quarter arcs are lines. Preserve SVG's starting point + // and sweep instead of normalizing the ellipse's axes. + let mut path = BezPath::new(); + path.move_to((*cx + *rx, *cy)); + path.line_to((*cx, *cy + *ry)); + path.line_to((*cx - *rx, *cy)); + path.line_to((*cx, *cy - *ry)); + path.close_path(); + Some(path) + } + _ => geometry_path(element), + } +} + fn geometry_path(element: &SvgGeometryElement) -> Option { match element { SvgGeometryElement::Circle { cx, cy, r } => Some(if *r > 0.0 { diff --git a/moli-svg/src/lib.rs b/moli-svg/src/lib.rs index bf6853b8f2..6ab8b2f2d7 100644 --- a/moli-svg/src/lib.rs +++ b/moli-svg/src/lib.rs @@ -74,6 +74,121 @@ mod tests { }) } + #[test] + fn degenerate_shapes_keep_distance_paths_without_rendered_bounds() { + for (element, length, endpoint) in [ + ( + SvgGeometryElement::Rect { + x: 7.0, + y: 9.0, + width: 0.0, + height: 10.0, + rx: 0.0, + ry: 0.0, + }, + 20.0, + SvgGeometryPoint::new(7.0, 9.0), + ), + ( + SvgGeometryElement::Rect { + x: 7.0, + y: 9.0, + width: 10.0, + height: 0.0, + rx: 0.0, + ry: 0.0, + }, + 20.0, + SvgGeometryPoint::new(7.0, 9.0), + ), + ( + SvgGeometryElement::Ellipse { + cx: 7.0, + cy: 9.0, + rx: 0.0, + ry: 10.0, + }, + 40.0, + SvgGeometryPoint::new(7.0, 9.0), + ), + ( + SvgGeometryElement::Ellipse { + cx: 7.0, + cy: 9.0, + rx: 10.0, + ry: 0.0, + }, + 40.0, + SvgGeometryPoint::new(17.0, 9.0), + ), + ] { + assert!(bounding_box_for_element(&element).is_none()); + let segments = segments_for_element(element); + assert_near( + segments.iter().map(SvgGeometrySegment::length).sum(), + length, + 1e-6, + ); + let point = point_at_length(&segments, 300.0); + assert_close(point.x, endpoint.x); + assert_close(point.y, endpoint.y); + } + for element in [ + SvgGeometryElement::Rect { + x: 7.0, + y: 9.0, + width: 0.0, + height: 0.0, + rx: 0.0, + ry: 0.0, + }, + SvgGeometryElement::Ellipse { + cx: 7.0, + cy: 9.0, + rx: 0.0, + ry: 0.0, + }, + SvgGeometryElement::Circle { + cx: 7.0, + cy: 9.0, + r: 0.0, + }, + SvgGeometryElement::Path { d: String::new() }, + ] { + assert!(segments_for_element(element).is_empty()); + } + for (rx, ry, points) in [ + ( + 0.0, + 10.0, + [(7.0, 9.0), (7.0, 19.0), (7.0, 9.0), (7.0, -1.0), (7.0, 9.0)], + ), + ( + 10.0, + 0.0, + [ + (17.0, 9.0), + (7.0, 9.0), + (-3.0, 9.0), + (7.0, 9.0), + (17.0, 9.0), + ], + ), + ] { + let segments = segments_for_element(SvgGeometryElement::Ellipse { + cx: 7.0, + cy: 9.0, + rx, + ry, + }); + for (distance, (x, y)) in [0.0, 10.0, 20.0, 30.0, 40.0].into_iter().zip(points) { + let point = point_at_length(&segments, distance); + assert_close(point.x, x); + assert_close(point.y, y); + } + } + } + fn assert_close(actual: f64, expected: f64) { assert!( (actual - expected).abs() < 1e-9,