mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(svg): preserve distance paths for shapes with one zero axis
Use a separate equivalent path for distance queries on rectangles and ellipses with one zero axis. Preserve their starting points and sweep while retaining empty rendered bounds. Core regression tests verify total length, endpoint and intermediate point queries, and distinguish fully empty geometry from a shape collapsed along one axis.
This commit is contained in:
@@ -98,7 +98,7 @@ pub enum SvgGeometryElement {
|
||||
}
|
||||
|
||||
pub fn segments_for_element(element: SvgGeometryElement) -> Vec<SvgGeometrySegment> {
|
||||
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<BezPath> {
|
||||
// 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<BezPath> {
|
||||
match element {
|
||||
SvgGeometryElement::Circle { cx, cy, r } => Some(if *r > 0.0 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user